Python на собеседовании: разбор вопросов с кодом
Что реально спрашивают на собеседованиях по Python — с короткими ответами и примерами кода: модель данных, ООП, генераторы, память и GIL, асинхронность, инструменты.
miki/python-na-sobesedovanii-razbor-voprosov-s-kodom · v3
Что реально спрашивают на собеседованиях по Python — с короткими ответами и примерами кода: модель данных, ООП, генераторы, память и GIL, асинхронность, инструменты.
Как пользоваться
Это не сборник заученных формулировок, а карта того, что проверяют вопросом. Почти каждый вопрос на собеседовании задаётся не ради термина: спрашивающий хочет понять, держите ли вы в голове модель — как устроены объекты, ссылки и время жизни. Поэтому у ответов здесь есть код: формулировку можно вызубрить, а объяснить, что напечатает пример, — нет.
Если на вопрос хочется ответить одним словом, это почти всегда признак, что модель за словом не стоит.
Типы и модель данных
Какие в Python типы и как они делятся?
Полезнее делить не на «простые и сложные», а на изменяемые и неизменяемые, потому что от этого зависит всё остальное: можно ли положить объект в ключ словаря, что случится при передаче в функцию, будет ли копия копией.
Неизменяемые: int, float, bool, str, bytes, tuple, frozenset, None. Изменяемые: list, dict, set, bytearray, обычные объекты своих классов.
И отдельно: имя в Python — не коробка со значением, а ярлык на объект. a = b не копирует объект, а вешает второй ярлык.
Чем list отличается от tuple?
Не «изменяемый и неизменяемый» — это следствие, а не ответ. Существенная разница в том, что неизменяемость даёт кортежу хешируемость, а значит право быть ключом словаря и элементом множества.
Оговорка, которую любят на собеседовании: кортеж хешируем не всегда — только если хешируемо всё, что внутри.
Что может быть ключом словаря?
Любой объект, у которого есть __hash__ и __eq__, и чей хеш не меняется за время жизни. Отсюда практическое правило: неизменяемые — можно, изменяемые — нельзя.
Обратная сторона, про которую забывают: если вы определили __eq__ в своём классе, Python снимает унаследованный __hash__, и объект перестаёт быть ключом. Возвращать его надо явно.
Отрицательные индексы и слайсы
Отрицательный индекс считает с конца: -1 — последний элемент. Слайс a[start:stop:step] берёт срез, stop не включается, любой из трёх параметров можно опустить.
Важно: слайс списка создаёт новый список, а слайс выходит за границы без ошибки — a[10:20] вернёт [], тогда как a[10] бросит IndexError.
В чём разница между is и ==?
== спрашивает «равны ли значения», is — «это один и тот же объект». Путаница возникает потому, что на маленьких числах и коротких строках is иногда «работает»: CPython держит готовые объекты для небольших целых и интернирует некоторые строки.
Правило без исключений: is — только для None, True, False и проверки идентичности объекта. Во всех остальных случаях ==.
Как проверить, что один кортеж содержит все элементы другого?
Очевидный ответ — через множества:
И сразу ловушка, ради которой вопрос и задают: множества теряют количество. Для a = (1, 2) и b = (1, 1, 2) ответ будет True, хотя двух единиц в a нет. Если количество важно — считайте:
Ссылки, изменяемость, копирование
Аргументы передаются по значению или по ссылке?
Ни то, ни другое в привычном смысле — передаётся ссылка на объект, по значению. Внутри функции имя указывает на тот же объект; присваивание нового значения имени связь разрывает, а изменение объекта на месте видно снаружи.
Почему нельзя писать def f(x=[])?
Значение по умолчанию вычисляется один раз — при определении функции, а не при каждом вызове. Изменяемый объект в умолчании становится общим для всех вызовов.
Правильно — сторож None:
Копия и глубокая копия
copy.copy копирует контейнер, но не то, что внутри: вложенные объекты остаются общими. copy.deepcopy рекурсивно копирует всё дерево (и корректно переживает циклические ссылки).
Deepcopy недёшев: для больших структур это заметная цена, и часто честнее пересобрать объект явно.
Зачем id() и когда он врёт
id(x) возвращает идентичность объекта (в CPython — адрес). Годится, чтобы показать, что два имени указывают на один объект, но как «постоянный номер» не годится: после смерти объекта тот же id может достаться новому.
Именно поэтому is сравнивают только с объектами, которые заведомо живы, — например, с None.
Функции
Что такое lambda и где она уместна
Анонимная функция из одного выражения. Она не делает ничего, чего не делает def; смысл — не заводить имя там, где функция живёт одну строку: ключ сортировки, аргумент map/filter, значение по умолчанию в defaultdict.
Если внутри лямбды появился тернарник в два этажа — это уже def с именем, и читателю станет легче.
*args и **kwargs
В объявлении — «собери остальные позиционные в кортеж» и «остальные именованные в словарь». В вызове те же звёздочки работают наоборот — распаковывают.
Это же ответ на вопрос «как передать необязательные параметры из одной функции в другую»: не перечислять их руками, а пробросить **kwargs — тогда добавление параметра в нижнюю функцию не требует правки верхней.
Функции высшего порядка и замыкания
Функция — обычный объект: её можно передать, вернуть, положить в словарь. Функция, принимающая или возвращающая функцию, и называется функцией высшего порядка (sorted(key=...), map, декораторы).
Замыкание запоминает не значение переменной, а саму переменную. Отсюда классический подвох:
/ и * в сигнатуре
Это способ запретить неудобный способ вызова. Всё до / можно передать только позиционно, всё после * — только по имени.
Зачем: имена позиционных параметров перестают быть частью публичного договора — их можно переименовать, не ломая чужой код.
Что такое pass и что такое docstring
pass — заглушка: синтаксис требует тела, а делать пока нечего. Ничего не выполняет.
Docstring — строковый литерал первой строкой модуля, функции или класса; в отличие от комментария, он остаётся в объекте и доступен через __doc__, help() и подсказки редактора.
ООП
Зачем нужен self и почему он явный
Метод — это обычная функция в теле класса, а self — её первый обычный параметр, куда при вызове через экземпляр подставляется сам экземпляр. Это не ключевое слово — имя можно заменить, хотя так никто не делает.
Ответ «чтобы обратиться к полям экземпляра» верен, но мелок. Точнее: в Python нет неявного контекста вроде this, и связывание видно в сигнатуре.
__init__ и __new__
__new__ создаёт объект и возвращает его; __init__ только настраивает уже созданный и ничего не возвращает. Поэтому для неизменяемых типов и для «один экземпляр на всех» работать надо в __new__.
Засада такого синглтона: __init__ вызовется каждый раз и перезатрёт поля. В большинстве задач честнее модульная переменная или functools.lru_cache на фабрике.
@classmethod, @staticmethod, @property
classmethod получает класс (cls) — главное применение это альтернативные конструкторы, которые правильно работают в наследниках. staticmethod не получает ничего — это просто функция, живущая рядом. property превращает вызов в доступ по атрибуту.
property — это и есть питоновский ответ на вопрос про аксессоры и мутаторы: геттеры пишут не заранее, а тогда, когда за атрибутом появилась логика — внешний код при этом не меняется.
Множественное наследование и MRO
Множественное наследование есть. Порядок поиска метода (MRO) строится линеаризацией C3, а не «слева направо вглубь», и его можно просто посмотреть.
super() идёт не «к родителю», а К СЛЕДУЮЩЕМУ КЛАССУ В MRO текущего экземпляра — именно поэтому в D вызов super() из B попадёт в C, а не в A. Несовместимый порядок основ — ошибка уже на создании класса (TypeError: Cannot create a consistent MRO).
Что хранится в __dict__ и зачем __slots__
Обычный экземпляр держит атрибуты в словаре __dict__. Это удобно (атрибут можно добавить на ходу), но стоит памяти. __slots__ заменяет словарь фиксированным набором слотов.
Берут ради памяти, когда объектов миллионы. Цена: нет динамических атрибутов, сложнее множественное наследование, и если хоть у одного класса в иерархии слотов нет, словарь вернётся и выигрыш испарится.
Магические (dunder) методы
Методы вида __len__, __eq__, __iter__, __enter__ — это точки, через которые ваш объект встраивается в язык. Питон не спрашивает тип — он спрашивает протокол.
Полезная деталь для ответа: bool(b) без __bool__ падает на __len__ — пустой контейнер ложен.
Дескрипторы — и чем они не декораторы
Дескриптор — объект с __get__/__set__, положенный в КЛАСС: он перехватывает доступ к атрибуту. Декоратор же просто оборачивает функцию в момент определения. Родство в том, что property, classmethod и обычные методы сами реализованы как дескрипторы.
Зачем: правило проверки описано ОДИН раз и переиспользуется, а не копируется в каждый сеттер.
Как создать класс без слова class и что такое метакласс
Класс — тоже объект, и его можно создать вызовом type(имя, основы, словарь). Именно это делает инструкция class под капотом.
Метакласс — класс класса: он управляет тем, как класс создаётся. Годится там, где правило надо применить ко всем подклассам сразу: регистрация плагинов, ORM, проверка договора на этапе объявления.
Честный ответ на вопрос «когда применять»: почти никогда. Сегодня большую часть задач закрывают __init_subclass__ и __set_name__ — и это хороший ответ собеседнику.
Monkey patching
Подмена атрибута или метода уже загруженного класса/модуля во время работы программы. Возможно потому, что класс — обычный объект с изменяемым словарём.
Законное применение одно — тесты (unittest.mock.patch делает ровно это и возвращает как было). В боевом коде это правка на расстоянии: читая модуль, невозможно узнать, что его подменили.
NotImplementedError и NotImplemented — разные вещи
NotImplementedError — исключение: его бросают в заготовке метода, который обязан переопределить наследник.
NotImplemented — значение-сигнал, которое возвращают из __eq__, __add__ и прочих, чтобы сказать «я не умею с этим типом, попробуй второй операнд».
Разница не косметическая: вернёте False — и Money(1, "RUB") == чужой_объект никогда не даст второму типу шанса ответить.
Декораторы
Что такое декоратор и как написать свой
Декоратор — функция, которая принимает функцию и возвращает другую. Синтаксис @deco над def f — ровно f = deco(f), ничего больше.
Забытый functools.wraps — частый вопрос на собеседовании: без него функция «переименовывается» в inner, ломая логи и help().
Декоратор с параметрами: повторить N раз
Популярное задание на собеседовании. Суть: декоратор с аргументами — это ещё один уровень вложенности.
Две вещи, которые смотрят отдельно: последняя попытка должна пробрасывать исключение, а не возвращать None, и ловить Exception целиком по умолчанию — спорно.
map, filter, reduce и однострочники
map и filter возвращают ленивые итераторы, reduce живёт в functools и сворачивает последовательность в одно значение.
Счёт заглавных букв в файле одной строкой — и сразу важная оговорка про память:
Здесь генераторное выражение, а не f.read(): файл читается построчно и не заезжает в память целиком. На собеседовании ценят именно эту оговорку, а не сам однострочник.
Итераторы и генераторы
Что такое итератор и чем iter отличается от next
Итерируемый объект умеет отдать итератор (__iter__). Итератор умеет отдать следующий элемент (__next__) и бросить StopIteration, когда кончились. iter(x) и next(it) — встроенные функции, дёргающие эти методы.
Список — итерируемый, но НЕ итератор: по нему можно пройти сколько угодно раз. Итератор одноразовый и исчерпывается.
Генератор и yield
Генератор — самый дешёвый способ написать итератор: функция с yield при вызове не выполняется вовсе, а возвращает объект, который выполняет тело порциями и помнит своё место между вызовами.
Выгода не в красоте, а в памяти: генератор держит один элемент, а не весь результат. Обработка файла на гигабайт через список и через генератор — разница между «не влезло» и «работает».
Что делает return внутри генератора
Любимый вопрос «что напечатается». return в генераторе не возвращает значение вызывающему в привычном смысле — он останавливает итерацию, а значение кладёт в StopIteration.value.
Именно это значение получает yield from — на нём держалась вся до-async корутинная машинерия.
Можно ли «перевернуть» генератор?
Лениво — нет, и это суть ответа. reversed() требует либо __reversed__, либо пары __len__ + __getitem__; у генератора нет ни длины, ни индексов — он вообще может быть бесконечным.
То есть «перевернуть» можно, но только отказавшись от того единственного, ради чего брали генератор.
collections и itertools, которые стоит назвать
Вопрос проверяет не память, а то, решали ли вы ими задачи.
Главная ловушка здесь — itertools.groupby: он группирует только ПОДРЯД идущие элементы, поэтому без предварительной сортировки по тому же ключу результат окажется не тем, которого ждут от SQL-подобного GROUP BY.
Исключения и контекстные менеджеры
Когда выполняется else в try
Ветка else срабатывает, если исключения не было. Смысл — держать в try только опасную строку, а не весь последующий код: иначе except незаметно поймает ошибку не из того места.
Порядок такой: try → except (если была ошибка) или else (если не было) → finally в любом случае.
Что такое контекстный менеджер
Объект с __enter__ и __exit__: язык гарантирует, что второй вызовется даже при исключении. То есть это способ не забыть освободить ресурс.
Деталь для собеседования: если __exit__ вернёт истинное значение, исключение будет подавлено. Это редко то, чего хотят, и частый источник тихих багов.
Почему голый except: — плохо
Он ловит всё, включая KeyboardInterrupt и SystemExit, то есть мешает остановить программу. Разумная граница — except Exception, а лучше конкретный тип.
Отдельно стоит знать про raise ... from e: без него исходная ошибка превращается в безымянное «During handling of the above exception», и причина теряется при разборе инцидента.
Память и GIL
Как устроена память и сборщик мусора
Основной механизм — счётчик ссылок: как только он дошёл до нуля, объект уничтожается немедленно и детерминированно. Отдельный сборщик нужен только для того, что счётчик не видит, — циклов.
Сборщик циклов — поколенческий (три поколения): молодые объекты проверяются часто, выжившие — реже, потому что большинство объектов умирают молодыми.
Почему память не возвращается ОС
Потому что между объектами и malloc есть собственный аллокатор (pymalloc) с аренами и пулами. Освобождённый объект возвращается в пул для повторного использования, и арена уйдёт ОС только когда освободится целиком — а один живой объект держит всю арену.
Практическое следствие, которое видно в мониторинге: RSS процесса после пиковой нагрузки часто так и остаётся высоким. Это не утечка; утечка — когда растёт ПОСТОЯННО, от пика к пику.
И отдельно: интернированные строки и кеш малых целых живут до конца работы интерпретатора по замыслу.
Что такое GIL и почему он всё ещё есть
GIL — один замок на интерпретатор: в каждый момент байткод исполняет только один поток. Он защищает счётчики ссылок от гонок — и делает однопоточный код быстрее, чем сделала бы тонкая блокировка каждого объекта.
Что из этого следует на практике:
- задачи, упирающиеся в процессор (CPU-bound), потоками НЕ ускоряются — нужны процессы;
- задачи, ждущие ввода-вывода, потоками ускоряются отлично: на время ожидания GIL отпускается;
- расширения на C (NumPy и прочие) отпускают GIL на тяжёлых участках.
Актуальное дополнение, которое стоит назвать: в CPython 3.13 появилась сборка без GIL (free-threading, PEP 703) — пока отдельная и не везде выгодная, но направление задано.
Потоки, процессы, async — что когда
Вопрос про разницу между многопоточным и многопроцессным приложением проверяет не определения, а выбор.
| Что делаем | Чем |
|---|---|
| ждём сеть/диск, сотни ожиданий | asyncio |
| ждём ввод-вывод, но библиотека синхронная | ThreadPoolExecutor |
| считаем числа своим кодом | ProcessPoolExecutor |
| считаем числа чужой C-библиотекой | потоки тоже годятся |
Процессы не делят память: данные едут через pickle, и большие аргументы могут съесть весь выигрыш. Это хорошее дополнение к ответу.
Утечка памяти — с чего начать отладку
Ответ «посмотреть в профайлере» слабый. Сильный — назвать порядок.
Дальше по убыванию частоты причин: глобальный список/кеш без границы, замыкание или обратный вызов, держащий большой объект, цикл с __del__, расширение на C. gc.get_objects() и objgraph помогают увидеть, КТО держит ссылку.
Производительность
Как ускорить код на Python
Правильный ответ начинается не с приёма, а с замера: без профиля ускоряют не то место.
Порядок действий по убыванию выгоды: сменить алгоритм и структуру данных (проверка вхождения в set, а не в list) → убрать работу из цикла → взять готовое на C (NumPy, re, стандартная библиотека) → кеш (functools.lru_cache) → и только потом PyPy, Cython или расширение.
Классический пример алгоритмической разницы:
Чем массив NumPy лучше списка
Список хранит указатели на разбросанные по памяти объекты, у каждого свой заголовок. Массив NumPy — один непрерывный блок чисел одного типа. Отсюда и память меньше, и операция идёт одним вызовом в C вместо миллиона шагов интерпретатора.
Оговорка: на маленьких данных NumPy проигрывает — накладные расходы на вызов больше самой работы. Выгода появляется на больших однородных массивах.
Почему в Python нет оптимизации хвостовой рекурсии
Это решение, а не недоработка. Гвидо ван Россум возражал против неё главным образом потому, что сворачивание кадров ломает трейсбек: стек вызовов — главный инструмент отладки, и менять его на экономию кадров не стоит.
Практическое следствие: глубокая рекурсия в Python — путь к RecursionError (по умолчанию предел около 1000 кадров). Рекурсию переписывают циклом или явным стеком.
Модули, пакеты, области видимости
Чем пакет отличается от модуля
Модуль — один файл .py. Пакет — каталог с модулями; файл __init__.py делает его обычным пакетом и исполняется при импорте. С PEP 420 каталог без __init__.py тоже импортируется — как namespace-пакет.
Собрать свой пакет сегодня — это pyproject.toml и одна команда:
Упоминание setup.py в ответе сегодня звучит как старый опыт: он работает, но декларативный pyproject.toml — текущая форма.
wheel и egg, менеджеры пакетов
wheel (.whl) — современный формат сборки: архив, который ставится распаковкой, без выполнения кода сборки на машине пользователя. egg — устаревший формат setuptools, в новом коде его не используют.
Про бинарные зависимости: именно для них есть теги совместимости вроде manylinux — колёсо собирают под конкретную платформу и версию Python, и pip выбирает подходящее.
Менеджеры: pip с venv — база; poetry и uv держат lock-файл, то есть воспроизводимую среду; conda решает ещё и не-Python зависимости.
Транзитивные зависимости — те, что пришли с чужими пакетами. Управляют ими не верой в requirements.txt, а lock-файлом и проверкой уязвимостей в CI.
Правило LEGB, global и nonlocal
Имя ищется в четырёх областях по порядку: Local → Enclosing → Global → Built-in. globals() и locals() показывают соответствующие словари.
Без global/nonlocal присваивание внутри функции делает имя ЛОКАЛЬНЫМ во всём теле — отсюда знаменитое UnboundLocalError на чтении до присваивания.
Глобальные переменные как способ общения между частями программы — плохая идея: невозможно сказать, кто их меняет, и тесты начинают влиять друг на друга.
«Статическая переменная» внутри функции
Отдельного static, как в C, нет — но состояние между вызовами держать можно несколькими способами, и выбор говорит о вкусе.
Перезагрузка модуля, __pycache__ и компиляция
Импорт выполняет модуль ОДИН раз и кладёт в sys.modules; повторный import берёт готовое. Перезагрузить можно так:
Но старые объекты, созданные до перезагрузки, продолжают ссылаться на прежние классы, и from mymod import f тоже держит старую функцию. Поэтому reload — инструмент REPL, а не способ обновлять боевой процесс.
Про компиляцию: исходник компилируется в байткод (.pyc в __pycache__), а байткод исполняет виртуальная машина. Кеш связан с версией интерпретатора и обновляется сам; удаление __pycache__ безопасно. Посмотреть байткод можно так:
Виртуальное окружение и переменные среды
Виртуальное окружение — отдельный каталог со своим site-packages, чтобы зависимости проектов не смешивались.
Переменные среды, про которые спрашивают:
PYTHONPATH— добавляет каталоги вsys.path;PYTHONDONTWRITEBYTECODE— не писать.pyc;PYTHONHASHSEED— фиксирует соль хеша строк (воспроизводимость в тестах);PYTHONOPTIMIZE(или флаг-O) — выключаетassertи__debug__.
Последнее важнее, чем кажется: если проверка прав или валидация написаны через assert, под -O они исчезнут целиком. Файлы .pth в site-packages — ещё один способ добавить пути в sys.path (им пользуются установки в режиме editable).
Асинхронность
Синхронный и асинхронный код
Синхронный код ждёт завершения каждой операции, ничего не делая. Асинхронный на время ожидания отдаёт управление циклу событий, и тот занимает поток другой задачей. Это один поток — не параллелизм, а умение не простаивать.
Главное правило и главная ошибка новичка: один синхронный time.sleep или тяжёлый расчёт внутри корутины блокирует ВЕСЬ цикл. Такое выносят в asyncio.to_thread или в пул процессов.
Инструменты и окружение
Проверка кода: синтаксис, статический анализ, отладчик
Синтаксис без запуска:
Статический анализ сегодня — это ruff (линтер и форматтер сразу), mypy или pyright (типы), bandit (безопасность). Назвать только pylint сейчас выглядит устаревшим ответом.
Отладчик встроен: breakpoint() в любой строке остановит выполнение и даст pdb.
Скрипт делают запускаемым не «чем-то одним», а по-разному на разных ОС: shebang и chmod +x в Unix, py-лончер в Windows, а для раздачи — точка входа в pyproject.toml ([project.scripts]) или PyInstaller.
CPython, PyPy, Cython, IronPython — зачем разные
CPython — эталонная реализация на C, та самая, что скачивается с python.org. PyPy — другая реализация с JIT: долго живущий вычислительный код может ускориться в разы, но часть C-расширений работает хуже.
Cython — не реализация, а компилятор в C: на нём пишут горячие участки и связки с C-библиотеками. IronPython и Jython — для .NET и JVM, сегодня экзотика.
Вызвать C из Python можно и без компилятора:
А современный способ связать быстрый язык с Python — cffi или PyO3 (Rust).
Регулярные выражения и проверка email
Регулярки в re, компиляция кешируется, но в горячем коде шаблон лучше собрать заранее через re.compile.
Задание «напишите регексп для email» — ловушка на самоуверенность. Полный разбор по RFC 5322 на регулярках чудовищен; сильный ответ — назвать границу применимости.
Смысл такой проверки — отсечь опечатки в форме, а не доказать существование адреса. Единственная настоящая проверка — отправленное письмо с подтверждением.
Чем фреймворк отличается от библиотеки и когда Python — правильный выбор
Библиотеку вызываете вы; фреймворк вызывает вас — он владеет потоком управления, а вы дописываете куски в его точки расширения. Поэтому библиотеку легко заменить, а фреймворк — архитектурное решение надолго.
Рядом часто спрашивают про NumPy и SciPy: NumPy — массив и базовая арифметика, SciPy — надстройка с численными методами (оптимизация, статистика, сигналы) поверх него.
А на вопрос «когда Python — правильный выбор» честный ответ звучит так: когда узкое место — время разработчика, ввод-вывод или готовая экосистема (данные, ML, скрипты, веб), а не микросекунды на ядро или жёсткий реалтайм.
Связанные списки
Настройка среды с нуля на Windows: редактор, расширения, терминал, Python, git, Docker, uv. К концу — папка проекта, готовая принять первую строку кода.
Плоская, слоистая, по фичам, гексагональная, чистая архитектура и модульный монолит. Что за каждой стоит, где она ломается и как выбрать под свой проект, а не под моду.
Разбираем каждую папку по отдельности: за что отвечает, что в неё класть нельзя и почему слои зависят друг от друга только в одну сторону. Создаём дерево до появления кода.
