Python*Разработка бэкенда*Карьера в IT*Сложность: Middle

10 каверзных вопросов по Python с собеседований, на которых валятся даже Middle

Разбор изменяемых аргументов по умолчанию, замыканий, GIL, разницы is vs == и работы генераторов с примерами кода и практическими нюансами.

Команда Capycodio
Команда Capycodio
·8 мин

Собеседования на позицию Middle Python Developer часто начинаются не с проектирования микросервисов или алгоритмов на графах, а с проверки глубокого понимания механики рантайма CPython.

Интервьюеру важно понять: пишет ли кандидат код механически, или он представляет, что происходит с объектами в куче, как резолвятся области видимости и почему код ведёт себя именно так.

В этой статье мы подробно разберем 5 классических вопросов и задач, на которых регулярно спотыкаются даже разработчики с опытом от 2 лет.


1. Изменяемый аргумент по умолчанию#

Один из самых частых «фильтрующих» вопросов на интервью. Рассмотрим следующий фрагмент:

PYTHON
def add_item(item, target_list=[]):
    target_list.append(item)
    return target_list

print(add_item(1))
print(add_item(2))

Большинство новичков ожидают увидеть два независимых списка [1] и [2]. Однако реальный вывод интерпретатора:

PYTHON
[1]
[1, 2]

В чем причина такого поведения?#

В Python значения аргументов по умолчанию вычисляются ровно один раз — в момент определения функции (компиляции в байткод), а не при каждом ее вызове.

Созданный объект пустого списка сохраняется в специальном атрибуте объекта функции — add_item.__defaults__. Каждый последующий вызов add_item() без второго аргумента работает с одной и той же ссылкой на этот мутабельный список в памяти.

💡
Идиоматичное решение (PEP 8)

Всегда используйте None в качестве значения по умолчанию для любых изменяемых типов (list, dict, set):

PYTHON
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):

PYTHON
multipliers = [lambda x: i * x for i in range(4)]
results = [m(2) for m in multipliers]
print(results)

Кажется логичным ожидать [0, 2, 4, 6]. Но консоль выдаст:

PYTHON
[6, 6, 6, 6]

Почему так происходит?#

В Python замыкания связывают переменные по имени, а не по значению. Во время создания лямбда-функций переменная i не фиксируется внутри тела лямбды — она будет искаться в объемлющей области видимости только в момент фактического выполнения функции m(2).

К моменту запуска цикла [m(2) for m in multipliers] генератор списка уже отработал до конца, и переменная i осталась равна 3. В результате каждая лямбда умножает аргумент 2 на 3, выдавая 6.

Как это исправить?#

Чтобы зафиксировать текущее значение переменной на каждой итерации, используют аргумент по умолчанию, так как он связывается в момент создания функции:

PYTHON
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 и ==#

На первый взгляд вопрос тривиальный, но на собеседованиях его любят углублять до кеширования целых чисел и интернирования строк:

PYTHON
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. Генераторы против Списковых включений#

В чем разница между круглыми и квадратными скобками?

PYTHON
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) только по мере запроса.

Заключение#

Знание тонкостей работы стандартных конструкций языка помогает не только успешно проходить технические секции, но и избегать трудноуловимых багов в проде (особенно при работе с многопоточностью и памятью).

Capycodio

Прокачивай IT-скиллы с Capycodio

Не зубри теорию часами перед компьютером. Короткие 3-минутные интерактивные сессии прямо с телефона: по дороге, за кофе или перед сном.

Теги публикации:
#python#собеседование#backend#cpython#архитектура
Капибара
Команда Capycodio

Мы создаем интерактивный мобильный тренажер для разработчиков. Короткие сессии по 3-5 минут, чтобы держать базу и закрывать пробелы перед собеседованиями.