7.10 帶額外狀態(tài)信息的回調(diào)函數(shù)

2018-02-24 15:26 更新

問題

你的代碼中需要依賴到回調(diào)函數(shù)的使用(比如事件處理器、等待后臺任務(wù)完成后的回調(diào)等),并且你還需要讓回調(diào)函數(shù)擁有額外的狀態(tài)值,以便在它的內(nèi)部使用到。

解決方案

這一小節(jié)主要討論的是那些出現(xiàn)在很多函數(shù)庫和框架中的回調(diào)函數(shù)的使用——特別是跟異步處理有關(guān)的。為了演示與測試,我們先定義如下一個需要調(diào)用回調(diào)函數(shù)的函數(shù):

def apply_async(func, args, *, callback):
    # Compute the result
    result = func(*args)

    # Invoke the callback with the result
    callback(result)

實際上,這段代碼可以做任何更高級的處理,包括線程、進程和定時器,但是這些都不是我們要關(guān)心的。我們僅僅只需要關(guān)注回調(diào)函數(shù)的調(diào)用。下面是一個演示怎樣使用上述代碼的例子:

>>> def print_result(result):
...     print('Got:', result)
...
>>> def add(x, y):
...     return x + y
...
>>> apply_async(add, (2, 3), callback=print_result)
Got: 5
>>> apply_async(add, ('hello', 'world'), callback=print_result)
Got: helloworld
>>>

注意到 print_result() 函數(shù)僅僅只接受一個參數(shù) result 。不能再傳入其他信息。而當你想讓回調(diào)函數(shù)訪問其他變量或者特定環(huán)境的變量值的時候就會遇到麻煩。

為了讓回調(diào)函數(shù)訪問外部信息,一種方法是使用一個綁定方法來代替一個簡單函數(shù)。比如,下面這個類會保存一個內(nèi)部序列號,每次接收到一個 result 的時候序列號加1:

class ResultHandler:

    def __init__(self):
        self.sequence = 0

    def handler(self, result):
        self.sequence += 1
        print('[{}] Got: {}'.format(self.sequence, result))

使用這個類的時候,你先創(chuàng)建一個類的實例,然后用它的 handler() 綁定方法來做為回調(diào)函數(shù):

>>> r = ResultHandler()
>>> apply_async(add, (2, 3), callback=r.handler)
[1] Got: 5
>>> apply_async(add, ('hello', 'world'), callback=r.handler)
[2] Got: helloworld
>>>

第二種方式,作為類的替代,可以使用一個閉包捕獲狀態(tài)值,例如:

def make_handler():
    sequence = 0
    def handler(result):
        nonlocal sequence
        sequence += 1
        print('[{}] Got: {}'.format(sequence, result))
    return handler

下面是使用閉包方式的一個例子:

>>> handler = make_handler()
>>> apply_async(add, (2, 3), callback=handler)
[1] Got: 5
>>> apply_async(add, ('hello', 'world'), callback=handler)
[2] Got: helloworld
>>>

還有另外一個更高級的方法,可以使用協(xié)程來完成同樣的事情:

def make_handler():
    sequence = 0
    while True:
        result = yield
        sequence += 1
        print('[{}] Got: {}'.format(sequence, result))

對于協(xié)程,你需要使用它的 send() 方法作為回調(diào)函數(shù),如下所示:

>>> handler = make_handler()
>>> next(handler) # Advance to the yield
>>> apply_async(add, (2, 3), callback=handler.send)
[1] Got: 5
>>> apply_async(add, ('hello', 'world'), callback=handler.send)
[2] Got: helloworld
>>>

討論

基于回調(diào)函數(shù)的軟件通常都有可能變得非常復雜。一部分原因是回調(diào)函數(shù)通常會跟請求執(zhí)行代碼斷開。因此,請求執(zhí)行和處理結(jié)果之間的執(zhí)行環(huán)境實際上已經(jīng)丟失了。如果你想讓回調(diào)函數(shù)連續(xù)執(zhí)行多步操作,那你就必須去解決如何保存和恢復相關(guān)的狀態(tài)信息了。

至少有兩種主要方式來捕獲和保存狀態(tài)信息,你可以在一個對象實例(通過一個綁定方法)或者在一個閉包中保存它。兩種方式相比,閉包或許是更加輕量級和自然一點,因為它們可以很簡單的通過函數(shù)來構(gòu)造。它們還能自動捕獲所有被使用到的變量。因此,你無需去擔心如何去存儲額外的狀態(tài)信息(代碼中自動判定)。

如果使用閉包,你需要注意對那些可修改變量的操作。在上面的方案中,nonlocal 聲明語句用來指示接下來的變量會在回調(diào)函數(shù)中被修改。如果沒有這個聲明,代碼會報錯。

而使用一個協(xié)程來作為一個回調(diào)函數(shù)就更有趣了,它跟閉包方法密切相關(guān)。某種意義上來講,它顯得更加簡潔,因為總共就一個函數(shù)而已。并且,你可以很自由的修改變量而無需去使用 nonlocal 聲明。這種方式唯一缺點就是相對于其他Python技術(shù)而已或許比較難以理解。另外還有一些比較難懂的部分,比如使用之前需要調(diào)用 next() ,實際使用時這個步驟很容易被忘記。盡管如此,協(xié)程還有其他用處,比如作為一個內(nèi)聯(lián)回調(diào)函數(shù)的定義(下一節(jié)會講到)。

如果你僅僅只需要給回調(diào)函數(shù)傳遞額外的值的話,還有一種使用 partial() 的方式也很有用。在沒有使用 partial() 的時候,你可能經(jīng)常看到下面這種使用lambda表達式的復雜代碼:

>>> apply_async(add, (2, 3), callback=lambda r: handler(r, seq))
[1] Got: 5
>>>

可以參考7.8小節(jié)的幾個示例,教你如何使用 partial() 來更改參數(shù)簽名來簡化上述代碼。

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號