Ползём в сеньоры. Вопросы к собеседованию на сеньора Python.
2 subscribers
5 links
Подготовка какие вопросы могут задавать для сеньора по python и framework
Download Telegram
Твой сервис лежит на проде, CPU в потолке, а ты не знаешь, какой участок кода виноват! Останавливать приложение для профилирования нельзя — пользователи разбегутся. Что делать? НАДО ЗВАТЬ py-spy!

Это твой личный шпион, который проникает в работающий процесс и подсматривает, чем занят интерпретатор. И ему НЕ НУЖНО останавливать твой код! Он работает в режиме чтения, как сисадмин, который тихо смотрит через плечо программиста.

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

Python — интерпретируемый язык. CPython выполняет байт-код в стековой виртуальной машине. py-spy подключается к процессу и снимает «снимок» стека вызовов каждого потока. Он делает это много раз, собирает статистику и показывает, где твой код проводит больше всего времени.

Главная фишка — py-spy использует системный вызов process_vm_readv на Linux, чтобы читать память другого процесса. Он НЕ внедряется внутрь, как отладчик, а просто читает данные. Поэтому твой код продолжает работать как ни в чём не бывало!

КАК ПОЛЬЗОВАТЬСЯ?

Установка простая:

pip install py-spy


А теперь представь: у тебя есть процесс с PID 12345, который жрёт весь CPU. Снимаем «фотографию» стека:

py-spy dump --pid 12345


Ты увидишь что-то вроде:

Thread 0x7f... (idle):
File "app.py", line 42, in process_data
result = heavy_computation(data)
File "utils.py", line 15, in heavy_computation
return [x * x for x in data]


Вот и виновник! Но это разовый снимок. А если проблема плавающая? Запускаем профилировщик на 10 секунд:

py-spy record --pid 12345 -o profile.svg --duration 10


После этого открой файл profile.svg в браузере. Ты увидишь flame graph — огненную диаграмму, где каждый «язычок пламени» — это функция. Чем шире язычок, тем больше времени в ней проводит программа. Это как тепловизор для кода!

КОГДА ЭТО СПАСАЕТ ЖИЗНЬ?

1. Продакшн-инциденты: когда сервис деградирует, а рестарт — это потерянные данные.
2. Трудноуловимые баги: когда проблема возникает раз в день, и ты не можешь поймать её в тестовой среде.
3. Deadlock: когда программа зависла, py-spy покажет, на каком мьютексе все ждут.
4. GIL-проблемы: если у тебя многопоточное приложение, py-spy покажет, какие потоки борются за глобальную блокировку интерпретатора.

ВАЖНЫЙ НЮАНС!

py-spy не работает на Windows (точнее, работает, но с ограничениями). На Linux и macOS — пожалуйста. Также ему нужны права на чтение памяти процесса, так что запускай от root или того же пользователя, что и процесс.

Ещё момент: py-spy показывает только Python-стеки. Если твой код вызывает C-библиотеку, которая зависла внутри, ты увидишь, что Python ждёт результата, но не увидишь, что происходит внутри C. Для этого уже нужны другие инструменты.

БОНУС ДЛЯ SENIOR'А

py-spy умеет работать с Docker-контейнерами! Если твоё приложение крутится в контейнере, добавь флаг --pid с PID процесса внутри контейнера или используй docker exec:

docker exec -it my_container py-spy dump --pid 1


Но помни: py-spy должен быть установлен ВНУТРИ контейнера или в образе.

И ещё: py-spy — это read-only инструмент. Он не может изменить состояние процесса. Это его главное преимущество и одновременно ограничение. Он только наблюдает.

Так что в следующий раз, когда прод начнёт тормозить, не паникуй и не перезапускай вслепую. Достань свой py-spy и посмотри, что на самом деле происходит!

А ты уже пользовался py-spy на проде? Или предпочитаешь другие инструменты? Расскажи в комментариях! 👇
ПОЧЕМУ ТВОЙ СЕРВИС ПАДАЕТ ПОД НАГРУЗКОЙ, ХОТЯ ТЫ ИСПОЛЬЗУЕШЬ asyncpg? 🤔

Скорее всего, ты открываешь новое соединение к PostgreSQL на каждый запрос! Это как заказывать пиццу с доставкой каждый раз, когда хочется есть. Вроде работает, но дико медленно и дорого. На собеседовании тебя сразу спалят, если не знаешь, как сделать пул. Давай разберём!

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

Пул — это набор заранее созданных соединений к БД, которые переиспользуются. Вместо того чтобы создавать новое подключение на каждый запрос (это затратно: handshake, аутентификация, память), ты берёшь готовое из пула, работаешь и возвращаешь обратно. Экономия времени и ресурсов — колоссальная!

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

asyncpg даёт нам готовый инструмент — create_pool. Это фабрика, которая создаёт пул соединений. Вот базовый пример:


import asyncio
import asyncpg

async def main():
# Создаём пул с минимумом 5 и максимумом 20 соединений
pool = await asyncpg.create_pool(
user='user',
password='password',
database='dbname',
host='localhost',
min_size=5,
max_size=20
)

# Берём соединение из пула и выполняем запрос
async with pool.acquire() as conn:
result = await conn.fetch('SELECT * FROM users')
print(result)

# Закрываем пул, когда он больше не нужен
await pool.close()

asyncio.run(main())


Видишь? Вместо asyncpg.connect() мы используем pool.acquire(). Это ключевой момент! acquire() берёт свободное соединение из пула, а когда мы выходим из блока async with — автоматически возвращает его обратно. Красиво и безопасно!

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

Представь, что к тебе приходит 1000 запросов в секунду. Без пула ты создаёшь 1000 соединений — это убийственно для БД и памяти. С пулом ты держишь, скажем, 20 соединений и просто переиспользуешь их. Быстро и эффективно.

ПОДВОДНЫЕ КАМНИ, О КОТОРЫХ ТЫ ДОЛЖЕН ЗНАТЬ

1. Не закрывай пул раньше времени! Если закроешь пул, а потом попытаешься выполнить запрос — получишь ошибку. Обычно пул создаётся при старте приложения и живёт всё время его работы.

2. Не создавай пул на каждый запрос! Это то же самое, что не иметь пула вообще. Создай его один раз и передавай через зависимости или глобальный объект.

3. Настрой размер пула правильно. Слишком маленький пул — будут очереди, слишком большой — перегрузишь БД. Ориентируйся на нагрузку и лимиты PostgreSQL. Обычно 10-20 соединений достаточно для большинства приложений.

4. Используй timeout в acquire(). Если все соединения заняты, acquire() будет ждать вечно. Добавь таймаут, чтобы не подвесить запрос:


conn = await pool.acquire(timeout=5) # ждём максимум 5 секунд


5. Следи за утечками. Если забудешь вернуть соединение (не используешь async with), оно потеряется. Через какое-то время пул исчерпается, и всё упадёт. Всегда используй async with или try/finally!

А ЧТО НАСЧЁТ ПРОИЗВОДИТЕЛЬНОСТИ?

asyncpg — самый быстрый драйвер для PostgreSQL. Он использует бинарный протокол и подготовленные запросы, что даёт прирост до 3 раз по сравнению с psycopg2. Но пул — это ещё один уровень оптимизации. Вместе они дают мощный тандем.

ИТОГ

Пул соединений — это must-have для любого серьёзного приложения. На собеседовании покажи, что понимаешь, зачем он нужен и как его использовать. Расскажи про create_pool, acquire(), настройку размера и типичные ошибки. Это сразу выделит тебя среди других кандидатов!

А теперь вопрос к тебе: как ты думаешь, что будет, если пул закончился, а все соединения заняты? Ответы в комментариях! 👇
⚡️ ТВОЙ FastAPI-сервер УТЕКАЕТ ресурсами, и ты даже не заметил! 🚨

Собеседование. Вопрос: «Как ты управляешь подключениями к БД в FastAPI?»
Ты: «Ну, создаю подключение в каждом эндпоинте...»
Интервьюер: «А если 1000 запросов в секунду?»
И вот тут ты понимаешь — ты в луже. Почему? Потому что не знаешь про lifespan!

Давай разберёмся, что это и как это тебя спасёт.

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

Lifespan — это «жизненный цикл» твоего приложения. Это код, который выполняется ДО того, как сервер начнёт принимать запросы, и ПОСЛЕ того, как он закончит их обрабатывать. Думай о нём как о церемонии открытия и закрытия Олимпийских игр: сначала зажигаем огонь (готовим ресурсы), потом всё работает, а в конце — гасим огонь и убираем стадион (чистим за собой).

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

Представь: у тебя есть модель машинного обучения, которая весит 2 гигабайта. Загружать её при каждом запросе — это suicide. Загрузить на уровне модуля? Тогда она будет грузиться даже при запуске простого теста, и тесты станут медленными, как черепаха. А вот lifespan позволяет загрузить модель ровно в тот момент, когда сервер реально стартует, и выгрузить, когда он останавливается.

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

Всё гениальное просто. Ты создаёшь асинхронную функцию с yield и помечаешь её @asynccontextmanager. Всё, что написано ДО yield, — это startup-логика. Всё, что ПОСЛЕ — shutdown-логика.

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

from contextlib import asynccontextmanager
from fastapi import FastAPI

ml_models = {}

@asynccontextmanager
async def lifespan(app: FastAPI):
# Это выполнится при старте
ml_models["answer"] = load_heavy_model()
yield
# Это выполнится при остановке
ml_models.clear()

app = FastAPI(lifespan=lifespan)


Внутри lifespan ты можешь открыть пул подключений к базе данных, загрузить модели, создать клиенты Redis — всё, что нужно всему приложению. И всё это будет доступно в эндпоинтах через глобальные переменные или зависимости.

А ЧТО С ДЕПРЕКИРОВАННЫМИ @app.on_event?

Ах да, этот нюанс! Раньше все использовали @app.on_event("startup") и @app.on_event("shutdown"). Но с некоторых пор это считается устаревшим (deprecated). FastAPI рекомендует именно lifespan. Почему? Потому что lifespan — это единый менеджер контекста, который гарантирует, что cleanup выполнится даже при ошибках. События могут «забыть» закрыть ресурсы, если что-то пошло не так.

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

Потому что это показывает, что ты думаешь о продакшене. Что ты понимаешь, как управлять ресурсами, как избегать утечек памяти и как делать приложение масштабируемым. Это уровень Senior, а не Junior, который пишет код «лишь бы работало».

ИТОГ

Lifespan — это твой шанс блеснуть. Запомни:
- Это код до и после обработки запросов.
- Используется для тяжёлых ресурсов: БД, ML-модели, кэши.
- Заменяет устаревшие @app.on_event.

Теперь ты вооружён. Иди и покажи этому интервьюеру, кто тут профи! 💪

А если хочешь ещё больше фишек — подписывайся, дальше будет только интереснее!
ТЫ ДУМАЕШЬ, ЧТО 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).

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

А ты уже использовал сигналы в своих проектах? Или боишься их как огня? Пиши в комментариях! 👇