Python 3 之 运算符重载详解
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python 3 之 运算符重载详解相关的知识,希望对你有一定的参考价值。
基础知识
实际上,“运算符重载”只是意味着在类方法中拦截内置的操作……当类的实例出现在内置操作中,Python自动调用你的方法,并且你的方法的返回值变成了相应操作的结果。以下是对重载的关键概念的复习:
运算符重载让类拦截常规的Python运算。
类可重载所有Python表达式运算符
类可以重载打印、函数调用、属性点号运算等内置运算
重载使类实例的行为像内置类型。
重载是通过特殊名称的类方法来实现的。
换句话说,当类中提供了某个特殊名称的方法,在该类的实例出现在它们相关的表达式时,Python自动调用它们。正如我们已经学习过的,运算符重载方法并非必须的,并且通常也不是默认的;如果你没有编写或继承一个运算符重载方法,只是意味着你的类不会支持相应的操作。然而,当使用的时候,这些方法允许类模拟内置对象的接口,因此表现得更一致。
构造函数和表达式:__init__ 和 __sub__
让我们来看一下一个简单的重载例子吧。例如,下列文件number.py类的Number类提供一个方法来拦截实例的构造函数(__init__),此外还有一个方法捕捉减法表达式(__sub__)。这种特殊的方法是钩子,可与内置运算相绑定。
class Number: def __init__(self, start): # On Number(start) self.data = start def __sub__(self, other): # On instance - other return Number(self.data - other) # Result is a new instance >>> from number import Number >>> X = Number(5) # Number.__init__(X, 5) >>> Y = X - 2 # Number.__sub__(X, 2) >>> Y.data # Y is new Number instance 3
就像前边讨论过的一样,该代码中所见到的__init__构造函数是Python中最常见的运算符重载方法,它存在于绝大多数类中。在本节中,我们会举例说明这个领域中其他一些可用的工具,病看一看这些工具常用的例程。
常见的运算符重载方法
在类中,对内置对象(例如,整数和列表)所能做的事,几乎都有相应的特殊名称的重载方法。下表列出其中一些常用的重载方法。事实上,很多重载方法有好几个版本(例如,加法就有__add__、__radd__和__iadd__)。
方法 | 重载 | 调用 |
__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), 真测试(在Python 2.6中叫做__nonzero__) |
__lt__, __gt__ __lt__, __ge__ __eq__, __ne__ | 特定比较 | X<Y, X>Y, X<=Y, X>=Y, X == Y, X != Y(或者在Python 2.6中只有__cmp__) |
__radd__ | 右侧加法 | Other + X |
__iadd__ | 实地(增强的)加法 | X += Y(or else __add__) |
__iter, __next__ | 迭代环境 | I = iter(X), next(I); for loops, in if no __contains__, all comprehensions, map(F, X), 其他(__next__在Python2.6中成为next) |
__contains__ | 成员关系测试 | item in X(任何可迭代的) |
__index__ | 整数值 | hex(X), bin(X), oct(X), O[X], O[X:](替代Python 2中的__oct__、__hex__) |
__enter__, __exit__ | 环境管理器 | with obj as var: |
__get__, __set__ __delete | 描述符属性 | X.attr, X.attr = value, del X.attr |
__new__ | 创建 | 在__init__之前创建对象 |
所有重载方法的名称前后都有两个下划线,以便把同类中定义的变量名区别开来。特殊方法名称和表达式或运算的映射关系,是由Python语言预先定义好的(在标准语言手册中有说明)。例如名称,__add__按照Python语言的定义,无论__add__方法的代码实际在做些什么,总是对应到了表达式 + 。
如果没有定义运算符重载方法的话,它可能继承自超类,就像任何其他的方法一样。运算符重载方法也都是可选的……如果没有编写或继承一个方法,你的类直接不支持这些运算,并且试图使用它们会引发一个异常。一些内置操作,比如打印,有默认的重载方法(继承自Python 3.x中隐含的object类),但是,如果没有给出相应的运算符重载方法的话,大多数内置函数会对类实例失败。
多数重载方法只用在需要对象行为表现得就像内置类型一样的高级程序中。然而__init__构造函数常出现在绝大多数类中。我们已见到过__init__初始定义构造函数,以及上表中的一些其他的方法。让我们通过例子来说明表中的其他方法吧。
索引和分片:__getitem__ 和 __setitem__
如果在类中定义了(或继承了)的话,则对于实例的索引运算,会自动调用__getitem__。当实例X出现在X[i]这样的索引运算中时,Python会调用这个实例继承的__getitem__方法(如果有的话),把X作为第一个参数传递,并且方括号类的索引值传给第二个参数。例如,下面的类将返回索引值的平方。
>>> class Indexer: def __getitem__(self, index): return index ** 2 >>> X = Indexer() >>> X[2] # X[i] calls X.__getitem__(i) 4 >>> for i in range(5): print(X[i], end=‘ ‘) # Runs __getitem__(X, i) each time 0 1 4 9 16
拦截分片
有趣的是,除了索引,对于分片表达式也调用__getitem__。 正式地讲,内置类型以同样的方式处理分片。例如,下面是在一个内置列表上工作的分片,使用了上边界和下边界以及一个stride(可以回顾关于分片的知识):
>>> L = [5, 6, 7, 8, 9] >>> L[2:4] # Slice with slice syntax [7, 8] >>> L[1:] [6, 7, 8, 9] >>> L[:-1] [5, 6, 7, 8] >>> L[::2] [5, 7, 9]
实际上,分片边界绑定到了一个分片对象中,并且传递给索引的列表实现。实际上,我们总是可以手动地传递一个分片对象……分片语法主要是用一个分片对象进行索引的语法糖:
>>> L[slice(2, 4)] # Slice with slice objects [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__可能在甚至比索引和分片更多的环境中自动调用,正如下面的小节所介绍的。
Python 2.6中的分片和索引 在Python 3.0之前,类也可以定义__getslice__和__setslice__方法来专门拦截分片获取和赋值;它们将传递一系列的分片表达式,并且优先于__getitem__和__setitem__用于分片。 这些特定于分片的方法已经从Python 3.0中移除了,因此,你应该使用__getitem__和__setitem__来替代,以考虑到索引和分片对象都可能作为参数。 |
索引迭代:__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、列表和元组赋值运算以及类型构造方法也会自动调用__getitem__(如果定义了的话)。
>>> ‘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, b, c (‘S‘, ‘p‘, ‘a‘) >>> list(X), tuple(X), ‘‘.join(X) ([‘S‘, ‘p‘, ‘a‘, ‘m‘], (‘S‘, ‘p‘, ‘a‘, ‘m‘), ‘Spam‘) >>> X <__main__.stepper object at 0x000001E19957DF28>
在实际应用中,这个技巧可用于建立提供序列接口的对下你给,并新增逻辑到内置的序列类型运算。
迭代器对象:__iter__ 和 __next__
尽管上文中的__getitem__技术有效,但它真的只是迭代的一种退而求其次的的方法。如今,Python中所有的迭代环境都会先尝试__iter__方法,再尝试__getitem__。也就是说,它们宁愿使用迭代协议,然后才是重复对对象进行索引运算。只有在对象不支持迭代协议的时候,才会尝试索引运算。一般来讲,你也应该优先使用__iter__,它能够比__getiter__更好地支持一般的迭代环境。
从技术角度来讲,迭代环境是通过调用内置函数iter去尝试寻找__iter__方法来实现的,而这种方法应该返回一个迭代器对象。如果已经提供了,Python就会重复调用这个迭代器对象的next方法,知道发生StopIteration异常。如果没找到这类__iter__方法,Python会改用__getitem__机制,就像之前那样通过偏移量重复索引,知道发生IndexError异常(对于手动迭代来说,一个next内置函数也可以很方便地使用:next(I)与I.__next__()是相同的)。
用户定义的迭代器
在__iter__机制中,类就是通过实现迭代器协议来实现用户定义的迭代器的。例如,下面的iters.py,定义了用户定义的迭代器来生成平方值。
>>> 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方法是这个类的一部分。在较为浮渣的场景中,迭代器对象可定义为个别的类或自己的状态信息的对象,对相同数据支持多种迭代(下面会看到这种例子)。以Python raise语句发出的信号表示迭代结束。手动迭代对内置类型也有效:
>>> X = Squares(1, 5) >>> I = iter(X) >>> next(I) 1 >>> next(I) 4 ...... >>> next(I) 25 >>> next(I) Traceback (most recent call last): File "<pyshell#91>", line 1, in <module> next(I) File "<pyshell#77>", line 9, in __next__ raise StopIteration StopIteration
__getitem__所写的等效代码可能不是很自然,应为for会对所有的0和比较高值的偏移值进行迭代。传入的偏移值和所产生的值的范围只有间接的关系(0..N需要因设为start..stop)。因为__iter__对象会在调用过程中明确地保留状态信息,所以比__getitem__具有更好的通用性。
另外,
本文出自 “Professor哥” 博客,转载请与作者联系!
以上是关于Python 3 之 运算符重载详解的主要内容,如果未能解决你的问题,请参考以下文章