第七部分:小插曲,Deferred
第七部分:小插曲,Deferred
你可以從這裏從頭開始閱讀這個系列
回調函數的後序發展
在第六部分我們認識這樣一個情況:回調是Twisted異步編程中的基礎。除了與reactor交互外,回調可以安插在任何我們寫的Twisted結構內。因此在使用Twisted或其它基於reactor的異步編程體系時,都意味需要將我們的代碼組織成一系列由reactor循環可以激活的回調函數鏈。
即使一個簡單的get_poetry函數都需要回調,兩個回調函數中一個用於處理正常結果而另一個用於處理錯誤。作為一個Twisted程序員,我們必須充分利用這一點。應該花點時間思考一下如何更好地使用回調及使用過程中會遇到什麽困難。
分析下3.1版本中的get_poetry函數:
...
def got_poem(poem):
print poem
reactor.stop()
def poem_failed(err):
print >>sys.stderr, ‘poem download failed‘
print >>sys.stderr, ‘I am terribly sorry‘
print >>sys.stderr, ‘try again later?‘
reactor.stop()
get_poetry(host, port, got_poem, poem_failed)
reactor.run()
我們想法很簡單:
1.如果完成詩歌下載,那麽就打印它
2.如果沒有下載到詩歌,那就打印出錯誤信息
3.上面任何一種情況出現,都要停止程序繼續運行
同步程序中處理上面的情況會采用如下方式:
...
try:
poem = get_poetry(host, port) # the synchronous version of get_poetry
except Exception, err:
print >>sys.stderr, ‘poem download failed‘
print >>sys.stderr, ‘I am terribly sorry‘
print >>sys.stderr, ‘try again later?‘
sys.exit()
else:
print poem
sys.exit()
即callback類似else處理路徑,而errback類似except處理路徑。這意味著激活errback回調函數類似於同步程序中拋出一個異常,而激活一個callback意味著同步程序中的正常執行路徑。
兩個版本有什麽不同之外嗎?可以明確的是,在同步版本中,Python解釋器可以確保只要get_poetry拋出何種類型的異步都會執行except塊。即只要我們相信Python解釋器能夠正確的解釋執行Python程序,那麽就可以相信異常處理塊會在恰當的時間點被執行。
不異步版本相反的是:poem_failed錯誤回調是由我們自己的代碼激活並調用的,即PeotryClientFactory的clientConnectFailed函數。是我們自己而不是Python來確保當出錯時錯誤處理代碼能夠執行。因此我們必須保證通過調用攜帶Failure對象的errback來處理任何可能的錯誤。
否則,我們的程序就會因為等待一個永遠不會出現的回調而止步不前。
這裏顯示出了同步與異步版本的又一個不同之處。如果我們在同步版本中沒有使用try/except捕獲異步,那麽Python解釋器會為我們捕獲然後關掉我們的程序並打印出錯誤信息。但是如果我們忘記拋出我們的異步異常(在本程序中是在PoetryClientFactory調用errback),我們的程序會一直運行下去,還開心地以為什麽事都沒有呢。
顯而易見,在異步程序中處理錯誤是相當重要的,甚至有些嚴峻。也可以說在異步程序中處理錯誤信息比處理正常的信息要重要的多,這是因為錯誤會以多種方式出現,而正確的結果出現的方式是唯一的。當使用Twisted編程時忘記處理異常是一個常犯的錯誤。
關於上面同步程序代碼的另一個默認實事是:else與except塊兩者只能是運行其中一個(假設我們的get_poetry沒有在一個無限循環中運行)。Python解釋器不會突然決定兩者都運行或突發奇想來運行else塊27次。對於通過Python來實現那樣的動作是不可能的。
但在異步程序中,我們要負責callback和errback的運行。因此,我們可能就會犯這樣的錯誤:同時調用了callback與errback或激活callback27次。這對於使用get_poetry的用戶來說是不幸的。雖然在描述文檔中沒有明確地說明,像try/except塊中的else與except一樣,對於每次調用get_poetry時callback與errback只能運行其中一個,不管是我們是否成功得下載完詩歌。
設想一下,我們在調試某個程序時,我們提出了三次詩歌下載請求,但是得到有7次callback被激活和2次errback被激活。可能這時,你會下來檢查一下,什麽時候get_poetry激活了兩次callback並且還拋出一個錯誤出來。
從另一個視角來看,兩個版本都有代碼重復。異步的版本中含有兩次reactor.stop,同步版本中含有兩次sys.exit調用。我們可以重構同步版本如下:
...
try:
poem = get_poetry(host, port) # the synchronous version of get_poetry
except Exception, err:
print >>sys.stderr, ‘poem download failed‘
print >>sys.stderr, ‘I am terribly sorry‘
print >>sys.stderr, ‘try again later?‘
else:
print poem
sys.exit()
我們可以以同樣的方式來重構異步版本嗎?說實話,確實不太可能,因為callback與errback是兩個不同的函數。難道要我們回到使用單一回調來實現重構嗎?
好下面是我們在討論使用回調編程時的一些觀點:
1.激活errback是非常重要的。由於errback的功能與except塊相同,因此用戶需要確保它們的存在。他們並不可選項,而是必選項。
2.不在錯誤的時間點激活回調與在正確的時間點激活回調同等重要。典型的用法是,callback與errback是互斥的即只能運行其中一個。
3.使用回調函數的代碼重構起來有些困難。
來下面的部分,我們還會討論回調,但是已經可以明白為什麽Twisted引入了deferred抽象機制來管理回調了。
Deferred
由於架設在異步程序中大量被使用,並且我們也看到了,正確的使用這一機制需要一些技巧。因此,Twisted開發者設計了一種抽象機制-Deferred-以讓程序員在使用回調時更簡便。
一個Deferred有一對回調鏈,一個是為針對正確結果,另一個針對錯誤結果。新創建的Deferred的這兩條鏈是空的。我們可以向兩條鏈裏分別添加callback與errback。其後,就可以用正確的結果或異常來激活Deferred。激活Deferred意味著以我們添加的順序激活callback或errback。圖12展示了一個擁有callback/errback鏈的Deferred對象:
圖12: Deferred
由於defered中不使用reactor,所以我們可以不用在事件循環中使用它。也許你在Deferred中發現一個seTimeout的函數中使用了reactor。放心,它將來將來的版本中刪掉。
下面是我們第一人使用deferred的例子twisted-deferred/defer-1.py:
from twisted.internet.defer import Deferred
def got_poem(res):
print ‘Your poem is served:‘
print res
def poem_failed(err):
print ‘No poetry for you.‘
d = Deferred()
# add a callback/errback pair to the chain
d.addCallbacks(got_poem, poem_failed)
# fire the chain with a normal result
d.callback(‘This poem is short.‘)
print "Finished"
代碼開始創建了一個新deferred,然後使用addCallbacks添加了callback/errback對,然後使用callback函數激活了其正常結果處理回調鏈。當然了,由於只含有一個回調函數還算不上鏈,但不要緊,運行它:
Your poem is served:
This poem is short.
Finished
有幾個問題需要註意:
1.正如3.1版本中我們使用的callback/errback對,添加到deferred中的回調函數只攜帶一個參數,正確的結果或出錯信息。其實,deferred支持回調函數可以有多個參數,但至少得有一個參數並且第一個只能是正確的結果或錯誤信息。
2.我們向deferred添加的是回調函數對
3.callbac函數攜帶僅有的一個參數即正確的結果來激活deferred
4.從打印結果順序可以看出,激活的deferred立即調用了回調。沒有任何異步的痕跡。這是因為沒有reactor參與導致的。
好了,讓我們來試試另外一種情況,twisted-deferred/defer-2.py激活了錯誤處理回調:
from twisted.internet.defer import Deferred
from twisted.python.failure import Failure
def got_poem(res):
print ‘Your poem is served:‘
print res
def poem_failed(err):
print ‘No poetry for you.‘
d = Deferred()
# add a callback/errback pair to the chain
d.addCallbacks(got_poem, poem_failed)
# fire the chain with an error result
d.errback(Failure(Exception(‘I have failed.‘)))
print "Finished"
運行它打印出的結果為:
No poetry for you.
Finished
激活errback鏈就調用errback函數而不是callback,並且傳進的參數也是錯誤信息。正如上面那樣,errback在deferred激活就被調用。
在前面的例子中,我們將一個Failure對象傳給了errback。deferred會將一個Exception對象轉換成Failure,因此我們可以這樣寫:
from twisted.internet.defer import Deferred
def got_poem(res):
print ‘Your poem is served:‘
print res
def poem_failed(err):
print err.__class__
print err
print ‘No poetry for you.‘
d = Deferred()
# add a callback/errback pair to the chain
d.addCallbacks(got_poem, poem_failed)
# fire the chain with an error result
d.errback(Exception(‘I have failed.‘))
運行結果如下:
twisted.python.failure.Failure [Failure instance: Traceback (failure with no frames): : I have failed. ]
No poetry for you.
這意味著在使用deferred時,我們可以正常地使用Exception。其中deferred會為我們完成向Failure的轉換。
下面我們來運行下面的代碼看看會出現什麽結果:
from twisted.internet.defer import Deferred
def out(s): print s
d = Deferred()
d.addCallbacks(out, out)
d.callback(‘First result‘)
d.callback(‘Second result‘)
print ‘Finished‘
輸出結果:
First result Traceback (most recent call last): ... twisted.internet.defer.AlreadyCalledError
很意外吧,也就是說deferred不允許別人激活它兩次。這也就解決了上面出現的那個問題:一個激活會導致多個回調同時出現。而deferred設計機制控制住了這種可能,如果你非要在一個deferred上要激活多個回調,那麽正如上面那樣,會報異常錯。
那deferred能幫助我們重構異步代碼嗎?考慮下面這個例子:
import sys
from twisted.internet.defer import Deferred
def got_poem(poem):
print poem
from twisted.internet import reactor
reactor.stop()
def poem_failed(err):
print >>sys.stderr, ‘poem download failed‘
print >>sys.stderr, ‘I am terribly sorry‘
print >>sys.stderr, ‘try again later?‘
from twisted.internet import reactor
reactor.stop()
d = Deferred()
d.addCallbacks(got_poem, poem_failed)
from twisted.internet import reactor
reactor.callWhenRunning(d.callback, ‘Another short poem.‘)
reactor.run()
這基本上與我們上面的代碼相同,唯一不同的是加進了reactor。我們在啟動reactor後調用了callWhenRunning函數來激活deferred。我們利用了callWhenRunning函數可以接收一個額外的參數給回調函數。多數Twisted的API都以這樣的方式註冊回調函數,包括向deferred添加callback的API。下面我們給deferred回調鏈添加第二個回調:
import sys
from twisted.internet.defer import Deferred
def got_poem(poem):
print poem
def poem_failed(err):
print >>sys.stderr, ‘poem download failed‘
print >>sys.stderr, ‘I am terribly sorry‘
print >>sys.stderr, ‘try again later?‘
def poem_done(_):
from twisted.internet import reactor
reactor.stop()
d = Deferred()
d.addCallbacks(got_poem, poem_failed)
d.addBoth(poem_done)
from twisted.internet import reactor
reactor.callWhenRunning(d.callback, ‘Another short poem.‘)
reactor.run()
addBoth函數向callback與errback鏈中添加了相同的回調函數。在這種方式下,deferred有可能也會執行errback鏈中的回調。這將在下面的部分討論,只要記住後面我們還會深入討論deferred。
總結:
在這部分我們分析了回調編程與其中潛藏的問題。我們也認識到了deferred是如何幫我們解決這些問題的:
1.我們不能忽視errback,在任何異步編程的API中都需要它。Deferred支持errbacks。
2.激活回調多次可能會導致很嚴重的問題。Deferred只能被激活一次,這就類似於同步編程中的try/except的處理方法。
3.含有回調的程序在重構時相當困難。有了deferred,我們就通過修改回調鏈來重構程序。
關於deferred的故事還沒有結束,後面還有大量的細節來講。但對於使用它來重構我們的客戶端已經夠用的了,在第八部分將講述這部分內容。
第七部分:小插曲,Deferred