文章目錄
前言
本文內容主要是對 Learn Python
索引迭代部分內容的摘抄。書籍內容講的非常詳細,完整閱讀請看原文。
重載運算符
下面從Learn Python
摘抄的部分重載運算符,所有重載方法前後都有兩個下劃線,以便把同類中定義的變量名區分開來。特殊方法名稱和表達式或運算的映射關係,是由Python提前定義好的。例如,名稱 __add__ 方法的代碼實際在做什麼,總是對應到了表達式 +。
方法 | 重載 | 調用 |
---|---|---|
__getitem__ | 索引運算 | X[key] , X[i:j] , 沒 __iter__ 的for循環和其它迭代器 |
__setitem__ | 索引賦值運算 | X[key]=value, X[i:j]=sequence |
__iter__, __next__ | 迭代環境 | I=iter(X), next(I) ; for loops , in if no *\_\_contains\_\_* , allcomprehensions, map(F, X) , 其它(__next__)在Python2.6稱爲next |
__contains__ | 成員關係測試 | item in X (任何可迭代的) |
__index__ | 整數值 | hex(X) , bin(X) , oct(X) , O[X] , O[X:] (替代Python2中的 __oct__、__hex__) |
索引和分片,getitem__和__setitem
如果在類中定義或繼承了 __getitem__,則對於實例的索引運算,會自動調用 __getitem__。實例X出現在X[i]這樣的實例運算中時,Python會調用這個實例集成的 __getitem__ 方法,X作爲第一個參數傳遞,第二個參數位方括號內的索引值
,self
就是實例X。
class Indexer:
def __getitem__(self, item):
return item ** 2
X = Indexer()
print(X[2])
for i in range[5]:
print(i)
運行結果如下:
0 1 4 9 16
###攔截分片
除了索引,分片也使用 getitem。 正式的講,內置類型以同樣的方式處理分片。
L = [5, 6, 7, 8, 9]
print(L[2:4])
print(L[1:])
print(L[::2])
運行結果如下:
[7, 8]
[6, 7, 8, 9]
[5, 7, 9]
實際上,分片邊界綁定到了一個分片對象上,並且傳遞給索引的列表實現。實際上,我們總是可以手動的傳遞一個分片對象–分片語法主要是用一個分片對象進行索引的語法糖。下面介紹分片對象 slice
。
L = [5, 6, 7, 8, 9]
print(L[slice(2, 4)])
print(L[slice(1, None)])
print(L[slice(None, -1)])
print(L[slice(None, None, 2)])
運行結果如下:
[7, 8]
[6, 7, 8, 9]
[5, 6, 7, 8]
[5, 7, 9]
在此,我們可以猜測分片語法 start:stop:step, 產生了slice
對象,並傳遞給列表。
對於一個帶有 __getitem__ 的類,重要的事實是該方法既針對基本索引調用,又針對分片調用。Indexr的 __getitem__ 可以同時處理整數索引和分片對象 slice
。這也驗證了我們上面的推測,即分片語法產生分片對象 slice
。
class Indexr:
data = [5, 6, 7, 8, 9]
def __getitem__(self, index):
print('getitem:', index)
return self.data[index]
x = Indexr()
print(x[0])
print(x[1:3])
運行結果:
getitem: 0
5
getitem: slice(1, 3, None)
[6, 7]
如果使用的話, *__setitem__*索引賦值方法類似的攔截索引和分片賦值—它爲後者接收了一個分片對象,它可能以同樣的方式傳遞到類似的另一個索引賦值中。value可能是一個整數值也可能是個索引對象。
def __setitem__(self, index, value):
self.data[index] = value
Python2.6中的分片和索引
在Python3之前,類也可以定義 __getslice__ 和 __setslice__ 方法來專門攔截分片獲取和賦值。它們將傳遞一系列的分片表達式,並且優先於 __getitem__ 和 *__setitem__*用於分片。但是前兩種方法在Python3中被移除了,所以使用Python3不需要考慮這兩種方法。
不要混淆Python3中用於索引攔截的 __index__ 方法,該方法針對一個實例返回一個整數值,供轉換爲數字字符串的內置函數使用。
class C:
def __index__(self):
return 255
X = C()
print(hex(X))
print(bin(X))
print(oct(X))
輸出結果:
0xff
0b11111111
0o377
該方法在Python2.6以同樣的方式工作,只不過他不會針對 hex
和 oct
內置函數調用(在python2.6中使用 __hex__ 和 __oct__ 來攔截這些調用)。
讓人困惑的 index
__index__ 來自官方的PEP357解釋了這一行爲。以下爲翻譯內容:
NumPy開發人員遇到了一個問題,只能通過添加一個新的特殊方法 __index__ 來解決。當使用如 [start:stop:step] 的分片表示法時,start、stop和step 只能使用整數或長整數。NumPy定義了與 8,16,32和64 位的無符號和有符號整數相對應的專用整數類型,但是沒有辦法指示這些類型可以用作索引切片。
切片不能使用現有的 __int__ 方法, 因爲該方法也用於對整數的強制轉換。如果切片使用 int, 浮點數也會成爲合法的切片索引,這顯然是一種不良行爲。相反,一個名爲 __index__ 的新特殊方法被添加了,它不帶參數,並返回一個整數,給出要使用的切片索引。 __index__ 返回值必須是Python整數或長整數。解釋器將檢查返回的類型是否正確,不滿組條件,則會引發TypeError
.
class C:
def __index__(self, value):
return 255
總結,__index__ 的作用是爲了Numpy的自定義整數類型,能夠像Python內置整數一樣表示分片對象。
索引迭代 getitem
for
語句的作用是從0到更大的索引值,重複對序列進行索引運算,直到檢測到超出邊界的異常。因此, __getitem__ 也可以是Pyhton中一種重載索引迭代的方式。如果定義了這個方法,for
循環每次循環時都會調用類的 __getitem__,並持續搭配有更高的索引值。這是一種“買一送一”的情況,任何會響應索引迭代的內置或用戶自定義對象,同樣會響應迭代。
class stepper:
def __init__(self, data):
self.data = data
def __getitem__(self, i):
return self.data[i]
x = stepper('spam')
print(x[1])
for item in x:
print(item)
運行結果:
p
s
p
a
m
任何支持 for
循環迭代的類也會自動支持Python所有的迭代環境。如,成員關係測試 in
,列表解析,內置函數 map
,列表和元組賦值以及類型構造方法也會調用 __getitem__ (如果定義了的話)。
迭代器對象 iter 和 next
儘管 __getitem__ 技術有效, 但它真的只是迭代的一種退而求其次的方法。如今 Python所有的迭代環境都會先嚐試 __iter__ 方法,再嘗試 __getitem__ 。 只有在對象不支持迭代協議的時候纔會嘗試索引運算。 一般來講,你應該優先使用 __iter__ ,它能夠比 __getitem__ 更好地支持一般的迭代環境。
從技術角度來講,迭代環境是通過調用內置函數 iter
去嘗試尋找 __iter__ 方法來實現的,而這種方法應該返回一個迭代器對象。 如果提供了,Python就會重複調用這個迭代器對象的 next
方法,直到發生 StopIteration
異常。 如果沒找到這類 __iter__ 方法, Pyhton就會改用 __getitem__ 就像之前那樣通關過偏移量重複索引,直到引起 IndexError
。
class stepper:
def __init__(self, data):
self.data = data
self.index = 0
def __getitem__(self, i):
print('getitem', end=' ')
return self.data[i]
def __iter__(self):
print('iter', end='\r\n')
return self
def __next__(self):
print('next', end=' ')
if self.index == len(self.data):
print('raise StopIteration!')
raise StopIteration
else:
self.index += 1
return self.data[self.index - 1]
x = stepper('spam')
for item in x:
print(item)
運行結果:
iter
next s
next p
next a
next m
next raise StopIteration!
先不討論 stepper
的實現細節,它同時存在 __getitem__ 和 *__iter__*方法。在 for
的迭代環境中,stepper忽略了索引運算,而是優先使用 __iter__ 方法,並由 __iter__ 調用 __next__ 內置函數,直到引發 StopIteration
。
用戶定義的迭代器
在 __iter__ 機制中, 類就是通過實現第14章和第20章介紹的迭代協議(見書本原文),來實現用戶定義的迭代器的。 例如,下面的文件 iters.py
, 定義了用戶定義的迭代器類來生成平方值。
# 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=' ')
# run result
>>> 1 4 9 16 25
在這裏, 迭代器就是實例 self
, 因爲 next
方法是這個類的一部分。在較爲複雜的場景中,迭代器對象可定義爲個別的類或有自己狀態信息的對象, 對相同對象支持多種迭代(下面會看到這種例子),以Python raise 語句發出信號表示迭代結束。 手動迭代對內置類型也有效(主動調用next方法)
>>> X = Squares(1, 3)
>>> I = iter(X)
>>> print(next(I))
>>> print(I.__next__())
>>> print(next(I))
>>> print(next(I))
StopIteration
__getitem__ 所寫的等效代碼可能不是很自然,因爲 for
會對所有的0和較高值的偏移值進行迭代, 傳入的偏移值和所產生的值的範圍只有間接的關係(0…N 需要映射爲 start…stop)。 因爲 __iter__ 對象會在調用過程中明確的保留狀態信息,所以比 __getitem__ 具有更好的通用性。 這裏還需要多理解理解
和 __getitem__ 不同的是 __iter__ 只循環一次,而不是循環多次。例如 Square類只循環一次,循環之後就變爲空。每次新的循環,都得創建一個新的迭代器對象。
X = Squares(1, 3)
print([n for n in X])
print([n for n in X])
X = Squares(1, 5)
print([n for n in X])
# run result
[1, 4, 9]
[]
[1, 4, 9, 16, 25]
有多個迭代器的對象
對S來講, 外層循環調用 iter
從對象中獲取迭代器,而每個嵌套的循環也做相同的事來獲得獨立的迭代器。 因爲每個激活狀態下的迭代器都有自己的狀態信息,而不管其它激活狀態下的循環是什麼狀態。 X返回的迭代器是自身, 嵌套的循環迭代同一個迭代器, 所以外訓環加內循環達到三次,結束迭代。
X = Squares(1, 3)
for x in X:
for y in X:
print('%d * %d' % (x, y), end=' ')
S = 'abc'
for x in S:
for y in S:
print(x + y, end=' ')
# run result
1 * 4 1 * 9
aa ab ac ba bb bc ca cb cc
當我們用類編寫用戶定義的迭代器的時候,由我們決定是否支持一個的或是多個活躍的迭代。 要達到多個迭代的效果, iter 只需替迭代器定義新的狀態對象,而不是返回self。
例如, 下面定義了一個迭代器類,迭代時,跳過下一個元素。 因爲迭代器對象會在每次迭代時都重新創建,所以能夠支持多個處於激活狀態下的循環。
class SkipIterator:
def __init__(self, wrapped):
self.wrapped = wrapped
self.offest = 0
def __next__(self):
if self.offest >= len(self.wrapped):
raise StopIteration
else:
item = self.wrapped[self.offest]
self.offest += 2
return item
class SkipObject:
def __init__(self, wrapped):
self.wrapped = wrapped
def __iter__(self):
# return a new iterator
return SkipIterator(self.wrapped)
if __name__ == '__main__':
alpha = 'abcdef'
skipper = SkipObject(alpha)
I = iter(skipper)
print(next(I), next(I), next(I))
for x in skipper:
for y in skipper:
print(x + y, end=' ')
運行時,這個例子工作起來就像是對內置字符串進行嵌套循環一樣, 因爲每個循環都會獲得獨立的迭代器對象來記錄自己的狀態信息, 所以每個激活狀態下的循環都有自己再字符串中的位置。
a c e
aa ac ae ca cc ce ea ec ee
成員關係: contains、iter__和__getitem
在迭代領域,類通常把 in
成員關係運算符實現爲一個迭代, 使用 __iter__ 方法或 __getitem__ 方法。 要支持更加特定的成員關係,類可能編寫一個 __contains__ 方法——當出現時,該方法優先於 __iter__ 方法, __iter__ 方法優先於 __getitem__ 方法。 __contains__ 方法應該把成員關係定義爲一個映射應用鍵,以及用於序列的搜索。
總結:優先級順序,__contains__ > __iter__ > __getitem__ (getitem
無人權!)
考慮如下的類, 它編寫了以上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, item):
print('contains: ', end='')
return item in self.data
def __index__(self):
return 255
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 True:
try:
print(next(I), end=' @ ')
except StopIteration:
break
這段腳本運行的時候, 其輸出如下所示——特定的 __contains__ 攔截成員關係,通用的 __iter__ 捕獲其它迭代環境以至 __next__ 重複的被調用, 而 _getitem_ 不會被調用。
contains: True
iter=>next:1 | next:2 | next:3 | next:4 | next:5 | next:
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__。
iter=>next:next:next:True
iter=>next:1 | next:2 | next:3 | next:4 | next:5 | next:
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]:1 | get[1]:2 | get[2]:3 | get[3]:4 | get[4]:5 | get[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__ 方法甚至更加通用: 除了迭代,它還攔截顯式索引和分片。 分片表達式用包含邊界的一個分片對象來觸發 __getitem__, 既針對內置類型,也針對用戶定義的類,因此,我們的類中分片式自動化的。
X = Iters('spam')
print(X[0])
print(X[1:])
print(X[slice(None, -1)])
# run result
get[0]:s
get[slice(1, None, None)]:pam
get[slice(None, -1, None)]:spa
然而, 在並非面向序列的,更加現實的迭代用例中, __iter__ 方法可能更容易編寫,因爲它不必管理一個整數索引,並且 __contains__ 可以作爲一種特殊情況優化成員關係。