掌握 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__ 的角色,yield 之后的代码(最好放在 finally 里)则扮演 __exit__。对象模型的另一根支柱是:typing 模块中的 Protocol 类型把鸭子类型形式化了 - 只要一个类暴露了正确的方法,它就是兼容的,无需声明任何继承关系。
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__。后果是什么?True 的 __exit__ 方法意味着什么?