欢迎来到 Python 学习计划的第 45 天!🎉
昨天我们学习了 __call__ 魔法方法,让对象可以像函数一样被调用。今天,我们将进一步释放对象的潜力,学习 运算符重载(Operator Overloading)。
通过运算符重载,你可以让自定义对象支持 +、-、*、==、[] 等原生运算符。这将让你的代码更简洁、更直观、更"Pythonic"!
一、什么是运算符重载?
1. 定义
运算符重载 是指为自定义类赋予内置运算符(如 +, -, *, == 等)特定含义的过程。本质上,它是通过实现特定的 魔法方法(Magic Methods) 来实现的。
2. 核心原理
当你使用运算符时,Python 解释器会自动调用对应的魔法方法。
运算符 | 魔法方法 | 说明 |
|---|
+
| __add__(self, other)
| 加法运算 |
-
| __sub__(self, other)
| 减法运算 |
*
| __mul__(self, other)
| 乘法运算 |
==
| __eq__(self, other)
| 相等比较 |
<
| __lt__(self, other)
| 小于比较 |
[]
| __getitem__(self, key)
| 索引访问 |
len()
| __len__(self)
| 获取长度 |
- 直观性:
v1 + v2 比 v1.add(v2) 更易读。 - 一致性:让自定义对象 behave 像内置类型(如 list, int)。
- 简洁性:减少冗余的方法调用代码。
二、算术运算符重载
1. 加法 __add__
实现两个对象相加的逻辑。
class Vector: def __init__(self, x, y): self.x = x self.y = y def __add__(self, other): # 返回一个新的 Vector 对象 return Vector(self.x + other.x, self.y + other.y) def __repr__(self): return f"Vector({self.x}, {self.y})"v1 = Vector(2, 3)v2 = Vector(4, 5)v3 = v1 + v2 # 自动调用 v1.__add__(v2)print(v3) # Vector(6, 8)
2. 乘法 __mul__
可以实现对象与标量相乘,或对象与对象相乘。
class Vector: # ... __init__, __add__ ... def __mul__(self, scalar): # 向量乘以标量 return Vector(self.x * scalar, self.y * scalar)v = Vector(2, 3)print(v * 3) # Vector(6, 9)
3. 反向运算 __radd__
如果左操作数不支持该运算,Python 会尝试调用右操作数的 __radd__。
class Vector: # ... def __radd__(self, other): # 处理 other + self 的情况 return self.__add__(other)# 即使左边不是 Vector,也能处理result = (10, 20) + v # 如果实现了相应逻辑
💡 关键点:算术运算通常应该返回新对象,而不是修改原对象(除非明确设计为原地修改 __iadd__)。三、比较运算符重载
默认情况下,对象比较的是内存地址。通过重载比较方法,可以实现值比较。
1. 相等 __eq__
class Money: def __init__(self, amount, currency="CNY"): self.amount = amount self.currency = currency def __eq__(self, other): # 比较金额和货币是否相同 return self.amount == other.amount and self.currency == other.currencym1 = Money(100, "CNY")m2 = Money(100, "CNY")m3 = Money(100, "USD")print(m1 == m2) # True (值相等)print(m1 == m3) # False (货币不同)
2. 大小比较 __lt__, __le__, __gt__, __ge__
只需实现 __lt__ 和 __eq__,Python 的 @functools.total_ordering 可以自动生成其他比较方法。
from functools import total_ordering@total_orderingclass Student: def __init__(self, name, score): self.name = name self.score = score def __eq__(self, other): return self.score == other.score def __lt__(self, other): return self.score < other.scores1 = Student("Alice", 90)s2 = Student("Bob", 85)print(s1 > s2) # True (自动推导)print(s1 >= s2) # True (自动推导)
四、容器/序列运算符重载
让自定义对象像列表或字典一样支持索引、长度检查等操作。
1. 长度 __len__
支持内置函数 len()。
class Collection: def __init__(self, items): self.items = items def __len__(self): return len(self.items)c = Collection([1, 2, 3, 4, 5])print(len(c)) # 5
2. 索引访问 __getitem__ 和 __setitem__
支持 obj[key] 读写操作。
class MyList: def __init__(self, data): self.data = data def __getitem__(self, index): return self.data[index] def __setitem__(self, index, value): self.data[index] = value def __repr__(self): return f"MyList({self.data})"ml = MyList([10, 20, 30])print(ml[1]) # 20 (调用 __getitem__)ml[1] = 200 # 调用 __setitem__print(ml) # MyList([10, 200, 30])
3. 迭代支持
实现了 __getitem__ 且索引从 0 开始连续的对象,通常自动支持迭代(for x in obj)。
for item in ml: print(item) # 10, 200, 30
五、实战案例:自定义金额类
结合算术和比较运算符,实现一个安全的金额计算类。
class Money: def __init__(self, amount, currency="CNY"): self.amount = amount self.currency = currency def __add__(self, other): if self.currency != other.currency: raise ValueError("货币单位不同,无法相加") return Money(self.amount + other.amount, self.currency) def __sub__(self, other): if self.currency != other.currency: raise ValueError("货币单位不同,无法相减") return Money(self.amount - other.amount, self.currency) def __eq__(self, other): return self.amount == other.amount and self.currency == other.currency def __repr__(self): return f"Money({self.amount}, {self.currency})"# 使用wallet1 = Money(100, "CNY")wallet2 = Money(50, "CNY")total = wallet1 + wallet2print(total) # Money(150, CNY)# wallet3 = Money(10, "USD")# total = wallet1 + wallet3 # ❌ 抛出 ValueError
六、常见误区与注意事项
1. 语义一致性
运算符的行为应符合直觉。不要让 + 执行减法,不要让 == 修改状态。
# ❌ 不推荐def __add__(self, other): self.amount -= other.amount # 加法变成了减法且修改了自身 return self
2. 类型检查
在运算前检查 other 的类型,避免 AttributeError。
def __add__(self, other): if not isinstance(other, Money): return NotImplemented # 让 Python 尝试 other.__radd__ # ...
3. 可变 vs 不可变
- 不可变对象(如 tuple, str):运算应返回新对象。
- 可变对象(如 list):可以提供
__iadd__ (+=) 原地修改,但 __add__ (+) 仍应返回新对象。
4. 不要过度重载
只重载有意义的运算符。不要让对象支持所有运算符,这会增加维护成本。
七、实战练习
练习 1:实现复数类
创建一个 Complex 类,支持复数的加法和乘法。
- 属性:
real (实部), imag (虚部) - 方法:
__add__, __mul__, __repr__
class Complex: def __init__(self, real, imag): self.real = real self.imag = imag def __add__(self, other): return Complex(self.real + other.real, self.imag + other.imag) def __mul__(self, other): real = self.real * other.real - self.imag * other.imag imag = self.real * other.imag + self.imag * other.real return Complex(real, imag) def __repr__(self): return f"Complex({self.real}, {self.imag})"c1 = Complex(1, 2)c2 = Complex(3, 4)print(c1 + c2) # Complex(4, 6)print(c1 * c2) # Complex(-5, 10)
练习 2:实现可索引的栈
创建一个 Stack 类,支持 push, pop, 以及通过索引访问元素 stack[i] 和获取长度 len(stack)。
class Stack: def __init__(self): self.items = [] def push(self, item): self.items.append(item) def pop(self): return self.items.pop() def __len__(self): return len(self.items) def __getitem__(self, index): return self.items[index] def __repr__(self): return f"Stack({self.items})"s = Stack()s.push(1)s.push(2)s.push(3)print(len(s)) # 3print(s[1]) # 2 (栈底第二个元素)print(s.pop()) # 3print(s) # Stack([1, 2])
八、总结
知识点 | 说明 |
|---|
运算符重载 | 通过魔法方法让对象支持内置运算符 |
算术运算 | __add__, __sub__, __mul__ 等
|
比较运算 | __eq__, __lt__ 等,支持 @total_ordering
|
容器操作 | __len__, __getitem__, __setitem__
|
最佳实践 | 保持语义一致,返回新对象,检查类型 |
注意事项 | 不要滥用,避免过度复杂化 |
📌 明日预告:property 装饰器
明天我们将进入 高级 OOP 特性第五天!
- 主题:
@property 装饰器 - 属性访问控制 - 核心问题:
- 如何将方法变成属性调用?
obj.attr vs obj.method() - Getter, Setter, Deleter 如何实现?
- 如何保护私有属性不被随意修改?
- 计算属性(Computed Property)有什么用?
- 它与 Java 的 Getter/Setter 有什么区别?
💡 提前思考:如果有一个 Circle 类,area 应该是一个属性还是一个方法?如何通过 @property 让它像属性一样访问?
掌握运算符重载,让你的对象更自然、更强大!继续加油!🚀