FluentPythonCh07

5th July 2017 at 10:07am

这章讲的几个点:

  • How Python evaluates decorator syntax
  • How Python decides whether a variable is local
  • Why closures exist and how they work
  • What problem is solved by nonlocal

看完之后可以实现:

  • Implementing a well-behaved decorator
  • Interesting decorators in the standard library
  • Implementing a parameterized decorator

由于 decorator 这个词的命名,跟 设计模式 中的装饰器没什么关系,它与编译器领域中的概念相近。因此下面的笔记中我不用「装饰器」一词,而是直接用 decorator 来表达。

Decorators 101

decorator 就是个语法糖:

@decorate
def target():
    print('running target()')

效果等同于:

def target():
    print('running target()')
target = decorate(target)

When Python Executes Decorators

一个 decorated function(比如上一节中的 decorate),在 import time(也就是它所处的模块被加载时)就被执行了。比如:

def register(func):
    print('running register(%s)' % func.__name__)
    return func

@register
def f1():
    print('running f1()')

上面这段代码,被解释器加载时就会输出 running register(f1),在 f1 被调用时才输出 running f1()

Decorator-Enhanced Strategy Pattern

怎样用 decorator 来更优雅地解决上一章中的问题,即用 decorator 来获得几个 promo 函数对象,以加入 promos 列表:

# old solution
promos = [fidelity_promo, bulk_item_promo, ...]

# new solution
promos = []
def promotion(promo_func):
    promos.append(promo_func)
    return promo_func

@promotion
def fidelity(order):
    """5% discount for customers with 1000 or more fidelity points"""
    # ...

@promotion
def bulk_item(order):
    """10% discount for each LineItem with 20 or more units"""
    # ...

Variable Scope Rules

这节只讲了一个简单的规则。详细的规则可以参考 官方文档。这节的内容比较重要。

Python 使用的是块作用域。什么是一个 呢?

A block is a piece of Python program text that is executed as a unit. The following are blocks: a module, a function body, and a class definition.

所以函数体是一个块,但是 if 的分支语句不是一个块。

Python 解释器如何解释一个变量呢?它会先从本地块开始找,如果找不到,就一层一层往更外面的块作用域找,最后找到全局作用域。如果还没有,就会抛出 NameError。如果一个函数体里面定义了一个变量,但是在定义它之前就使用了,那会抛出一个 UnboundLocalError

a = 2

def f1():
    print(a)    # 会抛 UnboundLocalError
    a = 3

a 被解释器认为是当前函数作用域中的变量,但是它还没被定义,所以会抛 UnboundLocalError

JavaScript 在这块的规则与 Python 类似,但是 JavaScript 可以声明而不定义变量(var a;)(Python 没有这种语法),所以它有一个 Variable Hoisting 机制来解释这种问题。同时 JavaScript 对错误非常容忍,访问同个作用域未初始化的变量时返回 undefined,而不会像 Python 这样抛 UnboundLocalError

有两个关键字可以打破这个解析变量名的过程:globalnonlocal。如果你给一个变量声明了 global,那解释器会直接在全局块找这个变量;如果声明了 nonlocal,那解释器就不会在这个声明所在的块里面去找变量,而是直接在最靠近的块去找:

a = 2

def f1():
    global a    # a 此时指全局块中的 a
    print(a)    # 这里不会抛异常,会输出 2
    a = 3       # 修改的是全局的 a

nonlocal 的例子在下一节的 closure 中会讲。

Closures

书里面用了两个例子对比,来说明什么是 closure。

如果你需要实现一个动态计算平均数的功能,你会怎样实现呢?效果如下:

>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0

即是,你需要在 avg 这个计算过程以外,有一个地方存放之前的数据(在这个例子中的 10, 11, 12)。

第一种实现:用 callable class instance 来保存中间变量 series,中间数据随着 avg 变量的存在而存在:

class Averager():
    def __init__(self):
        self.series = []
    def __call__(self, new_value):
        self.series.append(new_value)
        total = sum(self.series)
        return total/len(self.series)

>>> avg = Averager()
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0

第二种实现,用 closure:

def make_averager():
    series = []
    def averager(new_value):
        series.append(new_value)
        total = sum(series)
        return total/len(series)
    return averager

>>> avg = make_averager()
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0

closure 这个例子很有意思,它把中间数据 series 放在计算过程 averager 的外面一层的函数中。而 Python 是一门动态语言,使用了引用计数来做垃圾回收;虽然 make_average 执行完了,但是只要 averager 函数对象和 series 变量可以被引用到,那么它们就不会被回收。

所以什么是 closure 呢?一图胜千言:

To summarize: a closure is a function that retains the bindings of the free variables that exist when the function is defined, so that they can be used later when the function is invoked and the defining scope is no longer available.

free variables 是那些它自己的 local scope 已经被销毁了的变量。上面的 series 可以通过 avg.__code__.co_freevars, avg.__closure__ 访问到。

Closure 跟匿名函数经常混在一起,是因为有些语言的闭包经常使用匿名函数来实现,比如 JavaScript。在闭包中,使用匿名函数或者具名函数都是可以的,比如前面的 Python 例子就用了具名函数 averager。(不过 Python 语言没有匿名函数的特性)

The nonlocal Declaration

上面已经讲到 nonlocal 关键字的含义。那么什么时候需要用它?

def make_averager():
    count = 0
    total = 0
    def averager(new_value):
        count += 1
        total += new_value
        return total / count
    return averager

>>> avg = make_averager()
>>> avg(10)
Traceback (most recent call last):
  ...
UnboundLocalError: local variable 'count' referenced before assignment
>>>

这里为什么会抛异常呢?原因是 count, total 都是整数,是不可变类型。意味着它们没有实现 __iadd__ 函数,于是 count += 1 等同于 count = count + 1参考),于是在 averager 函数块里又定义了一个新变量 count。但是 count + 1 又提前使用了这个变量,于是报 UnboundLocalError

解决办法是使用 nonlocal

def make_averager():
    count = 0
    total = 0
    def averager(new_value):
        nonlocal count, total
        count += 1
        total += new_value
        return total / count
    return averager

但是这个关键字只在 Python 3 才有。Python 2 的话需要一些 walkaround,比如在上层块里面定义一个词典(可变类型)供下层块使用。

Implementing a Simple Decorator

这节定义了一个计算函数运行时间的 decorator,然后具体使用上了。比较简单。但是有一个问题是,被 decorated 的函数,它的 __name__, __doc__ 等变量也被替换了,于是会有:

@clock
def snooze(seconds):
    time.sleep(seconds)

>>> snooze.__name__
'clocked'

这个时候可以用标准库的 functools.wraps decorator 来解决,它会把被装饰的函数的几个属性拷过去:

def clock(func):
    @functools.wraps(func)
    def clocked(*args, **kwargs):
        # ...

@clock
def snooze(seconds):
    time.sleep(seconds)

>>> snooze.__name__
'snooze'

Decorators in the Standard Library

Memoization with functools.lru_cache

第一个介绍的是 functools.lru_cache。这是一个非常方便的装饰器,对于一些运行缓慢的函数,可以把运行结果缓存起来。比如下面的 fibonacci 函数:

def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n-2) + fibonacci(n-1)

if__name__=='__main__':
    print(fibonacci(6))

如果不对 fibonacci 的计算结果做缓存,这个计算是非常慢的,因为有很多不必要的重复计算(fibonacci(4) 就算了 2 次)。

fibonacci 加了 @functools.lru_cache() 装饰器,可以很高效率地提高速度,减少了不必要的计算。

lru_cache 缓存的依据是函数参数,默认缓存 128 个结果。同时注意它是个返回 decorator 函数的函数,所以你在加 @functools.lru_cache() 时,不能漏掉括号。

Generic Functions with Single Dispatch

我对 singledispatch 的理解还不够深,感觉它似乎有更多内涵。同时它跟 ABC (abstract base class) 那套机制有关系。现在不是太理解,书后面会讲 ABC。

singledispatch 针对的场景是,一些需要理解参数类型的函数过程。它可以实现一个函数针对不同参数有不同的行为:

>>> fun("test.", verbose=True)
Let me just say, test.
>>> fun(42, verbose=True)
Strength in numbers, eh? 42
>>> fun(['spam', 'spam', 'eggs', 'spam'], verbose=True)
Enumerate this:
0 spam
1 spam
2 eggs
3 spam
>>> fun(None)
Nothing.
>>> fun(1.23)
0.615

singledispatch 可以你不需要写一堆 if/else 来判断类型走分支。具体的实现方式看文档就可以了,这里不描述。

这种场景看起来跟面向对向中的接口(Interface)类似,为不同的类型定义一个共同的接口。Python 也可以有类似的实现,但是 singledispatch 有一些优势:

  1. 对于一些内置类型,你不好去给它实现接口函数。比如在 int 类型中实现一个新的方法,这是不好的实践;如果继承一个新类出来,又把问题搞复杂了。
  2. Python 提倡 duck typing,即不关心具体的类型而关注协议(比如内置的 len, sort 函数),用 singledispatch 可以实现这种 generic function(比如前面代码例子中的 fun),而且不需要像内置函数一样要求在各个类中实现 dunder 函数(比如 __len__)。

扩展阅读:

  1. PEP 443 -- Single-dispatch generic functions: 讲了这个设计的由来,以及一些相近的实现(没太看明白)
  2. What single-dispatch generic functions mean for you: 它讲述了一些具体的应用场景(UI widgets, serialization formats and protocol handlers),以及怎样在 class method 中使用 singledispatch