Python——運算子過載(1)

KLeonard發表於2016-02-22
運算子過載

關鍵概念:

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__方法更加通用。

相關文章