Это техническое собеседование Senior Python-разработчика. Компания и личные данные участников не влияют на разбор: нас интересует, как кандидат рассказывает об опыте, выбирает модель конкурентности и защищает данные от гонок.
Как Senior Python-разработчику рассказать о себе?
Короткий ответ: назовите текущую роль, область ответственности, один сильный кейс и реальный результат. На это достаточно 60–90 секунд.
Вместо длинной биографии можно сказать: «Я отвечаю за backend сервиса с такой-то нагрузкой. В последнем проекте устранил конкретное узкое место: изменил архитектуру, проверил результат по такой-то метрике. Сейчас ищу задачи большего масштаба». Цифры добавляем только те, которые действительно измеряли.
Причину поиска работы тоже лучше формулировать через следующий шаг: какие задачи и ответственность нужны сейчас. Подробности, не связанные с будущей ролью, рассеивают внимание.
Чем отличаются asyncio, потоки и процессы?
Короткий ответ: asyncio эффективно обслуживает много неблокирующих I/O-операций в event loop. Потоки полезны для блокирующих библиотек и I/O. Процессы обычно выбирают для CPU-нагрузки, которой нужны несколько ядер.
Корутина отдаёт управление в точке await; пока она ждёт сеть или БД, цикл запускает другую задачу. Если внутри корутины выполнить тяжёлый синхронный расчёт, остановятся и остальные задачи этого event loop. Механика описана в документации Python по asyncio.
В стандартной сборке CPython GIL ограничивает параллельное выполнение Python-кода в потоках. Но ответ не стоит превращать в абсолютное правило: существуют free-threaded сборки, а библиотеки на C могут освобождать GIL. Поэтому сначала называем характер нагрузки и только затем инструмент.
| Задача | Базовый выбор | Почему |
|---|---|---|
| Тысячи HTTP-запросов через async-клиент | asyncio |
Ожидание не блокирует event loop |
| Синхронный SDK внутри async-сервиса | Пул потоков | Изолирует блокирующий вызов |
| Тяжёлое вычисление на Python | Пул процессов | Использует несколько процессов и ядер |
Как ограничить запросы отдельно для каждого домена?
Короткий ответ: для каждого домена храним собственное состояние rate limiter. Перед запросом worker забирает токен; если токена нет, задача ждёт или безопасно возвращается в отложенную очередь.
Token Bucket пополняется с заданной скоростью и допускает короткий всплеск до размера корзины. Это точнее, чем собирать случайные пачки URL: лимит соблюдается независимо от числа workers.
import time
class TokenBucket:
def __init__(self, rate: float, capacity: int):
# rate — сколько токенов появляется за секунду.
self.rate = rate
self.capacity = capacity
self.tokens = float(capacity)
self.updated_at = time.monotonic()
def try_acquire(self) -> bool:
# Сначала пополняем корзину за прошедшее время.
now = time.monotonic()
elapsed = now - self.updated_at
self.tokens = min(self.capacity, self.tokens + elapsed * self.rate)
self.updated_at = now
# Один токен разрешает выполнить один запрос.
if self.tokens < 1:
return False
self.tokens -= 1
return TrueВ распределённой системе состояние должно быть общим и изменяться атомарно, например в Redis. Ключом становится домен, а значением — токены и время последнего пополнения.
Как связать rate limiter с RabbitMQ?
Короткий ответ: сообщение содержит задачу и домен, consumer пытается получить разрешение, а при временном ограничении переносит работу на более поздний срок без потери и горячего цикла.
У сообщения нужен идентификатор, потому что доставка может повториться. Обработчик должен быть идемпотентным: повтор не создаёт второй результат. Временные ошибки получают ограниченное число попыток и задержку; постоянные уходят в отдельную очередь для разбора. RabbitMQ поддерживает TTL сообщений и dead lettering, что позволяет строить отложенную повторную доставку; детали есть в официальном руководстве.
Важно не удерживать сообщение без подтверждения надолго и не переотправлять его мгновенно бесконечное число раз. Иначе очередь создаст нагрузку сама на себя.
Как защитить баланс от race condition?
Короткий ответ: два запроса могут прочитать один баланс и затереть результат друг друга. Способ защиты зависит от бизнес-проверок.
Если нужно просто прибавить сумму, используем один атомарный запрос: UPDATE accounts SET balance = balance + :delta WHERE id = :id. Если перед списанием надо проверить доступный остаток и связанные условия, читаем строку через SELECT ... FOR UPDATE, меняем её и быстро завершаем транзакцию. PostgreSQL блокирует конкурентное изменение выбранной строки до конца транзакции; поведение описано в документации.
Третий вариант — оптимистическая блокировка: вместе с данными храним версию и обновляем строку только при совпадении старой версии. Если она изменилась, повторяем операцию на свежих данных. Внешний API нельзя вызывать под долгой блокировкой БД: сначала проектируем короткую транзакцию и отдельный надёжный процесс для внешнего действия.
Итог: получил ли кандидат оффер?
Да. Кандидат не дал идеальный ответ на каждое уточнение, но уверенно работал с производственными сценариями, видел риски и развивал решение вместе с интервьюером. Для Senior-позиции это важный сигнал: незнакомая деталь не останавливает рассуждение.
Для подготовки соберите по каждому системному вопросу четыре пункта: нагрузка, состояние, сбой и повтор. Затем проговорите решение вслух и проверьте уточнения в тренажёре собеседований ЮНИКОД.
Источники
частые вопросы
Короткие ответы
Как коротко объяснить разницу между asyncio, потоками и процессами?
Asyncio переключает корутины в точках await и удобно для неблокирующего I/O. Потоки помогают встроить блокирующие вызовы. Процессы дают отдельные интерпретаторы и подходят для CPU-нагрузки в обычной сборке CPython.
Нужно ли сразу использовать SELECT FOR UPDATE для изменения баланса?
Нет. Для простого изменения на фиксированную сумму часто достаточно атомарного UPDATE. Блокировка строки нужна, когда перед записью требуется проверить несколько связанных условий.
Получил ли кандидат оффер?
Да. Кандидат получил оффер: интервьюер оценил практический опыт, системное мышление и способность развивать решение в диалоге.


