💻 Практические примеры

⚡ Минимальный рабочий пример

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)   # True
print(b1 >= b2)  # False

Пример 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.

Проверить по документации: о формате строк см. Formatted string literals, о специальных методах — Special method names.