前言魔术方法magic methods就是那些名字两头带双下划线、由 Python 语法隐式调用的特殊方法比如__init__、__repr__、__eq__。它们不是给你手动调的接口而是给语言本身看的钩子你写了__len__len(obj)才有意义你写了__enter__/__exit__对象才能用在with里。语言参考把它叫作「特殊方法名」special method names这是 Python 实现运算符重载的正规途径。这里先纠正两个常见误解。第一很多人以为特殊方法写在实例上就能生效——不能。语言参考明确说明对自定义类而言隐式调用特殊方法只保证在对象的类型上查找不查实例字典。第二很多人以为__str__和__repr__是同义词可以随便挑一个——它们面向的读者不同repr面向开发者、str面向用户容器打印时用的是repr。本文按「对象生命周期 → 表示与比较 → 可调用与容器 → 上下文管理 → 属性访问」的顺序把魔术方法串一遍重点是那些机制层面的细节而不是方法清单。一、对象是怎么造出来的__new__ 与 __init__创建实例时Python 先调__new__造对象再调__init__初始化它。语言参考里的关键约定有三条__new__是一个静态方法被特殊处理你不需要显式加staticmethod第一个参数是cls如果__new__返回的不是cls的实例__init__不会被调用__init__不能返回非None的值否则运行期报TypeError。# 适用于 Python 3.8class Point:__slots__ (x, y)def __new__(cls, x, y):# 常规做法先让父类把对象造出来instance super().__new__(cls)return instancedef __init__(self, x, y):self.x xself.y y# 这里不要 return 任何东西返回非 None 会抛 TypeErrorp Point(1, 2)print(p.x, p.y)__new__主要用于不可变类型的子类int、str、tuple 等它们的值在对象创建时就定下来了等不到__init__要做校验或变换必须在__new__里改。二、表示__repr__ 与 __str__ 的分工语言参考说得很清楚__repr__由repr()调用应尽量返回一个「能重建该对象的合法 Python 表达式」__str__由str()、print()调用返回「好读」的表示即可。还有一条关键规则如果一个类定义了__repr__却没定义__str__那么str()会退回使用__repr__。反过来不成立——只定义__str__不会让repr()变好看。# 适用于 Python 3.8class Money:def __init__(self, amount, currencyCNY):self.amount amountself.currency currencydef __repr__(self):# 面向开发者信息丰富、无歧义理想情况能重建对象return fMoney(amount{self.amount!r}, currency{self.currency!r})def __str__(self):# 面向用户简洁return f{self.amount} {self.currency}print(str(Money(9.9))) # 9.9 CNYprint(repr(Money(9.9))) # Money(amount9.9, currencyCNY)print([Money(1), Money(2)]) # 列表里显示的是 repr不是 str最后一行是最容易被忽视的细节容器打印用的是元素的repr不会调__str__。所以只写好看的__str__而把__repr__留成默认调试时看到的就是一串难懂的内存地址。三、比较与哈希__eq__ 与 __hash__ 的联动这是魔术方法里最阴的一个联动机制。语言参考原文的意思是一个类如果覆写了__eq__却没有定义__hash__它的__hash__会被隐式置为None。一旦__hash__是None实例就不可哈希——不能放进set不能当dict的键hash()会抛TypeError并且会被isinstance(obj, collections.abc.Hashable)正确识别为不可哈希。# 适用于 Python 3.8class BadKey:def __init__(self, name):self.name namedef __eq__(self, other):return isinstance(other, BadKey) and self.name other.nameclass GoodKey:def __init__(self, name):self.name namedef __eq__(self, other):return isinstance(other, GoodKey) and self.name other.namedef __hash__(self):# 只把「参与比较」的字段揉进哈希保证相等则哈希相同return hash(self.name)# hash(BadKey(a)) # TypeError: unhashable typeprint(hash(GoodKey(a)) hash(GoodKey(a))) # True__eq__的默认实现比较的是同一性is__ne__默认委托给__eq__并把结果取反除非__eq__返回了NotImplemented。另外比较方法可以返回NotImplemented表示「我不会比这一对参数」Python 会再去试对方的反射方法。如果你想让一个类彻底不支持哈希正确做法是在类里写__hash__ None而不是写一个 raise 的__hash__——后者会被Hashable误判为可哈希。四、可调用与容器协议__call__让实例像函数一样被调用写obj(...)大致等价于type(obj).__call__(obj, ...)。容器协议有明确的回落顺序这是理解in、for、len()的关键语法优先用缺省时回落到len(obj)__len__没有回落直接报错for x in obj__iter__老式序列协议__getitem__x in obj__contains__先试__iter__再试__getitem__bool(obj)__bool____len__返回 0 即为假reversed(obj)__reversed____len____getitem__还有一条反直觉的规则把特殊方法设为None表示该操作不可用。语言参考举例说一个类如果把__iter__设成None它就不是可迭代的iter()会抛TypeError而且不会回落到__getitem__。# 适用于 Python 3.8class Bag:def __init__(self, items):self._items list(items)def __len__(self):return len(self._items)def __getitem__(self, index):return self._items[index] # 支持 b[0] 和切片def __contains__(self, value):return value in self._items # 比逐个遍历更快更直观def __iter__(self):return iter(self._items)def __call__(self, index):return self._items[index]bag Bag([10, 20, 30])print(len(bag), bag[1], 20 in bag, list(bag), bag(2))五、上下文管理器与属性访问with语句需要一个实现了__enter__/__exit__的对象__enter__的返回值会绑定到as后面的名字__exit__(exc_type, exc_value, traceback)在退出时被调用返回真值表示「吞掉异常」否则异常继续向外传播。没有异常发生时三个参数都是None。语言参考特别提醒__exit__不应该重新抛出传入的异常那是调用方的职责。属性访问有两个钩子区别是理解它们的重点方法调用时机用途__getattr__(self, name)只在常规查找失败之后即__getattribute__抛了AttributeError才调用兜底给不存在的属性返回动态值__getattribute__(self, name)拦截所有属性访问完全接管属性查找容易写成死递归__getattr__里如果又去访问self.xxx而该属性仍不存在就会再次触发__getattr__形成无限递归最后RecursionError。__getattribute__里再访问任何属性都会再次进入自己写错更危险——必须用super().__getattribute__(name)或object.__getattribute__(self, name)走正常路径。# 适用于 Python 3.8class LazyConfig:def __init__(self, data):self._data dict(data)def __getattr__(self, name):# 只有常规查找失败才会到这里名字不存在时抛 AttributeError 而不是死递归try:return self._data[name]except KeyError:raise AttributeError(name) from Nonecfg LazyConfig({host: localhost})print(cfg.host) # 走 __getattr__print(cfg._data) # 命中实例字典不会走 __getattr__六、__slots__省内存与限制属性__slots__可以是一个字符串、可迭代对象或字符串序列它显式声明实例会有哪些数据成员并阻止自动创建__dict__和__weakref__。语言参考列出的注意点里最容易踩的有三条从没有__slots__的类继承时子类实例仍然会有__dict__给未列在__slots__里的名字赋值会抛AttributeError__slots__是用描述符实现的所以类属性不能给 slot 变量当默认值。# 适用于 Python 3.8class SlotPoint:__slots__ (x, y)def __init__(self, x, y):self.x xself.y ysp SlotPoint(1, 2)print(sp.x, sp.y)# sp.z 3 # AttributeError: SlotPoint object has no attribute z七、一个反直觉但重要的事实特殊方法查的是类型语言参考里专门有一节叫「特殊方法查找」给了这个例子给实例挂一个__len__len()仍然不认。 class C:... pass... c C() c.__len__ lambda: 5 len(c)Traceback (most recent call last):File stdin, line 1, in moduleTypeError: object of type C has no len()原因是__hash__、__repr__这类方法在类型对象身上也存在如果按常规查找走实例字典在类型对象上调用时就会失效。所以隐式调用一律查类型。魔术方法必须定义在类上写在实例上一个也不生效。常见坑点❌ 给实例挂obj.__len__ lambda: 5就想让len(obj)生效。✅ 隐式调用只查类型魔术方法必须定义在类里。❌ 覆写了__eq__却不写__hash__。✅__hash__会被隐式置为None实例变得不可哈希进不了set、当不了dict键。❌ 写一个「抛异常的__hash__」来表示不可哈希。✅ 那会被Hashable误判为可哈希正确做法是写__hash__ None。❌__init__里写了return self或return 0。✅__init__返回非None会抛TypeError它只负责初始化、不负责返回。❌ 以为不可变类型的子类可以靠__init__改值。✅ int / str / tuple 的值在__new__阶段就定了要定制必须在__new__里做。❌ 只定义__str__然后在列表里调试发现显示的是内存地址。✅ 容器打印用repr面向开发者的__repr__一定要写。❌__getattr__里访问一个不存在的属性。✅ 会再次触发__getattr__无限递归直到RecursionError不存在时应抛AttributeError。❌ 以为把__iter__设成None还会回落到__getitem__。✅ 设为None就是明确禁用该操作iter()直接抛TypeError。总结分组方法关键机制生命周期__new__/__init__先造后初始化__init__只能返回None表示__repr__/__str__无__str__时str()退回__repr__容器用 repr比较__eq__/__hash__覆写__eq__会把__hash__置为None调用__call__实例像函数一样被调用容器__len__/__getitem__/__iter__/__contains__有明确回落顺序上下文__enter__/__exit____exit__返回真值即吞异常属性__getattr__/__getattribute__前者兜底、后者拦截一切内存__slots__阻止__dict__限制属性名魔术方法的价值在于「让你写的类用起来像内置类型」。但它们的规则是由语法固定死的不是约定俗成的风格问题查类型不查实例、__eq__与__hash__联动、容器协议有回落顺序、None表示禁用。这几条记牢比背下一张方法清单有用得多。
阅读完成 · 觉得有帮助?