讲解

双下划线开头结尾的方法(initstrlen……)叫魔术方法(dunder methods,double underscore 的谐音),它们是 Python 的「协议」:你不需要主动调用它们,而是在特定语法场景下由解释器自动调用。len(obj) 实际调用 obj.len(),print(obj) 调用 obj.str(),obj1 + obj2 调用 obj1.add(obj2),obj[key] 调用 obj.getitem(key)。给自己的类实现这些方法,自定义对象就能像内置类型一样融入 Python 语法。

最高频的三个:init 初始化(已学)、str 返回「给人看」的字符串(print、str()、f-string 用)、repr 返回「给程序员看」的字符串(交互式解释器直接回显、调试日志用),约定 repr 尽量写成能重建对象的形式。只实现一个的话实现 repr——没定义 str 时会退而求其次用 repr。长度和容器行为:len 支持 len(),getitem 支持下标访问和迭代,contains 支持 in,eq 支持 == 判断(默认按身份比)。

算术和比较运算符也对应魔术方法:addsubmul 对应 + - *;eqltle 等对应比较。实现了 eqlt 后,functools.total_ordering 装饰器能自动补全其余比较。上下文管理器协议 enter/exit 让对象能用于 with 语句(文件就是靠它自动关闭)。记住规范:不要自己直接调 obj.str(),用 str(obj)——魔术方法是写给解释器调用的。

示例

class Vector:
    """二维向量:支持打印、相加、比较、取长度。"""

    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f'Vector({self.x}, {self.y})'  # 给程序员看

    def __str__(self):
        return f'({self.x}, {self.y})'  # 给用户看

    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)

    def __eq__(self, other):
        if not isinstance(other, Vector):
            return NotImplemented
        return (self.x, self.y) == (other.x, other.y)

    def __abs__(self):
        return (self.x ** 2 + self.y ** 2) ** 0.5


v1 = Vector(1, 2)
v2 = Vector(3, 4)
print('str:', str(v1))
print('repr:', repr(v1))
print('加法:', v1 + v2)
print('相等:', v1 + v2 == Vector(4, 6))
print('模长:', abs(Vector(3, 4)))


# __len__ / __getitem__ / __contains__:让对象像容器
class Backpack:
    def __init__(self, items):
        self._items = list(items)

    def __len__(self):
        return len(self._items)

    def __getitem__(self, index):
        return self._items[index]

    def __contains__(self, item):
        return item in self._items


bag = Backpack(['地图', '水壶', '压缩饼干'])
print('长度:', len(bag))
print('下标:', bag[1])
print('in:', '水壶' in bag)
print('可以迭代:', list(bag))  # 有 __getitem__ 就能 for


# __enter__ / __exit__:自制 with 块
class Timer:
    def __enter__(self):
        import time

        self._start = time.perf_counter()
        return self

    def __exit__(self, *exc_info):
        import time

        print(f'耗时:{time.perf_counter() - self._start:.4f} 秒')
        return False  # 不吞异常


with Timer():
    sum(range(1000000))

常见坑

  • 直接调用魔术方法:写 obj.len() 而不是 len(obj),虽然多数情况等价,但绕过内置函数失去了类型适配和可读性,违背协议的设计意图。
  • eq 返回 NotImplemented 的意识:遇到不认识的对象类型,返回 NotImplemented 让 Python 去尝试对方的反射方法,而不是草率返回 False。
  • 定义了 eq 忘了 hash:自定义 eq 后 Python 会把 hash 设为 None,对象就不能进集合、当字典键了。需要哈希就自己实现 hash 或用 @dataclass(frozen=True)。
  • repr 里放花哨排版:repr 是给调试看的,应简洁、信息完整、尽量像构造表达式;美化输出留给 str

小结

魔术方法是协议:str/repr 管展示,len/getitem/contains 管容器行为,add/eq 管运算符,enter/exit 管 with。下一章看迭代的引擎:迭代器与生成器。