非同步程式設計之使用yield from

公號_python學習開發發表於2018-12-12


yield from 是 Python3.3 後新加的語言結構。yield from的主要功能是開啟雙向通道,把最外層的呼叫方法與最內層的子生成器連線起來。這兩者就可以進行傳送值和返回值了,yeild from結構的本質是簡化巢狀的生產器,不理解這個是什麼意思的話,下面我將用幾個例子來對其使用方法進行講解。

簡化for迴圈中的yeild

首先看一個

def gene():
    for c in 'AB':
        yield c  #遇到yeild程式返回迴圈,下次從yeild後面開始。
    for i in range(3):
        yield i 
if __name__=="__main__":
    list(gene())#list內部會預激生成器
複製程式碼

輸出

['A','B','0','1', '2']
複製程式碼

上面的程式碼可以簡寫成

def gene():
     yield from 'ab' 
     yield from range(3)
if __name__=="__main__":
    list(gene()) 
複製程式碼

通過上面的程式碼我們可以知道,yield from 可以簡化for迴圈裡的yield表示式。當然yeild from的功能不僅僅是可以簡化for迴圈而已,要是這樣的話也就不值得,單獨寫一篇文章來介紹了。

我們仔細觀察,簡化後的式子有兩個yeild from,同樣的也就是說如果有10個for迴圈的yeild生成式,我們需要寫10個yeild from,此時我們要記得在python中如果重複的程式碼出現了兩次以及以上就該考慮優化了。好了接下來我們看一個優化後的例子。

通過yield from連結可迭代物件

def chain(*args):
    for i in args:
        # for m in i:
        #  yield m
        yield from i
p = list(chain("1234", "AB", [1, 2, 3, 4, 5]))
print(p)
複製程式碼

輸出

['1', '2', '3', '4', 'A', 'B', 1, 2, 3, 4, 5]
複製程式碼

這裡對之前的例子做了個優化處理,通過*args可變引數,配合後面的for迴圈進行了多個可迭代物件的連結處理。下面來看一個複雜點的例子:(來自Python cookbook 3 ,github原始碼地址 https://github.com/dabeaz/python-cookbook/blob/master/src/4/how_to_flatten_a_nested_sequence/example.py)

扁平化處理巢狀型的資料

# Example of flattening a nested sequence using subgenerators

from collections import Iterable

def flatten(items, ignore_types=(str, bytes)):
    for x in items:
        if isinstance(x, Iterable) and not isinstance(x, ignore_types):
            yield from flatten(x)
        else:
            yield x

items = [1, 2, [3, 4, [5, 6], 7], 8]

# Produces 1 2 3 4 5 6 7 8
for x in flatten(items):
    print(x)

items = ['Dave', 'Paula', ['Thomas', 'Lewis']]
for x in flatten(items):
    print(x)
複製程式碼

接下來通過說一下開篇提到的子生產器和呼叫方以及新的詞委託生成器。

瞭解幾個概念

yield from x 表示式對x物件做的第一件事是,呼叫 iter(x),從中獲取一個迭代器。所以x是可迭代物件。上面的例子中的x如果是可迭代物件就會執行,yield from flatten(x).

PEP380 的標題是 ”syntax for delegating to subgenerator“(把指責委託給子生成.器的句法)。由此我們可以知道,yield from是可以實現巢狀生成器的使用。

yield from在看接下來的程式碼之前我們必須知道這幾個概念:

委派生成器

包含yield from 表示式的生成器函式

子生成器

從yield from 部分獲取的生成器,含義yield的。

呼叫方

呼叫委派生成器的客戶端(呼叫方)程式碼,也就是執行入口。

ok,瞭解了這些我們看接下來的一個例子。

使用yeild from寫一個非同步爬蟲

import requests
from collections import namedtuple  ①

Response = namedtuple("rs", 'url status') ②


# 子生產器
def fecth(): ③
    res=[]
    while 1:
        url = yield ④
        if url is None: ⑤
            break
        req = requests.get(url)
        res.append(Response(url=url, status=req.status_code))
    return res

#委派生成器
def url_list(l, key):
    while 1: ⑥
        l[key] = yield from fecth() ⑦

#呼叫方
def main():
    l = {}
    u = ["http://www.baidu.com", "http://www.cnblogs.com"]
    for index, url in enumerate(u):
        if index == 0:
            ul = url_list(l, index)
            next(ul) ⑧
        ul.send(url)⑨
    ul.send(None)⑩
    return l


if __name__ == '__main__':
    res = main()
    print(res)
複製程式碼

接下來對上面的標準進行解釋:
① 引入一個具名元組,可以後面實現一個簡單的類。
② 對請求引數做一個格式化處理,後面通過獲取屬性即可。
③一個協程,通過requests模組可以發起網路請求。
④main函式的傳送的值繫結到這裡的url上
⑤ url為None即沒有url的時候結束迴圈的。
⑥這個迴圈每次都會新建一個fetch 例項,每個例項都是作為協程使用的生成器物件。
⑦ url_list傳送的每個值都會經由yield from 處理,然後傳給fetch 例項。url_list會在yield from表示式處暫停,等待fetch例項處理客戶端發來的值。fetch例項執行完畢後,返回的值繫結到l[key] 上。while 迴圈會不斷建立fetch例項,處理更多的值。
⑧啟用url_list生成器
⑨把各個url以及其序列號index,傳給url_list傳入的值最終到達fetch函式中,url_list並不知道傳入的是什麼,同時url_list例項在yield from處暫停。直到fetch的一個例項處理完才進行賦值。
⑩關鍵的一步,# 把None傳入url_list,傳入的值最終到達fetch函式中,導致當前例項終止。然後繼續建立下一個例項。如果沒有ul.send(None),那麼fetch子生成器永遠不會終止,因為ul.send()傳送的值實際是在fetch例項中進行,委派生成器也永遠不會在此啟用,也就不會為l[key]賦值

參考資料:

流暢的python 第16章
PEP 380-- Syntax for Delegating to a Subgenerator
How Python 3.3 "yield from" construct works



相關文章