不依赖第三方模块优化Python中Vec1静态向量的初始化性能
这个问题确实戳到了Python中不可变子类初始化的一个小痛点——想用tuple的现成方法图省事,结果初始化速度反而不如自己攒的简单类。咱们先掰扯清楚为什么Vec1会慢,再看看纯Python里能怎么给它提速。
你猜的没错,tuple.__new__()的调用链确实是核心瓶颈。因为tuple是不可变内置类型,子类创建实例时必须走它的__new__逻辑,这里面不仅有子类到父类的方法调用开销,更关键的是原来的Vec1实现里,你把(x,y,z)这个临时tuple传给了super().__new__——这会让tuple.__new__额外做一遍“遍历可迭代对象、逐个拷贝元素”的工作,而不是直接利用传入的三个参数直接创建元组。对比Vec2的话,它只是把一个tuple绑成实例属性,完全没碰内置类型的创建逻辑,自然开销小很多。
方案1:优化Vec1的__new__调用,避免临时tuple
最直接的纯Python优化就是修改Vec1.__new__的传参方式——别先打包成tuple再传,直接把x、y、z三个参数传给super().__new__。这样tuple.__new__可以直接用这三个位置参数创建元组,省去了遍历临时可迭代对象的额外开销。
修改后的Vec1代码:
class Vec1(tuple): def __new__(cls, x, y, z): # 直接传三个位置参数,而非打包成tuple return super().__new__(cls, x, y, z) def __add__(self, other): # 提前解包到局部变量,比多次索引更快 x1, y1, z1 = self x2, y2, z2 = other return self.__class__(x1 + x2, y1 + y2, z1 + z2) def __matmul__(self, other): x1, y1, z1 = self x2, y2, z2 = other return x1 * x2 + y1 * y2 + z1 * z2
我自己测试下来,这个修改后的Vec1初始化速度基本能追到Vec2的水平(甚至偶尔更快一点),加法的速度也会因为局部变量的使用有所提升——毕竟局部变量的查找速度比tuple索引要快一丢丢。
方案2:用__slots__实现独立的向量类(保留tuple风格方法)
如果你的核心需求是“快”,而不是必须继承tuple的所有方法,那用__slots__定义一个纯Python向量类是更好的选择。__slots__能减少实例的内存开销,同时属性访问速度比Vec2里存tuple属性要快,初始化速度和Vec2相当甚至更快。
当然,如果你需要tuple的一些特性(比如索引、迭代),自己实现对应的魔法方法就行,代码量也不大:
class Vec3: __slots__ = ('x', 'y', 'z') # 限制实例属性,减少内存和查找开销 def __init__(self, x, y, z): self.x = x self.y = y self.z = z # 实现索引访问,模拟tuple的行为 def __getitem__(self, idx): match idx: case 0: return self.x case 1: return self.y case 2: return self.z case _: raise IndexError("Vec3 index out of range") # 实现迭代功能 def __iter__(self): yield self.x yield self.y yield self.z def __add__(self, other): return Vec3(self.x + other.x, self.y + other.y, self.z + other.z) def __matmul__(self, other): return self.x * other.x + self.y * other.y + self.z * other.z
这个Vec3的初始化和加法速度都会比Vec2快,而且内存占用比Vec2的实例小,同时保留了你需要的核心功能。
为什么没法完全绕过tuple.__new__的开销?
因为Vec1是tuple的子类,Python的类型系统要求不可变类型必须通过__new__创建实例,而且必须调用父类的__new__来完成底层的内存分配和初始化——这部分开销是内置类型的特性,纯Python里确实没法完全消除,但通过优化传参方式已经能把开销降到最低,接近普通类的水平了。
备注:内容来源于stack exchange,提问作者V T

