✅ Решения заданий

⚡ Решения в двух словах

  • Задание 1 — Book с __eq__, __lt__ и __repr__.
  • Задание 2 — @total_ordering генерирует остальные сравнения.
  • Задание 3 — Notes с нормализацией ключей и guard clause для пустых строк.
  • Задание 4 — Vector: __add__ и __sub__ возвращают новый объект.
  • Задание 5 — Counter: __iadd__ изменяет self, __bool__ проверяет значение.
  • Задание 6 — CurrencyConverter через __call__.
⚠️ Рекомендация: попробуйте решить задания самостоятельно, прежде чем смотреть ответы. Решения — это последняя инстанция, когда собственные попытки исчерпаны.

Решение задания 1. Сравнение книг

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)
print(b1 < b3)
print(sorted([b3, b1, b2]))

Логика решения: __eq__ проверяет равенство по названию, __lt__ — порядок, __repr__ делает вывод списка читаемым. Проверка типа предотвращает неожиданные ошибки при сравнении с числом или строкой.

Решение задания 2. Автоматический порядок

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

Логика решения: декоратор видит __eq__ и __lt__ и выводит остальные операторы. Это сокращает код, но требует, чтобы базовые методы были реализованы корректно.

Решение задания 3. Коллекция заметок

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"])
print("todo" in notes)
print(len(notes))
print(notes)

Логика решения: все операции с ключом используют key.lower(), поэтому регистр не важен. Проверка на пустую строку стоит в самом начале __setitem__ — guard clause.

Решение задания 4. Векторная арифметика

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 __sub__(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)
print(a + b)
print(a - b)

Логика решения: и __add__, и __sub__ возвращают новый Vector, не изменяя исходные объекты. Это соответствует поведению чисел и кортежей в Python.

Решение задания 5. Счётчик с логическим значением

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

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

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

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


c = Counter(5)
print(id(c))
c += 3
print(id(c))
print(c)
print(bool(c))

empty = Counter(-1)
print(bool(empty))

Логика решения: __iadd__ изменяет self.value и возвращает self, поэтому id не меняется. __bool__ превращает счётчик в «истинный», только если значение положительное.

Решение задания 6. Вызываемый конвертер

class CurrencyConverter:
    def __init__(self, rate: float) -> None:
        self.rate = rate

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

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


euro = CurrencyConverter(0.88)
print(euro(10))
print(euro(5.5))
print(bool(euro))

broken = CurrencyConverter(0)
print(bool(broken))

Логика решения: __call__ делает объект вызываемым, а __bool__ защищает от использования бессмысленного нулевого курса.

Проверить по документации: о специальных методах см. Data model, о @total_orderingfunctools.total_ordering.