完整讲解 · 5 段教学·配 5 道练习题·预计 35 分钟
本页是本章的通读版,可直接读完全部讲解。想动手写代码、跑判分,去 闯关模式。
在 Python 中,所有以双下划线 __ 包起来的方法,都统称为 魔法方法(Magic Methods),也叫 魔术方法 / dunder 方法(dunder = double underscore)。
最常见的就是我们在上一章见过的 __init__。它在创建对象时自动被调用——你从来没有手动写过 obj.__init__(...),但它就是会跑。
魔法方法的能力远不止 __init__。它能让你自定义 Python 内置语法对你的对象的行为,比如:
| 写法 | 背后调用的魔法方法 |
|---|---|
print(obj) / str(obj) | __str__ |
obj1 == obj2 | __eq__ |
len(obj) | __len__ |
obj[0] | __getitem__ |
obj1 + obj2 | __add__ |
一句话:魔法方法 = 把你自己定义的类,接入 Python 内置语法。
任意一个类(哪怕一个空类)都已经从 object 继承了一堆魔法方法。用 dir() 可以查看:
class User:
pass
print(dir(User()))输出(截取一部分):
['__class__', '__delattr__', '__dict__', '__dir__', '__eq__', '__format__',
'__getattribute__', '__hash__', '__init__', '__lt__', '__new__', '__repr__',
'__setattr__', '__str__', ...]
可以看到,魔法方法非常多。本节我们挑最常用的几个学透——__str__ / __repr__、__eq__、__len__ / __getitem__、__add__。学完这几个,你写出的类就有了「Python 风味」。
__str__ 与 __repr__:让 print 输出友好先看一个问题。我们写一个最普通的 Book 类:
class Book:
def __init__(self, title):
self.title = title
b = Book('Python 入门')
print(b)输出是什么?
<__main__.Book object at 0x10a9f3d50>
一串 ✗ 的乱码。这是因为默认情况下,print(obj) 会调用 object 的 __str__,它只会告诉你「这是一个 Book 对象,地址是 0x...」——对人类来说毫无信息量。
__str__:给「人」看的字符串我们来重写它:
class Book:
def __init__(self, title):
self.title = title
def __str__(self):
return f'《{self.title}》'
b = Book('Python 入门')
print(b) # 《Python 入门》
print(str(b)) # 《Python 入门》__str__ 必须返回一个字符串。print(obj) 和 str(obj) 都会调用它。
__repr__:给「程序员」看的字符串__repr__ 也是返回字符串,但用途不同:它通常是面向开发者的、最好能复现这个对象的写法。
最典型的场景是:在交互式终端里直接敲变量名,或者把对象放进 list 后 print:
class Book:
def __init__(self, title):
self.title = title
def __str__(self):
return f'《{self.title}》'
def __repr__(self):
return f"Book(title={self.title!r})"
b = Book('Python 入门')
print(b) # 《Python 入门》 ← 走 __str__
print([b]) # [Book(title='Python 入门')] ← 容器里走 __repr__注意:
__repr__、没定义 __str__,那 print(obj) 会回退到 __repr____str__ 时,[obj] 仍然显示默认的乱码经验法则:至少先把
__repr__写了。再考虑要不要加更友好的__str__。
__eq__:自定义「相等」的含义默认情况下,== 比较两个对象是不是同一个对象(同一块内存),等价于 is:
class Money:
def __init__(self, amount):
self.amount = amount
a = Money(100)
b = Money(100)
print(a == b) # False ← 两个不同对象,默认就是不相等但从「业务」角度看,100 块钱就是 100 块钱——只要金额相同,就该相等。这时候定义 __eq__:
class Money:
def __init__(self, amount):
self.amount = amount
def __eq__(self, other):
return self.amount == other.amount
a = Money(100)
b = Money(100)
c = Money(50)
print(a == b) # True
print(a == c) # False
print(a != c) # True ← != 默认是 __eq__ 的反__eq__(self, other) 接收两个参数,返回 True / False。Python 会把 a == b 翻译成 a.__eq__(b)。
不过上面这个版本有个坑:other 不一定是 Money。要是有人写 Money(100) == 100,other 就是数字 100,没有 .amount 属性,直接抛 AttributeError 报错——比较运算不该报错,应该老老实实返回 False。
所以稳妥的写法是先检查类型再比较:
class Money:
def __init__(self, amount):
self.amount = amount
def __eq__(self, other):
return isinstance(other, Money) and self.amount == other.amount
print(Money(100) == 100) # False——不再报错isinstance(other, Money) 判断 other 是不是 Money 类型(含子类);不是就直接短路返回 False,后半句根本不会执行。
| 写法 | 魔法方法 |
|---|---|
a == b | __eq__ |
a != b | __ne__(不写也会自动用 __eq__ 取反) |
a < b | __lt__ |
a > b | __gt__ |
a <= b | __le__ |
a >= b | __ge__ |
举个完整例子:
class Number:
def __init__(self, value):
self.value = value
def __eq__(self, other):
return self.value == other.value
def __lt__(self, other):
return self.value < other.value
n1 = Number(2)
n2 = Number(3)
print(n1 == n2) # False
print(n1 < n2) # True(真实代码里这些方法同样建议先做 isinstance 检查,这里为了突出运算符家族省略了。)
__hash__ 的小坑只要你重写了 __eq__,Python 会自动把 __hash__ 设为 None——也就是说,这个对象不能再放进 set / dict 的 key。
如果想让对象既可比较又可哈希,要手动加上 __hash__:
class Money:
def __init__(self, amount):
self.amount = amount
def __eq__(self, other):
return isinstance(other, Money) and self.amount == other.amount
def __hash__(self):
return hash(self.amount)规则:相等的对象,hash 必须相等。所以
__hash__通常基于__eq__用到的同一组属性。
__len__ 与 __getitem__:让对象「像容器」Python 的 list、tuple、str、dict 都是「容器」。它们都支持:
len(x) 取长度x[0] 按 key 取元素for item in x 迭代如果你自己写的类也想支持这些语法呢?——只要实现对应的魔法方法即可。
__len__:让 len() 工作class Playlist:
def __init__(self, songs):
self.songs = songs
def __len__(self):
return len(self.songs)
p = Playlist(['歌1', '歌2', '歌3'])
print(len(p)) # 3__len__ 必须返回一个非负整数。如果不实现,len(p) 会抛 TypeError: object of type 'Playlist' has no len()。
__getitem__:让 obj[i] 工作class Playlist:
def __init__(self, songs):
self.songs = songs
def __len__(self):
return len(self.songs)
def __getitem__(self, index):
return self.songs[index]
p = Playlist(['歌1', '歌2', '歌3'])
print(p[0]) # 歌1
print(p[-1]) # 歌3__getitem__(self, key) 中的 key 可以是:
slice(1, 3)(来自 p[1:3] 这种写法)for 迭代只要你实现了 __getitem__,不用再写 __iter__,for 循环也能用了:
class Playlist:
def __init__(self, songs):
self.songs = songs
def __getitem__(self, i):
return self.songs[i]
p = Playlist(['歌1', '歌2', '歌3'])
for s in p:
print(s)
# 歌1
# 歌2
# 歌3Python 会从 p[0]、p[1]、p[2]... 一直叫,直到抛 IndexError 才停。
__setitem__class Playlist:
def __init__(self, songs):
self.songs = songs
def __getitem__(self, i):
return self.songs[i]
def __setitem__(self, i, value):
self.songs[i] = value
p = Playlist(['a', 'b', 'c'])
p[0] = 'A' # 触发 __setitem__
print(p.songs) # ['A', 'b', 'c']总结:实现
__len__+__getitem__就有「只读容器」;再加__setitem__/__delitem__就成「可变容器」。
__add__:让对象支持 + 运算Python 的 + 对不同类型有不同含义:
1 + 2 → 3(数字相加)'a' + 'b' → 'ab'(字符串拼接)[1] + [2] → [1, 2](列表拼接)这些都不是 Python「写死」的——它们各自的类都实现了 __add__。我们也可以让自己的类支持 +。
__add__ 实现二维向量加法class Vec2:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return Vec2(self.x + other.x, self.y + other.y)
def __repr__(self):
return f'Vec2({self.x}, {self.y})'
v1 = Vec2(1, 2)
v2 = Vec2(3, 4)
v3 = v1 + v2 # 触发 v1.__add__(v2)
print(v3) # Vec2(4, 6)注意:__add__ 通常返回一个新对象,而不是修改 self。这跟 1 + 2 不会改变 1 是同一个道理。
| 写法 | 魔法方法 |
|---|---|
a + b | __add__ |
a - b | __sub__ |
a * b | __mul__ |
a / b | __truediv__ |
a // b | __floordiv__ |
a % b | __mod__ |
a ** b | __pow__ |
-a | __neg__ |
abs(a) | __abs__ |
魔法方法非常多,本节只是入门最常用的几个。下面这些等你日后用到再查就行:
| 魔法方法 | 触发场景 |
|---|---|
__call__ | 让对象像函数一样被调用:obj() |
__iter__ / __next__ | 自定义迭代器 |
__enter__ / __exit__ | with 语句的上下文管理器 |
__getattr__ / __setattr__ | 拦截属性访问 |
__new__ | 控制对象创建(在 __init__ 之前) |
__name__ 形式、由 Python 自动调用的特殊方法__str__ / __repr__:定义对象怎么变成字符串__eq__ 等:定义比较运算__len__ / __getitem__:让对象像容器__add__ 等:让对象支持算术运算下一步去做练习,把这几个魔法方法亲手写一遍。
读完了?动手练一遍才算真会。
去闯关模式练习 →