Освой объектную модель Python: контракт __eq__/__hash__, значение NotImplemented, менеджеры контекста и структурную типизацию через Protocol.
Открыть этот урок в KodokonВ Python каждый оператор, каждое преобразование, каждая конструкция with проходит через специальный метод: a == b вызывает a.__eq__(b), len(x) вызывает x.__len__(). Важно различать __repr__ - однозначное представление для разработчика, которое видно в отладчике и внутри списков - и __str__, предназначенное для конечного пользователя. Если __str__ не определён, Python откатывается к __repr__: поэтому всегда реализуй сначала __repr__, в идеале в такой форме, по которой объект можно воссоздать.
class Money:
def __init__(self, amount: int, currency: str):
self.amount = amount
self.currency = currency
def __repr__(self) -> str:
return f"Money({self.amount!r}, {self.currency!r})"
def __eq__(self, other: object) -> bool:
if not isinstance(other, Money):
return NotImplemented
return (self.amount, self.currency) == (
other.amount, other.currency)
print(Money(10, "EUR") == Money(10, "EUR"))
print(Money(10, "EUR") == "10 EUR")Возврат NotImplemented (это значение, его не надо путать с исключением NotImplementedError) запускает протокол согласования операторов: Python пробует зеркальную операцию other.__eq__(self) и только в крайнем случае приходит к False. Если сразу вернуть False, это согласование обрывается и ломаются сравнения с подклассами или сторонними типами, которые на самом деле знали бы, что ответить.
import time
class Timer:
def __enter__(self):
self.start = time.perf_counter()
return self
def __exit__(self, exc_type, exc, tb):
stop = time.perf_counter()
self.elapsed = stop - self.start
return False
with Timer() as timer:
sum(range(1_000_000))
print(f"{timer.elapsed:.4f} s")Если __exit__ возвращает True, текущее исключение подавляется - именно так и работает contextlib.suppress. Чтобы не писать целый класс, используй @contextlib.contextmanager: он превращает генератор в менеджер контекста, где код до yield играет роль __enter__, а код после (в идеале в блоке finally) - роль __exit__. Ещё одна опора объектной модели: типы Protocol из модуля typing формализуют утиную типизацию - класс совместим, если предоставляет нужные методы, безо всякого объявленного наследования.
from typing import Protocol, runtime_checkable
@runtime_checkable
class Closable(Protocol):
def close(self) -> None: ...
class Session:
def close(self) -> None:
print("session closed")
def shutdown(resource: Closable) -> None:
resource.close()
shutdown(Session())
print(isinstance(Session(), Closable))__eq__, если попался тип, с которым сравнение невозможно?__eq__ в класс, не трогая __hash__. Каково последствие?__exit__, возвращающий True?