Вопросы по Python на собеседовании: Middle — часть 2/3 с разбором ответов
Вопросы по Python на собеседовании: Middle — часть 2/3 с разбором ответов
TL;DR: 8 вопросов для Middle Python-собеседований. GIL, генераторы, сборка мусора, shallow/deep copy, передача аргументов, контекстные менеджеры, декораторы с параметрами и
__slots__. С кодом, ошибками кандидатов и follow-up.
Как пользоваться: Если Junior-вопросы не вызывают затруднений — начинай отсюда. Каждый вопрос проверяет глубину понимания, а не заучивание.
На Middle-уровне интервьюеры уже не спрашивают «что такое list». Они проверяют, понимаешь ли ты как работает Python под капотом, и можешь ли ты принимать осознанные инженерные решения. По данным анализа 9 247 собеседований, Middle-разработчики составляют 45% всех кандидатов на Python-позиции.
Содержание
Middle
- 9. Что такое GIL и как он влияет на многопоточность?
- 10. Генераторы и yield — зачем и когда?
- 11. Как работает сборка мусора?
- 12. Shallow copy vs deep copy
- 13. Как передаются аргументы в функции?
- 14. Контекстные менеджеры — протокол и применение
- 15. Как написать декоратор с параметрами?
- 16. Что такое slots и зачем он нужен?
9. Что такое GIL и как он влияет на многопоточность?
Интервьюер проверяет: понимаешь ли ты ограничения CPython и как с ними работать.
GIL (Global Interpreter Lock) — мьютекс в CPython, который позволяет только одному потоку исполнять Python-байткод в каждый момент времени.
Зачем он нужен: упрощает управление памятью. Подсчёт ссылок (reference counting) не потокобезопасен без GIL — без блокировки два потока могут одновременно изменить счётчик, и объект будет удалён раньше времени или не удалён вовсе.
Как это влияет на код:
import threading
import time
def cpu_bound(n):
"""CPU-bound задача — GIL мешает"""
total = 0
for i in range(n):
total += i
# Два потока НЕ ускорят CPU-bound задачу
start = time.time()
t1 = threading.Thread(target=cpu_bound, args=(10**7,))
t2 = threading.Thread(target=cpu_bound, args=(10**7,))
t1.start(); t2.start()
t1.join(); t2.join()
print(f"Threads: {time.time() - start:.2f}s")
# Примерно столько же, сколько последовательно!
Когда потоки всё-таки полезны: для I/O-bound задач (сеть, файлы, БД). GIL освобождается, пока поток ждёт I/O:
import threading
import requests
# I/O-bound — потоки дают реальный параллелизм
def fetch(url):
return requests.get(url)
urls = ["https://example.com"] * 10
threads = [threading.Thread(target=fetch, args=(u,)) for u in urls]
for t in threads: t.start()
for t in threads: t.join()
Обходные пути для CPU-bound:
- multiprocessing — каждый процесс имеет свой GIL
- C-расширения (NumPy, pandas) — освобождают GIL во время вычислений
- concurrent.futures.ProcessPoolExecutor — удобная обёртка
Типичная ошибка: говорить «в Python нет многопоточности». Многопоточность есть, просто GIL ограничивает параллельное выполнение CPU-bound кода. Для I/O-bound — потоки работают отлично.
Follow-up: Что изменится с free-threaded Python (PEP 703, Python 3.13+)?
10. Генераторы и yield — зачем и когда?
Интервьюер проверяет: умеешь ли ты работать с ленивыми вычислениями.
Генератор — функция с yield вместо return. Возвращает итератор, который производит значения по одному, не загружая всё в память.
def fibonacci(limit):
a, b = 0, 1
while a < limit:
yield a
a, b = b, a + b
# Значения генерируются по требованию
for num in fibonacci(100):
print(num)
# Generator expression (аналог list comprehension)
squares = (x ** 2 for x in range(1_000_000))
# Занимает ~100 байт, а не ~8 МБ как list
Когда использовать:
- Большие наборы данных — чтение файлов построчно, потоки данных
- Пайплайны обработки — цепочка генераторов
- Бесконечные последовательности — itertools.count()
# Пайплайн генераторов
def read_lines(path):
with open(path) as f:
for line in f:
yield line.strip()
def filter_errors(lines):
for line in lines:
if "ERROR" in line:
yield line
def extract_message(lines):
for line in lines:
yield line.split("ERROR: ")[1]
# Всё обрабатывается лениво, по одной строке
pipeline = extract_message(filter_errors(read_lines("app.log")))
for msg in pipeline:
print(msg)
yield vs return: yield приостанавливает функцию, сохраняя состояние. При следующем вызове next() выполнение продолжается с того же места.
Типичная ошибка: забывать, что генератор можно проитерировать только один раз. После исчерпания — пустой.
gen = (x for x in range(3))
list(gen) # [0, 1, 2]
list(gen) # [] — уже исчерпан!
Follow-up: Что делают методы send(), throw(), close() у генератора?
11. Как работает сборка мусора?
Интервьюер проверяет: понимаешь ли ты управление памятью в CPython.
CPython использует два механизма:
1. Reference counting (основной) — каждый объект хранит счётчик ссылок. Когда он падает до 0 — объект удаляется немедленно.
import sys
a = [1, 2, 3]
print(sys.getrefcount(a)) # 2 (a + аргумент getrefcount)
b = a
print(sys.getrefcount(a)) # 3
del b
print(sys.getrefcount(a)) # 2
2. Generational garbage collector (для циклических ссылок) — reference counting не справляется с циклами:
# Циклическая ссылка — refcount никогда не станет 0
a = []
b = []
a.append(b)
b.append(a)
del a, b
# Объекты всё ещё в памяти — нужен GC
GC делит объекты на 3 поколения: - Gen 0 — новые объекты. Проверяется чаще всего. - Gen 1 — пережили одну сборку. - Gen 2 — долгоживущие объекты. Проверяется редко.
import gc
gc.get_threshold() # (700, 10, 10)
# Gen 0 собирается после 700 аллокаций
# Gen 1 — после 10 сборок Gen 0
# Gen 2 — после 10 сборок Gen 1
Типичная ошибка: говорить «Python сам убирает мусор, не нужно об этом думать». В реальных проектах утечки памяти через циклические ссылки — распространённая проблема, особенно с колбэками и кешами.
Follow-up: Как найти утечку памяти в Python-приложении?
Три ключевых вопроса позади — GIL, генераторы, GC. Это фундамент, на котором строится остальное. Дальше — практические паттерны, которые проверяют умение писать production-код.
12. Shallow copy vs deep copy
Интервьюер проверяет: понимаешь ли ты, как копируются вложенные структуры данных.
import copy
original = [[1, 2], [3, 4]]
# Shallow copy — копирует только верхний уровень
shallow = copy.copy(original)
shallow[0].append(99)
print(original) # [[1, 2, 99], [3, 4]] — изменился!
# Deep copy — рекурсивно копирует всё
original = [[1, 2], [3, 4]]
deep = copy.deepcopy(original)
deep[0].append(99)
print(original) # [[1, 2], [3, 4]] — не изменился
Способы создания shallow copy:
# Все эти способы — shallow copy
lst = [1, [2, 3]]
a = lst.copy()
b = lst[:]
c = list(lst)
d = copy.copy(lst)
Типичная ошибка: использовать lst[:] или .copy() для вложенных структур и удивляться, что изменения в оригинале видны в копии.
Follow-up: Что произойдёт при deepcopy объекта с циклическими ссылками?
13. Как передаются аргументы в функции?
Интервьюер проверяет: можешь ли ты объяснить механизм, который часто путают.
Ни «по значению», ни «по ссылке». Python использует call by object reference (или «call by sharing»). Передаётся ссылка на объект, но не сам объект и не ссылка на переменную.
def modify(lst, num):
lst.append(4) # изменяет оригинал (mutable)
num += 10 # создаёт новый объект (immutable)
my_list = [1, 2, 3]
my_num = 5
modify(my_list, my_num)
print(my_list) # [1, 2, 3, 4] — изменился
print(my_num) # 5 — не изменился
Правило: если объект mutable — изменения внутри функции видны снаружи. Если immutable — создаётся новый объект, оригинал не меняется.
def reassign(lst):
lst = [99, 100] # создаёт новую локальную переменную
# НЕ изменяет оригинальный список
my_list = [1, 2, 3]
reassign(my_list)
print(my_list) # [1, 2, 3] — не изменился!
Типичная ошибка: говорить «mutable передаётся по ссылке, immutable по значению». Механизм один и тот же. Разница в том, можно ли изменить объект по этой ссылке.
Follow-up: Как сделать функцию, которая гарантированно не изменит переданный список?
14. Контекстные менеджеры — протокол и применение
Интервьюер проверяет: знаешь ли ты протокол __enter__/__exit__ и умеешь ли управлять ресурсами.
Контекстный менеджер — объект, реализующий методы __enter__ и __exit__. Используется с with для гарантированной очистки ресурсов.
# Встроенный пример — файлы
with open("data.txt") as f:
content = f.read()
# f.close() вызовется автоматически, даже при исключении
Свой контекстный менеджер через класс:
class DatabaseConnection:
def __enter__(self):
self.conn = create_connection()
return self.conn
def __exit__(self, exc_type, exc_val, exc_tb):
self.conn.close()
return False # не подавляем исключения
with DatabaseConnection() as conn:
conn.execute("SELECT 1")
Через contextlib — проще:
from contextlib import contextmanager
@contextmanager
def timer(label):
import time
start = time.time()
yield # тело блока with выполняется здесь
print(f"{label}: {time.time() - start:.2f}s")
with timer("Query"):
expensive_operation()
__exit__ принимает информацию об исключении: exc_type, exc_val, exc_tb. Если вернуть True — исключение подавляется. Обычно возвращаем False (или None).
Типичная ошибка: забыть return False в __exit__ и случайно подавить исключение.
Follow-up: Что такое contextlib.suppress? Когда его использовать?
15. Как написать декоратор с параметрами?
Интервьюер проверяет: понимаешь ли ты замыкания на уровне трёх вложенных функций.
Декоратор с параметрами — это фабрика декораторов: функция, которая возвращает декоратор.
from functools import wraps
def repeat(times): # фабрика — принимает параметры
def decorator(func): # декоратор — принимает функцию
@wraps(func)
def wrapper(*args, **kwargs): # обёртка — выполняет логику
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(times=3)
def greet(name):
print(f"Hello, {name}!")
greet("Alice")
# Hello, Alice!
# Hello, Alice!
# Hello, Alice!
Три уровня вложенности:
repeat(times=3) → возвращает decorator
decorator(greet) → возвращает wrapper
wrapper("Alice") → выполняет логику
Практический пример — retry:
def retry(max_attempts=3, exceptions=(Exception,)):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except exceptions as e:
if attempt == max_attempts:
raise
return wrapper
return decorator
@retry(max_attempts=5, exceptions=(ConnectionError,))
def fetch_data(url):
...
Типичная ошибка: путать уровни вложенности — забыть, что @repeat(3) сначала вызывает repeat(3), а результат применяется как декоратор.
Follow-up: Как написать декоратор, который работает и с параметрами, и без: @cache и @cache(maxsize=100)?
Мы закрыли практические паттерны. Последний вопрос — про оптимизацию на уровне классов.
16. Что такое slots и зачем он нужен?
Интервьюер проверяет: знаешь ли ты, как оптимизировать потребление памяти на уровне классов.
По умолчанию Python хранит атрибуты экземпляра в __dict__ — словаре. Это гибко, но затратно по памяти. __slots__ заменяет __dict__ фиксированным набором слотов.
class WithDict:
def __init__(self, x, y):
self.x = x
self.y = y
class WithSlots:
__slots__ = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = y
import sys
a = WithDict(1, 2)
b = WithSlots(1, 2)
sys.getsizeof(a.__dict__) # 104 байта (dict overhead)
# b.__dict__ # AttributeError — нет dict!
Когда полезно:
- Миллионы однотипных объектов (ORM-записи, точки в геометрии, события)
- Защита от опечаток — нельзя случайно создать атрибут self.nmae вместо self.name
Ограничения:
- Нельзя добавлять произвольные атрибуты
- Наследование: подкласс без __slots__ получит __dict__ обратно
- Нельзя использовать __weakref__ без явного указания в слотах
b = WithSlots(1, 2)
b.z = 3 # AttributeError: 'WithSlots' object has no attribute 'z'
Типичная ошибка: использовать __slots__ везде «для оптимизации». Для единичных объектов разница незначительна, зато теряется гибкость.
Follow-up: Как совместить __slots__ с наследованием?
Итого Middle: 8 вопросов. Фокус — GIL, управление памятью, генераторы, копирование, контекстные менеджеры, продвинутые декораторы,
__slots__. Если уверенно отвечаешь на follow-up — ты готов к Middle-собесу.
Чего НЕ спрашивают на Middle
- Реализацию хеш-таблицы с нуля — важно понимать dict, но не писать его.
- C-расширения — это уже Senior/Expert.
- Конкретные фреймворки (Django/FastAPI) в теоретической части — фреймворки проверяют отдельно.
- Математику ML — если позиция не Data Science.
Как готовиться к Middle Python-собеседованию
- Прочитай CPython internals — хотя бы главы про GIL, GC и объектную модель.
- Напиши свои реализации — контекстный менеджер, декоратор с параметрами, генератор-пайплайн.
- Проведи code review — чужого и своего кода. Middle должен обосновывать решения.
- Прорешай 20-30 задач LeetCode Medium — акцент на структуры данных.
Как попробовать Sobes AI
Sobes AI — AI-помощник для технических собеседований. Как он поможет с этими вопросами:
- Скачай приложение на sobesai.app
- Выбери уровень Middle — AI адаптирует сложность
- Потренируй follow-up — именно на них проваливаются кандидаты
- Разбери слабые места — AI покажет, где ответ был поверхностным
- Используй на реальном собесе — подсказки в реальном времени
Готовитесь к собеседованию?
Sobes AI слушает вопросы интервьюера и генерирует ответы в реальном времени.
Скачать Sobes AI