Топ вопросов: ООП, внутренности, конкурентность
Как пользоваться уроком
Как и в предыдущем уроке: раскройте вопрос, ответьте вслух, сверьтесь и поиграйте с кодом. На middle-интервью важны не определения, а последствия: почему так сделано и где это стреляет на практике.
ООП
Какие принципы ООП вы знаете и как они выражены в Python?
- Инкапсуляция — данные и поведение вместе, детали скрыты. В Python нет приватности на уровне языка: соглашение
_name(«внутреннее»),__name— искажение имени (name mangling) против случайных конфликтов в наследниках,@propertyдля контролируемого доступа. - Наследование — в том числе множественное, порядок поиска методов определяет MRO.
- Полиморфизм — утиная типизация: важно, что объект умеет, а не его класс. Плюс магические методы:
len(x),x + y,forработают с любыми объектами, реализующими протокол. - Абстракция —
abc.ABCи@abstractmethod, протоколыtyping.Protocol.
Часто добавляют: композиция предпочтительнее наследования; SOLID.
class Account:
def __init__(self, owner):
self.owner = owner
self._balance = 0 # «внутреннее» — по соглашению
self.__pin = "1234" # искажается в _Account__pin
@property
def balance(self):
return self._balance
acc = Account("Аня")
print(acc.balance, acc._Account__pin)
try:
acc.balance = 100
except AttributeError as e:
print(e)
Что такое MRO и как работает super()?
MRO (method resolution order) — порядок, в котором Python ищет атрибут в иерархии классов; строится алгоритмом C3-линеаризации: потомки раньше предков, порядок базовых классов сохраняется. super() вызывает следующий класс в MRO текущего объекта, а не обязательно родителя — так кооперативное множественное наследование вызывает каждый метод ровно один раз.
class Base:
def save(self):
print("Base.save")
class LoggingMixin(Base):
def save(self):
print("LoggingMixin: до"); super().save()
class ValidationMixin(Base):
def save(self):
print("ValidationMixin: проверка"); super().save()
class Model(LoggingMixin, ValidationMixin):
pass
print([c.__name__ for c in Model.__mro__])
Model().save()
Чем отличаются __new__ и __init__?
__new__ — статический метод, который создаёт объект и возвращает его; __init__ — инициализирует уже созданный объект и ничего не возвращает. __new__ переопределяют редко: наследование от неизменяемых типов (int, str, tuple), синглтоны, кеширование экземпляров.
class Celsius(float):
def __new__(cls, value):
if value < -273.15:
raise ValueError("ниже абсолютного нуля")
return super().__new__(cls, value) # у float значение задаётся в __new__
class Singleton:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
print(Celsius(36.6) + 1, Singleton() is Singleton())
Какие магические методы вы знаете? Чем __repr__ отличается от __str__?
__repr__ — однозначное представление для разработчика (в идеале — код, создающий объект), используется в REPL, отладке, внутри контейнеров. __str__ — для пользователя (print, str(), f-строки); если его нет, используется __repr__. Другие: __eq__/__lt__/__hash__, __len__/__getitem__/__contains__/__iter__, __add__ и __radd__, __call__, __enter__/__exit__, __getattr__, __bool__.
from functools import total_ordering
@total_ordering
class Money:
def __init__(self, amount, currency="RUB"):
self.amount, self.currency = amount, currency
def __repr__(self):
return f"Money({self.amount!r}, {self.currency!r})"
def __str__(self):
return f"{self.amount:,.2f} {self.currency}".replace(",", " ")
def __add__(self, other):
return Money(self.amount + other.amount, self.currency)
def __radd__(self, other): # чтобы работал sum(): 0 + Money
return self if other == 0 else NotImplemented
def __eq__(self, other):
return (self.amount, self.currency) == (other.amount, other.currency)
def __lt__(self, other):
return self.amount < other.amount
def __hash__(self):
return hash((self.amount, self.currency))
prices = [Money(1500), Money(99.9), Money(25000)]
print(prices)
print(sum(prices), max(prices), Money(5) >= Money(3))
Что такое property и дескрипторы? Как они связаны?
Дескриптор — объект-атрибут класса с методами __get__/__set__/__delete__, который перехватывает доступ к атрибуту экземпляров. На дескрипторах построены property, методы (функции — дескрипторы, создающие связанные методы), classmethod, staticmethod, поля ORM (models.CharField, mapped_column). property — готовый дескриптор для вычисляемых и проверяемых атрибутов.
class Positive:
def __set_name__(self, owner, name):
self.name = "_" + name
def __get__(self, obj, objtype=None):
return self if obj is None else getattr(obj, self.name)
def __set__(self, obj, value):
if value <= 0:
raise ValueError(f"{self.name[1:]} должно быть > 0")
setattr(obj, self.name, value)
class Product:
price = Positive()
quantity = Positive()
def __init__(self, price, quantity):
self.price, self.quantity = price, quantity
@property
def total(self):
return self.price * self.quantity
p = Product(100, 3)
print(p.total)
try:
p.quantity = 0
except ValueError as e:
print(e)
Зачем нужны __slots__?
__slots__ = ("x", "y") заменяет словарь атрибутов экземпляра __dict__ фиксированными слотами: объект занимает заметно меньше памяти, доступ чуть быстрее, нельзя добавить произвольный атрибут. Полезно, когда объектов миллионы. В dataclass — @dataclass(slots=True).
import sys
class P:
def __init__(self, x, y): self.x, self.y = x, y
class S:
__slots__ = ("x", "y")
def __init__(self, x, y): self.x, self.y = x, y
p, s = P(1, 2), S(1, 2)
print(sys.getsizeof(p) + sys.getsizeof(p.__dict__), sys.getsizeof(s))
try:
s.z = 3
except AttributeError as e:
print(e)
ABC или Protocol: в чём разница?
abc.ABC — номинальная типизация: класс должен явно унаследоваться и реализовать абстрактные методы, иначе его нельзя создать. typing.Protocol — структурная (статическая утиная) типизация: подходит любой класс с нужными методами, наследоваться не надо; проверяет mypy, а с @runtime_checkable — и isinstance.
from abc import ABC, abstractmethod
from typing import Protocol, runtime_checkable
class Storage(ABC):
@abstractmethod
def save(self, data): ...
try:
Storage()
except TypeError as e:
print(e)
@runtime_checkable
class SupportsClose(Protocol):
def close(self) -> None: ...
class Connection: # ни от чего не наследуется
def close(self): print("закрыто")
print(isinstance(Connection(), SupportsClose))
Что такое метакласс? Приведите пример применения.
Метакласс — «класс класса»: он создаёт классы так же, как класс создаёт объекты. По умолчанию это type. Метаклассы применяют для регистрации подклассов, проверки или изменения атрибутов класса при его создании (так устроены модели Django ORM, Enum). В большинстве задач хватает более простого __init_subclass__ или декоратора класса.
print(type(42), type(int), type(type))
class PluginBase:
registry = {}
def __init_subclass__(cls, name=None, **kwargs):
super().__init_subclass__(**kwargs)
cls.registry[name or cls.__name__.lower()] = cls
class CsvExporter(PluginBase, name="csv"): ...
class JsonExporter(PluginBase): ...
print(PluginBase.registry)
Dynamic = type("Dynamic", (), {"greet": lambda self: "привет"}) # класс, созданный вызовом type
print(Dynamic().greet())
Внутренности CPython
Как Python управляет памятью? Что такое подсчёт ссылок и сборщик мусора?
Каждый объект хранит счётчик ссылок; когда он падает до нуля, объект удаляется сразу. Подсчёт ссылок не справляется с циклическими ссылками (объекты ссылаются друг на друга), поэтому есть ещё циклический сборщик мусора (gc) с тремя поколениями: новые объекты проверяются часто, «выжившие» — реже. Мелкие объекты выделяются из собственного аллокатора (pymalloc), поэтому память может не возвращаться ОС сразу.
import sys, gc
a = []
print(sys.getrefcount(a) - 1) # минус временная ссылка аргумента
b = a
print(sys.getrefcount(a) - 1)
class Node:
def __init__(self): self.other = None
x, y = Node(), Node()
x.other, y.other = y, x # цикл
del x, y
print("собрано объектов:", gc.collect())
print(gc.get_threshold())
Что такое интернирование строк и кеш малых чисел?
CPython заранее создаёт целые от -5 до 256 и переиспользует их; короткие строки, похожие на идентификаторы, и имена в коде интернируются (одна копия на всё приложение). Это оптимизация реализации, а не гарантия языка: сравнивать числа и строки нужно через ==. sys.intern позволяет интернировать вручную (экономия памяти на повторяющихся строках, быстрое сравнение).
import sys
a, b = 256, 256
print(a is b)
s1 = "".join(["пр", "ивет"]); s2 = "привет"
print(s1 == s2, s1 is s2, sys.intern(s1) is sys.intern(s2))
Python компилируемый или интерпретируемый?
И то и другое: CPython сначала компилирует исходный код в байткод (кеширует в __pycache__/*.pyc), затем виртуальная машина интерпретирует байткод. Другие реализации: PyPy (JIT-компиляция, часто в разы быстрее на чистом Python), Jython, GraalPy, MicroPython. В 3.11+ добавлен специализирующий адаптивный интерпретатор, в 3.13 — экспериментальный JIT.
import dis
def add_tax(price):
return price * 1.2
dis.dis(add_tax)
Что такое GIL? Почему его не убрали? Как обойти?
GIL (Global Interpreter Lock) — мьютекс CPython: байткод в каждый момент выполняет один поток. Он упрощает подсчёт ссылок и C-расширения и ускоряет однопоточный код. Обход для CPU-bound задач: процессы (multiprocessing, ProcessPoolExecutor), C-расширения, отпускающие GIL (NumPy), Cython/Rust, сборка Python 3.13+ без GIL (PEP 703). Для I/O-bound задач GIL не мешает: во время ожидания он отпускается.
import dis
counter = 0
def increment():
global counter
counter += 1
dis.dis(increment) # LOAD → BINARY_OP → STORE: между ними поток может переключиться
Конкурентность
Чем отличаются потоки, процессы и asyncio? Что когда выбирать?
| Потоки | Процессы | asyncio | |
|---|---|---|---|
| Параллельность CPU | нет (GIL) | да | нет |
| Память | общая | раздельная (обмен через сериализацию) | общая, один поток |
| Переключение | ОС, в любой момент | ОС | только в await |
| Масштаб | десятки–сотни | по числу ядер | десятки тысяч задач |
| Подходит | I/O с синхронными библиотеками | CPU-bound | много I/O, async-библиотеки |
Правило: CPU-bound → процессы; I/O-bound и есть async-библиотеки → asyncio; I/O-bound с синхронными библиотеками → потоки (ThreadPoolExecutor).
Что такое состояние гонки и как его избежать?
Гонка — результат зависит от того, как переплелись операции потоков. Классика — неатомарное «прочитать → изменить → записать». Решения: блокировки (threading.Lock), атомарные операции на стороне базы (UPDATE ... SET x = x + 1), очереди (queue.Queue) вместо общих данных, неизменяемые данные. Взаимная блокировка (deadlock) возникает, когда потоки ждут блокировки друг друга; лечится единым порядком захвата и таймаутами.
import asyncio
balance = 100
async def withdraw(amount):
global balance
current = balance
await asyncio.sleep(0) # точка переключения между «прочитал» и «записал»
if current >= amount:
balance = current - amount
return True
return False
async def main():
global balance
print(await asyncio.gather(withdraw(80), withdraw(80)), balance) # оба «успешны»!
balance = 100
lock = asyncio.Lock()
async def safe(amount):
async with lock:
return await withdraw(amount)
print(await asyncio.gather(safe(80), safe(80)), balance)
asyncio.run(main())
Как работает async/await? Что такое цикл событий?
async def создаёт корутину — функцию, которую можно приостановить. await отдаёт управление циклу событий, пока ожидаемая операция (сеть, таймер) не завершится; в это время цикл выполняет другие задачи. Всё происходит в одном потоке, переключения только в await, поэтому блокирующий вызов (time.sleep, requests.get) останавливает всё. Конкурентность — через asyncio.gather, TaskGroup, create_task; блокирующий код выносят в asyncio.to_thread.
import asyncio, time
async def fetch(name, delay):
await asyncio.sleep(delay)
return f"{name} за {delay} с"
async def main():
start = time.perf_counter()
results = await asyncio.gather(fetch("A", 0.3), fetch("B", 0.2), fetch("C", 0.1))
print(results, f"всего {time.perf_counter() - start:.1f} с") # ~0.3, а не 0.6
asyncio.run(main())
Типизация и инструменты
Проверяет ли Python аннотации типов во время выполнения?
Нет. Аннотации — метаданные (__annotations__), интерпретатор их не проверяет. Проверяют статические анализаторы (mypy, pyright) до запуска, а библиотеки вроде Pydantic и FastAPI используют аннотации для валидации данных во время выполнения.
def double(x: int) -> int:
return x * 2
print(double("ab"), double.__annotations__)
Как вы ищете проблемы с производительностью?
Сначала измерить, потом оптимизировать: timeit для микробенчмарков, cProfile + snakeviz для поиска горячих функций, py-spy для работающего процесса, tracemalloc для памяти, логирование медленных SQL-запросов. Типичные находки: N+1 запросы к базе, поиск в списке вместо множества, лишние копирования, отсутствие кеша, синхронные вызовы в async-коде. Затем — алгоритм и структуры данных, кеширование, батчинг, и только потом — C-расширения или PyPy.
Итог
- ООП: MRO и кооперативный
super(), магические методы, дескрипторы подpropertyи ORM,__slots__, ABC против Protocol, метаклассы и__init_subclass__. - CPython: подсчёт ссылок + поколенческий GC, кеш малых чисел и интернирование, байткод, GIL и способы его обойти.
- Конкурентность: CPU → процессы, много I/O → asyncio, синхронный I/O → потоки; гонки лечат блокировками и атомарными операциями.
Практика решено 0 из 4
Классы: class A: ..., class B(A): ..., class C(A): ..., class D(B, C): .... Запишите MRO класса D через запятую (без object), например: X, Y, Z
Что лучше подходит для задачи?
| threading | multiprocessing | asyncio | |
|---|---|---|---|
| Посчитать хеши тысячи больших файлов на 8 ядрах (CPU-bound) | |||
| 10 000 одновременных WebSocket-соединений | |||
| Параллельно скачать 20 файлов синхронной библиотекой requests | |||
| Обработать большую матрицу чистым Python-кодом на всех ядрах |
Что такое GIL и на что он влияет?
Что верно про @staticmethod и @classmethod?
Войдите, чтобы отмечать пройденные уроки.