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)
@total_ordering — functools.total_ordering, о dataclasses — dataclasses.