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 + ytype(x).__add__(x, y), for i in xtype(x).__iter__(x), with xtype(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.

Sources

Related topics