Python 魔术方法入门
目录
- 第一部分:先补齐两个必要概念
- [1.1 什么是"类"和"对象"](#1.1 什么是"类"和"对象" "#11-%E4%BB%80%E4%B9%88%E6%98%AF%E7%B1%BB%E5%92%8C%E5%AF%B9%E8%B1%A1")
- [1.2
self到底是什么](#1.2 self 到底是什么 "#12-self-%E5%88%B0%E5%BA%95%E6%98%AF%E4%BB%80%E4%B9%88") - [1.3 方法:写在类里面的函数](#1.3 方法:写在类里面的函数 "#13-%E6%96%B9%E6%B3%95%E5%86%99%E5%9C%A8%E7%B1%BB%E9%87%8C%E9%9D%A2%E7%9A%84%E5%87%BD%E6%95%B0")
- 第二部分:魔术方法是什么
- [2.1 一个生活化类比:门与自动门](#2.1 一个生活化类比:门与自动门 "#21-%E4%B8%80%E4%B8%AA%E7%94%9F%E6%B4%BB%E5%8C%96%E7%B1%BB%E6%AF%94%E9%97%A8%E4%B8%8E%E8%87%AA%E5%8A%A8%E9%97%A8")
- [2.2 为什么名字要带双下划线](#2.2 为什么名字要带双下划线 "#22-%E4%B8%BA%E4%BB%80%E4%B9%88%E5%90%8D%E5%AD%97%E8%A6%81%E5%B8%A6%E5%8F%8C%E4%B8%8B%E5%88%92%E7%BA%BF")
- [2.3 它与普通方法的三个区别](#2.3 它与普通方法的三个区别 "#23-%E5%AE%83%E4%B8%8E%E6%99%AE%E9%80%9A%E6%96%B9%E6%B3%95%E7%9A%84%E4%B8%89%E4%B8%AA%E5%8C%BA%E5%88%AB")
- [2.4 什么情况下会被自动调用](#2.4 什么情况下会被自动调用 "#24-%E4%BB%80%E4%B9%88%E6%83%85%E5%86%B5%E4%B8%8B%E4%BC%9A%E8%A2%AB%E8%87%AA%E5%8A%A8%E8%B0%83%E7%94%A8")
- [2.5 一个可以自己验证的方法](#2.5 一个可以自己验证的方法 "#25-%E4%B8%80%E4%B8%AA%E5%8F%AF%E4%BB%A5%E8%87%AA%E5%B7%B1%E9%AA%8C%E8%AF%81%E7%9A%84%E6%96%B9%E6%B3%95")
- 第三部分:逐个学会常用魔术方法
- [3.1
__init__:给新对象填初始信息](#3.1 init:给新对象填初始信息 "#31-init%E7%BB%99%E6%96%B0%E5%AF%B9%E8%B1%A1%E5%A1%AB%E5%88%9D%E5%A7%8B%E4%BF%A1%E6%81%AF") - [3.2
__str__:面向用户的文字描述](#3.2 str:面向用户的文字描述 "#32-str%E9%9D%A2%E5%90%91%E7%94%A8%E6%88%B7%E7%9A%84%E6%96%87%E5%AD%97%E6%8F%8F%E8%BF%B0") - [3.3
__repr__:面向程序员的文字描述](#3.3 repr:面向程序员的文字描述 "#33-repr%E9%9D%A2%E5%90%91%E7%A8%8B%E5%BA%8F%E5%91%98%E7%9A%84%E6%96%87%E5%AD%97%E6%8F%8F%E8%BF%B0") - [3.4
__len__:让对象有"长度"](#3.4 len:让对象有"长度" "#34-len%E8%AE%A9%E5%AF%B9%E8%B1%A1%E6%9C%89%E9%95%BF%E5%BA%A6") - [3.5
__getitem__:让对象能用[]取值](#3.5 getitem:让对象能用 [] 取值 "#35-getitem%E8%AE%A9%E5%AF%B9%E8%B1%A1%E8%83%BD%E7%94%A8-%E5%8F%96%E5%80%BC") - [3.6
__call__:让对象能像函数一样被调用](#3.6 call:让对象能像函数一样被调用 "#36-call%E8%AE%A9%E5%AF%B9%E8%B1%A1%E8%83%BD%E5%83%8F%E5%87%BD%E6%95%B0%E4%B8%80%E6%A0%B7%E8%A2%AB%E8%B0%83%E7%94%A8") - [3.7
__add__:让对象能用+](#3.7 add:让对象能用 + "#37-add%E8%AE%A9%E5%AF%B9%E8%B1%A1%E8%83%BD%E7%94%A8-") - [3.8
__eq__:让对象能用==比较](#3.8 eq:让对象能用 == 比较 "#38-eq%E8%AE%A9%E5%AF%B9%E8%B1%A1%E8%83%BD%E7%94%A8-%E6%AF%94%E8%BE%83") - [3.9
__del__:对象被回收时的收尾](#3.9 del:对象被回收时的收尾 "#39-del%E5%AF%B9%E8%B1%A1%E8%A2%AB%E5%9B%9E%E6%94%B6%E6%97%B6%E7%9A%84%E6%94%B6%E5%B0%BE") - [3.10 综合示例:把学到的串起来](#3.10 综合示例:把学到的串起来 "#310-%E7%BB%BC%E5%90%88%E7%A4%BA%E4%BE%8B%E6%8A%8A%E5%AD%A6%E5%88%B0%E7%9A%84%E4%B8%B2%E8%B5%B7%E6%9D%A5")
- [3.1
- 第四部分:速查表
- 第五部分:给初学者的五条建议
- 附录:如何运行本文代码
第一部分:先补齐两个必要概念
要理解魔术方法,只需要两个概念。这一部分刻意不涉及继承、装饰器、元类等进阶内容。
1.1 什么是"类"和"对象"
类 是一张图纸 ,对象 是按照图纸造出来的实物。
生活里到处是这个结构:
| 图纸(类) | 实物(对象) |
|---|---|
| 房子的设计图 | 你住的那套房 |
| 月饼模具 | 一块块的月饼 |
| 宠物狗的品种描述 | 你家的旺财 |
写代码时,"图纸"用 class 定义:
python
class Dog: # 定义一个类,名叫 Dog
pass # 先留空,后面再往里加东西
有了图纸,就能造实物。造实物的写法是类名后面加括号:
python
d = Dog() # 造出一个 Dog 对象,赋值给 d
到这里,d 就是一个真实存在的对象了,虽然它现在还什么都不会。
一句话记住:类名加括号,就得到一个对象。
1.2 self 到底是什么
这是初学者第一个坎。
self 表示"当前这个对象自己"。
看这个例子:
python
class Dog:
def set_name(self, name):
self.name = name # 把 name 存进"我自己"身上
d1 = Dog()
d2 = Dog()
d1.set_name("旺财") # 此时 self 就是 d1
d2.set_name("小黑") # 此时 self 就是 d2
d1.set_name("旺财") 这个写法,Python 实际理解成 Dog.set_name(d1, "旺财")------d1 被自动填进了 self 的位置。
所以:
self不需要你手动传,Python 自动传。- 写方法时,
self必须是第一个参数。 self.name = name的意思是"在这个对象身上存一个叫 name 的东西"。
存进去之后,用 对象.名字 就能取出来:
python
print(d1.name) # 旺财
print(d2.name) # 小黑
为什么两个对象互不干扰? 因为每个对象都有自己的一小块存储空间。可以用 __dict__ 看到里面装了什么:
python
print(d1.__dict__) # {'name': '旺财'}
实测确认:Dog("旺财") 之后,d.__dict__ 是 {'name': '旺财'}------信息确实存在对象自己身上。
1.3 方法:写在类里面的函数
同一段逻辑,写在外面叫函数 ,写在类里面叫方法:
python
def bark(): # 函数:外面
print("汪汪")
class Dog:
def bark(self): # 方法:类里面,第一个参数是 self
print("汪汪")
调用方式不同:
python
bark() # 函数:直接写名字调用
d = Dog()
d.bark() # 方法:用"对象.方法名()"调用
唯一实质差别就是多了 self。 除此之外,写法和函数完全一样------一样可以有参数、有返回值、有 if 和 for。
记住这句就够了:方法是待在类里面的函数,第一个参数是
self。
第二部分:魔术方法是什么
2.1 一个生活化类比:门与自动门
想象一栋楼里的两种门:
| 普通门 | 自动门 | |
|---|---|---|
| 怎么开 | 你得手动推 | 你走近它自己开 |
| 谁来操作 | 你自己 | 门自己感应 |
| 你的感受 | "我要去推门" | "走过去就行了" |
普通方法 = 普通门:
python
d.bark() # 你得手动写出来,它才会执行
魔术方法 = 自动门:
python
print(d) # 你没写 __str__,但它被自动触发了
len(d) # 你没写 __len__,但被自动触发了
d1 == d2 # 你没写 __eq__,但被自动触发了
这就是魔术方法的全部秘密:它不是让你手动调用的,而是在你使用某个运算符或内建函数时,Python 自动帮你调用的。
所以叫"魔术"------你只做了一个很自然的动作(print、len、+),对象却神奇地做出了反应。
2.2 为什么名字要带双下划线
用生活场景理解:
假设一个小区里有两种信箱------
- 普通信箱,写着住户名字:张三
- 系统信箱,统一刷成特殊颜色、写着
__物业__:只有物业才能往里投东西
双下划线就是那个"特殊颜色标记"。它告诉 Python:这个名字是系统保留的,不是你随便起的。
如果你起的名字和系统保留名撞了,Python 会按系统规则来解释你的代码,行为很可能和你想的不一样。所以:
| 命名形式 | 含义 | 你该怎么做 |
|---|---|---|
name |
普通名字 | 随便起 |
_name |
"我打算内部使用" | 一种礼貌约定,没有强制力 |
__name |
类内私有 | 会被 Python 改名,日常少用 |
__name__ |
系统保留 | 只使用 Python 已定义的,不要自己造 |
最后一行是最重要的:看到 __x__ 这种形式,先认为"这是 Python 已经规定好的",去查它的含义,而不是自己发明一个。
2.3 它与普通方法的三个区别
| 对比项 | 普通方法 | 魔术方法 |
|---|---|---|
| 谁来调用 | 你亲手写 对象.方法名() |
Python 在你用运算符 / 内建函数时自动调用 |
| 名字能不能自己起 | 能,随便起 | 不能,名字由 Python 规定 |
| 要不要记住名字 | 要,不然你调不到 | 不用刻意背,用到哪个学哪个 |
第三点对初学者很重要:你不需要一次性背完几十个魔术方法。常用的就十来个,用到再学,本文覆盖的就是这些。
2.4 什么情况下会被自动调用
规律很简单:只要你的代码里出现了某个运算符或内建函数,Python 就会去找对应的魔术方法。
对应关系如下(左边是你写的,右边是 Python 悄悄调的):
| 你写的代码 | Python 自动调用 |
|---|---|
print(对象) |
__str__ |
len(对象) |
__len__ |
对象[0] |
__getitem__ |
对象() |
__call__ |
对象a + 对象b |
__add__ |
对象a == 对象b |
__eq__ |
对象 = 类名(...) |
__init__ |
| 对象被删掉时 | __del__ |
有一件事可能会让你意外:你其实早就在用魔术方法了,只是没意识到。
python
len([3, 1, 2]) # 3 <- 调用的是 list 的 __len__
[3, 1, 2][0] # 3 <- 调用的是 list 的 __getitem__
"a" + "b" # 'ab' <- 调用的是 str 的 __add__
3 == 3.0 # True <- 调用的是 int 的 __eq__
str(123) # '123' <- 调用的是 int 的 __str__
以上五条均为实测输出。列表、字符串、数字这些内建类型,全都实现了一套魔术方法 ------你用 + 能拼字符串,正是 str.__add__ 在工作。
学魔术方法,本质上是让自己的类也具备内建类型那样的能力。
2.5 一个可以自己验证的方法
想知道一个魔术方法有没有被调用?在里面加一句 print 就行了:
python
class Dog:
def __str__(self):
print(" [__str__ 被调用了]") # 加这一句观察
return "一只狗"
print(Dog())
输出:
text
[__str__ 被调用了]
一只狗
以后每学一个魔术方法,都可以用这招确认它到底有没有触发、什么时候触发。本文后面所有"运行结果"都是这样实测出来的。
第三部分:逐个学会常用魔术方法
以下 9 个魔术方法,每个都按统一结构展开:
作用 → 何时被自动触发 → 极简代码示例 → 运行结果 → 初学者易错点
3.1 __init__:给新对象填初始信息
作用
创建对象时,自动把初始信息存进对象里。可以理解成"对象出生时的登记手续"。
何时被自动触发
写 对象 = 类名(...) 的时候,括号里传的东西会原样交给 __init__。
极简代码示例
python
class Dog:
def __init__(self, name): # 创建对象时自动执行
self.name = name # 把名字存进对象里
def __str__(self): # 被 print() 时自动执行
return f"一只叫 {self.name} 的狗"
d = Dog("旺财") # 括号里的 "旺财" 传给了 name
print(d)
运行结果
text
一只叫 旺财 的狗
初学者易错点
易错点 1:把 __init__ 写成 __int__ 或 _init_。
少了或多了下划线,它就不是魔术方法了,创建对象时不会报错,但也不会执行。写错后如果发现"我明明写了却没生效",先检查下划线。
易错点 2:忘记写 self。
python
def __init__(name): # 少了 self,会报错
self.name = name
报错信息类似 TypeError: __init__() takes 1 positional argument but 2 were given。原因见 1.2 节------self 是自动传进来的第一个参数,少了它参数数量就错位了。
易错点 3:以为 __init__ 是"创建对象"。
它只是给已经创建好的对象填信息 。真正创建对象的是 __new__,但初学阶段完全不用管它。
易错点 4:在 __init__ 里写 return。
__init__ 必须返回 None(也就是不写 return)。写了 return 值 会报 TypeError。
3.2 __str__:面向用户的文字描述
作用
决定 print(对象) 时打印出什么。这是给用户看的文字。
何时被自动触发
print(对象)、str(对象)、f-string 里插对象时。
极简代码示例
python
class Book:
def __init__(self, title):
self.title = title
def __str__(self):
return f"《{self.title}》" # 面向用户,写好看点
b = Book("小王子")
print(b)
运行结果
text
《小王子》
如果不写 __str__,打印出来会是这样:
text
<__main__.Book object at 0x00000180FF303ED0>
这个形式初学者常觉得像乱码,其实它的意思是"这里有个 Book 对象,住在内存的某个地址"。
初学者易错点
易错点 1:忘了 return,写成 print。
python
def __str__(self):
print(f"《{self.title}》") # 错!这里应该是 return
写成 print 会报 TypeError: __str__ returned non-string。所有以 __str__ 为首的"返回文字"的魔术方法,都必须用 return。
易错点 2:返回的不是字符串。
python
def __str__(self):
return self.title # 如果 title 是数字就报错
必须是 str 类型。保险做法是用 f-string 包一层:return f"{self.title}"。
易错点 3:以为它能管住列表里的显示。
python
print(b) # 《小王子》 ← 走 __str__,符合预期
print([b]) # [<__main__.Book object at 0x...>] ← 不是想要的效果!
放进列表后显示变丑了。这涉及下一个魔术方法 __repr__,见 3.3。
3.3 __repr__:面向程序员的文字描述
作用
同样是"返回一段文字",但对象不同 ------__repr__ 是给程序员看的,用于调试和列表显示。
何时被自动触发
repr(对象)、在列表或字典里显示对象、以及在交互式命令行里直接输入对象名时。
极简代码示例
python
class Book:
def __init__(self, title):
self.title = title
def __repr__(self):
return f"Book({self.title!r})" # 面向程序员:能看出怎么造出来
def __str__(self):
return f"《{self.title}》" # 面向用户:好看
b = Book("小王子")
print("print(b) ->", b)
print("repr(b) ->", repr(b))
print("列表里显示 ->", [b])
运行结果
text
print(b) -> 《小王子》
repr(b) -> Book('小王子')
列表里显示 -> [Book('小王子')]
三行的差异值得仔细看 :同一个对象,print 时是给用户看的,repr() 和列表里显示时是给程序员看的。
两者分工速记
__str__ |
__repr__ |
|
|---|---|---|
| 给谁看 | 用户 | 程序员(调试) |
| 触发方式 | print() / str() |
repr() / 列表内 / 命令行 |
| 要求 | 好看、易懂 | 精确、能看出对象怎么造出来的 |
| 没写会怎样 | 回退到 __repr__ |
无处回退 ,显示 <object at 0x...> |
初学者易错点
易错点 1:只写 __str__,然后发现列表里显示很难看。
实测验证:
python
class OnlyStr:
def __str__(self):
return "我是 OnlyStr"
print(OnlyStr()) # 我是 OnlyStr
print([OnlyStr()]) # [<__main__.OnlyStr object at 0x00000180FF303ED0>]
记住这条规律:容器(列表、字典)内部显示元素时,一律用 __repr__。 所以如果你想让对象在列表里也好看,必须写 __repr__。
易错点 2:不确定写哪个,就纠结很久。
实用建议:初学阶段优先写 __repr__ 。因为它同时管住了"调试时看"和"列表里看"两个最常出问题的场景,而 __str__ 只是让 print 更漂亮。想两个都要,就都写。
易错点 3:两个方法写成一模一样。
那不如只写 __repr__(__str__ 会自动回退到它)。实测验证:只写 __repr__ 的类,print() 时确实会显示 __repr__ 的内容。
易错点 4:不理解结尾的 !r。
f"{self.title!r}" 里的 !r 表示"用 repr 的形式插入"。如果 title 是 小王子,插入结果是 '小王子'(带引号);不加 !r 则插入 小王子(不带引号)。带引号更像代码,这对调试更友好。
3.4 __len__:让对象有"长度"
作用
让 len(对象) 能工作,也能决定对象在 if 里算 True 还是 False。
何时被自动触发
len(对象)、bool(对象)(当你没写 __bool__ 时)。
极简代码示例
python
class Playlist:
def __init__(self, songs):
self.songs = songs
def __len__(self): # 被 len() 时自动执行
return len(self.songs)
p = Playlist(["A", "B", "C"])
print(len(p))
运行结果
text
3
附带效果:if 判断
实测确认,写了 __len__ 后,对象在 if 里的真假也会跟着变:
text
bool(有3首歌的歌单) -> True
bool(空歌单) -> False ← 长度为 0 视为 False
规律:长度为 0 → 假;长度非 0 → 真。 这和列表、字符串的行为一致(bool([]) 是 False)。
初学者易错点
易错点 1:返回了负数或非整数。
python
def __len__(self):
return -1 # 报错 ValueError: __len__() should return >= 0
必须返回大于等于 0 的整数。
易错点 2:忘了 return。
不返回东西会报 TypeError: 'NoneType' object cannot be interpreted as an integer。
易错点 3:return len(self.songs) 写成无限递归。
python
def __len__(self):
return len(self) # 错!len(self) 又去调 __len__,死循环
要 len 的是里面的那个容器 (self.songs),不是对象自己。
易错点 4:以为写了 __len__ 就能用 [] 取值。
len() 和 [] 是两个不同的魔术方法。要用 [] 还得写 __getitem__,见下一节。
3.5 __getitem__:让对象能用 [] 取值
作用
让对象支持 对象[下标] 这种写法,效果和列表一样。
何时被自动触发
对象[下标]、切片 对象[1:3]、for 遍历对象、in 判断。
极简代码示例
python
class MyList:
def __init__(self, data):
self.data = data
def __getitem__(self, i): # 被 对象[下标] 时自动执行
return self.data[i]
m = MyList([10, 20, 30])
print(m[0])
运行结果
text
10
附带的三个能力
实测确认,只写这一个方法,就白送了三种能力:
text
m[2] -> 30 支持正下标
m[-1] -> 30 支持负下标(倒数第一个)
m[0:2] -> [10, 20] 支持切片
for 遍历: 10 20 30 支持 for 循环
20 in m -> True 支持 in 判断
其中最反直觉的一点 :只要写了 __getitem__,即使不写 __iter__,对象也能被 for 遍历。实测:
python
class Counter:
def __init__(self, n):
self.n = n
def __getitem__(self, i): # 只有这个方法,也能被 for 遍历
if i >= self.n:
raise IndexError # 用 IndexError 告诉 for"结束了"
return i * i
c = Counter(4)
print([x for x in c]) # [0, 1, 4, 9]
for 会从下标 0 开始不断要值,直到你抛 IndexError 为止,它理解为"没有更多了"。
初学者易错点
易错点 1:忘了抛 IndexError。
python
def __getitem__(self, i):
return self.data[i] # 如果 data 是列表,列表自己会抛,没问题
但如果你自己手写逻辑而不抛异常:
python
def __getitem__(self, i):
return i * i # 永远不抛异常 → for 循环不会停!
这会导致 for 无限循环,程序卡死。
易错点 2:想用 [] 修改值,却只写了 __getitem__。
python
m[0] = 99 # 报错:不支持赋值
赋值需要另外写 __setitem__,删除需要 __delitem__。初学阶段先掌握取值即可。
易错点 3:raise IndexError 写成 return IndexError。
return 只是把异常类当普通值返回了,for 依然不会停。必须用 raise。
3.6 __call__:让对象能像函数一样被调用
作用
让对象后面直接加括号就能调用,像函数一样。
何时被自动触发
对象(参数) 这种写法。
极简代码示例
python
class Adder:
def __init__(self, n):
self.n = n
def __call__(self, x): # 被 对象() 时自动执行
return self.n + x
add5 = Adder(5)
print(add5(3))
运行结果
text
8
注意 add5(3) 这个写法 ------add5 是一个对象,不是函数,但它后面加了括号还能传参,这就是 __call__ 的作用。
实测确认对象与函数的可调用性一致:
text
callable(add5) -> True 可以像函数一样调用
callable(object()) -> False 普通对象不能加括号调用
初学者易错点
易错点 1:以为 __call__ 就是"创建对象"。
不是。Adder(5) 创建对象时调用的其实是 __init__;add5(3) 加上括号时调用的才是 __call__。两者是不同时机。
python
add5 = Adder(5) # 这里调 __init__
add5(3) # 这里调 __call__
易错点 2:不知道它有什么用。
最典型的用途是"记住一个配置,反复使用 "。比如 Adder(5) 先记住 n=5,之后 add5(1)、add5(100) 都能用这个 5,不必每次重传。这类对象常被称为"带记忆的函数"。
易错点 3:以为普通对象都能加括号。
不能。没写 __call__ 的对象加括号会报 TypeError: 'X' object is not callable。
3.7 __add__:让对象能用 +
作用
让两个对象能用 + 相加,自己决定"相加"的含义。
何时被自动触发
对象a + 对象b 时。
极简代码示例
python
class Money:
def __init__(self, yuan):
self.yuan = yuan
def __add__(self, other): # 被 a + b 时自动执行
return Money(self.yuan + other.yuan)
def __str__(self):
return f"{self.yuan} 元"
a = Money(10)
b = Money(20)
print(a + b)
运行结果
text
30 元
关键在 __add__ 里返回了什么 ------它返回的是一个新的 Money 对象 ,而不是数字 30。这样连续相加也没问题。
初学者易错点
易错点 1:不写 __add__ 就想用 +。
python
class Plain:
def __init__(self, y):
self.y = y
Plain(1) + Plain(2)
实测报错:
text
TypeError: unsupported operand type(s) for +: 'Plain' and 'Plain'
这句话的意思是"我不知道怎么把两个 Plain 加起来"。Python 不会猜你的意图 ,必须自己写 __add__ 告诉它。
易错点 2:忘了 other 参数。
python
def __add__(self): # 少了 other,a + b 会报错
return self.yuan
+ 是两个对象参与,所以 __add__(self, other) 必须收两个:self 是左边的,other 是右边的。
易错点 3:self.yuan + other 写错成 self.yuan + other.yuan 之外的形式。
只写 other 的时候,other 是整个对象,不是数字。要取里面的值得写 other.yuan。
易错点 4:以为所有运算符都能直接用。
+、-、*、/、<、> 等等每个运算符都对应一个独立的魔术方法(__add__、__sub__、__mul__......),写哪个才能用哪个。
3.8 __eq__:让对象能用 == 比较
作用
决定两个对象"相等"是什么意思。
何时被自动触发
对象a == 对象b 时。
极简代码示例
python
class Point:
def __init__(self, x, y):
self.x, self.y = x, y
def __eq__(self, other): # 被 == 时自动执行
return self.x == other.x and self.y == other.y
p1 = Point(1, 2)
p2 = Point(1, 2)
print(p1 == p2)
运行结果
text
True
对比:不写 __eq__ 会怎样
这是理解 __eq__ 最好的对照。实测:
text
不写 __eq__:
两个内容相同的对象比较 -> False ← 即使内容一样也是 False!
对象和自己比较 -> True
这是初学者最容易困惑的地方 :默认情况下,== 比较的不是"内容是否相同",而是"是不是同一个对象"。
所以:
python
Raw(1) == Raw(1) # False,这是两个不同的对象
r = Raw(1)
r == r # True,这是同一个对象
只有写了 __eq__,== 才会去比较内容。
初学者易错点
易错点 1:以为 == 默认比较内容。
如上所述,默认比较的是"是不是同一个"。想让内容相同的对象判为相等,必须自己写 __eq__。
易错点 2:忘记判断类型,拿对象和数字比。
python
p1 = Point(1, 2)
p1 == 5
实测报错:
text
AttributeError: 'int' object has no attribute 'x'
因为 5 没有 .x 属性。安全写法是先判断类型:
python
def __eq__(self, other):
if not isinstance(other, Point): # 不是 Point 就不比
return NotImplemented
return self.x == other.x and self.y == other.y
(NotImplemented 是 Python 的特殊约定写法,初学阶段知道要加这个判断即可。)
易错点 3:只写了 __eq__,结果对象不能放进集合了。
写了 __eq__ 之后,Python 会认为这个对象"默认不可哈希",于是不能放进 set 或作字典的键。初学阶段如果遇到"我的对象怎么放进 set 就报错",回头看看是不是写了 __eq__ 但没写 __hash__。
3.9 __del__:对象被回收时的收尾
作用
对象被销毁(回收)时自动执行的收尾动作。
何时被自动触发
- 用
del删掉最后一个引用时; - 程序结束时,所有还活着的对象被统一回收时。
极简代码示例
python
class Temp:
def __init__(self, name):
self.name = name
print(f" 创建 {self.name}")
def __del__(self): # 对象被回收时自动执行
print(f" 回收 {self.name}")
t = Temp("对象A")
del t # 删掉引用,对象被回收
运行结果
text
创建 对象A
回收 对象A
初学者易错点
易错点 1:以为 del t 是"删除对象"。
更准确的理解是:del t 删掉的是"名字 t 对对象的引用" 。只有当没有任何名字再指向这个对象时,对象才会被回收,__del__ 才执行。
python
a = Temp("X")
b = a # 现在两个名字指向同一个对象
del a # 只是少了一个名字,对象还在,__del__ 不执行
del b # 最后一个名字也没了,这时才回收
易错点 2:以为 __del__ 会立刻、准时执行。
这是最重要的一条。实测验证:
python
def make():
x = Temp("函数内对象")
return "函数返回"
print(make())
实测输出:
text
创建 函数内对象
回收 函数内对象
调用函数 -> 函数返回
注意顺序:"回收"那行虽然写在函数里,却是等到函数返回之后才打印的。 而且它出现的时机由 Python 的垃圾回收决定,你无法精确控制。
易错点 3:用 __del__ 做重要的清理工作(例如关文件、存数据)。
不要这么做。 因为触发时机不可控,程序可能在回收前就退出了,导致清理没执行。
需要"保证一定执行的清理"时,用 with 语句(对应 __enter__ / __exit__ 两个魔术方法),这是初学阶段值得养成的习惯,本文不展开。
易错点 4:把 __del__ 和 __delete__ 搞混。
是两个不同的东西。__del__ 管对象回收,__delete__ 管属性删除。初学阶段只需记住 __del__。
3.10 综合示例:把学到的串起来
下面这个类同时用了前面 6 个魔术方法,可以对照理解它们如何协作:
python
class Student:
def __init__(self, name, scores):
self.name = name
self.scores = scores
def __str__(self):
return f"{self.name},共 {len(self.scores)} 门课"
def __len__(self):
return len(self.scores)
def __getitem__(self, i):
return self.scores[i]
def __eq__(self, other):
return self.name == other.name
s = Student("小明", [90, 85, 77])
print(s)
print("门数:", len(s))
print("第一门:", s[0])
print("成绩遍历:", [x for x in s])
print("同名相等:", s == Student("小明", [1, 2]))
运行结果
text
小明,共 3 门课
门数: 3
第一门: 90
成绩遍历: [90, 85, 77]
同名相等: True
逐行对应:
| 代码 | 触发的魔术方法 |
|---|---|
print(s) |
__str__ |
len(s) |
__len__ |
s[0] |
__getitem__ |
[x for x in s] |
__getitem__(借它实现了遍历) |
s == Student(...) |
__eq__ |
同一个对象,配合不同的运算符,自动调用了不同的魔术方法------这就是"魔术"两个字的由来。
第四部分:速查表
常用魔术方法
| 方法名 | 触发场景 | 示例表达式 |
|---|---|---|
__init__ |
创建对象 | d = Dog("旺财") |
__str__ |
转成给用户看的文字 | print(d) |
__repr__ |
转成给程序员看的文字 | repr(d)、[d] |
__len__ |
求长度 | len(d) |
__getitem__ |
用下标取值 | d[0] |
__setitem__ |
用下标赋值 | d[0] = 9 |
__delitem__ |
用下标删除 | del d[0] |
__contains__ |
in 判断 |
x in d |
__iter__ |
for 遍历 |
for x in d: |
__call__ |
像函数一样调用 | d(3) |
__add__ |
加法 | d1 + d2 |
__sub__ |
减法 | d1 - d2 |
__mul__ |
乘法 | d * 3 |
__eq__ |
== 比较 |
d1 == d2 |
__lt__ |
< 比较 |
d1 < d2 |
__bool__ |
真假判断 | if d: |
__del__ |
对象被回收 | del d |
内建类型里同样的魔术方法
| 表达式 | 实际调用 |
|---|---|
len([1, 2]) |
list.__len__ |
[1, 2][0] |
list.__getitem__ |
"a" + "b" |
str.__add__ |
3 == 3.0 |
int.__eq__ |
str(123) |
int.__str__ |
[1] + [2] |
list.__add__ |
以上均为实测输出。你早就在使用魔术方法了。
命名规则速查
| 形式 | 含义 |
|---|---|
name |
普通名字 |
_name |
约定为内部使用(无强制力) |
__name |
类内私有,会被改名 |
__name__ |
系统保留,只使用 Python 已定义的 |
第五部分:给初学者的五条建议
1. 不要试图背完全部
Python 有上百个魔术方法,但日常真正用到的就十几个。本文覆盖的 9 个已经能解决绝大多数需求。剩余的用到再查。
2. 写错了不会报错,是最难查的一类问题
__init__ 写成 _init_、__str__ 里写成 print 而不是 return------这类错误往往不报异常,只是"没生效" 。养成习惯:写完魔术方法,先用 print 验证它有没有被触发(方法见 2.5 节)。
3. 分清"给用户看"和"给程序员看"
这是初学者最容易混淆的一对。记住:
__str__→ 用户,写成《小王子》__repr__→ 程序员,写成Book('小王子')- 只写一个的话,优先写
__repr__
4. __del__ 不要用来做必要的清理
它的触发时机由垃圾回收决定,你控制不了 。需要"保证执行"的收尾动作,用 with 语句。
5. 每次只加一个魔术方法
不要一次给类加五个魔术方法,然后发现某个不生效却不知道是哪个的问题。加一个、跑一次、确认生效、再加下一个。 这个节奏在初学阶段最稳。
本文档面向零 OOP 基础的 Python 初学者编写