(轉) Twisted :第七部分 小插曲,Deferred

回調函數的後序發展html

在第六部分咱們認識這樣一個狀況:python

    回調是Twisted異步編程中的基礎。除了與reactor交互外,回調能夠安插在任何咱們寫的Twisted結構內。所以在使用Twisted或其它基於reactor的異步編程體系時,都意味須要將咱們的代碼組織成一系列由reactor循環能夠激活的回調函數鏈。react

即便一個簡單的get_poetry函數都須要回調,兩個回調函數中一個用於處理正常結果而另外一個用於處理錯誤。做爲一個Twisted程序員,咱們必須充分利用這一點。應該花點時間思考一下如何更好地使用回調及使用過程當中會遇到什麼困難。git

分析下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()

咱們想法很簡單:github

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錯誤回調是由咱們本身的代碼激活並調用的,即PeotryClientFactoryclientConnectFailed函數。是咱們本身而不是Python來確保當出錯時錯誤處理代碼可以執行。所以咱們必須保證經過調用攜帶Failure對象的errback來處理任何可能的錯誤。

不然,咱們的程序就會由於等待一個永遠不會出現的回調而止步不前。

這裏顯示出了同步與異步版本的又一個不一樣之處。若是咱們在同步版本中沒有使用try/except捕獲異步,那麼Python解釋器會爲咱們捕獲而後關掉咱們的程序並打印出錯誤信息。可是若是咱們忘記拋出咱們的異步異常(在本程序中是在PoetryClientFactory調用errback),咱們的程序會一直運行下去,還開心地覺得什麼事都沒有呢。

顯而易見,在異步程序中處理錯誤是至關重要的,甚至有些嚴峻。也能夠說在異步程序中處理錯誤信息比處理正常的信息要重要的多,這是由於錯誤會以多種方式出現,而正確的結果出現的方式是惟一的。當使用Twisted編程時忘記處理異常是一個常犯的錯誤。

關於上面同步程序代碼的另外一個默認實事是:elseexcept塊二者只能是運行其中一個(假設咱們的get_poetry沒有在一個無限循環中運行)。Python解釋器不會忽然決定二者都運行或突發奇想來運行else27次。對於經過Python來實現那樣的動做是不可能的。

但在異步程序中,咱們要負責callbackerrback的運行。所以,咱們可能就會犯這樣的錯誤:同時調用了callbackerrback或激活callback27次。這對於使用get_poetry的用戶來講是不幸的。雖然在描述文檔中沒有明確地說明,像try/except塊中的elseexcept同樣,對於每次調用get_poetrycallbackerrback只能運行其中一個,不論是咱們是否成功得下載完詩歌。

設想一下,咱們在調試某個程序時,咱們提出了三次詩歌下載請求,可是獲得有7callback被激活和2errback被激活。可能這時,你會下來檢查一下,何時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()


咱們能夠以一樣的方式來重構異步版本嗎?說實話,確實不太可能,由於callbackerrback是兩個不一樣的函數。難道要咱們回到使用單一回調來實現重構嗎?

好下面是咱們在討論使用回調編程時的一些觀點:

1.激活errback是很是重要的。因爲errback的功能與except塊相同,所以用戶須要確保它們的存在。他們並不可選項,而是必選項。

2.不在錯誤的時間點激活回調與在正確的時間點激活回調同等重要。典型的用法是,callbackerrback是互斥的即只能運行其中一個。

3.使用回調函數的代碼重構起來有些困難。

來下面的部分,咱們還會討論回調,可是已經能夠明白爲何Twisted引入了deferred抽象機制來管理回調了。


Deferred

因爲架設在異步程序中大量被使用,而且咱們也看到了,正確的使用這一機制須要一些技巧。所以,Twisted開發者設計了一種抽象機制-Deferred-以讓程序員在使用回調時更簡便。

一個Deferred有一對回調鏈,一個是爲針對正確結果,另外一個針對錯誤結果。新建立的Deferred的這兩條鏈是空的。咱們能夠向兩條鏈裏分別添加callbackerrback。其後,就能夠用正確的結果或異常來激活Deferred。激活Deferred意味着以咱們添加的順序激活callbackerrback。圖12展現了一個擁有callback/errback鏈的Deferred對象:

第七部分:小插曲,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,而且傳進的參數也是錯誤信息。正如上面那樣,errbackdeferred激活就被調用。

在前面的例子中,咱們將一個Failure對象傳給了errbackdeferred會將一個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函數能夠接收一個額外的參數給回調函數。多數TwistedAPI都以這樣的方式註冊回調函數,包括向deferred添加callbackAPI。下面咱們給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函數向callbackerrback鏈中添加了相同的回調函數。在這種方式下,deferred有可能也會執行errback鏈中的回調。這將在下面的部分討論,只要記住後面咱們還會深刻討論deferred


總結:

在這部分咱們分析了回調編程與其中潛藏的問題。咱們也認識到了deferred是如何幫咱們解決這些問題的:

1.咱們不能忽視errback,在任何異步編程的API中都須要它。Deferred支持errbacks

2.激活回調屢次可能會致使很嚴重的問題。Deferred只能被激活一次,這就相似於同步編程中的try/except的處理方法。

3.含有回調的程序在重構時至關困難。有了deferred,咱們就經過修改回調鏈來重構程序。

關於deferred的故事尚未結束,後面還有大量的細節來說。但對於使用它來重構咱們的客戶端已經夠用的了,在第八部分將講述這部份內容。

相關文章
相關標籤/搜索