Магические методы (dunder methods) — обзор
Python — очень «умный» язык. Когда вы пишете len(obj), print(obj) или a + b, Python не просто выполняет операцию — он ищет у вашего объекта специальный метод и вызывает его. Эти методы называют магическими или dunder-методами, и они открывают двери к подлинной интеграции ваших классов в экосистему Python.
🔤 Откуда название «dunder»
Вы уже знаете один такой метод — __init__. Обратите внимание: он начинается и заканчивается двойным подчёркиванием. В английском «двойное подчёркивание» — double underscore, которое сокращают до dunder. Произносится как «дандер».
Поэтому любой метод вида __что_то__ называют dunder-методом. Вот несколько примеров, которые вы встретите в этом уроке:
🧱
__init__
Уже знаем — конструктор. Вызывается при создании объекта.
🔤
__str__
Вызывается при print(obj). Возвращает человекочитаемую строку.
📏
__len__
Вызывается при len(obj). Возвращает «длину» объекта.
➕
__add__
Вызывается при a + b. Определяет, что значит «сложить» два объекта.
💡 Важно: dunder-методы пишет программист (вы), а вызывает их сам Python — автоматически, в нужный момент. Вы никогда не пишете obj.__len__() напрямую — вы пишете len(obj), и Python сам разберётся, что нужно вызвать.
✨ Почему их называют «магическими»
Слово «магия» здесь — метафора для неявного автоматического поведения. Когда вы вызываете встроенную функцию или оператор Python, язык за кулисами ищет нужный dunder-метод и вызывает его. Вы этого «не видите» — отсюда и «магия».
Разберём три жизненных примера, чтобы это стало конкретным:
📌 Пример 1: print(obj)
Когда вы пишете print(obj), Python внутри делает примерно print(obj.__str__()). Если у объекта нет __str__, Python выведет что-то некрасивое вроде <MyClass object at 0x...>. Но стоит вам написать метод __str__ — и print покажет ровно то, что вы захотите.
📌 Пример 2: len(obj)
Встроенная функция len() работает со строками, списками, словарями. Почему? Потому что у всех них определён __len__. Когда вы пишете len(obj), Python вызывает obj.__len__(). Хотите, чтобы len() работал с вашим классом? Напишите __len__ — и всё.
📌 Пример 3: a + b
Оператор + работает и для чисел, и для строк, и для списков — потому что у каждого из них определён __add__. Когда Python видит a + b, он вызывает a.__add__(b). Вы можете сделать то же самое для своего класса.
🎯 Главная идея: dunder-методы — это договор между вашим классом и Python. Python говорит: «Если хочешь поддерживать операцию X — реализуй метод __X__». Реализовав нужные методы, вы делаете свой объект «родным» в языке: он будет вести себя так же, как встроенные типы — числа, строки, списки.
🗺️ Карта самых частых dunder-методов
Dunder-методов в Python несколько десятков, но на практике используется небольшой набор. Сгруппируем их по смыслу — так проще запомнить и найти нужный.
🧱 Жизненный цикл объекта
__init__(self, ...)
Конструктор. Вызывается сразу после создания объекта. Инициализируем атрибуты.
__new__(cls, ...)
Создаёт объект (до __init__). Нужен редко — при работе с иммутабельными типами или паттерном Singleton.
__del__(self)
Деструктор. Вызывается когда объект удаляется из памяти. Используется для освобождения ресурсов.
🔤 Строковое представление
__str__(self)
Вызывается при print(obj) и str(obj). Для людей — удобно читать.
__repr__(self)
Вызывается при repr(obj) и в интерактивной консоли. Для разработчиков — максимально полная информация.
➕ Арифметические операторы
__add__(self, other)
Оператор +: a + b
__sub__(self, other)
Оператор -: a - b
__mul__(self, other)
Оператор *: a * b
__truediv__(self, other)
Оператор /: a / b
⚖️ Операторы сравнения
__eq__(self, other)
Равенство: a == b
__lt__(self, other)
Меньше: a < b
__gt__(self, other)
Больше: a > b
__le__ / __ge__
Меньше-или-равно / больше-или-равно: <= и >=
📦 Контейнерный протокол
__len__(self)
Вызывается при len(obj)
__getitem__(self, key)
Доступ по индексу: obj[key]
__setitem__(self, key, value)
Присвоение по индексу: obj[key] = value
__contains__(self, item)
Оператор in: item in obj
🔄 Итерация
__iter__(self)
Делает объект итерируемым — возвращает итератор. Нужен для for x in obj.
__next__(self)
Возвращает следующий элемент итератора. Вызывается каждый шаг цикла for.
🔒 Контекстные менеджеры (оператор with)
__enter__(self)
Вызывается при входе в блок with obj as x:. Возвращает значение, связываемое с x.
__exit__(self, ...)
Вызывается при выходе из блока with — даже если произошла ошибка. Используется для очистки ресурсов.
💻 Пример 1: len(obj) и метод __len__
Представьте класс Library — библиотека с книгами. Было бы удобно узнавать количество книг через len(library), как для обычного списка. Без __len__ это не работает. Давайте добавим его:
class Library:
def __init__(self, name):
self.name = name
self.books = [] # Список книг в библиотеке
def add_book(self, title):
self.books.append(title)
def __len__(self):
# Python вызывает этот метод, когда пишут len(library)
return len(self.books)
lib = Library("Городская библиотека")
lib.add_book("Война и мир")
lib.add_book("Преступление и наказание")
lib.add_book("Мастер и Маргарита")
# Теперь len() работает с нашим объектом!
print(len(lib)) # 3
# Что происходит «за кулисами»:
# len(lib) → lib.__len__() → 3
🔍 Что произошло: Мы определили метод __len__ в классе. Теперь когда где-либо в коде напишут len(lib), Python автоматически вызовет lib.__len__(). Это и есть «магия» — мы написали метод один раз, а Python подключает его к встроенной функции len().
Обратите внимание: внутри __len__ мы написали return len(self.books) — это нормально. self.books — обычный список Python, для него len() работает из коробки. Мы просто делегируем вычисление длины нашего объекта встроенному списку.
💻 Пример 2: оператор + и метод __add__
Создадим класс Vector — математический вектор (точка в пространстве). Векторы можно складывать: (1, 2) + (3, 4) = (4, 6). Чтобы оператор + работал между нашими объектами, нужно реализовать __add__:
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
# Python вызывает этот метод, когда пишут v1 + v2
# other — это второй операнд (правый от знака +)
new_x = self.x + other.x
new_y = self.y + other.y
return Vector(new_x, new_y) # Возвращаем НОВЫЙ вектор с суммой
def __str__(self):
return f"Vector({self.x}, {self.y})"
v1 = Vector(1, 2)
v2 = Vector(3, 4)
# Используем + между нашими объектами!
v3 = v1 + v2
print(v3) # Vector(4, 6)
# Что происходит «за кулисами»:
# v1 + v2 → v1.__add__(v2) → Vector(4, 6)
# Можно складывать цепочкой
v4 = Vector(0, 1)
result = v1 + v2 + v4
print(result) # Vector(4, 7)
💡 Параметр other — это второй объект в операции. Когда Python видит v1 + v2, он вызывает v1.__add__(v2), где self — это v1, а other — это v2. Внутри метода вы можете делать с other что угодно — обращаться к его атрибутам, вызывать его методы.
Заметьте: метод __add__ создаёт и возвращает новый объект Vector. Сами v1 и v2 остаются нетронутыми. Это соответствует тому, как работает сложение чисел: 3 + 4 не меняет тройку, а создаёт новое число 7.
💻 Пример 3: print(obj) без __str__ и с __str__
Пожалуй, самый частый dunder-метод после __init__. Посмотрим наглядно, что происходит, если его НЕТ, и как всё меняется, когда мы его добавляем:
# --- БЕЗ __str__ ---
class Book:
def __init__(self, title, author, year):
self.title = title
self.author = author
self.year = year
book = Book("Мастер и Маргарита", "Булгаков", 1967)
print(book)
# <__main__.Book object at 0x10b3a5f40>
# Некрасиво и бесполезно: адрес объекта в памяти.
print(str(book))
# <__main__.Book object at 0x10b3a5f40>
# То же самое.
# --- С __str__ ---
class Book:
def __init__(self, title, author, year):
self.title = title
self.author = author
self.year = year
def __str__(self):
# Этот метод вызывается при print(book) и str(book)
return f'"{self.title}" — {self.author} ({self.year})'
book = Book("Мастер и Маргарита", "Булгаков", 1967)
print(book)
# "Мастер и Маргарита" — Булгаков (1967)
# Красиво, понятно, полезно!
print(str(book))
# "Мастер и Маргарита" — Булгаков (1967)
# Также работает в f-строках:
message = f"Рекомендую прочитать: {book}"
print(message)
# Рекомендую прочитать: "Мастер и Маргарита" — Булгаков (1967)
🔎 __str__ vs __repr__: Python имеет два метода для строкового представления. __str__ — для людей (красивый вывод), __repr__ — для разработчиков (точная техническая информация, как правило показывает как воссоздать объект: Book('Мастер и Маргарита', 'Булгаков', 1967)). Если определён только __repr__ — Python использует его и вместо __str__. Если определён только __str__ — в консоли всё равно показывается дефолтное представление.
Правило: всегда определяйте хотя бы __repr__ в своих классах. Это сэкономит вам часы на отладке — вы всегда будете видеть осмысленный вывод вместо адреса в памяти.
🔬 Как Python связывает операцию с методом
Вот компактная таблица — что вы пишете в коде и что Python вызывает за кулисами:
| Что вы пишете | Что Python вызывает | Метод |
|---|---|---|
len(obj) |
obj.__len__() |
__len__ |
print(obj) |
obj.__str__() |
__str__ |
repr(obj) |
obj.__repr__() |
__repr__ |
a + b |
a.__add__(b) |
__add__ |
a - b |
a.__sub__(b) |
__sub__ |
a == b |
a.__eq__(b) |
__eq__ |
a < b |
a.__lt__(b) |
__lt__ |
obj[key] |
obj.__getitem__(key) |
__getitem__ |
item in obj |
obj.__contains__(item) |
__contains__ |
with obj as x: |
obj.__enter__() / obj.__exit__(...) |
__enter__, __exit__ |
📚 Где смотреть полный список
Dunder-методов в Python несколько десятков. Все они задокументированы в официальной документации Python в разделе, который называется «Data Model» (Модель данных):
🔗 docs.python.org/3/reference/datamodel.html — официальная документация Python, раздел «Data Model». Здесь полный список всех dunder-методов с подробным описанием, когда и как они вызываются.
Не пытайтесь выучить их все сразу — это не нужно. Работает принцип: столкнулся с задачей → посмотрел, какой dunder-метод её решает → реализовал. Со временем самые популярные запомнятся сами.
💡 Совет: хотите проверить, какие dunder-методы уже определены у объекта? Используйте встроенную функцию dir(). Например, dir([]) покажет список со всеми методами и атрибутами обычного Python-списка, включая __add__, __len__, __iter__ и десятки других.
✅ Итоги урока
- Dunder = double underscore: методы вида
__имя__. Называются «магическими», потому что Python вызывает их автоматически. - Python вызывает dunder-методы при операциях:
len(obj)→__len__,obj1 + obj2→__add__,print(obj)→__str__. - Реализуя dunder-методы, вы делаете свой класс «родным» в Python — он начинает поддерживать стандартные операции и синтаксис.
- Основные категории: жизненный цикл (
__init__,__del__), вывод (__str__,__repr__), арифметика, сравнение, контейнеры, итерация, контекстные менеджеры. - Вы не вызываете dunder-методы напрямую — Python делает это сам. Вы только определяете их.
- Полный список: docs.python.org/3/reference/datamodel.html. Выучивать всё сразу не нужно.
👉 В следующих уроках мы разберём каждую группу методов в деталях. Начнём с самых важных и практичных: __str__ и __repr__ — как делать читаемый вывод для ваших объектов. Затем реализуем арифметику и сравнения, и к концу модуля вы сможете строить классы, которые работают с Python «как родные».