Ползём в сеньоры. Вопросы к собеседованию на сеньора Python.
2 subscribers
5 links
Подготовка какие вопросы могут задавать для сеньора по python и framework
Download Telegram
🚨 ТВОЙ DOCKER-ОБРАЗ ВЕСИТ 1.5 ГБ, А ДОЛЖЕН 150 МБ? СЕЙЧАС РАЗБЕРЁМСЯ, КАК ЭТО ИСПРАВИТЬ!

Представь: ты собираешь контейнер для простого Python-приложения, а он раздувается до гигантских размеров. Знакомо? Это классическая ошибка, из-за которой твой CI/CD пайплайн ползёт как улитка, а деплой превращается в пытку. Но самое страшное — каждый лишний слой в образе — это потенциальная дыра в безопасности. 😱

Сегодня разбираем многоступенчатую сборку Docker — твой спасательный круг на собеседовании и в проде.

ЧТО ТАКОЕ MULTI-STAGE BUILD?

Простыми словами: это когда в одном Dockerfile ты используешь несколько инструкций FROM. Каждая FROM — как отдельный этап сборки. На первом этапе ты ставишь всё тяжёлое: компиляторы, dev-зависимости, исходники. А на финальном — копируешь только то, что реально нужно для запуска приложения.

Представь, что ты готовишь блюдо. Сначала ты рубишь овощи на разделочной доске, потом жаришь на сковороде, а в конце красиво сервируешь тарелку. Доска и сковорода — это инструменты, которые не попадают на стол. Так и здесь: компилятор не попадает в финальный образ!

ПОЧЕМУ ЭТО КРИТИЧНО ДЛЯ PYTHON?

Python-проекты часто страдают от лишних зависимостей. Если ты ставишь всё через pip install без разбора, в образ попадают:

• dev-библиотеки (pytest, black, mypy) — они нужны только для разработки
• инструменты сборки (gcc, make) — нужны только чтобы скомпилировать некоторые пакеты
• кэш pip — который занимает десятки мегабайт

Итог: образ раздувается, а злоумышленник получает лишние инструменты для атаки. 😡

КАК ВЫГЛЯДИТ ПРАВИЛЬНЫЙ DOCKERFILE?

Вот тебе базовый шаблон для Python-приложения:


# Этап 1: сборка зависимостей
FROM python:3.12-slim AS builder

WORKDIR /app

# Копируем только файл с зависимостями
COPY requirements.txt .

# Ставим зависимости в отдельную папку
RUN pip install --prefix=/install -r requirements.txt

# Этап 2: финальный образ
FROM python:3.12-slim

WORKDIR /app

# Копируем установленные пакеты из первого этапа
COPY --from=builder /install /usr/local

# Копируем код приложения
COPY . .

# Запускаем от непривилегированного пользователя
RUN useradd --create-home appuser
USER appuser

CMD ["python", "app.py"]


Видишь магию? Мы используем AS builder для первого этапа, а во втором — COPY --from=builder. Всё, что не нужно для запуска, остаётся в первом этапе и выбрасывается после сборки. Образ худеет в разы!

БОНУС: БЕЗОПАСНОСТЬ

Заметил строку USER appuser? Это ещё один критичный момент. Если запускать контейнер от root, то при взломе приложения злоумышленник получает полный контроль над контейнером. А с непривилегированным пользователем — только ограниченные права. Мелочь, а спасает продакшен!

ЧАСТЫЕ ОШИБКИ НОВИЧКОВ

1. Забывают про .dockerignore — и в образ попадают .git, venv, кэши. Добавь его обязательно!

2. Используют apt-get install без очистки кэша. Добавь rm -rf /var/lib/apt/lists/* после установки.

3. Не объединяют RUN-команды. Каждый RUN создаёт слой, а лишние слои — лишний вес.

ИТОГ

Многоступенчатая сборка — это не просто фишка для красоты. Это способ сделать образы:

лёгкими (в 5-10 раз меньше)
безопасными (меньше инструментов для атаки)
быстрыми в деплое

На собеседовании можешь смело рассказывать про multi-stage build, а если ещё и про USER appuser вспомнишь — интервьюер точно оценит! 💪

А у тебя были случаи, когда образ раздувался до неприличия? Делись в комментариях! 👇
ПОЧЕМУ ТВОЯ БАЗА «ПОПЛЫЛА» ПОСЛЕ ДЕПЛОЯ, И ТЫ ДАЖЕ НЕ ЗАМЕТИЛ?

Скорее всего, ты просто забыл про миграции. Или, что ещё хуже, обновлял схему вручную через админку. А потом — бац! — и прод упал. Знакомая боль? Давай разберёмся, как работает Alembic и почему Django ORM тут вообще ни при чём.

ЧТО ТАКОЕ МИГРАЦИИ?

Миграции — это как система контроля версий для твоей базы данных. Помнишь Git? Там ты коммитишь изменения кода. Здесь ты «коммитишь» изменения схемы БД. Каждая миграция — это набор инструкций: «создать таблицу», «добавить колонку», «изменить тип». И всё это применяется последовательно, чтобы база всегда была в актуальном состоянии.

КАК РАБОТАЕТ ALEMBIC?

Alembic — это инструмент миграций для SQLAlchemy. Он работает в связке с твоими моделями. Вот ключевой момент: Alembic СРАВНИВАЕТ текущее состояние базы с тем, что описано в моделях, и генерирует миграцию, которая приводит базу к нужному виду.

Представь: ты добавил поле email в модель User. Запускаешь alembic revision --autogenerate — и Alembic сам смотрит, что в базе нет колонки email, и создаёт файл миграции с командой ALTER TABLE user ADD COLUMN email VARCHAR. Ты проверяешь этот файл, правишь, если нужно, и применяешь через alembic upgrade head.

Но есть нюанс: Alembic НЕ ВСЁ МОЖЕТ ПОНЯТЬ САМ. Например, переименование колонки он часто воспринимает как удаление старой и добавление новой. Поэтому ВСЕГДА проверяй сгенерированные миграции! Это правило номер один.

ЧЕМ ОТЛИЧАЕТСЯ DJANGO ORM?

Django ORM — это не просто библиотека для работы с БД, это целый фреймворк со своими правилами. И миграции там встроены прямо в ядро. Команда python manage.py makemigrations создаёт миграции, а migrate применяет. Всё автоматически, но есть принципиальное отличие:

Автогенерация в Django — более «умная» в плане переименований, потому что Django хранит историю миграций и может отслеживать изменения точнее.
Alembic — более гибкий, потому что работает с SQLAlchemy, а не привязан к конкретному фреймворку. Ты можешь использовать его с FastAPI, Flask, да хоть с голым Python.
Django — это «всё включено», но если ты выходишь за рамки стандартных моделей, начинаются танцы с бубном.

ПРАКТИЧЕСКИЙ ПРИМЕР

Допустим, у тебя есть модель User в FastAPI:


class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
name = Column(String)


Ты решил добавить поле age. Что делаешь?

1. Добавляешь поле в модель:

age = Column(Integer)


2. Запускаешь автогенерацию:

alembic revision --autogenerate -m "add age to users"


3. Проверяешь файл миграции. Видишь что-то типа:

op.add_column('users', sa.Column('age', sa.Integer(), nullable=True))


4. Применяешь:

alembic upgrade head


Всё, база обновлена. Но если бы ты забыл шаг 3 и сразу применил — могли бы быть проблемы. Например, Alembic мог бы решить, что нужно удалить таблицу и создать заново. А это потеря данных!

ИТОГ

Alembic — мощный инструмент, но он не волшебник. Он лишь помогает, а ответственность за схему лежит на тебе. Django ORM предлагает более «интегрированный» подход, но забирает гибкость. Выбирай по задаче, но помни: миграции — это твой страховочный трос на проде. Не пренебрегай ими!

А ты уже сталкивался с проблемами при миграциях? Расскажи в комментариях!
ТЕБЕ ГОВОРИЛИ, ЧТО SQLAlchemy САМ ВСЁ СОХРАНЯЕТ? А ПОТОМ ПРОД «ПАДАЕТ» ИЗ-ЗА ПОЛУСОХРАНЁННЫХ ДАННЫХ. 😱

Разберём паттерн Unit of Work — то, что спасает твою базу от хаоса. Это как официант, который запоминает весь заказ и приносит его разом, а не бегает по одному блюду. 🍽️

ЧТО ЭТО ТАКОЕ?

Unit of Work (UoW) — это паттерн, который отслеживает все изменения объектов в памяти и применяет их к базе данных ОДНИМ коммитом. Если что-то пошло не так — делает rollback, и база остаётся в целости. В SQLAlchemy этот паттерн реализован через сессию.

КАК ЭТО РАБОТАЕТ В SQLALCHEMY?

Сессия — твой главный инструмент. Она создаётся через фабрику sessionmaker, привязанную к engine (подключение к БД):

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

engine = create_engine('postgresql://user:pass@localhost/db')
Session = sessionmaker(bind=engine)
session = Session()


Сессия следит за объектами. Изменил атрибут — она пометила объект как «грязный». Добавил новый — пометила как «новый». И всё это копится, пока ты не вызовешь commit().

ЖИЗНЕННЫЙ ЦИКЛ ОБЪЕКТА

У объектов в сессии есть состояния:
Transient — создан, но не в сессии и не в БД.
Pending — добавлен через add(), но не сохранён.
Persistent — в сессии и в БД.
Detached — был в сессии, но она закрылась.

Вот как это выглядит на практике:

new_user = User(name='Alice')
# Transient
session.add(new_user)
# Pending
session.commit()
# Persistent
session.close()
# Detached


ПОЧЕМУ ЭТО ВАЖНО?

Представь: ты сохраняешь заказ с товарами. Без UoW ты бы писал каждый товар отдельно, и если бы один не сохранился — получил бы битые данные. А с UoW всё или ничего. Это гарантия атомарности.

КАК ПРАВИЛЬНО РАБОТАТЬ С СЕССИЕЙ?

Всегда используй try/except/finally:

try:
session.commit()
except:
session.rollback()
raise
finally:
session.close()


UOW + REPOSITORY = ИДЕАЛЬНАЯ ПАРА

Repository скрывает детали запросов, а UoW управляет транзакциями. Вместе они дают чистую архитектуру: бизнес-логика не знает, как устроена БД, а данные всегда консистентны.

ЧАСТЫЕ ОШИБКИ

• Забываешь закрыть сессию — утечка соединений.
• Делаешь commit после каждой мелочи — теряешь смысл UoW.
• Используешь одну сессию на всё приложение — рискуешь конфликтами.

ИТОГ

Unit of Work — это не просто паттерн, а твой щит от прод-инцидентов. Освой его — и будешь спать спокойно. 😉

#Python #SQLAlchemy #UnitOfWork #Собеседование
ТЕБЕ НЕ НУЖЕН ОГРОМНЫЙ if/elif ДЛЯ ПЛАТЕЖЕЙ! СЕРЬЕЗНО, ХВАТИТ ЭТО ТЕРПЕТЬ!

Представь: ты пишешь код для оплаты. Карта, PayPal, крипта... И вот твой метод превращается в монстра:


def pay(self, method, amount):
if method == "card":
# 50 строк логики
elif method == "paypal":
# ещё 50 строк
elif method == "crypto":
# и ещё 50


А теперь вопрос: что будет, когда добавится новый способ оплаты? Ты полезешь в этот монстр и допишешь ещё один elif. А потом ещё один. И вот уже твой класс на 500 строк, а баги плодятся как кролики. Знакомо?

ВОТ ТУТ И ВЫХОДИТ НА СЦЕНУ ПАТТЕРН STRATEGY!

По-простому, это как пульт от телевизора. У тебя есть кнопки (интерфейс), но что происходит при нажатии — зависит от того, какой телевизор подключен. Нажал на «громкость» — один телевизор сделал звук громче, другой — тише. Ты не меняешь пульт, ты меняешь поведение.

В Python это выглядит просто и элегантно. Смотри:


class PaymentStrategy:
def pay(self, amount):
pass

class CardPayment(PaymentStrategy):
def pay(self, amount):
print(f"Оплата картой: {amount}")

class PayPalPayment(PaymentStrategy):
def pay(self, amount):
print(f"Оплата PayPal: {amount}")


Каждый класс — это отдельная стратегия. У них одинаковый интерфейс (метод pay), но разная реализация. Теперь создаём контекст — класс, который будет использовать стратегию:


class PaymentContext:
def __init__(self, strategy):
self.strategy = strategy

def execute_payment(self, amount):
self.strategy.pay(amount)


И вот магия! Теперь выбор стратегии происходит на лету:


payment = PaymentContext(CardPayment())
payment.execute_payment(100)

payment.strategy = PayPalPayment() # меняем стратегию!
payment.execute_payment(200)


ВИДИШЬ? Мы просто подменяем объект strategy, и поведение меняется. Никаких if/elif! Никаких изменений в существующем коде!

НО ПОГОДИ, ЭТО ЖЕ ПРОСТО ПОЛИМОРФИЗМ? ДА, ВО МНОГОМ! Но паттерн Strategy — это не просто полиморфизм. Это ИДЕЯ о том, как организовать код, чтобы алгоритмы были взаимозаменяемыми и инкапсулированными.

ГЛАВНЫЕ ПЛЮСЫ, КОТОРЫЕ ТЫ НАЗОВЕШЬ НА СОБЕСЕДОВАНИИ:

• Инкапсуляция алгоритмов — каждый алгоритм живёт в своём классе, его легко тестировать отдельно.
• Отказ от условных операторов — код становится чище и читабельнее.
• Принцип открытости/закрытости (SOLID) — можно добавлять новые стратегии, не трогая существующий код.
• Взаимозаменяемость — стратегии можно менять на лету, во время выполнения программы.

А ТЕПЕРЬ ПОДВОДНЫЕ КАМНИ, О КОТОРЫХ ЧАСТО ВРУТ НА ИНТЕРВЬЮ:

1. «Стратегия — это то же самое, что и просто передача функции». В Python да, можно передать lambda. Но Strategy — это про целые семейства алгоритмов с общим интерфейсом и возможностью расширения. Если у тебя одна функция — не нужен паттерн.

2. «Этот паттерн решает все проблемы». НЕТ! Он добавляет классы. Если у тебя два варианта поведения — иногда проще оставить if. Не усложняй код ради паттерна.

3. «Стратегия и Декоратор — одно и то же». НЕТ! Декоратор оборачивает объект и добавляет поведение, а Стратегия — подменяет алгоритм целиком.

КАК ОТВЕТИТЬ НА СОБЕСЕДОВАНИИ, ЧТОБЫ ВСЕХ УДИВИТЬ?

Скажи так: «Strategy позволяет мне выделить семейство алгоритмов, инкапсулировать каждый из них и сделать их взаимозаменяемыми. Это даёт мне возможность менять поведение объекта на лету, не изменяя его код. В контексте платежей это идеально: я добавляю новый способ оплаты, просто создавая новый класс, реализующий интерфейс стратегии».

И добавь: «Но я всегда помню, что если алгоритмов всего два и они вряд ли изменятся — проще использовать if, чтобы не плодить сущности».

ВОТ ТАК! Ты теперь знаешь паттерн Strategy не как теорию из книжки, а как рабочий инструмент. Иди и покажи это на собеседовании!

А если хочешь больше таких разборов — ставь реакции и подписывайся, чтобы не пропустить следующую тему!
Твой Python-сервис жив? А ты УВЕРЕН? 😱

Вот ты деплоишь контейнер, все порты открыты, логи пишутся... Но твой сервис может быть МЕРТВ, а Docker об этом даже не узнает! Почему? Потому что контейнер запущен, но приложение внутри — упало или зависло. И вот тут на сцену выходит он — HEALTHCHECK! 🦸‍♂️

ПРЕДСТАВЬ: ты заказал пиццу. Курьер приехал, отдал коробку — и уехал. А пицца внутри — холодная и невкусная. Docker без healthcheck — это курьер, который привез коробку и не проверил, что внутри. А healthcheck — это курьер, который открывает коробку и проверяет: «Пицца горячая? Сыр тянется?» Если нет — бьет тревогу! 🚨

ЧТО ТАКОЕ HEALTHCHECK?
Это команда в Dockerfile, которая говорит Docker: «Периодически выполняй вот эту проверку. Если она провалится N раз подряд — контейнер болен, лечи или перезапускай!»

СИНТАКСИС ПРОСТОЙ:
HEALTHCHECK [OPTIONS] CMD команда_проверки


ГЛАВНЫЕ ПАРАМЕТРЫ:
--interval=30s — как часто проверяем (по умолчанию 30 сек)
--timeout=5s — сколько ждем ответа от проверки (по умолчанию 30 сек)
--retries=3 — сколько провалов подряд, чтобы признать контейнер больным (по умолчанию 3)
--start-period=10s — даем приложению время на запуск, проверки не начинаются (по умолчанию 0)

КАК НАСТРОИТЬ ДЛЯ PYTHON-СЕРВИСА?

Допустим, у тебя FastAPI или Flask. Самый простой и надежный способ — проверить, отвечает ли HTTP-эндпоинт. Например, специальный /health.

ВОТ ТВОЙ DOCKERFILE:
FROM python:3.12-slim

WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .

# Добавляем healthcheck!
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]


РАЗБИРАЕМ ПО КОСТОЧКАМ:
1. python -c "..." — выполняем Python-код прямо в команде
2. urllib.request.urlopen(...) — делаем GET-запрос к нашему эндпоинту
3. Если запрос упал (сервис не отвечает) — Python выбросит исключение, и команда завершится с ошибкой. А это значит — контейнер нездоров!
4. || exit 1 — на всякий случай явно выходим с кодом 1, чтобы Docker точно понял: все плохо

А ЧТО НА СЧЕТ /health В КОДЕ?

В FastAPI это одна строка:
@app.get("/health")
async def health():
return {"status": "ok"}


НО! Не делай проверку «просто верни 200». Проверяй то, что реально важно! Например, доступность базы данных:
@app.get("/health")
async def health():
try:
# Проверяем, что БД отвечает
await db.execute("SELECT 1")
return {"status": "ok"}
except Exception:
# Если БД лежит — сервис болен!
raise HTTPException(status_code=503)


ВОТ ЭТО УЖЕ ПО-ВЗРОСЛОМУ! 😎

КАК DOCKER РЕАГИРУЕТ?
У контейнера три состояния:
healthy — все ок
unhealthy — проверка провалена
starting — идет старт, проверки еще не начались (в новых версиях Docker)

ВАЖНЫЙ МОМЕНТ! Docker сам НЕ перезапускает больной контейнер! Он просто помечает его как unhealthy. Чтобы он перезапустился, нужен оркестратор (Kubernetes, Docker Swarm) или настройки в docker-compose.

В DOCKER-COMPOSE ЭТО ВЫГЛЯДИТ ТАК:
services:
app:
build: .
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
restart: unless-stopped


ВОТ ТЕПЕРЬ ТВОЙ СЕРВИС ПОД НАДЕЖНОЙ ЗАЩИТОЙ! 🛡️

ЗАПОМНИ ГЛАВНОЕ:
• Healthcheck — это не про «процесс запущен», а про «сервис реально работает»
• Проверяй зависимости (БД, кэш, внешние API)
• Не забывай про start_period, иначе получишь ложные срабатывания при старте

А теперь вопрос: у тебя в проде настроен healthcheck? Если нет — БЕГОМ ИСПРАВЛЯТЬ! 🏃‍♂️💨

#docker #python #devops #healthcheck #собеседование
ТВОЙ json.dumps() ЖРЁТ 80% ВРЕМЕНИ, А ТЫ ДАЖЕ НЕ ЗАМЕТИЛ?! 😱

Сеньор-разработчик смотрит на твой код и молча страдает. Почему? Потому что ты сериализуешь JSON стандартным модулем, когда есть orjson — молния на Rust, которая делает ту же работу в 5-10 раз быстрее. И сегодня я докажу, что это не маркетинг, а чистая математика!

ЧТО ТАКОЕ СЕРИАЛИЗАЦИЯ?

Это превращение Python-объекта (словарь, список) в строку JSON. Звучит просто, но под капотом CPython — это миллионы операций. Каждый ключ, каждая строка, каждое число проходят через виртуальную машину. А теперь представь, что ты обрабатываешь 10 000 документов в секунду. Вот тут и начинается боль.

ПОЧЕМУ ORJSON ТАКОЙ БЫСТРЫЙ?

orjson написан на Rust. Это язык, который компилируется в нативный машинный код БЕЗ интерпретатора. Пока CPython интерпретирует байткод, Rust просто выполняет инструкции процессора напрямую. Это как сравнивать курьера на велосипеде и на гоночном мотоцикле.

Но главный секрет — orjson НЕ создаёт промежуточные Python-объекты. Стандартный json сначала строит строку через кучу временных объектов, а orjson пишет результат сразу в буфер. Меньше аллокаций памяти — меньше работы для сборщика мусора.

КАК ЭТО ВЫГЛЯДИТ В КОДЕ?

Стандартный подход:
import json
data = {"user": "ivan", "age": 30}
result = json.dumps(data)


С orjson:
import orjson
result = orjson.dumps(data)


Всё! Одна строка замены. Но есть нюанс: orjson возвращает bytes, а не str. Для большинства API это даже лучше — меньше памяти и быстрее отправка по сети. Если нужна строка — просто добавь .decode().

ПОДВОДНЫЕ КАМНИ, О КОТОРЫХ ВСЕ ВРУТ

⚠️ orjson НЕ поддерживает Decimal. Если у тебя в данных есть Decimal('10.5') — получишь ошибку. Нужно конвертировать в float или строку.

⚠️ Сериализация datetime — orjson по умолчанию отдаёт timestamp, а не ISO-строку. Чтобы получить привычный формат, передай option=orjson.OPT_NAIVE_UTC или OPT_ORJSON.

⚠️ Словари с нестроковыми ключами — стандартный json их конвертирует, а orjson требует только строки. Это фича, а не баг: JSON-спецификация говорит, что ключи — всегда строки.

КОГДА ЭТО ДЕЙСТВИТЕЛЬНО ВАЖНО?

Представь: ты пишешь микросервис, который принимает 1000 запросов в секунду. Каждый запрос — это JSON на 10 КБ. С json ты тратишь на сериализацию 5 мс. С orjson — 0.5 мс. Разница в 4.5 мс на запрос. Умножь на 1000 запросов — это 4.5 секунды экономии каждую секунду работы!

И это не теория. В одном реальном проекте (статья на Хабре) профилировщик показал, что сериализация JSON съедала больше всего CPU. После замены на orjson скорость обработки выросла в 3 раза без единого изменения архитектуры!

ИТОГ: КОГДА ИСПОЛЬЗОВАТЬ?

Используй orjson если:
- У тебя высоконагруженный API
- Ты сериализуешь большие объёмы данных
- У тебя простые типы (строки, числа, списки, словари)

Не используй если:
- Тебе нужна поддержка Decimal или кастомных объектов
- Ты работаешь с Python 3.7 и ниже
- Твой проект — маленький скрипт, где скорость не критична

Помни: оптимизация ради оптимизации — зло. Но когда ты упираешься в CPU на сериализации — orjson это твой спаситель. Ставь лайк, если хочешь больше разборов быстрых библиотек! 🚀
ПОЧЕМУ ТВОЙ КОД КРИЧИТ, КОГДА ТЫ ПОДКЛЮЧАЕШЬСЯ К ЧУЖОМУ API? 😱

Скорее всего, ты забыл про Adapter — паттерн, который спасает интеграции. Давай разберём, как он работает и почему без него твой проект — это бомба замедленного действия.

ЧТО ТАКОЕ ADAPTER?

Представь: у тебя есть розетка (европейская) и вилка от американского устройства. Они несовместимы! Но ты не ломаешь стену и не переделываешь проводку — ты берёшь переходник. Вот это и есть Adapter. Он приводит интерфейс одного класса к интерфейсу, который ожидает клиент, не трогая сами классы.

В Python это выглядит так: у тебя есть внешний сервис (например, погодный API), который возвращает данные в одном формате, а твоё приложение ожидает другой. Вместо того чтобы менять код приложения или лезть в чужой сервис, ты создаёшь класс-адаптер, который «переводит» один интерфейс в другой.

ЗАЧЕМ ЭТО НУЖНО?

1. Инкапсуляция — ты прячешь детали реализации внешней библиотеки за своим интерфейсом. Если завтра сменишь API — поменяешь только адаптер, а не весь код.
2. Гибкость — можно подключать разные сервисы, просто подменяя адаптеры.
3. Тестируемость — легко замокать адаптер в тестах.

КАК ЭТО РАБОТАЕТ НА ПРИМЕРЕ?

Допустим, у тебя есть класс WeatherService (внешний), который возвращает температуру в фаренгейтах:


class WeatherService:
def get_temp_f(self):
return 75 # градусы Фаренгейта


А твоё приложение ожидает цельсии:


class WeatherAdapter:
def __init__(self, service):
self.service = service

def get_temp_c(self):
f = self.service.get_temp_f()
return (f - 32) * 5 / 9


Всё! Теперь ты можешь использовать WeatherAdapter в своём коде, не думая о конвертации. Это простейший пример, но в реальности адаптеры делают гораздо больше: преобразуют JSON-ответы в объекты, обрабатывают ошибки, добавляют кэширование.

АДАПТЕР VS ФАСАД

Часто путают Adapter и Facade. Запомни: Adapter меняет интерфейс, а Facade упрощает сложную систему, не меняя её интерфейс. Если у тебя есть класс с кучей методов, и ты хочешь дать клиенту простой доступ к нескольким из них — это Facade. Если интерфейс не совпадает — Adapter.

ГДЕ ИСПОЛЬЗОВАТЬ?

- Интеграция с внешними API (самое частое!).
- Подключение разных БД (например, MySQL и PostgreSQL) через единый интерфейс.
- Работа с legacy-кодом, который нельзя менять.

ЧАСТАЯ ОШИБКА

Не пытайся сделать адаптер для всего подряд. Если интерфейсы совместимы — не нужен. И не забывай: адаптер должен делегировать работу сервису, а не дублировать его логику. Иначе получишь «божественный» класс, который делает всё сам.

ИТОГ

Adapter — это твой спасательный круг при работе с чужим кодом. Он делает систему независимой от внешних библиотек и позволяет менять их без боли. На собеседовании скажи: «Adapter приводит интерфейс класса к виду, который ожидает клиент, не изменяя исходный класс». И приведи пример с розеткой — это всегда работает!

А ты уже использовал Adapter в своих проектах? Или только планируешь? Пиши в комментариях! 👇
ДРУГ, ТЫ ДО СИХ ПОР ДУМАЕШЬ, ЧТО async/await В ТЕСТАХ — ЭТО МАГИЧЕСКАЯ КНОПКА УСКОРЕНИЯ? 🚀

Спойлер: нет. Но когда это реально нужно и как правильно настроить — сейчас разберём. Готовься, будет горячо!

СНАЧАЛА — ЗАЧЕМ ВООБЩЕ АСИНХРОННЫЕ ТЕСТЫ?

Представь: твой тест ждёт ответ от API 2 секунды. В это время процессор простаивает. Асинхронность позволяет в это время выполнять другой код. Но ВАЖНО: async/await в Python работает в ОДНОМ потоке! Это не параллелизм, а кооперативная многозадачность. Пока одна корутина ждёт ответа, другая начинает выполняться.

НО ЕСТЬ НЮАНС: если твой код синхронный (например, requests вместо httpx), то асинхронность НЕ ДАСТ НИЧЕГО. Всё упрётся в блокирующий вызов. Поэтому для асинхронных тестов нужны асинхронные библиотеки: httpx.AsyncClient, aiohttp, asyncpg и т.д.

КАК ЭТО ВЫГЛЯДИТ НА ПРАКТИКЕ?

Ставим плагин:
pip install pytest-asyncio httpx


Теперь пишем тест:
import pytest
import httpx

@pytest.mark.asyncio
async def test_get_user():
async with httpx.AsyncClient() as client:
response = await client.get("https://api.example.com/user/1")
assert response.status_code == 200
assert response.json()["name"] == "Alice"


Всё просто? Да! Но есть подводные камни.

ПОДВОДНЫЙ КАМЕНЬ №1: ФИКСТУРЫ

Если у тебя синхронные фикстуры, они будут блокировать event loop. Решение — асинхронные фикстуры:
@pytest.fixture
async def client():
async with httpx.AsyncClient() as c:
yield c

@pytest.mark.asyncio
async def test_get_user(client):
response = await client.get("/user/1")
assert response.status_code == 200


Обрати внимание: фикстура помечена @pytest.fixture, а не @pytest_asyncio.fixture. Но если тест асинхронный, pytest-asyncio сам подхватит асинхронную фикстуру. Хотя для ясности лучше использовать @pytest_asyncio.fixture.

ПОДВОДНЫЙ КАМЕНЬ №2: EVENT LOOP

По умолчанию pytest-asyncio создаёт новый event loop для каждого теста. Это безопасно, но медленно. Можно переиспользовать loop, задав в pytest.ini:
[pytest]
asyncio_mode = auto


Или:
asyncio_mode = strict


В режиме auto тесты с async автоматически помечаются, не нужно писать @pytest.mark.asyncio. Но будь осторожен: в strict нужно явно указывать.

ПОДВОДНЫЙ КАМЕНЬ №3: ПАРАЛЛЕЛИЗМ

Асинхронность ≠ параллельность. Если ты хочешь реально ускорить прогон, используй pytest-xdist для параллельного запуска. Но тогда каждый воркер имеет свой event loop. Это ок.

КОГДА АСИНХРОННОСТЬ РЕАЛЬНО НУЖНА?

- Если ты тестируешь асинхронный код (FastAPI, aiohttp, asyncpg).
- Если у тебя много IO-bound операций (сетевые запросы, чтение файлов) и ты хочешь выполнять их конкурентно внутри одного теста.
- Если ты используешь Playwright для UI — он имеет async API.

А ЕСЛИ НЕТ? НЕ ПАРЬСЯ!

Если твой код синхронный, асинхронные тесты только добавят головной боли. Лучше использовать обычные тесты и параллелить их через pytest-xdist. Это даст реальный прирост.

ИТОГ: КАК ОТВЕТИТЬ НА СОБЕСЕДОВАНИИ?

1. Скажи: "Асинхронные тесты полезны, когда тестируемый код сам асинхронный, или когда нужно конкурентно выполнять IO-операции. Для этого использую pytest-asyncio и httpx.AsyncClient."
2. Покажи пример с фикстурой.
3. Добавь: "Но важно помнить, что async/await не делает тесты параллельными, это кооперативная многозадачность в одном потоке. Для реального параллелизма использую pytest-xdist."

ВОТ ТАК! ТЕПЕРЬ ТЫ ВООРУЖЁН. А КАКИЕ ПОДВОДНЫЕ КАМНИ ВСТРЕЧАЛ ТЫ? ПИШИ В КОММЕНТАРИЯХ! 👇

#python #pytest #async #тестирование #собеседование
🔥 ПОЧЕМУ ТВОЙ КОД ТОРМОЗИТ, А ТЫ ДАЖЕ НЕ ЗНАЕШЬ ГДЕ? ДЕКОРАТОР ДЛЯ ЗАМЕРА ВРЕМЕНИ — ТВОЁ СПАСЕНИЕ!

Представь: ты написал функцию, которая должна летать. А она ползёт, как улитка. Что делать? Вручную вставлять time.time() до и после? Ну, так себе идея. А если таких функций десять? Или сто?

Тут на сцену выходит ДЕКОРАТОР. Это такая «обёртка», которая позволяет добавить код до и после вызова функции, не меняя её саму. Звучит сложно? Сейчас разберём на пальцах.

Представь, что функция — это пицца. Декоратор — это коробка, в которую ты её кладёшь. Коробка может сделать что-то до того, как ты откроешь пиццу (замерить время), и после (снова замерить). А сама пицца остаётся вкусной и невредимой!

ТЕПЕРЬ К ДЕЛУ. Базовый декоратор выглядит так:


import time
import functools

def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
end = time.perf_counter()
print(f"{func.__name__} заняла {end - start:.4f} сек")
return result
return wrapper


Что здесь происходит? Мы создаём функцию timer, которая принимает другую функцию. Внутри неё определяем wrapper — она принимает любые аргументы (*args и **kwargs — это как «мешок» для всех возможных параметров). Внутри wrapper мы засекаем время ДО вызова функции, вызываем её, засекаем время ПОСЛЕ, выводим разницу и возвращаем результат.

А @functools.wraps(func) — это маленькая хитрость. Она копирует имя и документацию исходной функции в wrapper, чтобы не сломать интроспекцию. Без неё твоя функция будет называться wrapper, а не своё имя. Мелочь, а неприятно.

Теперь, чтобы применить декоратор, просто напиши над функцией:


@timer
def heavy_work():
return sum(range(1000000))

heavy_work() # Выведет: heavy_work заняла 0.0452 сек


ВОТ И ВСЁ! Ты только что добавил замер времени одной строчкой. Красота!

НО ЕСТЬ НЮАНС, О КОТОРОМ ВСЕ ЗАБЫВАЮТ. Какой таймер использовать? time.time()? НЕТ! Это ошибка новичка. time.time() возвращает время с начала эпохи (1970 год) и может «прыгать», если системные часы синхронизируются с интернетом. Для точных замеров используй time.perf_counter() — он специально создан для измерения коротких интервалов и максимально точен.

А если функция падает с ошибкой? Тогда мы не увидим время. Давай улучшим декоратор — добавим try/finally:


def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
try:
return func(*args, **kwargs)
finally:
end = time.perf_counter()
print(f"{func.__name__} заняла {end - start:.4f} сек")
return wrapper


Теперь время выведется даже если функция упадёт. Идеально для отладки!

НО СТОП. А что если ты хочешь замерять время в разных местах, не только для функций? Для этого есть модуль timeit. Но это уже другая история.

ЗАПОМНИ: декоратор для замера времени — это твой первый шаг к профилированию. Он покажет, какие функции «съедают» время. А дальше — cProfile и прочие инструменты. Но начинать стоит с малого.

А теперь вопрос к тебе: сколько раз ты писал код, который потом оказывался медленным? И сколько времени потратил на поиск проблемы? С этим декоратором ты сэкономишь часы! Так что бери и используй.

И напоследок: не забывай, что декораторы — это мощный инструмент. Их можно использовать не только для времени, но и для логирования, кеширования, проверки прав. Освоишь их — станешь настоящим senior'ом!

А теперь иди и напиши свой первый декоратор!
ТВОЙ КОД ПРЕВРАЩАЕТСЯ В КАШУ ИЗ СКОБОК И ВЕРТИКАЛЬНЫХ ЧЕРТ? 😱

Вот ты пишешь аннотацию для функции, которая принимает словарь с данными пользователя и возвращает либо число, либо строку, либо None... И получается вот такое:

def process(data: dict[str, dict[str, list[tuple[int, str]]]] | list[tuple[int, str]] | None) -> dict[str, list[tuple[int, str]]] | None:
...


Глаза кровоточат, интервьюер в ужасе, ты сам через неделю не поймёшь, что тут написано. ЗНАКОМО? 👆

Так вот, Python 3.10+ дал нам спасительный инструмент — TypeAlias. И сегодня разберём, как он превращает этот кошмар в конфетку.

ЧТО ТАКОЕ TypeAlias?

Простыми словами: это способ ДАТЬ ИМЯ сложному типу. Вместо того чтобы каждый раз писать гигантскую конструкцию, ты объявляешь её один раз и используешь короткое имя.

Синтаксис — одна строчка:

from typing import TypeAlias

MyComplexType: TypeAlias = dict[str, list[tuple[int, str]]]


Всё! Теперь вместо этой простыни ты пишешь просто MyComplexType. Но погоди, это же можно было сделать и без TypeAlias, просто через присваивание? ДА! И вот тут самый важный нюанс, о котором часто врут в интернете. 👇

ЗАЧЕМ ОН НУЖЕН, ЕСЛИ МОЖНО ПРОСТО ПРИСВОИТЬ?

Смотри. Если ты напишешь просто:

MyType = dict[str, int]


...то для человека это выглядит как обычная переменная. А вот если добавить : TypeAlias, ты ЯВНО говоришь и читателю кода, и инструментам типизации: «Эй, это не переменная, это АЛИАС (псевдоним) типа!».

TypeAlias — это чисто аннотация для разработчика и статических анализаторов (mypy, pyright). В рантайме она НЕ делает НИЧЕГО. Это просто подсказка.

КАК ЭТО УПРОЩАЕТ ЖИЗНЬ?

Вернёмся к нашему примеру. С TypeAlias код становится читаемым:

from typing import TypeAlias

# Объявили алиасы
UserData: TypeAlias = dict[str, str]
UserStats: TypeAlias = dict[str, list[tuple[int, str]]]

# Используем короткие имена
def get_user_stats(data: UserData) -> UserStats | None:
...


Согласись, читать такое — одно удовольствие. Ты сразу видишь, что функция принимает данные пользователя и возвращает статистику или ничего.

КОГДА ИСПОЛЬЗОВАТЬ?

1. Когда один и тот же сложный тип повторяется в нескольких местах. Не дублируй простыню — вынеси в алиас!
2. Когда хочешь дать семантическое имя. UserID вместо int — сразу понятно, что за число.
3. Когда тип сложный и его трудно прочитать. Алиас — как комментарий, только лучше.

ЧАСТАЯ ОШИБКА НОВИЧКОВ

Многие путают TypeAlias с TypedDict. Это РАЗНЫЕ вещи!

TypedDict — описывает СТРУКТУРУ словаря: какие ключи, какие значения. Это как чертёж.
TypeAlias — просто даёт ИМЯ любому типу. Это как ярлык.

Их можно комбинировать:

from typing import TypeAlias, TypedDict

class User(TypedDict):
id: int
name: str

UserList: TypeAlias = list[User]


Вот теперь ты во всеоружии! На собеседовании смело говори: «TypeAlias — это способ объявить псевдоним для типа, чтобы сделать код чище и избежать дублирования сложных аннотаций». И покажи пример. Это сразу выделит тебя из толпы кандидатов, которые пишут простыни из типов.

А теперь вопрос к тебе: ты уже используешь TypeAlias в своих проектах или до сих пор мучаешься с длинными аннотациями? Пиши в комментариях! 👇
ТВОЙ САЙТ МОЛЧА УПАЛ, А В ЛОГАХ ТИШИНА? 😱

Скорее всего, ты забыл про сигналы Django! Это как система оповещения внутри твоего приложения: когда что-то происходит (например, пользователь сохранился), Django кричит об этом на весь код. А ты можешь подписаться на этот крик и сделать что нужно.

ПОЧЕМУ ЭТО ВАЖНО ДЛЯ СОБЕСЕДОВАНИЯ?

Потому что сигналы — это то, что отделяет новичка от сеньора. Новичок пишет логику прямо во view, а сеньор — использует сигналы, чтобы код был чистым и модульным. Интервьюер это сразу видит!

ЧТО ТАКОЕ СИГНАЛЫ ПРОСТЫМИ СЛОВАМИ?

Представь, что Django — это огромный дом. В каждой комнате (приложении) что-то происходит. Сигналы — это звонки между комнатами. Когда в спальне (модели User) что-то случилось (юзер сохранился), звонок летит во все остальные комнаты. А те, кто на него подписался, слышат и реагируют.

КАК ЭТО РАБОТАЕТ ВНУТРИ?

1. Django отправляет сигнал (например, post_save).
2. Сигнал — это объект, который хранит отправителя и аргументы.
3. Все функции-приёмники (receivers), подписанные на этот сигнал, вызываются автоматически.

ГЛАВНЫЕ СИГНАЛЫ, КОТОРЫЕ НАДО ЗНАТЬ:

• pre_save / post_save — до и после сохранения объекта.
• pre_delete / post_delete — до и после удаления.
• m2m_changed — когда меняются связи many-to-many.
• request_started / request_finished — начало и конец HTTP-запроса.

КАК ПОДПИСАТЬСЯ? ВОТ ЖИВОЙ ПРИМЕР:

from django.db.models.signals import post_save
from django.dispatch import receiver
from django.contrib.auth.models import User

@receiver(post_save, sender=User)
def create_profile(sender, instance, created, **kwargs):
if created:
# Создаём профиль только при первом сохранении
Profile.objects.create(user=instance)


Что тут происходит? Мы говорим Django: «Когда сохраняется User, вызови мою функцию». Если пользователь только что создан (created=True), мы создаём ему профиль. Всё! Логика вынесена из view, код стал чище.

ВАЖНЫЙ НЮАНС, О КОТОРОМ ВСЕ ВРУТ 🤥

Многие думают, что сигналы выполняются в том же потоке, что и основная операция. НЕТ! Если ты сохраняешь объект, а в сигнале шлёшь email — пользователь будет ждать ответа, пока письмо уйдёт. Это блокирующая операция!

Решение: используй сигналы для лёгких задач (создание связанных объектов, запись в лог), а тяжёлые задачи отправляй в очередь через Celery.

ЕЩЁ ОДНА ЛОВУШКА 🪤

Сигналы в Django — синхронные. Они выполняются в том же процессе. Если сигнал упадёт с ошибкой — упадёт и основная операция. Поэтому внутри сигналов всегда оборачивай код в try/except, если не хочешь потерять данные.

ГДЕ ХРАНИТЬ СИГНАЛЫ?

Создай файл signals.py в приложении и импортируй его в apps.py через метод ready(). Иначе Django просто не увидит твои сигналы!

# apps.py
from django.apps import AppConfig

class MyAppConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'myapp'

def ready(self):
import myapp.signals # Важно!


КОГДА СИГНАЛЫ НЕ НУЖНЫ?

Честно? Иногда они создают больше проблем, чем решают. Если логика нужна только в одном месте — просто вызови функцию напрямую. Сигналы хороши, когда событие может произойти из разных мест (админка, API, консольная команда).

ИТОГ ДЛЯ ИНТЕРВЬЮ:

Сигналы — это реализация паттерна Observer. Django отправляет уведомления о событиях, а подписчики реагируют. Это держит код чистым и следует принципу DRY (Don't Repeat Yourself).

Запомни: сигналы — это мощный инструмент, но с ним легко выстрелить себе в ногу. Используй осознанно!

А ты уже использовал сигналы в своих проектах? Или боишься их как огня? Пиши в комментариях! 👇
ТЕБЕ ГОВОРИЛИ «НЕ ИСПОЛЬЗУЙ if-elif ДЛЯ СОЗДАНИЯ ОБЪЕКТОВ», А ТЫ ВСЁ РАВНО ДЕЛАЕШЬ? 😱

Стоп! Прежде чем писать простыню из if type == 'cat': return Cat() — прочитай. Разбираем Фабричный метод (Factory Method) — порождающий паттерн, обязательный для Senior.

СУТЬ ПРОСТО
Ресторан: клиент говорит «Мне утку!», не зная, как её готовят. Фабрика — просишь объект без деталей создания. Фабричный метод — ресторан (базовый класс) делегирует создание подклассам.

КОГДА НУЖНО?
• Не знаешь заранее класс объекта.
• Решение о создании принимают подклассы.
• Хочешь скрыть сложную логику создания.

РАЗБИРАЕМ НА ПАЛЬЦАХ
Абстрактный Transport с методом deliver(). Конкретные: Truck (земля), Ship (море). Создатель Logistics объявляет create_transport(), а RoadLogistics и SeaLogistics возвращают нужный объект.

КОД ДЛЯ ИНТЕРВЬЮ
from abc import ABC, abstractmethod

class Transport(ABC):
@abstractmethod
def deliver(self):
pass

class Truck(Transport):
def deliver(self):
return "Доставка по земле"

class Ship(Transport):
def deliver(self):
return "Доставка по морю"

class Logistics(ABC):
@abstractmethod
def create_transport(self) -> Transport:
pass

def plan_delivery(self):
transport = self.create_transport()
return transport.deliver()

class RoadLogistics(Logistics):
def create_transport(self) -> Transport:
return Truck()

class SeaLogistics(Logistics):
def create_transport(self) -> Transport:
return Ship()


plan_delivery() не знает, какой транспорт создаётся. Новый Drone — добавляешь класс и подкласс, не трогая старый код.

ПОЧЕМУ ЛУЧШЕ if-elif?
1. Открытость расширению: новый продукт = новый класс.
2. Чистый код без условий.
3. Инкапсуляция создания.

ВАЖНО! НЕ ПУТАЙ
Фабричный метод ≠ Простая фабрика (функция с if-elif). Здесь создание делегируется подклассам через наследование. Подчеркни это на собеседовании.

ЕЩЁ ПРИМЕР
Парсеры: базовый Parser с create_parser(), JsonParserFactory возвращает JsonParser, XmlParserFactoryXmlParser. Код работает через общий интерфейс.

ОТВЕТ НА СОБЕСЕДОВАНИИ
1. Цель: отделить создание от использования.
2. Структура: Product, ConcreteProduct, Creator, ConcreteCreator.
3. Пример из кода.
4. Плюсы (гибкость) и минусы (подкласс на каждый продукт).

Задание: где видел паттерн в реальном коде? Делись в комментариях! 👇

Ставь 🔥 за больше разборов!
ПОЧЕМУ ТВОЙ async def get_user() ТОРМОЗИТ ВСЮ ПРОДУ, А ТЫ ДАЖЕ НЕ ПОНЯЛ?

А всё потому, что ты синхронно ходишь в Redis внутри асинхронной функции! 😱 Блокируешь event loop, и все твои «асинхронные» запросы встают в очередь. Интервьюер это обожает — спрашивает про асинхронный кэш и смотрит, как ты будешь выкручиваться. Давай разберём, как сделать правильно, чтобы ты не просто ответил, а блеснул!

ПРЕДСТАВЬ: у тебя популярный сервис, и каждый запрос дёргает базу данных. Чтобы не убить её, ты ставишь кэш. Но если кэш работает синхронно — это как кассир, который обслуживает одного клиента и замирает, пока тот ищет мелочь. Остальные ждут! 🐌

Асинхронный кэш — это когда кассир передал задачу стажёру и сразу зовёт следующего клиента. В Python это делается с помощью aiocache и Redis. Работает это так: твой код не блокируется, пока идёт запрос в Redis, а спокойно ждёт ответа через await. Event loop в это время занимается другими задачами.

КАК ЭТО ВЫГЛЯДИТ НА ПРАКТИКЕ?

Сначала ставим библиотеки:
pip install aiocache[redis]


Теперь настраиваем кэш. Самый простой способ — использовать декоратор @cached:
from aiocache import cached
from aiocache.serializers import JsonSerializer

@cached(ttl=60, serializer=JsonSerializer())
async def get_user(user_id):
# тут тяжёлый запрос к базе
return {"id": user_id, "name": "Alice"}


Что здесь происходит? При первом вызове функция выполняется, результат сохраняется в Redis на 60 секунд. При повторном вызове в течение этого времени — данные берутся из кэша, и функция не выполняется! Магия

НО ЕСТЬ НЮАНС! Декоратор @cached по умолчанию использует локальный кэш в памяти, а не Redis. Чтобы подключить именно Redis, нужно настроить его глобально или передать параметры:
from aiocache import Cache

@cached(ttl=60, cache=Cache.REDIS, endpoint="127.0.0.1", port=6379, serializer=JsonSerializer())
async def get_user(user_id):
return {"id": user_id, "name": "Alice"}


Или можно настроить кэш один раз в приложении:
from aiocache import caches

caches.set_config({
"default": {
"cache": "aiocache.RedisCache",
"endpoint": "127.0.0.1",
"port": 6379,
"serializer": {
"class": "aiocache.serializers.JsonSerializer"
}
}
})


Теперь все @cached без указания cache будут использовать Redis. Удобно!

А ЧТО НАСЧЁТ РУЧНОГО УПРАВЛЕНИЯ?

Иногда нужно самим положить в кэш или достать. Для этого используем aiocache.RedisCache:
from aiocache import RedisCache

cache = RedisCache(endpoint="127.0.0.1", port=6379, serializer=JsonSerializer())

# Положить
await cache.set("user:1", {"name": "Alice"}, ttl=60)

# Достать
user = await cache.get("user:1")


ВОТ ТУТ ЧАСТАЯ ОШИБКА: если ты забудешь про await, то получишь корутину вместо данных. И потом удивляешься, почему в ответе <coroutine object>. Всегда проверяй!

ПОЧЕМУ ЭТО ВАЖНО НА СОБЕСЕДОВАНИИ?

Потому что асинхронность — это не просто модно, а критично для высоконагруженных систем. Если ты покажешь, что понимаешь разницу между блокирующим и неблокирующим кэшем, и умеешь пользоваться aiocache, ты сразу выделишься среди других кандидатов. 🔥

А теперь вопрос к тебе: как думаешь, что будет, если два запроса одновременно попросят один и тот же ключ, которого ещё нет в кэше? Пиши в комментариях! 👇
ТЫ УВЕРЕН, ЧТО ТВОЙ КЛАСС ПРИНИМАЕТ ЛЮБОЙ ТИП? ИЛИ ТЫ ПРОСТО НАПИСАЛ list И НАДЕЕШЬСЯ НА ЛУЧШЕЕ?

Пишешь функцию для чисел и строк? Или класс-контейнер? Для каждого типа отдельный класс? НЕТ! Выход — TypeVar и Generic. Это пропуск в мир гибкого кода.

1. ПРОСТЫМИ СЛОВАМИ

Заказываешь пиццу? Ты не говоришь «с грибами или колбасой», а «с НАЧИНКОЙ». Какую — решаешь потом. TypeVar — «начинка». Generic — «коробка», позволяющая классу сказать: «Работаю с любым типом, договоримся, каким, при создании».

2. ЧУТЬ ГЛУБЖЕ

TypeVar — переменная, обозначающая «некоторый тип». Generic — базовый класс для обобщения.

Пример: класс «Пара» без дженериков работает, но не скажешь, что первый — число, второй — строка. С ними:

from typing import TypeVar, Generic

T = TypeVar('T')
U = TypeVar('U')

class Pair(Generic[T, U]):
def __init__(self, first: T, second: U):
self.first: T = first
self.second: U = second

pair = Pair[int, str](1, 'hello')
# mypy знает: pair.first — int, pair.second — str. Ошибка при сложении!


3. TypeVar БЫВАЕТ РАЗНЫМ

Можно ограничить. Например, только числа:

Number = TypeVar('Number', int, float)

def add(a: Number, b: Number) -> Number:
return a + b
# add('a', 'b') — mypy не даст.


Или bound — тип должен быть наследником класса:

class Animal:
def make_sound(self): pass

A = TypeVar('A', bound=Animal)

def get_sound(animal: A) -> str:
return animal.make_sound()


4. ЗАЧЕМ НА СОБЕСЕДОВАНИИ?

Скажи: «Generic — код с разными типами, сохраняющий информацию о типе для статического анализа». Это создаёт переиспользуемые компоненты без потери безопасности. Ключевое отличие от Any: Any — «забей», TypeVar — «зафиксируем тип».

5. ГЛАВНЫЙ НЮАНС

TypeVar ≠ Any. Any — «всё равно». TypeVar — «важен ОДИН И ТОТ ЖЕ тип»:

def bad_pair(a: Any, b: Any) -> Any:
return (a, b)

T = TypeVar('T')
def good_pair(a: T, b: T) -> tuple[T, T]:
return (a, b)

# bad_pair(1, 'строка') — ок.
# good_pair(1, 'строка') — ошибка: T не может быть и int, и str.


Это отличает сеньора от джуна!

Пишешь класс-обёртку? Вспомни TypeVar. Это инструмент для безопасного и понятного кода. На собеседовании объяснишь не термин, а суть. Удачи!
Твой FastAPI-эндпоинт отвечает 300 мс, а мог бы за 3 мс. И ты ТЕРЯЕШЬ ДЕНЬГИ на каждом запросе, даже не подозревая об этом!

СЕГОДНЯ РАЗБИРАЕМ, КАК РАБОТАЕТ МЕХАНИЗМ КЭШИРОВАНИЯ С REDIS И ДЕКОРАТОРАМИ. Это один из самых частых вопросов на собеседованиях — и один из самых недопонятых.

ПОЧЕМУ REDIS, А НЕ ПРОСТО СЛОВАРЬ В ПАМЯТИ?

Представь: у тебя 10 серверов за балансировщиком. Каждый хранит свой кэш в оперативке. Пришёл запрос на сервер №1 — кэш промахнулся, пошли в базу. Следующий запрос улетел на сервер №2 — и снова промах! База падает под нагрузкой, хотя данные уже кто-то запрашивал.

Redis — это внешнее хранилище в памяти, общее для всех инстансов. Один раз записал — все серверы читают. Плюс у него есть TTL (время жизни ключа), атомарные операции и он сам умеет вытеснять старые данные.

КАК ЭТО РАБОТАЕТ ПОД КАПОТОМ?

1. Прилетает запрос на эндпоинт
2. Декоратор строит ключ — например, item:42 или user:7:profile
3. Проверяет Redis: есть ли значение по этому ключу?
4. Если есть — возвращает закэшированный JSON, база данных даже не просыпается
5. Если нет — выполняет твою функцию, получает результат
6. Сохраняет результат в Redis с TTL (например, 300 секунд)
7. Возвращает результат клиенту

Вот минимальный декоратор, который реально работает:


import functools
import json

def redis_cache(key_builder, ttl=300):
def decorator(func):
@functools.wraps(func)
async def wrapper(*args, **kwargs):
request = kwargs.get("request")
redis = request.app.state.redis
cache_key = key_builder(*args, **kwargs)

cached = await redis.get(cache_key)
if cached is not None:
return json.loads(cached)

result = await func(*args, **kwargs)
await redis.set(cache_key, json.dumps(result), ex=ttl)
return result
return wrapper
return decorator


Использование — одна строка:


@redis_cache(lambda request, item_id: f"item:{item_id}", ttl=300)
@app.get("/items/{item_id}")
async def get_item(request: Request, item_id: int):
# тяжёлый запрос в базу
return item


ГЛАВНЫЙ ПОДВОДНЫЙ КАМЕНЬ, О КОТОРОМ ВСЕ ЗАБЫВАЮТ

Кэшировать чтение — легко. Но что происходит, когда пользователь ОБНОВЛЯЕТ данные? Если ты не очистишь кэш — он будет отдавать УСТАРЕВШИЕ данные, пока не истечёт TTL. Это классическая проблема инвалидации кэша.

Решение простое: при записи в базу — удаляй ключ из Redis:


await redis.delete(f"item:{item_id}")


Или используй паттерн Cache-Aside: сначала читаем из кэша, при промахе идём в базу, обновляем кэш. При записи — сначала пишем в базу, потом инвалидируем кэш.

ТРИ ОШИБКИ, КОТОРЫЕ ВАЛЯТ ПРОДАКШН

1. Забыл decode_responses=True при создании клиента. Получишь b"{"key":"value"}" вместо строки — и JSON не распарсится.

2. Используешь KEYS * для поиска. В проде это блокирует Redis на секунды! Используй SCAN — он итерируется порциями.

3. Кэшируешь всё подряд. Не кэшируй ответы, которые меняются каждую секунду, и не кэшируй большие объёмы — Redis хранит всё в памяти, а память конечна.

ЧТО ОТВЕЧАТЬ НА СОБЕСЕДОВАНИИ?

Скажи так: «Кэширование — это компромисс между скоростью и актуальностью. Redis даёт нам общее хранилище в памяти со скоростью доступа микросекунды. Декоратор — это перехватчик: он проверяет кэш до выполнения функции и сохраняет результат после. Главная сложность — не записать в кэш, а вовремя его инвалидировать».

И добавить: «Для инвалидации использую паттерн Cache-Aside и удаляю ключи при операциях записи. TTL — как страховка от забытых инвалидаций».

Такой ответ показывает, что ты понимаешь не только механику, но и подводные камни. А это уровень Senior.

Сохраняй себе — пригодится перед интервью! 🔥
ПОЧЕМУ ТВОЙ ПАРАЛЛЕЛЬНЫЙ PYTHON-КОД РАБОТАЕТ МЕДЛЕННЕЕ, ЧЕМ ОДИНОЧНЫЙ?

Создал 10 процессов, а программа еле ползёт? Знакомо? Скорее всего, ты неправильно используешь multiprocessing.Pool и Queue. На собеседовании за такое бьют по рукам. Давай разберём, как сделать правильно.

СНАЧАЛА — БАЗА

Представь, что компьютер — это кухня. Процесс — отдельный повар со своей плитой и ножами. Он изолирован. В Python каждый процесс — отдельный интерпретатор со своей памятью и GIL. Поэтому процессы реально используют несколько ядер CPU.

Поток — тот же повар, но на общей кухне. Он быстро передаёт соль, но если застрял — блокирует всех. Из-за GIL потоки не выполняются параллельно на разных ядрах. Потоки — для ожидания (I/O-bound), процессы — для вычислений (CPU-bound).

ГЛАВНЫЙ ВОПРОС: POOL ИЛИ QUEUE?

Это разные инструменты.

Queue — конвейерная лента. Ты вручную создаёшь процессы через Process, раскидываешь задачи, собираешь результаты. Низкий уровень. Контролируешь каждый шаг, но легко ошибиться.

Pool — менеджер, который сам раздаёт задачи свободным поварам. Говоришь: «Вот функция, вот данные, обработай». Pool сам создаёт процессы, распределяет нагрузку и собирает результаты. Высокий уровень.

Секрет: Pool уже использует Queue внутри. Не изобретай велосипед. Городить Queue там, где хватит Pool — минус в карму.

КАК ПРАВИЛЬНО ИСПОЛЬЗОВАТЬ POOL

Магия в три строки:


from multiprocessing import Pool

def factorial_sum(n):
result = 1
for i in range(2, n + 1):
result *= i
return result

if __name__ == "__main__":
numbers = [1000, 2000, 3000, 4000]
with Pool(processes=4) as pool:
results = pool.map(factorial_sum, numbers)
print(results)


Pool(processes=4) — пул из 4 процессов.
pool.map(factorial_sum, numbers) — менеджер сам раздаёт задачи.
with ... as pool — гарантия корректного закрытия. Без with рискуешь оставить зомби-процессы.

ПОЧЕМУ НУЖЕН if __name__ == "__main__"?

Критично на Windows. Там процессы запускаются через spawn: дочерний процесс импортирует модуль заново. Без защиты каждый дочерний процесс создаст свои пулы — бесконечный взрыв. Код зависнет или упадёт. На собеседовании спросят: «Почему без этой конструкции всё ломается?» — отвечай про повторный импорт.

КОГДА НУЖНА QUEUE?

Если нужен постоянный обмен сообщениями, например, паттерн «производитель-потребитель». Один генерирует данные, другой обрабатывает. Тут Queue — друг.


from multiprocessing import Process, Queue

def producer(q):
for i in range(10):
q.put(i)
q.put(None)

def consumer(q):
while True:
item = q.get()
if item is None:
break
print(f"Обработал: {{{{item}}}}")

if __name__ == "__main__":
q = Queue()
p1 = Process(target=producer, args=(q,))
p2 = Process(target=consumer, args=(q,))
p1.start()
p2.start()
p1.join()
p2.join()


Здесь мы сами управляем процессами и очередью. Гибко, но сложнее. Queue — безопасный способ передачи данных между изолированными процессами. Она заботится о блокировках.

ЧАСТАЯ ОШИБКА

Пытаться использовать обычный список для обмена данными. Нельзя! Каждый процесс имеет свою копию памяти. Изменения не видны другим. Нужны Queue, Pipe, Value или Array. Или просто Pool.

ИТОГ ДЛЯ ИНТЕРВЬЮ

1. Для массовой обработки — Pool с map. Просто и безопасно.
2. Для гибкой архитектуры с обменом — Queue с Process.
3. Всегда защищай точку входа if __name__ == "__main__".
4. Процессы — для CPU-bound, потоки — для I/O-bound.

А ты уже пробовал Pool? Или мучаешься с потоками? Пиши в комментариях!
ПОЧЕМУ ТВОЙ PYTHON ЖРЁТ ПАМЯТЬ КАК НЕ В СЕБЯ, ХОТЯ ТЫ ВСЕГО ЛИШЬ СКОПИРОВАЛ СПИСОК?

Стоп. Давай сразу разберёмся, потому что тут 90% джунов путаются и на собесе говорят ерунду.

Когда ты пишешь b = a, Python НЕ копирует данные. Он просто вешает на тот же объект вторую бирку. А вот когда пишешь b = list(a) — вот тут начинается магия.

ЧТО ТАКОЕ COPY-ON-WRITE (CoW)?

Представь папку с документом. Ты даёшь другу копию — но не бумажную, а ссылку на тот же файл в облаке. Пока никто не редактирует — файл один. Как только кто-то нажимает "изменить" — облако МГНОВЕННО создаёт отдельную версию для него. Всё, больше не пересекаются.

Вот это и есть copy-on-write: данные физически копируются только в момент записи, а до этого все смотрят на один и тот же блок памяти.

НО ВАЖНЫЙ НЮАНС — в самом Python это НЕ встроено!

Многие верят в миф, что "в Python 3.11+ появился CoW для списков и словарей". ЭТО НЕПРАВДА. CPython так не работает. list(a) и dict(a) делают честную, полную копию. Сразу. Всю память.

Так где же CoW реально живёт?

В pandas 2.0+ — вот там это ядро архитектуры. С версии 2.0 режим CoW включён, а в 3.0 станет дефолтом.
В файловых системах (ZFS, Btrfs, APFS) — на уровне ОС.
В fork() на Linux — дочерний процесс делит память с родителем, пока не начнёт писать.
В numpy — через views vs copies.

КАК ЭТО РАБОТАЕТ В PANDAS

Раньше было больно:
df = pd.DataFrame({"grade": ["A", "C", "D"]})
grades = df["grade"]
grades.iloc[0] = "E" # изменился и df тоже!


Это баг-ловушка. Ты меняешь одно — а ломается другое. CoW это убивает:

grades = df["grade"] — пока никто не пишет, это просто ссылка на те же данные.
grades.iloc[0] = "E" — ВОТ ТУТ pandas делает копию только для grades.
df остаётся нетронутым. Всё честно.

ЗАЧЕМ ЭТО ТЕБЕ НА СОБЕСЕДЕ

1. Экономия памяти. Если у тебя DataFrame на 10 ГБ и ты делаешь 5 срезов — без CoW это 50 ГБ. С CoW — 10 ГБ, пока не начнёшь писать.
2. Предсказуемость. Никаких SettingWithCopyWarning и загадочных мутаций.
3. Скорость. Копирование откладывается до реальной необходимости.

ЧТО СПРОСЯТ В ЛОБ

"Как включить CoW в pandas?" — В pandas 2.x: pd.options.mode.copy_on_write = True. В 3.0 — по умолчанию.

"А в чистом Python есть?" — Нет. Только через сторонние структуры или ОС-механизмы.

"В чём подвох?" — CoW ломает старый код с chained assignment. df[df.x > 2]["y"] = 5 теперь кидает ChainedAssignmentError. Зато это правильно.

Запомни главное: CoW — это не про Python-списки, а про pandas, numpy и файловые системы. Не путай. На собесе за этот ответ тебя запомнят.
ПОЧЕМУ ТВОЙ МНОГОПОТОЧНЫЙ КОД НЕ УСКОРЯЕТСЯ, А ИНОГДА РАБОТАЕТ МЕДЛЕННЕЕ, ЧЕМ ОБЫЧНЫЙ?

Ты пишешь threading, запускаешь 8 потоков на 8-ядерной машине и ждёшь чуда. А чуда нет. И виноват в этом GIL — самый нелюбимый и самый непонятый механизм в Python.

ЧТО ТАКОЕ GIL ПРОСТЫМИ СЛОВАМИ

GIL (Global Interpreter Lock) — это глобальная блокировка интерпретатора. Представь комнату с ОДНИМ микрофоном. В комнате 8 человек (потоков), но говорить в микрофон может только тот, у кого он в руках. Остальные стоят и ждут. Именно так работает CPython: в любой момент времени байт-код исполняет РОВНО ОДИН поток.

Это мьютекс, который защищает внутренние структуры интерпретатора: счётчики ссылок, сборку мусора, словари. Без него два потока могли бы одновременно менять счётчик ссылок на объект — и получить утечку памяти или краш.

ЧТО ЭТО ЗНАЧИТ НА ПРАКТИКЕ

CPU-bound задачи (математика, парсинг, обработка изображений) — потоки НЕ дают ускорения. Ты платишь за переключение контекста, но параллелизма нет. Иногда 4 потока работают МЕДЛЕННЕЕ, чем 1.

I/O-bound задачи (сеть, диск, БД) — потоки работают отлично! Пока один поток ждёт ответа от сервера, GIL освобождается, и другой поток спокойно работает. Вот почему requests в потоках — норм.

КОГДА GIL ОТПУСКАЕТСЯ

Интерпретатор отдаёт GIL добровольно:
- при блокирующих I/O-операциях;
- по таймеру (в Python 3.2+ — каждые 5 мс);
- внутри C-расширений (NumPy, lxml), которые сами его освобождают.

Именно поэтому NumPy обгоняет чистый Python в разы — тяжёлые вычисления уходят в C, а GIL там снимается.

ЧТО ДЕЛАТЬ

1. I/O-boundthreading или asyncio. Потоки тут дают реальный выигрыш.
2. CPU-boundmultiprocessing. Каждый процесс — свой интерпретатор, свой GIL, своя память. Вот где работает параллелизм на ядрах.
3. Тяжёлые вычисления → NumPy, Cython, C-расширения, которые снимают GIL.

ЧАСТЫЙ МИФ

"GIL есть во всех Python" — НЕТ. GIL есть только в CPython. Jython, IronPython, PyPy (частично) его не имеют. Но именно CPython — стандарт де-факто, поэтому GIL касается всех нас.

А ЧТО С PYTHON 3.13+?

PEP 703 принят! В 3.13 появилась экспериментальная сборка без GIL (free-threaded). Это не значит, что GIL исчез — он пока опционален, и однопоточные скрипты в такой сборке медленнее. Полный отказ — дело будущего.

ЧТО СКАЗАТЬ НА ИНТЕРВЬЮ

GIL — это мьютекс в CPython, защищающий внутреннее состояние интерпретатора. Он ограничивает параллелизм CPU-bound задач в потоках, но не мешает I/O-bound. Для параллельных вычислений — multiprocessing. Это честный, точный ответ, который ждёт интервьюер.