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方法做的兩件事是:
- 更新iterator的狀態,到下次調用;
- 返回當前調用結果, 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
下圖說明了各個概念之間的關係,
注: 圖片部分引用自 http://nvie.com/posts/iterators-vs-generators/
最後,談談為什麼要使用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 |