Python的魔术方法:那些藏在双下划线背后的魔法

写过几年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__之前被调用,返回的是一个还没初始化的空壳对象。日常写业务代码基本用不到它,但如果你要实现单例模式,或者继承tuplestr这种不可变类型,就绕不开它了,因为不可变对象的值必须在创建阶段就定下来。

__init__(self, ...)就是大家最熟悉的构造方法,负责给已经创建好的对象初始化属性。严格来说它不构造对象,只是初始化。

__del__(self)是析构方法,在对象被垃圾回收前调用。这个方法平时最好少碰,因为它的调用时机不太可控------Python用的是引用计数加分代回收,__del__到底什么时候触发,很难精确预测,滥用它容易埋坑。


🎭 让对象学会自我介绍

__str____repr__是使用频率最高的一对魔术方法,但很多人分不清它们的区别。

__repr__的目标是给开发者看的,要求信息尽可能完整、无歧义,理想状态下eval(repr(obj))能还原出这个对象。而__str__的目标是给最终用户看的,追求可读性,可以牺牲一点精确度。

一个小细节,如果你只定义了__repr__没定义__str__,Python会自动用__repr__兜底。反过来则不行。所以很多人图省事,只写__repr__

还有个容易被忽略的__format__,它决定了对象在f-stringformat()函数里怎么被格式化,比如你想让自定义的金额类支持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)\forall a, b:\ a = b \implies hash(a) = hash(b) ∀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的propertyclassmethodstaticmethod底层实现豁然开朗。

描述符是实现了__get____set____delete__任意一个方法的类。当描述符作为另一个类的类属性存在时,访问它就不再是简单取值,而是触发这些方法。property装饰器本质上就是一个语法糖,帮你快速创建了一个描述符对象。

Python 3.6之后还加了个__set_name__,在描述符被赋值给类属性时自动调用,能让描述符知道自己在宿主类里叫什么名字,省去了以前需要手动传参数的麻烦。


📞 让对象变得可调用

__call__方法能让一个对象像函数一样被调用,写法是obj()。这个特性用得最多的场景是实现带状态的函数对象,比如一个计数器类,每调用一次自增一次,或者PyTorch里的神经网络层,本质上都是通过__call__实现的可调用对象。


🔀 类型转换与布尔判断

Python里if obj:这样的写法背后其实调用了__bool__,返回TrueFalse决定对象在布尔上下文里的取值。如果没定义__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__这几个基础款就够用了。真正需要动用运算符重载、描述符协议这些进阶功能的场景,往往是在写框架、写库、或者实现某种特定的领域建模(比如数学向量、物理单位系统)的时候才会派上用场。理解了它们的存在和原理,至少下次看到别人代码里冒出个陌生的双下划线方法,不至于一脸懵了。

相关推荐
米码收割机1 小时前
【Python】Django恒达科技门户网站(源码+文档)【独一无二】
数据库·python·科技
Ivanqhz1 小时前
Rust parse() 浅析
开发语言·后端·rust
卷无止境1 小时前
别再被"乱码"吓到了:Python文件操作的门道
后端·python
程序员爱钓鱼1 小时前
Rust Result 详解:可靠的错误处理机制
前端·后端·rust
程序员爱钓鱼2 小时前
GOPATH 与 Go Modules:Go 项目依赖管理的演变
后端·go
STLearner2 小时前
ICML 2026 | LLM×Graph论文总结[2]【Graph4LLM,Graph4Agent,智能体记忆(Memory)
大数据·人工智能·python·深度学习·学习·机器学习·数据挖掘
牛奔10 小时前
Go 如何打印调试深层或嵌套的结构体
开发语言·后端·golang
红烧大青虫10 小时前
HarmonyOS应用开发实战:小事记 - 数据迁移策略:RDB 表结构变更的版本号管理与 onUpgrade 回调
后端·华为·harmonyos·鸿蒙系统
geovindu10 小时前
go: Iterative Algorithms
开发语言·后端·算法·golang·迭代算法