[Twisted] Part 10: Poetry Transformed

本文由Dave的Part 10: Poetry Transformed翻譯而成,你可以由Part 1開始閱讀這個系列的文章,也可以在這裡找到整個系列的目錄。

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,它會隨機執行下面其中一項操作:

  1. 返回詩歌被cummingsify修改後的版本。
  2. 拋出一個GibberishError。
  3. 拋出一個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()

所以當程式從伺服器下載一首詩歌後,它可能有其中一種情況:
因此,當從伺服器上下載一首詩歌時,可能會出現如下情況:

  1. 印出詩歌被cummingsify修改(改小寫)後的版本。
  2. 印出 「Cummingsify failed」並附上原始的詩歌。
  3. 印出「The peom download failed」。

儘管我們保留了從多個伺服器下載的功能,但當你測試5.0版用戶端時,只使用一台伺服器,並且多執行幾次程式,你會更容易看到三種不同的結果。也可以嘗試連接沒有伺服器的埠。

我們會從get_poetry得到三種結果的Deferred,讓我們在圖19畫出每個Deferred上的callback/errback鏈:
19.the%2Bdeferred%2Bchain%2Bin%2Bclient%2B5.0.png-Part 10: Poetry Transformed

圖19.5.0版本用戶端中的deferred鏈


注意由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的隨機決定的,基本上可以將兩者視為一體。)正常工作的情況:
20.when%2Bwe%2Bdownload%2Ba%2Bpoem%2Band%2Btransform%2Bit%2Bcorrectly.png-Part 10: Poetry Transformed

圖20.當我們下載了一首詩歌而且正確的轉換它


在這個情況中沒有callback失敗,所以控制權沿著callback路線往下傳遞。注意poem_done收到的結果是None,因為got_poem實際上不會return一個值。如果我們想要got_poem後面的callbacks可以存取這首詩,我們會修改got_poem讓它可以明確地回傳這首詩。

圖21顯示我們獲得了一首詩,但cummingsify拋出了一個GibberishError:
21.when%2Bwe%2Bdownload%2Ba%2Bpoem%2Band%2Bget%2Ba%2BGibberishError.png-Part 10: Poetry Transformed

圖21.當我們下載了一首詩歌而且得到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:
22.when%2Bwe%2Bdownload%2Ba%2Bpoem%2Band%2Bcummingsify%2Bfails.png-Part 10: Poetry Transformed

圖22.當我們下載一首詩歌而cummingsify函式失敗了


除了got_poem收到原始版本的詩歌而不是轉換後的版本外,這與圖20相同。控制權的切換都發生在try_to_cummingsify callback裡面,它使用了try/except statement捕捉了ValueError,並回傳了原本的詩歌。這個deferred物件根本不會看到ValueError。

最後,我們在圖23將看到當我們嘗試從一個不存在的伺服器下載詩歌的情況:
23.when%2Bwe%2Bcannot%2Bconnect%2Bto%2Ba%2Bserver.png-Part 10: Poetry Transformed

圖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的結構:
24.the%2Bdeferred%2Bchain%2Bin%2Bclient%2B5.1.png-Part 10: Poetry Transformed

圖24.5.1版本用戶端的deferred鏈


來看看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例外時會發生什麼事情:
25.when%2Bwe%2Bget%2Ba%2BCannotCummingsify%2Berror.png-Part 10: Poetry Transformed

圖25.當我們收到CannotCummingsify錯誤


所以,當我們使用deferred實例時,我們可以選擇用try/except statement來處理例外,或者讓deferred實例把錯誤改送到errback中。

Summary

在Part 10我們更新了我們的詩歌用戶端,利用Deferred實例的能力來沿著鏈路由錯誤與正常結果。雖然這個範例是虛構的,但它確實說明了deferred實例中的控制流程如何根據每個階段的結果在callback與errback路線間來回切換。

所以現在我們完全了解deferreds了,對吧?還沒,我們將在未來的部分探索更多deferreds的功能。但在Part 11我們將稍微繞道一下,先來實現我們Twisted版本的詩歌伺服器。

Suggested Exercises

  1. 圖25顯示了5.1版本用戶端中deferred實例能觸發的四種方法之一。畫出其他三種。
  2. 使用deferred模擬器去模擬5.0與5.1版本用戶端所有可能觸發的狀況。為了幫助你開始,這個模擬器程式可以這樣表示在5.0版本用戶端中try_to_cummingsify函式成功的情況:
    r poem p
    r None r None
    r None r None

留言