对于简单接口使用函数而不是类
Python
中的许多内置的API
都允许你通过向函数传递参数来自定义行为。这些被API
使用的hooks
将会在它们运行的时候回调给你的代码。例如:list
类型的排序方法中有一个可选的key
参数来决定排序过程中每个下标的值。这里,我使用一个lambda
表达式作为这个键钩子,根据名字中字符的长度来为这个集合排序。
names = ['Socrates', 'Archimedes', 'Plato', 'Aristotle']
names.sort(key=lambda x: len(x))
print(names)
>>>
['Plato', Socrates', 'Aristotle', 'Archimedes']
在其他的编程语言中,你可能期望一个抽象类作为这个hooks
。但是在Python
中,许多的hooks
都是些无状态的有良好定义参数和返回值的函数。而对于hooks
而言,使用函数是很理想的。因为更容易藐视,相对于类而言定义起来也更加的简单。函数可以作为钩子来工作是因为Python
有first-class
函数:在编程的时候函数,方法可以像其他的变量值一样被引用,或者被传递给其他的函数。
例如:你想定制defaultdict
类的一些行为(详见第46项:使用内置的算法和数据结构)。这个数据结构允许你提供一个函数,这个函数会在字典中的缺省值被访问的时候调用。而且为字典中的这个缺省键来返回一个默认值。这里,我顶一个一个hook
来打印每次缺省键的情况,并返回一个默认的值0.
def log_missing():
print("Key added")
return 0
给定一个初始化的字典和一组合理的增量,我可以导致log_missing
函数执行并打印两次信息(这里是red
和orange
)。
current = {'green': 12, 'blue': 3}
incremetns = [
('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: {'orange': 9, 'green': 12, 'blue': 20, 'red': 5}
提供像上面的log_missing
函数可以使得API
更容易被构建和测试,因为它们从确定性的行为中分离了副作用。例如:你想转给defaultdict
的默认值hook
来为缺省的key
计数。一个实现的方式就是使用状态闭包(详见第15
项:了解闭包与变量作用于的联系)。这里,我定义了一个使用了这样的一个闭包的工具函数来作为默认值hook
。
def increment_with_report(current, increments):
added_count = 0
def missing():
nonlocal added_count # 状态闭包
added_count += 1
return 0
result = defaultdict(missing, current)
for key, amount in crements:
result[key] += amount
return result, added_count
运行代码可以产生预期的结果2
,即使defaultdict
对缺省的hook
状态保持情况一无所知。这也是接口接受简单函数而受益的有一个例子。可以通过在随后的闭包中隐藏状态来很容易地添加功能。
result, count = increment_with_report(current, increments)
assert count == 2
为状态hook
定义一个闭包的问题就在于:相比于无状态函数而言其可阅读性变差了。另一个方法就是定义一个小类来封装你想追踪的状态信息。
class CountMissing(object):
def __init__(self):
self.added = 0
def missing(self):
self.added += 1
return 0
在其他的编程语言中,你可能预期现在的defaultdict
被修改来容纳CountMissing
这个接口。但是在Python
中,感谢first-class
函数的存在,你可以直接引用CountMissing.missing
方法,将其作为一个对象作为默认hook
来传给defaultdict
函数。而实现这样的一个函数来满足需求也是如此的简单。
counter = CountMissing()
result = defaultdict(counter.missing, current)
for key, amount in increments:
result[key] += amount
assert counter.added == 2
使用像这样的一个工具类来提供状态闭包相对于前面的increment_with_report
函数更为清晰,简洁。然而,独立地看,CountMissing
类是干什么的还不是特别的明显。是谁够早了CountMissing
对象呢?谁调用了missing
方法?类中的那些公共方法又被添加到功能区了呢?知道你看到了使用它的defaultdict
函数,你才会明白到底是干什么的,而之前都是不知道的。
为了改善这个问题,Python
允许类来定义__call__
这个特殊的方法。它允许一个对象像被函数一样来被调用。这样的一个实例也引起了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函数的默认的hook值来追踪缺省值被添加的次数。
counter = BetterCountMissing()
result = defaultdict(counter, current)
for key, amount in increments:
result[key] += amount
assert counter.added == 2
现在,比单纯的使用CountMissing.missing
的案例清晰多了吧。__call__
方法表明本类将会在某处作为函数的参数来使用时也是可以的。 BetterCountMissing
类使得第一次看这段代码的读者更好的把握其行为。对于该类来追踪状态闭包的这个行为提供了强有力的提示。
最好的是,当你调用__call__
方法的时候,defaultdict函数仍然对要发生什么不甚了解。defaultdict
需要的仅仅是一个函数作为其默认值hook
就足够了,不会管你传进来的到底是什么函数。Python
提供了很多的方式来来满足你需要完成的依赖于简单函数接口的事务,这很方便。
备忘录
- 在
Python
中,不需要定义或实现什么类,对于简单接口组件而言,函数就足够了。 Python
中引用函数和方法的原因就在于它们是first-class
,可以直接的被运用在表达式中。- 特殊方法
__call__
允许你像调用函数一样调用一个对象实例。 - 当你需要一个函数来维护状态信息的时候,考虑一个定义了
__call__
方法的状态闭包类哦(详见第15
项:了解闭包是怎样与变量作用域的联系)。