← Пример: bound methods · 🏠 Домой · Управление памятью →
Коротко. MRO (Method Resolution Order) — линейный порядок классов, в котором
ищется атрибут. Строится алгоритмом C3-линеаризации. super() не «вызывает
родителя», а передаёт управление следующему классу в MRO текущего объекта —
и этот класс может вообще не быть предком того, где написан super().
class A:
def who(self): return "A"
class B(A):
def who(self): return "B->" + super().who()
class C(A):
def who(self): return "C->" + super().who()
class D(B, C):
def who(self): return "D->" + super().who()
[c.__name__ for c in D.__mro__]
# ['D', 'B', 'C', 'A', 'object']
D().who()
# 'D->B->C->A'flowchart TB
subgraph hierarchy["Иерархия (ромб)"]
direction TB
D1["D"] --> B1["B"]
D1 --> C1["C"]
B1 --> A1["A"]
C1 --> A1
A1 --> O1["object"]
end
flowchart TB
subgraph mro["MRO — линейный порядок обхода super()"]
direction LR
m1["D"] --> m2["B"] --> m3["C"] --> m4["A"] --> m5["object"]
end
Обратите внимание: из B вызов super() уходит в C, который не является
предком B — порядок задаёт MRO конкретного экземпляра, а не статическая
иерархия класса, где написан вызов.
Это ответ на diamond problem: A вызывается один раз, а super() внутри
B уходит в C, хотя C не является родителем B. Правила C3: сам класс
идёт первым, порядок базовых классов сохраняется, и каждый класс появляется
после всех своих потомков.
Подвох. Не любую иерархию можно линеаризовать — противоречивый порядок баз падает ещё на объявлении класса:
class Bad(A, B): ...
# TypeError: Cannot create a consistent method resolution order (MRO)
# for bases A, BB — потомок A, значит должен идти раньше, но в списке баз он записан
позже. Отсюда практическое правило: базовые классы перечисляют от частного
к общему.
Глубже. Из «super() идёт по MRO экземпляра» следует требование
кооперативного наследования: сигнатуры методов по цепочке должны быть
совместимы, и super().__init__(**kwargs) вызывают все классы в цепочке,
а не только те, у кого «есть родитель». Посмотреть цепочку — D.__mro__
или D.mro().
Коротко. Дескриптор — объект с методом __get__ (и, возможно,
__set__/__delete__), лежащий в классе как атрибут. Если есть __set__
или __delete__ — это data-дескриптор, и он приоритетнее __dict__
экземпляра; если только __get__ — non-data, и его перекрывает запись
в экземпляр.
class Positive:
def __set_name__(self, owner, name):
self.name = "_" + name # имя поля, куда прячем значение
def __get__(self, obj, objtype=None):
if obj is None:
return self # обращение через класс
return getattr(obj, self.name)
def __set__(self, obj, value):
if value < 0:
raise ValueError("must be >= 0")
setattr(obj, self.name, value)
class Account:
balance = Positive()
def __init__(self, b):
self.balance = b # уже проходит через __set__
a = Account(10)
a.balance, a.__dict__
# (10, {'_balance': 10})
a.balance = -1
# ValueError: must be >= 0Разница видна на non-data дескрипторе — его легко «затереть»:
class NonData:
def __get__(self, obj, objtype=None): return "from descriptor"
class Q:
x = NonData()
q = Q()
q.x # from descriptor
q.x = "from instance"
q.x # from instance — non-data проигралПолный порядок поиска obj.attr: data-дескриптор в классе → obj.__dict__ →
non-data дескриптор или обычный атрибут класса → __getattr__.
Подвох. Именно поэтому обычный метод (у функции есть только __get__,
это non-data дескриптор) можно подменить у конкретного экземпляра,
а property (data-дескриптор) — нельзя: присваивание уйдёт в её сеттер,
а без сеттера даст AttributeError.
Глубже. На дескрипторах построены property, classmethod,
staticmethod, functools.cached_property, поля dataclass со значениями по
умолчанию и ORM-модели. Ручная реализация первых трёх разобрана в
примере с bound methods. Метод __set_name__
(Python 3.6+) вызывается при создании класса и избавляет от дублирования
имени поля в конструкторе дескриптора.
Коротко. __slots__ фиксирует набор атрибутов экземпляра: вместо
__dict__ создаются слоты-дескрипторы с фиксированными смещениями. Экономия
памяти в разы плюс чуть более быстрый доступ.
import sys
class WithDict:
def __init__(self): self.a, self.b = 1, 2
class WithSlots:
__slots__ = ("a", "b")
def __init__(self): self.a, self.b = 1, 2
d = WithDict()
sys.getsizeof(d) + sys.getsizeof(d.__dict__) # 344 байта
sys.getsizeof(WithSlots()) # 48 байтПлата:
- нельзя добавить атрибут, которого нет в
__slots__; - нет
__dict__, значит не работаетvars(obj)и код, который его ждёт; - нет
__weakref__, если не добавить его в__slots__явно; - при множественном наследовании нельзя объединить два класса
с непустыми
__slots__; - достаточно одному классу в цепочке не объявить
__slots__— и__dict__вернётся, а экономия пропадёт.
s = WithSlots()
s.c = 3
# AttributeError: WithSlots object has no attribute c
# and no __dict__ for setting new attributesПодвох. __slots__ — оптимизация под много экземпляров (миллионы
точек, строк лога, узлов графа). На классе, у которого будет пять объектов,
это лишь ограничение без выгоды. Готовая форма — @dataclass(slots=True)
(см. Классы). Ещё одна ловушка: если объявить
слот и дескриптор (например @property) с одним и тем же именем, они
конкурируют за один и тот же дескриптор на уровне класса — получится
AttributeError или property молча перекроет слот.
Коротко. Метакласс — класс, экземплярами которого являются классы. По
умолчанию это type. Кастомный метакласс перехватывает создание класса:
регистрация плагинов, валидация объявления, автоподстановка атрибутов.
registry = {}
class PluginMeta(type):
def __new__(mcls, name, bases, ns):
cls = super().__new__(mcls, name, bases, ns)
if bases: # сам Plugin не регистрируем
registry[name] = cls
return cls
class Plugin(metaclass=PluginMeta): ...
class Json(Plugin): ...
class Csv(Plugin): ...
sorted(registry) # ['Csv', 'Json']
type(Json) # <class PluginMeta>
type(int) # <class type>Сопутствующий вопрос — разница __new__ и __init__: __new__ создаёт
объект и возвращает его, __init__ только настраивает уже созданный. Для
классов ровно то же самое, но на уровне метакласса.
Подвох. В подавляющем большинстве случаев метакласс не нужен. Регистрацию
подклассов закрывает __init_subclass__, настройку дескрипторов —
__set_name__, а изменение класса целиком — декоратор класса. Метаклассы
ещё и конфликтуют при наследовании: у потомка метакласс обязан быть подклассом
всех метаклассов баз, иначе TypeError.
class Base:
def __init_subclass__(cls, /, tag=None, **kwargs):
super().__init_subclass__(**kwargs)
print("subclass:", cls.__name__, tag)
class Sub(Base, tag="x"): ...
# subclass: Sub xКоротко. Объект станет нехешируемым: Python выставит __hash__ = None.
Иначе нарушился бы контракт «равные объекты имеют равный хеш».
class P:
def __init__(self, x): self.x = x
def __eq__(self, other):
return isinstance(other, P) and self.x == other.x
{P(1)}
# TypeError: unhashable type: 'P'
P.__hash__
# NoneЧинится определением __hash__ по тем же полям, что и __eq__:
class P:
def __init__(self, x): self.x = x
def __eq__(self, other): return isinstance(other, P) and self.x == other.x
def __hash__(self): return hash(self.x)Изменяемые объекты часто оставляют нехешируемыми намеренно — иначе объект,
положенный в set, «потеряется» после изменения поля
(см. Типы данных). У @dataclass то же правило
автоматизировано: eq=True (по умолчанию) выключает хеш, а frozen=True
его возвращает.
Глубже. Соседняя пара — __repr__ и __str__. __repr__ предназначен
для разработчика и по возможности однозначен (P(x=1)), __str__ — для
пользователя. Если определён только __repr__, str() использует его;
наоборот — нет. Поэтому в своих классах в первую очередь пишут __repr__.