Вы уже умеете создавать классы, наследовать их и использовать полиморфизм. Но знаете ли вы, что можно заставить ваш собственный объект вести себя как список? Или как число? Или чтобы его можно было вызвать как функцию? Всё это делают магические методы (их ещё называют dunder-методы — от double underscore). Это специальные имена вроде __init__, __str__, __add__, которые Python вызывает автоматически в определённых ситуациях.
Сегодня мы разберём самые полезные магические методы и превратим скучный класс в интеллектуальный объект.
1. Что такое магические методы?
Это методы, имя которых начинается и заканчивается двумя подчёркиваниями (__). Вы их уже использовали: __init__ вызывается при создании объекта. Но есть десятки других.
Когда вы пишете print(obj), Python ищет у объекта метод __str__. Когда пишете len(obj) — ищет __len__. Когда obj1 + obj2 — ищет __add__. Это позволяет вашим классам органично вписываться в синтаксис языка.
2. Самые важные магические методы
__init__(self, ...) — конструктор
Вызывается при создании объекта. Знаком каждому.
__str__(self) и __repr__(self) — текстовое представление
- __str__ — для пользователя (неформальное, красивое). Используется в print() и str().
- __repr__ — для разработчика (однозначное, желательно чтобы можно было восстановить объект). Используется в отладке, в консоли.
class Book:
def __init__(self, title, author):
self.title = title
self.author = author
def __str__(self):
return f"{self.title} — {self.author}"
def __repr__(self):
return f"Book('{self.title}', '{self.author}')"
b = Book("1984", "Оруэлл")
print(b) # 1984 — Оруэлл (__str__)
print(repr(b)) # Book('1984', 'Оруэлл') (__repr__)
__len__(self) — длина
class Library:
def __init__(self, books):
self.books = books
def __len__(self):
return len(self.books)
lib = Library(["Book1", "Book2"])
print(len(lib)) # 2
__getitem__(self, key) и __setitem__ — доступ по индексу или ключу
Делает объект похожим на список или словарь.
class ShoppingCart:
def __init__(self):
self.items = []
def __getitem__(self, index):
return self.items[index]
def __setitem__(self, index, value):
self.items[index] = value
def add(self, item):
self.items.append(item)
cart = ShoppingCart()
cart.add("яблоко")
cart.add("банан")
print(cart[0]) # яблоко
cart[1] = "груша"
print(cart[1]) # груша
__call__(self, ...) — объект как функция
class Multiplier:
def __init__(self, factor):
self.factor = factor
def __call__(self, x):
return x * self.factor
double = Multiplier(2)
print(double(5)) # 10 (вызываем объект как функцию)
__add__, __sub__, __mul__, __truediv__ — арифметика
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 __str__(self):
return f"({self.x}, {self.y})"
v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1 + v2) # (4, 6)
__eq__, __lt__, __gt__ — сравнение
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __eq__(self, other):
return self.age == other.age
def __lt__(self, other):
return self.age < other.age
alice = Person("Алиса", 30)
bob = Person("Боб", 25)
print(alice == bob) # False
print(bob < alice) # True
3. Полезные менее очевидные методы
Метод Что делает
__contains__(self, item) для item in obj
__iter__(self) делает объект итерируемым (для for x in obj)
__next__(self) для итераторов
__enter__ и __exit__ для контекстного менеджера (with ... as)
__del__(self) деструктор (вызывается при удалении объекта)
4. Типичные ошибки новичков
❌ Забыли вернуть что-то в __add__
Метод должен возвращать новый объект, а не None.
❌ Путают __str__ и __repr__
Если определён только __repr__, то print(obj) использует его как запасной. Но лучше определять оба.
❌ __len__ возвращает не целое число
Должен возвращать int, иначе ошибка.
❌ Слишком много магии
Не стоит переопределять __getitem__, если объект не является по сути коллекцией. Код должен быть предсказуемым.
5. Живой пример: класс "Дробь" с арифметикой
Создадим класс обыкновенной дроби, который умеет складываться, вычитаться, сравниваться и красиво печататься:
import math
class Fraction:
def __init__(self, numerator, denominator):
if denominator == 0:
raise ValueError("Знаменатель не может быть нулём")
self.num = numerator
self.den = denominator
self._reduce()
def _reduce(self):
gcd = math.gcd(self.num, self.den)
self.num //= gcd
self.den //= gcd
def __add__(self, other):
new_num = self.num * other.den + other.num * self.den
new_den = self.den * other.den
return Fraction(new_num, new_den)
def __sub__(self, other):
new_num = self.num * other.den - other.num * self.den
new_den = self.den * other.den
return Fraction(new_num, new_den)
def __mul__(self, other):
return Fraction(self.num * other.num, self.den * other.den)
def __truediv__(self, other):
return Fraction(self.num * other.den, self.den * other.num)
def __eq__(self, other):
return self.num == other.num and self.den == other.den
def __str__(self):
return f"{self.num}/{self.den}"
def __repr__(self):
return f"Fraction({self.num}, {self.den})"
# Использование
a = Fraction(1, 2)
b = Fraction(1, 3)
print(a + b) # 5/6
print(a - b) # 1/6
print(a * b) # 1/6
print(a / b) # 3/2
print(a == Fraction(2, 4)) # True (сократится до 1/2)
Всего несколько методов — и ваши дроби ведут себя как встроенные числа. Магия, да?
Заключение
Магические методы позволяют вашим классам говорить на языке Python. Вы узнали:
- как управлять строковым представлением (__str__, __repr__),
- как сделать объект контейнером (__len__, __getitem__),
- как добавить арифметику (__add__, __sub__ и другие),
- как сделать объект вызываемым (__call__).
Теперь ваш код может быть не только правильным, но и красивым — словно он всегда был частью языка.
Следующая статья будет про итераторы и генераторы — как создавать свои последовательности и эффективно работать с данными. А пока — попробуйте добавить магические методы в один из ваших старых классов. Результат вас удивит.
Пишите в комментариях, какой магический метод показался самым неожиданным и где вы его применили.
Статья подготовлена для канала «Код как искусство». Подписывайтесь, чтобы писать код, который удивляет своей естественностью.