1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        Python生成器那些事兒

        共 5370字,需瀏覽 11分鐘

         ·

        2021-10-03 21:12


        導讀

        讀完本文你會有兩點收獲:

        1. 了解Python生成器的工作原理。
        2. 學會查詢PEP(Python Enhancement Proposals)。讓你知道python的一些設計理念。
        3. 最后會通過碼農(nóng)工作中一個實際的例子,來了解不同實現(xiàn)的好壞。

        什么是生成器?

        生成器(Generator)是一個非常強大的迭代器。其按照一定的算法生成一個序列。

        包含yield的函數(shù)會返回一個生成器。生成器函數(shù)和普通函數(shù)看上去很像,不同的是生成器的返回值是用yield實現(xiàn)的。

        也有一種寫法是(expression for i in s if condition),注意兩邊是括號而不是方括號。這種寫法等效于

        def?a_generator():
        ????for?i?in?s:
        ????????if?condition:
        ????????????yield?expression

        我們先實現(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,而且實現(xiàn)了__iter____next__,符合迭代器協(xié)議。說明函數(shù)gen返回了一個迭代器。

        可以用next來進行迭代,迭代到最后會返回一個StopIteration的異常。

        與普通迭代器不同的是,生成器只能迭代一次。如果我們接著上面的代碼重新迭代,會發(fā)現(xiàn)沒有任何輸出。

        for?item?in?a:
        ????print(item,?end='?')

        # output:?沒有任何輸出。

        生成器與yield關鍵字息息相關,我們先來了解一下yield。

        yield的發(fā)展史

        yield in python2.3

        yield在python2.3引入。最早的功能只有一個,就是將值返回給調(diào)用方,然后停止執(zhí)行函數(shù),保存現(xiàn)場并且再下次調(diào)用時回復。對比普通的函數(shù),函數(shù)在return后就退出了。中斷然后恢復是yield的特異功能。

        yield in python2.5

        到了python2.5,yield又有了表達式的功能(詳細見:https://www.python.org/dev/peps/pep-0342/)。也就是

        a?=?yield?b

        需要注意的是,這個表達式其實有兩部分,一部分是右側的yield b,一部分是賦值操作=。按照運算優(yōu)先級來說,要先執(zhí)行yield b。因為yield的特性,返回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

        這段代碼可以用如下流程來解釋,右側帶顏色的程序塊之間會暫停,并等待新的send信號:

        yield in python3.3

        python3.3中,新加了yield from這個操作。yield from g 基本等價于 for v in g: yield v,但是內(nèi)部幫忙實現(xiàn)了很多邊界處理,比如異常等。

        明白了yield的基本操作,那么生成器有什么用呢?

        Python中生成器的作用

        這里告訴大家一個小技巧,當你不知道python某個功能有什么用的時候,可以查一下PEP(Python Enhancement Proposals),中文叫《Python增強提 案》。這里面除了重要的通知外,還有一些新功能的描述,以及為什么要設計這個功能。

        目錄為:https://www.python.org/dev/peps/ 可以按關鍵字檢索。

        拿生成器來說,首次出行在PEP255《Simple Generators》(https://www.python.org/dev/peps/pep-0255/),在Motivation一欄詳細描述了其設計的動機??梢钥闯觯善鞯脑O計初衷是要優(yōu)化生產(chǎn)者函數(shù)迭代+回調(diào)函數(shù)的場景。某些處理可能會使用生成器之前生產(chǎn)的值,會讓調(diào)用者的設計變復雜。而yield的中斷恢復機制讓迭代和調(diào)用者都變得更加自然?;谶@一功能,可以很方便的實現(xiàn) 協(xié)程操作。

        所以說使用生成器有如下的好處:

        1. 實現(xiàn)協(xié)程(Coroutine),通過yield的中斷恢復功能,可以實現(xiàn)一個線程實現(xiàn)多任務的調(diào)度。沒有了線程切換和鎖,沒有了用戶態(tài)和內(nèi)核態(tài)的切換 ,性能上會提升不少,尤其是IO場景較多的情況下。關于協(xié)程內(nèi)容較多,而且有更好的方法(async/await),有時間另開一篇講。
        2. 一般情況下生成器比迭代速度要快一些。
        3. 代碼可讀性更高。
        4. 由于本身也是個迭代器,所以也擁有迭代器的優(yōu)點:惰性計算。不需要把所有的數(shù)據(jù)加載到內(nèi)存,而是即取即用。

        不適合生成器的場景:

        1. 多次讀取。此時生成器無法滿足,可以用list(a_generator)來轉換成list
        2. 隨機讀取。生成器沒有類似x[i]這樣的下標操作。
        3. 拼接字符串。''.join(alist)''.join(a_generator)更快。

        生成器現(xiàn)實中的例子

        比如要讀取一些網(wǎng)絡日志,然后統(tǒng)計發(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ù)字累計求和。

        第一種方法,硬編碼直接實現(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

        直接實現(xiàn)很好,但是復用性較差。假如我換個任務,統(tǒng)計ip的頻次。那就要修改現(xiàn)有的邏輯,或者重寫一個類似的函數(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

        第二種邏輯清晰,可擴展性也好。但是占用內(nèi)存太恐怖。

        第三種實現(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)場的變量。

        第四種實現(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

        生成器實現(xiàn)就很舒服,可以說是綜合了第二第三種的優(yōu)點。

        第五種:走火入魔生成器

        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)用,但是可讀性并不太好,只適合簡單場景。不建議使用。

        第六種:實際工作中采用的方法

        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

        程序員總是想用最少的代碼來實現(xiàn)一個功能。對于文本處理來說,awk和sed可以滿足絕大多數(shù)的需求,而且速度比python更快。但是python勝在跨平臺,畢竟windows可沒有awk。

        你喜歡哪一種呢?





        瀏覽 16
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            91精品国产一区二区三区 | 在线天堂av | 久青草中文在线观看 | 成人免费看AAA片 | 中国女人野外做爰a正片 | 91国产丝袜在线播放 | 操大逼视频 | 国产区一区 | 小黄片在线免费 | 怡春院免费视频 |