函数装饰器和闭包


装饰器基础知识

装饰器是可调用对象,其参数是另一个函数(被装饰的函数)。装饰器可能会处理被装饰的函数,然后把它返回,或者将其替换成另一个函数或可调用对象。 假如有一个名为decorate的装饰器:

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

上述代码的效果与下述写法一样:

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

两种写法的最终结果一样:上述两个代码片段执行完毕后得到的target不一定是原来那个target函数,而是decorate(target)返回的函数。
严格来说,装饰器只是语法糖。装饰器可以像常规的可调用对象那样调用,其参数是另一个函数。有时,这样更方便,尤其是元编程(在运行时改变程序行为)时。
综上,装饰器的一大特征是,能把被装饰的函数替换为其他函数。第二个特性是,装饰器在加载模块时立即执行,这通常是在导入时(即python加载模块时。

闭包

闭包时指延伸了作用域的函数,其中包含函数定义体中引用、但是不在定义体中定义的非全局变量。 参考下面函数:

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

上面函数的作用时计算平均值,但是其中的seriesmake_averager函数的局部变量,因为那个函数的定义体中初始化了series:series = []。可是,调用avg(10) 时,make_averager函数已经返回了,而它的本地作用域也一去不复返了。
averager函数中,series是自由变量(free variable)。这是一个技术术语,指未在本地 作用域中绑定的变量。如下图: make_averager averager 的闭包延伸到那个函数的作用域之外,包含自由变量 series 的绑定
审查返回的averager对象,我们发现 Python 在 __code__ 属性(表示编译后的函数定义体)中保存局部变量和自由变量的名称:

>>> avg.__code__.co_varnames
('new_value', 'total')
>>> avg.__code__.co_freevars
('series',)

series的绑定在返回的avg函数的__closure__属性中。avg.__closure__中的各个元素对应于avg.__code__.co_freevars中的一个名称。这些元素是cell 对象,有个cell_ contents属性,保存着真正的值。这些属性的值如下所示。

>>> avg.__code__.co_freevars
('series',)
>>> avg.__closure__
(<cell at 0x10b30ab58: list object at 0x10b525348>,)
>>> avg.__closure__[0].cell_contents
[10, 11, 12]

综上,闭包时一种函数,它会保留定义函数时存在的自由变量的绑定,这样调用函数时,虽然定义作用域不可用了,但是仍然能使用那些绑定。 注意,只有嵌套在其他函数中的函数才可能需要处理不在全局作用域中的外部变量。

nonlocal声明

参考下面代码片段:

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

    return averager

在调用时,会得到如下结果UnboundLocalError: local variable 'count' referenced before assignment 问题是,当count是数字或任何不可变类型时,count += 1语句的作用与count = count + 1一样。因此我们在averager的定义体中为count赋值了,这会把count变成局部变量,total变量也受这个问题影响。
第一个版本的示例没遇到这个问题,因为我们没有给series赋值,我们只是调用series.append,并把它传给 sumlen。也就是说,我们利用了列表是可变的对象这一事实。
但是对数字、字符串、元组等不可变类型来说,只能读取,不能更新。如果尝试重新绑 定,例如count = count + 1,其实会隐式创建局部变量count。这样,count就不是自由变量了,因此不会保存在闭包中。
为了解决这个问题,Python 3 引入了nonlocal声明。它的作用是把变量标记为自由变量, 即使在函数中为变量赋予新值了,也会变成自由变量。如果为nonlocal声明的变量赋予新值,闭包中保存的绑定会更新。最新版 make_averager的正确实现如下所示:

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 把被装饰的函数作为第一个参数传给装饰器函数。那怎么让装饰器接受其他参数呢?答案是:创建一个装饰器工厂函数,把参数传给它,返回一个装饰器,然后再把它应用到要装饰的函数上。如下所示:

registry = set()
def register(active=True):
    def decorate(func):
        print('running register(active=%s)->decorate(%s)'
              % (active, func))
        if active:
            registry.add(func)
        else:
            registry.discard(func)
        return func

    return decorate

@register(active=False)
def f1():
    print('running f1()')

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

def f3():
    print('running f3()')

或者如下类似:

def use_logging(level):
    def decorator(func):
        def wrapper(*args, **kwargs):
            if level == "warn":
                logging.warn("%s is running" % func.__name__)
            elif level == "info":
                logging.info("%s is running" % func.__name__)
            return func(*args)
        return wrapper

    return decorator

@use_logging(level="warn")
def foo(name='foo'):
    print("i am %s" % name)

foo()

上面的use_logging是允许带参数的装饰器。它实际上是对原有装饰器的一个函数封装,并返回一个装饰器。我们可以将它理解为一个含有参数的闭包。当我们使用@use_logging(level="warn")调用的时候,Python能够发现这一层的封装,并把参数传递到装饰器的环境中。@use_logging(level="warn")等价于@decorator