Ползём в сеньоры. Вопросы к собеседованию на сеньора Python.
2 subscribers
5 links
Подготовка какие вопросы могут задавать для сеньора по python и framework
Download Telegram
ТЫ ДУМАЕШЬ, ЧТО Pydantic v2 — ЭТО ПРОСТО БЫСТРЫЙ v1? А ВОТ И НЕТ! 😱

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

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

Это превращение объекта в формат, который можно передать по сети или сохранить в файл. В Python чаще всего это JSON или словарь. Pydantic — это библиотека, которая парсит и валидирует данные. Она берёт на себя всю грязную работу: проверяет типы, приводит их, а потом умеет отдавать данные обратно. Вот с этим «обратно» во второй версии и произошла революция.

ГЛАВНОЕ ОТЛИЧИЕ: .dict() УМЕР, ДА ЗДРАВСТВУЕТ .model_dump()!

В v1, чтобы получить словарь из модели, ты писал:
user.dict()


В v2 этот метод удалён. Теперь это:
user.model_dump()


А для JSON вместо .json() используй .model_dump_json(). Казалось бы, мелочь, но это только верхушка айсберга. За этими переименованиями стоит фундаментальное изменение архитектуры.

ПОЧЕМУ ТАК СДЕЛАЛИ?

В v1 сериализация была «связана» с валидацией. Каждый раз, когда ты вызывал .dict(), Pydantic мог заново прогонять данные через валидаторы. Это было медленно и непредсказуемо. В v2 создатели разделили эти процессы. Теперь валидация — это одно, а сериализация — совсем другое. Это как разделить кухню и столовую: готовить можно отдельно, а есть отдельно, и никто никому не мешает.

В v2 сериализация работает через отдельный механизм — сериализаторы. Ты можешь управлять тем, как поле превращается в JSON, независимо от того, как оно валидируется. Хочешь, чтобы дата выводилась в одном формате, а принималась в другом? Легко! В v1 это была боль.

ВТОРОЕ ОТЛИЧИЕ: ТИПЫ СТАЛИ УМНЕЕ

В v1, если ты писал поле типа int, а передавал строку '123', Pydantic молча приводил её к числу. В v2 это поведение осталось, но стало настраиваемым. Появился параметр strict=True, который запрещает приведение типов. Это важно, когда ты работаешь с деньгами или ID — там неожиданное приведение строки к числу может привести к багам.

Пример:
from pydantic import BaseModel

class User(BaseModel):
id: int

user = User(id='123') # v2: ок, приведёт к int
print(user.id) # 123

class StrictUser(BaseModel):
model_config = {'strict': True}
id: int

strict_user = StrictUser(id='123') # Ошибка! Строка не пройдёт


ТРЕТЬЕ: КАСТОМНЫЕ СЕРИАЛИЗАТОРЫ

В v1, чтобы изменить способ сериализации поля, приходилось писать валидаторы и городить костыли. В v2 есть декоратор @field_serializer. Он позволяет указать, как именно поле должно превращаться в JSON. Например, ты хранишь пароль в виде хэша, но при сериализации хочешь отдавать маску:

from pydantic import BaseModel, field_serializer

class Account(BaseModel):
password_hash: str

@field_serializer('password_hash')
def mask_password(self, value):
return '***' + value[-4:]


Это мощный инструмент, который в v1 потребовал бы танцев с бубном.

ЧЕТВЁТОЕ: СКОРОСТЬ

Не буду грузить цифрами, но скажу так: v2 работает в разы быстрее, потому что ядро переписано на Rust. Если у тебя API с миллионами запросов, разница будет колоссальной.

ЧТО ЗАПОМНИТЬ ДЛЯ ИНТЕРВЬЮ?

1. В v2 методы .dict() и .json() заменены на .model_dump() и .model_dump_json().
2. Сериализация отделена от валидации — это два независимых процесса.
3. Появились @field_serializer и @model_serializer для тонкой настройки вывода.
4. Строгий режим strict=True позволяет запретить приведение типов.
5. Всё это работает быстрее благодаря Rust.

Расскажешь это — и интервьюер поймёт, что ты не просто читал документацию, а реально работал с библиотекой. Удачи на собеседовании! 💪
🔥 СЛОЖНАЯ ТЕМА, КОТОРУЮ СПРАШИВАЮТ НА КАЖДОМ СОБЕСЕДОВАНИИ: SAGA PATTERN В PYTHON. РАЗБИРАЕМСЯ, ПОКА ГОРИТО! 🔥

Представь: ты — архитектор огромного интернет-магазина. У тебя есть отдельные сервисы: заказы, платежи, склад, доставка. И вот клиент оформляет заказ. Ты создаёшь запись в БД заказов, списываешь деньги, резервируешь товар, отправляешь доставку. Всё бы ничего, но КАЖДЫЙ СЕРВИС ХРАНИТ ДАННЫЕ В СВОЕЙ БАЗЕ. И тут склад падает — товара нет. Деньги списаны, а заказ не выполнен. КАК ОТКАТИТЬ ПЛАТЁЖ? 🤯

В монолите ты бы просто сделал ROLLBACK, и все изменения отменились бы. Но в микросервисах ACID-транзакции между разными БД НЕ РАБОТАЮТ. И тут на сцену выходит SAGA PATTERN — твой спаситель! 🦸‍♂️

ЧТО ТАКОЕ SAGA?
Это паттерн, который разбивает большую распределённую транзакцию на последовательность маленьких локальных транзакций. Каждый шаг — это отдельная операция в одном сервисе. Если что-то пошло не так, запускаются КОМПЕНСИРУЮЩИЕ ТРАНЗАКЦИИ — они отменяют уже сделанные шаги и возвращают систему в консистентное состояние.

ЕСТЬ ДВА ПОДХОДА:

1️⃣ CHOREOGRAPHY (ХОРЕОГРАФИЯ) — децентрализованный танец без дирижёра. Каждый сервис слушает события (например, через Kafka) и сам решает, что делать дальше. Создал заказ → опубликовал событие OrderCreated → платёжный сервис услышал и списал деньги → опубликовал PaymentReserved → склад услышал и зарезервировал товар... И так по цепочке. Если шаг упал, сервис публикует событие об ошибке, и предыдущие сервисы делают компенсацию.

Плюсы: сервисы полностью независимы, нет единой точки отказа.
Минусы: при длинных цепочках сложно понять, что вообще происходит. А если сервисы зациклились — пиши пропало! 😅

2️⃣ ORCHESTRATION (ОРКЕСТРОВКА) — есть центральный координатор (оркестратор), который управляет всеми шагами. Он говорит: «Платеж, спиши деньги!», «Склад, зарезервируй товар!». Если что-то падает, оркестратор сам запускает компенсации.

Плюсы: весь процесс виден в одном месте, легко отлаживать.
Минусы: оркестратор — это потенциальное узкое место и единая точка отказа.

КАК РЕАЛИЗОВАТЬ SAGA В PYTHON?

Для хореографии — используй Kafka или RabbitMQ. Каждый сервис — отдельное приложение на FastAPI или Django, которое слушает события. Примерно так:

# Сервис заказов (публикует событие)
from kafka import KafkaProducer
producer = KafkaProducer(bootstrap_servers='localhost:9092')
producer.send('orders', key=b'order-123', value=b'OrderCreated')

# Сервис платежей (слушает событие)
from kafka import KafkaConsumer
consumer = KafkaConsumer('orders', bootstrap_servers='localhost:9092')
for msg in consumer:
if msg.value == b'OrderCreated':
# списываем деньги
# публикуем PaymentReserved или PaymentFailed


Для оркестрации — можно использовать библиотеку saga-pattern или написать свой координатор на asyncio. Например:

async def create_order_saga():
try:
await create_order()
await reserve_payment()
await reserve_inventory()
await create_shipping()
except Exception:
await cancel_payment()
await cancel_order()


ВАЖНЫЙ НЮАНС, О КОТОРОМ ЧАСТО ВРУТ:
Некоторые говорят, что Saga — это просто «транзакция с откатом». НЕТ! Компенсации — это НЕ откат. Они выполняются уже ПОСЛЕ того, как локальная транзакция закоммичена. И они должны быть идемпотентными — если компенсация выполнится дважды, не должно быть ошибки.

КОГДА ЧТО ВЫБРАТЬ?
- Хореографию — для простых цепочек (2-4 шага) и когда важна независимость сервисов.
- Оркестрацию — для сложных процессов с ветвлениями и когда нужен чёткий контроль.

А теперь вопрос к тебе: какой подход ты бы выбрал для своего проекта и почему? Пиши в комментариях! 👇

#python #saga #микросервисы #собеседование #архитектура
🔥 ТВОЙ PYTHON-СКРИПТ ЖРЁТ ПАМЯТЬ, А ТЫ НЕ ЗНАЕШЬ ГДЕ? ДАВАЙ РАЗБЕРЁМСЯ!

Представь: ты написал отличный сервис, всё работает. Но через день-два он начинает тормозить, а потом и вовсе падает с MemoryError. Знакомо? Это классические утечки памяти. И сегодня мы научимся их ловить с помощью memory_profiler!

Сразу к делу: memory_profiler — это библиотека для Python, которая показывает, сколько памяти потребляет каждая строка твоего кода. Это как рентген для твоего скрипта: видно каждый «орган» и сколько он «весит». Устанавливается одной командой:

pip install memory_profiler

А теперь — как пользоваться. Есть два способа.

Способ 1: Декоратор @profile

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


from memory_profiler import profile

@profile
def my_func():
a = [i for i in range(100000)] # список из 100к элементов
b = {i: i**2 for i in range(10000)} # словарь
return a, b

my_func()


Запускаешь скрипт и получаешь таблицу! В ней будет:
- Line # — номер строки
- Mem usage — сколько памяти занято после выполнения строки
- Increment — на сколько выросло потребление
- Occurrences — сколько раз строка выполнилась

Смотришь на Increment — и сразу видно, где твой код «раздувается». Если какая-то строка добавляет десятки мегабайт — вот твой подозреваемый!

Способ 2: Замер по времени

Хочешь посмотреть, как память меняется во времени? Используй mprof:

mprof run my_script.py

А потом построй график:

mprof plot

Получишь наглядную кривую. Если она растёт бесконечно — утечка на лицо!

Теперь главный вопрос: как найти утечку?

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

Вот пример коварной утечки:


import gc

class Node:
def __init__(self):
self.ref = None

def leak():
a = Node()
b = Node()
a.ref = b # a ссылается на b
b.ref = a # b ссылается на a — цикл!
# после выхода из функции a и b должны удалиться,
# но они ссылаются друг на друга — сборщик мусора их не тронет

for _ in range(1000):
leak()


Каждый вызов создаёт два объекта, которые навсегда остаются в памяти. Запусти с @profile — увидишь, как память растёт с каждой итерацией!

Как бороться?

1. Используй слабые ссылки (weakref) — они не мешают сборщику мусора.
2. Не храни большие данные в глобальных переменных — они живут вечно.
3. Явно удаляй объекты через del или gc.collect().

Но помни: memory_profiler — это профайлер, а не детектор утечек. Он показывает, где память тратится, но не всегда говорит, что это утечка. Для глубокого анализа используй tracemalloc — он отслеживает каждый блок памяти!

И ещё один нюанс: memory_profiler сам по себе замедляет код. Не запускай его на проде! Только на тестовых данных.

Практический совет: если твой сервис падает через N часов, запусти его с mprof run и оставь на ночь. Утром посмотри график — если линия ползёт вверх без остановки, значит, утечка. Дальше — точечно проверяй функции через @profile.

Теперь ты вооружён! Иди и найди свои утечки! А если найдёшь — расскажи в комментариях, что это было. 😉
ТВОЙ asyncio-КОД МОЖЕТ ТИХО УПАСТЬ НА ПРОДЕ, И ТЫ ДАЖЕ НЕ ПОЙМЁШЬ ПОЧЕМУ!

Сколько раз ты писал что-то вроде «async with lock:» и надеялся, что всё будет работать? А потом — бац! — и данные в кэше дублируются, или 1000 запросов летит на сервер, хотя нужно было всего 5. Знакомо? Тогда этот разбор — для тебя.

ДАВАЙ РАЗБЕРЁМСЯ, КАК РЕАЛЬНО РАБОТАЮТ БЛОКИРОВКИ В ASYNCIO.

1️⃣ ПРОБЛЕМА: ПОЧЕМУ ОНИ ВООБЩЕ НУЖНЫ?

asyncio — однопоточный. Казалось бы, откуда взяться гонкам? Но вот в чём фишка: задачи выполняются ПООЧЕРЁДНО, переключаясь в момент await. Представь: две задачи одновременно проверяют кэш. Обе видят, что данных нет. Обе делают await на запрос к сайту. И вот уже ДВА запроса улетели вместо одного!

Это классическая гонка (race condition). Она возникает, когда между проверкой условия и изменением состояния есть точка переключения контекста.

2️⃣ РЕШЕНИЕ: asyncio.Lock

Lock — это как «туалет с замком»: один зашёл, закрылся, другие ждут снаружи. Пока первый не выйдет и не откроет, никто не войдёт.

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

lock = asyncio.Lock()

async def get_value(key):
async with lock: # ждём, пока освободится
if key not in cache:
value = await request_remote()
cache[key] = value
else:
value = cache[key]
return value


⚠️ КРИТИЧЕСКИЙ МОМЕНТ: блокировка должна захватываться ДО проверки условия! Если ты поставишь async with lock только вокруг самого запроса, гонка останется. Запомни: защищаем ВЕСЬ критический участок, от проверки до обновления.

3️⃣ НО ЕСТЬ НЮАНС: Lock НЕ ограничивает количество

Lock пропускает только ОДНУ задачу. А что если нам нужно разрешить, скажем, 5 одновременных подключений к базе? Тут на сцену выходит Semaphore.

4️⃣ asyncio.Semaphore: ОГРАНИЧИТЕЛЬ ПОТОКА

Semaphore — это как «шлагбаум на парковке»: внутри счётчик. Каждый async with уменьшает счётчик на 1, выход — увеличивает. Когда счётчик = 0, все ждут.

sem = asyncio.Semaphore(5)  # максимум 5 одновременных

async def fetch(url):
async with sem:
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
return await resp.text()


ВОТ ТАК МЫ ОГРАНИЧИВАЕМ НАГРУЗКУ НА ВНЕШНИЙ СЕРВИС И НЕ ЛОВИМ 429-ю ошибку!

5️⃣ ЧАСТЫЕ ОШИБКИ, КОТОРЫЕ ВАЛЯТ НА СОБЕСЕДОВАНИИ

Захват блокировки ВНУТРИ проверки — гонка остаётся.
Забыл освободить Lock (не через async with) — дедлок. Все задачи вечно ждут.
Использование Lock вместо Semaphore, когда нужно N одновременных.
Создание нового Lock на каждую задачу — блокировка не работает вообще.

6️⃣ КАК ОТВЕЧАТЬ НА СОБЕСЕДОВАНИИ?

Расскажи так:

«В asyncio гонки возникают из-за переключения контекста в точках await. Lock гарантирует эксклюзивный доступ к ресурсу, а Semaphore — ограничение количества одновременных доступов. Главное — защищать весь критический участок, а не только его часть».

И добавь пример с кэшем — это покажет, что ты понимаешь, а не зазубрил.

А ТЫ УЖЕ ЛОВИЛ ТАКИЕ БАГИ? ДЕЛИСЬ В КОММЕНТАРИЯХ! 👇

#python #asyncio #собеседование #блокировки #senior
ТЫ ДУМАЕШЬ, ЧТО ЗНАЕШЬ SOLID? А СЛАБО ОБЪЯСНИТЬ, ПОЧЕМУ ТВОЙ DJANGO-ПРОЕКТ РАЗВАЛИВАЕТСЯ, КОГДА ТЫ МЕНЯЕШЬ ОДНУ СТРОЧКУ В МОДЕЛИ? 😱

Скорее всего, ты нарушаешь Dependency Inversion Principle (DIP) — принцип инверсии зависимостей. И сегодня мы разберем его так, что на собеседовании ты будешь сыпать терминами и примерами, как сеньор с 10-летним стажем. Поехали!

СУТЬ ПРИНЦИПА ОДНОЙ ФРАЗОЙ

Это правило говорит: НЕ ЗАВИСИ ОТ КОНКРЕТИКИ, ЗАВИСИ ОТ АБСТРАКЦИИ. Твой высокоуровневый код (бизнес-логика) не должен знать, как именно работает низкоуровневый код (база данных, API). Они оба должны общаться через интерфейс (абстракцию).

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

Представь, что твой сервис отправки писем — это розетка. А конкретная реализация (SMTP, Slack, Telegram) — это вилка. Если ты в коде жёстко прописал «шлём через SMTP», то при смене провайдера тебе придётся переписывать всю бизнес-логику. А если ты воткнул переходник (интерфейс), то просто меняешь вилку — и всё работает. 🔌

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

Самый частый анти-паттерн — когда ты в views.py или services.py напрямую дёргаешь модель и её методы. Вот так делать НЕЛЬЗЯ:


# ПЛОХО: жёсткая зависимость от модели
from .models import Order

def send_invoice(order_id):
order = Order.objects.get(id=order_id)
# ... логика, которая знает про поля модели


А теперь — правильный подход. Мы создаём абстракцию (интерфейс) для работы с заказами:


# ХОРОШО: зависим от абстракции
from abc import ABC, abstractmethod

class OrderRepository(ABC):
@abstractmethod
def get_by_id(self, order_id):
pass

class DjangoOrderRepository(OrderRepository):
def get_by_id(self, order_id):
from .models import Order
return Order.objects.get(id=order_id)


Теперь твоя бизнес-логика зависит от интерфейса OrderRepository, а не от конкретной модели. И если завтра ты перейдёшь с PostgreSQL на MongoDB или вообще на внешний API — ты просто создашь новую реализацию интерфейса, и всё продолжит работать! 🚀

КАК ПРИМЕНИТЬ ЭТО В РЕАЛЬНОМ DJANGO-ПРОЕКТЕ?

1. Выдели интерфейсы для работы с данными. Не таскай Model.objects.filter() по всему проекту. Создай репозитории или сервисы.

2. Используй внедрение зависимостей (DI). Передавай зависимости в конструктор или метод, а не создавай их внутри. В Django для этого часто используют django-injector или просто передают зависимости через аргументы функций.

3. Для сложной бизнес-логики — отдельный слой. Не пиши всё во views.py. Вынеси логику в services.py, который будет зависеть от абстракций.

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

Многие путают DIP с Dependency Injection (DI). Запомни: DIP — это принцип (ЧТО делать), а DI — это паттерн (КАК делать). DIP говорит «зависи от абстракций», а DI предлагает способ это реализовать — передавать зависимости извне.

ПОЧЕМУ ЭТО СПАСЁТ ТВОЙ ПРОЕКТ?

Тестируемость. Ты можешь подменить реальный репозиторий на фейковый (mock) в тестах, не трогая базу данных.
Гибкость. Смена технологий становится простой заменой одной реализации на другую.
Читаемость. Код становится чище, потому что бизнес-логика не засорена деталями работы с БД.

ИТОГ

DIP — это не просто абстрактная теория из книжек. Это реальный инструмент, который делает твой код на Django гибким, тестируемым и готовым к изменениям. Начни с малого — вынеси работу с моделями в репозитории, и ты почувствуешь разницу.

А теперь вопрос к тебе: сколько мест в твоём проекте напрямую обращаются к Model.objects? Если больше десяти — пора рефакторить! 🔥

#Python #Django #SOLID #DIP #Собеседование #SeniorPython
ТЫ ДУМАЕШЬ, ЧТО ЗНАЕШЬ ПРО typing.Protocol? А СЛАБО ОБЪЯСНИТЬ ИНТЕРВЬЮЕРУ, ПОЧЕМУ ЭТО НЕ ПРОСТО «ИНТЕРФЕЙС»?

Стоп. Сначала честно ответь: ты когда-нибудь писал свой Protocol? Или только видел в чужом коде и кивал? Если второе — садись, сейчас разложу по полочкам так, что на собеседовании будешь сыпать терминами и примерами, как сеньор с 10-летним стажем.

ПОЧЕМУ ВООБЩЕ ПРОТОКОЛЫ, А НЕ ABC?

Вспомни утиную типизацию: если объект крякает как утка и плавает как утка — значит, это утка. Python — динамический язык, ему плевать на объявленные типы. Но когда код разрастается, хочется ловить ошибки ДО запуска. Тут и приходит на помощь Protocol из модуля typing (Python 3.8+).

Protocol — это способ ОПИСАТЬ структуру объекта, не привязываясь к конкретному классу. Ты говоришь: «Мне нужен объект, у которого есть методы quack() и swim()». И любой класс, у которого эти методы есть, — автоматически удовлетворяет твоему протоколу. Без наследования, без регистрации, без магии.

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

Смотри. Обычно ты пишешь:

class Duck:
def quack(self):
print("Quack!")

def swim(self):
print("Swim!")


Теперь хочешь функцию, которая принимает любую «утку»:

from typing import Protocol

class Quackable(Protocol):
def quack(self) -> None: ...

def swim(self) -> None: ...

def make_it_swim(duck: Quackable) -> None:
duck.quack()
duck.swim()


И ВСЁ! Теперь любой объект с методами quack и swim — валидный аргумент. Неважно, что класс Duck не наследует Quackable. Статический анализатор (mypy, pyright) это проверит. А в рантайме — просто вызовет методы, если они есть.

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

Protocol — это НЕ рантайм-проверка. Если ты передашь объект без нужных методов, Python не упадёт при вызове функции. Ошибка будет только когда ты вызовешь отсутствующий метод. Поэтому Protocol — это инструмент для СТАТИЧЕСКОЙ проверки типов и документации, а не для защиты от дурака в рантайме.

Но есть способ сделать и рантайм-проверку: используй декоратор @runtime_checkable из typing. Тогда можно делать isinstance(obj, MyProtocol). Но помни: он проверяет только НАЛИЧИЕ методов, а не их сигнатуры. Так что не обольщайся.

ГДЕ ЭТО ПРИМЕНЯЕТСЯ В РЕАЛЬНОЙ ЖИЗНИ?

- В библиотеках типа requests: ты можешь описать протокол для объекта, который умеет делать HTTP-запросы, и подсунуть свою реализацию.
- В DI-контейнерах: вместо наследования от абстрактного класса — опиши протокол, и любой класс, который ему соответствует, автоматически подходит.
- В тестах: создай фейковый объект, который удовлетворяет протоколу, и не нужно мокать целый класс.

ОШИБКА, КОТОРАЯ ВАЛИТ ВСЁ НА СОБЕСЕДОВАНИИ

Когда тебя спросят «Чем Protocol отличается от ABC?» — не говори «ABC — это для наследования, а Protocol — для структурной типизации». Это половина правды. Главное отличие: Protocol использует СТРУКТУРНУЮ СОВМЕСТИМОСТЬ, а не номинальную. То есть типы совместимы, если у них одинаковая структура (методы), а не потому что один наследует другому. Это как раз тот случай, когда утка — это не тот, кто объявил себя уткой, а тот, кто крякает.

И ЕЩЁ ОДИН ПОДВОХ

Protocol — это НЕ интерфейс в смысле Java. В Java интерфейс — это контракт, который класс ОБЯЗАН реализовать. В Python Protocol — это просто описание, которое можно использовать для проверки. Если класс не реализует метод — это не ошибка компиляции, а ошибка логики.

ИТОГОВАЯ ШПАРГАЛКА ДЛЯ ИНТЕРВЬЮ

- Protocol — это способ описать структуру объекта.
- Он использует утиную типизацию, но со статической проверкой.
- Для рантайм-проверки используй @runtime_checkable.
- Отличие от ABC: Protocol не требует наследования, а ABC — требует.
- Главный кейс: когда нужно описать «что-то, что умеет делать X», не привязываясь к конкретному классу.

А теперь вопрос к тебе: напиши в комментариях, где ты уже использовал Protocol, или задай вопрос, если осталось непонятно. Разберём всё до дна!
⚡️ ТВОЙ ASYNC КОД ТЕЧЁТ, А ТЫ ДАЖЕ НЕ ЗАМЕТИЛ? ⚡️

Собеседование. Вопрос: «Как реализовать кастомный менеджер контекста для асинхронных операций с БД?»

Ты начинаешь рассказывать про __enter__ и __exit__, а интервьюер хитро прищуривается и ждёт подвоха. И он есть! В async-мире всё иначе.

Давай разберём, как не опозориться и показать глубину.

1️⃣ СИНХРОННЫЙ ПРОТОКОЛ — БАЗА

Обычный контекстный менеджер строится на двух магических методах:
• __enter__ — захват ресурса (открыть файл, подключиться к БД)
• __exit__ — гарантированная очистка (закрыть, откатить транзакцию)

Примерно так:

class SyncDB:
def __enter__(self):
self.conn = create_connection()
return self.conn

def __exit__(self, exc_type, exc_val, exc_tb):
self.conn.close()


Всё просто. Но это работает только для синхронного кода. А если мы работаем с async-драйвером? Попробуй вызвать await внутри __enter__ — и получишь SyntaxError! Python не позволит.

2️⃣ АСИНХРОННЫЙ ПРОТОКОЛ — ДВА НОВЫХ МЕТОДА

Для асинхронщины придумали отдельный протокол. Вместо __enter__ и __exit__ используются:
• __aenter__
• __aexit__

Их сигнатура почти такая же, но они должны быть корутинами (async def). И использовать их можно только через конструкцию async with.

Вот как выглядит правильный менеджер для async-БД:

class AsyncDB:
async def __aenter__(self):
self.conn = await create_async_connection()
return self.conn

async def __aexit__(self, exc_type, exc_val, exc_tb):
await self.conn.close()


Ключевые моменты:
• Оба метода — корутины
• Возвращаемое значение __aenter__ попадает в переменную после as
• __aexit__ получает информацию об исключении (тип, значение, traceback)

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

Когда ты пишешь:

async with AsyncDB() as db:
await db.query(...)


Интерпретатор делает следующее:
1. Вызывает await AsyncDB().__aenter__()
2. Присваивает результат переменной db
3. Выполняет тело блока
4. Гарантированно вызывает await __aexit__(...) — даже если внутри было исключение

Это аналог try/finally, но с автоматическим управлением ресурсами.

4️⃣ ПОДВОДНЫЕ КАМНИ, КОТОРЫЕ ЛЮБЯТ СПРАШИВАТЬ

🔹 Если в __aexit__ вернуть True — исключение будет «проглочено» и не всплывёт наружу. Вернуть False или None — исключение продолжится. Это мощный инструмент для обработки ошибок, но используй осторожно!

🔹 Не путай __exit__ и __aexit__. Если объект поддерживает оба протокола, то в async with вызовется именно асинхронный вариант. Но лучше не смешивать.

🔹 Для работы с БД часто используют паттерн «транзакция». В __aenter__ открываем соединение, в __aexit__ делаем commit или rollback в зависимости от наличия исключения:

async def __aexit__(self, exc_type, exc_val, exc_tb):
if exc_type is None:
await self.conn.commit()
else:
await self.conn.rollback()
await self.conn.close()


5️⃣ БОНУС: КОНТЕКСТНЫЙ МЕНЕДЖЕР-ДЕКОРАТОР

Если не хочешь писать класс целиком, используй @asynccontextmanager из contextlib:

from contextlib import asynccontextmanager

@asynccontextmanager
async def db_session():
conn = await create_async_connection()
try:
yield conn
finally:
await conn.close()


Это лаконичнее, и код читается легче. Но на собеседовании лучше сначала показать класс — это демонстрирует понимание протокола.

ИТОГ: Запомни два слова — __aenter__ и __aexit__. Расскажи про их асинхронность, про обработку исключений через аргументы __aexit__, и про @asynccontextmanager как альтернативу. Тогда вопрос закрыт!

А теперь проверь себя: какой метод вызывается, если внутри async with возникло исключение? Пиши ответ в комментариях! 👇
🚨 ТВОЙ 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. Это инструмент для безопасного и понятного кода. На собеседовании объяснишь не термин, а суть. Удачи!