什麼是迭代器
相關概念定義
迭代器(Iterator):
滿足迭代協議的物件就是迭代器
iterator就是實現了Iteration Protocol的物件,這類物件都支援迴圈遍歷的操作(for/while/支援迭代的函式list() sum()…)。
迭代協議(Iteration Protocol):
內建函式iter()
接收一個可迭代物件,並返回一個可迭代物件.
每次將這個可迭代物件傳遞給next()
函式,都會返回它所包含的下一個元素,當迭代完最後一個元素時,就會觸發StopIteration異常。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
>>> x = iter([1, 2, 3]) >>> x <listiterator object at 0x1004ca850> >>> next(x) 1 >>> next(x) 2 >>> next(x) 3 >>> next(x) Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration |
滿足以上要求的物件,就是迭代器。
迭代隱含的操作
在每次的迭代語句中,python都會按照迭代協議去對迭代器進行迭代。其實,在實際執行中,python會進行一些其他的操作:
- 將需要迭代的物件作為引數傳遞給
iter
函式 iter
返回一個迭代器物件- 每次迴圈則將返回的迭代器物件傳遞給
next
函式 - 迴圈至最後一個元素,觸發
StopIteration
以for語句為例:
當我們在Python中執行迴圈語句for i in foo
的時候,其背後的操作是:
- foo = iter(foo)
- next(foo)
next(foo)在python3中執行的是:foo.__next__()
,在python2中則是:foo.next()
迭代器的實現
迭代器是用class來實現的。其中必需實現的有兩個方法:__iter__
、next
(python2)/__next__
(python3)。其中,__iter__
必需返回一個迭代器物件,next
則負責迭代邏輯並在迭代完畢時觸發異常。
如下:
1 2 3 4 5 6 7 8 9 |
def Iter(object) def __init__(self): pass def __iter__(self): pass def __next__(self): # python3 pass def next(self): # python2 pass |
迭代器的特殊情況
迭代器的__iter__
返回self
迭代器的__iter__
方法需要返回的是一個具有next
方法的可迭代物件。如果當__iter__
返回的是self
的話,就會產生其他意想不到的效果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
class yrange: def __init__(self, n): self.i = 0 self.n = n def __iter__(self): return self def next(self): if self.i < self.n: i = self.i self.i += 1 return i else: raise StopIteration() class zrange: def __init__(self, n): self.i = 0 self.n = n def __iter__(self): return zrange(self.n) def next(self): if self.i < self.n: i = self.i self.i += 1 return i else: raise StopIteration() |
執行結果:
1 2 3 4 5 6 7 8 9 10 |
>>> y = yrange(5) >>> list(y) [0, 1, 2, 3, 4] >>> list(y) [] >>> z = zrange(5) >>> list(z) [0, 1, 2, 3, 4] >>> list(z) [0, 1, 2, 3, 4] |
在yrange
中,iter
返回的是self
,在執行list(y)
時iter
返回的都是同一個self
,所以再次呼叫list(y)
時只會觸發結束迭代異常,列表中並無內容。
而在zrange
中,每次執行list(z)
時,iter
都是返回一個新的迭代器zrange(self.n)
,所以每次執行list(z)
都得到完整的元素。
生成器的迭代
通常,對於資料量特別大的序列,我們會用生成器generator
來代替容器物件container
,這樣可以利用lazy evaluable來節省記憶體開銷。值得注意的是,生成器也是一個只能迭代一次的迭代器。
1 2 3 4 5 |
def grange(n): i = 0 while i < n: yield i i += 1 |
執行結果:
1 2 3 4 5 |
>>> glist = grange(10) >>> list(glist) [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] >>> list(glist) [] |
如果是利用便捷的生成器表示式也是一樣:
1 2 3 4 5 |
>>> alist = (i for i in range(10)) >>> list(alist) [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] >>> list(alist) [] |
要解決這個問題,可以將迭代器和生成器組合使用:
1 2 3 4 5 6 7 |
class Grange(object): def __init__(self, n): self.n = n def __iter__(self): for i in range(self.n): yield i |
結果:
1 2 3 4 5 |
>>> glist = Grange(10) >>> list(glist) [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] >>> list(glist) [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] |
值得注意的是,平常我們利用到生成器的地方都是資料量特別大的情況,這個時候,其實應該儘量避免多次迭代生成器。我想這應該也是python沒有支援對生成器多次迭代的特性的原因。
程式設計建議
在實際的程式設計中,往往需要在函式中多次迭代一個序列,如果這個序列是呼叫API得到的,而你又不能保證它是沒有陷阱的迭代器時。可以在遍歷迭代器的時候,加入一個判斷語句,避免無法多次迭代的情況發生:
1 2 |
def iterator_checker(iterator): assert iter(iterator) is not iter(iterator), "iter() return self" |
參考資料
- python官網迭代器文件
- python practice book
- <<Effective Python>>