本篇的主角正是“点”,今天要用运算符重载来,把它玩出“点”花样来!那什么是运算符重载呢?
运算符重载
运算符重载是面向对象编程中的一个概念,它允许程序员为自定义类型(如类或结构体)定义特定的运算符行为,使得这些类的实例可以使用语言中预定义的运算符。在Python等编程语言中,运算符重载是一种强大的特性,它使得我们可以用更加自然和直观的方式处理自定义类型。在实际编程中,我们应该根据需要合理使用这一特性,以提高代码的质量和效率。
主角点类
class Point 这个类很简单,就两个属性:横坐标x和纵坐标y。
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})'
测试:
>>> a = Point() >>> a Point(0, 0) >>> str(a) '(0, 0)' >>> b = Point(2, 5) >>> b Point(2, 5)
对于只需要整数坐标的类,比如二维数组的行列坐标,本文主要讨论整数坐标值的坐标,可以在类初始化函数里加上类型判断:
class Point: def __init__(self, x=0, y=0): self.x, self.y = x, y assert(isinstance(x, str) and isinstance(y, str)) def __repr__(self): return f'Point({self.x}, {self.y})' def __str__(self): return f'({self.x}, {self.y})'
测试:
>>> p = Point(2, 5) >>> p Point(2, 5) >>> q = Point(2.1, 5.5) Traceback (most recent call last): File "<pyshell#25>", line 1, in <module> q = Point(2.1, 5.5) File "<pyshell#22>", line 4, in __init__ assert(isinstance(x, int) and isinstance(y, int)) AssertionError
魔法方法
也称为特殊方法或双下划线方法,是python语言中的一种特殊方法,用于在类中实现一些特殊的功能。这些方法的名称始终以双下划线开头和结尾,比如上面点类定义时用到 __init__,__repr__,__str__。重载运算符时,我们就是靠魔法方法来重新定义运算符的,例如 __add__,__sub__,__mul__,__truediv__ 分别对应加减乘除四则运算。
在重载运算符前,再来学习几个其他类型的魔法方法:
__getitem__
__getitem__ 方法用于获取下标对应的值。
__setitem__
__setitem__ 方法用于设置下标对应的值。
定义完后,点类可以用下标0,1或者-2,-1来取值,和元组、列表等一样:obj[0], obj[1]。
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 __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.")
测试:
>>> a = Point(1,2) >>> a[0], a[1] (1, 2) >>> a[-1], a[-2] (2, 1) >>> a[0] = 5 >>> a Point(5, 2) >>> a[1] = 3 >>> a Point(5, 3) >>> [i for i in a] [5, 3] >>> x, y = a >>> x 5 >>> y 3 >>> b = iter(a) >>> next(b) 5 >>> next(b) 3 >>> next(b) Traceback (most recent call last): File "<pyshell#67>", line 1, in <module> next(b) StopIteration
__iter__
__next__
共同定义一个对象的迭代行为,迭代器必须实现__iter__()方法,该方法返回迭代器自身,或者返回一个新的迭代器对象。__next__()方法返回迭代器的下一个元素。
class Point: def __init__(self, x=0, y=0): self.x, self.y = x, y self.index = 0 def __repr__(self): return f'Point({self.x}, {self.y})' def __iter__(self): self.index = 0 return self def __next__(self): if self.index < 2: result = self.y if self.index else self.x self.index += 1 return result else: raise StopIteration
测试:
>>> a = Point(5, 3) >>> x, y = a >>> x, y (5, 3) >>> next(a) Traceback (most recent call last): File "<pyshell#115>", line 1, in <module> next(a) File "<pyshell#111>", line 16, in __next__ raise StopIteration StopIteration >>> a = Point(5, 3) >>> next(a) 5 >>> next(a) 3 >>> a Point(5, 3) >>> a.x 5 >>> next(a) Traceback (most recent call last): File "<pyshell#121>", line 1, in <module> next(a) File "<pyshell#111>", line 16, in __next__ raise StopIteration StopIteration >>> a[0] Traceback (most recent call last): File "<pyshell#122>", line 1, in <module> a[0] TypeError: 'Point' object is not subscriptable
对于点类说,可迭代魔法方法完全可弃用;因为使用__getitem__方法和iter()函数已有此功能。
__len__
求长度的方法,原义就是计算可迭代对象元素的个数;点类的长度就是2。
def __len__(self): return 2
__neg__
求相反数的方法,也就是单目的“ - ”符号;重载为横纵坐标都取相反数。
def __neg__(self): return Point(-self.x, -self.y)
__pos__
这是单目的“ + ”符号,一般无需重新定义;但是我们还是把它重载成穿过点的横纵两条直线上所有的整数点坐标,还是有点象形的,如一个十字架。
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 __pos__(self): n = 0 while True: yield Point(n, self.y), Point(self.x, n) n += 1
测试:
>>> a = Point(2, 4) >>> b = +a >>> next(b) (Point(0, 4), Point(2, 0)) >>> next(b) (Point(1, 4), Point(2, 1)) >>> next(b) (Point(2, 4), Point(2, 2)) >>> next(b) (Point(3, 4), Point(2, 3)) >>> next(b) (Point(4, 4), Point(2, 4)) >>> next(b) (Point(5, 4), Point(2, 5)) >>> b = +a >>> horizontal = [next(b)[0] for _ in range(5)] >>> horizontal [Point(0, 4), Point(1, 4), Point(2, 4), Point(3, 4), Point(4, 4)] >>> b = +a >>> vertical = [next(b)[1] for _ in range(5)] >>> vertical [Point(2, 0), Point(2, 1), Point(2, 2), Point(2, 3), Point(2, 4)]
这种设计返回的点太多,使用并不方便。有必要改成只返回上下左右相邻的四个点:
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 __pos__(self): return Point(self.x, self.y+1), Point(self.x, self.y-1), Point(self.x-1, self.y), Point(self.x+1, self.y)
__abs__
求绝对值的方法,重载时定义为把横纵坐标都取绝对。
def __abs__(self): return Point(*map(abs,(self.x, self.y)))
以上三种方法不改变类自身,注意以下写法会使类改变自身
def __neg__(self): self.x = -self.x return self def __pos__(self): self.y = -self.y return self def __abs__(self): self.x, self.y = map(abs,(self.x, self.y)) return self
__bool__
布尔值方法,重载时定义为点处在坐标系第一象限及其边界上,就返回True;否则返回False。
def __bool__(self): return self.x>=0 and self.y>=0
__call__
这个魔术方法比较特殊,它允许一个类像函数一样被调用;我们借此定义一个点的移动。
def __call__(self, dx=0, dy=0): return Point(self.x + dx, self.y + dy)
测试:
>>> a = Point(-5,5) >>> b = a(3, 2) >>> b Rc(-2, 7) >>> b = b(3, 2) >>> b Rc(1, 9) >>> a Rc(-5, 5)
扩展一下__call__方法,让它除了能移动点还能计算点到点的实际距离:
def __call__(self, dx=0, dy=0, distance=False): if distance: return ((self.x-dx)**2 + (self.y-dy)**2)**0.5 return Point(self.x + dx, self.y + dy)
测试:
>>> a = Point(3,4) >>> a(0,0,True) 5.0 >>> len(a) 5 >>> a(*a(1, 1), True) 1.4142135623730951 >>> a Rc(3, 4) >>> a(2, 3, True) 1.4142135623730951
注:一旦定义了__call__这个方法,__pos__方法就能改进得更简洁。
class Point: def __init__(self, r=0, c=0): self.x, self.y = r, c def __repr__(self): return f'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)
测试:
>>> a=Point() >>> +a (Point(0, 1), Point(0, -1), Point(-1, 0), Point(1, 0)) >>> +Point() # 直接返回上下左右四个方向 (Point(0, 1), Point(0, -1), Point(-1, 0), Point(1, 0)) >>> b = Point(7, 8) >>> +b (Point(7, 9), Point(7, 7), Point(6, 8), Point(8, 8))
最后,综合以上所有有用的魔术方法,代码如下:
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.") 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)
重载运算符
python中,常用的运算符都有对应的魔法方法可以重新定义新的运算操作。
比较运算符
相等 ==
两个点相等,就是它俩的横纵坐标分别相等。
def __eq__(self, other): return self.x == other.x and self.y == other.y
为使得类更强健,可以对参数other作一类型判断:
def __eq__(self, other): assert(isinstance(other, Point)) return self.x == other.x and self.y == other.y 或者: def __eq__(self, other): if isinstance(other, Point): return self.x == other.x and self.y == other.y else: raise TypeError("Operand must be an instance of Point")
不等 !=
def __ne__(self, other): return self.x != other.x or self.y != other.y
也可以这样表示:
def __ne__(self, other):
return not self.__eq__(er.y 。
因为 not self.x == other.x and self.y == other.y 即 not self.x == other.x or not self.y == other.y 。
经测试,有了__eq__,__ne__可有可无,直接可以用 != 运算。
>>> class Point: ... def __init__(self, x=0, y=0): ... self.x, self.y = x, y ... def __eq__(self, other): ... return self.x == other.x and self.y == other.y ... ... >>> a = Point(2, 5) >>> b = Point(2, 5) >>> c = Point(1, 3) >>> a == a True >>> a == b True >>> a == c False >>> a != b False >>> b != c True >>> class Point: ... def __init__(self, x=0, y=0): ... self.x, self.y = x, 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 ... ... >>> a = Point(2, 5) >>> b = Point(2, 3) >>> a != b True
Python 妙用运算符重载——玩出“点”花样来(下)https://developer.aliyun.com/article/1490285