写过几年Python的人大概都遇到过这样的场景,明明只是写了个a + b,解释器却悄悄帮你调用了某个叫__add__的东西。这背后其实是Python整个对象模型的核心机制------魔术方法,业内更喜欢叫它dunder methods(double underscore的缩写)。它们不是什么奇技淫巧,而是Python语言设计哲学的直接体现:让自定义对象也能像内置类型一样,参与到加减乘除、比较大小、迭代循环这些日常操作里去。
下面这份梳理会把Python里主要的魔术方法按功能分类讲清楚,尽量少堆代码术语,多用人话解释每个方法到底在解决什么问题。
🧭 魔术方法到底是什么
简单说,魔术方法是Python对象模型里预定义好的一套接口。当你写len(obj)的时候,Python并不是直接去数对象里有多少元素,而是去调用obj.__len__()。这套机制叫协议(protocol),本质上是一种约定------只要你的类实现了对应的方法,Python内置函数和运算符就会自动认得它。
打个比方,这有点像插座和插头的关系。国标插座长成什么样是固定的,只要你的电器插头符合这个标准,插进去就能用。魔术方法就是Python给对象定义的一套标准插头,__len__对应len(),__iter__对应for循环,__add__对应+号,谁实现了这个接口,谁就能享受对应的语法糖。
用一张图大致梳理一下这些协议的分类关系:

接下来一类一类拆开讲。
🌱 对象的生老病死
一个对象从诞生到消亡,Python给了三个关键节点。
__new__(cls, ...)才是真正创建 实例的方法,它在__init__之前被调用,返回的是一个还没初始化的空壳对象。日常写业务代码基本用不到它,但如果你要实现单例模式,或者继承tuple、str这种不可变类型,就绕不开它了,因为不可变对象的值必须在创建阶段就定下来。
__init__(self, ...)就是大家最熟悉的构造方法,负责给已经创建好的对象初始化属性。严格来说它不构造对象,只是初始化。
__del__(self)是析构方法,在对象被垃圾回收前调用。这个方法平时最好少碰,因为它的调用时机不太可控------Python用的是引用计数加分代回收,__del__到底什么时候触发,很难精确预测,滥用它容易埋坑。
🎭 让对象学会自我介绍
__str__和__repr__是使用频率最高的一对魔术方法,但很多人分不清它们的区别。
__repr__的目标是给开发者看的,要求信息尽可能完整、无歧义,理想状态下eval(repr(obj))能还原出这个对象。而__str__的目标是给最终用户看的,追求可读性,可以牺牲一点精确度。
一个小细节,如果你只定义了__repr__没定义__str__,Python会自动用__repr__兜底。反过来则不行。所以很多人图省事,只写__repr__。
还有个容易被忽略的__format__,它决定了对象在f-string或format()函数里怎么被格式化,比如你想让自定义的金额类支持f"{price:.2f}"这种写法,就得实现它。
➕ 运算符重载家族
这大概是魔术方法里戏份最多的一类,因为Python的每个运算符背后几乎都对应一个方法。
| 运算符 | 对应方法 | 说明 |
|---|---|---|
+ |
__add__ |
加法,反向调用为__radd__ |
- |
__sub__ |
减法,反向为__rsub__ |
* |
__mul__ |
乘法 |
/ |
__truediv__ |
真除法(返回浮点数) |
// |
__floordiv__ |
地板除(取整) |
% |
__mod__ |
取模 |
** |
__pow__ |
幂运算 |
== != |
__eq__ __ne__ |
相等性比较 |
< <= > >= |
__lt__ __le__ __gt__ __ge__ |
大小比较 |
+= -= 等 |
__iadd__ __isub__ 等 |
原地运算,有则优先用,没有则退化为普通运算符 |
有个特别有意思的设计叫反向运算方法 。假设你写了个向量类Vector,支持vector * 3,用的是__mul__。但如果用户写成3 * vector呢?Python会先尝试int.__mul__(3, vector),整数类不认识向量,返回NotImplemented,Python这时候才会退而求其次去调用vector.__rmul__(3)。这个设计保证了运算的对称性,是个挺精巧的兜底机制。
比较运算符这块,Python 3有个贴心之处。你只要认真实现了__eq__和__lt__,剩下的比较关系理论上可以用functools.total_ordering装饰器自动推导出来,不用六个方法全写一遍。
哈希方法__hash__也常和__eq__成对出现,规则很硬性
∀a,b: a=b⟹hash(a)=hash(b)
也就是说两个相等的对象哈希值必须一样,否则把它们塞进字典或集合里会出现莫名其妙的bug。所以Python的规定是,一旦你自定义了__eq__,__hash__会被自动置为None,除非你手动重新实现它。
📦 容器与迭代协议
这一类是让自定义对象能像列表、字典一样被操作的关键。
__len__对应len(obj),返回对象的长度。
__getitem__和__setitem__分别对应obj[key]的读和写,__delitem__对应del obj[key]。有个冷知识,只要实现了__getitem__,哪怕不实现__iter__,Python也能对这个对象做for循环,解释器会从索引0开始不断调用__getitem__,直到抛出IndexError为止------这是一种老式的隐式迭代协议,早期Python版本用得比较多。
真正标准的迭代协议是__iter__加__next__。__iter__返回一个迭代器对象(通常是self),__next__每次调用返回下一个值,没有更多值的时候抛出StopIteration。生成器函数其实就是编译器帮你自动实现了这套协议的语法糖。
__contains__对应in关键字,如果不实现它,Python会退化去用__iter__遍历查找,效率上会差一些,所以对于查找频繁的容器类型,最好单独实现一个高效的__contains__。
🚪 上下文管理器
写with open("file") as f:这种代码的时候,背后其实是__enter__和__exit__两个方法在起作用。
__enter__在进入with代码块时被调用,返回值绑定给as后面的变量。__exit__在离开代码块时被调用,无论是正常退出还是抛异常退出都会触发,参数里带着异常类型、异常值和traceback信息,如果__exit__返回True,异常就会被吞掉不再向外传播。
这套机制特别适合处理需要成对出现的操作,比如打开关闭文件、加锁解锁、开启关闭数据库事务,用装饰器@contextmanager配合生成器也能达到同样效果,只是底层原理还是这两个方法。
🔧 属性访问的四兄弟
这一组容易搞混,但逻辑理清楚了其实很简单。
__getattribute__是最底层的钩子,每次访问对象属性都会先经过它,包括访问已经存在的正常属性,慎用,写不好容易死循环。
__getattr__只在属性找不到的时候才会被调用,也就是正常的属性查找失败之后的兜底方案,常用来实现动态属性或者代理模式。
__setattr__在给属性赋值时触发,__delattr__在del obj.attr时触发。
用一句话总结区别就是,__getattribute__管所有读取,__getattr__只管读取失败后的补救,__setattr__管写入,__delattr__管删除。
🎯 描述符协议,属性背后的引擎
这一块相对进阶,但理解了会对Python的property、classmethod、staticmethod底层实现豁然开朗。
描述符是实现了__get__、__set__或__delete__任意一个方法的类。当描述符作为另一个类的类属性存在时,访问它就不再是简单取值,而是触发这些方法。property装饰器本质上就是一个语法糖,帮你快速创建了一个描述符对象。
Python 3.6之后还加了个__set_name__,在描述符被赋值给类属性时自动调用,能让描述符知道自己在宿主类里叫什么名字,省去了以前需要手动传参数的麻烦。
📞 让对象变得可调用
__call__方法能让一个对象像函数一样被调用,写法是obj()。这个特性用得最多的场景是实现带状态的函数对象,比如一个计数器类,每调用一次自增一次,或者PyTorch里的神经网络层,本质上都是通过__call__实现的可调用对象。
🔀 类型转换与布尔判断
Python里if obj:这样的写法背后其实调用了__bool__,返回True或False决定对象在布尔上下文里的取值。如果没定义__bool__,Python会退而求其次去看__len__,长度为0就当作False,这也是为什么空列表、空字典在条件判断里都是假值的原因。
数值转换类还有__int__、__float__、__complex__,分别对应int(obj)、float(obj)、complex(obj)的调用,__index__则专门用于需要精确整数索引的场景,比如切片操作obj[1:5]底层依赖的就是__index__。
🧬 类级别的定制协议
有几个魔术方法作用对象不是实例而是类本身。
__init_subclass__在某个类被继承产生子类时自动触发,可以用来做子类注册、参数校验这类元编程操作,不需要动用完整的元类机制就能实现类似效果。
__class_getitem__让类本身支持下标操作,比如List[int]这种泛型写法,就是靠它实现的。
__instancecheck__和__subclasshook__则跟isinstance()和issubclass()的行为定制有关,abc模块里大量使用了这套机制来实现鸭子类型的抽象基类判断。
⚡ 异步世界的魔术方法
伴随asyncio的普及,Python补充了一批异步版本的协议方法。
__aenter__和__aexit__是异步上下文管理器协议,对应async with语法。__aiter__和__anext__是异步迭代协议,对应async for语法。__await__则让一个对象变成可等待对象 ,能被await关键字消费,协程对象底层就是靠它驱动的。
这几个方法的逻辑和它们的同步版本几乎一一对应,只是多了个async前缀,理解了同步版本再看异步版本会非常轻松。
🗂 拷贝与序列化相关
__copy__和__deepcopy__分别对应copy模块的浅拷贝和深拷贝行为,如果不自定义,Python会用默认的通用拷贝逻辑,但有些复杂对象(比如包含文件句柄、网络连接的对象)需要自己控制拷贝细节。
__reduce__、__getstate__、__setstate__跟pickle序列化有关,决定了对象在被腌制 成字节流以及解腌恢复时该怎么处理,涉及跨进程传输对象、缓存持久化这类场景时会用到。
💡 写在最后
魔术方法这套体系乍看起来数量庞大、容易让人望而生畏,但只要抓住一个核心思路------Python里几乎所有的语法糖背后都有一个对应的魔术方法在撑腰 ------理解起来就会顺畅很多。想搞懂len()怎么工作,去找__len__;想搞懂with语句,去找__enter__和__exit__;想搞懂+号,去找__add__。
实际写代码的时候倒不必强迫自己把每个魔术方法都用上,绝大多数业务类只需要__init__、__repr__、__eq__这几个基础款就够用了。真正需要动用运算符重载、描述符协议这些进阶功能的场景,往往是在写框架、写库、或者实现某种特定的领域建模(比如数学向量、物理单位系统)的时候才会派上用场。理解了它们的存在和原理,至少下次看到别人代码里冒出个陌生的双下划线方法,不至于一脸懵了。