05-模拟数值类型
约 2933 字大约 10 分钟
2026-09-28
纸牌案例说明了自定义对象如何表现得像序列。本节换一个方向:定义一个二维向量 Vector,让它表现得像一个可以计算的数值对象。
目标使用方式是:
v1 = Vector(2, 4)
v2 = Vector(2, 1)
print(v1 + v2) # Vector(4, 5)
print(abs(v1)) # 4.47213595499958
print(v1 * 3) # Vector(6, 12)这些表达式分别依赖不同的特殊方法:
repr(v) -> __repr__
abs(v) -> __abs__
bool(v) -> __bool__
v1 + v2 -> __add__
v * scalar -> __mul__1. 二维向量是什么
二维向量可以用两个坐标表示:x 和 y。
例如,向量 Vector(2, 4) 表示从原点出发,沿 x 轴移动 2 个单位、沿 y 轴移动 4 个单位的方向和长度。

图中:
Vector(2, 4)和Vector(2, 1)是两个输入向量;- 它们相加后得到
Vector(4, 5); - 向量加法本质上是对应坐标分别相加:
(x1, y1) + (x2, y2) = (x1 + x2, y1 + y2)。
本节不实现完整的线性代数库,只用这个小模型说明 Python 的数值协议。
2. 完整可运行示例
下面的代码只依赖标准库。它实现了:
repr(vector):返回明确的对象表示;abs(vector):计算向量的长度;bool(vector):判断是否为零向量;vector1 + vector2:向量加法;vector * scalar:标量乘法。
import math
from numbers import Real
class Vector:
def __init__(self, x=0, y=0):
self.x = x
self.y = y
def __repr__(self):
return f"Vector({self.x!r}, {self.y!r})"
def __abs__(self):
return math.hypot(self.x, self.y)
def __bool__(self):
return bool(abs(self))
def __add__(self, other):
if not isinstance(other, Vector):
return NotImplemented
return Vector(self.x + other.x, self.y + other.y)
def __mul__(self, scalar):
if not isinstance(scalar, Real):
return NotImplemented
return Vector(self.x * scalar, self.y * scalar)
if __name__ == "__main__":
v1 = Vector(2, 4)
v2 = Vector(2, 1)
print(v1 + v2)
vector = Vector(3, 4)
print(abs(vector))
print(vector * 3)
print(abs(vector * 3))
print(bool(Vector(0, 0)))
print(bool(vector))运行结果为:
Vector(4, 5)
5.0
Vector(9, 12)
15.0
False
True下面逐个解释这些方法为什么这样实现。
3. __repr__:让对象有明确的表示
向量对象如果没有自定义表示,交互式控制台可能显示类似下面的内容:
<__main__.Vector object at 0x000001...>这能表明对象属于 Vector,但看不出坐标。定义 __repr__ 后:
def __repr__(self):
return f"Vector({self.x!r}, {self.y!r})"就可以得到:
>>> Vector(2, 4)
Vector(2, 4)这里使用 !r,表示在 f-string 中调用每个坐标的 repr()。对于数字,repr(2) 和 str(2) 看起来相同;但如果坐标是字符串,区别就很明显:
class Demo:
def __repr__(self):
value = "x"
return f"repr={value!r}, str={value}"
print(Demo())repr='x', str=x使用 !r 有助于保持对象表示的明确性:字符串的引号不会被误认为表示的一部分缺失,调试时也更容易区分不同类型的值。
本节只需要 __repr__ 就能让控制台和容器显示有用信息。__repr__ 与 __str__ 的分工会在下一小节专门展开。
4. __abs__:接入 abs()
二维向量的长度按照勾股定理计算:
长度 = sqrt(x² + y²)Python 标准库的 math.hypot(x, y) 已经提供了这个计算,并且在数值范围较大时比手动写 math.sqrt(x * x + y * y) 更稳健。
因此实现很直接:
def __abs__(self):
return math.hypot(self.x, self.y)当代码执行:
abs(Vector(3, 4))调用链是:
abs(vector)
-> Vector.__abs__(vector)
-> math.hypot(vector.x, vector.y)
-> 5.0__abs__ 应返回一个表示“绝对值”或“大小”的数值,而不是向量对象本身。这样才能继续参与比较、格式化和真假判断等操作。
5. __bool__:零向量为假
在这个模型中,零向量 Vector(0, 0) 没有长度,可以把它视为假;其他向量视为真:
def __bool__(self):
return bool(abs(self))调用过程是:
bool(vector)
-> Vector.__bool__(vector)
-> abs(vector)
-> bool(向量长度)长度为 0.0 时,bool(0.0) 是 False;其他长度会得到 True。
__bool__ 必须返回真正的布尔值。下面的实现不符合协议:
class BrokenVector:
def __bool__(self):
return abs(self) # 错误:返回了浮点数调用 bool(BrokenVector()) 时会抛出 TypeError。如果一个对象的真假语义本来就是“数值是否为零”,应像示例一样显式调用 bool()。
也可以直接按坐标判断:
def __bool__(self):
return bool(self.x or self.y)两种写法对普通实数坐标的结果一致。复用 abs() 的版本更直接表达“长度是否为零”,但会多做一次平方根计算。对于高频调用的类型,可以根据坐标类型和精度要求选择更便宜的平方和判断:
def __bool__(self):
return bool(self.x or self.y)这属于实现取舍,不改变 bool(vector) 的公开语义。
6. __add__:让 + 表示向量加法
向量加法的代码是:
def __add__(self, other):
if not isinstance(other, Vector):
return NotImplemented
return Vector(self.x + other.x, self.y + other.y)执行:
v1 = Vector(2, 4)
v2 = Vector(2, 1)
result = v1 + v2调用链是:
v1 + v2
-> Vector.__add__(v1, v2)
-> Vector(v1.x + v2.x, v1.y + v2.y)
-> Vector(4, 5)运算应返回新对象
__add__ 返回一个新的 Vector,不会修改参与运算的两个对象:
v1 = Vector(2, 4)
v2 = Vector(2, 1)
result = v1 + v2
print(v1)
print(v2)
print(result)Vector(2, 4)
Vector(2, 1)
Vector(4, 5)这种行为与 Python 中整数、浮点数和元组等不可变值的直觉一致。表达式计算出一个新值,原对象仍可安全地被其他代码使用。
如果希望提供原地加法,例如 v1 += v2 直接修改对象,可以另外实现 __iadd__。没有实现时,Python 通常会先尝试普通加法,再把结果重新绑定给左侧变量;不要因为看到 += 就假设对象一定发生了原地修改。
为什么不能直接访问 other.x
如果不检查类型,下面的写法虽然短,但对错误类型的行为不够清晰:
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)当 other 是整数时,调用会在读取 other.x 的位置抛出 AttributeError。这不是“加法不支持整数”的明确表达。
当前实现先检查 isinstance(other, Vector),再决定是否支持该操作。更复杂的数值类型层次可以使用抽象基类或自定义协议,但原则相同:对不支持的操作给出明确、可组合的结果。
7. NotImplemented 不等于 NotImplementedError
当一个运算方法无法处理右侧操作数时,应返回特殊单例 NotImplemented:
def __add__(self, other):
if not isinstance(other, Vector):
return NotImplemented
return Vector(self.x + other.x, self.y + other.y)NotImplemented 的意思是:
当前这个操作数顺序的实现不支持该类型,请让解释器尝试其他候选协议。
它不是异常,也不是布尔值;更不是 NotImplementedError。
NotImplementedError 是一个异常类,通常用于抽象方法或明确表示“子类必须实现”的代码:
def required_operation(self):
raise NotImplementedError如果在 __add__ 中直接抛出 NotImplementedError,解释器不会按正常的二元运算流程尝试右侧的反向方法。
当左右两侧都返回 NotImplemented,解释器最终会抛出适合调用者的 TypeError:
vector = Vector(1, 2)
try:
vector + (3, 4)
except TypeError as error:
print(type(error).__name__)TypeError8. __mul__:向量与标量相乘
向量乘以标量时,两个坐标都乘以同一个数:
(x, y) * scalar = (x * scalar, y * scalar)实现如下:
def __mul__(self, scalar):
if not isinstance(scalar, Real):
return NotImplemented
return Vector(self.x * scalar, self.y * scalar)numbers.Real 是标准库提供的实数抽象基类,整数和浮点数都属于它。使用它比只检查 int 更宽松,也比接受任意对象更明确。
执行:
vector = Vector(3, 4)
print(vector * 3)
print(abs(vector * 3))结果为:
Vector(9, 12)
15.0不要把向量乘法和点积混为一谈
本例中的 * 表示标量乘法,不表示两个向量的点积:
Vector(1, 2) * Vector(3, 4)会因为 Vector 不是 Real 而最终抛出 TypeError。
如果领域模型确实需要点积,应明确选择接口,例如:
def dot(self, other):
...或者经过充分定义后让 * 表示点积。运算符重载的目标是让已有语法表达自然语义,不是把所有可能的数学操作都塞进同一个符号。
9. 为什么 3 * vector 暂时不支持
当前只实现了左操作数为向量的情况:
vector * 3如果交换顺序:
3 * vectorPython 会先尝试整数的乘法实现。整数不知道如何与 Vector 相乘后,才会考虑向量一侧的反向乘法方法 __rmul__。当前类没有定义它,因此最终得到 TypeError。
如果标量乘法满足交换关系,可以用一行方法补上反向操作:
def __rmul__(self, scalar):
return self * scalar加入后:
print(3 * Vector(3, 4))Vector(9, 12)这里 __rmul__ 复用已经写好的 __mul__,避免复制类型检查和坐标计算。只有在数学语义确实允许交换操作数时,才应该这样实现;减法和除法通常需要不同的反向逻辑。
10. 坐标类型与接口边界
当前构造函数没有强制检查 x、y 的类型:
Vector("left", "top")对象可以被创建,但调用 abs() 或参与数值运算时才可能失败。这种“延迟发现错误”的设计适合教学和简单值对象,但不一定适合所有生产代码。
如果类型需要尽早拒绝无效输入,可以在 __init__ 中验证:
from numbers import Real
class ValidatedVector:
def __init__(self, x=0, y=0):
if not isinstance(x, Real) or not isinstance(y, Real):
raise TypeError("x and y must be real numbers")
self.x = x
self.y = y是否验证、接受哪些数值类型,应由对象的公开契约决定。不要为了让示例更“严格”而加入与核心协议无关的限制。
11. 完整运行示例:包含反向乘法
如果希望向量同时支持 vector * scalar 和 scalar * vector,可以使用下面的扩展版本:
import math
from numbers import Real
class Vector:
def __init__(self, x=0, y=0):
self.x = x
self.y = y
def __repr__(self):
return f"Vector({self.x!r}, {self.y!r})"
def __abs__(self):
return math.hypot(self.x, self.y)
def __bool__(self):
return bool(abs(self))
def __add__(self, other):
if not isinstance(other, Vector):
return NotImplemented
return Vector(self.x + other.x, self.y + other.y)
def __mul__(self, scalar):
if not isinstance(scalar, Real):
return NotImplemented
return Vector(self.x * scalar, self.y * scalar)
def __rmul__(self, scalar):
return self * scalar
if __name__ == "__main__":
v1 = Vector(2, 4)
v2 = Vector(2, 1)
print(v1 + v2)
print(abs(Vector(3, 4)))
print(Vector(3, 4) * 3)
print(3 * Vector(3, 4))
print(bool(Vector(0, 0)))输出为:
Vector(4, 5)
5.0
Vector(9, 12)
Vector(9, 12)
False12. 本节小结
Vector 用少量特殊方法接入了 Python 的数值和对象表示协议:
__repr__让控制台和调试输出能表达坐标;__abs__让abs(vector)返回向量长度;__bool__定义零向量与非零向量的真假;__add__让加法表达对应坐标相加,并返回新对象;__mul__让向量支持右侧标量乘法;__rmul__可以补充左侧标量乘法;- 对不支持的操作返回
NotImplemented,让解释器完成后续尝试; NotImplemented与NotImplementedError的用途完全不同;- 运算符重载应表达稳定、自然的领域语义,而不是追求方法数量。
下一节将单独讨论 __repr__ 与 __str__:为什么控制台、调试器、日志和 print() 可能需要不同的字符串表示。
拓展阅读与查阅
以下资料用于进一步研究或日后查阅。理解本节内容不依赖这些链接。
