对于简单接口使用函数而不是类


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而言,使用函数是很理想的。因为更容易藐视,相对于类而言定义起来也更加的简单。函数可以作为钩子来工作是因为Pythonfirst-class函数:在编程的时候函数,方法可以像其他的变量值一样被引用,或者被传递给其他的函数。

例如:你想定制defaultdict类的一些行为(详见第46项:使用内置的算法和数据结构)。这个数据结构允许你提供一个函数,这个函数会在字典中的缺省值被访问的时候调用。而且为字典中的这个缺省键来返回一个默认值。这里,我顶一个一个hook来打印每次缺省键的情况,并返回一个默认的值0.

def log_missing():
    print("Key added")
    return 0

给定一个初始化的字典和一组合理的增量,我可以导致log_missing函数执行并打印两次信息(这里是redorange)。

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项:了解闭包是怎样与变量作用域的联系)。

results matching ""

    No results matching ""