Python中的迭代協議
前言
Python的特色之一是基於協議實現功能。比如改變一個加號(+
)的行爲,在C++中需要操作符重載,在Python中則是重寫__add__
方法。爲了描述可迭代對象和迭代器,Python提供了兩個魔法方法,分別是__iter__
和__next__
。又爲了支持for...in...
行爲,牽扯進了__getitem__
。
我們先從可迭代對象說起。
Iterable
Python在collections.abc模板中提供了Iterable,用於判斷對象是否可迭代,還提供了Iterator,判斷一個對象是否是迭代器。這裏拿熟知的列表類型試水:
lyst = ["t", "y"]
print(isinstance(lyst, Iterable)) # 輸出:True
print(isinstance(lyst, Iterator)) # 輸出:False
可見list只是一個可迭代對象,而非迭代器。
讓一個對象成爲可迭代對象只需要添加__iter__
方法。
class Students(object):
def __iter__(self):
pass
if __name__ == "__main__":
stu = Students()
print(isinstance(stu, Iterable)) # 輸出:True
儘管上面看來其有些欺詐——如果真的將它迭代會發現解釋器報錯TypeError。但不可否認的是,此時解釋器認爲stu是一個可迭代對象了(涉及到**isinstance()**的處理機制,可見《Python中的abc模塊》),所以isinstance()
返回出True。
Iterator
與可迭代對象最大的區別在於,可以對迭代器使用next()
方法。認爲,迭代器是特殊的可迭代對象,它需要__iter__
和__next__
兩個協議支持。
用之前的欺騙手法做個測試:
class Students(object):
def __iter__(self):
pass
def __next__(self):
pass
if __name__ == "__main__":
stu = Students()
print(isinstance(stu, Iterable)) # 輸出:True
print(isinstance(stu, Iterator)) # 輸出:True
對象stu是可迭代對象,也是迭代器。打印結果證實了前邊說法。
for…in…
直接在協議函數下面寫pass
,讓其充裝可迭代對象、迭代器,這是利用了Python鴨子類型的特點。然而,如果希望對象可以正常的被for...in...
遍歷,有些邏輯需要我們手動實現。
先說__iter__
方法,它只需要完成一個任務——返回一個迭代器。如果我們不返回迭代器,將引發報錯:
class Students(object):
def __iter__(self):
return [0, 1, 2] # 列表類型是可迭代對象,而不是迭代器
if __name__ == "__main__":
stu = Students()
for s in stu:
print(s)
# 輸出:
Traceback (most recent call last):
File "xxx", line 50, in <module>
for s in stu:
TypeError: iter() returned non-iterator of type 'list'
現在試着返回一個生成器(生成器是特殊的迭代器):
class Students(object):
def __iter__(self):
return (i for i in range(3))
if __name__ == "__main__":
stu = Students()
for s in stu:
print(s)
# 輸出:
0
1
2
事實上,在for...in...
過程中,Python會自動調用iter()
函數,也就是說for i in x
被轉換成了for i in iter(x)
處理。想要繼續運行下去,得先保證iter()
函數不會拋錯。
基於前面結論,__iter__
不負責邏輯處理,那麼處理邏輯就得寫在__next__
方法裏面。像下面這樣:
class Students(object):
def __init__(self):
self.items = [1, 2, 3, 4]
self.index = 0
def __next__(self):
if self.index >= len(self.items):
raise StopIteration
result = self.items[self.index]
self.index += 1
return result
此時還不可以對Students的實例對象使用for遍歷,但可以通過next()
訪問內部元素:
print(next(stu))
print(next(stu))
print(next(stu))
# 輸出:
1
2
3
之前也說過了,迭代器需要實現__iter__
和__next__
兩個方法,所以我們得添加__iter__
。又因爲__iter__
的任務是返回一個迭代器,擁有了__iter__
和__next__
方法的Students的對象本身就是一個迭代器(似乎有點繞),所以返回自身(self)即可:
class Students(object):
# ...
def __iter__(self):
return self
# ...
另外需要說明的是,在for...in...
遍歷過程中,需要一個終止信號,這個信號就是StopIteration。for…in…遍歷的時候等同於:
while True:
try:
print(next(stu))
except StopIteration:
break
getitem
__getitem__
的作用可見《Python中的切片》,for...in...
運作的完整流程需要用到這個方法。從問第一個問題開始:
- 問題1:對象中有
__iter__
方法嗎?- 如果有,調用
iter(對象)
;(此時StopIteration是終止信號,for…in…會自動處理StopIteration) - 如果沒有,進入問題2。
- 如果有,調用
- 問題2:對象中有
__getitem__
方法嗎?- 如果有,從0開始傳入索引值訪問內部元素,之後索引值依次累加:1,2,3,…(此時IndexError是終止信號,for…in…會自動處理IndexError)
- 如果沒有,拋出異常TypeError。
感謝
- 參考慕課Bobby老師課程Python高級編程和異步IO併發編程