☰
python深入讲解魔术方法
2026/10/11 12:45:25 网站建设 项目流程

前言


魔术方法(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.8+
class Point:
__slots__ = ("x", "y")

def __new__(cls, x, y):
# 常规做法:先让父类把对象造出来
instance = super().__new__(cls)
return instance

def __init__(self, x, y):
self.x = x
self.y = y
# 这里不要 return 任何东西,返回非 None 会抛 TypeError


p = 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.8+
class Money:
def __init__(self, amount, currency="CNY"):
self.amount = amount
self.currency = currency

def __repr__(self):
# 面向开发者:信息丰富、无歧义,理想情况能重建对象
return f"Money(amount={self.amount!r}, currency={self.currency!r})"

def __str__(self):
# 面向用户:简洁
return f"{self.amount} {self.currency}"


print(str(Money(9.9))) # 9.9 CNY
print(repr(Money(9.9))) # Money(amount=9.9, currency='CNY')
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.8+
class BadKey:
def __init__(self, name):
self.name = name

def __eq__(self, other):
return isinstance(other, BadKey) and self.name == other.name


class GoodKey:
def __init__(self, name):
self.name = name

def __eq__(self, other):
return isinstance(other, GoodKey) and self.name == other.name

def __hash__(self):
# 只把「参与比较」的字段揉进哈希,保证相等则哈希相同
return hash(self.name)


# hash(BadKey("a")) # TypeError: unhashable type
print(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.8+
class 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.8+
class 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 None


cfg = LazyConfig({"host": "localhost"})
print(cfg.host) # 走 __getattr__
print(cfg._data) # 命中实例字典,不会走 __getattr__

六、__slots__:省内存与限制属性


__slots__可以是一个字符串、可迭代对象或字符串序列,它显式声明实例会有哪些数据成员,并阻止自动创建__dict__和__weakref__。语言参考列出的注意点里,最容易踩的有三条:从没有__slots__的类继承时,子类实例仍然会有__dict__;给未列在__slots__里的名字赋值会抛AttributeError;__slots__是用描述符实现的,所以类属性不能给 slot 变量当默认值。


# 适用于 Python 3.8+
class SlotPoint:
__slots__ = ("x", "y")

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


sp = 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 <module>
TypeError: object of type 'C' has no len()

原因是__hash__、__repr__这类方法在类型对象身上也存在,如果按常规查找走实例字典,在类型对象上调用时就会失效。所以隐式调用一律查类型。魔术方法必须定义在类上,写在实例上一个也不生效。


常见坑点



  1. ❌ 给实例挂obj.__len__ = lambda: 5就想让len(obj)生效。


✅ 隐式调用只查类型;魔术方法必须定义在类里。



  1. ❌ 覆写了__eq__却不写__hash__。


✅__hash__会被隐式置为None,实例变得不可哈希,进不了set、当不了dict键。



  1. ❌ 写一个「抛异常的__hash__」来表示不可哈希。


✅ 那会被Hashable误判为可哈希;正确做法是写__hash__ = None。



  1. ❌__init__里写了return self或return 0。


✅__init__返回非None会抛TypeError,它只负责初始化、不负责返回。



  1. ❌ 以为不可变类型的子类可以靠__init__改值。


✅ int / str / tuple 的值在__new__阶段就定了,要定制必须在__new__里做。



  1. ❌ 只定义__str__,然后在列表里调试发现显示的是内存地址。


✅ 容器打印用repr;面向开发者的__repr__一定要写。



  1. ❌__getattr__里访问一个不存在的属性。


✅ 会再次触发__getattr__,无限递归直到RecursionError;不存在时应抛AttributeError。



  1. ❌ 以为把__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表示禁用。这几条记牢,比背下一张方法清单有用得多。




需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询