Пример 1. Сравнение книг
Реализуем __eq__ и __lt__, чтобы книги сравнивались по названию, а не по адресу в памяти.
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]))
Что происходит: sorted() использует __lt__. __repr__ нужен для читаемого вывода списка.
Пример 2. Автоматический порядок через @total_ordering
Декоратор генерирует недостающие операторы на основе __eq__ и __lt__.
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 не защитит вас от всех ситуаций самостоятельно.
Пример 3. Коллекция заметок
Класс Notes ведёт себя как словарь с нечувствительными к регистру ключами и фильтрацией пустых строк.
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)
Почему так: нормализация ключа происходит в одном месте. При попытке сохранить пустую строку мы сразу бросаем ValueError — это guard clause.
Пример 4. Сложение векторов
__add__ возвращает новый объект, не изменяя исходные векторы.
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)
print(a, b) # (2, 3) (1, 4)
Пример 5. Изменяемый счётчик
__iadd__ изменяет объект на месте и возвращает self.
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)
Пример 6. Логическое значение счётчика
__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), bool(c2)) # False True
if c1:
print("Есть элементы")
else:
print("Пусто")
Пример 7. Объект-конвертер валют
__call__ превращает объект в функцию с внутренним состоянием.
class CurrencyConverter:
def __init__(self, rate: float) -> None:
self.rate = rate
def __call__(self, dollars: float) -> float:
return dollars * self.rate
euro = CurrencyConverter(0.88)
print(euro(10)) # 8.8
print(euro(5.5)) # 4.84
Пример 8. Сводный пример: абстракция, исключения и магические методы вместе
Итоговый пример блока ООП: абстрактный товар (урок 76), своя иерархия исключений (урок 76), инкапсуляция корзины (урок 74) и магические методы этого урока в одном приложении.
from abc import ABC, abstractmethod
class ProductError(Exception):
"""Базовая ошибка товара."""
class InvalidPriceError(ProductError):
"""Некорректная цена товара."""
class Product(ABC):
"""
Абстрактный товар.
Attributes:
name (str): Название товара.
price (float): Цена товара.
"""
def __init__(self, name: str, price: float) -> None:
if price < 0:
raise InvalidPriceError("Цена не может быть отрицательной.")
self.name = name
self.price = price
@abstractmethod
def get_description(self) -> str:
"""Возвращает описание товара."""
def __str__(self) -> str:
return f"{self.name}: {self.price:.2f} €"
def __repr__(self) -> str:
return f"{self.__class__.__name__}(name={self.name!r}, price={self.price!r})"
def __eq__(self, other) -> bool:
if not isinstance(other, Product):
return NotImplemented
return self.name == other.name and self.price == other.price
def __lt__(self, other) -> bool:
if not isinstance(other, Product):
return NotImplemented
return self.price < other.price
class Phone(Product):
def __init__(self, name: str, price: float, memory: int) -> None:
super().__init__(name, price)
self.memory = memory
def get_description(self) -> str:
return f"Телефон {self.name}, память {self.memory} ГБ"
class Laptop(Product):
def __init__(self, name: str, price: float, ram: int) -> None:
super().__init__(name, price)
self.ram = ram
def get_description(self) -> str:
return f"Ноутбук {self.name}, оперативная память {self.ram} ГБ"
class ShoppingCart:
"""Корзина товаров."""
def __init__(self) -> None:
self._products: list[Product] = []
def add(self, product: Product) -> None:
if not isinstance(product, Product):
raise TypeError("В корзину можно добавлять только товары.")
self._products.append(product)
def total(self) -> float:
return sum(product.price for product in self._products)
def __len__(self) -> int:
return len(self._products)
def __iter__(self):
return iter(self._products)
def __getitem__(self, index: int) -> Product:
return self._products[index]
def __contains__(self, product: Product) -> bool:
return product in self._products
def __str__(self) -> str:
return f"Корзина: {len(self)} товар(а), сумма {self.total():.2f} €"
Использование — обратите внимание, что корзина ведёт себя как встроенная коллекция:
phone = Phone(name="Google Pixel", price=899, memory=256)
laptop = Laptop(name="Lenovo ThinkPad", price=1400, ram=32)
cart = ShoppingCart()
cart.add(phone)
cart.add(laptop)
print(cart) # Корзина: 2 товар(а), сумма 2299.00 €
print(len(cart)) # 2 → __len__
print(cart[0]) # Google Pixel: 899.00 € → __getitem__
print(phone in cart) # True → __contains__
print(phone < laptop) # True → __lt__
print(repr(phone)) # Phone(name='Google Pixel', price=899)
for product in cart: # → __iter__
print(product.get_description())
try:
Phone(name="Ошибка", price=-100, memory=128)
except InvalidPriceError as error:
print(f"Не удалось создать товар: {error}")
Что даёт такая связка: абстрактный класс гарантирует наличие get_description() у любого товара, своё исключение отделяет ошибку предметной области от системных, а магические методы позволяют работать с корзиной привычными средствами языка — len(), in, индексы и for.