【Python基础 | 第八篇】魔法函数(Dunder Methods)核心详解
前言
Python 魔法函数(Magic Methods),也叫双下方法(Dunder Methods),是那些以双下划线开头和结尾的特殊方法,如__init__、__str__。它们不是给你主动调用的,而是由 Python 解释器在特定场景下自动触发。理解魔法函数,是写出"Pythonic"代码的关键一步。
本文覆盖 8 类核心魔法函数,每类配独立示例类,可直接运行。
目录
- 1. 对象生命周期:__new__ / __init__ / __del__
- 2. 字符串表示:__str__ / __repr__
- 3. 比较运算:__eq__ / __lt__ / __hash__
- 4. 算术运算:__add__ / __sub__ / __mul__
- 5. 容器协议:__len__ / __getitem__ / __setitem__ / __contains__
- 6. 可调用对象:__call__
- 7. 上下文管理器:__enter__ / __exit__
- 8. 属性访问:__getattr__ / __setattr__
- 总结
1. 对象生命周期:__new__ / __init__ / __del__
Python 创建对象分三步:创建实例 → 初始化属性 → 销毁回收,分别对应三个魔法函数。
调用顺序
| 阶段 | 方法 | 作用 | 谁调用 |
|---|---|---|---|
| 1. 创建 | __new__ | 分配内存,返回实例 | object.__new__(cls) |
| 2. 初始化 | __init__ | 给实例赋属性 | __new__返回后自动调用 |
| 3. 销毁 | __del__ | 垃圾回收时清理 | 解释器,不保证立即触发 |
cls 与 self 的区别
在__new__中参数是cls,在__init__中参数是self,这二者的区别:
self | cls | |
|---|---|---|
| 指代 | 实例(对象) | 类本身 |
| 用在 | 实例方法 | 类方法(@classmethod) |
| 谁调用 | obj.method() | MyClass.method() |
代码示例
classBook:"""演示对象的创建、初始化和销毁过程"""def__new__(cls,*args,**kwargs):# __new__ 在 __init__ 之前调用,负责创建实例(返回类的实例)# 一般不需要重写,单例模式/不可变对象会用到print(f'[__new__] 创建{cls.__name__}实例')instance=super().__new__(cls)returninstancedef__init__(self,title,price):# __init__ 在实例创建后调用,负责初始化属性# Python 实例属性不需要提前声明——赋值即创建print(f'[__init__] 初始化 Book:{title}')self.title=title self.price=pricedef__del__(self):# __del__ 在对象被垃圾回收时调用(不保证立即触发)print(f'[__del__] 销毁 Book:{self.title}')book=Book('Python 编程',59.0)# 输出:# [__new__] 创建 Book 实例# [__init__] 初始化 Book: Python 编程delbook# 主动触发销毁# 输出: [__del__] 销毁 Book: Python 编程关于 Python 属性不需要提前声明
self.title = title这行代码本身就是在"创造"属性。Python 实例内部用__dict__字典存所有属性,赋值即创建,不需要像 Java/C++ 那样先声明。只有使用__slots__时才需要提前声明属性。
一句话总结:
__new__造对象,__init__填属性,__del__收尾巴——99% 的情况你只需要写__init__。
2. 字符串表示:__str__ / __repr__
这两个方法都返回字符串,但目标受众不同。
核心区别
__str__ | __repr__ | |
|---|---|---|
| 触发方式 | print(obj)/str(obj) | repr(obj)/ 解释器直接输入变量名 |
| 目标受众 | 用户,追求可读 | 开发者,追求无歧义 |
| 理想效果 | 一看就懂 | 能eval(repr(obj))重建对象 |
代码示例
classUser:def__init__(self,name,age):self.name=name self.age=agedef__str__(self):# 面向用户,由 print() / str() 触发returnf'用户{self.name}({self.age}岁)'def__repr__(self):# 面向开发者,由 repr() 触发,理想情况可 eval 重建returnf"User(name='{self.name}', age={self.age})"user=User('zhupeng',23)print(str(user))# 用户 zhupeng(23 岁)print(repr(user))# User(name='zhupeng', age=23)只定义一个选哪个?
优先__repr__。因为__str__没定义时会自动回退到__repr__,反过来不行。
一句话总结:
__str__告诉用户我是谁,__repr__告诉开发者怎么重建我。
3. 比较运算:__eq__ / __lt__ / __hash__
重载比较运算符后,自定义对象可以用==、<比较,用sorted()排序,放入set/ 当dict的 key。
注意事项
- 重写
__eq__后必须重写__hash__,否则对象不能放入set/ 当dict的 key - 返回
NotImplemented而非False,让 Python 尝试反方向比较
代码示例
classScore:def__init__(self,subject,value):self.subject=subject self.value=valuedef__eq__(self,other):# == 运算符:判断两个分数是否相等ifnotisinstance(other,Score):returnNotImplementedreturnself.value==other.valuedef__lt__(self,other):# < 运算符:定义后可使用 sorted() 排序ifnotisinstance(other,Score):returnNotImplementedreturnself.value<other.valuedef__hash__(self):# 重写 __eq__ 后需要重写 __hash__returnhash((self.subject,self.value))def__repr__(self):returnf'Score({self.subject}={self.value})'s1=Score('数学',90)s2=Score('英语',90)s3=Score('语文',80)print(s1==s2)# Trueprint(s1<s3)# Falseprint(sorted([s1,s2,s3]))# [Score(语文=80), Score(数学=90), Score(英语=90)]print({s1,s2,s3})# 可放入 set一句话总结:想让对象能比较、能排序、能当 key?重写
__eq__+__lt__+__hash__三件套。
4. 算术运算:__add__ / __sub__ / __mul__
通过算术运算符重载,让自定义对象支持+、-、*运算,代码更直观。
常用算术魔法函数一览
| 魔法函数 | 运算符 | 示例 |
|---|---|---|
__add__ | + | v1 + v2 |
__sub__ | - | v1 - v2 |
__mul__ | * | v1 * 3 |
__truediv__ | / | v1 / 2 |
__floordiv__ | // | v1 // 2 |
__mod__ | % | v1 % 2 |
__pow__ | ** | v1 ** 2 |
代码示例
classVector:"""演示算术运算符重载(二维向量)"""def__init__(self,x,y):self.x=x self.y=ydef__add__(self,other):# + 运算符:向量相加ifisinstance(other,Vector):returnVector(self.x+other.x,self.y+other.y)returnNotImplementeddef__sub__(self,other):# - 运算符:向量相减ifisinstance(other,Vector):returnVector(self.x-other.x,self.y-other.y)returnNotImplementeddef__mul__(self,scalar):# * 运算符:向量乘以标量ifisinstance(scalar,(int,float)):returnVector(self.x*scalar,self.y*scalar)returnNotImplementeddef__repr__(self):returnf'Vector({self.x},{self.y})'v1=Vector(1,2)v2=Vector(3,4)print(v1+v2)# Vector(4, 6)print(v2-v1)# Vector(2, 2)print(v1*3)# Vector(3, 6)一句话总结:算术魔法函数让你的对象像数字一样运算,返回
NotImplemented交给 Python 处理类型不匹配。
5. 容器协议:__len__ / __getitem__ / __setitem__ / __contains__
实现这几个方法,你的对象就像内置容器一样支持len()、索引、in运算符和for遍历。
各方法触发场景
| 魔法函数 | 触发方式 | 说明 |
|---|---|---|
__len__ | len(obj) | 返回容器长度 |
__getitem__ | obj[index] | 读取元素,也支持for遍历 |
__setitem__ | obj[index] = value | 设置元素 |
__contains__ | item in obj | 成员判断 |
代码示例
classMyList:"""自定义容器,支持 len() / 索引 / in 运算符"""def__init__(self,items=None):self._items=list(items)ifitemselse[]def__len__(self):returnlen(self._items)def__getitem__(self,index):# 支持 for 循环遍历:Python 会从 index=0 开始调用,直到 IndexErrorreturnself._items[index]def__setitem__(self,index,value):self._items[index]=valuedef__contains__(self,item):returniteminself._itemsdef__repr__(self):returnf'MyList({self._items})'ml=MyList([10,20,30])print(len(ml))# 3print(ml[1])# 20ml[1]=99print(20inml)# Falseprint(99inml)# Trueforiteminml:# 10 99 30print(item,end=' ')一句话总结:实现
__len__+__getitem__,你的对象就是半个列表;再加上__setitem__+__contains__,就是完整容器。
6. 可调用对象:__call__
定义__call__后,实例可以像函数一样被调用:obj()等价于obj.__call__()。
代码示例
classCounter:"""让实例像函数一样被调用"""def__init__(self,start=0):self.count=startdef__call__(self,step=1):# 实例() 触发self.count+=stepreturnself.countcounter=Counter()print(counter())# 1print(counter())# 2print(counter(10))# 12实际应用场景
- 函数式编程:用带状态的可调用对象替代闭包
- 装饰器:带参数的装饰器通常用类 +
__call__实现 - API 设计:
model(x)比model.predict(x)更简洁
一句话总结:
__call__让对象"可调用",模糊了函数和对象的边界。
7. 上下文管理器:__enter__ / __exit__
实现这两个方法就能用with语句,自动管理资源的获取和释放。
执行流程
with MyContext() as obj: ← 触发 __enter__,返回值赋给 obj # 使用资源 pass ← 触发 __exit__,无论是否异常__exit__参数说明
| 参数 | 含义 |
|---|---|
exc_type | 异常类型(无异常时为None) |
exc_val | 异常值 |
exc_tb | 异常追踪栈 |
| 返回值 | True吞掉异常,False/None传播异常 |
代码示例
classFileOpener:"""自定义 with 语句,自动管理文件资源"""def__init__(self,filename,mode='r'):self.filename=filename self.mode=mode self.file=Nonedef__enter__(self):# 进入 with 块时调用,返回值赋给 as 后的变量print(f'[__enter__] 打开文件{self.filename}')self.file=open(self.filename,self.mode,encoding='utf-8')returnself.filedef__exit__(self,exc_type,exc_val,exc_tb):# 退出 with 块时调用(无论是否异常)print(f'[__exit__] 关闭文件{self.filename}')ifself.file:self.file.close()returnFalsewithFileOpener('test.txt','w')asf:f.write('hello')# 输出:# [__enter__] 打开文件 test.txt# [__exit__] 关闭文件 test.txt一句话总结:
__enter__获取资源,__exit__释放资源,with语句保证不漏。
8. 属性访问:__getattr__ / __setattr__
这两个方法拦截属性的读取和赋值,可以实现动态属性、属性校验等高级功能。
关键区别
__getattr__ | __getattribute__ | |
|---|---|---|
| 触发时机 | 属性不存在时才触发 | 每次访问属性都触发 |
| 风险 | 低 | 容易无限递归 |
代码示例
classConfig:"""动态属性访问,类似字典但支持点号语法"""def__init__(self):# 用 object.__setattr__ 绕过自身的 __setattr__,避免无限递归object.__setattr__(self,'_data',{})def__getattr__(self,name):# 只有访问不存在的属性时才触发ifnameinself._data:returnself._data[name]raiseAttributeError(f'Config 没有属性:{name}')def__setattr__(self,name,value):# 任何属性赋值都会触发,包括 self.x = 1self._data[name]=valuedef__repr__(self):returnf'Config({self._data})'cfg=Config()cfg.host='localhost'# 触发 __setattr__cfg.port=8080print(cfg.host)# 触发 __getattr__ → localhostprint(cfg.port)# 8080print(cfg)# Config({'host': 'localhost', 'port': 8080})为什么要用object.__setattr__?
在__init__中self._data = {}会触发__setattr__,而__setattr__内部又访问self._data,此时_data还不存在,导致无限递归。object.__setattr__(self, '_data', {})直接绕过自定义的__setattr__,在底层设置属性。
一句话总结:
__getattr__拦截"找不到的属性",__setattr__拦截"所有赋值"——注意用object.__setattr__避开递归陷阱。
总结
8 类魔法函数速查表
| 分类 | 魔法函数 | 核心用途 |
|---|---|---|
| 对象生命周期 | __new__/__init__/__del__ | 创建、初始化、销毁对象 |
| 字符串表示 | __str__/__repr__ | 用户可读 / 开发者无歧义 |
| 比较运算 | __eq__/__lt__/__hash__ | 比较、排序、可哈希 |
| 算术运算 | __add__/__sub__/__mul__ | 自定义 +、-、* 行为 |
| 容器协议 | __len__/__getitem__/__setitem__/__contains__ | 让对象像容器一样使用 |
| 可调用对象 | __call__ | 让实例像函数一样调用 |
| 上下文管理器 | __enter__/__exit__ | 支持with语句 |
| 属性访问 | __getattr__/__setattr__ | 拦截属性读写 |
使用原则
- 不要滥用:魔法函数是给 Python 解释器调用的,不要在代码里主动
obj.__str__(),应该用str(obj) - 返回
NotImplemented:运算符重载遇到类型不匹配时,返回NotImplemented而非False,让 Python 尝试反向操作 - 成对重写:重写
__eq__必须重写__hash__;优先写__repr__而非__str__ - 注意递归:
__setattr__内部赋值会再次触发自己,用object.__setattr__绕过
如果用一句话总结:魔法函数是 Python 和你的对象之间的"契约"——你实现这些方法,Python 就知道如何创建、打印、比较、运算和销毁你的对象。
如果觉得这篇文章对你有帮助,欢迎 点赞 + 收藏 + 评论,你的支持是我持续写作的动力!