Python生成器那些事兒
大家好,歡迎來到 Crossin的編程教室 !
生成器是Python語法中一個很常見的概念。今天我們給大家分享一篇有關(guān)生成器的文章。
讀完本文你會有以下收獲:
了解Python生成器的工作原理。 學(xué)會查詢PEP(Python Enhancement Proposals)。讓你知道python的一些設(shè)計(jì)理念。 最后會通過碼農(nóng)工作中一個實(shí)際的例子,來了解不同實(shí)現(xiàn)的好壞。
什么是生成器?
生成器(Generator)是一個非常強(qiáng)大的迭代器。其按照一定的算法生成一個序列。
包含yield的函數(shù)會返回一個生成器。生成器函數(shù)和普通函數(shù)看上去很像,不同的是生成器的返回值是用yield實(shí)現(xiàn)的。
也有一種寫法是(expression for i in s if condition),注意兩邊是括號而不是方括號。這種寫法等效于
def?a_generator():
????for?i?in?s:
????????if?condition:
????????????yield?expression
我們先實(shí)現(xiàn)一個簡單的生成器來演示一下:
def?gen():
????yield?3
????yield?1
????yield?4
a?=?gen()
next(a)?#?output?3
next(a)?#?output?1
next(a)?#?output?4
next(a)
#?Traceback?(most?recent?call?last):
#???File?"",?line?1,?in?
#?StopIteration
通過help(a),你會看到a是一個generator object,而且實(shí)現(xiàn)了__iter__和__next__,符合迭代器協(xié)議。說明函數(shù)gen返回了一個迭代器。
可以用next來進(jìn)行迭代,迭代到最后會返回一個StopIteration的異常。
與普通迭代器不同的是,生成器只能迭代一次。如果我們接著上面的代碼重新迭代,會發(fā)現(xiàn)沒有任何輸出。
for?item?in?a:
????print(item,?end='?')
# output:?沒有任何輸出。
生成器與yield關(guān)鍵字息息相關(guān),我們先來了解一下yield。
yield的發(fā)展史
yield in python2.3
yield在python2.3引入。最早的功能只有一個,就是將值返回給調(diào)用方,然后停止執(zhí)行函數(shù),保存現(xiàn)場并且再下次調(diào)用時回復(fù)。對比普通的函數(shù),函數(shù)在return后就退出了。中斷然后恢復(fù)是yield的特異功能。
yield in python2.5
到了python2.5,yield又有了表達(dá)式的功能(詳細(xì)見:https://www.python.org/dev/peps/pep-0342/)。也就是
a?=?yield?b
需要注意的是,這個表達(dá)式其實(shí)有兩部分,一部分是右側(cè)的yield b,一部分是賦值操作=。按照運(yùn)算優(yōu)先級來說,要先執(zhí)行yield b。因?yàn)閥ield的特性,返回b之后函數(shù)暫停,所以下一次yield前會執(zhí)行賦值操作,那么a到底被賦予了一個什么樣的值呢?
這就要解釋PEP-0342為迭代器引入的一個新函數(shù),send,在yield之后,程序暫停,然后a就等待下一次send的輸入。注意,send(None)和next是等價的。如下示例:
c?=?None
def?gen2():
????global?c
????b?=?0
????a?=?yield?b
????yield
????c?=?yield?a
x?=?gen2()
x.send(None)?# output 0, a等待接受send來的值。
x.send(3)????#?output?None,?輸出前執(zhí)行a=3
x.send(None)?# output 3, c等待接受send來的值。
x.send(5)????#?output?StopIteration,?同時輸出前c=5
print(c)?????#?output?5
這段代碼可以用如下流程來解釋,右側(cè)帶顏色的程序塊之間會暫停,并等待新的send信號:

yield in python3.3
python3.3中,新加了yield from這個操作。yield from g 基本等價于 for v in g: yield v,但是內(nèi)部幫忙實(shí)現(xiàn)了很多邊界處理,比如異常等。
明白了yield的基本操作,那么生成器有什么用呢?
Python中生成器的作用
這里告訴大家一個小技巧,當(dāng)你不知道python某個功能有什么用的時候,可以查一下PEP(Python Enhancement Proposals),中文叫《Python增強(qiáng)提 案》。這里面除了重要的通知外,還有一些新功能的描述,以及為什么要設(shè)計(jì)這個功能。
目錄為:
https://www.python.org/dev/peps/?
可以按關(guān)鍵字檢索。
拿生成器來說,首次出行在PEP255《Simple Generators》(https://www.python.org/dev/peps/pep-0255/),在Motivation一欄詳細(xì)描述了其設(shè)計(jì)的動機(jī)。可以看出,生成器的設(shè)計(jì)初衷是要優(yōu)化生產(chǎn)者函數(shù)迭代+回調(diào)函數(shù)的場景。某些處理可能會使用生成器之前生產(chǎn)的值,會讓調(diào)用者的設(shè)計(jì)變復(fù)雜。而yield的中斷恢復(fù)機(jī)制讓迭代和調(diào)用者都變得更加自然。基于這一功能,可以很方便的實(shí)現(xiàn) 協(xié)程操作。
所以說使用生成器有如下的好處:
實(shí)現(xiàn)協(xié)程(Coroutine),通過yield的中斷恢復(fù)功能,可以實(shí)現(xiàn)一個線程實(shí)現(xiàn)多任務(wù)的調(diào)度。沒有了線程切換和鎖,沒有了用戶態(tài)和內(nèi)核態(tài)的切換 ,性能上會提升不少,尤其是IO場景較多的情況下。關(guān)于協(xié)程內(nèi)容較多,而且有更好的方法(async/await),有時間另開一篇講。 一般情況下生成器比迭代速度要快一些。 代碼可讀性更高。 由于本身也是個迭代器,所以也擁有迭代器的優(yōu)點(diǎn):惰性計(jì)算。不需要把所有的數(shù)據(jù)加載到內(nèi)存,而是即取即用。
不適合生成器的場景:
多次讀取。此時生成器無法滿足,可以用list(a_generator)來轉(zhuǎn)換成list 隨機(jī)讀取。生成器沒有類似 x[i]這樣的下標(biāo)操作。拼接字符串。 ''.join(alist)比''.join(a_generator)更快。
生成器現(xiàn)實(shí)中的例子
比如要讀取一些網(wǎng)絡(luò)日志,然后統(tǒng)計(jì)發(fā)送的字節(jié)數(shù)。格式如下:
127.0.0.1?-?-?[24/Feb/2008:00:08:59?-0600]?"GET?/ply/ply.html?HTTP/1.1"?200?97238
192.168.0.1?-?-?[24/Feb/2008:00:08:59?-0600]?"GET?/favicon.ico?HTTP/1.1"?404?133
最后一列是發(fā)送字節(jié)數(shù)。我們要把文件中每一行最后一列的數(shù)字累計(jì)求和。
第一種方法,硬編碼直接實(shí)現(xiàn)。
def?read_file_count_inside(filename):
????total?=?0
????with?open(filename)?as?wwwlog:
????????for?line?in?wwwlog:
????????????bytes_sent?=?line.rsplit(None,?1)[1]
????????????if?bytes_sent?!=?'-':
????????????????total?+=?int(bytes_sent)
????return?total
#?User?time?(seconds):?13.09
#?System?time?(seconds):?0.35
#?Elapsed?(wall?clock)?time?(h:mm:ss?or?m:ss):?0:13.45
#?Maximum?resident?set?size?(kbytes):?6880
直接實(shí)現(xiàn)很好,但是復(fù)用性較差。假如我換個任務(wù),統(tǒng)計(jì)ip的頻次。那就要修改現(xiàn)有的邏輯,或者重寫一個類似的函數(shù)。
第二種實(shí)現(xiàn),讀取和處理分開。
def?read_file(filename):
????with?open(filename)?as?wwwlog:
????????return?wwwlog.readlines()
def?count_bytes(line_list):
????total?=?0
????for?line?in?line_list:
????????bytes_sent?=?line.rsplit(None,?1)[1]
????????if?bytes_sent?!=?'-':
????????????total?+=?int(bytes_sent)
????return?total
#?User?time?(seconds):?13.90
#?System?time?(seconds):?2.87
#?Elapsed?(wall?clock)?time?(h:mm:ss?or?m:ss):?0:16.78
#?Maximum?resident?set?size?(kbytes):?2315280
第二種邏輯清晰,可擴(kuò)展性也好。但是占用內(nèi)存太恐怖。
第三種實(shí)現(xiàn):callback函數(shù)
G_TOTAL?=?0
def?count_callback(line):
????global?G_TOTAL
????bytes_sent?=?line.rsplit(None,?1)[1]
????if?bytes_sent?!=?'-':
????????G_TOTAL?+=?int(bytes_sent)
def?read_file_with_callback(filename,?callback_fn):
????with?open(filename)?as?wwwlog:
????????for?line?in?wwwlog:
????????????callback_fn(line)
????return?G_TOTAL
#?User?time?(seconds):?15.18
#?System?time?(seconds):?0.38
#?Elapsed?(wall?clock)?time?(h:mm:ss?or?m:ss):?0:15.57
#?Maximum?resident?set?size?(kbytes):?6880
callback代碼也算清晰,但是不免保存一些現(xiàn)場的變量。
第四種實(shí)現(xiàn):生成器
def?read_file_gen(filename):
????with?open(filename)?as?wwwlog:
????????for?line?in?wwwlog:
????????????yield?line
def?count_gen(generator):
????bytecolumn?=?(line.rsplit(None,?1)[1]?for?line?in?generator)
????bytes_sent?=?(int(x)?for?x?in?bytecolumn?if?x?!=?'-')
????return?sum(bytes_sent)
#?User?time?(seconds):?14.92
#?System?time?(seconds):?0.34
#?Elapsed?(wall?clock)?time?(h:mm:ss?or?m:ss):?0:15.26
#?Maximum?resident?set?size?(kbytes):?6884
生成器實(shí)現(xiàn)就很舒服,可以說是綜合了第二第三種的優(yōu)點(diǎn)。
第五種:走火入魔生成器
sum(int(x)?for?x?in?(line.rsplit(None,?1)[1]?for?line?in?open(filename))?if?x?!=?'-')
#?User?time?(seconds):?14.03
#?System?time?(seconds):?0.32
#?Elapsed?(wall?clock)?time?(h:mm:ss?or?m:ss):?0:14.35
#?Maximum?resident?set?size?(kbytes):?6916
這種方法雖然免去了很多函數(shù)調(diào)用,但是可讀性并不太好,只適合簡單場景。不建議使用。
第六種:實(shí)際工作中采用的方法
awk?{?total?+=?$NF?}?END?{?print?total?}?big-access-log
#?User?time?(seconds):?10.72
#?System?time?(seconds):?0.32
#?Elapsed?(wall?clock)?time?(h:mm:ss?or?m:ss):?0:11.04
#?Maximum?resident?set?size?(kbytes):?1348
程序員總是想用最少的代碼來實(shí)現(xiàn)一個功能。對于文本處理來說,awk和sed可以滿足絕大多數(shù)的需求,而且速度比python更快。但是python勝在跨平臺,畢竟windows可沒有awk。
你喜歡哪一種呢?
以上就是對Python生成器的一些介紹。如果文章對你有幫助,歡迎轉(zhuǎn)發(fā)/點(diǎn)贊/收藏~
作者:碼農(nóng)要術(shù)
_往期文章推薦_
