python魔术方法运算篇

发布时间 2023-07-16 01:43:52作者: 天才九少

emulate numeric type

python中这个类型系统叫做duck type,简单的说就是它不去检查具体的这个对象是什么类型,而是检查这个对象有没有相应的功能,而python中有大量的魔术方法就是给某一个对象加上相应的功能,本篇章要讲的就是emulating numeric types ,也就是让你的类型实现一些数的功能。

5,运算篇

 

这里举一个二维向量的例子,对于两个二维向量来说它们是可以相加的,但是如果我们尝试用加号把它俩直接加起来,肯定是会报错的,因为python并不知道你自定义的这个数据结构应该怎么做加法

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f"Vector({self.x}, {self.y})"

    # def __add__(self, other):
    #     return Vector(self.x + other.x, self.y + other.y)


v1 = Vector(0, 1)
v2 = Vector(2, 3)
print(v1 + v2)
TypeError: unsupported operand type(s) for +: 'Vector' and 'Vector'

这个时候呢,我们可以定义这个__add__函数,通过__add__函数定义当这个数据结构遇到加号的时候,它应该怎么办,我们看下面__add__(self, other) self是加号前的数据结构,other是加号之后的数据结构,我们这个呢就返回一个新的向量,它的x是两个向量x的和 然后y是两个向量y的和,定义这个魔术方法之后我们就可以把两个Vector加起来,拿到一个新的Vector

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f"Vector({self.x}, {self.y})"

    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)


v1 = Vector(0, 1)
v2 = Vector(2, 3)
print(v1 + v2)
# Vector(2, 4)

 

在python中一共有10来个跟数有关的符号或者函数,是可以被定义魔术方法的。

注意__and__和__or__并不是重写python的关键字and和or,它们重写的是这个符号& | 。

对于这些符号重载我们有几点要注意一下,第一,self和other并不一定要是同一个类型,比如说这里对于一个二维向量来说,我们完全可以让__mul__是一个数乘,也就是当other比如说是一个整型的时候,我们就返回一个数乘的结果,这里我们可以通过对other类型的判断,来实现在这个函数里面既定义数乘也定义点乘。第二,很多时候我们用魔术方法,只是借用这个符号,并不一定要符合它符号原来的定义。

这里我们定义了数乘,也就是说当我们做v1 * 2的时候,我们是有据可循的,它本质上相当于调用了一个v1.__mul__(2),但是如果我们做

2 * v1 ,我们运行一下就会发现它报错了,这里的原因是对于python来说,v1 * 2 跟 2 * v1 是两个完全不同的事情,2 * v1 调用的是2.__mul__(v1),而2作为一个内置的数据结构integer,它是完全不知道怎么乘一个Vector的

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f"Vector({self.x}, {self.y})"

    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)

    def __sub__(self, other):
        # v1 - v2
        pass

    def __mul__(self, other):
        # v1 * v2
        if isinstance(other, int):
            return Vector(self.x * other, self.y * other)

    def __matmul__(self, other):
        # v1 @ v2
        pass

    def __truediv__(self, other):
        # v1 / v2
        pass

    def __floordiv__(self, other):
        # v1 // v2
        pass

    def __mod__(self, other):
        # v1 % v2
        pass

    def __divmod__(self, other):
        # 既拿到商也拿到余数
        # divmod(v1, v2)
        pass

    def __pow__(self, power, modulo=None):
        # pow(v1, v2, mod=None) => (v1 ** v2) % mod
        # v1 ** v2
        pass

    def __lshift__(self, other):
        # v1 << v2
        pass

    def __rshift__(self, other):
        # v1 >> v2
        pass

    def __and__(self, other):
        # v1 & v2
        pass

    def __xor__(self, other):
        # v1 ^ v2
        pass

    def __or__(self, other):
        # v1 | v2
        pass


v1 = Vector(0, 1)
print(v1 * 2)   # v1.__mul__(2)
print(2 * v1)   # 2.__mul__(v1)
Vector(0, 2)
TypeError: unsupported operand type(s) for *: 'int' and 'Vector'

这个时候我们的解决办法是定义一个叫做__rmul__的函数,如果操作符左侧的这个数据结构,没有定义这个操作应该怎么完成的话,它就会尝试去找操作符右侧的这个数据的 r 版本,也就是当我们做 other * self,但是other 不知道这个操作应该怎么完成的时候,我们就会调用self的__rmul__函数,然后把other作为argument传进来,这时我们就可正常进行运算了

...
    def __rmul__(self, other):
        # other * self
        if isinstance(other, int):
            return Vector(self.x * other, self.y * other)
...

v1 = Vector(0, 1)
print(v1 * 2)   # v1.__mul__(2)
print(2 * v1)   # 2.__mul__(v1)
# Vector(0, 2)
# Vector(0, 2)

这里我们提到的所有和数的计算有关的魔术方法,都有它们的r版本,也就是把魔术方法名字前面加一个r,就代表反过来运算的时候,应该调用什么东西。

那除了r版本之外还有一个 i 版本,这个叫做 in place,也就是修改self,一般来说对于这个i 版本,我们都不是返回一个新的数据结构,而是修改这个self数据结构,然后把self返回回去,那它对应的符号就是原来的操作符号后面加一个等号,也就是 v1 += v2,它就会调用 v1.__iadd__(v2),我们这里让v1 +=  Vector(2, 3),再打印v1,v1就是(2,4)了,在我们提到的这些魔术方法里,所有使用符号触发的,都有这个i 版本,也就是in place版本,而它们in place 版本对应的符号,就是原来那个符号加一个等号,但是比如这种 __divmod__它当然就没有in place版本了

...
    def __iadd__(self, other):
        # v1 += v2
        self.x += other.x
        self.y += other.y
        return self
...
v1 = Vector(0, 1)
v1 += Vector(2, 3)
print(v1)
# Vector(2, 4)

 

下面是两个一元运算符,__neg____pos__也就是negative跟positive,它们对应的就是在这个数据结构之前加一个减号或者加一个加号,

我们这里定义了个v1 ,然后我们打印了 v1 -v1跟+v1,出来的结果是(1,2) (-1,-2) (1,2),注意这里我们特意在__pos__这个函数里面打印了一些东西,这样我们就知道__pos__这个函数,确实是在前面加一个正号的时候被调用了

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f"Vector({self.x}, {self.y})"

    def __neg__(self):
        return Vector(-self.x, -self.y)

    def __pos__(self):
        print("Pos!")
        return Vector(self.x, self.y)


v1 = Vector(1, 2)
print(v1)
print(-v1)
print(+v1)

 

再来两个一元操作,一个是__abs__也就是absolute,另外一个呢叫做__invert__取反,我们在使abs这个这个内置函数的时候会调用__abs__这个魔术方法,而__invert__对应的是这个波浪线,它一般用作这个位运算,当然这里我们就给它随便定义了一下,我们认为invert之后这个x变y,y变x。

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f"Vector({self.x}, {self.y})"

    def __abs__(self):
        return Vector(abs(self.x), abs(self.y))

    def __invert__(self):
        return Vector(self.y, self.x)


v1 = Vector(-1, 1)
print(v1)
print(abs(v1))
print(~v1)
Vector(-1, 1)
Vector(1, 1)
Vector(1, -1)

 

下面3个conversion魔术方法,__complex__  __int____float__,当我们使用python的内置函数complex、int、float的时候,调用的就是它们三个魔术方法。在一般使用的时候,如果你这个数据结构并不能合理的被传换成一个integer,你就不要定义这个魔术方法,这三个魔术方法规定你返回的值,必须是它们对应的数据结构

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f"Vector({self.x}, {self.y})"

    def __complex__(self):
        return complex(self.x)

    def __int__(self):
        return int(self.x)

    def __float__(self):
        return float(self.x)


v1 = Vector(1.5, 2.5)
print(int(v1))
print(float(v1))
print(complex(v1))
1
1.5
(1.5+0j)

假如你想要重新定义这个Vector取int的含义,我认为int这个vector是对这个vector里面的x跟y都取int,然后返回这个vector,想法虽然是好的,但是Python会报错,

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f"Vector({self.x}, {self.y})"

    def __complex__(self):
        return complex(self.x)

    def __int__(self):
        return Vector(int(self.x), int(self.y))

    def __float__(self):
        return float(self.x)


v1 = Vector(1.5, 2.5)
print(int(v1))
TypeError: __int__ returned non-int (type Vector)

 

然后是这个__index__函数,他代表当你把这个数据结构当成index使用的时候,它等价于什么,比如这里我们返回一个int(self.x),当我们做lst[v1]的时候,它相当做了一个lst[int(self.x)],也就是lst[1],返回值就是1

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f"Vector({self.x}, {self.y})"

    def __index__(self):
        return int(self.x)


v1 = Vector(1.5, 2.5)
lst = [0, 1, 2, 3]
print(lst[v1])
# 1

 

同时我们注意一下,只要我们定义了这个__index__,那么python内置函数int,float跟complex都会默认使用这个函数,当然前提是我们前面提到的3个对应的魔术方法没有被定义,我们看在只定义了index函数的情况下,int、float和complex都可以使用了

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f"Vector({self.x}, {self.y})"

    def __index__(self):
        return int(self.x)


v1 = Vector(1.5, 2.5)
print(int(v1))
print(float(v1))
print(complex(v1))
1
1.0
(1+0j)

 

最后看一下取整四兄弟:__round__  __trunc__  __floor__  __ceil__它们四个都是取整的意思,对应的分别是内置函数round和math这个库里面的trunc,floor和ceil,那这里具体的行为当然可以自己去定义。我们简单看一下这个4个取整有什么区别,round一般的语义叫做四舍五入,trunc代表小数点后面的不要也就是向0取整,floor是向下取整也就是向负无穷取整,ceil是向上取整也就是向正无穷取整。通过这些魔术方法可以改变它们作为numeric type的behavior

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f"Vector({self.x}, {self.y})"

    def __round__(self, ndigits):
        return int(self.x)

    def __trunc__(self):
        return int(self.x)

    def __floor__(self):
        return int(self.x)

    def __ceil__(self):
        return int(self.x)


v1 = Vector(1.5, 2.5)