PythonMiddleTechnical
Что такое magic / dunder methods? Какие ты реально использовал?
Dunder/magic methods — методы с __ префиксом и суффиксом, через которые Python подключает класс к data model: операторы, len(), iter(), with, call(), сравнение, hash, attribute access. CPython ищет их на типе, а не на экземпляре.
Что это
Dunder (double underscore) methods — protocol-методы Python data model. Вы не вызываете их напрямую, их вызывает синтаксис: len(x) → type(x).__len__(x), x + y → type(x).__add__(x, y), for i in x → type(x).__iter__(x), with x → type(x).__enter__(x).
Важно: CPython ищет dunder на типе, а не на инстансе — переопределение obj.__len__ = ... не повлияет на len(obj). Это сделано из соображений скорости и предсказуемости.
Группы, которые встречаются чаще всего
- Идентичность и представление:
__repr__(для дебага, обязателен),__str__(для пользователя),__format__. - Сравнение:
__eq__,__hash__,__lt__/__le__/__gt__/__ge__(или@functools.total_ordering). - Контейнерные:
__len__,__contains__,__iter__,__getitem__/__setitem__/__delitem__. - Context manager:
__enter__/__exit__,__aenter__/__aexit__. - Числовые:
__add__/__radd__,__sub__,__mul__,__truediv__,__neg__, in-place__iadd__. - Callable:
__call__— экземпляр становится вызываемым. - Атрибуты:
__getattr__(fallback),__getattribute__(всегда),__setattr__,__delattr__,__slots__. - Lifecycle:
__init__,__new__,__del__,__init_subclass__,__class_getitem__(для Generic типов).
Пример
from functools import total_ordering
@total_ordering
class Version:
def __init__(self, major: int, minor: int, patch: int):
self.t = (major, minor, patch)
def __repr__(self) -> str:
return f"Version({self.t[0]}.{self.t[1]}.{self.t[2]})"
def __eq__(self, other: object) -> bool:
return isinstance(other, Version) and self.t == other.t
def __lt__(self, other: "Version") -> bool:
return self.t < other.t
def __hash__(self) -> int:
return hash(self.t)
# Контейнер
class Page:
def __init__(self, items):
self._items = list(items)
def __len__(self) -> int:
return len(self._items)
def __iter__(self):
return iter(self._items)
def __getitem__(self, idx):
return self._items[idx]
def __contains__(self, x) -> bool:
return x in self._items
def __repr__(self) -> str:
return f"Page({self._items!r})"
# Callable
class Adder:
def __init__(self, n):
self.n = n
def __call__(self, x):
return x + self.n
p = Page([1, 2, 3])
print(len(p), 2 in p, list(p), p[0])
print(Adder(10)(5)) # 15
print(sorted([Version(1, 2, 3), Version(1, 1, 9)]))
Подводные камни
- Реализовать
__eq__без__hash__— экземпляр становится unhashable (Python автоматически выставляет__hash__ = None). - Нарушать симметрию
__eq__:a == bиb == aдолжны давать одинаковый результат, иначе сломаютсяin, dict-lookup, set. - Возвращать не-
NotImplementedиз__add__при неизвестном типе — Python тогда не попробует__radd__у второго операнда. - Динамически менять
__len__на экземпляре — CPython ищет dunder на типе и проигнорирует. - Перегружать
__getattr__и забыть, что он вызывается только если обычный lookup провалился (__getattribute__бросилAttributeError). __del__с побочными эффектами — порядок не гарантирован при выходе из интерпретатора; используйтеweakref.finalize.
Common mistakes
- Перечислять методы без понимания протоколов.
- Вызывать len напрямую вместо len(obj).
- Не знать про поиск специальных методов на типе.
What the interviewer is testing
- Связывает dunder с протоколами.
- Приводит реальные методы из опыта.
- Понимает риски нарушения data model.