Python仙术 第9期 运算符重载

前置章节

这篇文章介绍过了一些常见的魔术方法,例如__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__魔术方法。它接受一个intslice(切片)类型的参数,访问列表中指定索引位置的元素。

其实,这个参数可以接受任何类型的对象,例如访问字典的索引。

以下类方法实现了访问索引的方式,我们这里接受的是intslice类型参数,切片和索引的访问行为与列表对象一致,但是当访问的索引不存在时,我们选择返回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__}')
23 Likes

厉害了佬

砂糖又更python了,过来参观一下

下班就学编程吗?这也太勤奋了叭~

5 Likes

好东西啊,感觉有想用的冲动了 :tieba_024:

能看懂,就是不会写

不错噢

憋了俩星期,一直在忙,周末了才有时间写一点

2 Likes

忙着带薪摸鱼么?成为摸鱼界一颗冉冉升起的新星

5 Likes

仙术居然还没完

砂糖终于摸完鱼了

1 Like

感觉这些魔法方法用的真不多,还不如__dict__。很多我都感觉和学习元编程一样学院派

用途太少了,
魔法函数,我基本只用__setItem__和__getItem__,
偶尔写个__call__

From #dev to 开发调优