10 каверзных вопросов по Python с собеседований, на которых валятся даже Middle
Разбор изменяемых аргументов по умолчанию, замыканий, GIL, разницы is vs == и работы генераторов с примерами кода и практическими нюансами.
Собеседования на позицию Middle Python Developer часто начинаются не с проектирования микросервисов или алгоритмов на графах, а с проверки глубокого понимания механики рантайма CPython.
Интервьюеру важно понять: пишет ли кандидат код механически, или он представляет, что происходит с объектами в куче, как резолвятся области видимости и почему код ведёт себя именно так.
В этой статье мы подробно разберем 5 классических вопросов и задач, на которых регулярно спотыкаются даже разработчики с опытом от 2 лет.
1. Изменяемый аргумент по умолчанию#
Один из самых частых «фильтрующих» вопросов на интервью. Рассмотрим следующий фрагмент:
def add_item(item, target_list=[]):
target_list.append(item)
return target_list
print(add_item(1))
print(add_item(2))
Большинство новичков ожидают увидеть два независимых списка [1] и [2]. Однако реальный вывод интерпретатора:
[1]
[1, 2]
В чем причина такого поведения?#
В Python значения аргументов по умолчанию вычисляются ровно один раз — в момент определения функции (компиляции в байткод), а не при каждом ее вызове.
Созданный объект пустого списка сохраняется в специальном атрибуте объекта функции — add_item.__defaults__. Каждый последующий вызов add_item() без второго аргумента работает с одной и той же ссылкой на этот мутабельный список в памяти.
Всегда используйте None в качестве значения по умолчанию для любых изменяемых типов (list, dict, set):
def add_item(item, target_list=None):
if target_list is None:
target_list = []
target_list.append(item)
return target_list
2. Замыкания и позднее связывание (Late Binding)#
Классическая задача на понимание областей видимости LEGB (Local, Enclosing, Global, Built-in):
multipliers = [lambda x: i * x for i in range(4)]
results = [m(2) for m in multipliers]
print(results)
Кажется логичным ожидать [0, 2, 4, 6]. Но консоль выдаст:
[6, 6, 6, 6]
Почему так происходит?#
В Python замыкания связывают переменные по имени, а не по значению. Во время создания лямбда-функций переменная i не фиксируется внутри тела лямбды — она будет искаться в объемлющей области видимости только в момент фактического выполнения функции m(2).
К моменту запуска цикла [m(2) for m in multipliers] генератор списка уже отработал до конца, и переменная i осталась равна 3. В результате каждая лямбда умножает аргумент 2 на 3, выдавая 6.
Как это исправить?#
Чтобы зафиксировать текущее значение переменной на каждой итерации, используют аргумент по умолчанию, так как он связывается в момент создания функции:
multipliers = [lambda x, i=i: i * x for i in range(4)]
results = [m(2) for m in multipliers]
print(results) # [0, 2, 4, 6]
3. Разница между операторами is и ==#
На первый взгляд вопрос тривиальный, но на собеседованиях его любят углублять до кеширования целых чисел и интернирования строк:
a = 256
b = 256
print(a is b) # True
x = 257
y = 257
print(x is y) # False (в интерактивной консоли REPL)
Разбор механики#
==— оператор равенства значений. Он вызывает магический метод__eq__у левого операнда.is— оператор идентичности. Он проверяет равенство адресов в памяти:id(a) == id(b).
В CPython существует механизм Small Integer Caching (кеширование малых целых чисел). При старте интерпретатор заранее выделяет память под массив целых чисел в диапазоне от -5 до 256. Любая переменная в этом диапазоне ссылается на уже существующий синглтон.
Для чисел >= 257 в REPL выделяются независимые блоки памяти, поэтому x is y возвращает False.
Если выполнить x = 257; y = 257 в одном файле скрипта (.py), компилятор Python применит оптимизацию констант для одного блока кода (Code Object), и x is y может вернуть True. Однако полагаться на is для сравнения чисел категорически запрещено!
4. Как на самом деле устроен GIL (Global Interpreter Lock)?#
Вопрос про GIL задают практически на каждом собеседовании от уровня Middle.
GIL (Global Interpreter Lock) — это мьютекс, предотвращающий одновременное выполнение нескольких нативных потоков байткода Python в рамках одного процесса CPython.
Зачем он появился?#
CPython использует механизм подсчета ссылок (Reference Counting) для управления памятью. Без глобальной блокировки любой доступ двух потоков к счетчику ссылок одного объекта создал бы состояние гонки (Race Condition). Создание отдельных легковесных мьютексов на каждый объект приводило к колоссальным накладным расходам по CPU.
Сравнение поведения подходов:#
| Тип задачи | Потоки (threading) | Процессы (multiprocessing) | Асинхронность (asyncio) |
|---|---|---|---|
| CPU-bound (шифрование, парсинг JSON, ML) | ❌ Медленнее из-за переключения GIL | ✅ Утилизирует все ядра CPU | ❌ Блокирует Event Loop |
| I/O-bound (запросы к БД, HTTP API) | ✅ GIL отпускается во время ожидания сокета | ⚠️ Высокий оверхед по памяти | ✅ Максимальная пропускная способность |
Начиная с Python 3.13 в язык добавлена экспериментальная сборка без GIL (PEP 703: free-threaded CPython). В ней сборщик мусора использует технологию Biased Reference Counting. Но в подавляющем большинстве рабочих проектов сегодня работает стандартный CPython с GIL.
5. Генераторы против Списковых включений#
В чем разница между круглыми и квадратными скобками?
list_comp = [x ** 2 for x in range(10_000_000)]
gen_exp = (x ** 2 for x in range(10_000_000))
list_compмгновенно вычисляет все значения и аллоцирует в оперативной памяти сотни мегабайт.gen_expсоздает генераторное выражение (объект с протоколом итератора__next__), потребляя фиксированные ~100 байт памяти вне зависимости от количества элементов. Значения вычисляются лениво (lazy evaluation) только по мере запроса.
Заключение#
Знание тонкостей работы стандартных конструкций языка помогает не только успешно проходить технические секции, но и избегать трудноуловимых багов в проде (особенно при работе с многопоточностью и памятью).
Прокачивай IT-скиллы с Capycodio
Не зубри теорию часами перед компьютером. Короткие 3-минутные интерактивные сессии прямо с телефона: по дороге, за кофе или перед сном.
Мы создаем интерактивный мобильный тренажер для разработчиков. Короткие сессии по 3-5 минут, чтобы держать базу и закрывать пробелы перед собеседованиями.