[Twisted] Part 17: Just Another Way to Spell “Callback”
本文由Dave的Part 17: Just Another Way to Spell “Callback”翻譯而成,你可以由Part 1開始閱讀這個系列的文章,也可以在這裡找到整個系列的目錄。
生成器(和迭代器)通常用來表示以惰性建立(lazily-created)的數值序列(譯註:關於惰性可以參考一下維基的Lazy evaluation)。看一下inline-callbacks/gen-1.py中的範例:
這裡我們有一個建立了序列1、2、3的生成器。如果你執行程式碼,當迴圈的循環遍歷生成器時,你會看到生成器的print statement與for迴圈的print statement交錯出現。
我們可以透過建立我們自己的生成器,讓這些程式碼對於上述內容表現得更明確(inline-callbacks/gen-2.py):
被視為一個序列,生成器只是為了取得連續數值的一個物件。但我們也可以從生成器本身的角度來看事情:
這很像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中所示:
在Python 2.5與之後的版本中,yield statement是一個求值的表達式。而重新啟動生成式的程式碼可以使用send方法代替next方法(如果你使用next方法則該數值為None)來決定這些數值。更重要的是,你可以在生成器中使用throw方法來實際的拋出任意例外。很酷對吧?
現在,當一系列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函式的概念。
執行這個範例你會看到生成器執行到最後並終止了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的範例說明了這兩種可能性。
在我們的新版本中,inlineCallbacks裝飾的生成器函式get_transformed_poem負責取得詩歌然後套用轉換(透過轉換服務)。由於這兩個操作都是非同步的,我們每次都會產生deferred實例,然後(隱性的)等待結果。與6.0版本用戶端一樣,如果轉換失敗我們就回傳原始詩歌。注意我們可以使用try/except statement來處理生成器內部的非同步錯誤。
我們可以像以前一樣測試新的用戶端。首先啟動轉換伺服器:
然後啟動兩個詩歌伺服器:
現在你可以執行新的用戶端:
嘗試關閉一個或多個伺服器來查看用戶端如何處理錯誤。
圖29解釋了當deferred實例中的callback回傳另一個deferred時會發生什麼事情,我們可以調整它來顯示當inlineCallbacks裝飾的生成式yield deferred實例會發生什麼事情。看看圖36:
同樣的圖在兩種案例中都可以運作,因為闡述的想法式一樣的-一個非同步的操作正在等待另一個。
由於inlineCallbacks與deferred解決了許多相同的問題,要如何在它們之間選擇呢?以下是一些inlineCallbacks的潛在優勢:
而這裡有些潛在的陷阱:
與任何技術一樣,練習會提供做出明智選擇所需的經驗。
在Part 18我們學習一種管理一組「並行(parallel)」的非同步操作的技術。
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
被視為一個序列,生成器只是為了取得連續數值的一個物件。但我們也可以從生成器本身的角度來看事情:
- 直到被迴圈「呼叫」(使用next方法),生成器函式才會開始執行。
- 一旦生成器開始執行,它會一直執行直到它「回到」迴圈(使用yield)。
- 當迴圈執行其他程式碼時(如print statement),生成器不會執行。
- 當生成器執行時,迴圈不會執行(迴圈會被「阻塞」著等待生成器)。
- 一旦生成器用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顯示了相應的概念:現在,當一系列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:
同樣的圖在兩種案例中都可以運作,因為闡述的想法式一樣的-一個非同步的操作正在等待另一個。
由於inlineCallbacks與deferred解決了許多相同的問題,要如何在它們之間選擇呢?以下是一些inlineCallbacks的潛在優勢:
- 由於callbacks分享命名空間,因此不需要傳遞額外的狀態。
- 更容易看出callback的順序,因為它們就是從上到下的執行。
- 由於沒有對於個別callback的函式宣告與隱式流程控制,通常會減少輸入(譯註:指撰寫程式碼)。
- 使用熟悉的try/except statement處理錯誤。
而這裡有些潛在的陷阱:
- 生成器內部的callbacks不能被個別調用,這會使程式碼重用變的困難。使用deferred實例時,建構deferred實例的程式碼可以以任意順序自由的加入任意callbacks。
- 生成器緊湊的形式可能掩蓋了非同步callback實際上是很複雜的這個事實。僅管生成器外表看起來像普通的序列函式,但它的行為卻非常不同。使用inlineCallbacks函式不是避免學習非同步程式設計模型的一種方法。
與任何技術一樣,練習會提供做出明智選擇所需的經驗。
Summary
在這個章節我們學習了inlineCallbacks裝飾器,以及如何讓我們以Python生成器的形式來表達一系列的非同步callback。在Part 18我們學習一種管理一組「並行(parallel)」的非同步操作的技術。
Suggested Exercises
- 為什麼inlineCallbacks函式使用複數的「Callbacks」
- 研究inlineCallbacks的實現與它的輔助函式_inlineCallbacks。思考「魔鬼藏在細節裡」這句話。
- 假設沒有迴圈或if statement,有N個yield statement的生成器包含了幾個callbacks?
- 7.0版本用戶端可能同時有三個生成器在執行。概念上來說,它們之間可以有多少種不同的交錯方式?考慮到在詩歌用戶端中它們被調用的方式與inlineCallbacks的實現,你認為有幾種方法是真的可行的?
- 把7.0版本用戶端的got_poem callback搬到生成器中。
- 接著把poem_done搬到生成器中。小心!確認處理到所有錯誤的狀況,讓reactor無論如何都會關閉。程式碼與使用deferred實例關閉reactor相比起來如何?
- 在while迴圈中具有yield statement的生成器可以表示為一個概念上的無限序列。用inlineCallbacks來裝飾這樣的生成器代表了什麼?
留言
張貼留言