[Twisted] Part 10: Poetry Transformed
本文由Dave的Part 10: Poetry Transformed翻譯而成,你可以由Part 1開始閱讀這個系列的文章,也可以在這裡找到整個系列的目錄。
不幸的是,這個演算法簡單到永遠不會出錯,所以在twisted-client-5/get-poetry.py的5.0版用戶端中,我們修改了cummingsify,它會隨機執行下面其中一項操作:
用這種方法,我們模擬了一個比較複雜的演算法,它有時候會以意想不到的方式失敗。
5.0版用戶端中其它有更改到的地方都在poetry_main函式中:
所以當程式從伺服器下載一首詩歌後,它可能有其中一種情況:
因此,當從伺服器上下載一首詩歌時,可能會出現如下情況:
儘管我們保留了從多個伺服器下載的功能,但當你測試5.0版用戶端時,只使用一台伺服器,並且多執行幾次程式,你會更容易看到三種不同的結果。也可以嘗試連接沒有伺服器的埠。
我們會從get_poetry得到三種結果的Deferred,讓我們在圖19畫出每個Deferred上的callback/errback鏈:
注意由assCallback加入的pass_throuht errback,它會將它收到的所有Failure物件傳給下一個errback(poem_failed)。因此poem_failed就可以處理來自get_poetry(也就式deferred實例是以errback觸發)與cummingsify函式的錯誤了。
也要注意圖19中在deferred邊界周圍那些令人難以忘懷的美麗陰影,這不重要,我只是想告訴大家我發現在Inkscape中如何做到這點,預計未來還會有更多的陰影。
讓我們來分析一下可以觸發我們deferred的幾種方法,圖20顯示了我們獲得了一首詩,並且cummingsify函式(譯註:原文中這邊是寫cummingsify而不是try_to_cummingsify,你可以看原始碼就會了解兩者的關係,cummingsify是個一般函式而try_to_cummingsify是個callback,try_to_cummingsify會回傳什麼結果是靠cummingsify的隨機決定的,基本上可以將兩者視為一體。)正常工作的情況:
在這個情況中沒有callback失敗,所以控制權沿著callback路線往下傳遞。注意poem_done收到的結果是None,因為got_poem實際上不會return一個值。如果我們想要got_poem後面的callbacks可以存取這首詩,我們會修改got_poem讓它可以明確地回傳這首詩。
圖21顯示我們獲得了一首詩,但cummingsify拋出了一個GibberishError:
由於try_to_cummingsify callback重新拋出了GibberishError,控制權切換到errback路線,並且poem_failed以一個例外作為引數(當然包裝在Failure物件中)被呼叫。
之後,由於poem_failed不會拋出例外或者回傳Failure物件,poem_failed完成工作後控制權就切換回callback路線。如果我們希望poem_failed可以完全處理好錯誤,那回傳None是個很好的做法。相反的,如果我們想讓poem_failed採取一些行動,但繼續傳遞錯誤,我們可以改寫poem_failed讓它回傳err引數,並且繼續沿著errback路線處理。
注意一點,在目前的程式碼中got_poem與poem_failed都不可能出現失敗的狀況,所以errback的poem_done永遠不會被呼叫。但在任何情況下這樣做都是安全的,這樣做代表了一種「defensive (防禦性)」程式設計的例子,畢竟got_poem或poem_failed可能有我們不知道的bugs。由於addBoth方法確保不管deferred實例怎樣觸發,addBoth加入的特定函式都會執行,所以使用addBoth類似於try/except statement中的finally語句。
現在來看看這個例子,我們下載了詩歌,但cummingsify函式拋出了一個VauleError,如圖22:
除了got_poem收到原始版本的詩歌而不是轉換後的版本外,這與圖20相同。控制權的切換都發生在try_to_cummingsify callback裡面,它使用了try/except statement捕捉了ValueError,並回傳了原本的詩歌。這個deferred物件根本不會看到ValueError。
最後,我們在圖23將看到當我們嘗試從一個不存在的伺服器下載詩歌的情況:
和以前一樣,poem_failed回傳了None,所以控制權又切換到callback路線。
假設我們想要讓deferred實例捕捉到GibberishError 與ValueError這兩個例外,並且將它們送到errback路線。如果要保留程式目前的行為,我們後續的errback需要去檢查錯誤是否為ValueError,如果是,就藉由回傳原始的詩歌來處理它,這樣控制權就會回到callback路線,並且印出原始的詩歌。
但是有一個問題:errback不會得到原始詩歌,它只會得到由cummungsify函式拋出並打包為Failure物件的ValueError。為了讓這個errback處理這個錯誤,我們需要安排它來接收原始詩歌。
一種方法是修改cummingsify函式,使得例外中包含原始詩歌。這就是我們在5.1版本用戶端中所做的,程式碼在twisted-client-5/get-poetry-1.py中。我們把ValueError例外改為一個自訂的CannotCummingsify例外,它會將原始詩歌作為第一個引數。
如果cummingsify是一個在外部模組中真正的函式,那麼最好用另一個函式來包裝cummingsify,而該函式會捕捉所有非GibberishError的例外,並拋出CannotCummingsify來代替。有了這個新的方案,我們的poetry_main函式看起來會像這樣:
在這個新方案中,我們建立的每個deferred會有如圖24的結構:
來看看cummingsify_failed的errback:
我們使用了Failure物件的check方法來測試嵌入在Failure物件中的例外是否是CannotCummingsify的實例。如果是這樣,我們回傳異常的第一個引數(原始詩歌),用這種方式處理錯誤。由於回傳值不是Failure物件,所以控制權回到callback路線。否則,我們會回傳Failure物件本身,並沿著errback路線傳送(重新拋出)異常。如你在上面程式碼所見,例外可以作為Failure物件的value屬性。
圖25顯示了當我們收到CannotCummingsify例外時會發生什麼事情:
所以,當我們使用deferred實例時,我們可以選擇用try/except statement來處理例外,或者讓deferred實例把錯誤改送到errback中。
所以現在我們完全了解deferreds了,對吧?還沒,我們將在未來的部分探索更多deferreds的功能。但在Part 11我們將稍微繞道一下,先來實現我們Twisted版本的詩歌伺服器。
Client 5.0
現在我們按照Part 9的建議,為我們的詩歌用戶端添加一些轉換邏輯。但首先我要先跟各位懺悔:我不知道怎樣寫一個Byronification引擎。所以我將時做一個更簡單的轉換來代替,叫做Cummingsifier。Cummingsifier是一種演算法,它會讀取一首詩,然後以e.e. cummings的風格編寫後再將它回傳。這是Cummingsifier演算法的內容:def cummingsify(poem)
return poem.lower()
不幸的是,這個演算法簡單到永遠不會出錯,所以在twisted-client-5/get-poetry.py的5.0版用戶端中,我們修改了cummingsify,它會隨機執行下面其中一項操作:
- 返回詩歌被cummingsify修改後的版本。
- 拋出一個GibberishError。
- 拋出一個ValueError。
用這種方法,我們模擬了一個比較複雜的演算法,它有時候會以意想不到的方式失敗。
5.0版用戶端中其它有更改到的地方都在poetry_main函式中:
def poetry_main():
addresses = parse_args()
from twisted.internet import reactor
poems = []
errors = []
def try_to_cummingsify(poem):
try:
return cummingsify(poem)
except GibberishError:
raise
except:
print 'Cummingsify failed!'
return poem
def got_poem(poem):
print poem
poems.append(poem)
def poem_failed(err):
print >>sys.stderr, 'The poem download failed.'
errors.append(err)
def poem_done(_):
if len(poems) + len(errors) == len(addresses):
reactor.stop()
for address in addresses:
host, port = address
d = get_poetry(host, port)
d.addCallback(try_to_cummingsify)
d.addCallbacks(got_poem, poem_failed)
d.addBoth(poem_done)
reactor.run()
所以當程式從伺服器下載一首詩歌後,它可能有其中一種情況:
因此,當從伺服器上下載一首詩歌時,可能會出現如下情況:
- 印出詩歌被cummingsify修改(改小寫)後的版本。
- 印出 「Cummingsify failed」並附上原始的詩歌。
- 印出「The peom download failed」。
儘管我們保留了從多個伺服器下載的功能,但當你測試5.0版用戶端時,只使用一台伺服器,並且多執行幾次程式,你會更容易看到三種不同的結果。也可以嘗試連接沒有伺服器的埠。
我們會從get_poetry得到三種結果的Deferred,讓我們在圖19畫出每個Deferred上的callback/errback鏈:
注意由assCallback加入的pass_throuht errback,它會將它收到的所有Failure物件傳給下一個errback(poem_failed)。因此poem_failed就可以處理來自get_poetry(也就式deferred實例是以errback觸發)與cummingsify函式的錯誤了。
也要注意圖19中在deferred邊界周圍那些令人難以忘懷的美麗陰影,這不重要,我只是想告訴大家我發現在Inkscape中如何做到這點,預計未來還會有更多的陰影。
讓我們來分析一下可以觸發我們deferred的幾種方法,圖20顯示了我們獲得了一首詩,並且cummingsify函式(譯註:原文中這邊是寫cummingsify而不是try_to_cummingsify,你可以看原始碼就會了解兩者的關係,cummingsify是個一般函式而try_to_cummingsify是個callback,try_to_cummingsify會回傳什麼結果是靠cummingsify的隨機決定的,基本上可以將兩者視為一體。)正常工作的情況:
在這個情況中沒有callback失敗,所以控制權沿著callback路線往下傳遞。注意poem_done收到的結果是None,因為got_poem實際上不會return一個值。如果我們想要got_poem後面的callbacks可以存取這首詩,我們會修改got_poem讓它可以明確地回傳這首詩。
圖21顯示我們獲得了一首詩,但cummingsify拋出了一個GibberishError:
由於try_to_cummingsify callback重新拋出了GibberishError,控制權切換到errback路線,並且poem_failed以一個例外作為引數(當然包裝在Failure物件中)被呼叫。
之後,由於poem_failed不會拋出例外或者回傳Failure物件,poem_failed完成工作後控制權就切換回callback路線。如果我們希望poem_failed可以完全處理好錯誤,那回傳None是個很好的做法。相反的,如果我們想讓poem_failed採取一些行動,但繼續傳遞錯誤,我們可以改寫poem_failed讓它回傳err引數,並且繼續沿著errback路線處理。
注意一點,在目前的程式碼中got_poem與poem_failed都不可能出現失敗的狀況,所以errback的poem_done永遠不會被呼叫。但在任何情況下這樣做都是安全的,這樣做代表了一種「defensive (防禦性)」程式設計的例子,畢竟got_poem或poem_failed可能有我們不知道的bugs。由於addBoth方法確保不管deferred實例怎樣觸發,addBoth加入的特定函式都會執行,所以使用addBoth類似於try/except statement中的finally語句。
現在來看看這個例子,我們下載了詩歌,但cummingsify函式拋出了一個VauleError,如圖22:
除了got_poem收到原始版本的詩歌而不是轉換後的版本外,這與圖20相同。控制權的切換都發生在try_to_cummingsify callback裡面,它使用了try/except statement捕捉了ValueError,並回傳了原本的詩歌。這個deferred物件根本不會看到ValueError。
最後,我們在圖23將看到當我們嘗試從一個不存在的伺服器下載詩歌的情況:
和以前一樣,poem_failed回傳了None,所以控制權又切換到callback路線。
Client 5.1
在5.0版本用戶端中,我們使用普通的try/except statement在try_to_cummingsify callback中捕捉來自cummungsify的例外,而不是讓deferred實例捕捉他們。這個策略沒有什麼問題,但我們將會用不同的方式來做這件事情,理解它將對你很有幫助。假設我們想要讓deferred實例捕捉到GibberishError 與ValueError這兩個例外,並且將它們送到errback路線。如果要保留程式目前的行為,我們後續的errback需要去檢查錯誤是否為ValueError,如果是,就藉由回傳原始的詩歌來處理它,這樣控制權就會回到callback路線,並且印出原始的詩歌。
但是有一個問題:errback不會得到原始詩歌,它只會得到由cummungsify函式拋出並打包為Failure物件的ValueError。為了讓這個errback處理這個錯誤,我們需要安排它來接收原始詩歌。
一種方法是修改cummingsify函式,使得例外中包含原始詩歌。這就是我們在5.1版本用戶端中所做的,程式碼在twisted-client-5/get-poetry-1.py中。我們把ValueError例外改為一個自訂的CannotCummingsify例外,它會將原始詩歌作為第一個引數。
如果cummingsify是一個在外部模組中真正的函式,那麼最好用另一個函式來包裝cummingsify,而該函式會捕捉所有非GibberishError的例外,並拋出CannotCummingsify來代替。有了這個新的方案,我們的poetry_main函式看起來會像這樣:
def poetry_main():
addresses = parse_args()
from twisted.internet import reactor
poems = []
errors = []
def cummingsify_failed(err):
if err.check(CannotCummingsify):
print 'Cummingsify failed!'
return err.value.args[0]
return err
def got_poem(poem):
print poem
poems.append(poem)
def poem_failed(err):
print >>sys.stderr, 'The poem download failed.'
errors.append(err)
def poem_done(_):
if len(poems) + len(errors) == len(addresses):
reactor.stop()
for address in addresses:
host, port = address
d = get_poetry(host, port)
d.addCallback(cummingsify)
d.addErrback(cummingsify_failed)
d.addCallbacks(got_poem, poem_failed)
d.addBoth(poem_done)
在這個新方案中,我們建立的每個deferred會有如圖24的結構:
來看看cummingsify_failed的errback:
def cummingsify_failed(err):
if err.check(CannotCummingsify):
print 'Cummingsify failed!'
return err.value.args[0]
return err
我們使用了Failure物件的check方法來測試嵌入在Failure物件中的例外是否是CannotCummingsify的實例。如果是這樣,我們回傳異常的第一個引數(原始詩歌),用這種方式處理錯誤。由於回傳值不是Failure物件,所以控制權回到callback路線。否則,我們會回傳Failure物件本身,並沿著errback路線傳送(重新拋出)異常。如你在上面程式碼所見,例外可以作為Failure物件的value屬性。
圖25顯示了當我們收到CannotCummingsify例外時會發生什麼事情:
所以,當我們使用deferred實例時,我們可以選擇用try/except statement來處理例外,或者讓deferred實例把錯誤改送到errback中。
Summary
在Part 10我們更新了我們的詩歌用戶端,利用Deferred實例的能力來沿著鏈路由錯誤與正常結果。雖然這個範例是虛構的,但它確實說明了deferred實例中的控制流程如何根據每個階段的結果在callback與errback路線間來回切換。所以現在我們完全了解deferreds了,對吧?還沒,我們將在未來的部分探索更多deferreds的功能。但在Part 11我們將稍微繞道一下,先來實現我們Twisted版本的詩歌伺服器。
Suggested Exercises
- 圖25顯示了5.1版本用戶端中deferred實例能觸發的四種方法之一。畫出其他三種。
- 使用deferred模擬器去模擬5.0與5.1版本用戶端所有可能觸發的狀況。為了幫助你開始,這個模擬器程式可以這樣表示在5.0版本用戶端中try_to_cummingsify函式成功的情況:
r poem p r None r None r None r None
留言
張貼留言