Вопросы на собеседовании Python-разработчика с ответами
Открытая подборка для подготовки к Python-интервью: начните со своего уровня, отвечайте вслух и проверяйте выводы на небольших примерах. Уровни обозначают глубину обсуждения, а не универсальные требования всех работодателей.
CPython 3.12; SQL-примеры проверяются отдельно на SQLite/PostgreSQL с указанными ограничениями.
Вопросы и ответы
Junior: основы
КоллекцииЧем список отличается от кортежа?
Список позволяет менять состав элементов: добавлять, удалять и заменять значения. Кортеж фиксирует последовательность ссылок после создания. При этом объект внутри кортежа может оставаться изменяемым: кортеж со списком не становится глубоко неизменяемым. Выбор определяется смыслом данных: последовательность результатов удобно накапливать в списке, а фиксированную пару координат — возвращать кортежем. Хешируемость кортежа зависит от всех его элементов, поэтому не каждый кортеж подходит для ключа словаря.
- Пример
- Пара (10, 20) может быть ключом словаря; ([10], 20) содержит список и не хешируется.
- Частая ошибка
- Считать, что tuple автоматически делает все вложенные объекты неизменяемыми.
- Уточнение интервьюера
- Что произойдёт при изменении списка внутри кортежа?
ОбъектыЧто означает присваивание в Python?
Имя связывается с объектом, а не получает независимую копию его содержимого. После b = a оба имени могут указывать на один список. Изменение этого списка через любое имя видно через другое; перепривязка b к новому объекту не меняет a. Для корректного ответа полезно нарисовать имена и ссылки. Именно различие между изменением объекта и перепривязкой имени объясняет многие ошибки с аргументами функций и общими настройками.
- Пример
- После a = [1]; b = a; b.append(2) оба имени показывают [1, 2].
- Частая ошибка
- Ожидать копирование списка при обычном присваивании.
- Уточнение интервьюера
- Чем shallow copy отличается от deepcopy для вложенной структуры?
СравнениеЧем is отличается от ==?
Оператор is проверяет, ссылаются ли выражения на один объект, а == вызывает сравнение значений. Для None используют is None: здесь нужна проверка специального единственного объекта. Равные списки могут быть разными объектами. Результаты is для небольших чисел или строк могут зависеть от интернирования и реализации, поэтому на них нельзя строить проверку пользовательских данных.
- Пример
- Два отдельно созданных списка [1, 2] равны через ==, но не обязаны совпадать через is.
- Частая ошибка
- Использовать is для сравнения строк, потому что короткий пример однажды сработал.
- Уточнение интервьюера
- Какие методы класса участвуют в проверке равенства?
ФункцииПочему изменяемый аргумент по умолчанию опасен?
Значение аргумента по умолчанию вычисляется при определении функции. Один и тот же список затем используется в разных вызовах, если вызывающий не передал свой аргумент. Если функция добавляет элементы, состояние неожиданно накапливается. Обычное решение — None в сигнатуре и создание нового списка внутри вызова. При этом нужно явно договориться, что None не является самостоятельным допустимым значением задачи.
- Пример
- def collect(value, items=None): создаёт items = [] только при items is None.
- Частая ошибка
- Очищать общий список в конце функции: исключение может оставить в нём данные.
- Уточнение интервьюера
- Как задать отдельный sentinel, если None — разрешённое значение?
КоллекцииКак безопасно получить значение из словаря?
Обращение d[key] сигнализирует об отсутствии ключа через KeyError и подходит, когда ключ обязателен. Метод get возвращает заданное значение по умолчанию и удобен для необязательных данных. Важно отличать отсутствие ключа от сохранённого None: одного get без sentinel для этого недостаточно. Если нужно проверить только наличие, используйте key in d. Выбор операции должен отражать контракт входных данных, а не желание спрятать любую ошибку.
- Пример
- Для обязательного user_id отсутствие ключа — ошибка входного сообщения; необязательный язык может иметь значение по умолчанию.
- Частая ошибка
- Заменять все обращения на get и затем получать ошибку значительно дальше по коду.
- Уточнение интервьюера
- Когда полезны defaultdict и setdefault?
ИсключенияКак обрабатывать исключения без потери причины?
Перехватывайте исключения там, где можете восстановиться или добавить полезный контекст. Выбирайте конкретный тип вместо безусловного except. Внутри обработчика bare raise пробрасывает текущую ошибку; raise NewError(...) from exc явно связывает новую ошибку с причиной. Освобождение ресурса обеспечивают finally или контекстный менеджер. Логировать одну ошибку на каждом уровне обычно не нужно: это создаёт несколько сообщений об одном сбое.
- Пример
- Ошибку разбора даты можно преобразовать в понятную ошибку поля, сохранив исходный ValueError как причину.
- Частая ошибка
- Возвращать None при любой ошибке сети и трактовать это как отсутствие записи.
- Уточнение интервьюера
- Чем отличаются else и finally у try?
ПоследовательностиКогда использовать comprehension?
Comprehension хорошо подходит для короткого преобразования или фильтрации последовательности. Список создаётся целиком, а генераторное выражение выдаёт значения по запросу. Несколько вложенных циклов, побочные эффекты и сложные ветвления снижают читаемость; в таком случае обычный цикл проще проверить. Решение зависит и от объёма данных: генератор может сократить дополнительную память, но не отменяет стоимость вычисления каждого элемента.
- Пример
- [x * x for x in values if x >= 0] выбирает неотрицательные значения и строит список квадратов.
- Частая ошибка
- Использовать comprehension только ради вызова функции с побочным эффектом.
- Уточнение интервьюера
- Когда генераторное выражение не уменьшит общую память программы?
ТестированиеКакие проверки нужны простой функции?
Начните с контракта: допустимые входы, результат и поведение при ошибке. Затем проверьте обычный случай, границы, пустые данные и особые значения. Важно проверять наблюдаемое поведение, а не переписывать алгоритм в тесте. Для коллекций отдельно уточняют порядок, дубликаты и изменение входа. Негативный тест полезен только при определённом ожидаемом отказе: произвольное исключение не подтверждает правильную валидацию.
- Пример
- Для удаления повторов проверьте пустой список, все одинаковые элементы и сохранение порядка первого появления.
- Частая ошибка
- Проверить один счастливый случай и считать, что контракт покрыт.
- Уточнение интервьюера
- Что стоит параметризовать, а что оставить отдельным тестом?
Middle: применение
ИтераторыЧем iterable отличается от iterator?
Итерируемый объект может предоставить итератор через iter(). Итератор хранит состояние обхода и отдаёт очередное значение через next(), пока не возникнет StopIteration. Список обычно позволяет начать новый обход, а конкретный генератор после исчерпания нельзя просто запустить заново. Передавая итератор в библиотечную функцию, выясните, будет ли она потреблять его полностью: повторное чтение может вернуть пустой результат.
- Пример
- После list(iterator) тот же iterator обычно уже исчерпан, тогда как исходный список остаётся доступным.
- Частая ошибка
- Дважды обходить генератор, ожидая одинаковые данные.
- Уточнение интервьюера
- Как сделать объект, у которого каждый iter() начинает новый обход?
ГенераторыЧто сохраняет генератор между yield?
Генератор приостанавливает выполнение функции и сохраняет локальное состояние, необходимое для продолжения. Это позволяет обрабатывать элементы постепенно, не собирая весь результат. Ленивость переносит и ошибки: исключение может возникнуть при следующем next(), а не при создании генератора. Если генератор удерживает файл или соединение, нужно продумать своевременное закрытие, особенно при досрочном прекращении обхода.
- Пример
- Чтение строк большого файла генератором не требует загрузить весь файл в список.
- Частая ошибка
- Считать ленивое выражение уже выполненной проверкой данных.
- Уточнение интервьюера
- Как досрочное завершение потребителя влияет на освобождение ресурса?
ФункцииКак работает декоратор и зачем wraps?
Декоратор принимает функцию и возвращает объект, который будет использоваться под её именем. Обёртка может добавлять измерение времени, проверку доступа или повтор запроса, но должна сохранять контракт аргументов и результата. functools.wraps переносит важные метаданные исходной функции и устанавливает ссылку __wrapped__. Для async-функции обёртка тоже должна корректно ожидать результат; обычная синхронная обёртка не делает сетевой вызов асинхронным.
- Пример
- Декоратор измерения времени должен использовать try/finally, если нужно учитывать также вызовы с исключением.
- Частая ошибка
- Терять имя и документацию функции либо проглатывать её исключения.
- Уточнение интервьюера
- Как меняется порядок работы двух декораторов?
РесурсыЧто гарантирует with?
Контекстный менеджер задаёт вход и выход из области использования ресурса. При штатном выполнении и исключении Python вызывает выход, что помогает закрыть файл, снять блокировку или завершить транзакцию. Это не обещание восстановления после аварийного завершения процесса. Метод __exit__ может подавить исключение, если вернёт истинное значение, поэтому такое поведение должно быть осознанным. Для асинхронного освобождения используют async with.
- Пример
- with open(...) закрывает файл и при ошибке разбора строки внутри блока.
- Частая ошибка
- Думать, что with автоматически делает все операции атомарными.
- Уточнение интервьюера
- Чем менеджер транзакции отличается от менеджера соединения?
ТипизацияПроверяются ли аннотации типов во время исполнения?
Обычные аннотации сами по себе не запрещают передать значение другого типа. Их используют статические анализаторы, редактор и библиотеки, которые явно выполняют валидацию. Статическая проверка помогает находить несогласованные контракты, но не заменяет проверку внешнего JSON или данных из базы. На собеседовании полезно разделить три уровня: подсказки типов, runtime-проверки и ограничения предметной области.
- Пример
- Параметр age: int не запрещает отрицательный возраст и сам по себе не отклоняет строку.
- Частая ошибка
- Считать успешную проверку типов доказательством корректности внешнего API payload.
- Уточнение интервьюера
- Когда использовать Protocol вместо наследования от общего класса?
АсинхронностьПочему async def не делает любой код неблокирующим?
Корутина отдаёт управление event loop на операциях ожидания, которые действительно могут приостановить выполнение. Синхронное чтение сети, time.sleep и длительные вычисления внутри async def продолжают блокировать поток цикла. Для I/O нужен асинхронный клиент либо перенос блокирующего вызова в отдельный поток. Для тяжёлого CPU-кода отдельно оценивают процессы и специализированные библиотеки. Много задач не означает бесконечную пропускную способность внешнего сервиса.
- Пример
- Замена time.sleep на await asyncio.sleep позволяет другим задачам выполнять работу во время ожидания.
- Частая ошибка
- Вызывать requests внутри async handler и ожидать конкурентной обработки запросов.
- Уточнение интервьюера
- Как ограничить число одновременно выполняемых запросов?
SQLЧем LEFT JOIN отличается от INNER JOIN?
INNER JOIN оставляет только совпавшие пары. LEFT JOIN сохраняет строки левой таблицы, подставляя NULL справа, если совпадения нет. Условие на правую таблицу в WHERE может затем удалить эти строки и фактически изменить ожидаемую семантику. При связи один-ко-многим результат содержит несколько строк для одного объекта слева. Поэтому до агрегации нужно понимать кардинальность и отдельно решить, считать строки, объекты или ненулевые значения.
- Пример
- Для списка всех пользователей с числом заказов нужен LEFT JOIN и COUNT(order.id), а не COUNT(*).
- Частая ошибка
- Переносить фильтр правой таблицы из ON в WHERE без проверки строк с NULL.
- Уточнение интервьюера
- Как выбрать пользователей, у которых нет ни одного заказа?
ОбъектыКогда поверхностной копии недостаточно?
Поверхностная копия создаёт новый внешний контейнер, но сохраняет ссылки на вложенные объекты. Это достаточно, если вложенные значения неизменяемы или ими осознанно делятся. При независимом редактировании вложенного списка такая копия неожиданно меняет и оригинал. deepcopy копирует граф глубже, однако не решает автоматически вопросы внешних ресурсов, идентичности и стоимости. Иногда лучше явное построение новой доменной структуры.
- Пример
- У copy = [row[:] for row in matrix] независимы строки, но более глубокие вложения всё ещё требуют анализа.
- Частая ошибка
- Применять deepcopy ко всему объекту сервиса вместо выяснения владельца состояния.
- Уточнение интервьюера
- Как избежать копирования большой структуры при небольшой правке?
Senior: компромиссы и надёжность
ПараллелизмЧто ограничивает GIL в обычном CPython 3.12?
В стандартном CPython 3.12 GIL ограничивает одновременное исполнение Python-кода потоками одного интерпретатора. Поэтому увеличение числа потоков обычно не ускоряет чисто вычислительный Python-цикл. При ожидании I/O и в некоторых расширениях блокировка может освобождаться, так что потоки остаются полезны. Это свойство конкретной реализации и версии: его нельзя переносить на все Python-интерпретаторы или специальные сборки. GIL также не заменяет синхронизацию многосоставных операций приложения.
- Пример
- Два потока, загружающие файлы, могут перекрывать ожидание; два Python-цикла обработки чисел требуют отдельного измерения.
- Частая ошибка
- Объявлять весь код потокобезопасным только из-за GIL.
- Уточнение интервьюера
- Когда выбирать процессы, потоки или asyncio?
АсинхронностьКак проектировать отмену группы задач?
Отмена — часть жизненного цикла операции. Задача должна корректно освободить ресурсы в finally и обычно пробросить CancelledError дальше. При использовании TaskGroup ошибка одной дочерней задачи приводит к отмене остальных и ожиданию их завершения. Важно различать таймаут одной попытки, всей операции и внешнюю отмену запроса. После частичного внешнего эффекта повтор может быть небезопасен; здесь нужны идентификатор операции и идемпотентность.
- Пример
- Если отправка оплаты завершилась на стороне провайдера, таймаут ответа не разрешает повторить списание без проверки.
- Частая ошибка
- Проглатывать отмену общим обработчиком и оставлять фоновые операции работать.
- Уточнение интервьюера
- Как определить общий дедлайн нескольких последовательных запросов?
ПроизводительностьКак искать причину медленного Python-сервиса?
Сначала определите измеряемый симптом: задержка, CPU, память, число ошибок или длина очереди. Затем отделите ожидание базы и сети от вычислений и проверьте профиль под похожей нагрузкой. Оптимизация одной функции может не изменить время запроса, если узкое место — N+1 запросов или ограничение соединений. После изменения сравните распределение задержек и потребление ресурсов, а не только самый удачный запуск. Корректность результата остаётся отдельным критерием.
- Пример
- Ускорение сериализации на 2 мс мало поможет запросу, который 800 мс ждёт базу данных.
- Частая ошибка
- Начинать с переписывания всего на async без измерения причины.
- Уточнение интервьюера
- Как проверить, что кеш не изменил консистентность данных?
НадёжностьКак сделать повторяемую обработку задания безопасной?
Предположите, что сообщение может прийти повторно, а процесс — завершиться между внешним эффектом и подтверждением обработки. Выберите стабильный идентификатор операции и ограничение уникальности в надёжном хранилище. Изменение бизнес-состояния и запись исходящего события объединяйте транзакцией, если они в одной базе. Внешний сервис требует собственного механизма идемпотентности или сверки состояния. Одна проверка существования перед вставкой не защищает от двух конкурентных обработчиков.
- Пример
- Два запроса с одним ключом заказа должны возвращать один результат, а не создавать два списания.
- Частая ошибка
- Обещать exactly once только потому, что consumer подтверждает сообщения.
- Уточнение интервьюера
- Как обработать неопределённый результат внешнего HTTP-запроса?
Практические задания
1. Убрать повторы с сохранением порядка
Для списка хешируемых значений вернуть первое появление каждого элемента. Вход не изменять.
Ожидаемый результат. [3, 1, 3, 2, 1] → [3, 1, 2]; [] → [].
Решение и объяснение
Множество хранит уже встреченные значения, список — результат в исходном порядке. Ожидаемое время O(n), дополнительная память O(k). Ограничение хешируемости — часть контракта.
def unique_in_order(values):
seen = set()
result = []
for value in values:
if value not in seen:
seen.add(value)
result.append(value)
return result
assert unique_in_order([3, 1, 3, 2, 1]) == [3, 1, 2]
assert unique_in_order([]) == []
assert unique_in_order([0, 0]) == [0]2. Лениво выдавать ограниченные серии
Написать генератор batches(iterable, size), выдающий списки не длиннее size. Последний неполный блок сохранить; size должен быть положительным.
Ожидаемый результат. range(5), size=2 → [[0, 1], [2, 3], [4]]. Пустой вход не даёт блоков.
Решение и объяснение
Накапливаем только текущий блок. Валидация в функции-генераторе выполняется при первом чтении. Дополнительная память O(size), сам источник не материализуется.
def batches(values, size):
if size <= 0:
raise ValueError('size must be positive')
batch = []
for value in values:
batch.append(value)
if len(batch) == size:
yield batch
batch = []
if batch:
yield batch
assert list(batches(range(5), 2)) == [[0, 1], [2, 3], [4]]
assert list(batches([], 2)) == []3. Ограничить конкурентность операций
Выполнить асинхронный обработчик для каждого значения, одновременно не более limit вызовов. Сохранить порядок результатов и отменять группу при ошибке.
Ожидаемый результат. Результаты идут в порядке входа; одновременно внутри обработчика не больше limit операций.
Решение и объяснение
Semaphore ограничивает активную работу, TaskGroup управляет жизненным циклом дочерних задач. Этот пример создаёт задачу для каждого входа, поэтому для огромного потока нужен пул работников с ограниченной очередью.
import asyncio
async def bounded_map(values, handler, limit):
if limit < 1:
raise ValueError('limit must be positive')
semaphore = asyncio.Semaphore(limit)
async def run(value):
async with semaphore:
return await handler(value)
async with asyncio.TaskGroup() as group:
tasks = [group.create_task(run(value)) for value in values]
return [task.result() for task in tasks]План подготовки на семь дней
- Выпишите требования вакансии и отметьте знакомые и незнакомые темы.
- Ответьте на вопросы Junior вслух, затем проверьте себя по объяснениям.
- Разберите вопросы Middle. Для каждой ошибки запишите свой небольшой пример.
- Решите практические задания без подсказок и проверьте граничные случаи.
- Разберите вопросы Senior и объясните компромиссы применительно к своему проекту.
- Проведите пробное интервью: уточняйте условия, рассуждайте и проверяйте выводы.
- Повторите ошибки, подготовьте реальные истории опыта и проверьте технику.