Python 的自定义类并不只能保存数据。通过实现一组特殊方法,也就是常说的 dunder methods,可以让对象参与加法、比较、迭代、长度计算,甚至被内置函数直接处理。掌握这些方法,是理解 Python 数据模型和面向对象编程的重要一步。
运算符背后的特殊方法
当代码写成 a + b 时,Python 会尝试调用 a.__add__(b)。类似地,a == b 通常对应 __eq__,a < b 对应 __lt__,len(a) 对应 __len__。
下面的例子实现了一个二维向量类:
from __future__ import annotations
from math import hypot
class Vector:
def __init__(self, x: float, y: float) -> None:
self.x = x
self.y = y
def __add__(self, other: Vector) -> Vector:
if not isinstance(other, Vector):
return NotImplemented
return Vector(self.x + other.x, self.y + other.y)
def __eq__(self, other: object) -> bool:
if not isinstance(other, Vector):
return NotImplemented
return self.x == other.x and self.y == other.y
def __abs__(self) -> float:
return hypot(self.x, self.y)
def __repr__(self) -> str:
return f"Vector({self.x}, {self.y})"
first = Vector(3, 4)
second = Vector(1, 2)
print(first + second) # Vector(4, 6)
print(first == Vector(3, 4)) # True
print(abs(first)) # 5.0
返回 NotImplemented 比直接返回 False 或抛出无关异常更合适。它告诉 Python 当前对象无法处理这个操作,解释器仍有机会尝试右操作数的反向方法,例如 __radd__。
让对象支持内置函数
函数重载在 Python 中通常不是通过同名函数写多个参数版本实现的。更常见的做法是实现协议方法,让内置函数知道如何使用对象。
例如,len(value) 会寻找 value.__len__(),str(value) 会寻找 value.__str__(),而 list(value) 会尝试从对象获取迭代器。
class Playlist:
def __init__(self, songs: list[str]) -> None:
self._songs = list(songs)
def __len__(self) -> int:
return len(self._songs)
def __iter__(self):
return iter(self._songs)
def __contains__(self, song: str) -> bool:
return song in self._songs
def __getitem__(self, index: int) -> str:
return self._songs[index]
def __repr__(self) -> str:
return f"Playlist({self._songs!r})"
playlist = Playlist(["Intro", "Theme", "Finale"])
print(len(playlist)) # 3
print("Theme" in playlist) # True
print(playlist[0]) # Intro
print(list(playlist)) # ['Intro', 'Theme', 'Finale']
这种设计的价值在于,调用方可以使用熟悉的 Python 语法,而不必记住一组专门的 API 名称。不过,协议方法应保持直观语义:__len__ 应返回数量,__contains__ 应表达成员关系,__iter__ 应返回可迭代对象。
实践时要注意什么
运算符重载不是把所有操作都塞进类里的理由。可以根据对象的自然含义选择少量方法:
- 用
__add__表达有明确业务意义的合并或相加。 - 用
__eq__定义值相等,而不是默认的对象身份相等。 - 用
__lt__、__le__等支持排序时,确保比较规则稳定。 - 用
__repr__提供便于调试的表达式风格文本。 - 对不支持的操作返回
NotImplemented,让 Python 按协议继续处理。 - 注意
__eq__与__hash__的关系:可变对象不应轻易实现可哈希行为。
一个好的自定义类,应该让 obj + other、len(obj) 或 item in obj 看起来像自然的 Python 代码。如果某个重载需要用户阅读大量说明才能理解,普通方法可能更清晰。
小结
运算符和函数重载的核心不是记住一长串特殊方法,而是理解 Python 数据模型:语法和内置函数会通过约定好的方法与对象交互。先明确对象的语义,再实现最小的协议集合,并用不支持的类型和边界情况测试它们,通常能得到更可靠、也更符合 Python 风格的类。