前置章节
这篇文章介绍过了一些常见的魔术方法,例如__str__
,__getattribute__
等,这些魔术方法的使用能够重新定义特定对象的某些操作行为,例如__str__
影响str()
函数返回值,__getattribute__
影响未实现的子方法的调用,等等。
而运算符重载同样利用了魔术方法,它可以重写对象的四则运算、等价判断等方法,更自由地定义对象行为。
运算符重载
我们先不使用任何基类,从头构建一个简单的自定义多维向量类。它使用一个列表有序地保存向量的所有维度。
from typing import TypeVar, Union
from numbers import Real as RealNumber
import math
NumOrVec = TypeVar('NumOrVec', bound=Union[RealNumber, 'Vector'])
class Vector:
def __init__(self, *args):
self.coords: list = list(args)
def __str__(self) -> str:
return f'({",".join(map(str, self.coords))})'
def __repr__(self) -> str:
return f'{type(self).__name__}({",".join(map(str, self.coords))})'
def __len__(self) -> int:
return len(self.coords)
def __getitem__(self, item: int):
if isinstance(item, slice):
return Vector(*self.coords[item])
elif isinstance(item, int):
if item < len(self.coords):
return self.coords[item]
else:
return 0
raise TypeError(f'Unsupported operand type(s) for []: {type(self).__name__} and {type(item).__name__}')
def __abs__(self) -> float:
return math.sqrt(sum([_a ** 2 for _a in self.coords]))
以上定义的一些魔术方法,已经介绍过大部分,此处不做过多赘述,以下表格供参考:
魔术方法 | 行为 | 内置方法 |
---|---|---|
__init__ |
类的初始化实例 | - |
__str__ |
字符串表达 | str() |
__repr__ |
对象的官方字符串表示,通常用于调试 | repr() |
__len__ |
返回对象的长度(这里就是返回向量的维数) | len() |
__abs__ |
返回对象的绝对值(对于向量来说,就是计算模长) | abs() |
获取索引
当我们使用list[index]
访问列表索引时,其实正在使用__getitem__
魔术方法。它接受一个int
或slice
(切片)类型的参数,访问列表中指定索引位置的元素。
其实,这个参数可以接受任何类型的对象,例如访问字典的索引。
以下类方法实现了访问索引的方式,我们这里接受的是int
或slice
类型参数,切片和索引的访问行为与列表对象一致,但是当访问的索引不存在时,我们选择返回int(0)
而非抛出错误,这是因为某个低维数的向量,可以认为其在更高维度的坐标为0。
def __getitem__(self, item):
if isinstance(item, slice):
return Vector(*self.coords[item])
elif isinstance(item, int):
if item < len(self.coords):
return self.coords[item]
else:
return 0
raise TypeError(f'Unsupported operand type(s) for []: {type(self).__name__} and {type(item).__name__}')
四则运算
运算符 | 行为 | 魔术方法 |
---|---|---|
+ | 加 | __add__ |
- | 减 | __sub__ |
* | 乘 | __mul__ |
/ | 除 | __truediv__ |
// | 整除 | __floordiv__ |
% | 求余 | __mod__ |
+= | 自增 | __iadd__ |
-= | 自减 | __isub__ |
*= | 自乘 | __imul__ |
/= | 自除 | __itruediv__ |
//= | 自整除 | __ifloordiv__ |
%= | 自求余 | __imod__ |
其皆为二元运算,其中普通操作返回的是计算后的结果,而自操作(开头有i的)返回的是这个类的实例本身,也就是self。
例如:+
运算符的重载。+
运算符是一个二元运算符,因此这个函数需要接受一个参数other
,来表示加号后面的那个变量;这个函数返回一个新的向量对象。
向量相加即各维度坐标分别相加,对于维数不同的向量,可以认为低维向量的高维坐标为0
def __add__(self, other: 'Vector') -> 'Vector':
if not isinstance(other, Vector):
raise TypeError(f'Unsupported operand type(s) for +: {type(self).__name__} and {type(other).__name__}')
return Vector(*[_a + _b for _a, _b in zip(self.coords, other.coords)])
而类似于+=
这类运算符,我们称之为自操作,其特点是,对于不可变对象,自操作一般将式左侧的变量重新赋值为操作结果,而对于可变对象,自操作一般直接修改对应的对象实例。
以下例子实现+=
运算符。可以看到,它的实现与上文的__add__
方法基本一致,但是其在函数内直接对自身进行了修改,并随后返回self
,即对象自身。
def __iadd__(self, other):
if not isinstance(other, Vector):
raise TypeError(f'Unsupported operand type(s) for +: {type(self).__name__} and {type(other).__name__}')
self.coords = [_a + _b for _a, _b in zip(self.coords, other.coords)]
return self
等价判断
运算符 | 行为 | 魔术方法 |
---|---|---|
== | 等于 | __eq__ |
!= | 不等于 | __ne__ |
> | 大于 | __gt__ |
>= | 大于等于 | __ge__ |
< | 小于 | __lt__ |
<= | 小于等于 | __le__ |
皆为二元运算符,用于比较此对象与运算符后对象的大小关系或等价关系,因此对应的魔术方法函数接收一个待比较对象,并返回Bool类型的结果。
例如以下方法重载了==
运算符,以比较两个向量是否相等:
def __eq__(self, other: 'Vector') -> bool:
if not isinstance(other, Vector):
raise TypeError(f'Unsupported operand type(s) for ==: {type(self).__name__} and {type(other).__name__}')
return self.coords == other.coords
示例
以下示例是一个完全使用内置方法完成多种运算符重载的类定义,可以参考写法并修改和测试。
# Python 3.5+
# 运算符重载示例
from typing import TypeVar, Union
from numbers import Real as RealNumber
import math
NumOrVec = TypeVar('NumOrVec', bound=Union[RealNumber, 'Vector'])
class Vector:
def __init__(self, *args):
self.coords: list = list(args)
def __str__(self) -> str:
return f'({",".join(map(str, self.coords))})'
def __repr__(self) -> str:
return f'{type(self).__name__}({",".join(map(str, self.coords))})'
def __len__(self) -> int:
return len(self.coords)
def __getitem__(self, item: int):
if isinstance(item, slice):
return Vector(*self.coords[item])
elif isinstance(item, int):
if item < len(self.coords):
return self.coords[item]
else:
return 0
raise TypeError(f'Unsupported operand type(s) for []: {type(self).__name__} and {type(item).__name__}')
def __add__(self, other: 'Vector') -> 'Vector':
if not isinstance(other, Vector):
raise TypeError(f'Unsupported operand type(s) for +: {type(self).__name__} and {type(other).__name__}')
return Vector(*[_a + _b for _a, _b in zip(self.coords, other.coords)])
def __iadd__(self, other):
if not isinstance(other, Vector):
raise TypeError(f'Unsupported operand type(s) for +: {type(self).__name__} and {type(other).__name__}')
self.coords = [_a + _b for _a, _b in zip(self.coords, other.coords)]
return self
def __sub__(self, other):
if not isinstance(other, Vector):
raise TypeError(f'Unsupported operand type(s) for -: {type(self).__name__} and {type(other).__name__}')
return Vector(*[_a - _b for _a, _b in zip(self.coords, other.coords)])
def __isub__(self, other):
if not isinstance(other, Vector):
raise TypeError(f'Unsupported operand type(s) for -: {type(self).__name__} and {type(other).__name__}')
self.coords = [_a - _b for _a, _b in zip(self.coords, other.coords)]
return self
def __mul__(self, other: NumOrVec) -> Union[RealNumber, 'Vector']:
if isinstance(other, RealNumber):
return Vector(*[_a * other for _a in self.coords])
elif isinstance(other, Vector):
return sum([_a * _b for _a, _b in zip(self.coords, other.coords)])
raise TypeError(f'Unsupported operand type(s) for *: {type(self).__name__} and {type(other).__name__}')
def __imul__(self, other: RealNumber):
if isinstance(other, RealNumber):
self.coords = [_a * other for _a in self.coords]
return self
raise TypeError(f'Unsupported operand type(s) for *: {type(self).__name__} and {type(other).__name__}')
def __truediv__(self, other: RealNumber):
if other == 0:
raise ZeroDivisionError('Division by zero')
elif isinstance(other, RealNumber):
return Vector(*[_a / other for _a in self.coords])
raise TypeError(f'Unsupported operand type(s) for /: {type(self).__name__} and {type(other).__name__}')
def __itruediv__(self, other: RealNumber):
if other == 0:
raise ZeroDivisionError('Division by zero')
elif isinstance(other, RealNumber):
self.coords = [_a / other for _a in self.coords]
return self
raise TypeError(f'Unsupported operand type(s) for /: {type(self).__name__} and {type(other).__name__}')
def __abs__(self) -> float:
return math.sqrt(sum([_a ** 2 for _a in self.coords]))
def __matmul__(self, other: 'Vector') -> 'Vector':
if not isinstance(other, Vector):
raise TypeError(f'Unsupported operand type(s) for @: {type(self).__name__} and {type(other).__name__}')
dot_product_ab: RealNumber = self * other
dot_product_bb: RealNumber = self * self
if dot_product_bb == 0:
raise ValueError("Cannot project onto the zero vector")
return self * (dot_product_ab / dot_product_bb)
def __rmatmul__(self, other: 'Vector') -> 'Vector':
return other @ self
def __imatmul__(self, other) -> 'Vector':
if not isinstance(other, Vector):
raise TypeError(f'Unsupported operand type(s) for @: {type(self).__name__} and {type(other).__name__}')
dot_product_ab: RealNumber = self * other
dot_product_bb: RealNumber = self * self
if dot_product_bb == 0:
raise ValueError("Cannot project onto the zero vector")
self.coords = [_a * (dot_product_ab / dot_product_bb) for _a in self.coords]
return self
def __eq__(self, other: 'Vector') -> bool:
if not isinstance(other, Vector):
raise TypeError(f'Unsupported operand type(s) for ==: {type(self).__name__} and {type(other).__name__}')
return self.coords == other.coords
def __ne__(self, other: 'Vector') -> bool:
if not isinstance(other, Vector):
raise TypeError(f'Unsupported operand type(s) for !=: {type(self).__name__} and {type(other).__name__}')
return self.coords != other.coords
def __lt__(self, other: 'Vector') -> bool:
if not isinstance(other, Vector):
raise TypeError(f'Unsupported operand type(s) for <: {type(self).__name__} and {type(other).__name__}')
return abs(self) < abs(other)
def __le__(self, other: 'Vector') -> bool:
if not isinstance(other, Vector):
raise TypeError(f'Unsupported operand type(s) for <=: {type(self).__name__} and {type(other).__name__}')
return abs(self) <= abs(other)
def __gt__(self, other: 'Vector') -> bool:
if not isinstance(other, Vector):
raise TypeError(f'Unsupported operand type(s) for >: {type(self).__name__} and {type(other).__name__}')
return abs(self) > abs(other)
def __ge__(self, other: 'Vector') -> bool:
if not isinstance(other, Vector):
raise TypeError(f'Unsupported operand type(s) for >=: {type(self).__name__} and {type(other).__name__}')
return abs(self) >= abs(other)
def cos(self, other: 'Vector' = None):
if other is None:
other = Vector(1)
if not isinstance(other, Vector):
raise TypeError(f'Unsupported operand type(s) for cos: {type(self).__name__} and {type(other).__name__}')
elif abs(other) == 0 or abs(self) == 0:
raise ValueError("Cannot calculate cosine of zero vector")
return self * other / (abs(self) * abs(other))
def sin(self, other: 'Vector' = None):
if other is None:
other = Vector(1)
if not isinstance(other, Vector):
raise TypeError(f'Unsupported operand type(s) for sin: {type(self).__name__} and {type(other).__name__}')
elif abs(other) == 0 or abs(self) == 0:
raise ValueError("Cannot calculate sine of zero vector")
return math.sqrt(1 - self * self / (abs(self) * abs(other)) ** 2)
def extend(self, other: NumOrVec) -> None:
if isinstance(other, RealNumber):
self.coords.append(other)
elif isinstance(other, Vector):
self.coords.extend(other.coords)
else:
raise TypeError(f'Unsupported operand type(s) for append: {type(self).__name__} and {type(other).__name__}')