$ sudo teach IT
МОДУЛЬ 7 · УРОК 1

Магические методы (dunder methods) — обзор

Python — очень «умный» язык. Когда вы пишете len(obj), print(obj) или a + b, Python не просто выполняет операцию — он ищет у вашего объекта специальный метод и вызывает его. Эти методы называют магическими или dunder-методами, и они открывают двери к подлинной интеграции ваших классов в экосистему Python.

⏱ ~40 минут 🎓 Для новичков 🐍 Python 3

🔤 Откуда название «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 «как родные».