Kodokon kodokon.com

协议与特殊方法:__repr__、__eq__、上下文管理器

掌握 Python 的对象模型:__eq__/__hash__ 契约、NotImplemented 值、上下文管理器,以及用 Protocol 实现的结构化类型。

9 分钟 · 3 题

在 Kodokon 中打开本课

在 Python 中,每个运算符、每次转换、每条 with 语句都要经过一个特殊方法:a == b 会调用 a.__eq__(b)len(x) 会调用 x.__len__()。请注意区分 __repr__ - 面向开发者的无歧义表示,会显示在调试器中和列表内部 - 与面向最终用户的 __str__。如果缺少 __str__,Python 会回退到 __repr__:所以请始终先实现 __repr__,最好写成能够重建该对象的形式。

PYTHON
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,而绝不是 False。

返回 NotImplemented(这是一个值,不要与 NotImplementedError 异常混淆)会触发运算符的协商协议:Python 随后会尝试反射操作 other.__eq__(self),只有在万不得已时才判定为 False。直接返回 False 会短路这场协商,破坏与子类或第三方类型之间的比较 - 而它们其实是知道该如何回应的。

PYTHON
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")
返回 False 会让所有异常正常地向外传播。

如果 __exit__ 返回 True,当前的异常就会被吞掉 - contextlib.suppress 正是这样工作的。为了避免写一整个类,@contextlib.contextmanager 可以把一个生成器变成上下文管理器:yield 之前的代码扮演 __enter__ 的角色,yield 之后的代码(最好放在 finally 里)则扮演 __exit__。对象模型的另一根支柱是:typing 模块中的 Protocol 类型把鸭子类型形式化了 - 只要一个类暴露了正确的方法,它就是兼容的,无需声明任何继承关系。

PYTHON
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))
Session 没有声明任何继承:符合性是结构性的。

知识检测

确认你已牢记本课的重点内容。

  1. __eq__ 中,面对一个你无法与之比较的类型时,应该返回什么?
    • False,以便清晰地表示不相等
    • 抛出 NotImplementedError
    • NotImplemented,好让 Python 去尝试另一个操作数的反射操作
    • None,Python 会把它转换成 False
  2. 你给一个类添加了 __eq__,却没有动 __hash__。后果是什么?
    • 没有后果:__hash__ 仍然继承自 object
    • __hash__ 被设为 None:实例再也不能用作字典的键或集合的元素
    • 哈希值会自动变成属性元组的哈希值
  3. 一个返回 True__exit__ 方法意味着什么?
    • with 代码块没有出错就结束了
    • 这个上下文管理器可以被复用
    • Python 会把 with 代码块再运行一遍
    • 当前的异常被吞掉,执行从 with 代码块之后继续