带有额外状态的回调函数

编码中常碰到的一个情形是需要编写回调函数,如事件处理函数等。一般的回调函数如常规函数,传递参数,返回计算的值。

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

    # Invoke the callback with the result
    callback(result)

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()函数仅接受一个参数,即result。 没有其他信息传递。当希望回调函数与环境的其他变量或部分交互时,信息的缺乏有时会带来问题。
在回调中携带额外状态或信息的一种方式是使用绑定的方法而不是一个简单的函数。比如说使用类定义。如下所示:

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

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

使用类实例可以携带其属性状态sequence,并在每次进行回调时获取其状态并更新。
若不使用类实例,可以使用闭包的方式捕获状态。如下:

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
>>>

另一种变体是使用协程,如下所示:

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

>>> 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
>>>

最后,也很重要的是,还可以使用额外的参数和partial函数将状态携带到回调中:

class SequenceNo:
    def __init__(self):
        self.sequence = 0

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

seq = SequenceNo()
from functools import partial
apply_async(add, (2, 3), callback=partial(handler, seq=seq))
[1] Got: 5
apply_async(add, ('hello', 'world'), callback=partial(handler, seq=seq))
[2] Got: helloworld

基于回调函数的软件经常冒着陷入混乱的风险。 问题的一部分是,回调函数通常与发出初始请求而导致执行回调的代码断开连接。 因此,有效地失去了发出请求和处理结果之间的执行环境。 如果希望回调函数继续执行涉及多个步骤的过程,则必须弄清楚如何保存和恢复关联的状态。

实际上有两种主要方法可用于捕获和携带状态。 可以在类实例上携带它(附加到绑定方法上),也可以在闭包中携带它(内部函数)。 在这两种技术中,闭包可能更轻便,更自然,因为它们仅由函数构建。 它们还自动捕获所有正在使用的变量。 因此,不必担心需要存储确切的状态(由代码自动确定)。

使用协程作为回调处理程序很有趣,因为它与闭包方法密切相关。 从某种意义上说,它甚至更干净,因为它只有一个函数。 此外,变量可以自由修改,而不必担心nonlocal声明。

潜在的缺点是协程不像Python的其他部分那样容易理解。 还有一些棘手的问题,例如在使用协程之前需要在协程上调用next()。 在实践中,这很容易忘记。 尽管如此,协程在这里还有其他潜在用途,例如内联回调(inlined callback)的定义。

posted @ 2019-12-04 23:33  Jeffrey_Yang  阅读(228)  评论(0编辑  收藏  举报