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 知识结构图

下面这张图把二维向量类的核心逻辑串起来了:

二维向量类核心结构 Vector2D 类 属性:self.x 属性:self.y __add__ 加法 __sub__ 减法 __mul__ 数乘 __rmul__ 反向数乘 norm() 模长计算 核心原则 分量独立运算,类型检查不可少

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__,否则标量在左边时会报错

你想想看,有了这个向量类,后面做更复杂的操作——比如点积、叉积、向量旋转——就只需要在这个基础上加方法就行了。面向对象的魅力就在这儿:把数据和操作封装在一起,用起来顺手,改起来也安全。