在第六部分我們認識到這樣一個情況:回調(diào)是Twisted異步編程中的基礎(chǔ)。除了與reactor交互外,回調(diào)可以安插在任何我們寫的Twisted結(jié)構(gòu)內(nèi)。因此在使用Twisted或其它基于reactor的異步編程體系時,都意味需要將我們的代碼組織成一系列由reactor循環(huán)可以激活的回調(diào)函數(shù)鏈。
即使一個簡單的get_poetry函數(shù)都需要回調(diào),兩個回調(diào)函數(shù)中一個用于處理正常結(jié)果而另一個用于處理錯誤。作為一個Twisted程序員,我們必須充分利用這一點。應(yīng)該花點時間思考一下如何更好地使用回調(diào)及使用過程中會遇到什么困難。
分析下3.1版本中的get_poetry函數(shù):
...
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()
我們想法很簡單:
同步程序中處理上面的情況會采用如下方式:
...
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回調(diào)函數(shù)類似于同步程序中拋出一個異常,而激活一個callback意味著同步程序中的正常執(zhí)行路徑。 兩個版本有什么不同之外嗎?可以明確的是,在同步版本中,Python解釋器可以確保只要get_poetry拋出異常(任意類型, 這里特指Exception及其子類)就會執(zhí)行except塊。也就是說只要我們相信Python解釋器能夠正確的解釋執(zhí)行Python程序,那么就可以相信異常處理塊會在恰當(dāng)?shù)臅r間點被執(zhí)行。
和異步版本不同的是:poem_failed錯誤回調(diào)是由我們自己的代碼激活并調(diào)用的,即PoetryClientFactory的clientConnectFailed函數(shù)。是我們自己而不是Python來確保當(dāng)出錯時錯誤處理代碼能夠執(zhí)行。因此我們必須保證通過調(diào)用攜帶Failure對象的errback來處理任何可能的錯誤。
否則,我們的程序就會因為等待一個永遠不會出現(xiàn)的回調(diào)而止步不前。
這里顯示出了同步與異步版本的又一個不同之處。如果我們在同步版本中沒有使用try/except捕獲異步,那么Python解釋器會為我們捕獲然后關(guān)掉我們的程序并打印出錯誤信息。但是如果我們忘記拋出我們的異步異常(在本程序中是在PoetryClientFactory調(diào)用errback),我們的程序會一直運行下去,還開心地以為什么事都沒有呢。
顯而易見,在異步程序中處理錯誤是相當(dāng)重要的,甚至有些嚴峻。也可以說在異步程序中處理錯誤信息比處理正常的信息要重要的多,這是因為錯誤會以多種方式出現(xiàn),而正確的結(jié)果出現(xiàn)的方式是唯一的。當(dāng)使用Twisted編程時忘記處理異常是一個常犯的錯誤。
關(guān)于上面同步程序代碼的另一個默認的事實是:else與except塊兩者只能是運行其中一個(假設(shè)我們的get_poetry沒有在一個無限循環(huán)中運行)。Python解釋器不會突然決定兩者都運行或突發(fā)奇想來運行else塊27次。對于通過Python來實現(xiàn)那樣的動作是不可能的。
但在異步程序中,我們要負責(zé)callback和errback的運行。因此,我們可能就會犯這樣的錯誤:同時調(diào)用了callback與errback或激活callback27次。這對于使用get_poetry的用戶來說是不幸的。雖然在描述文檔中沒有明確地說明,像try/except塊中的else與except一樣,對于每次調(diào)用get_poetry時callback與errback只能運行其中一個,不管我們是否成功的下載完詩歌。
設(shè)想一下,我們在調(diào)試某個程序時,我們提出了三次詩歌下載請求,但是得到有7次callback被激活和2次errback被激活??赡苓@時,你會下來檢查一下,什么時候get_poetry激活了兩次callback并且還拋出一個錯誤出來。
從另一個視角來看,兩個版本都有代碼重復(fù)。異步的版本中含有兩次reactor.stop,同步版本中含有兩次sys.exit調(diào)用。我們可以重構(gòu)同步版本如下:
...
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()
我們可以以同樣的方式來重構(gòu)異步版本嗎?說實話,確實不太可能,因為callback與errback是兩個不同的函數(shù)。難道要我們回到使用單一回調(diào)來實現(xiàn)重構(gòu)嗎?
好, 下面是我們在討論使用回調(diào)編程時的一些觀點:
后面的部分我們還會討論回調(diào),但是到這里已經(jīng)可以明白為什么Twisted引入了抽象機制(Deferred)來管理回調(diào)了。
由于回調(diào)在異步程序中大量被使用,并且正確的使用這一機制需要一些技巧。因此,Twisted開發(fā)者設(shè)計了一種抽象機制-Deferred-讓程序員在使用回調(diào)時更加簡便。
一個Deferred有一對回調(diào)鏈,一個是為針對正確結(jié)果,另一個針對錯誤結(jié)果。新創(chuàng)建的Deferred的這兩條鏈?zhǔn)强盏?。我們可以向兩條鏈里分別添加callback與errback。其后,就可以用正確的結(jié)果或異常來激活Deferred。激活Deferred意味著以我們添加的順序激活callback或errback。圖12展示了一個擁有callback/errback鏈的Deferred對象:
http://wiki.jikexueyuan.com/project/twisted-intro/images/p07_deferred-1.png" alt="" />
由于defered中不使用reactor,所以使用它并不需要啟動事件循環(huán)。也許你在Deferred中發(fā)現(xiàn)一個setTimeout的函數(shù)中使用了reactor。放心,它已經(jīng)廢棄并且會在將來的版本中刪掉,我們可以直接無視它。
下面是我們看看第一個使用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"
代碼開始創(chuàng)建了一個新deferred,然后使用addCallbacks添加了callback/errback對,然后使用callback函數(shù)激活了其正常結(jié)果處理回調(diào)鏈。當(dāng)然了,由于只含有一個回調(diào)函數(shù)還算不上鏈,但不要緊,運行它:
Your poem is served:
This poem is short.
Finished
有幾個問題需要注意:
好了,讓我們來試試另外一種情況,twisted-deferred/defer-2.py激活了錯誤處理回調(diào):
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"
運行它打印出的結(jié)果為:
No poetry for you.
Finished
激活errback鏈就調(diào)用errback函數(shù)而不是callback,并且傳進的參數(shù)也是錯誤信息。正如上面那樣,errback在deferred激活就被調(diào)用。
在前面的例子中,我們將一個Failure對象傳給了errback。deferred會將一個Exception對象轉(zhuǎn)換成Failure,因此我們可以這樣寫twisted-deferred/defer-3.py:
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.'))
運行結(jié)果如下:
twisted.python.failure.Failure
[Failure instance: Traceback (failure with no frames): : I have failed.
]
No poetry for you.
這意味著在使用deferred時,我們可以正常地使用Exception。其中deferred會為我們完成向Failure的轉(zhuǎn)換。
下面我們來運行代碼twisted-deferred/defer4.py看看會出現(xiàn)什么結(jié)果:
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'
輸出結(jié)果:
First result
Traceback (most recent call last):
...
twisted.internet.defer.AlreadyCalledError
很意外吧,也就是說deferred不允許別人激活它兩次。這也就解決了上面出現(xiàn)的那個問題:一個激活會導(dǎo)致多個回調(diào)同時出現(xiàn)。而deferred設(shè)計機制控制住了這種可能,如果你非要在一個deferred上要激活多個回調(diào),那么正如上面那樣,會報異常錯。
這里展示了更多的例子:
那deferred能幫助我們重構(gòu)異步代碼嗎?考慮下面twisted-deferred/defer-8.py這個例子:
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后調(diào)用了callWhenRunning函數(shù)來激活deferred。我們利用了callWhenRunning函數(shù)可以接收一個額外的參數(shù)給回調(diào)函數(shù)。多數(shù)Twisted的API都以這樣的方式注冊回調(diào)函數(shù),包括向deferred添加callback的API。下面我們給deferred回調(diào)鏈添加第二個回調(diào):
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函數(shù)向callback與errback鏈中添加了相同的回調(diào)函數(shù)。在這種方式下,deferred有可能也會執(zhí)行errback鏈中的回調(diào)。這將在下面的部分討論,只要記住后面我們還會深入討論deferred。
在這部分我們分析了回調(diào)編程與其中潛藏的問題。我們也認識到了deferred是如何幫我們解決這些問題的:
關(guān)于deferred的故事還沒有結(jié)束,后面還有大量的細節(jié)來講。但對于使用它來重構(gòu)我們的客戶端已經(jīng)夠用的了,在第八部分將講述這部分內(nèi)容。
本部分原作參見: dave @ http://krondo.com/?p=1682
本部分翻譯內(nèi)容參見楊曉偉的博客 http://blog.sina.com.cn/s/blog_704b6af70100q52t.html