Решение задания 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__ защищает от использования бессмысленного нулевого курса.
@total_ordering — functools.total_ordering.