3. 算术操作符重载(上):__add__、__sub__、__mul__、__truediv__、__floordiv__
算术操作符重载,说白了就是让自定义对象也能用 +、-、*、/ 这些符号。Python 默认只认识数字和字符串怎么相加,换成你的对象它就懵了。我们得告诉它:「嘿,这两个对象相加,应该这么算。」
我个人习惯把操作符重载看作「给类安装数学基因」。你想想看,如果设计一个 Vector2D 类,每次相加都要写 v1.add(v2) 多别扭?直接用 v1 + v2 才符合直觉。
+ 表达的含义,就别用方法名硬凑。
3.1 五虎上将:算术操作符家族
Python 为每个算术操作符都预留了「双下划线方法」。我们这章先搞定最常用的五个:
| 操作符 | 方法名 | 数学含义 | 典型场景 |
|---|---|---|---|
+ |
__add__ |
加法 | 向量相加、金额合并 |
- |
__sub__ |
减法 | 坐标偏移、库存扣减 |
* |
__mul__ |
乘法 | 标量缩放、数量×单价 |
/ |
__truediv__ |
真除法 | 平均分配、精确计算 |
// |
__floordiv__ |
整除 | 分页、分组、取整 |
嗯,这里要注意:__truediv__ 和 __floordiv__ 是 Python 3 才严格区分的。Python 2 里 / 默认是整除,踩过坑的老程序员都懂。我刚开始用 Python 3 时,还习惯性地写 from __future__ import division,后来发现完全没必要了。
3.2 实战:构建一个 Money 类
光讲理论没意思,我们直接写一个 Money 类。它代表金额,包含币种和数值。两个 Money 对象相加,币种必须一致,否则报错。
class Money:
def __init__(self, amount: float, currency: str = "CNY"):
self.amount = amount
self.currency = currency
def __add__(self, other):
"""实现 Money + Money 或 Money + 数字"""
if isinstance(other, Money):
if self.currency != other.currency:
raise ValueError(f"币种不一致:{self.currency} vs {other.currency}")
return Money(self.amount + other.amount, self.currency)
elif isinstance(other, (int, float)):
return Money(self.amount + other, self.currency)
return NotImplemented
def __sub__(self, other):
"""实现 Money - Money 或 Money - 数字"""
if isinstance(other, Money):
if self.currency != other.currency:
raise ValueError(f"币种不一致:{self.currency} vs {other.currency}")
return Money(self.amount - other.amount, self.currency)
elif isinstance(other, (int, float)):
return Money(self.amount - other, self.currency)
return NotImplemented
def __mul__(self, other):
"""实现 Money * 数字(标量乘法)"""
if isinstance(other, (int, float)):
return Money(self.amount * other, self.currency)
return NotImplemented
def __truediv__(self, other):
"""实现 Money / 数字(精确除法)"""
if isinstance(other, (int, float)):
return Money(self.amount / other, self.currency)
return NotImplemented
def __floordiv__(self, other):
"""实现 Money // 数字(整除)"""
if isinstance(other, (int, float)):
return Money(self.amount // other, self.currency)
return NotImplemented
def __repr__(self):
return f"Money({self.amount:.2f}, '{self.currency}')"
NotImplemented 而不是抛出 TypeError。这样 Python 会给对方类型一个「反向操作」的机会,更符合鸭子类型哲学。
来测试一下:
salary = Money(15000.0, "CNY")
bonus = Money(3000.0, "CNY")
total = salary + bonus
print(total) # Money(18000.00, 'CNY')
tax_rate = 0.15
after_tax = salary * (1 - tax_rate)
print(after_tax) # Money(12750.00, 'CNY')
per_person = total // 3
print(per_person) # Money(6000.00, 'CNY')
你看,代码读起来就像在操作普通数字一样自然。我在项目中遇到过类似场景——处理多币种订单时,用这种类封装金额,比到处传 (amount, currency) 元组安全得多。
3.3 避坑指南:类型检查与边界情况
操作符重载最容易翻车的地方,就是类型处理。我曾经在线上环境踩过一个坑:
__add__ 里只处理了 Money + Money,没考虑 Money + int。结果某天业务方传了个 salary + 5000,直接报 TypeError,线上告警炸了。从那以后,我所有算术方法都加上了 isinstance 分支。
写算术操作符时,建议遵循以下检查顺序:
- 先判断类型:用
isinstance区分同类运算和混合运算 - 再校验数据:币种、单位、量纲是否一致
- 最后执行计算:返回新对象,不要修改原对象
- 兜底返回
NotImplemented:让 Python 尝试反向操作
还有一个容易忽略的点:不可变性。算术操作应该返回新对象,而不是修改 self。你想想看,a + b 如果偷偷把 a 改了,调试起来得多崩溃?
3.4 知识体系:操作符重载的调用链路
为了让你更直观地理解 Python 内部是怎么处理 a + b 的,我画了张流程图:
这个流程解释了为什么我们返回 NotImplemented 而不是直接抛异常。Python 的「反向操作」机制(比如 __radd__)给了右操作数一个补救机会。虽然这章我们没讲反向方法,但了解这个链路能帮你理解操作符重载的全貌。
3.5 实战对比:不同返回类型的取舍
写算术方法时,返回类型怎么定?我见过三种做法:
| 策略 | 示例 | 优点 | 缺点 |
|---|---|---|---|
| 返回同类对象 | Money + Money → Money |
链式调用方便,类型一致 | 需要处理类型转换 |
| 返回基础类型 | Money + Money → float |
简单直接 | 丢失上下文(币种信息) |
| 返回新类型 | Money * int → Money |
语义清晰 | 类型系统变复杂 |
我个人推荐「返回同类对象」。你想想看,salary + bonus 的结果应该还是个 Money,这样才能继续参与后续运算。如果返回 float,币种信息就丢了,再跟其他 Money 相加时还得重新包装。
3.6 本章小结
算术操作符重载,说白了就是让自定义类「学会」数学运算。我们这章搞定了五个最常用的方法:
__add__和__sub__:同类对象相加/相减,注意类型检查和数据校验__mul__:标量乘法,常用于缩放__truediv__和__floordiv__:精确除法和整除,场景不同要区分- 返回
NotImplemented而不是抛异常,给反向操作留机会 - 保持不可变性,每次运算返回新对象