咱先唠唠啥是 Dunder 方法?其实就是 Python 里那些用双下划线__
包裹的方法(比如__eq__
、__str__
),翻译过来叫 “特殊方法”。这些方法能让你的类拥有 “超能力”—— 比如让对象能比较相等、能自定义打印格式、甚至能用下标访问(像列表一样)。
今天咱就用一个Fruit
类当例子,把 5 个最常用的 Dunder 方法讲明白。先定义一个基础的Fruit
类,后面每个方法都基于它扩展:
# 基础Fruit类:包含名称、颜色、数量三个属性
class Fruit:
def __init__(self, name, color, quantity):
self.name = name # 水果名,比如"苹果"
self.color = color # 颜色,比如"红色"
self.quantity = quantity # 数量,比如5
__eq__
:让对象能比较 “相等”咱先想个问题:如果创建两个 “看起来一样” 的水果对象,用==
比较会咋样?
先跑段代码看看默认行为:
# 创建两个"相同"的苹果
apple1 = Fruit("苹果", "红色", 5)
apple2 = Fruit("苹果", "红色", 5)
# 用==比较
print(apple1 == apple2) # 输出:False
为啥会是False
?因为 Python 默认的==
比较的是 “对象的内存地址”(相当于is
运算符),哪怕两个对象的属性完全一样,只要是不同的实例,就判定为不相等。这显然不符合咱的实际需求 —— 咱想要的是 “属性相同就相等”。
__eq__
方法要解决这个问题,就得重写__eq__
方法,告诉 Python “怎么判断两个 Fruit 相等”。咱规定:只要name
、color
、quantity
都相同,就认为是相等的。
修改后的Fruit
类(新增__eq__
):
class Fruit:
def __init__(self, name, color, quantity):
self.name = name
self.color = color
self.quantity = quantity
# 新增__eq__方法:比较两个Fruit的核心属性
def __eq__(self, other):
# 先判断other是不是Fruit类型,避免报错
if not isinstance(other, Fruit):
return False
# 比较三个核心属性,全相同则返回True
return (self.name == other.name
and self.color == other.color
and self.quantity == other.quantity)
再跑之前的代码,结果就对了:
apple1 = Fruit("苹果", "红色", 5)
apple2 = Fruit("苹果", "红色", 5)
apple3 = Fruit("苹果", "绿色", 5) # 颜色不同
print(apple1 == apple2) # 输出:True(属性全相同)
print(apple1 == apple3) # 输出:False(颜色不同)
print(apple1 == "苹果") # 输出:False(other不是Fruit类型)
return self.name == other.name...
,当other
不是 Fruit(比如字符串)时,会报AttributeError
(找不到 name 属性)。所以必须先加isinstance(other, Fruit)
判断。name
,那 “红色 5 个苹果” 和 “绿色 10 个苹果” 会被判相等,这显然不对。要根据业务场景选全量或核心属性比较。__format__
:自定义格式化输出咱平时用format()
函数或 f-string 格式化输出时,默认打印对象会是"<__main__.Fruit object at 0x0000023456789ABC>"
,特别不直观。__format__
方法能让咱自定义格式化规则。
__format__
方法咱给Fruit
加个__format__
,支持 3 种格式化规则(用表格列清楚):
格式化符 | 作用 | 示例输出 |
---|---|---|
| 输出完整信息 | 苹果(红色,5 个) |
| 只输出颜色和名称 | 红色苹果 |
| 只输出名称和数量 | 苹果:5 个 |
修改后的Fruit
类(新增__format__
):
class Fruit:
def __init__(self, name, color, quantity):
self.name = name
self.color = color
self.quantity = quantity
def __eq__(self, other):
if not isinstance(other, Fruit):
return False
return (self.name == other.name
and self.color == other.color
and self.quantity == other.quantity)
# 新增__format__方法:自定义格式化逻辑
def __format__(self, format_spec):
# 根据format_spec(格式化符)返回不同结果
if format_spec == "s":
return f"{self.name}({self.color},{self.quantity}个)"
elif format_spec == "c":
return f"{self.color}{self.name}"
elif format_spec == "q":
return f"{self.name}:{self.quantity}个"
# 默认情况(没传格式化符),返回完整信息
else:
return f"Fruit(name={self.name}, color={self.color}, quantity={self.quantity})"
用format()
或 f-string 测试,结果完全符合预期:
apple = Fruit("苹果", "红色", 5)
# 1. 用format()函数
print(format(apple, "s")) # 输出:苹果(红色,5个)
print(format(apple, "c")) # 输出:红色苹果
print(format(apple, "q")) # 输出:苹果:5个
print(format(apple)) # 输出:Fruit(name=苹果, color=红色, quantity=5)(默认)
# 2. 用f-string(更常用)
print(f"我买了一个{apple:s}") # 输出:我买了一个苹果(红色,5个)
print(f"这个{apple:c}真甜") # 输出:这个红色苹果真甜
format(apple, "x")
),会走默认逻辑。如果没写默认分支,会返回None
,打印出来是None
,所以一定要加默认处理。__format__
必须返回字符串,要是不小心返回了数字(比如return self.quantity
),会报TypeError
,这点要注意。__or__
:实现对象的 “合并” 操作__or__
方法对应 Python 里的|
运算符(按位或),咱可以用它实现 “合并两个水果对象” 的逻辑。比如把 “红色 3 个苹果” 和 “绿色 2 个苹果” 合并成 “红绿色 5 个苹果”。
__or__
方法咱定义合并规则:
name
必须相同(不同水果不能合并,比如苹果和香蕉不能合并);color
合并成集合(去重,比如 “红色”+“绿色”→“红绿色”);quantity
相加(3+2=5);修改后的Fruit
类(新增__or__
):
class Fruit:
def __init__(self, name, color, quantity):
self.name = name
self.color = color
self.quantity = quantity
def __eq__(self, other):
if not isinstance(other, Fruit):
return False
return (self.name == other.name
and self.color == other.color
and self.quantity == other.quantity)
def __format__(self, format_spec):
if format_spec == "s":
return f"{self.name}({self.color},{self.quantity}个)"
elif format_spec == "c":
return f"{self.color}{self.name}"
elif format_spec == "q":
return f"{self.name}:{self.quantity}个"
else:
return f"Fruit(name={self.name}, color={self.color}, quantity={self.quantity})"
# 新增__or__方法:实现水果合并(对应|运算符)
def __or__(self, other):
# 先判断两个水果的name是否相同,不同则报错
if self.name != other.name:
raise ValueError(f"不能合并不同种类的水果:{self.name} 和 {other.name}")
# 合并颜色:用集合去重,再转成字符串(比如{"红色","绿色"}→"红绿色")
merged_color = "".join(set([self.color, other.color]))
# 合并数量:直接相加
merged_quantity = self.quantity + other.quantity
# 返回新的Fruit对象
return Fruit(self.name, merged_color, merged_quantity)
用|
运算符合并水果,看看效果:
# 创建两个苹果(同种类,不同颜色和数量)
red_apple = Fruit("苹果", "红色", 3)
green_apple = Fruit("苹果", "绿色", 2)
# 合并两个苹果
merged_apple = red_apple | green_apple
print(format(merged_apple, "s")) # 输出:苹果(红绿色,5个)(颜色合并,数量相加)
# 尝试合并苹果和香蕉(不同种类)
banana = Fruit("香蕉", "黄色", 4)
try:
red_apple | banana
except ValueError as e:
print(e) # 输出:不能合并不同种类的水果:苹果 和 香蕉(符合预期)
__or__
应该返回新对象,而不是修改self
或other
。如果写成self.quantity += other.quantity
,会导致原对象被修改,后续再用原对象就出问题了。red_apple | 123
(other 不是 Fruit),会报AttributeError
(找不到 name 属性)。可以加if not isinstance(other, Fruit): raise TypeError(...)
优化。__str__
& __repr__
:控制对象的 “字符串表示”这俩方法经常被搞混,都是用来显示对象的字符串形式,但用途完全不同。咱先拿表格对比清楚,再写代码。
特性 |
|
|
---|---|---|
作用 | 给 “用户” 看的友好输出 | 给 “开发者” 看的详细输出 |
使用场景 |
|
|
核心要求 | 简洁、易懂 | 详细、准确(最好能通过输出重建对象) |
默认行为 | 同 | 打印 “<类名 object at 内存地址>” |
__str__
和__repr__
咱给Fruit
加这两个方法,遵循上面的区别:
__str__
:友好显示,比如 “红色苹果(5 个)”;__repr__
:详细到能重建对象,比如Fruit("苹果", "红色", 5)
。修改后的Fruit
类(新增__str__
和__repr__
):
class Fruit:
def __init__(self, name, color, quantity):
self.name = name
self.color = color
self.quantity = quantity
def __eq__(self, other):
if not isinstance(other, Fruit):
return False
return (self.name == other.name
and self.color == other.color
and self.quantity == other.quantity)
def __format__(self, format_spec):
if format_spec == "s":
return f"{self.name}({self.color},{self.quantity}个)"
elif format_spec == "c":
return f"{self.color}{self.name}"
elif format_spec == "q":
return f"{self.name}:{self.quantity}个"
else:
return f"Fruit(name={self.name}, color={self.color}, quantity={self.quantity})"
def __or__(self, other):
if not isinstance(other, Fruit):
raise TypeError("只能合并Fruit类型的对象")
if self.name != other.name:
raise ValueError(f"不能合并不同种类的水果:{self.name} 和 {other.name}")
merged_color = "".join(set([self.color, other.color]))
merged_quantity = self.quantity + other.quantity
return Fruit(self.name, merged_color, merged_quantity)
# 新增__str__:给用户看的友好输出
def __str__(self):
return f"{self.color}{self.name}({self.quantity}个)"
# 新增__repr__:给开发者看的详细输出(可重建对象)
def __repr__(self):
return f'Fruit(name="{self.name}", color="{self.color}", quantity={self.quantity})'
分别测试print()
(调用__str__
)和repr()
(调用__repr__
):
apple = Fruit("苹果", "红色", 5)
# 1. 测试__str__(用户视角)
print(apple) # 输出:红色苹果(5个)(调用__str__)
print(str(apple)) # 输出:红色苹果(5个)(直接调用__str__)
print(f"我有一个{apple}") # 输出:我有一个红色苹果(5个)(f-string调用__str__)
# 2. 测试__repr__(开发者视角)
print(repr(apple)) # 输出:Fruit(name="苹果", color="红色", quantity=5)(调用__repr__)
# 在交互式环境里直接输apple,也会显示__repr__的结果
# >>> apple
# Fruit(name="苹果", color="红色", quantity=5)
__str__
,那repr(apple)
会用默认的内存地址输出;只写__repr__
,print(apple)
会用__repr__
的结果,不够友好。建议两个都写。__repr__
只写f"Fruit({self.name})"
,开发者无法通过这个输出重建对象,失去了__repr__
的核心意义。__getitem__
:让对象支持 “下标访问”列表能用list[0]
,字典能用dict["key"]
,那咱的 Fruit 对象能做到吗?默认不行,但加了__getitem__
方法就可以!咱让 Fruit 支持两种下标访问:
fruit[0]
→name,fruit[1]
→color,fruit[2]
→quantity;fruit["name"]
→name,fruit["color"]
→color,fruit["quantity"]
→quantity。__getitem__
方法修改后的Fruit
类(新增__getitem__
):
class Fruit:
def __init__(self, name, color, quantity):
self.name = name
self.color = color
self.quantity = quantity
def __eq__(self, other):
if not isinstance(other, Fruit):
return False
return (self.name == other.name
and self.color == other.color
and self.quantity == other.quantity)
def __format__(self, format_spec):
if format_spec == "s":
return f"{self.name}({self.color},{self.quantity}个)"
elif format_spec == "c":
return f"{self.color}{self.name}"
elif format_spec == "q":
return f"{self.name}:{self.quantity}个"
else:
return f"Fruit(name={self.name}, color={self.color}, quantity={self.quantity})"
def __or__(self, other):
if not isinstance(other, Fruit):
raise TypeError("只能合并Fruit类型的对象")
if self.name != other.name:
raise ValueError(f"不能合并不同种类的水果:{self.name} 和 {other.name}")
merged_color = "".join(set([self.color, other.color]))
merged_quantity = self.quantity + other.quantity
return Fruit(self.name, merged_color, merged_quantity)
def __str__(self):
return f"{self.color}{self.name}({self.quantity}个)"
def __repr__(self):
return f'Fruit(name="{self.name}", color="{self.color}", quantity={self.quantity})'
# 新增__getitem__:支持下标访问
def __getitem__(self, key):
# 1. 处理数字下标
if key == 0:
return self.name
elif key == 1:
return self.color
elif key == 2:
return self.quantity
# 2. 处理字符串下标
elif key == "name":
return self.name
elif key == "color":
return self.color
elif key == "quantity":
return self.quantity
# 3. 处理无效下标
else:
raise KeyError(f"无效的下标:{key}(支持0/1/2或'name'/'color'/'quantity')")
现在 Fruit 对象能像列表 / 字典一样用下标访问了:
apple = Fruit("苹果", "红色", 5)
# 1. 数字下标访问
print(apple[0]) # 输出:苹果(对应name)
print(apple[1]) # 输出:红色(对应color)
print(apple[2]) # 输出:5(对应quantity)
# 2. 字符串下标访问
print(apple["name"]) # 输出:苹果
print(apple["color"]) # 输出:红色
print(apple["quantity"]) # 输出:5
# 3. 无效下标(测试错误处理)
try:
apple[3]
except KeyError as e:
print(e) # 输出:无效的下标:3(支持0/1/2或'name'/'color'/'quantity')
try:
apple["price"]
except KeyError as e:
print(e) # 输出:无效的下标:price(支持0/1/2或'name'/'color'/'quantity')
else
分支,传无效下标会返回None
,而不是报错,开发者很难排查问题。一定要主动抛KeyError
或IndexError
。apple[0]
返回 quantity,apple["name"]
返回 color,会让使用者 confusion。下标对应关系要清晰,最好在文档或错误信息里说明。方法 | 常见坑 | 避坑方案 |
---|---|---|
| 没判断 other 类型,导致 AttributeError | 先加 |
| 只比较部分属性,导致逻辑错误 | 根据业务场景,比较全量或核心属性 |
| 没处理默认格式化符,返回 None | 加 else 分支,返回默认字符串 |
| 返回非字符串类型,导致 TypeError | 确保所有分支都返回字符串 |
| 修改原对象,而不是返回新对象 | 新建实例返回,不修改 self/other |
| 没处理不同类型的合并(比如 Fruit 和 int) | 加 |
| 只写一个方法,输出不友好 / 不详细 | 两个方法都写,遵循 “用户友好 / 开发者详细” 原则 |
| 输出不完整,无法重建对象 | 包含所有初始化参数,格式对应 |
| 没处理无效下标,返回 None | 抛 KeyError/IndexError,说明支持的下标 |
| 下标对应关系混乱 | 清晰定义下标逻辑,在错误信息里说明 |
__eq__
方法和==
运算符是什么关系?默认的__eq__
做什么?答:==
运算符其实就是调用对象的__eq__
方法。比如a == b
,本质是执行a.__eq__(b)
。默认的__eq__
比较的是 “对象的内存地址”,相当于is
运算符 —— 哪怕两个对象属性一样,只要是不同实例,就返回 False。所以咱需要重写__eq__
,按属性比较。
__str__
和__repr__
有啥区别?分别在什么时候调用?答:核心区别是 “给谁看”:
__str__
是给用户看的,要友好易懂,比如print(对象)
、str(对象)
、f-string 都会调用它;__repr__
是给开发者看的,要详细准确(最好能重建对象),比如repr(对象)
、交互式环境直接输对象会调用它。
默认情况下,两者都打印内存地址,但重写后就按咱定义的来。__getitem__
方法有啥用?怎么让对象支持像列表一样的切片(比如obj[1:3]
)?答:__getitem__
能让对象支持下标访问(比如obj[key]
),key 可以是数字、字符串甚至切片对象。要支持切片的话,在__getitem__
里判断 key 是不是slice
类型就行 —— 比如if isinstance(key, slice)
,然后用key.start
、key.stop
、key.step
处理切片逻辑,最后返回切片后的结果(比如新列表)。
__or__
方法对应哪个运算符?实现时要注意什么?答:__or__
对应|
运算符(按位或),但咱可以自定义它的逻辑(比如合并对象)。实现时要注意两点:
self
和other
),要返回新对象 —— 不然原对象会被污染;__eq__
后,要不要重写__hash__
?为什么?答:要!因为 Python 有个规则:“如果两个对象的__eq__
返回 True,那它们的__hash__
必须返回相同的值”。默认的__hash__
是根据内存地址计算的,重写__eq__
后,哪怕两个对象__eq__
返回 True,默认__hash__
还是不同,这会导致对象在字典、集合里出问题(比如集合里存两个__eq__
为 True 的对象,会存成两个,而不是一个)。所以重写__eq__
时,一定要同步重写__hash__
—— 比如def __hash__(self): return hash((self.name, self.color, self.quantity))
,用__eq__
比较的属性来计算 hash 值。
这 5 个 Dunder 方法是 Python 类的 “核心工具”,光看不行,得自己动手试:
Fruit
类代码,跑一遍所有测试案例,感受每个方法的效果;Fruit
加__add__
(对应+
运算符,实现数量相加)、__len__
(返回数量,支持len(apple)
);__getitem__
让自定义的Student
类支持student["score"]
访问成绩。掌握这些方法,你写的 Python 类会更灵活、更符合 Python 风格,编程效率也会翻倍!
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。