標籤:

Python Generator漫談

作為一個Python初學者, Python的格式化語法讓眾多編程小白追捧, 它的語法糖讓代碼變得簡潔易讀,它的龐大開源庫讓它在各個領域都能發揮作用. 但我時常感受到這個門檻極低的語言遠沒有表面上看起來易懂易用. 在Python的學習之路上, 我也時常迷茫於自己是否真正掌握了這門語言. Stackoverflow上有一篇帖子很好地說明了Python的進階之路, 於是我準備順著這個思路寫寫各個要點.

根據這篇帖子, Python進階之路: 從小白到大神. 其中, 前兩點分別是Discover list comprehensions 和 Discover generators(生成器). 身為小白的我, 心裡暗想generators是個啥。於是,研究了一番,遂成此文。

注: 全文使用Python3.5, 標準庫.

談起Generator, 與之相關的的概念有

  • {list, set, tuple, dict} comprehension and container
  • iterable
  • iterator
  • generator fuction and iterator
  • generator expression

接下來, 我們分別來看看這些概念:

{list, set, tuple, dict} comprehension and container

Container是存儲元素的數據結構, 一般存儲在內存中. 在Python中,常見的container包括但不限於:

  • list, deque, …
  • set, …
  • tuple, namedtuple, …
  • dict, defaultdict, Counter, …

簡單舉例:

assert 1 in [1, 2, 3] # listassert 1 in {1, 2, 3} # setassert 1 in (1, 2, 3) # tupled = {1: foo, 2: bar, 3: qux}assert 1 in d # dict

特別的, String也是container. 我們可以查詢一個substring是否在string里, 如:

s = foobarassert foo in sassert x not in s # string 包含所有的 substrings. 需要注意的是string並不存儲這些substrings, 只是可以如此查詢

iterable

一般說來,大部分的containers是iterable.而iterable不限於containers,還包括像文件.

iterable是實現了__iter__()方法的對象.該方法返回的是的一個iterator對象.其中,iterator的目的是返回所有元素.來看例子:

from collections import Iterable, Iteratorx = [1, 2, 3]y = iter(x)z = iter(x)next(y)

Out: 1

next(y)

Out: 2

next(z)

Out: 1

next(z)

Out: 2

type(x)

Out: list

isinstance(x, Iterable)

Out: True

type(y)

Out: list_iterator

isinstance(y, Iterable)

Out: True

可以看出, x是list, 也是iterable. y和z都是x的返回值, 也就是iterators, 互相獨立. iterators通過___next___()這個方法返回元素,每執行一次, 返回一個元素. 值得注意的是,y作為iterable的實例, 它也是iterator. iterable是一個比iterator更大的概念.

import disx = [1, 2, 3]dis.dis(for _ in x: pass)

1 0 SETUP_LOOP 14 (to 17) 3 LOAD_NAME 0 (x) 6 GET_ITER >> 7 FOR_ITER 6 (to 16) 10 STORE_NAME 1 (_) 13 JUMP_ABSOLUTE 7 >> 16 POP_BLOCK >> 17 LOAD_CONST 0 (None) 20 RETURN_VALUE

更深入的, 我們可以明顯看出在for循環中, Python每次都是調用FOR_ITER這個指令實現了讀取下一個next()的功能, 也就是說, for循環中,首先利用GET_ITER得到x的返回值, 一個iterator. 再通過FOR_ITER得到其中的元素. 如此實現了循環的功能.

iterator

那麼什麼是iterator? 就像之前我們提到的, iterator可以通過調用__next___()生成下一個值. 我們也可以說有__next___()這個方法的都可以是iterator. 這與它如何生成值無關. 我們具體看一個例子:

from itertools import isliceclass seq: def __init__(self): self.gap = 2 self.curr = 1 def __iter__(self): return self def __next__(self): value = self.curr self.curr += self.gap return value f = seq()list(islice(f, 0, 10))

Out: [1, 3, 5, 7, 9, 11, 13, 15, 17, 19]

以上, 我們構建了一個等差數列, 差為2. 其中, f既是iterable(因為iter方法), 也是iterator(因為有next方法).

對於iterator, next方法做的兩件事是:

  1. 更新iterator的狀態,到下次調用;
  2. 返回當前調用結果, eg: return value.

generator function and iterator

Ok. 終於到了本文的正題 generator. 首先下定義, generator是返回generator iterator的function(函數). 具體來說, 它可以讓你更優雅的實現iterator, 而不用寫帶有__iter__() 和 __next__() 的類. 繼續以上面那個等差數列為例, 我們看看generator如何的優雅的完成它:

def seq(): gap, curr = 2, 1 while True: yield curr curr = curr + gapf = seq()list(islice(f, 0, 10))

Out: [1, 3, 5, 7, 9, 11, 13, 15, 17, 19]

以上, 我們看到了讓它如此優雅的原因 yield.

讓我們看看這段code是如何運行的,

首先, seq是一個Python程序, 它返回的是iterator. 當 f = seq() 被調用, generator 會準備好返回. 但這時沒有執行任何代碼.

接下來, 這個generator實例在islice()中被使用. 仍然沒有執行代碼. 然後, list()開始建立list. 為了得到list的元素, list()開始調用islice()的next()方法抓取元素, 也就是要在 f 函數中取元素.

記住每次只生成一個元素. 這是代碼開始運行到 gap, curr = 2, 1 初始化變數. 接著進入始終為真的循環, 到了yield語句. 它做兩件事: 1.暫停, 並更新狀態到下一次yield, 2. 返回當前結果, 也就是curr值, 1. 這個值接著被傳到了islice(), 最後加到list中. list現在是 [1].

list()繼續要下一個值. 暫停的 f 函數繼續運行下一個語句curr + gap. curr 現在是2. 繼續進入下一個while循環, 遇到 yield. 同樣的, yield做兩件事: 1.暫停下來調整狀態; 2. 返回值2. 值返回到list, list 等於 [1, 2].

如此循環直到list取完10個元素. 特別的, 在第11次去值, islice()會有exception: StopIteration. 告訴list已經取值結束.

generator expression

generator expression是Python的另一種generator. 相信大家都用過list expression, 比如生成一列數的平方:

numbers = [1, 2, 3, 4, 5, 6][x ** 2 for x in numbers]

[1, 4, 9, 16, 25, 36]

generator expression很類似, 比如

squares_list = (x * x for x in numbers)squares_list

<generator object <genexpr> at 0x10435eaf0>

next(squares_list)

Out: 1

list(squares_list)

Out: [4, 9, 16, 25, 36]

以上, 不再累述.

Summary

下圖說明了各個概念之間的關係,

注: 圖片部分引用自 nvie.com/posts/iterator

最後,談談為什麼要使用generators. 它能幫你實現更優雅的代碼, 減少中間變數和不必要的數據結構,從而代碼量, 節省內存空間和運算性能.

針對一個循環代碼,

def something(): result = [] for ... in ...: result.append(x) return result

你可以做這樣的轉換

def iter_something(): for ... in ...: yield x

感謝閱讀,歡迎大家修改指正.

本文作者:Early Kid

更多內容,請訪問:BitTiger.io, 掃描下面二維碼,關注微信公眾賬號「論碼農的自我修養」


推薦閱讀:

在 Pycom 使用 Python + Micropython + MQTT 進行物聯網編程
Python 字元編碼的二三事
乾貨|Scikit-Learn的五種機器學習方法使用案例(python代碼)
文獻引文分析利器 HistCite 詳細使用教程(精簡易用免安裝版本 HistCite Pro 首發頁面)
OnlineJudge 2.0發布

TAG:Python |