詳細介紹一下:Python 的關鍵字 yield 的用法和用途!小白都懂了

yield 的用法有以下四種常見的情況:一個是生成器,二是用於定義上下文管理器,三是協程,四是配合 from 形成 yield from 用於消費子生成器並傳遞消息。這四種用法,其實都源於 yield 所具有的暫停的特性,也就說程序在運行到 yield 所在的位置 result = yield expr 時,先執行 yield expr 將產生的值返回給調用生成器的 caller,然後暫停,等待 caller 再次激活並恢復程序的執行。而根據恢復程序使用的方法不同,yield expr 表達式的結果值 result 也會跟著變化。如果使用 __next()__ 來調用,則 yield 表達式的值 result 是 None;如果使用 send() 來調用,則 yield 表達式的值 result 是通過 send 函數傳送的值。下面是官方文檔介紹 yield 表達式時的一個例子,能夠很好地說明關鍵字 yield 的特性和用法:

詳細介紹一下:Python 的關鍵字 yield 的用法和用途!小白都懂了

<code>非誠勿擾:正在學習python的小夥伴或者打算學習的,可以私信小編“01”領取資料!/<code>

上面這段代碼的說明如下圖所示:

詳細介紹一下:Python 的關鍵字 yield 的用法和用途!小白都懂了

  1. 執行第一個 next(generator) 的時候,也就是預激活生成器,生成器開始執行,打印 Begin... 字符串,執行到 value = (yield value) 的位置時,首先調用 yield value 產生數字 1,然後生成器在 yield 的位置暫停。
  2. 接著調用第 2 個 next(generator) 的時候,生成器恢復執行,由於使用 next() 來調用生成器函數, value 的值會變成 None ,因此生成器函數繼續執行到 yield value 時,會將 value 的值 None 返回給解釋器,然後再次暫停。
  3. 接著使用 send(2) 方法繼續調用生成器,value 接收到傳入的數字 2,繼續到執行 value = (yield value) ,將數字 2 返回給解釋器後暫停。
  4. 此後,解釋器再次通過 throw(TypeError, "spam") 方法調用,生成器恢復執行,並拋出異常,生成器捕獲到異常,並將異常 TypeError('spam') 賦值給變量 value,然後程序再次執行到 value = (yield value) ,將 TypeError('spam') 返回給解釋器。
  5. 最後,程序調用 close() 方法,在生成器函數的位置拋出 GeneratorExit ,異常被拋出,生成器正常退出,並最終執行最外層 try 語句對應的 finally 分支,打印輸出 Clean up。

生成器

不出意外,你最先遇到 yield 一定會是一個生成器函數里面。生成器是一個用於不斷生成數字或者其他類型的值的函數,可以通過 for 循環或者 next() 函數逐一調用。這裡需要強調的是,生成器包含的是一個沒有賦值的 yield 表達式,所以下面兩種形式是等價的:


詳細介紹一下:Python 的關鍵字 yield 的用法和用途!小白都懂了

這裡之所以強調第二種形式,是為了在理解通過 send() 方法發送 value 時,能夠更好地理解 yield。同時,也能夠更正確地說明,調用生成器返回的值是 yield 關鍵字右邊的表達式 i + 1 的值,而不是 yield 表達式本身的結果值。

我們試著調用一下:

詳細介紹一下:Python 的關鍵字 yield 的用法和用途!小白都懂了

上下文管理器

配合 Python 的 contexlib 模塊裡的 @contextmanager 裝飾器,yield 也可以用於定義上下文管理器,下面是 Python Tricks 書中的一個例子

詳細介紹一下:Python 的關鍵字 yield 的用法和用途!小白都懂了

上面通過裝飾器和 yield 關鍵字定義的上下文管理器和下面類的方法定義等同:

詳細介紹一下:Python 的關鍵字 yield 的用法和用途!小白都懂了

可以利用下面的方法分別進行調用:

詳細介紹一下:Python 的關鍵字 yield 的用法和用途!小白都懂了

協程

協程的概念充滿了美感,非常符合人的辦事模式,想要完全掌握卻還是需要花費一些功夫。不過這些功夫是值得的,因為有時多線程所帶來的麻煩會遠遠比協程多。下面是 Python Cookbook 中的一個只用 yield 表達式編寫的協程實例:

<code>from collections import deque# Two simple generator functionsdef countdown(n):    while n > 0:        print('T-minus', n)        yield        n -= 1    print('Blastoff!')def countup(n):    x = 0    while x < n:        print('Counting up', x)        yield        x += 1class TaskScheduler:    def __init__(self):        self._task_queue = deque()    def new_task(self, task):        '''        Admit a newly started task to the scheduler        '''        self._task_queue.append(task)    def run(self):        '''        Run until there are no more tasks        '''        while self._task_queue:            task = self._task_queue.popleft()            try:                # Run until the next yield statement                next(task)                self._task_queue.append(task)            except StopIteration:                # Generator is no longer executing                pass# Example usesched = TaskScheduler()sched.new_task(countdown(2))sched.new_task(countup(5))sched.run()/<code>

運行上面的腳本,可以得到以下輸出:

詳細介紹一下:Python 的關鍵字 yield 的用法和用途!小白都懂了

countdown 和 countup 兩個任務交替執行,主程序在執行到 countdown 函數的 yield 表達式時,暫停後將被重新附加到隊列裡面。然後,countup 任務從隊列中取了出來,並開始執行到 yield 表達式的地方後暫停,同樣將暫停後的協程附加到隊列裡面,接著從隊列裡取出最左邊的任務 countdown 繼續執行。重複上述過程,直到隊列為空。

上面的協程可以利用 Python3.7 中的 asyncio 庫改寫為:

詳細介紹一下:Python 的關鍵字 yield 的用法和用途!小白都懂了

可以看到利用 asyncio 庫編寫的協程示例比用 yield 來編寫的協程要優雅地多,也簡單地多,更容易被人理解。

yield from

說實話,yield from 實在有點令人費解,讓人摸不著頭腦。yield from 更多地被用於協程,而 await 關鍵字的引入會大大減少 yield from 的使用頻率。yield from 一方面可以迭代地消耗生成器,另一方面則建立了一條雙向通道,可以讓調用者和子生成器便捷地通信,並自動地處理異常,接收子生成器返回的值。下面是 Python Cookbook 書裡的一個例子,用於展開嵌套的序列:

詳細介紹一下:Python 的關鍵字 yield 的用法和用途!小白都懂了

而 yield from 用於建立雙向通道的用法則可以參考 Fluent Python 裡例子[6],這裡就不詳細地解釋這段代碼:

<code># BEGIN YIELD_FROM_AVERAGERfrom collections import namedtupleResult = namedtuple('Result', 'count average')# the subgeneratordef averager():    total = 0.0    count = 0    average = None    while True:        term = yield        if term is None:            break        total += term        count += 1        average = total/count    return Result(count, average)# the delegating generatordef grouper(results, key):    while True:        results[key] = yield from averager()# the client code, a.k.a. the callerdef main(data):    results = {}    for key, values in data.items():        group = grouper(results, key)        next(group)        for value in values:            group.send(value)        group.send(None)    report(results)# output reportdef report(results):    for key, result in sorted(results.items()):        group, unit = key.split(';')        print(f'{result.count:2} {group:5} averaging {result.average:.2f}{unit}')data = {    'girls;kg':        [40.9, 38.5, 44.3, 42.2, 45.2, 41.7, 44.5, 38.0, 40.6, 44.5],    'girls;m':        [1.6, 1.51, 1.4, 1.3, 1.41, 1.39, 1.33, 1.46, 1.45, 1.43],    'boys;kg':        [39.0, 40.8, 43.2, 40.8, 43.1, 38.6, 41.4, 40.6, 36.3],    'boys;m':        [1.38, 1.5, 1.32, 1.25, 1.37, 1.48, 1.25, 1.49, 1.46],}if __name__ == '__main__':    main(data)/<code>

可能對於熟練掌握 Python 的程序員來說,yield 和 yield from 相關的語法充滿了美感。但對於剛入門的我來說,除了生成器語法讓我感覺到了美感,其他的語法都讓我理解起來很是費解。不過還好,asyncio 庫融入了 Python 的標準庫裡,關鍵字 async 和 await 的引入,將會讓我們更少地在編寫協程時去使用 yield 和 yield from。 但不管怎麼樣,yield 都是 Python 裡非常特別的一個關鍵字,值得花時間好好掌握瞭解。

結尾

最後多說一句,小編是一名python開發工程師,這裡有我自己整理了一套最新的python系統學習教程,包括從基礎的python腳本到web開發、爬蟲、數據分析、數據可視化、機器學習等。想要這些資料的可以關注小編,並在後臺私信小編:“01”即可領取。


分享到:


相關文章: