python 有许多内置的 API ,都允许调用者传入函数,以定制其行为。API在招待的时候,会通过这些挂钩(hook)函数, 回调函数内的代码。例如,list 类型的 sort 方法接受可选的 key 参数,用以指定每个索引位置上的值之间该如何排序。 下面这段代码,用 lambda 表达式充当key挂钩,以便根据每个名字的长度来排序:
names = ['Socrates', 'Archimedes', 'Plato', 'Aristotle']
names.sort(key=lambda x:len(x))
print(names)
其他编程语言可能会用抽象类来定义挂钩。然而在python中,很多挂钩只是无状态的函数,这此有明确的参数及返回值。用函数做挂钩是比较合适的,因为它们很容易就能描述出这个挂钩的功能,而且比定义一个类要简单。python中的函数之所以能充当挂钩,原因就在于,它是一级(first-class)对象,也就是说,函数与方法可以像语言中的其他值那样传递和引用。
例如,要定制 defaultdice 类(参见该书第46条) 的行为。这种数据结构允许使用者提供一个函数,以后在查询本字典时,如果里面没有待查的键,那就用这个函数为该键创建新值。当字典中没有待查询的键时,此函数必须返回那个键所应具备的默认值。下面定义的这个挂钩函数会在字典里找不到待查询的键时打印一条信息,并返回 0 ,以作为该键所对应的值。
def log_missing():
print('key added')
return 0
一开始,我们在字典里放入一系列键值对,并给出某些值的增量,然后,就可以两次触发log_missing函数,并在控制台里打印两次消息(一次是在查询‘red’键的时候,还有一次是在查询 ‘orange’ 键的时候)。
from collections import defaultdict
def log_missing():
print('key added')
return 0
current = {
'green':12, 'blue':3}
increments = [
('red', 5),
('blue', 17),
('orange', 9),
]
result = defaultdict(log_missing, current)
print('Before:', dict(result))
for key, amount in increments:
result[key] += amount
print('After: ', dict(result))
输出结果:
Before: {
'green': 12, 'blue': 3}
key added
key added
After: {
'green': 12, 'blue': 20, 'red': 5, 'orange': 9}
提供 log_missing 这样的函数,可以使api更易构建,也更易测试,因为它能够把附带的效果与确定的行为分隔开。例如,现在要给defaultdict 传入一个产生默认值的挂钩,并令其统计出该字典一共遇到了多少个缺失的键。一种实现方式是使用带状态的闭包(参见该书第15条)。下面定义的这个辅助函数就使用这种闭包作为产生默认值的挂钩函数:
def increment_with_report(current, increments):
added_count = 0
def missing():
nonlocal added_count # stateful closure
added_count += 1
return 0
result = defaultdice(missing, current)
for key, amount in increments:
result[key] += amount
return result, added_count
尽管defaultdict 并不知道 missing 挂钩函数里保存了状态,但是运行上面这个函数之后,依然可以产生预期的结果,也就是说,count的值会是2 。这就是令接口接受简单函数的又一个好处。把状态隐藏到闭包里面,稍后我们就可以很方便地为闭包集数添加新的功能。
resutl, count = increment_with_report(current, increments)
assert count == 2
把带状态的闭包函数用作挂钩有一个缺点,就是读起来要比无状态的函数难懂一些。还有个办法也能实现上述功能,那就是定义一个小型的类,把需要追踪的状态封装起来。
class CountMissing(object):
def __init__(self):
self.added = 0
def missing(self):
self.added += 1
return 0
在其他编程语言中,为了适配CountMissing的接口,我们可能要修改 defaultdice 类,而 Python 的函数则是一级函数,所以可以直接在 CountMissing 扑过去上面引用 CountMissing.missing 方法, 并将其传给 defaultdict, 以便用它作为默认值挂钩。 令方法满足函数接口,是相当容易的。
from collections import defaultdict
class CountMissing(object):
def __init__(self):
self.added = 0
def missing(self):
self.added += 1
return 0
current = {
'green':12, 'blue':3}
increments = [
('red', 5),
('blue', 17),
('orange', 9),
]
counter = CountMissing()
result = defaultdict(counter.missing, current) # Method ref
for key, amount in increments:
result[key] += amount
assert counter.added == 2
使用上述辅助类来改写带状态的闭包,确实要比 increment_with_report 函数清晰。但是,如果单看这个灰,我们依然不太容易理解 CountMissing 的意图。 CountMissing对象由谁来构建? missing 方法由谁来调用? 该类以后是否需要添加新的公共方法? 这些问题, 都必须等看过了 defaultdict 的用法之后,才能明白。
为了厘清这些问题,我们可以在Python代码中定义名为 __call__的特殊方法。 该方法使相关对象能够像函数那样得到调用。此外,如果把这样的补全传给内置的 callable 函数, 那么callable 函数会返回 True。
class BetterCountMissing(object):
def __init__(self):
self.added = 0
def __call__(self):
self.added += 1
return 0
counter = BetterCountMissing()
counter()
assert callable(counter)
下面这段代码,把 BetterCountMissing实例用作 defaultdict 的默认值挂钩, 以便记录该字典在查询过程中一共添加了多少个原来所没有的键:
counter = BetterCountMissing()
result = defaultdict(counter, current) # relies on __call__
for key, amount in increments:
result[key] += amount
assert counter.added == 2
上面这段代码要比 CountMissing.missing 清晰这么多。 __call__
方法表明:BetterCountMissing类的实例也会像函数那样,在合适的时候充当鞭个 API 的参数(例如,充当 API 的挂钩)。于是,刚读到这段代码的人就可以从这个方法开始,来理解 BetterCountMissing 类的主要功能。 __call__
方法强烈地暗示了该类的用途,它告诉我们,这个类的功能就相当于一个带有状态的闭包。
像上面这样修改之后中,defaultdict 仍然无需关注 __call__
方法得到调用时的效果,而是只要求使用者传入一个用来生成默认值的挂钩函数即可。在Python程序中,我们可以通过种种上手段来满足这种接受简单函数的接口,以实现自己想要的功能。
要点:
__call__
的特殊方法, 可以使类的实例能够像普通的Python函数那样得到调用。__call__
方法,而不要定义带状态的闭包(参见该书第15条)。这篇文章也是比较难理解的,我自己感觉也没有完全理解透彻。不过可以先放着,等过几天了再看一看这篇文章,到时如果有新的想法,再写出来与大家分享。