Топ вопросов: ООП, внутренности, конкурентность

⏱ 50 мин · 🧩 заданий: 4

Как пользоваться уроком

Как и в предыдущем уроке: раскройте вопрос, ответьте вслух, сверьтесь и поиграйте с кодом. На 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

Вы не вошли — ответы проверяются, но прогресс не сохранится. Войти или зарегистрироваться.
Задание 1 Ввод ответа

Классы: class A: ..., class B(A): ..., class C(A): ..., class D(B, C): .... Запишите MRO класса D через запятую (без object), например: X, Y, Z

Задание 2 Заполнить таблицу

Что лучше подходит для задачи?

threadingmultiprocessingasyncio
Посчитать хеши тысячи больших файлов на 8 ядрах (CPU-bound)
10 000 одновременных WebSocket-соединений
Параллельно скачать 20 файлов синхронной библиотекой requests
Обработать большую матрицу чистым Python-кодом на всех ядрах
Отметьте ячейку — «+», пустая — «−».
Задание 3 Один вариант ответа

Что такое GIL и на что он влияет?

Задание 4 Несколько вариантов

Что верно про @staticmethod и @classmethod?

Войдите, чтобы отмечать пройденные уроки.