首先拋出我們在討論使用回調編程時的一些觀點:
啟用errback是非常重要的。由於errback的功能與except塊相同,因此使用者需要確保它們的存在。他們並不是可選項,而是必選項。
不在錯誤的時間點啟用回調與在正確的時間點啟用回調同等重要。典型的用法是,callback與errback是互斥的即只能運行其中一個。
使用回呼函數的代碼重構起來有些困難。
Deferred
Twisted使用Deferred對象來管理回呼函數的序列。有些情況下可能要把一系列的函數關聯到Deferred對象上,以便在在非同步作業完成時按次序地調用(這些一系列的回呼函數叫做回呼函數鏈);同時還要有一些函數在非同步作業出現異常時來調用。當操作完成時,會先調用第一個回呼函數,或者當錯誤發生時,會先調用第一個錯誤處理回呼函數,然後Deferred對象會把每個回呼函數或錯誤處理回呼函數的傳回值傳遞給鏈中的下一個函數。
Callbacks
一個twisted.internet.defer.Deferred對象代表著在將來某個時刻一定會產生結果的一個函數。我們可以把一個回呼函數關聯到Deferred對象上,一旦這個Deferred對象有了結果,這個回呼函數就會被調用。另外,Deferred對象還允許開發人員為它註冊一個錯誤處理回呼函數。Deferred機制對於各種各樣的阻塞或者延時操作都為開發人員提供了標準化的介面。
from twisted.internet import reactor, defer
def getDummyData(inputData): """ This function is a dummy which simulates a delayed result and returns a Deferred which will fire with that result. Don't try too hard to understand this. """ print('getDummyData called') deferred = defer.Deferred() # simulate a delayed result by asking the reactor to fire the # Deferred in 2 seconds time with the result inputData * 3 reactor.callLater(2, deferred.callback, inputData * 3) return deferred def cbPrintData(result): """ Data handling function to be added as a callback: handles the data by printing the result """ print('Result received: {}'.format(result)) deferred = getDummyData(3)deferred.addCallback(cbPrintData) # manually set up the end of the process by asking the reactor to# stop itself in 4 seconds timereactor.callLater(4, reactor.stop)# start up the Twisted reactor (event loop handler) manuallyprint('Starting the reactor')reactor.run()
多個回呼函數
在一個Deferred對象上可以關聯多個回呼函數,這個回呼函數鏈上的第一個回呼函數會以Deferred對象的結果為參數來調用,而第二個回呼函數以第一個函數的結果為參數來調用,依此類推。為什麼需要這樣的機制呢?考慮一下這樣的情況,twisted.enterprise.adbapi返回一個Deferred對象——一個一個SQL查詢的結果,可能有某個web視窗會在這個Deferred對象上添加一個回呼函數,以把查詢結果轉換成HTML的格式,然後把Deferred對象繼續向前傳遞,這時Twisted會調用這個回呼函數並把結果返回給HTTP用戶端。在出現錯誤或者異常的情況下,回呼函數鏈不會被調用。
from twisted.internet import reactor, defer class Getter: def gotResults(self, x): """ The Deferred mechanism provides a mechanism to signal error conditions. In this case, odd numbers are bad. This function demonstrates a more complex way of starting the callback chain by checking for expected results and choosing whether to fire the callback or errback chain """ if self.d is None: print("Nowhere to put results") return d = self.d self.d = None if x % 2 == 0: d.callback(x * 3) else: d.errback(ValueError("You used an odd number!")) def _toHTML(self, r): """ This function converts r to HTML. It is added to the callback chain by getDummyData in order to demonstrate how a callback passes its own result to the next callback """ return "Result: %s" % r def getDummyData(self, x): """ The Deferred mechanism allows for chained callbacks. In this example, the output of gotResults is first passed through _toHTML on its way to printData. Again this function is a dummy, simulating a delayed result using callLater, rather than using a real asynchronous setup. """ self.d = defer.Deferred() # simulate a delayed result by asking the reactor to schedule # gotResults in 2 seconds time reactor.callLater(2, self.gotResults, x) self.d.addCallback(self._toHTML) return self.d def cbPrintData(result): print(result) def ebPrintError(failure): import sys sys.stderr.write(str(failure)) # this series of callbacks and errbacks will print an error messageg = Getter()d = g.getDummyData(3)d.addCallback(cbPrintData)d.addErrback(ebPrintError) # this series of callbacks and errbacks will print "Result: 12"g = Getter()d = g.getDummyData(4)d.addCallback(cbPrintData)d.addErrback(ebPrintError) reactor.callLater(4, reactor.stop)reactor.run()
需要注意的一點是,在方法gotResults中處理self.d的方式。在Deferred對象被結果或者錯誤啟用之前,這個屬性被設定成了None,這樣Getter執行個體就不會再持有將要啟用的Deferred對象的引用。這樣做有幾個好處,首先,這樣可以避免Getter.gotResults有時會重複啟用相同的Deferred對象的可能性(這樣會導致出現AlreadyCalledError異常)。其次,這樣做可以使得該Deferred對象上可以添加一個調用了Getter.getDummyData函數的回呼函數,而不會產生問題。還有,這樣使得Python垃圾收集器更容易通過引用迴圈來檢測出一個對象是否需要回收。
可視化的解釋
這裡寫圖片描述
1.要求方法請求資料到Data Sink,得到返回的Deferred對象。
2.要求方法把回呼函數關聯到Deferred對象上。
1.當結果已經準備好後,把它傳遞給Deferred對象。如果操作成功就調用Deferred對象的.callback(result)方法,如果操作失敗就調用Deferred對象的.errback(faliure)方法。注意failure是twisted.python.failure.Failure類的一個執行個體。
2.Deferred對象使用result或者faliure來啟用之前添加的回呼函數或者錯誤處理回呼函數。然後就按照下面的規則來沿著回呼函數鏈繼續執行下去:
回呼函數的結果總是當做第一個參數被傳遞給下一個回呼函數,這樣就形成了一個鏈式的處理器。
如果一個回呼函數拋出了異常,就轉到錯誤處理回呼函數來執行。
如果一個faliure沒有得到處理,那麼它會沿著錯誤處理回呼函數鏈一直傳遞下去,這有點像非同步版本的except語句。
如果一個錯誤處理回呼函數沒有拋出異常或者返回一個twisted.python.failure.Failure執行個體,那麼接下來就轉到去執行回呼函數。
錯誤處理回呼函數
Deferred對象的錯誤處理模型是以Python的異常處理為基礎的。在沒有錯誤發生的情況下,所有的回呼函數都會被執行,一個接著一個,就像上面所說的那樣。
如果沒有執行回呼函數而是執行了錯誤處理回呼函數(比如DB查詢發生了錯誤),那麼一個twisted.python.failure.Failure對象會被傳遞給第一個錯誤處理回呼函數(你可以添加多個錯誤處理回呼函數,就像回呼函數鏈一樣)。可以把錯誤處理回呼函數鏈當做普通Python代碼中的except代碼塊。
除非在except代碼塊中顯式地raise了一個錯誤,否則Exception對象就會被捕捉到且不再繼續傳播下去,然後又開始正常地執行程式。對於錯誤處理回呼函數鏈來說也是一樣,除非你顯式地return一個Faliure或者重新拋出一個異常,否則錯誤就會停止繼續傳播,然後就會從那裡開始執行正常的回呼函數鏈(使用錯誤處理回呼函數返回的值)。如果錯誤處理回呼函數返回了一個Faliure或者拋出了一個異常,那麼這個Faliure或者異常就會被傳遞給下一個錯誤處理回呼函數。
注意,如果一個錯誤處理回呼函數什麼也沒有返回,那它實際上返回的是None,這就意味著在這個錯誤處理回呼函數執行之後會繼續回呼函數鏈的執行。這可能不是你實際上期望的那樣,所以要確保你的錯誤處理回呼函數返回一個Faliure對象(或者就是傳遞給它當參數的那個Faliure對象)或者一個有意義的傳回值以便來執行下一個回呼函數。
twisted.python.failure.Failure有一個有用的方法叫做trap,可以讓下面的代碼變成更有效率的另一種形式:
try: # code that may throw an exception cookSpamAndEggs()except (SpamException, EggException): # Handle SpamExceptions and EggExceptions ...
可以寫成:
def errorHandler(failure): failure.trap(SpamException, EggException) # Handle SpamExceptions and EggExceptions d.addCallback(cookSpamAndEggs)d.addErrback(errorHandler)
如果傳遞給faliure.trap的參數沒有能和Faliure中的錯誤匹配的,那它會重新拋出這個錯誤。
還有一個需要注意的地方,twisted.internet.defer.Deferred.addCallbacks方法的功能和addCallback再跟上addErrback的功能是類似的,但不完全一樣。考慮一下下面的情況:
# Case 1d = getDeferredFromSomewhere()d.addCallback(callback1) # Ad.addErrback(errback1) # Bd.addCallback(callback2)d.addErrback(errback2) # Case 2d = getDeferredFromSomewhere()d.addCallbacks(callback1, errback1) # Cd.addCallbacks(callback2, errback2)
對於Case 1來說,如果在callback1裡面發生了錯誤,那麼errback1就會被調用。而對於Case 2來說,被調用的卻是是errback2。
實際上是因為,在Case 1中,行A會處理getDeferredFromSomewhere執行成功的情況,行B會處理髮生在getDeferredFromSomewhere執行時或者行A的callback1執行時的錯誤。而在Case 2中,行C中的errback1隻會處理getDeferredFromSomewhere執行時產生的錯誤,而不會負責callback1中產生的錯誤。
未處理的錯誤
如果一個Deferred對象在還有一個未處理的錯誤時(即如果它還有下一個errback就一定會調用)就被垃圾收集器清除掉了,那麼Twisted會把這個錯誤的traceback記錄到記錄檔裡。這意味著你可能不用添加errback仍然能夠記錄錯誤。不過要小心的是,如果你還持有這個Deferred對象的引用,並且它永遠不會被垃圾收集器清理,那麼你就會永遠看不到這個錯誤(而且你的callbacks會神秘地永遠不會執行)。如果不確定上述情況是否會發生,你應當在回呼函數鏈之後顯式地添加一個errback,即使只是這樣寫:
# Make sure errors get loggedfrom twisted.python import logd.addErrback(log.err)
處理同步和非同步結果
在一些應用中,可能同時會有同步的函數,也會有非同步函數。例如,對於一個使用者認證函數,如果它是從記憶體中檢查使用者是否已經認證,這樣就可以立即返回結果;但是如果它需要等待網路上的資料,那它就應當返回一個當資料到達時就啟用的Deferred對象。這就是說,一個想要去檢查使用者是否已經認證的函數需要能同時接受立即返回的結果和Deferred對象。
下面的例子中,authenticateUser使用了isValidUser來認證使用者:
def authenticateUser(isValidUser, user): if isValidUser(user): print("User is authenticated") else: print("User is not authenticated")
這個函數假定isValidUser是立即返回的,然而實際上isValidUser可能是非同步認證使用者的並且返回的是一個Deferred對象。把這個函數調整為既能接收同步的isValidUser又能接收非同步isValidUser是有可能的。同時把同步的函數改成傳回值為Deferred對象也是可以的。
在庫函數代碼中處理可能的Deferred對象
這是一個可能被傳遞給authenticateUser的同步的使用者認證方法:
def synchronousIsValidUser(user): ''' Return true if user is a valid user, false otherwise ''' return user in ["Alice", "Angus", "Agnes"]
這是一個非同步使用者認證方法,返回一個Deferred對象:
from twisted.internet import reactor, defer def asynchronousIsValidUser(user): d = defer.Deferred() reactor.callLater(2, d.callback, user in ["Alice", "Angus", "Agnes"]) return d
我們最初對authenticateUser的實現希望isValidUser是同步的,但是現在需要把它改成既能處理同步又能處理非同步isValidUser實現。對此,可以使用maybeDeferred函數來調用isValidUser,這個函數可以保證isValidUser函數的傳回值是一個Deferred對象,即使isValidUser是一個同步的函數:
from twisted.internet import defer def printResult(result): if result: print("User is authenticated") else: print("User is not authenticated") def authenticateUser(isValidUser, user): d = defer.maybeDeferred(isValidUser, user) d.addCallback(printResult)
現在isValidUser無論是同步的還是非同步都可以了。
也可以把synchronousIsValidUser函數改寫成返回一個Deferred對象,可以參考這裡。
取消回呼函數
動機:一個Deferred對象可能需要很長時間才會調用回呼函數,甚至於永遠也不會調用。有時候可能沒有那麼好的耐心來等待Deferred返回結果。既然Deferred完成後要執行的所有代碼都在你的應用中或者調用的庫中,那麼你就可以選擇在已經過去了很長時間才收到結果時忽略這個結果。然而,即使你選擇忽略這個結果,這個Deferred對象產生的底層操作仍然在後台工作著,並且消耗著機器資源,比如CPU時間、記憶體、網路頻寬甚至磁碟容量。因此,當使用者關閉視窗,點擊了取消按鈕,從你的伺服器上中斷連線或者發送了一個“停止”的指令,這時你需要顯式地聲明你對之前原定的操作的結果已經不再感興趣了,以便原先的Deferred對象可以做一些清理的工作並釋放資源。
這是一個簡單的例子,你想串連到一個外部的機器,但是這個機器太慢了,所以需要在應用中添加一個取消按鈕來終止這次串連企圖,以便使用者可以串連到另一個機器。這裡是這樣的一個應用的大概邏輯:
def startConnecting(someEndpoint): def connected(it): "Do something useful when connected." return someEndpoint.connect(myFactory).addCallback(connected)# ...connectionAttempt = startConnecting(endpoint)def cancelClicked(): connectionAttempt.cancel()
顯然,startConnecting被一些UI元素用來讓使用者選擇串連哪個機器。然後是一個取消按鈕陪著到cancelClicked函數上。
當connectionAttempt.cancel被調用時,會發生以下操作:
導致潛在的串連操作終止,如果它仍然在進行中的話
不管怎樣,使得connectionAttempt這個Deferred對象及時地被完成
有可能導致connectionAttempt這個Deferred對象因為CancelledError錯誤調用錯誤處理函數
即使這個取消操作已經表達了讓底層的操作停止的需求,但是底層的操作不大可能馬上就對此作出反應。甚至在這個簡單的例子中就有一個不會被中斷的操作:網域名稱解析,因此需要在在一個線程中執行;這個應用中的串連操作如果在等待網域名稱解析的時候就不能被取消。所以你要取消的Deferred對象可能不會立即調用回呼函數或錯誤處理回呼函數。
一個Deferred對象可能會在執行它的回呼函數鏈的任何一點時等待另一個Deferred對象的完成。沒有方法可以在回呼函數鏈的特定一個點知道是否每件事都已經準備好了。由於有可能一個回呼函數鏈的很多層次上的函數都會希望取消同一個Deferred對象,那麼鏈上任何層次的函數在任意時刻都有可能調用.cancel()函數。.cancel()函數從不拋出任何異常或者返回任何值。你可以重複調用它,即使這個Deferred對象已經被啟用了,它已經沒有剩餘的回呼函數了。
在執行個體化了一個Deferred對象的同時,可以給它提供一個取消函數(Deferred對象的建構函式為def __init__(self, canceller=None): (source)),這個canceller可以做任何事情。理想情況下,它做的每件事情都都會阻止之前你請求的操作,但是並不總是能保證這樣。所以Deferred對象的取消只是儘力而為。原因有幾點:
Deferred對象不知道怎樣取消底層的操作。
底層的操作已經執行到了一個不可取消的狀態,因為可能已經執行了一些無法復原的操作。
Deferred對象可能已經有了結果,所以沒有要取消的東西了。
調用cancel()函數後,不管是否能取消,總會得到成功的結果,不會出現出錯的情況。在第一種和第二和情況下,由於底層的操作仍在繼續,Deferred對象大可以twisted.internet.defer.CancelledError為參數來調用它的errback。
如果取消的Deferred對象正在等待另一個Deferred對象,那麼取消操作會向前傳遞到此Deferred對象。
可以參考API。
預設的取消行為
所有的Deferred對象都支援取消,但是只提供了很簡單的行為,也沒有釋放任何資源。
考慮一下下面的例子:
operation = Deferred()def x(result): print("Hooray, a result:" + repr(x))operation.addCallback(x)# ...def operationDone(): operation.callback("completed")
如果需要取消operation這個Deferred對象,而operation沒有一個canceller的取消函數,就會產生下面兩種之一的結果:
如果operationDone已經被調用了,也就是operation對象已經完成了,那麼什麼都不會改變。operation仍然有一個結果,不過既然沒有其他的回呼函數了,所以沒有什麼行為上可以看到的變化。
如果operationDone已經還沒有被調用,那麼operation會馬上以CancelledError為參數啟用errback。
在正常情況下,如果一個Deferred對象已經調用了回呼函數再來調用callback會導致一個AlreadyCalledError。因此,callback可以在已經取消的、但是沒有canceller的Deferred對象上再調用一次,只會導致一個空操作。如果你多次調用callback,仍會得到一個AlreadyCalledError異常。
建立能取消的Deferred對象:自訂取消函數
假設你在實現一個HTTP用戶端,返回一個在伺服器返迴響應的時候會啟用的Deferred對象。取消最好是關閉串連。為了讓取消函數這麼做,可以向Deferred對象的建構函式中傳遞一個函數作為參數(當Deferred對象被取消的時候會調用這個函數):
class HTTPClient(Protocol): def request(self, method, path): self.resultDeferred = Deferred( lambda ignore: self.transport.abortConnection()) request = b"%s %s HTTP/1.0\r\n\r\n" % (method, path) self.transport.write(request) return self.resultDeferred def dataReceived(self, data): # ... parse HTTP response ... # ... eventually call self.resultDeferred.callback() ...
現在如果在HTTPClient.request()返回的Deferred對象上調用了cancel()函數,這個HTTP請求就會取消(如果沒有太晚的話)。要注意的是,還要在一個已經被取消的、帶有canceller的Deferred對象上調用callback()。
DeferredList
有時你想在幾個不同的事件都發生後再得到通知,而不是每個事件發生都會通知一下。例如,你想等待一個列表中所有的串連都關閉。twisted.internet.defer.DeferredList就適用於這種情況。
用多個Deferred對象來建立一個DeferredList,只需傳遞一個你想等待的Deferred對象的列表即可:
# Creates a DeferredList
dl = defer.DeferredList([deferred1, deferred2, deferred3])
現在就可以把這個DeferredList當成一個普通的Deferred來看待了,例如你也可以調用addCallbacks等等。這個DeferredList會在所有的Deferred對象都完成之後才調用它的回呼函數。這個回呼函數的參數是這個DeferredList對象中包含的所有Deferred對象返回結果的列表,例如:
# A callback that unpacks and prints the results of a DeferredListdef printResult(result): for (success, value) in result: if success: print('Success:', value) else: print('Failure:', value.getErrorMessage()) # Create three deferreds.deferred1 = defer.Deferred()deferred2 = defer.Deferred()deferred3 = defer.Deferred() # Pack them into a DeferredListdl = defer.DeferredList([deferred1, deferred2, deferred3], consumeErrors=True) # Add our callbackdl.addCallback(printResult) # Fire our three deferreds with various values.deferred1.callback('one')deferred2.errback(Exception('bang!'))deferred3.callback('three') # At this point, dl will fire its callback, printing:# Success: one# Failure: bang!# Success: three# (note that defer.SUCCESS == True, and defer.FAILURE == False)
正常情況下DeferredList不會調用errback,但是除非把cousumeErrors設定成True,否則在Deferred對象中產生的錯誤仍然會啟用每個Deferred對象各自的errback。
注意,如果你想在添加到DeferredList中去的Deferred對象上應用回呼函數,那麼就需要注意添加回呼函數的時機。把一個Deferred對象添加到DeferredList中會導致同時也給該Deferred對象添加了一個回呼函數(當這個回呼函數啟動並執行時候,它的功能是檢查DeferredList是否已經完成了)。最重要的是,變數這個回呼函數把記錄了Deferred對象的傳回值並把這個值傳遞到最終交給DeferredList回呼函數當做參數的列表中。
因此,如果你在把一個Deferred添加到DeferredList之後又給這個Deferred對象添加了一個回呼函數,那麼這個新添加的回呼函數的傳回值不會被傳遞到DeferredList的回呼函數中。為了避免這種情況的發生,建議不要在把一個Deferred對象添加到DeferredList中之後再給這個Deferred添加回呼函數。
def printResult(result): print(result) def addTen(result): return result + " ten" # Deferred gets callback before DeferredList is createddeferred1 = defer.Deferred()deferred2 = defer.Deferred()deferred1.addCallback(addTen)dl = defer.DeferredList([deferred1, deferred2])dl.addCallback(printResult)deferred1.callback("one") # fires addTen, checks DeferredList, stores "one ten"deferred2.callback("two")# At this point, dl will fire its callback, printing:# [(True, 'one ten'), (True, 'two')] # Deferred gets callback after DeferredList is createddeferred1 = defer.Deferred()deferred2 = defer.Deferred()dl = defer.DeferredList([deferred1, deferred2])deferred1.addCallback(addTen) # will run *after* DeferredList gets its valuedl.addCallback(printResult)deferred1.callback("one") # checks DeferredList, stores "one", fires addTendeferred2.callback("two")# At this point, dl will fire its callback, printing:# [(True, 'one), (True, 'two')]
DeferredList接受三個關鍵字參數來定製它的行為:fireOnOneCallback、fireOnOneErrback和cousumeErrors。如果設定了fireOnOneCallback,那麼只要有一個Deferred對象調用了它的回呼函數,DeferredList就會立即調用它的回呼函數。相似的,如果設定了fireOnOneErrback,那麼只要有一個Deferred調用了errback,DeferredList就會調用它的errback。注意,DeferredList只是一次性的,所以在一次callback或者errback調用之後,它就會什麼也不做(它會忽略它的Deferred傳遞給它的所有結果)。
fireOnOneErrback選項在你想等待所有事情成功執行,而且需要在出錯時馬上知道的情形下是很有用的。
consumeErrors參數會使DeferredList中包含的Deferred對象中產生的錯誤在建立了DeferredList對象之後,不會傳遞給原來每個Deferred對象各自的errbacks。建立了DeferredList對象之後,任何單個Deferred對象中產生的錯誤會被轉化成結果為None的回調調用。用這個選項可以防止它所包含的Deferred中的“Unhandled error in Deferred”警告,而不用添加額外的errbacks(否則要消除這個警告就需要為每個Deferred對象添加errback)。 給consumeErrors參數傳遞一個True不會影響fireOnOneCallback和fireOnOneErrback的行為。應該總是使用這個參數,除非你想在將來給這些列表中的Deferred對象添加callbacks或errbacks,或者除非你知道它們不會產生錯誤。否則,產生錯誤的話會導致一個被Twisted記錄到日誌中的“unhandled error”。
DeferredList一個普遍的用法是把一些並行的非同步作業結果組合到一起。如果所有的操作都成功了,那就可以操作成功,如果有一個操作失敗了,那麼就操作失敗。twisted.internet.defer.gatherResults是一個捷徑:
from twisted.internet import deferd1 = defer.Deferred()d2 = defer.Deferred()d = defer.gatherResults([d1, d2], consumeErrors=True) def cbPrintResult(result): print(result) d.addCallback(cbPrintResult) d1.callback("one")# nothing is printed yet; d is still awaiting completion of d2d2.callback("two")# printResult prints ["one", "two"]
鏈式的Deferred
如果你需要一個Deferred對象來等待另一個Deferred對象的執行,你所要做的只是從它的回呼函數鏈中的回呼函數中返回一個Deferred對象。具體點,如果你從某個Deferred對象A的一個回呼函數中返回Deferred對象B,那麼A的回呼函數鏈就會在B的callback()函數調用之前進行等待。此時,A的下一個回呼函數的第一個參數就是B的最後一個回呼函數返回的結果。
注意,如果一個`Deferred`對象在它的回呼函數中直接或者間接地返回了它本身,那麼這樣的行為是沒有定義的。代碼會試圖檢測出這種情況然後給出警告。在將來可能會直接拋出異常。
如果這看起來有點複雜,也不要擔心——當你遇到這種情況的時候,你可能會直接認出來並且知道為什麼會產生這樣的結果。如果你需要手動地把Deferred對象
連結起來,這裡有一個方便的方法:
chainDeferred(otherDeferred)
總結
我們認識到了deferred是如何幫我們解決這些問題的:
我們不能忽視errback,在任何非同步編程的API中都需要它。Deferred支援errbacks。
啟用回調多次可能會導致很嚴重的問題。Deferred只能被啟用一次,這就類似於同步編程中的try/except的處理方法。
含有回調的程式在重構時相當困難。有了deferred,我們就通過修改回調鏈來重構程式。