【Python基础 | 第八篇】魔法函数(Dunder Methods)核心详解
【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这二者的区别selfcls指代实例对象类本身用在实例方法类方法classmethod谁调用obj.method()MyClass.method()代码示例classBook:演示对象的创建、初始化和销毁过程def__new__(cls,*args,**kwargs):# __new__ 在 __init__ 之前调用负责创建实例返回类的实例# 一般不需要重写单例模式/不可变对象会用到print(f[__new__] 创建{cls.__name__}实例)instancesuper().__new__(cls)returninstancedef__init__(self,title,price):# __init__ 在实例创建后调用负责初始化属性# Python 实例属性不需要提前声明——赋值即创建print(f[__init__] 初始化 Book:{title})self.titletitle self.pricepricedef__del__(self):# __del__ 在对象被垃圾回收时调用不保证立即触发print(f[__del__] 销毁 Book:{self.title})bookBook(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.namename self.ageagedef__str__(self):# 面向用户由 print() / str() 触发returnf用户{self.name}{self.age}岁def__repr__(self):# 面向开发者由 repr() 触发理想情况可 eval 重建returnfUser(name{self.name}, age{self.age})userUser(zhupeng,23)print(str(user))# 用户 zhupeng23 岁print(repr(user))# User(namezhupeng, age23)只定义一个选哪个优先__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.subjectsubject self.valuevaluedef__eq__(self,other):# 运算符判断两个分数是否相等ifnotisinstance(other,Score):returnNotImplementedreturnself.valueother.valuedef__lt__(self,other):# 运算符定义后可使用 sorted() 排序ifnotisinstance(other,Score):returnNotImplementedreturnself.valueother.valuedef__hash__(self):# 重写 __eq__ 后需要重写 __hash__returnhash((self.subject,self.value))def__repr__(self):returnfScore({self.subject}{self.value})s1Score(数学,90)s2Score(英语,90)s3Score(语文,80)print(s1s2)# Trueprint(s1s3)# 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.xx self.yydef__add__(self,other):# 运算符向量相加ifisinstance(other,Vector):returnVector(self.xother.x,self.yother.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):returnfVector({self.x},{self.y})v1Vector(1,2)v2Vector(3,4)print(v1v2)# 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,itemsNone):self._itemslist(items)ifitemselse[]def__len__(self):returnlen(self._items)def__getitem__(self,index):# 支持 for 循环遍历Python 会从 index0 开始调用直到 IndexErrorreturnself._items[index]def__setitem__(self,index,value):self._items[index]valuedef__contains__(self,item):returniteminself._itemsdef__repr__(self):returnfMyList({self._items})mlMyList([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,start0):self.countstartdef__call__(self,step1):# 实例() 触发self.countstepreturnself.countcounterCounter()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异常类型无异常时为Noneexc_val异常值exc_tb异常追踪栈返回值True吞掉异常False/None传播异常代码示例classFileOpener:自定义 with 语句自动管理文件资源def__init__(self,filename,moder):self.filenamefilename self.modemode self.fileNonedef__enter__(self):# 进入 with 块时调用返回值赋给 as 后的变量print(f[__enter__] 打开文件{self.filename})self.fileopen(self.filename,self.mode,encodingutf-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(fConfig 没有属性:{name})def__setattr__(self,name,value):# 任何属性赋值都会触发包括 self.x 1self._data[name]valuedef__repr__(self):returnfConfig({self._data})cfgConfig()cfg.hostlocalhost# 触发 __setattr__cfg.port8080print(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 就知道如何创建、打印、比较、运算和销毁你的对象。如果觉得这篇文章对你有帮助欢迎 点赞 收藏 评论你的支持是我持续写作的动力