📖 Теория: магические методы

⚡ Кратко

Магические методы — это «крючки», которые связывают объекты ваших классов со встроенными операторами и функциями Python.

  • Сравнение: __eq__, __lt__ и др. Всегда проверяйте тип операнда и возвращайте NotImplemented, если он не подходит.
  • @total_ordering экономит код: реализуете __eq__ и один метод порядка, остальные генерируются автоматически.
  • Контейнеры: __getitem__, __setitem__, __contains__, __len__.
  • Арифметика: __add__ возвращает новый объект, __iadd__ изменяет текущий и возвращает self.
  • __bool__ управляет истинностью объекта; __call__ позволяет вызывать объект как функцию.

1. Методы сравнения

Магические методы сравнения определяют, как объекты сравниваются при использовании операторов ==, !=, <, <=, >, >=.

МетодОператорНазначение
__eq__==Равенство
__ne__!=Неравенство
__lt__<Меньше
__le__<=Меньше или равно
__gt__>Больше
__ge__>=Больше или равно
from __future__ import annotations


class Book:
    def __init__(self, title: str) -> None:
        self.title = title

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Book):
            return NotImplemented
        return self.title == other.title

    def __lt__(self, other: Book) -> bool:
        if not isinstance(other, Book):
            return NotImplemented
        return self.title < other.title

    def __repr__(self) -> str:
        return f"Book(title={self.title!r})"


b1 = Book("1984")
b2 = Book("1984")
b3 = Book("Brave New World")
print(b1 == b2)        # True
print(b1 < b3)         # True
print(sorted([b3, b1, b2]))  # [Book('1984'), Book('1984'), Book('Brave New World')]

Почему именно так: проверка isinstance защищает от сравнения книги со строкой. Возврат NotImplemented даёт Python шанс попробовать зеркальную операцию (например, other.__lt__(self)) или бросить понятное исключение.

2. Декоратор @total_ordering

Вместо шести методов сравнения можно реализовать только __eq__ и один из методов порядка. Декоратор из functools сгенерирует остальные.

from __future__ import annotations
from functools import total_ordering


@total_ordering
class Book:
    def __init__(self, title: str) -> None:
        self.title = title

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Book):
            return NotImplemented
        return self.title == other.title

    def __lt__(self, other: Book) -> bool:
        return self.title < other.title


b1 = Book("1984")
b2 = Book("Brave New World")
print(b1 < b2)   # < реализован вручную
print(b1 >= b2)  # >= создан автоматически
print(b1 == b1)  # == реализован вручную
print(b1 != b2)  # != создан автоматически

Guard clause: в __lt__ мы не повторяем проверку isinstance, потому что @total_ordering работает корректно, только если __eq__ уже защищён. В реальных проектах лучше проверять тип и здесь тоже.

3. Методы индексации и доступа к элементам

Эти методы позволяют использовать объекты класса как контейнеры: обращаться по ключу, проверять вхождение и получать длину.

МетодНазначениеПример
__getitem__Получить элемент по ключу/индексуobj[key]
__setitem__Установить элемент по ключуobj[key] = value
__contains__Проверить вхождениеkey in obj
__len__Вернуть количество элементовlen(obj)
class Notes:
    def __init__(self) -> None:
        self._data: dict[str, str] = {}

    def __getitem__(self, key: str) -> str:
        return self._data[key.lower()]

    def __setitem__(self, key: str, value: str) -> None:
        if not value.strip():
            raise ValueError("Empty notes are not allowed")
        self._data[key.lower()] = value

    def __contains__(self, key: str) -> bool:
        return key.lower() in self._data

    def __len__(self) -> int:
        return len(self._data)

    def __str__(self) -> str:
        lines = (f"- {k}: {v}" for k, v in self._data.items())
        return "Notes:\n" + "\n".join(lines)


notes = Notes()
notes["Idea"] = "Build a game"
notes["TODO"] = "Finish the project"
print(notes["idea"])      # Build a game
print("todo" in notes)    # True
print(len(notes))         # 2
print(notes)

Почему именно так: ключи приводятся к нижнему регистру в одном месте — в сеттере. Пустые строки отклоняются сразу (guard clause). Это предотвращает неоднородность данных.

4. Арифметические методы

Магические методы позволяют переопределить поведение арифметических операторов для пользовательских классов.

МетодОператорНазначение
__add__+Сложение
__sub__-Вычитание
__mul__*Умножение
__truediv__/Деление
__floordiv__//Целочисленное деление
__mod__%Остаток
__pow__**Возведение в степень
from __future__ import annotations


class Vector:
    def __init__(self, x: float, y: float) -> None:
        self.x = x
        self.y = y

    def __add__(self, other: Vector) -> Vector:
        if not isinstance(other, Vector):
            return NotImplemented
        return Vector(self.x + other.x, self.y + other.y)

    def __str__(self) -> str:
        return f"({self.x}, {self.y})"


a = Vector(2, 3)
b = Vector(1, 4)
c = a + b
print(c)  # (3, 7)

Инкрементные арифметические методы

Операторы +=, -= и т.д. используют методы с префиксом __i.... Метод должен изменить текущий объект и вернуть self.

МетодОператорНазначение
__iadd__+=Сложение с присваиванием
__isub__-=Вычитание с присваиванием
__imul__*=Умножение с присваиванием
__itruediv__/=Деление с присваиванием
__ifloordiv__//=Целочисленное деление с присваиванием
__imod__%=Остаток с присваиванием
__ipow__**=Степень с присваиванием
class Counter:
    def __init__(self, value: int = 0) -> None:
        self.value = value

    def __iadd__(self, other: int) -> Counter:
        self.value += other
        return self

    def __str__(self) -> str:
        return f"Counter({self.value})"


c = Counter(5)
print(id(c))
c += 3
print(id(c))  # id не изменился
print(c)      # Counter(8)

5. Магический метод __bool__

Метод определяет, как объект ведёт себя в логических выражениях: if, while, bool().

class Counter:
    def __init__(self, value: int) -> None:
        self.value = value

    def __bool__(self) -> bool:
        return self.value > 0


c1 = Counter(-5)
c2 = Counter(3)
print(bool(c1))  # False
print(bool(c2))  # True

if c1:
    print("Есть элементы")
else:
    print("Пусто")

6. Магический метод __call__

Метод позволяет обращаться к объекту как к функции: obj(...). Удобно для объектов с внутренним состоянием.

class CurrencyConverter:
    def __init__(self, rate: float) -> None:
        self.rate = rate  # сколько единиц нужно за 1 доллар

    def __call__(self, dollars: float) -> float:
        return dollars * self.rate


euro_converter = CurrencyConverter(0.88)
print(euro_converter(10))   # 8.8
print(euro_converter(5.5))  # 4.84

7. Современные практики

Современный код использует аннотации типов, f-strings, guard clauses и, где уместно, dataclasses. Они убирают шаблонный __init__ и __repr__, оставляя только бизнес-логику.

from __future__ import annotations
from dataclasses import dataclass
from functools import total_ordering


@total_ordering
@dataclass
class Book:
    title: str

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Book):
            return NotImplemented
        return self.title == other.title

    def __lt__(self, other: Book) -> bool:
        return self.title < other.title


b1 = Book("1984")
b2 = Book("Brave New World")
print(b1 < b2)
print(b1 >= b2)
Проверить по документации: о специальных методах см. Data model — Special method names, о @total_orderingfunctools.total_ordering, о dataclasses — dataclasses.