Python 妙用运算符重载——玩出“点”花样来(上)https://developer.aliyun.com/article/1490276
大于和小于 >、<
坐标比大小没什么物理意义,就搞点“花样”出来:小于 < 判断左边的横坐标是否相等;大于 > 判断右边的纵坐标是否相等;但纵横坐标不能同时相等。实际用处就是判断两点是否在同一水平线或同一垂直线上。
def __lt__(self, other): return self.x == other.x and self.y != other.y def __gt__(self, other): return self.x != other.x and self.y == other.y
大于等于和小于等于 >=、<=
在大于小于的基础上,大于等于和小于等于就重载成计算同一水平线或垂直线上的两点的距离。即小于等于 <= 横坐标相等时计算纵坐标的差;而大于等于 >= 纵坐标相等时计算横坐标的差。
def __le__(self, other): return self.x == other.x and self.y - other.y def __ge__(self, other): return self.y == other.y and self.x - other.x
测试:
>>> a = Point(2, 5) >>> b = Point(2, 3) >>> a <= b 2 >>> b <= a -2 >>> a >= b False >>> b >= a False >>> a = Point(5, 1) >>> b = Point(2, 1) >>> a >= b 3 >>> b >= a -3 >>> a <= b False b <= a False a < b False b > a True b < a False a > b True c = Point(2, 2) c >= c 0 c > c False 0 is False False 0 == False True
基于以上结果,只要注意对相同两点判断时,使用==False可能误判,因为0 is False,但用 is 来判断就能区别开来,is False 和 is 0 效果是不相同。所以我们把>=和<=的功能让给>和<,大于等于和小于等于重新定义为两点的横坐标或纵坐标是否(整数)相邻,并且为了好记忆,让 < 和 <= 管左边的横坐标,让 > 和 >= 管右边的纵坐标。修改后的所有比较运算符的重载代码如下:
class Point: def __init__(self, x=0, y=0): self.x, self.y = x, y def __repr__(self): return f'Point({self.x}, {self.y})' def __str__(self): return f'({self.x}, {self.y})' def __eq__(self, other): return self.x == other.x and self.y == other.y def __ne__(self, other): return self.x != other.x or self.y != other.y def __gt__(self, other): return self.x == other.x and self.y - other.y def __lt__(self, other): return self.y == other.y and self.x - other.x def __ge__(self, other): return self.x == other.x and abs(self.y - other.y)==1 def __le__(self, other): return self.y == other.y and abs(self.x - other.x)==1
位运算符
位运算符是一类专门用于处理整数类型数据的二进制位操作的运算符。
位与 &
原义是对两个数的二进制表示进行按位与操作,只有当两个位都为1时,结果位才为1,否则为0。
位或 |
原义是对两个数的二进制表示进行按位或操作,只要有一个位为1,结果位就为1。
== 和 != 分别表示横纵坐标 “x,y都相等” 和 “至少有一个不等”,互为反运算;
那就把 与 & 和 或 | 重载成 “x,y都不相等” 和 “至少有一个不等”,正好也互为反运算。
def __and__(self, other): return self.x != other.x and self.y != other.y def __or__(self, other): return self.x == other.x or self.y == other.y
可以理解为:==是严格的相等,“与” 是严格的不等;“或”是不严格的相等,!=是不严格的不等。
位异或 ^
原义是对两个数的二进制表示进行按位异或操作,当两个位不相同时,结果位为1;相同时为0。
那就把 异或 ^ 重载成横纵坐标 “x,y有且只有一个值相等”,非常接近异或的逻辑意义。
def __xor__(self, other): return self.x == other.x and self.y != other.y or self.x != other.x and self.y == other.y
位取反 ~
原义是将整数的二进制每一位进行取反操作,即将0变为1,将1变为0。对一个十进制整数n来说, ~n == -n-1;有个妙用列表的索引从0开始,索引下标0,1,2,3表示列表的前4个
,而~0,~1,~2,~3正好索引列表的倒数4个元素,因为它们分别等于-1, -2, -3, -4。
取反重载时,我们把它定义成交换坐标点的纵横坐标。
def __invert__(self): return Point(self.y, self.x)
测试:
>>> a = Point(1, 5) >>> a Point(1, 5) >>> ~a Point(5, 1) >>> a Point(1, 5) >>> a = ~a >>> a Point(5, 1)
左位移 <<
位移运算符的原义是将整数的二进制位全部左移或右移指定的位数。左移时,低位溢出的位被丢弃,高位空出的位用0填充;左移运算相当于对数值进行乘以2的运算 。
右位移 >>
右移运算对于有符号整数,右移时会保留符号位(即最高位),并在左侧填充与符号位相同的位。对于无符号整数,右移时左侧填充0;每次右移相当于将数值整除2的运算。
位移运算符重载时采用和比较运算符重载时相同的箭头指向性,即左位移管横坐标的移动,右位移管纵坐标的位移,此时other为整数,正整数指坐标点向右移动或向上移动;负整数刚好相反。
def __lshift__(self, other): return Point(self.x + other, self.y) def __rshift__(self, other): return Point(self.x, self.y + other)
测试:
>>> a = Point(5, 1) a Point(5, 1) a >> 4 Point(5, 5) a << -4 Point(1, 1) a Point(5, 1) a >>= 4 a Point(5, 5) a <<= -4 a Point(1, 5) >>> a = Point(1, 5) >>> b = Point(1, 1) >>> a > b 4 >>> if (n:=a>b): ... a >>= -n ... ... >>> a == b True
位移运算重载后,同时位移并赋值功能也生效,即 >>= 和 <<= 也同时被重载。 这种赋值功能也可以用 __ilshift__(self, other),__irshift__(self, other);大部分方法都有字母 i 开头对应的运算并赋值的方法,如__iadd__、__isub__、__imul__、__ior__......等等。
与__call__方法调用的比较:__call__更灵活可以同时改变横、纵坐标。
>>> a = Point(-5,5) >>> a << 5 Rc(0, 5) >>> a Rc(-5, 5) >>> a(5) Rc(0, 5) >>> a Rc(-5, 5) >>> a(0,-5) Rc(-5, 0) >>> a >> -5 Rc(-5, 0) >>> a Rc(-5, 5)
综合所有位运算符,代码如下:
class Point: def __init__(self, x=0, y=0): self.x, self.y = x, y def __repr__(self): return f'Point({self.x}, {self.y})' def __and__(self, other): return self.x != other.x and self.y != other.y def __or__(self, other): return self.x == other.x or self.y == other.y def __xor__(self, other): return self.x == other.x and self.y != other.y or self.x != other.x and self.y == other.y def __invert__(self): return Point(self.y, self.x) def __lshift__(self, other): return Point(self.x + other, self.y) def __rshift__(self, other): return Point(self.x, self.y + other)
算术运算符
算术运算很简单,除了加减乘除+,-,*,/,还有幂运算 **、取模 %、整除 // 等。
加 +
加法很明显重载成坐标值的相加最接近加法的本义:
def __add__(self, other):
return Point(self.x + other.x, self.y + other.y)
我们可以让other的定义域进一步扩大不仅限于是个点类,只要符合指定条件都可以“相加”,即移动为另一个点;如果“点”和不符合条件的对象相加,则返回 None。
指定条件为 hasattr(other, '__getitem__') and len(other)==2 ,重载定义如下:
class Point: def __init__(self, x=0, y=0): self.x, self.y = x, y def __repr__(self): return f'Point({self.x}, {self.y})' def __getitem__(self, index): if index in range(-2,2): return self.y if index in (1,-1) else self.x raise IndexError("Index out of range") def __len__(self): return 2 def __call__(self, dx=0, dy=0): return Point(self.x + dx, self.y + dy) def __add__(self, other): if hasattr(other, '__getitem__') and len(other)==2: return self.__call__(*map(int, other))
测试:
>>> a = Point(3,4) >>> b = Point(2,-2) >>> a + b Point(5, 2) >>> a + (1,1) Point(4, 5) >>> a + '11' Point(4, 5) >>> a + '1' # None 与调用__call__方法的比较: >>> p = Point(5, 5) >>> d = Point(2, 3) >>> p + d Point(7, 8) >>> p(d.x, d.y) Point(7, 8) >>> p(2, 3) Point(7, 8) >>> p Point(5, 5)
减 -
因为减一个数就是加它的相反数,所以没必要把减法运算重载成和加法一样的模式;我们可以把减法重载成两点之间的距离,重载定义为:
class Point: def __init__(self, x=0, y=0): self.x, self.y = x, y def __repr__(self): return f'Point({self.x}, {self.y})' def __getitem__(self, index): if index in range(-2,2): return self.y if index in (1,-1) else self.x raise IndexError("Index out of range") def __len__(self): return 2 def __sub__(self, other): if hasattr(other, '__getitem__') and len(other)==2: dx, dy = tuple(map(float, other)) return ((self.x-dx)**2 + (self.y-dy)**2)**0.5
测试:
>>> a = Point(3,4) >>> a - Point() 5.0 >>> a - (4,5) 1.4142135623730951 >>> a - '44' 1.0 >>> a - 3 # None
乘 *
乘法就重载为判断两点是否整数相邻,即: self >= other or self <= other
class Point: def __init__(self, x=0, y=0): self.x, self.y = x, y def __repr__(self): return f'Point({self.x}, {self.y})' def __ge__(self, other): return self.x == other.x and abs(self.y - other.y)==1 def __le__(self, other): return self.y == other.y and abs(self.x - other.x)==1 def __call__(self, dx=0, dy=0): return Point(self.x + dx, self.y + dy) def __mul__(self, other): return self >= other or self <= other
测试:
>>> P0 = Point(3, 5) >>> lst = (0,1), (0,-1), (-1,0), (1,0), (1,1) >>> P5 = [P0(x,y) for x,y in lst] >>> P5 [Point(3, 6), Point(3, 4), Point(2, 5), Point(4, 5), Point(4, 6)] >>> [P0*p for p in P5] [True, True, True, True, False]
除 /
除法就重载为判断在同一水平线或垂直线上的两点,是正序还是反序;正序是指前左后右或前下后上,返回1;反序则相反,前右后左或前上后下,返回-1;不符条件的,则返回False。
class Point: def __init__(self, x=0, y=0): self.x, self.y = x, y def __repr__(self): return f'Point({self.x}, {self.y})' def __gt__(self, other): return self.x == other.x and self.y - other.y def __lt__(self, other): return self.y == other.y and self.x - other.x def __xor__(self, other): return self.x == other.x and self.y != other.y or self.x != other.x and self.y == other.y def __truediv__(self, other): return (self^other) and (1 if (self<other)<0 or (self>other)<0 else -1)
测试:
>>> a = Point(1, 2) >>> b = Point(5, 2) >>> c = Point(5, 5) >>> a / b , b / a, b / c, c / b (1, -1, 1, -1) >>> a / c, c / a, a / a, c / c (False, False, False, False)
幂 **
幂运算就重载为返回在同一水平线或垂直线上的两点之间的点;不符合条件的,则返回None。
class Point: def __init__(self, x=0, y=0): self.x, self.y = x, y def __repr__(self): return f'Point({self.x}, {self.y})' def __gt__(self, other): return self.x == other.x and self.y - other.y def __lt__(self, other): return self.y == other.y and self.x - other.x def __xor__(self, other): return self.x == other.x and self.y != other.y or self.x != other.x and self.y == other.y def __truediv__(self, other): return (self^other) and (1 if (self<other)<0 or (self>other)<0 else -1) def __pow__(self, other): if self^other: if self<other: return [Point(_,self.y) for _ in range(self.x+(self/other),other.x,self/other)] if self>other: return [Point(self.x,_) for _ in range(self.y+(self/other),other.y,self/other)]
测试:
>>> a = Point(1, 2) >>> b = Point(5, 2) >>> c = Point(5, 5) >>> a ** b [Point(2, 2), Point(3, 2), Point(4, 2)] >>> b ** a [Point(4, 2), Point(3, 2), Point(2, 2)] >>> b ** c [Point(5, 3), Point(5, 4)] >>> c ** b [Point(5, 4), Point(5, 3)] >>> a ** c # None >>> a ** a # None >>> a ** a == [] False >>> a ** a == None True >>> a ** c == None True
取模 %
取模运算就重载为返回所给两点作对角线的水平矩形的另外两个端点;如果所得矩形面积为0,则返回值就是原来所给的两点。
class Point: def __init__(self, x=0, y=0): self.x, self.y = x, y def __repr__(self): return f'Point({self.x}, {self.y})' def __mod__(self, other): return [Point(self.x, dy), Point(dx, self.y)]
测试:
>>> a = Point(1, 2) >>> b = Point(5, 5) >>> a % b [Point(1, 5), Point(5, 2)] >>> (a%b)[0] Point(1, 5) >>> (a%b)[1] Point(5, 2) >>> c, d = a % b >>> c % d [Point(1, 2), Point(5, 5)] >>> c % a [Point(1, 2), Point(1, 5)] >>> c % b [Point(1, 5), Point(5, 5)]
示意图:
整除 //
整除运算就重载为返回所给两点作对角线的矩形上的两组邻边上的所有点;返回点的列表也分组,如上图,一组是路径a->c->b上的点,另一级是路径a->d->b上的点。
class Point: def __init__(self, x=0, y=0): self.x, self.y = x, y def __repr__(self): return f'Point({self.x}, {self.y})' def __gt__(self, other): return self.x == other.x and self.y - other.y def __lt__(self, other): return self.y == other.y and self.x - other.x def __and__(self, other): return self.x != other.x and self.y != other.y def __xor__(self, other): return self.x == other.x and self.y != other.y or self.x != other.x and self.y == other.y def __mod__(self, other): return [Point(self.x, other.y), Point(other.x, self.y)] def __truediv__(self, other): return (self^other) and (1 if (self<other)<0 or (self>other)<0 else -1) def __pow__(self, other): if self^other: if self<other: return [Point(_,self.y) for _ in range(self.x+(self/other),other.x,self/other)] if self>other: return [Point(self.x,_) for _ in range(self.y+(self/other),other.y,self/other)] def __floordiv__(self, other): if self&other: mod1, mod2 = self % other return self**mod1 + [mod1] + mod1**other, self**mod2 + [mod2] + mod2**other
测试:
>>> a = Point(1, 2) >>> b = Point(5, 5) >>> a // b ([Point(1, 3), Point(1, 4), Point(1, 5), Point(2, 5), Point(3, 5), Point(4, 5)], [Point(2, 2), Point(3, 2), Point(4, 2), Point(5, 2), Point(5, 3), Point(5, 4)]) >>> b // a ([Point(5, 4), Point(5, 3), Point(5, 2), Point(4, 2), Point(3, 2), Point(2, 2)], [Point(4, 5), Point(3, 5), Point(2, 5), Point(1, 5), Point(1, 4), Point(1, 3)])
右加 +
大部分运算符都有对应的右侧运算操作,由字母 r 开头,如右加__radd__、右减__rsub__、右乘__rmul__......等等;只有当自定义对象位于运算符号的右侧时,右运算方法才会被调用。与普通的加法相比,__radd__(self, other) 方法只有当 other 在加号+的左边,对象在加号+的右边才会被调用。
我们把右加方法重载成单目运算+差不多的规则,只是多了距离n,不一定是相邻的四点:
def __radd__(self, n): return self(0, n), self(0, -n), self(-n), self(n)
测试:
>>> a = Point(1,2); b = Point(); n = 2 >>> a + b Point(1, 2) >>> b + a Point(1, 2) >>> a + n Traceback (most recent call last): File "<pyshell#29>", line 1, in <module> a + n AttributeError: 'int' object has no attribute 'x' >>> n + a (Point(1, 4), Point(1, 0), Point(-1, 2), Point(3, 2)) >>> n + b (Point(0, 2), Point(0, -2), Point(-2, 0), Point(2, 0)) >>> 1 + b # 当n==1就返回上下左右相邻的四个点 (Point(0, 1), Point(0, -1), Point(-1, 0), Point(1, 0)) >>> 1 + b == +b True >>> -1 + b (Point(0, -1), Point(0, 1), Point(1, 0), Point(-1, 0)) >>> 0 + b (Point(0, 0), Point(0, 0), Point(0, 0), Point(0, 0))
右幂 **
右整除 //
分别返回两点间的水平线或垂直线上所有的整数点,other是由横纵坐标x,y与n组成的三元元组
def __rpow__(self, other): assert(isinstance(other, tuple) and len(other)==3) x, y, n = other return [Point(i, n) for i in range(min(x, self.x), max(x, self.x)+1)] def __rfloordiv__(self, other: tuple): assert(isinstance(other, tuple) and len(other)==3) x, y, n = other return [Point(n, i) for i in range(min(y, self.y), max(y, self.y)+1)]
测试:
>>> a = Point(1,2) >>> b = Point(5,5) >>> (a.x, a.y, 4)**b [Point(1, 4), Point(2, 4), Point(3, 4), Point(4, 4), Point(5, 4)] >>> (*b, 4)**a [Point(1, 4), Point(2, 4), Point(3, 4), Point(4, 4), Point(5, 4)] >>> (a.x, a.y, 3)//b [Point(3, 2), Point(3, 3), Point(3, 4), Point(3, 5)] >>> (*a, 3)//b [Point(3, 2), Point(3, 3), Point(3, 4), Point(3, 5)] >>> (*b, 3)//a [Point(3, 2), Point(3, 3), Point(3, 4), Point(3, 5)]
示意图:
总结
本文通过魔法方法的巧妙使用,为 Point 类定义了丰富多彩的“花样”功能,使其不仅能够表示一个二维空间中的点,还能够执行各种运算和操作。例如,我们可以重载加法运算符来计算两个点之间的移动,重载比较运算符来判断两个点是否在同一直线上,或者重载位运算符来交换点的横纵坐标等。本篇共涉及了四大类30多种魔法方法:
算术运算符:包括加 __add__、右加 __radd__、减 __sub__、乘 __mul__、除 __truediv__、取模 __mod__、整除 __floordiv__、__rfloordiv__ 和幂 __pow__、__rpow__。
比较运算符:包括等于 __eq__、不等于 __ne__、小于 __lt__、大于 __gt__、小于等于 __le__ 和大于等于 __ge__。
位运算符:包括位与 __and__、位或 __or__、位异或 __xor__、位取反 __invert__、左位移 << 和右位移 >>。
其他魔术方法:如显示__repr__、转字符串__str__、求长度 __len__、单目正负操作符 __pos__, __neg__、求绝对值 __abs__、布尔值 __bool__、下标获取和设置 __getitem__, __setitem__ 以及迭代功能 __iter__, __next__。
在实际编程中,我们应该根据实际需求来决定是否需要重载这些运算符;但过度使用运算符重载可能会导致代码难以理解和维护,而恰当的使用则可以提高代码的可读性和效率。总的来说,运算符重载是一种强大的工具,它可以让自定义类型更加自然地融入到Python的生态系统中。
全部完整代码:
class Point: def __init__(self, x=0, y=0): self.x, self.y = x, y def __repr__(self): return f'Point({self.x}, {self.y})' def __str__(self): return f'({self.x}, {self.y})' def __getitem__(self, index): if index in range(-2,2): return self.y if index in (1,-1) else self.x raise IndexError("Index out of range") def __setitem__(self, index, value): if index in (0, -2): self.x = value elif index in (1, -1): self.y = value else: raise IndexError("Index out of range.") @property def value(self): return self.x, self.y def __len__(self): return 2 def __abs__(self): return Point(*map(abs,(self.x, self.y))) def __bool__(self): return self.x>=0 and self.y>=0 def __neg__(self): return Point(-self.x, -self.y) def __pos__(self): return self(0, 1), self(0, -1), self(-1), self(1) def __call__(self, dx=0, dy=0): return Point(self.x + dx, self.y + dy) def __eq__(self, other): return self.x == other.x and self.y == other.y def __ne__(self, other): return self.x != other.x or self.y != other.y def __gt__(self, other): return self.x == other.x and self.y - other.y def __lt__(self, other): return self.y == other.y and self.x - other.x def __ge__(self, other): return self.x == other.x and abs(self.y - other.y)==1 def __le__(self, other): return self.y == other.y and abs(self.x - other.x)==1 def __and__(self, other): return self.x != other.x and self.y != other.y def __radd__(self, n): return self(0, n), self(0, -n), self(-n), self(n) def __or__(self, other): return self.x == other.x or self.y == other.y def __xor__(self, other): return self.x == other.x and self.y != other.y or self.x != other.x and self.y == other.y def __invert__(self): return Point(self.y, self.x) def __lshift__(self, other): return Point(self.x + other, self.y) def __rshift__(self, other): return Point(self.x, self.y + other) def __add__(self, other): return Point(self.x + other.x, self.y + other.y) def __sub__(self, other): return ((self.x - other.x)**2 + (self.y - other.y)**2)**0.5 def __mul__(self, other): return self >= other or self <= other def __truediv__(self, other): return (self^other) and (1 if (self<other)<0 or (self>other)<0 else -1) def __pow__(self, other): if self^other: if self<other: return [Point(_, self.y) for _ in range(self.x+(self/other),other.x,self/other)] if self>other: return [Point(self.x, _) for _ in range(self.y+(self/other),other.y,self/other)] def __mod__(self, other): return [Point(self.x, other.y), Point(other.x, self.y)] def __floordiv__(self, other): if self&other: mod1, mod2 = self % other return self**mod1 + [mod1] + mod1**other, self**mod2 + [mod2] + mod2**other def __rpow__(self, other): assert(isinstance(other, (tuple, list)) and len(other)==3) x, y, n = other return [Point(i, n) for i in range(min(x, self.x), max(x, self.x)+1)] def __rfloordiv__(self, other: tuple): assert(isinstance(other, (tuple, list)) and len(other)==3) x, y, n = other return [Point(n, i) for i in range(min(y, self.y), max(y, self.y)+1)]
注:把这些代码保存为文件 pointlib.py,可以当作一个自定义库来使用。这个自定义库的实例操作请见链接:Python 一步一步教你用pyglet制作“彩色方块连连看”游戏(续)-CSDN博客
完。