(轉) Twisted :第十七部分 構造"回調"的另外一種方法

簡介

這部分咱們將回到"回調"這個主題.咱們將介紹另一種寫回調函數的方法,即在Twisted中使用 generators. 咱們將演示如何使用這種方法而且與使用"純" Deferreds 進行對比. 最後, 咱們將使用這種技術重寫詩歌客戶端. 但首先咱們來回顧一下generators 的工做原理,以便弄清楚它爲什麼是建立回調的候選方法.html


簡要回顧生成器

你可能知道, 一個Python生成器是一個"可重啓的函數",它是在函數體中用 yield 語句建立的. 這樣作可使這個函數變成一個"生成器函數",它返回一個"iterator"能夠用來以一系列步驟運行這個函數. 每一個迭代循環都會重啓這個函數,繼續執行到下一個yield 語句.python

生成器(和迭代器)一般被用來表明以惰性方式建立的值序列. 看一下如下文件中的代碼 inline-callbacks/gen-1.py:react

def my_generator():
    print 'starting up'
    yield 1
    print "workin'"
    yield 2
    print "still workin'"
    yield 3
    print 'done'

for n in my_generator():
    print n

這裏咱們用生成器建立了1,2,3序列. 若是你運行這些代碼,會看到在生成器上作迭代時,生成器中的 print 與循環語句中的print 語句交錯出現.git

如下自定義迭代器代碼使上面的說法更加明顯(inline-callbacks/gen-2.py):github

def my_generator():
    print 'starting up'
    yield 1
    print "workin'"
    yield 2
    print "still workin'"
    yield 3
    print 'done'

gen = my_generator()

while True:
    try:
        n = gen.next()
    except StopIteration:
        break
    else:
        print n

視做一個序列,生成器僅僅是獲取連續值的一個對象.但咱們也能夠以生成器自己的角度看問題:編程

  1. 生成器函數在被循環調用以前並無執行(使用 next 方法).服務器

  2. 一旦生成器開始運行,它將一直執行直到返回"循環"(使用 yield)app

  3. 當循環中運行其餘代碼時(如 print 語句),生成器則沒有運行.異步

  4. 當生成器運行時, 則循環沒有運行(等待生成器返回前它被"阻滯"了).ide

  5. 一旦生成器將控制交還到循環,再啓動須要等待任意可能時間(其間任意量的代碼可能被執行).

這與異步系統中的回調工做方式很是相似. 咱們能夠把 while 循環視做 reactor, 把生成器視做一系列由 yield 語句分隔的回調函數. 有趣的是, 全部的回調分享相同的局部變量名空間, 並且名空間在不一樣回調中保持一致.

進一步,你能夠一次激活多個生成器(參考例子 inline-callbacks/gen-3.py),使得它們的"回調"互相交錯,就像在Twisted系統中獨立運行的異步程序.

然而,這種方法仍是有一些欠缺.回調不只僅被 reactor 調用, 它還能接受信息.做爲 deferred 鏈的一部分,回調要麼接收Python值形式的一個結果,要麼接收 Failure 形式的一個錯誤.

從Python2.5開始,生成器功能被擴展了.當你再次啓動生成器時,能夠給它發送信息,如 inline-callbacks/gen-4.py 所示:

class Malfunction(Exception):
    pass

def my_generator():
    print 'starting up'

    val = yield 1
    print 'got:', val

    val = yield 2
    print 'got:', val

    try:
        yield 3
    except Malfunction:
        print 'malfunction!'

    yield 4

    print 'done'

gen = my_generator()

print gen.next() # start the generator
print gen.send(10) # send the value 10
print gen.send(20) # send the value 20
print gen.throw(Malfunction()) # raise an exception inside the generator

try:
    gen.next()
except StopIteration:
    pass

在Python2.5之後的版本中, yield 語句是一個計算值的表達式.從新啓動生成器的代碼可使用 send 方法代替 next 決定它的值(若是使用 next 則值爲 None), 並且你還能夠在迭代器內部使用 throw 方法拋出任何異常. 是否是很酷?


內聯回調

根據咱們剛剛回顧的能夠向生成器發送值或拋出異常的特性,能夠設想它是像 deferred 中的一系列回調,便可以接收結果或錯誤. 每一個回調被 yield 分隔,每個 yield 表達式的值是下一個回調的結果(或者 yield 拋出異常表示錯誤).圖35顯示相應概念:

_static/p17_generator-callbacks1.png

圖35:做爲回調序列的生成器

如今一系列回調以 deferred 方式被連接在一塊兒,每一個回調從它前面的回調接收結果.生成器很容易作到這一點——當再次啓動生成器時,僅僅使用 send 發送上一次調用生成器的結果( yield 產生的值).但這看起來有點笨,既然生成器從開始就計算這個值,爲何還須要把它發送回來? 生成器能夠將這個值儲存在一個變量中供下一次使用. 所以這究竟是爲何呢?

回憶一下咱們在第十三部分 中所學, deferred 中的回調還能夠返回 deferred 自己. 在這種狀況下, 外層的 deferred 先暫停等待內層的 deferred 激發,接下來外層 deferred 鏈使用內層 deferred 的返回結果(或錯誤)激發後續的回調(或錯誤回調).

因此設想咱們的生成器生成一個 deferred 對象而不是一個普通的Python值. 這時生成器會自動"暫停";生成器老是在每一個yield 語句後暫停直到被顯示的重啓.於是咱們能夠延遲它的重啓直到 deferred 被激發, 屆時咱們會使用 send 方法發送值(若是 deferred 成功)或者拋出異常(若是 deferred 失敗).這就使咱們的生成器成爲一個真正的異步回調序列,這正是twisted.internet.defer 中 inlineCallbacks 函數背後的概念.


進一步討論內聯回調

考慮如下例程, 位於 inline-callbacks/inline-callbacks-1.py:

from twisted.internet.defer import inlineCallbacks, Deferred

@inlineCallbacks
def my_callbacks():
    from twisted.internet import reactor

    print 'first callback'
    result = yield 1 # yielded values that aren't deferred come right back

    print 'second callback got', result
    d = Deferred()
    reactor.callLater(5, d.callback, 2)
    result = yield d # yielded deferreds will pause the generator

    print 'third callback got', result # the result of the deferred

    d = Deferred()
    reactor.callLater(5, d.errback, Exception(3))

    try:
        yield d
    except Exception, e:
        result = e

    print 'fourth callback got', repr(result) # the exception from the deferred

    reactor.stop()

from twisted.internet import reactor
reactor.callWhenRunning(my_callbacks)
reactor.run()

運行這個例子能夠看到生成器運行到最後並終止了 reactor, 這個例子展現了 inlineCallbacks 函數的不少方面.首先,inlineCallbacks 是一個修飾符,它老是修飾生成器函數,如那些使用 yield 語句的函數. inlineCallbacks 的所有目的是將一個生成器按照上述策略轉化爲一系列異步回調.

第二,當咱們調用一個用 inlineCallbacks 修飾的函數時,不須要本身調用 send 或 throw 方法.修飾符會幫助咱們處理細節,並確保生成器運行到結束(假設它不拋出異常).

第三,若是咱們從生成器生成一個非延遲值,它將以 yield 生成的值當即重啓.

最後,若是咱們從生成器生成一個 deferred,它不會重啓除非此 deferred 被激發.若是 deferred 成功返回,則 yield 的結果就是 deferred 的結果.若是 deferred 失敗了,則 yield 會拋出異常. 注這個異常僅僅是一個普通的 Exception 對象,而不是Failure,咱們能夠在 yield 外面用 try/except 塊捕獲它們.

在上面的例子中,咱們僅用 callLater 在一小段時間以後去激發 deferred.雖然這是一種將非阻塞延遲放入回調鏈的實用方法,但一般咱們會生成一個 deferred,它是被生成器中其餘的異步操做(如 get_poetry)返回的.

OK,如今咱們知道了 inlineCallbacks 修飾的函數是如何運行的,但當你實際調用時會返回什麼值呢?正如你認爲的,將獲得deferred.因爲不能確切地知道生成器什麼時候中止(它可能生成一個或多個 deferred),裝飾函數自己是異步的,因此 deferred 是一個合適的返回值.注:這個返回的 deferred 不是生成器中 yield 生成的 deferred.相反,它在生成器徹底結束(或拋出異常)後才被激發.

若是生成器拋出一個異常,那麼返回的 deferred 將激發它的錯誤回調鏈,把異常包含在一個 Failure 中. 可是若是咱們但願生成器返回一個正常值,必須使用 defer.returnValue 函數. 就像普通 return 語句同樣,它也會終止生成器(實際會拋出一個特殊異常).例子 inline-callbacks/inline-callbacks-2.py 說明了這兩種可能.


客戶端7.0

讓咱們在新版本的詩歌客戶端中加入 inlineCallbacks,你能夠在 twisted-client-7/get-poetry.py 中查看源代碼.也許你須要與客戶端6.0—— twisted-client-6/get-poetry.py 進行對比,它們的相對變化位於 poetry_main:

def poetry_main():
    addresses = parse_args()

    xform_addr = addresses.pop(0)

    proxy = TransformProxy(*xform_addr)

    from twisted.internet import reactor

    results = []

    @defer.inlineCallbacks
    def get_transformed_poem(host, port):
        try:
            poem = yield get_poetry(host, port)
        except Exception, e:
            print >>sys.stderr, 'The poem download failed:', e
            raise

        try:
            poem = yield proxy.xform('cummingsify', poem)
        except Exception:
            print >>sys.stderr, 'Cummingsify failed!'

        defer.returnValue(poem)

   def got_poem(poem):
       print poem

   def poem_done(_):
       results.append(_)
       if len(results) == len(addresses):
           reactor.stop()

   for address in addresses:
       host, port = address
       d = get_transformed_poem(host, port)
       d.addCallbacks(got_poem)
       d.addBoth(poem_done)

   reactor.run()

在這個新版本里, inlineCallbacks 生成函數 get_transformed_poem 負責取回詩歌而且應用變換(經過變換服務).因爲這兩個操做都是異步的,咱們每次生成一個 deferred 而且隱式地等待結果.與客戶端6.0同樣,若是變換失敗則返回原始詩歌.咱們可使用 try/except 語句捕獲生成器中的異步錯誤.

咱們以先前的方式測試新版客戶端. 首先啓動一個變換服務:

python twisted-server-1/tranformedpoetry.py --port 10001

而後啓動兩個詩歌服務器:

python twisted-server-1/fastpoetry.py --port 10002 poetry/fascination.txt
python twisted-server-1/fastpoetry.py --port 10003 poetry/science.txt

如今能夠運行新的客戶端:

python twisted-client-7/get-poetry.py 10001 10002 10003

試試關閉一個或多個服務器,看一看客戶端如何捕獲錯誤.


討論

就像 Deferred 對象, inlineCallbacks 函數給咱們一種組織異步回調的新方式.同時,如同使用 deferred,inllineCallbacks 沒有改變遊戲規則.特別地,咱們的回調仍然一次調用一個回調,它們仍然被 reactor 調用.咱們能夠經過打印內聯回調的回溯跟蹤信息來證明這一點,參見腳本 inline-callbacks/inline-callbacks-tb.py.運行此代碼你將首先得到一個關於 reactor.run() 的回溯,而後是許多幫助函數信息,最後是咱們的回調.

圖29解釋了當 deferred 中一個回調返回另外一個 deferred 時會發生什麼,咱們調整它來展現當一個 inlineCallbacks 生成器生成一個 deferred 時會發生什麼,參考圖36:

_static/p17_inline-callbacks1.png

圖36: inlineCallbacks 函數中的控制流

一樣的圖對兩種狀況都適用,由於它們表示的想法都是同樣的 —— 一個異步操做正在等待另外一個.

因爲 inlineCallbacks 和 deferred 解決許多相同的問題,在它們之間如何選擇呢?下面列出一些 inlineCallbacks 的潛在優點.

  • 因爲回調分享相同的名空間,所以沒有必要傳遞額外狀態.

  • 回調的順序很容易看到,由於它老是從上到下執行.

  • 節省了每一個回調函數的聲明和隱式控制流,一般減小輸入.

  • 可使用熟悉的 try/except 語句處理錯誤.

固然也存在一些缺陷:

  • 生成器中的回調不能被單獨調用,這使代碼重用比較困難.而構造 deferred 的代碼則可以以任意順序自由地添加任何回調.

  • 生成器的緊緻性可能混淆一個事實,其實異步回調很是晦澀.儘管生成器看起來像一個普通的函數序列,可是它的行爲卻很是不同. inlineCallbacks 函數不是一種避免學習異步編程模型的方式.

就像任何技術,實踐將積累出必要的經驗,幫你作出明智選擇.


總結

在這個部分,咱們學習了 inlineCallbacks 裝飾符以及它怎樣使咱們可以以Python生成器的形式表達一系列異步回調.

在 第十八部分 中,咱們將學習一種管理 一組 "並行"異步操做的技術.


參考練習

  1. 爲何 inlineCallbacks 函數是複數(形式)?

  2. 研究 inlineCallbacks 的實現以及它們幫助函數 _inlineCallbacks. 並思考短語"魔鬼在細節處".

  3. 有N個 yield 語句的生成器中包含多少個回調,假設其中沒有循環或者 if 語句?

  4. 詩歌客戶端7.0可能同時運行三個生成器.概念上,它們之間有多少種不一樣的交錯方式?考慮詩歌客戶端和 inlineCallbacks的實現,你認爲實際有多少種可能?

  5. 把客戶端7.0中的 got_poem 放入到生成器中.

  6. 把 poem_done 回調放入生成器.當心!確保處理全部失敗狀況以便不管怎樣 reactor 都會關閉.與使用 deferred 關閉reactor 對比代碼有何不一樣?

  7. 一個在 while 循環中使用 yield 語句的生成器表明一個概念上的無限序列.那麼一樣的裝飾有 inlineCallbacks 的生成器又表明什麼呢?

相關文章
相關標籤/搜索