Магические (Dunder) методы Python
Магические методы Python: __init__, __str__, __repr__, перегрузка операторов, протоколы контейнеров и менеджеров контекста с примерами.
Магические методы — также называемые dunder-методами (от double-underscore, двойное подчёркивание) — это специальные методы, имена которых начинаются и заканчиваются двумя символами подчёркивания, например __init__ или __len__. Они представляют собой систему хуков Python: определяя их в собственных классах, вы указываете Python, как объект должен вести себя со встроенными операторами и функциями, такими как +, len(), print(), in и with.
Dunder-методы никогда не вызываются напрямую. Вместо этого Python вызывает их автоматически за кулисами:
| Выражение Python | Вызываемый dunder |
|---|---|
str(obj) | obj.__str__() |
len(obj) | obj.__len__() |
a + b | a.__add__(b) |
a == b | a.__eq__(b) |
item in obj | obj.__contains__(item) |
with obj as x: | obj.__enter__() / obj.__exit__(...) |
В этой главе рассматриваются:
- Строковое представление —
__repr__и__str__ - Операторы сравнения —
__eq__,__lt__и другие - Арифметические операторы —
__add__,__mul__,__rmul__и другие - Протокол контейнера —
__len__,__getitem__,__contains__ - Протокол итератора —
__iter__и__next__ - Проверка истинности —
__bool__ - Вызываемые объекты —
__call__ - Протокол менеджера контекста —
__enter__и__exit__ __hash__— использование объектов в качестве ключей словаря
Перед чтением убедитесь, что вы знакомы с классами и объектами Python и наследованием в Python. Подробнее о вычисляемых атрибутах — см. @property.
Строковое представление: __repr__ и __str__
Эти два метода управляют преобразованием объекта в строку.
| Метод | Вызывается | Назначение |
|---|---|---|
__repr__ | repr(), интерактивная оболочка | Однозначное представление для разработчика |
__str__ | str(), print(), f-строки | Читаемый вывод для пользователя |
Если __str__ не определён, Python использует __repr__. Поэтому рекомендуется всегда определять __repr__ и определять __str__ только тогда, когда нужен отдельный человекочитаемый формат.
class Book:
def __init__(self, title, author, pages):
self.title = title
self.author = author
self.pages = pages
def __repr__(self):
return f"Book(title={self.title!r}, author={self.author!r}, pages={self.pages})"
def __str__(self):
return f'"{self.title}" by {self.author} ({self.pages} pages)'
b = Book("Clean Code", "Robert C. Martin", 431)
print(repr(b)) # Book(title='Clean Code', author='Robert C. Martin', pages=431)
print(str(b)) # "Clean Code" by Robert C. Martin (431 pages)
print(b) # "Clean Code" by Robert C. Martin (431 pages)Флаг преобразования !r внутри f-строки вызывает repr() для этого значения, что оборачивает строки в кавычки. Это делает вывод __repr__ копируемым кодом Python.
Совет: хороший __repr__ позволяет воссоздать объект из его вывода. Считайте это ментальной моделью eval(repr(obj)) == obj (даже если буквально это не всегда верно).
Операторы сравнения
Все операторы сравнения Python отображаются на dunder-методы. Определяйте их, если хотите, чтобы ==, <, >, <= или >= осмысленно сравнивали ваши объекты.
| Оператор | Метод | Отражённый метод |
|---|---|---|
== | __eq__ | __eq__ |
!= | __ne__ | __ne__ |
< | __lt__ | __gt__ |
<= | __le__ | __ge__ |
> | __gt__ | __lt__ |
>= | __ge__ | __le__ |
Отражённый означает, что Python вызывает метод правого операнда, когда левый операнд возвращает NotImplemented. Например, если a < b вызывает a.__lt__(b) и тот возвращает NotImplemented, Python пробует отражённый метод: b.__gt__(a).
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
def __eq__(self, other):
if not isinstance(other, Temperature):
return NotImplemented
return self.celsius == other.celsius
def __lt__(self, other):
if not isinstance(other, Temperature):
return NotImplemented
return self.celsius < other.celsius
def __le__(self, other):
if not isinstance(other, Temperature):
return NotImplemented
return self.celsius <= other.celsius
def __repr__(self):
return f"Temperature({self.celsius}°C)"
t1 = Temperature(20)
t2 = Temperature(30)
t3 = Temperature(20)
print(t1 == t3) # True
print(t1 < t2) # True
print(t2 > t1) # True — Python derives __gt__ from __lt__ via reflection
print(t1 <= t3) # TrueСокращение: если нужно только сделать объекты сортируемыми без реализации всех шести операторов, используйте декоратор @functools.total_ordering. Определите __eq__ и один из __lt__, __le__, __gt__ или __ge__, а total_ordering автоматически заполнит остальные.
from functools import total_ordering
@total_ordering
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
def __eq__(self, other):
if not isinstance(other, Temperature):
return NotImplemented
return self.celsius == other.celsius
def __lt__(self, other):
if not isinstance(other, Temperature):
return NotImplemented
return self.celsius < other.celsiusАрифметические операторы
Арифметические dunder-методы позволяют объектам работать с +, -, *, /, //, % и **.
| Выражение | Метод | Примечания |
|---|---|---|
a + b | __add__ | |
a - b | __sub__ | |
a * b | __mul__ | |
b * a | __rmul__ | правосторонняя версия; вызывается, когда b.__mul__(a) возвращает NotImplemented |
-a | __neg__ | унарное отрицание |
abs(a) | __abs__ | |
a += b | __iadd__ | на месте; отступает к __add__, если не определён |
Классический пример — класс двумерного вектора:
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
def __sub__(self, other):
return Vector(self.x - other.x, self.y - other.y)
def __mul__(self, scalar):
return Vector(self.x * scalar, self.y * scalar)
def __rmul__(self, scalar): # supports: 3 * v
return self.__mul__(scalar)
def __neg__(self):
return Vector(-self.x, -self.y)
def __abs__(self):
return (self.x ** 2 + self.y ** 2) ** 0.5
def __repr__(self):
return f"Vector({self.x}, {self.y})"
v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1 + v2) # Vector(4, 6)
print(v2 - v1) # Vector(2, 2)
print(v1 * 3) # Vector(3, 6)
print(3 * v1) # Vector(3, 6) — uses __rmul__
print(-v1) # Vector(-1, -2)
print(abs(v2)) # 5.0__rmul__ позволяет работать выражению 3 * v1. Когда Python вычисляет 3 * v1, он сначала вызывает int.__mul__(3, v1). Встроенный класс целых чисел не знает, как умножить целое число на Vector, поэтому возвращает NotImplemented. Тогда Python пробует отражённый метод: v1.__rmul__(3), который успешно выполняется.
Протокол контейнера
Реализуйте эти методы, чтобы ваш класс вёл себя как последовательность или коллекция.
| Метод | Вызывается | Что обеспечивает |
|---|---|---|
__len__ | len(obj) | Длина контейнера |
__getitem__ | obj[index] | Доступ по индексу и срезу |
__setitem__ | obj[index] = val | Присваивание по индексу |
__delitem__ | del obj[index] | Удаление по индексу |
__contains__ | item in obj | Проверка вхождения |
Определения __len__ вместе с __getitem__ достаточно, чтобы класс стал автоматически итерируемым — цикл for в Python будет вызывать __getitem__ с последовательными индексами, начиная с 0, пока не получит IndexError.
class WordBag:
def __init__(self, *words):
self._words = list(words)
def __len__(self):
return len(self._words)
def __contains__(self, item):
return item in self._words
def __getitem__(self, index):
return self._words[index]
def __repr__(self):
return f"WordBag({self._words!r})"
bag = WordBag("apple", "banana", "cherry")
print(len(bag)) # 3
print("banana" in bag) # True
print("grape" in bag) # False
print(bag[0]) # apple
print(bag[-1]) # cherry
# __len__ + __getitem__ makes the object iterable automatically
for word in bag:
print(word)
# apple
# banana
# cherryПротокол итератора
Если нужно полноценное поведение итератора (работа с iter() и next() напрямую или использование в местах, требующих именно итератор, а не просто итерируемый объект), определите и __iter__, и __next__:
__iter__— вызываетсяiter(obj)и в начале циклаfor; должен возвращать объект итератора (обычноself).__next__— вызывается последовательно для получения следующего значения; должен генерироватьStopIterationпри исчерпании значений.
class Countdown:
def __init__(self, start):
self.start = start
def __iter__(self):
self.current = self.start
return self
def __next__(self):
if self.current < 0:
raise StopIteration
value = self.current
self.current -= 1
return value
for n in Countdown(3):
print(n)
# 3
# 2
# 1
# 0Для более мощных паттернов итерации — особенно ленивых последовательностей, генерирующих значения по требованию — см. генераторы Python и итераторы Python.
Проверка истинности: __bool__
Python вызывает __bool__, когда объект используется в boolean-контексте (оператор if, цикл while, not, and, or). Если __bool__ не определён, но определён __len__, Python использует len(obj) != 0 как значение истинности. Если не определён ни один из них, объект всегда считается истинным.
class Stack:
def __init__(self):
self._data = []
def push(self, item):
self._data.append(item)
def pop(self):
return self._data.pop()
def __len__(self):
return len(self._data)
def __bool__(self):
return len(self._data) > 0
def __repr__(self):
return f"Stack({self._data!r})"
s = Stack()
print(bool(s)) # False — empty stack is falsy
s.push(1)
print(bool(s)) # True
print(len(s)) # 1
if s:
print("stack has items") # stack has itemsЭто отражает поведение встроенных коллекций: пустой список, dict или set — ложные; непустой — истинный.
Вызываемые объекты: __call__
Определение __call__ позволяет использовать экземпляр как функцию. Это полезно для объектов, которые сохраняют состояние между вызовами — чего обычная функция не может сделать без замыкания или глобальной переменной.
class Multiplier:
def __init__(self, factor):
self.factor = factor
def __call__(self, value):
return value * self.factor
double = Multiplier(2)
triple = Multiplier(3)
print(double(5)) # 10
print(triple(5)) # 15
print(callable(double)) # Truedouble и triple — обычные объекты, но вызываются с () так же, как функции. Встроенная функция callable() возвращает True для любого объекта, имеющего __call__.
Этот паттерн распространён в фреймворках машинного обучения (слои, функции потерь) и в фабриках декораторов. См. декораторы Python — близкий по теме пример использования.
Протокол менеджера контекста: __enter__ и __exit__
Оператор with — это способ Python надёжно устанавливать и освобождать ресурс, даже если возникает исключение. Любой объект, определяющий __enter__ и __exit__, может использоваться как менеджер контекста.
__enter__(self)— выполняется при входе в блокwith; возвращаемое значение привязывается к переменнойas.__exit__(self, exc_type, exc_val, exc_tb)— выполняется при выходе из блока, нормальном или через исключение. ВернитеTrue, чтобы подавить исключение; вернитеFalse(илиNone), чтобы дать ему распространиться.
class ManagedFile:
def __init__(self, path, mode="r"):
self.path = path
self.mode = mode
self._file = None
def __enter__(self):
self._file = open(self.path, self.mode)
return self._file # the value bound to the "as" variable
def __exit__(self, exc_type, exc_val, exc_tb):
if self._file:
self._file.close()
return False # do not suppress exceptions
with ManagedFile("/etc/hostname") as f:
content = f.read()
# The file is guaranteed to be closed here, even if an exception occurred inside the block.Декоратор contextlib.contextmanager из стандартной библиотеки позволяет написать ту же логику в виде функции-генератора — более лёгкая альтернатива для простых случаев. Полное описание см. в оператор with в Python.
Хэширование: __hash__
Python использует __hash__ для размещения объектов в множествах и словарях. По умолчанию __hash__ основан на адресе объекта в памяти (идентичности). Когда вы переопределяете __eq__, Python автоматически устанавливает __hash__ в None, делая объекты нехэшируемыми — если вы всё равно хотите использовать их в множествах или как ключи dict, нужно явно определить __hash__.
Правило: объекты, равные при сравнении, должны иметь одинаковый хэш.
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __eq__(self, other):
if not isinstance(other, Point):
return NotImplemented
return self.x == other.x and self.y == other.y
def __hash__(self):
return hash((self.x, self.y)) # hash of an immutable tuple
def __repr__(self):
return f"Point({self.x}, {self.y})"
p1 = Point(1, 2)
p2 = Point(1, 2)
p3 = Point(3, 4)
print(p1 == p2) # True
print(p1 is p2) # False — different objects in memory
print(hash(p1) == hash(p2)) # True
seen = {p1, p2, p3}
print(len(seen)) # 2 — p1 and p2 are equal, so only one copy kept
print(p1 in seen) # TrueЕсли ваш класс является изменяемым (его поля могут меняться после создания), не определяйте __hash__. Изменяемые объекты не должны быть хэшируемыми, поскольку изменение их полей изменит хэш, нарушив работу любого множества или словаря, уже содержащего их.
Сводная таблица
| Категория | Метод | Вызывается при |
|---|---|---|
| Представление | __repr__ | repr(obj), интерактивная оболочка |
| Представление | __str__ | str(obj), print(obj), f-строки |
| Сравнение | __eq__, __ne__ | ==, != |
| Сравнение | __lt__, __le__, __gt__, __ge__ | <, <=, >, >= |
| Арифметика | __add__, __sub__, __mul__ | +, -, * |
| Арифметика | __rmul__, __radd__, … | правосторонние отражённые формы |
| Арифметика | __neg__, __abs__ | унарный -, abs() |
| Контейнер | __len__ | len(obj) |
| Контейнер | __getitem__, __setitem__, __delitem__ | obj[i], obj[i] = v, del obj[i] |
| Контейнер | __contains__ | item in obj |
| Итератор | __iter__ | iter(obj), цикл for |
| Итератор | __next__ | next(obj) |
| Истинность | __bool__ | bool(obj), if obj: |
| Вызываемый | __call__ | obj(args) |
| Менеджер контекста | __enter__, __exit__ | with obj as x: |
| Хэширование | __hash__ | hash(obj), ключи dict, множества |
Когда использовать магические методы
- Используйте их, когда класс представляет тип-значение (точку, вектор, денежную сумму, диапазон дат) — перегрузка операторов и сравнений делает класс естественным в использовании.
- Используйте их, когда класс оборачивает ресурс (файл, соединение с базой данных, сетевой сокет) —
__enter__/__exit__гарантирует, что ресурс всегда будет освобождён. - Используйте их, когда класс является пользовательской коллекцией — протоколы контейнера и итератора позволяют работать с
for,in,len()и списковыми включениями. - Избегайте их для обычных классов приложения, которые не являются типами-значениями или контейнерами. Перегрузка
+для классаUserбыла бы запутывающей.
Для более продвинутых паттернов ООП см. абстрактные классы Python, инкапсуляцию в Python и полиморфизм в Python.