Вопросы по Python на собеседовании: Senior — часть 3/3 с разбором ответов
Вопросы по Python на собеседовании: Senior — часть 3/3 с разбором ответов
TL;DR: 8 вопросов Senior-уровня: метаклассы, дескрипторы, asyncio, MRO, потоки vs процессы vs asyncio, профилирование, ABC vs Protocol, и что нового в Python 3.13+. Это территория архитектурных решений и глубокого понимания рантайма.
Как пользоваться: Если Middle-вопросы не вызывают затруднений — проверь себя здесь. Senior-собеседование — это не столько правильный ответ, сколько глубина рассуждений и trade-off анализ.
Senior-собеседование по Python — это разговор, а не экзамен. Интервьюер хочет услышать, как ты думаешь, какие компромиссы видишь, и как принимаешь решения. По статистике собеседований за 2024–2026, Python — #1 по количеству технических интервью, и Senior-позиции требуют понимания внутренностей CPython, асинхронности и архитектурных паттернов.
Содержание
Senior
- 17. Метаклассы — что это и зачем?
- 18. Дескрипторы и протокол дескрипторов
- 19. asyncio — event loop, корутины, задачи
- 20. MRO и проблема ромба
- 21. Потоки vs процессы vs asyncio — когда что?
- 22. Профилирование и оптимизация памяти
- 23. ABC vs Protocol — когда что использовать?
- 24. Что нового в Python 3.12–3.13 и будущее языка
17. Метаклассы — что это и зачем?
Интервьюер проверяет: понимаешь ли ты, что классы в Python — тоже объекты.
В Python всё — объект. Классы — тоже объекты. Метакласс — это «класс класса», то есть то, что создаёт классы. По умолчанию метакласс — type.
# Обычное создание класса
class MyClass:
pass
# Эквивалент через type
MyClass = type('MyClass', (), {})
# type — это метакласс по умолчанию
type(MyClass) # <class 'type'>
type(type) # <class 'type'> — type сам себе метакласс
Свой метакласс:
class AutoRepr(type):
"""Автоматически добавляет __repr__ ко всем классам."""
def __new__(mcs, name, bases, namespace):
cls = super().__new__(mcs, name, bases, namespace)
attrs = [k for k in namespace if not k.startswith('_')]
def __repr__(self):
values = ', '.join(
f'{a}={getattr(self, a)!r}' for a in attrs
)
return f'{name}({values})'
cls.__repr__ = __repr__
return cls
class User(metaclass=AutoRepr):
def __init__(self, name, age):
self.name = name
self.age = age
print(User("Alice", 30)) # User(name='Alice', age=30)
Реальные применения: - ORM (Django models) — метакласс собирает поля из атрибутов класса - API-фреймворки (Django REST, Pydantic v1) — валидация на уровне класса - Реестры — автоматическая регистрация подклассов
Когда НЕ использовать: в 99% случаев. __init_subclass__ (Python 3.6+) или декораторы классов решают те же задачи проще.
Типичная ошибка: использовать метаклассы для простой валидации, когда достаточно __init_subclass__.
Follow-up: Чем метакласс отличается от декоратора класса? Когда нужен именно метакласс?
18. Дескрипторы и протокол дескрипторов
Интервьюер проверяет: знаешь ли ты механизм, на котором работают @property, @classmethod, @staticmethod.
Дескриптор — объект, определяющий хотя бы один из методов: __get__, __set__, __delete__.
class Validated:
"""Дескриптор, проверяющий тип значения."""
def __init__(self, expected_type):
self.expected_type = expected_type
self.name = None
def __set_name__(self, owner, name):
self.name = name # Python 3.6+
def __get__(self, obj, objtype=None):
if obj is None:
return self
return obj.__dict__.get(self.name)
def __set__(self, obj, value):
if not isinstance(value, self.expected_type):
raise TypeError(
f"{self.name} must be {self.expected_type.__name__}, "
f"got {type(value).__name__}"
)
obj.__dict__[self.name] = value
class User:
name = Validated(str)
age = Validated(int)
def __init__(self, name, age):
self.name = name # вызывает Validated.__set__
self.age = age
user = User("Alice", 30) # OK
user = User("Alice", "30") # TypeError!
Два типа дескрипторов:
- Data descriptor — определяет __set__ и/или __delete__. Приоритет выше, чем __dict__ экземпляра.
- Non-data descriptor — только __get__. Приоритет ниже, чем __dict__.
@property — это дескриптор:
# property — встроенный data descriptor
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def area(self):
return 3.14159 * self._radius ** 2
# Эквивалентно:
# area = property(fget=lambda self: ...)
Типичная ошибка: не понимать порядок поиска атрибутов (data descriptor → instance dict → non-data descriptor).
Follow-up: Почему @staticmethod и @classmethod работают? Какой протокол дескрипторов они реализуют?
19. asyncio — event loop, корутины, задачи
Интервьюер проверяет: умеешь ли ты проектировать асинхронные системы.
asyncio — однопоточная модель конкурентности через event loop. Корутины (async def) приостанавливаются на await, позволяя другим задачам выполняться.
import asyncio
async def fetch_data(url, delay):
print(f"Start fetching {url}")
await asyncio.sleep(delay) # имитация I/O
print(f"Done fetching {url}")
return f"Data from {url}"
async def main():
# Параллельный запуск через gather
results = await asyncio.gather(
fetch_data("api/users", 2),
fetch_data("api/orders", 1),
fetch_data("api/products", 3),
)
# Все три завершатся за ~3 секунды, не за 6
asyncio.run(main())
Ключевые концепции:
- Coroutine — функция с async def. Вызов возвращает coroutine object, а не результат.
- Task — обёртка над coroutine, запланированная для выполнения в event loop.
- await — точка, где корутина приостанавливается и отдаёт управление loop.
async def main():
# Task — запускается сразу, не ждёт await
task = asyncio.create_task(fetch_data("api/users", 2))
# ... другая работа ...
result = await task # ждём результат
gather vs TaskGroup (Python 3.11+):
# TaskGroup — структурная конкурентность, лучше обработка ошибок
async def main():
async with asyncio.TaskGroup() as tg:
task1 = tg.create_task(fetch_data("api/users", 2))
task2 = tg.create_task(fetch_data("api/orders", 1))
# Если одна задача упала — все отменяются
Типичная ошибка: вызывать блокирующий код (синхронный I/O, time.sleep, тяжёлые вычисления) внутри корутины — это блокирует весь event loop.
# BAD — блокирует event loop
async def bad():
import requests
return requests.get("https://api.example.com") # синхронный!
# GOOD — выносим в executor
async def good():
loop = asyncio.get_event_loop()
return await loop.run_in_executor(None, requests.get, url)
# BEST — используем async-библиотеку
async def best():
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
return await resp.json()
Follow-up: Чем asyncio.gather отличается от asyncio.wait? Когда использовать TaskGroup?
Мы разобрали три столпа Senior-знаний: метапрограммирование, дескрипторы и асинхронность. Дальше — наследование, конкурентность, производительность и будущее Python.
20. MRO и проблема ромба
Интервьюер проверяет: понимаешь ли ты, как Python разрешает множественное наследование.
MRO (Method Resolution Order) — порядок, в котором Python ищет методы при множественном наследовании. Используется алгоритм C3-линеаризации.
class A:
def method(self):
return "A"
class B(A):
def method(self):
return "B"
class C(A):
def method(self):
return "C"
class D(B, C):
pass
d = D()
print(d.method()) # "B"
print(D.__mro__)
# (D, B, C, A, object)
Правила C3-линеаризации:
- Дети перед родителями
- Порядок наследования сохраняется (B перед C, потому что class D(B, C))
- Монотонность — если B перед C в MRO, то и в любом подклассе
super() и кооперативное наследование:
class A:
def __init__(self):
print("A.__init__")
super().__init__()
class B(A):
def __init__(self):
print("B.__init__")
super().__init__()
class C(A):
def __init__(self):
print("C.__init__")
super().__init__()
class D(B, C):
def __init__(self):
print("D.__init__")
super().__init__()
D()
# D.__init__
# B.__init__
# C.__init__
# A.__init__
super() следует MRO, а не прямому родителю. B.super() вызывает C.__init__, не A.__init__.
Типичная ошибка: думать, что super() в B всегда вызывает A. В кооперативном наследовании super() следует MRO текущего экземпляра.
Follow-up: Когда C3-линеаризация невозможна и Python выбросит TypeError?
21. Потоки vs процессы vs asyncio — когда что?
Интервьюер проверяет: умеешь ли ты выбирать модель конкурентности под задачу.
- threading — для I/O-bound задач с shared state. GIL не мешает, потому что потоки ждут I/O.
- multiprocessing — для CPU-bound задач. Каждый процесс — свой GIL, настоящий параллелизм.
- asyncio — для I/O-bound с большим количеством конкурентных задач (тысячи соединений). Один поток, без overhead потоков.
from concurrent.futures import (
ThreadPoolExecutor,
ProcessPoolExecutor
)
import asyncio
# I/O-bound: скачать 100 URL
# Потоки — просто, shared state
with ThreadPoolExecutor(max_workers=10) as ex:
results = list(ex.map(requests.get, urls))
# I/O-bound: 10 000 WebSocket-соединений
# asyncio — меньше overhead
async def handle_connections():
tasks = [handle(ws) for ws in websockets]
await asyncio.gather(*tasks)
# CPU-bound: обработка 1000 изображений
# Процессы — настоящий параллелизм
with ProcessPoolExecutor() as ex:
results = list(ex.map(process_image, images))
Сравнение:
- Threading: ~8 КБ на поток, shared memory, GIL-ограничение для CPU. Хорош для 10-100 параллельных I/O задач. - Multiprocessing: ~30 МБ на процесс, изолированная память, pickle для IPC. Хорош для CPU-параллелизма. - Asyncio: ~1 КБ на корутину, один поток, нужны async-библиотеки. Хорош для 1000+ конкурентных I/O задач.
Типичная ошибка: использовать asyncio для CPU-bound или multiprocessing для простых HTTP-запросов — overhead не оправдан.
Follow-up: Как комбинировать asyncio и multiprocessing для задачи, которая и I/O-bound, и CPU-bound?
22. Профилирование и оптимизация памяти
Интервьюер проверяет: умеешь ли ты находить узкие места в production-коде.
Профилирование CPU:
# cProfile — встроенный, детерминистический
import cProfile
cProfile.run('my_function()', sort='cumulative')
# line_profiler — построчный анализ
# pip install line_profiler
@profile # декоратор line_profiler
def slow_function():
result = [x ** 2 for x in range(10**6)] # 0.3s
filtered = [x for x in result if x % 2] # 0.2s
return sum(filtered) # 0.01s
Профилирование памяти:
# tracemalloc — встроенный, Python 3.4+
import tracemalloc
tracemalloc.start()
data = [x ** 2 for x in range(10**6)]
snapshot = tracemalloc.take_snapshot()
for stat in snapshot.statistics('lineno')[:5]:
print(stat)
# objgraph — визуализация ссылок (pip install objgraph)
import objgraph
objgraph.show_most_common_types(limit=10)
objgraph.show_backrefs(obj, max_depth=3)
Типичные утечки памяти:
- Циклические ссылки с __del__ — GC не может собрать
- Глобальные кеши без ограничения размера — functools.lru_cache без maxsize
- Замыкания, захватывающие большие объекты
- Незакрытые файлы/соединения
# Утечка через неограниченный кеш
from functools import lru_cache
@lru_cache(maxsize=None) # опасно! растёт без ограничений
def get_user(user_id):
return db.query(User, user_id)
# Безопасно
@lru_cache(maxsize=1000)
def get_user(user_id):
return db.query(User, user_id)
Типичная ошибка: оптимизировать без профилирования. «Premature optimization is the root of all evil» — сначала измерь, потом оптимизируй.
Follow-up: Как найти утечку памяти в долго работающем production-сервисе?
23. ABC vs Protocol — когда что использовать?
Интервьюер проверяет: понимаешь ли ты разницу между номинальной и структурной типизацией.
ABC (Abstract Base Class) — номинальная типизация. Класс должен явно наследовать ABC.
from abc import ABC, abstractmethod
class Repository(ABC):
@abstractmethod
def get(self, id: int):
...
@abstractmethod
def save(self, entity):
...
class UserRepository(Repository):
def get(self, id: int):
return db.query(User, id)
def save(self, entity):
db.add(entity)
# Нельзя создать без реализации всех abstractmethod
# Repository() # TypeError!
Protocol (Python 3.8+, typing.Protocol) — структурная типизация (duck typing со статической проверкой). Класс НЕ наследует Protocol — достаточно реализовать нужные методы.
from typing import Protocol
class Drawable(Protocol):
def draw(self) -> None:
...
class Circle: # НЕ наследует Drawable!
def draw(self) -> None:
print("Drawing circle")
def render(shape: Drawable) -> None:
shape.draw()
render(Circle()) # OK — mypy проверит структуру
Когда что:
- ABC — когда нужна гарантия контракта в runtime. Наследование явное, isinstance() работает.
- Protocol — когда нужна гибкость без связывания. Для библиотек, плагинов, интеграций.
В Python 3.12+ Protocol стал runtime-checkable по умолчанию:
from typing import Protocol, runtime_checkable
@runtime_checkable
class Closable(Protocol):
def close(self) -> None:
...
import io
isinstance(io.StringIO(), Closable) # True
Типичная ошибка: использовать ABC для каждого интерфейса. Protocol лучше подходит для Python, потому что сохраняет дух duck typing.
Follow-up: Как совместить ABC и Protocol в одном проекте?
Последний вопрос — про будущее Python. Senior должен понимать, куда движется язык.
24. Что нового в Python 3.12–3.13 и будущее языка
Интервьюер проверяет: следишь ли ты за развитием языка.
Python 3.12 (октябрь 2023):
- Per-interpreter GIL (PEP 684) — каждый субинтерпретатор получает свой GIL. Шаг к настоящему параллелизму. - Type Parameter Syntax (PEP 695):
# До 3.12
from typing import TypeVar
T = TypeVar('T')
def first(lst: list[T]) -> T: ...
# Python 3.12+
def first[T](lst: list[T]) -> T: ...
# Дженерик-классы
class Stack[T]:
def push(self, item: T) -> None: ...
def pop(self) -> T: ...
- f-string без ограничений — вложенные кавычки, обратные слеши, комментарии.
Python 3.13 (октябрь 2024):
- Free-threaded mode (PEP 703) — экспериментальная сборка CPython без GIL. Настоящие параллельные потоки.
# Установка free-threaded сборки
python3.13t # суффикс 't' для free-threaded
# Проверка
import sys
sys._is_gil_enabled() # False в free-threaded сборке
- JIT-компилятор (PEP 744) — экспериментальный copy-and-patch JIT. Пока +5% производительности, но фундамент для будущих оптимизаций. - Улучшенный REPL — многострочное редактирование, цветной вывод. - Улучшенные сообщения об ошибках — ещё более точные подсказки.
Что это значит для разработчиков:
- Free-threaded Python меняет парадигму — CPU-bound код на потоках станет реальным
- Но пока это экспериментально — C-расширения (NumPy, pandas) нужно адаптировать
- JIT пока минимальный, но в 3.14+ ожидается значительное ускорение
Типичная ошибка: говорить «GIL убрали в Python 3.13». Нет — GIL опциональный, в отдельной сборке, и пока экспериментальный.
Follow-up: Как free-threaded Python повлияет на существующие библиотеки? Что нужно адаптировать?
Итого Senior: 8 вопросов. Фокус — метапрограммирование, асинхронность, внутренности CPython, архитектурные решения. На Senior-собесе важен не столько ответ, сколько глубина рассуждений и способность обсуждать trade-offs.
Чего НЕ спрашивают на Senior
- Синтаксис базовых конструкций — это Junior-территория.
- «Напишите сортировку» — от сеньора ждут архитектуру, а не алгоритмы.
- Конкретные API-методы наизусть — важнее принципы, чем
dict.setdefaultvscollections.defaultdict. - Задачи на время — Senior-собесы обычно в формате дискуссии, не speedrun.
Как готовиться к Senior Python-собеседованию
- Читай PEP — хотя бы ключевые: 703, 684, 695, 572, 3107. Это показывает глубину.
- Изучи исходники CPython —
Objects/,Python/ceval.c. Не весь, а ключевые модули. - Построй что-то с asyncio — не TODO-лист, а реальный concurrency: WebSocket-сервер, crawler.
- Подготовь истории — «расскажите про сложную проблему» с конкретными метриками и решениями.
- Пройди mock-интервью — навык объяснять архитектурные решения тренируется только практикой.
Как попробовать Sobes AI
Sobes AI — AI-помощник для технических собеседований. Как он поможет на Senior-уровне:
- Скачай приложение на sobesai.app
- Выбери Senior-режим — AI задаёт вопросы с глубокими follow-up
- Потренируй trade-off дискуссии — «почему asyncio, а не threading?»
- Разбери архитектурные кейсы — system design с Python-спецификой
- Используй на реальном собесе — AI анализирует вопрос и подсказывает структуру ответа
Готовитесь к собеседованию?
Sobes AI слушает вопросы интервьюера и генерирует ответы в реальном времени.
Скачать Sobes AI