1. 程式人生 > >一篇文章看懂 Python iterable, iterator 和 generato

一篇文章看懂 Python iterable, iterator 和 generato

<

Python 中的 iterable, iterator 以及 generator,一直是非常親密但是難以區分的概念。nvie 有一個很好的 帖子闡述了它們之間的關係,但是內容偏向於概括和總結,對於新手來說仍然難以理解。Fluent Python 的第 14 章也有非常好的演繹,但是我認為它對「為什麼要有這種語言特性」缺乏闡釋。我試圖從演變的角度,總結這些概念的來源和演化,以得到一個符合邏輯和容易理解的版本。

Simple Loop

幾乎每一個 Python 入門教程,都會用類似下面的程式碼來講述最簡單的 for 迴圈:

>>> l = [2, 1, 3]
>>> for i in l:
...     print(i)
2
1
3

在 Python 中,執行 l[i] 實際上是呼叫了 l 的 __getitem__ 函式list 型別中會實現了這個函式,用來返回某個 index 下的元素。而早期 Python 的實現上利用了這個操作。上面的 for 迴圈實際上是從 l[0] 開始取元素,等價於這段程式碼:

i = 0
while True:
    try:
        print(l[i])  # 亦可是 print(l.__getitem__(i))
        i += 1
    except IndexError:
        break

Python 內建的大多數容器型別(listtuplesetdict)都支援 __getitem__,因此它們都可以用在 for .. in迴圈中。如果你有個自定義型別也想用在迴圈中,你需要在類裡面實現一個 __getitem__ 函式,Python 就會識別到並使用它。Fluent Python 一書提供了一個例子

Lazy Evaluation

在上面的程式碼例子中,l 的值是全部被載入到記憶體中,再在迴圈中被一個一個取出來的。設想這樣一個場景,你要從資料庫中查詢出一千萬條資料做處理,

  • 如果全部載入到記憶體,可能會將記憶體撐滿
  • 在處理第一條資料前,需要等待大量時間從資料庫中取出這些資料
  • 一些特殊的場景下,你可能並不需要對全部的資料做處理,比如處理到第五百萬條資料時即可以結束

前輩們提出了惰性求值( Lazy Evaluation )來解決這個問題。有些地方也叫它「延遲載入」「懶載入」等。它的基本理念是「按需載入」,在上面的例子中,可以將取資料過程變成一頁頁取,比如先取 100 條資料進行處理,處理完後再取下一個 100 條,直至全部取完。

The Iterator Protocol

Python 為了在語言層面支援 lazy evaluation,給出了 iterator 協議。如果一個類實現了 __next__ 函式,並且:

  • 每次呼叫該函式,都可以返回一個新的資料
  • 沒有新的資料時,呼叫它丟擲 StopIteration 異常(當然如果序列是無限長,那麼可以不拋)

那麼這個類即支援 iterator 協議。於是「按需載入」,即可以通過每次 __next__ 被呼叫時去實現。Python 的內建函式 next(iterator) 實際上是呼叫 iterator.__next__。下面的例子給出一個 iterator 的實現,用來按需地計算出下一個斐波那契數:

>>> class FibonacciIterator:
...     def __init__(self, maximum):
...         # 為了簡單,將初始值設為 1, 2 而不是 0, 1
...         self.a, self.b = 1, 2
...         self.maximum = maximum
...     def __next__(self):
...         fib = self.a
...         if fib > self.maximum:
...             raise StopIteration
...         self.a, self.b = self.b, self.a + self.b
...         return fib
        
>>> f = FibonacciIterator(5)
>>> next(f)
1
>>> next(f)
2
>>> next(f)
3
>>> next(f)
5
>>> next(f)
# StopIteration occured

Enhanced iterable

上文中的 FibonacciIterator 已經實現了按需載入,那可以直接將它用在 for 迴圈中嗎?試試:

>>> for i in FibonacciIterator(5):
...     print(i)
... 
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'FibonacciIterator' object is not iterable

可以看到有 is not iterable 的報錯。按上一節的描述,早期的 Python 僅在一個類有 __getitem__ 函式時 Python 才將它當成 iterable,同時為了配合新的 iterator 的機制,Python 在 2.2 版本中將 __iter__ 協議加入了進來:

  1. 一個類如果實現了 __iter__ 並返回一個 iterator,那麼它是 iterable 的
  2. 如果沒有實現 __iter__,但是有 __getitem__,那麼它仍然是 iterable 的

那麼對於一個 iterator,如果你想能在 for 迴圈中使用,只需要實現一個 __iter__ 函式返回自己就可以了:

>>> class FibonacciIterator:
...     # **函式實現省略,見上文
...     def __iter__(self):
...         return self
>>> for i in FibonacciIterator(5):
...     print(i)
1
2
3
5

Generator Function

上面雖然花了挺大篇幅講述 iterator 的機制,但是事實上 Python 中以 __next__ 方式來實現 iterator 的並不多。Python 在 2.2 版本中支援了 iterator,但是也同時給出了另外一種更靈活方便,也更重要的機制 —— generator。

識別 generator 的標誌在 yield 關鍵字。上文的斐波那契數列,用 generator 來實現是:

>>> def fib(maximum):
...     a, b = 1, 2
...     while a <= maximum:
...         yield a
...         a, b = b, a+b
...
>>> for i in fib(5):
...     print(i)
1
2
3
5

上面的 fib() 雖然也是用 def 定義,但是它的函式體中有 yield 關鍵字,因此它不是個普通函式,而是個 generator function。它返回的是一個 generator object,即 fib(5) 處。generator 是一種在語言層面被支援的 iterator,它的規則是:

  • next() 呼叫一個 generator 時,Python 會執行函式體到下一個 yield 語句,並將 yield 後的值作用 next() 的返回值;然後該函式的執行暫停,直至下一次 next() 呼叫時,繼續執行到下一個 yield
  • 當整個函式體被執行完畢時,丟擲 StopIteration 異常

這套規則清晰直觀,可以將它套用在上面程式碼中驗證一下。yield 及 generator 是非常重要的機制,不僅僅在於它比 iterator 更簡單直觀,而在於它同時引入了一種控制語言執行的機制。對於普通函式,一旦執行則必須從函式頭執行到函式尾,之後才把控制權交給呼叫方;但是對於 generator function,你可以只執行一小段程式碼,即把控制權交回呼叫方(yield 時)。這種機制也對後面提出 coroutine 及 asyncio 中起到了重要的作用。

Generator Expression

試試執行下面的程式碼:

>>> sum([i**2 for i in range(11)])
385
>>> sum((i**2 for i in range(11)))
385
>>> sum(i**2 for i in range(11))
385

後兩種寫法,跟第一種有什麼區別呢?後兩種即是 generator expression,是一種方便生成 generator 的語法糖,形式上是用括號包裹的 list comprehension。背後的理念大概是這樣:list comprehension 是用來生成元素的,generator 也是用來生成元素的,那為什麼不提供一種類似 list comprehension 語法的 expression 來表示 generator 呢?它跟下面的程式碼是等價的:

def gen():
    for i in range(11):
        yield i**2

sum(gen())

至於第三種寫法為啥不用括號包起來,是 Python 為了可讀性故意設計的,如果作為唯一的函式引數使用,則可以省略。

總結

定義上:

  • 實現了 __iter__ 或 __getitem__,並滿足一定規則的型別是 iterable 的,它的例項是個 iterable
  • 實現了 __next__ 並滿足一定規則的型別,它是一種 iterator,它的例項是個 iterator
  • 在函式體中使用了 yield 的函式是 generator function ;使用了括號包裹的類 list comprehension 是 generator expression。它們都會產生 generator

語言機制上:

  • for .. in 迴圈所消費的物件,需要是個 iterable

它們之間的關聯:

  • 容器型別(listdict, etc.)大部分是 iterable ;dict 有多個不同函式生成不同用途的 iterator
  • iterator 大部分情況下是個 iterable
  • generator 是個 iterator,同時是個 iterable

文:giy.hkv

更多人工智慧相關文章:http://siligence.ai