[Twisted] Part 17: Just Another Way to Spell “Callback”

本文由Dave的Part 17: Just Another Way to Spell “Callback”翻譯而成,你可以由Part 1開始閱讀這個系列的文章,也可以在這裡找到整個系列的目錄。

Introduction

在這個章節中我們將回到callback這個主題。我們將介紹另一種在Twisted中撰寫callback的技術,那就是使用生成器(generator)。我們會展示這種技術如何運作,並把它與使用「純」Deferreds的技術進行對比。最後,我們將使用這種技術重寫我們的詩歌用戶端。但首先讓我們回顧一下生成器是如何運作的,以便我們可以了解為什麼它是建立callback的候選方法。

A BRIEF REVIEW OF GENERATORS

你可能知道,Python生成器是一種「可重新啟動的函式」,你可以藉由在你的函式主體中使用yield表達式來建立。透過這種做法,該函示成為一個「生成器函式」,它會回傳一個你可以用來以一系列的步驟來執行這個函式的迭代器(iterator)迭代器的每個循環都會重新啟動這個函式,這個函式會繼續執行直到它到達下一個yield。(譯註:如果這個生成器函式有多個yeild,第一個迴圈會執行到第一個yield,第二次迴圈會從第一個yield之後開始執行到第二個yield,並以此類推,所以前面才會說這是「一系列的步驟」,因為它是一段一段在執行的)

生成器(和迭代器)通常用來表示以惰性建立(lazily-created)的數值序列(譯註:關於惰性可以參考一下維基的Lazy evaluation)。看一下inline-callbacks/gen-1.py中的範例:
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 statement與for迴圈的print statement交錯出現。

我們可以透過建立我們自己的生成器,讓這些程式碼對於上述內容表現得更明確(inline-callbacks/gen-2.py):
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)。
  3. 當迴圈執行其他程式碼時(如print statement),生成器不會執行。
  4. 當生成器執行時,迴圈不會執行(迴圈會被「阻塞」著等待生成器)。
  5. 一旦生成器用yield把控制權交給迴圈,直到生成器再次執行,可能經過了任意長度的時間(並且可能執行了任意數量的程式碼)。

這很像callback在非同步系統中工作的方式。我們可以把while迴圈視為reactor,而生成器視為一系列由yield statement分隔的callback,有趣的是,所有callbacks分享相同的區域變數命名空間,而這個命名空間會從一個callback持續存在到下一個callback。

此外,你可以同時啟動多個生成器(參閱inline-callbacks/gen-3.py的範例),它們的「callbacks」會互相交錯,就像你可以在像Twisted這樣的系統中執行獨立的非同步任務一樣。

但是仍然缺少一些東西。Callbacks不僅是被reactor呼叫,它們也接收資訊。當作為deferred實例中鏈的一部分時,callback要不就接收單一Python值形式的結果,或者接收以Failure物件形式的錯誤。

從Python 2.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

在Python 2.5與之後的版本中,yield statement是一個求值的表達式。而重新啟動生成式的程式碼可以使用send方法代替next方法(如果你使用next方法則該數值為None)來決定這些數值。更重要的是,你可以在生成器中使用throw方法來實際的拋出任意例外。很酷對吧?

Inline Callbacks

根據我們剛才回顧的向生成器send數值與throw例外,我們可以將生成器想像為一系列在deferred實例中接收成功結果或失敗結果的callback。這些callbacks被yield分隔,而每個yield表達式的值是給下一個callback的成功結果(或者yield拋出例外的話就是給下一個callback的失敗結果)。圖35顯示了相應的概念:
35.generator%2Bas%2Ba%2Bcallback%2Bsequence.png-Part 17: Just Another Way to Spell “Callback”

圖35.生成器作為callback序列


現在,當一系列callback在deferred實例中鏈在一起時,每一個callback都會從前一個callback中接收結果。這對生成器來說很容易做到-只要在下次重新啟動生成器時,send上次執行生成器時你所得到的值(yield產生的值)。但這似乎有點傻。既然生成器原本就計算了這個值,為什麼還要把它發送回來?生成器可以儲存這個值在一個變數中供下一次使用。這麼做的意義在哪?

回憶我們在Part 13學到的,deferred實例中的callbacks可以回傳deferreds本身。當這種情況發生時,外部deferred會暫停直到內部deferred觸發,然後以內部deferred的成功結果(或失敗結果)呼叫下一個外層的deferred鏈的callback(或errback)。

所以假設我們的生成器yield一個deferred物件而不是普通的Python值。此時生成器會自動「暫停」;生成器總會在每個yield statement後被暫停,直到它們被明確的重新啟動。因此我們可以延遲重新啟動生成器,直到deferred觸發,到時候我們要不是send數值(如果deferred實例成功),就是throw例外(如果deferred實例失敗)。這將使我們的生成器成為真正的非同步callbacks的序列,這就是twisted.internet.defer中inlineCallbacks函式的概念。

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的目的是根據我們上述的方案,將生成器轉換成一系列的非同步callbacks。

第二,當我們調用inlineCallbacks裝飾器函式時,我們不需要自己呼叫next、send、或者throw。裝飾器會為我們處理這些細節,並確保生成器執行到最後(假設它不拋出例外)。

第三,如果我們從生成器yield一個非deferred值,生成器會立即以與yield結果相同的值來重新啟動。

最後,如果我們從生成器中yield deferred實例,直到deferred觸發之前生成器都不會被重新啟動。如果deferred實例結果是成功,yield的結果就是deferred實例的結果。如果deferred實例結果是失敗,yield statement會拋出例外。注意到例外只是一個普通的Exception物件而不是Failure物件,我們可以在yield表達式外用try/except statement捕捉它。

在上面的範例中,我們只是使用callLater在一小段時間之後觸發deferred實例。雖然這是一種在我們callback鏈中放入非阻塞延遲的便利辦法,通常我們會yield deferred實例,它是由我們生成器調用的某些非同步操作(即get_poetry)回傳的。

好,現在我們知道inlineCallbacks裝飾器函式如何執行的,但如果你實際呼叫它你會得到什麼回傳值?就像你猜的,你得到deferred實例。由於我們無法準確的知道生成器什麼時候會停止執行(它可能yield一到多個deferreds),所以裝飾器函式本身是非同步的,而deferred實例是合適的回傳值。注意回傳的deferred實例不是生成器可能yield的其中一個deferred實例。相反的,這個deferred實例只在生成器完全完成(或拋出例外)之後才觸發。

如果生成器拋出例外,回傳的deferred實例會以Failure包裹這個例外來觸發它的errback鏈。但如果我們想要生成器回傳一個一般的數值,我們必須使用defer.returnValue函式來「回傳」它。像普通return statement,defer.returnValue也會停止生成器(它實際上拋出了一個特殊的例外)。inline-callbacks/inline-callbacks-2.py的範例說明了這兩種可能性。

Client 7.0

讓我們把inlineCallbacks與新版本的詩歌用戶端一起使用。你可以在twisted-client-7/get-poetry.py看到程式碼。你可能會希望與twisted-client-6/get-poetry.py的6.0版本用戶端進行比較。有關的改變在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 statement來處理生成器內部的非同步錯誤。

我們可以像以前一樣測試新的用戶端。首先啟動轉換伺服器:
python twisted-server-1/transformedpoetry.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

嘗試關閉一個或多個伺服器來查看用戶端如何處理錯誤。

Discussion

與Deferred物件一樣,inlineCallbacks函式為我們提供了組織非同步callback的新方法。而且與deferreds一樣,inllineCallbacks不會改變遊戲規則。具體來說,我們的callbacks仍然一次執行一個,並且它們仍然被reactor調用。我們可以透過從inline callback中印出traceback這種我們常用的方法來確認這件事,例如inline-callbacks/inline-callbacks-tb.py這個範例script。執行這個程式碼你會得到一個traceback,最上面有reactor.run(),中間有很多輔助函式,而我們的callback在最底部。

圖29解釋了當deferred實例中的callback回傳另一個deferred時會發生什麼事情,我們可以調整它來顯示當inlineCallbacks裝飾的生成式yield deferred實例會發生什麼事情。看看圖36:
36.%2Bflow%2Bcontrol%2Bin%2Ban%2BinlineCallbacks%2Bfunction.png-Part 17: Just Another Way to Spell “Callback”

圖36. inlineCallbacks函式的流程控制


同樣的圖在兩種案例中都可以運作,因為闡述的想法式一樣的-一個非同步的操作正在等待另一個。

由於inlineCallbacks與deferred解決了許多相同的問題,要如何在它們之間選擇呢?以下是一些inlineCallbacks的潛在優勢:

  • 由於callbacks分享命名空間,因此不需要傳遞額外的狀態。
  • 更容易看出callback的順序,因為它們就是從上到下的執行。
  • 由於沒有對於個別callback的函式宣告與隱式流程控制,通常會減少輸入(譯註:指撰寫程式碼)。
  • 使用熟悉的try/except statement處理錯誤。

而這裡有些潛在的陷阱:

  • 生成器內部的callbacks不能被個別調用,這會使程式碼重用變的困難。使用deferred實例時,建構deferred實例的程式碼可以以任意順序自由的加入任意callbacks。
  • 生成器緊湊的形式可能掩蓋了非同步callback實際上是很複雜的這個事實。僅管生成器外表看起來像普通的序列函式,但它的行為卻非常不同。使用inlineCallbacks函式不是避免學習非同步程式設計模型的一種方法。

與任何技術一樣,練習會提供做出明智選擇所需的經驗。

Summary

在這個章節我們學習了inlineCallbacks裝飾器,以及如何讓我們以Python生成器的形式來表達一系列的非同步callback。

Part 18我們學習一種管理一組「並行(parallel)」的非同步操作的技術。

Suggested Exercises

  1. 為什麼inlineCallbacks函式使用複數的「Callbacks」
  2. 研究inlineCallbacks的實現與它的輔助函式_inlineCallbacks。思考「魔鬼藏在細節裡」這句話。
  3. 假設沒有迴圈或if statement,有N個yield statement的生成器包含了幾個callbacks?
  4. 7.0版本用戶端可能同時有三個生成器在執行。概念上來說,它們之間可以有多少種不同的交錯方式?考慮到在詩歌用戶端中它們被調用的方式與inlineCallbacks的實現,你認為有幾種方法是真的可行的?
  5. 把7.0版本用戶端的got_poem callback搬到生成器中。
  6. 接著把poem_done搬到生成器中。小心!確認處理到所有錯誤的狀況,讓reactor無論如何都會關閉。程式碼與使用deferred實例關閉reactor相比起來如何?
  7. 在while迴圈中具有yield statement的生成器可以表示為一個概念上的無限序列。用inlineCallbacks來裝飾這樣的生成器代表了什麼?

留言