3. 自定义向量类:实现一个二维向量类,支持加法、减法、数乘操作
向量,说白了就是既有大小又有方向的量。二维向量就是平面上的一个箭头,从原点指向某个点 (x, y)。
我刚开始学 Python 面向对象时,第一个自己写的类就是向量类。为什么?因为向量运算太常用了——游戏开发里的角色移动、图形学里的坐标变换、甚至机器学习里的特征向量,底层全是这玩意儿。
今天我们就手撸一个二维向量类,把加法、减法、数乘这三个核心操作实现出来。
3.1 向量类的骨架
先搭个最基本的架子。一个二维向量,只需要存两个分量:x 和 y。
class Vector2D:
def __init__(self, x: float, y: float):
self.x = x
self.y = y
def __repr__(self):
return f"Vector2D({self.x}, {self.y})"
嗯,这里要注意:__repr__ 不是必须的,但加上它,你在控制台打印向量时能看到直观的内容,而不是 <__main__.Vector2D object at 0x...> 这种鬼东西。
3.2 加法:让向量「合二为一」
向量加法的几何意义是「平行四边形法则」——两个向量首尾相接,结果向量是从起点指向终点的对角线。
在代码里,加法就是对应分量相加:
def __add__(self, other: 'Vector2D') -> 'Vector2D':
if not isinstance(other, Vector2D):
raise TypeError(f"不支持 Vector2D 与 {type(other).__name__} 相加")
return Vector2D(self.x + other.x, self.y + other.y)
小技巧: 用
__add__ 这个魔术方法,你就可以直接用 v1 + v2 的写法了。我个人习惯在方法签名里写上类型注解,这样 IDE 能给出智能提示,团队协作时也少踩坑。
3.3 减法:向量之间的「距离」
减法其实就是加上一个反向向量。几何上,v1 - v2 的结果向量是从 v2 的终点指向 v1 的终点。
def __sub__(self, other: 'Vector2D') -> 'Vector2D':
if not isinstance(other, Vector2D):
raise TypeError(f"不支持 Vector2D 与 {type(other).__name__} 相减")
return Vector2D(self.x - other.x, self.y - other.y)
为什么减法这么重要?我在项目中做过一个寻路系统,判断两个物体是否靠近,其实就是算它们位置向量的差,然后取模长。减法是一切距离计算的基础。
3.4 数乘:把向量「拉长」或「缩短」
数乘就是用一个标量去乘向量的每个分量。结果向量的方向不变(如果标量是正数),长度变为原来的 k 倍。
def __mul__(self, scalar: float) -> 'Vector2D':
if not isinstance(scalar, (int, float)):
raise TypeError(f"数乘要求标量是数字,不能是 {type(scalar).__name__}")
return Vector2D(self.x * scalar, self.y * scalar)
def __rmul__(self, scalar: float) -> 'Vector2D':
return self.__mul__(scalar)
踩坑提醒: 我曾经忘了实现
__rmul__,结果 3 * v 这种写法直接报错。因为 Python 会先尝试 int.__mul__(3, v),发现不支持,才回头调 v.__rmul__(3)。没有 __rmul__,反向操作就挂了。
3.5 完整代码与测试
把上面三块拼起来,再加上一个计算模长的方法,就是一个能用的二维向量类了:
import math
class Vector2D:
def __init__(self, x: float, y: float):
self.x = x
self.y = y
def __repr__(self):
return f"Vector2D({self.x}, {self.y})"
def __add__(self, other: 'Vector2D') -> 'Vector2D':
if not isinstance(other, Vector2D):
raise TypeError(f"不支持 Vector2D 与 {type(other).__name__} 相加")
return Vector2D(self.x + other.x, self.y + other.y)
def __sub__(self, other: 'Vector2D') -> 'Vector2D':
if not isinstance(other, Vector2D):
raise TypeError(f"不支持 Vector2D 与 {type(other).__name__} 相减")
return Vector2D(self.x - other.x, self.y - other.y)
def __mul__(self, scalar: float) -> 'Vector2D':
if not isinstance(scalar, (int, float)):
raise TypeError(f"数乘要求标量是数字,不能是 {type(scalar).__name__}")
return Vector2D(self.x * scalar, self.y * scalar)
def __rmul__(self, scalar: float) -> 'Vector2D':
return self.__mul__(scalar)
def norm(self) -> float:
"""返回向量的模长(长度)"""
return math.sqrt(self.x ** 2 + self.y ** 2)
# 测试一下
v1 = Vector2D(3, 4)
v2 = Vector2D(1, 2)
print(v1 + v2) # Vector2D(4, 6)
print(v1 - v2) # Vector2D(2, 2)
print(v1 * 2) # Vector2D(6, 8)
print(3 * v1) # Vector2D(9, 12)
print(v1.norm()) # 5.0
3.6 知识结构图
下面这张图把二维向量类的核心逻辑串起来了:
3.7 操作对比一览
| 操作 | 魔术方法 | 数学含义 | 代码示例 |
|---|---|---|---|
| 加法 | __add__ |
(x₁+x₂, y₁+y₂) | v1 + v2 |
| 减法 | __sub__ |
(x₁−x₂, y₁−y₂) | v1 - v2 |
| 数乘 | __mul__ |
(k·x, k·y) | v * 3 |
| 反向数乘 | __rmul__ |
(k·x, k·y) | 3 * v |
关键点总结:
- 向量运算本质是分量独立运算,x 和 y 各算各的
- 魔术方法让自定义类支持原生运算符,代码更自然
- 类型检查是防御性编程的好习惯——我在生产代码里吃过亏,传错类型导致静默 bug 查了一下午
- 别忘了
__rmul__,否则标量在左边时会报错
你想想看,有了这个向量类,后面做更复杂的操作——比如点积、叉积、向量旋转——就只需要在这个基础上加方法就行了。面向对象的魅力就在这儿:把数据和操作封装在一起,用起来顺手,改起来也安全。