ООП в Python устроено проще, чем в Java: нет модификаторов доступа, интерфейсы заменяет утиная типизация, а почти всё поведение объекта настраивается магическими методами. Именно про них и про MRO любят спрашивать.
Класс, экземпляр, атрибуты
class User:
role = 'guest' # атрибут класса, общий для всех
def __init__(self, name):
self.name = name # атрибут экземпляра
u = User('Аня')
u.role # 'guest': не нашли в экземпляре, нашли в классе
u.role = 'admin' # создали атрибут экземпляра, класс не изменился
Поиск атрибута: сначала u.__dict__, потом класс и его родители по MRO. Изменяемый атрибут класса (items = []) общий для всех экземпляров: классическая ловушка.
__init__ не конструктор, а инициализатор: объект уже создан методом __new__. __new__ переопределяют редко: для неизменяемых типов (наследник str, tuple) и синглтонов.
Методы: обычный, класса, статический
- Обычный метод получает экземпляр
self. @classmethodполучает классcls; типичное применение: альтернативные конструкторыUser.from_dict(data), которые правильно работают в наследниках.@staticmethodничего не получает, это просто функция в пространстве имён класса.
Инкапсуляция по соглашению
_name значит «внутреннее, не трогайте». __name включает искажение имён (name mangling): внутри класса A оно становится _A__name. Это защита от случайных конфликтов в наследниках, а не приватность.
@property превращает метод в атрибут с вычислением и проверкой, не ломая внешний интерфейс.
Наследование и MRO
Python поддерживает множественное наследование. Порядок поиска методов (MRO) строится алгоритмом C3-линеаризации: потомок раньше предков, порядок родителей из объявления сохраняется.
class A: ...
class B(A): ...
class C(A): ...
class D(B, C): ...
D.__mro__ # D, B, C, A, object
super() вызывает следующий класс в MRO экземпляра, а не обязательно прямого родителя. Поэтому в «ромбе» каждый __init__ с super().__init__() выполнится ровно один раз.
Магические методы
| Метод | Когда вызывается |
|---|---|
__repr__, __str__ | repr(x) и отладка, str(x) и print |
__eq__, __hash__ | ==, ключи словаря |
__lt__ и др. | сравнения, sorted |
__len__, __getitem__, __contains__, __iter__ | протокол коллекции |
__call__ | вызов объекта как функции |
__enter__, __exit__ | with |
__getattr__ | только если обычный поиск атрибута не нашёл ничего |
Если определить __eq__ без __hash__, Python выставит __hash__ = None, и объекты перестанут быть хешируемыми.
Дескрипторы и slots (senior)
Дескриптор это объект с __get__/__set__, лежащий в атрибуте класса. На дескрипторах построены property, classmethod, staticmethod и сами методы (функция это дескриптор, который возвращает связанный метод).
__slots__ = ('x', 'y') убирает __dict__ у экземпляров: память меньше, доступ чуть быстрее, но новые атрибуты добавить нельзя.
dataclasses
from dataclasses import dataclass, field
@dataclass(frozen=True, slots=True)
class Point:
x: float
y: float
tags: list[str] = field(default_factory=list)
Генерирует __init__, __repr__, __eq__; с order=True сравнения; с frozen=True запрещает присваивание и даёт __hash__. Изменяемое значение по умолчанию задают только через default_factory. Для валидации входных данных в вебе обычно берут Pydantic, а dataclass оставляют для внутренних структур.
Метаклассы (senior)
Класс это объект, созданный метаклассом (по умолчанию type). Метакласс управляет созданием классов: регистрация, проверка, автодобавление методов. В современном коде большую часть этих задач решают __init_subclass__ и декораторы классов.
Что спросят на собесе
- Разница между атрибутом класса и экземпляра,
@classmethodи@staticmethod. - Что такое MRO, как работает
super()при множественном наследовании. - Зачем
__repr__, связь__eq__и__hash__. - Что делает
__slots__, что такое дескриптор иproperty. - Что генерирует
@dataclassи почему нельзяtags: list = [].