python数据结构算法题一百一十九:将单方法的类转换为函数

问题
你有一个除 init() 方法外只定义了一个方法的类。为了简化代码,你想将它转换成一个函数。
解决方案
大多数情况下,可以使用闭包来将单个方法的类转换成函数。举个例子,下面示例中的类允许使用者根据某个模板方案来获取到 URL 链接地址。

class UrlTemplate:
def __init__(self, template):
self.template = template
def open(self, **kwargs):
return urlopen(self.template.format_map(kwargs))
# Example use. Download stock data from yahoo
yahoo = UrlTemplate('http://finance.yahoo.com/d/quotes.csv?s={names}&f=
,
→{fields}')
for line in yahoo.open(names='IBM,AAPL,FB', fields='sl1c1v'):
print(line.decode('utf-8'))

这个类可以被一个更简单的函数来代替:

def opener(**kwargs):
return urlopen(template.format_map(kwargs))
return opener
# Example use
yahoo = urltemplate('http://finance.yahoo.com/d/quotes.csv?s={names}&f=
,
→{fields}')
for line in yahoo(names='IBM,AAPL,FB', fields='sl1c1v'):
print(line.decode('utf-8'))

讨论
大部分情况下,你拥有一个单方法类的原因是需要存储某些额外的状态来给方法使用。比如,定义 UrlTemplate 类的唯一目的就是先在某个地方存储模板值,以便将来可以在 open() 方法中使用。
使用一个内部函数或者闭包的方案通常会更优雅一些。简单来讲,一个闭包就是一个函数,只不过在函数内部带上了一个额外的变量环境。闭包关键特点就是它会记住自己被定义时的环境。因此,在我们的解决方案中,opener() 函数记住了 template
参数的值,并在接下来的调用中使用它。
任何时候只要你碰到需要给某个函数增加额外的状态信息的问题,都可以考虑使用闭包。相比将你的函数转换成一个类而言,闭包通常是一种更加简洁和优雅的方案。

带额外状态信息的回调函数
问题
你的代码中需要依赖到回调函数的使用 (比如事件处理器、等待后台任务完成后的回调等),并且你还需要让回调函数拥有额外的状态值,以便在它的内部使用到。
解决方案
这一小节主要讨论的是那些出现在很多函数库和框架中的回调函数的使用——特别是跟异步处理有关的。为了演示与测试,我们先定义如下一个需要调用回调函数的函数:

# Compute the result
result = func(*args)
# Invoke the callback with the result
callback(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 。不能再传入其他信息。而当你想让回调函数访问其他变量或者特定环境的变量值的时候就会遇到麻烦。
为了让回调函数访问外部信息,一种方法是使用一个绑定方法来代替一个简单函数。比如,下面这个类会保存一个内部序列号,每次接收到一个 result 的时候序列号加 1:

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

使用这个类的时候,你先创建一个类的实例,然后用它的 handler() 绑定方法来做为回调函数:

>>> apply_async(add, (2, 3), callback=r.handler)
[1] Got: 5
>>> apply_async(add, ('hello', 'world'), callback=r.handler)
[2] Got: helloworld
>>>
第二种方式,作为类的替代,可以使用一个闭包捕获状态值,例如:
sequence = 0
def handler(result):
nonlocal sequence
sequence += 1
print('[{}] Got: {}'.format(sequence, result))
return handler

下面是使用闭包方式的一个例子:

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

还有另外一个更高级的方法,可以使用协程来完成同样的事情:

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

对于协程,你需要使用它的 send() 方法作为回调函数,如下所示:

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

讨论
基于回调函数的软件通常都有可能变得非常复杂。一部分原因是回调函数通常会跟请求执行代码断开。因此,请求执行和处理结果之间的执行环境实际上已经丢失了。如果你想让回调函数连续执行多步操作,那你就必须去解决如何保存和恢复相关的状态信息了。
至少有两种主要方式来捕获和保存状态信息,你可以在一个对象实例 (通过一个绑定方法) 或者在一个闭包中保存它。两种方式相比,闭包或许是更加轻量级和自然一点,因为它们可以很简单的通过函数来构造。它们还能自动捕获所有被使用到的变量。因
此,你无需去担心如何去存储额外的状态信息 (代码中自动判定)。
如果使用闭包,你需要注意对那些可修改变量的操作。在上面的方案中,nonlocal声明语句用来指示接下来的变量会在回调函数中被修改。如果没有这个声明,代码会报错。
而使用一个协程来作为一个回调函数就更有趣了,它跟闭包方法密切相关。某种意义上来讲,它显得更加简洁,因为总共就一个函数而已。并且,你可以很自由的修改变量而无需去使用nonlocal 声明。这种方式唯一缺点就是相对于其他 Python 技术而言或许比较难以理解。另外还有一些比较难懂的部分,比如使用之前需要调用 next() ,实际使用时这个步骤很容易被忘记。尽管如此,协程还有其他用处,比如作为一个内联回调函数的定义 (下一节会讲到)。
如果你仅仅只需要给回调函数传递额外的值的话,还有一种使用 partial() 的方式也很有用。在没有使用 partial() 的时候,你可能经常看到下面这种使用 lambda 表达式的复杂代码:

[1] Got: 5
>>>

可以参考 7.8 小节的几个示例,教你如何使用 partial() 来更改参数签名来简化上述代码。

  • 17
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值