W3docs

Магические (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 + ba.__add__(b)
a == ba.__eq__(b)
item in objobj.__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))  # True

double и 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.

Практика

Практика
Какой dunder-метод вызывает Python, когда объект используется в boolean-контексте, например 'if obj:'?
Какой dunder-метод вызывает Python, когда объект используется в boolean-контексте, например 'if obj:'?
Was this page helpful?