运算符重载
关键概念:
1.运算符重载让类拦截常规的Python运算。
2.类可重载所有的Python表达式运算符。
3.类也可重载打印、函数调用、属性点号运算等内置运算。
4.重载使类实例的行为像内置类型。
5.重载是通过特殊名称的类方法来实现的。
运算符重载只是意味着在类方法中拦截内置的操作——当类的实例出现在内置操作中,Python自动调用你的方法,并且你的方法的返回值变成了相应操作的结果。
=======================================================================
构造函数和表达式:__init__和__sub__
看一个简单的重载例子。下述Number类提供了一个方法来拦截实例的构造函数(__init__),此外还有一个方法捕捉减法表达式(__sub__)。这种特殊的方法是钩子,可与内置运算绑定。
>>> class Number:
def __init__(self,start):
self.data = start
def __sub__(self,other):
return Number(self.data - other)
>>> X = Number(5)
>>> X.data
5
>>> Y = X - 2
>>> Y.data
3
=======================================================================
常见的运算符重载方法
在类中,对内置对象(例如,整数和列表)所能做的事,几乎都有相应的特殊名称的重载方法。下表列出其中一些最常用的重载方法。
方法 | 重载 | 调用 |
__init__ | 构造函数 | 对象建立:X = Class(args) |
__del__ | 析构函数 | X对象收回 |
__add__ | 运算符+ | 如果没有_iadd_,X+Y,X+=Y |
__or__ | 运算符|(位OR) | 如果没有_ior_,X|Y,X|=Y |
__repr__,__str__ | 打印、转换 | print(X)、repr(X),str(X) |
__call__ | 函数调用 | X(*args,**kargs) |
__getattr__ | 点号运算 | X.undefined |
__setattr__ | 属性赋值语句 | X.any = value |
__delattr__ | 属性删除 | del X.any |
__getattribute__ | 属性获取 | X.any |
__getitem__ | 索引运算 | X[key],X[i:j],没__iter__时的for循环和其他迭代器 |
__setitem__ | 索引赋值语句 | X[key] = value,X[i:j] = sequence |
__delitem__ | 索引和分片删除 | del X[key],del X[i:j] |
__len__ | 长度 | len(X),如果没有__bool__,真值测试 |
__bool__ | 布尔测试 | bool(X),真测试 |
__lt__,__gt__, | 特定的比较 | X < Y,X > Y |
__le__,__ge__, | | X<=Y,X >= Y |
__eq__,__ne__ | | X == Y,X != Y |
__radd__ | 右侧加法 | Other+X |
__iadd__ | 实地(增强的)加法 | X += Y (or else __add__) |
__iter__,__next__ | 迭代环境 | I = iter(X),next(I) |
__contains__ | 成员关系测试 | item in X (任何可迭代的) |
__index__ | 整数值 | hex(X),bin(X),oct(X),O[X],O[X:] |
__enter__,__exit__ | 环境管理器 | with obj as var: |
__get__,__set__ | 描述符属性 | X.attr,X.attr = value,del X.attr |
__new__ | 创建 | 在__init__之前创建对象 |
所有重载方法的名称前后都有两个下划线字符,以便把同类中定义的变量名区别开来。
运算符重载方法都是可选的——如果没有编写或继承一个方法,你的类直接不支持这些运算,并且试图使用它们会引发一个异常。
多数重载方法只用在需要对象行为表现的就像内置类型一样的高级程序中。然而,__init__构造函数常出现在绝大多数类中。
下面介绍一些运算符重载的用法示例。
=======================================================================
索引和分片:__getitem__和__setitem__
如果在类中定义了的话,则对于实例的索引运算,会自动调用__getitem__,把X作为第一个参数传递,并且方括号内的索引值传给第二个参数。例如,下面的类将返回索引值的平方。
>>> class Indexer:
def __getitem__(self,index):
return index**2
>>> X = Indexer()
>>> X[2]
4
>>> for i in range(5):
print(X[i],end = ' ')
0 1 4 9 16
------------------------------------------------------------------------------------------------------------------
拦截分片
有趣的是,除了索引,对于分片表达式也调用__getitem__。正式地讲,内置类型以同样的方式处理分片。例如,下面是一个内置列表上工作的分片,使用了上边界和下边界以及一个stride。(分片的知识可以回顾这里)
>>> L = [5,6,7,8,9]
>>> L[2:4]
[7, 8]
>>> L[1:]
[6, 7, 8, 9]
>>> L[:-1]
[5, 6, 7, 8]
>>> L[::2]
[5, 7, 9]
实际上,分片边界绑定到了一个分片对象中(即slice对象),并且传递给索引的列表实现。实际上,我们总是可以手动地传递一个分片对象——分片语法主要是用一个分片对象进行索引的语法糖:
>>> L[slice(2,4)]
[7, 8]
>>> L[slice(1,None)]
[6, 7, 8, 9]
>>> L[slice(None,-1)]
[5, 6, 7, 8]
>>> L[slice(None,None,2)]
[5, 7, 9]
__getitem__既针对基本索引调用,又针对分片调用。我们前面的类没有处理分片,但是,如下类将会处理分片。当针对基本索引调用时,参数像前面一样是一个整数。
>>> class Indexer:
data = [5,6,7,8,9]
def __getitem__(self,index):
print('getitem:',index)
return self.data[index]
>>> X = Indexer()
>>> X[0]
getitem: 0
5
>>> X[1]
getitem: 1
6
>>> X[-1]
getitem: -1
9
然而,当针对分片调用时,方法接收一个分片对象,它将一个新的索引表达式直接传递给嵌套的列表索引:
>>> X[2:4]
getitem: slice(2, 4, None)
[7, 8]
>>> X[1:]
getitem: slice(1, None, None)
[6, 7, 8, 9]
>>> X[:-1]
getitem: slice(None, -1, None)
[5, 6, 7, 8]
>>> X[::2]
getitem: slice(None, None, 2)
[5, 7, 9]
如果使用的话,__setitem__索引赋值方法类似地拦截索引和分片赋值——它为后者接收了一个分片对象,它可能以同样的方式传递到另一个索引赋值中:
def __setitem__(self,index,value):
...
self.data[index] = value
-------------------------------------------------------------------
-----------------------------------------------
索引迭代:__getitem__
这是一个很有用的技巧。for语句的作用是从0到更大的索引值,重复对序列进行【索引运算】,直到检测到超出边界的异常。因此,__getitem__也可以是Python中一种重载迭代的方式。如果定义了这个方法,for循环每次循环时都会调用类的__getitem__方法,并持续搭配有更高的偏移值。如下:
>>> class stepper:
def __getitem__(self,i):
return self.data[i]
>>> X = stepper()
>>> X.data = 'Spam'
>>> X[1]
'p'
>>> for item in X:
print(item,end = ' ')
S p a m
我们知道,任何支持for循环的类也会自动支持Python所有的迭代环境,比如成员关系测试in、列表解析、内置函数map、列表和元祖赋值运算以及类型构造方法等,如下示例:
>>> 'p' in X
True
>>> [c for c in X]
['S', 'p', 'a', 'm']
>>> list(map(str.upper,X))
['S', 'P', 'A', 'M']
>>> a,b,c,d = X
>>> a,c,d
('S', 'a', 'm')
>>> list(X),tuple(X),''.join(X)
(['S', 'p', 'a', 'm'], ('S', 'p', 'a', 'm'), 'Spam')
>>> X
<__main__.stepper object at 0x0376A6B0>
在实际应用中,这个技巧可以用于建立提供序列接口的对象,并新增逻辑到内置的序列的类型运算。
=======================================================================
迭代器对象:__iter__和__next__
尽管上述介绍的__getitem__技术有效,但它只是迭代的一种退而求其次的方法。一般Python中的迭代环境都会先尝试__iter__方法,再尝试__getitem__方法。
迭代环境是通过调用内置函数iter去尝试寻找__iter__方法来实现的,而这种方法应该返回一个迭代器对象。如果已经提供了,Python就会重复调用这个迭代器对象的next方法,直到发生StopIteration异常。如果没有找到这类__iter__方法,Python会改用__getitem__机制,就像之前那样通过偏移量重复索引,直到引发IndexError异常。
------------------------------------------------------------------------------------------------------------------
用户定义的迭代器
在__iter__机制中,类就是通过之前介绍的【迭代器协议】来实现用户定义的迭代器的。下述示例定义了用户定义的迭代器类来生成平方值。
>>> class Squares:
def __init__(self,start,stop):
self.value = start - 1
self.stop = stop
def __iter__(self):
return self
def __next__(self):
if self.value == self.stop:
raise StopIteration
self.value += 1
return self.value ** 2
>>> for i in Squares(1,5):
print(i,end = ' ')
1 4 9 16 25
在这个例子中,迭代器对象就是实例self,因为next方法是这个类的一部分。
和__getitem__不同的是,__iter__只循环一次,而不是循环多次。每次新的循环,都得创建一个新的迭代器对象,如下:
>>> X = Squares(1,5)
>>> [n for n in X]
[1, 4, 9, 16, 25]
>>> [n for n in X]
[]
>>> [n for n in Squares(1,5)]
[1, 4, 9, 16, 25]
-------------------------------------------------------------------
-----------------------------------------------
有多个迭代器的对象
之前提到过迭代器对象可以定义成一个独立的类,有其自己的状态信息,从而能够支持相同数据的多个迭代。看下例:
>>> s = 'ace'
>>> for x in s:
for y in s:
print(x+y,end=' ')
aa ac ae ca cc ce ea ec ee
在这里,外层循环调用iter从字符串中取得迭代器,而每个嵌套的循环也做相同的事来获得独立的迭代器。
之前介绍过,生成器表达式,以及map和zip这样的内置函数,都证明是单迭代对象;相反,range内置函数和其他的内置类型(如列表),支持独立位置的多个活跃迭代器。
当我们用类编写自己定义的迭代器的时候,由我们来决定是支持一个单个的或是多个活跃的迭代器。要达到多个迭代器的效果,__iter__只需替迭代器定义新的状态对象,而不是返回self.
例如,下面定义了一个迭代器类,迭代时,跳过下一个元素。因为迭代器对象会在每次迭代时都重新创建,所以能够支持多个处于激活状态下的循环。
>>> class SkipIterator:
def __init__(self,wrapped):
self.wrapped = wrapped
self.offset = 0
def __next__(self):
if self.offset >= len(self.wrapped):
raise StopIteration
else:
item = self.wrapped[self.offset]
self.offset += 2
return item
>>> class SkipObject:
def __init__(self,wrapped):
self.wrapped = wrapped
def __iter__(self):
return SkipIterator(self.wrapped)
>>> alpha = 'abcdef'
>>> skipper = SkipObject(alpha)
>>> I = iter(skipper)
>>> print(next(I),next(I),next(I))
a c e
>>> for x in skipper:
for y in skipper:
print(x+y,end = ' ')
aa ac ae ca cc ce ea ec ee
这个例子工作起来就像是对内置字符串进行嵌套循环一样,因为每个循环都会获得独立的迭代器对象来记录自己的状信息,所以每个激活状态下的循环都有自己在字符串中的位置。
=======================================================================
成员关系:__contains__、__iter__和__getitem__
在迭代领域,类通常把in成员关系运算符实现为一个迭代,使用__iter__方法或__getitem__方法。要支持更加特定的成员关系,类可能编写一个__contains__方法——当出现的时候,该方法优先于__iter__方法,__iter__方法优先于__getitem__方法。
__contains__方法应该把成员关系定义为对一个映射应用键,以及用于序列的搜索。
考虑下面的类,它编写了3个方法和测试成员关系以及应用于一个实例的各种迭代环境。调用的时候,其方法会打印出跟踪消息。
class Iters:
def __init__(self,value):
self.data = value
def __getitem__(self,i):
print('get[%s]:'%i,end = '')
return self.data[i]
def __iter__(self):
print('iter=> ',end='')
self.ix = 0
return self
def __next__(self):
print('next:',end='')
if self.ix == len(self.data):
raise StopIteration
item = self.data[self.ix]
self.ix += 1
return item
def __contains__(self,x):
print('contains:',end='')
return x in self.data
if __name__ == '__main__':
X = Iters([1,2,3,4,5])
print(3 in X)
for i in X:
print(i,end='')
print()
print([i**2 for i in X])
print(list(map(bin,X)))
I = iter(X)
while 1:
try:
print(next(I),end = ' @ ')
except StopIteration:
break
这段脚本运行的时候,其输出如下所示:
contains:True
iter=> next:1next:2next:3next:4next:5next:
iter=> next:next:next:next:next:next:[1, 4, 9, 16, 25]
iter=> next:next:next:next:next:next:['0b1', '0b10', '0b11', '0b100', '0b101']
iter=> next:1 @ next:2 @ next:3 @ next:4 @ next:5 @ next:
可以看到,特定的__contains__拦截成员关系,通用的__iter__捕获其他的迭代环境以致__next__被重复调用,而__getitem__不会被调用。
但是,要观察如果注释掉__contains__方法后代码的输出发生了什么变化——成员关系现在路由到了通用的__iter__:
iter=> next:next:next:True
iter=> next:1next:2next:3next:4next:5next:
iter=> next:next:next:next:next:next:[1, 4, 9, 16, 25]
iter=> next:next:next:next:next:next:['0b1', '0b10', '0b11', '0b100', '0b101']
iter=> next:1 @ next:2 @ next:3 @ next:4 @ next:5 @ next:
但是,如果__contains__和__iter__都注释掉的话,其输出如下——索引__getitem__替代方法会被调用,针对成员关系和其他迭代环境使用连续较高的索引:
get[0]:get[1]:get[2]:True
get[0]:1get[1]:2get[2]:3get[3]:4get[4]:5get[5]:
get[0]:get[1]:get[2]:get[3]:get[4]:get[5]:[1, 4, 9, 16, 25]
get[0]:get[1]:get[2]:get[3]:get[4]:get[5]:['0b1', '0b10', '0b11', '0b100', '0b101']
get[0]:1 @ get[1]:2 @ get[2]:3 @ get[3]:4 @ get[4]:5 @ get[5]:
正如我们看到的,__getitem__方法更加通用。