函数装饰器和闭包


装饰器基础

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

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

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

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

两种写法的最终结果完全一样,上述两个代码片段执行完毕后得到的target不一定是原来的那个target函数。而是decorate(target)返回的函数。
为了确认被装饰的函数会被替换,请参考下面代码片段:

>>> def deco(func):
...     def inner():
...         print('running inner()')
...     return inner
...
>>> @deco
... def target():
...     print('running target()')
...     
>>> target()
running inner()
>>> target
<function deco.<locals>.inner at 0x10bdf8d90>

<function deco.<locals>.inner at 0x10bdf8d90>我们就可以看出,target现在是inner的引用。
严格上来说,装饰器只是语法糖。如上所示,装饰器可以像常规的可调用对象那样调用,其参数是另一个函数。
综上,装饰器的一大特性是,能把被装饰的函数替换成其他函数。第二个特性是,装饰器在加载模块时立即执行。也就是,装饰器在倒入模块时立即执行,而被装饰的函数只在明确调用时运行。

变量作用域规则

参考下面代码段:

>>> b=6
>>> def f2(a):
...     print(a)
...     print(b)
...     b=9
...     
>>> f2(3)
3
Traceback (most recent call last):
  File "<input>", line 1, in <module>
  File "<input>", line 3, in f2
UnboundLocalError: local variable 'b' referenced before assignment
>>> b
6

这样错误的原因是,Python在编译函数的定义体时,判断b是局部变量,因为在函数中给它赋值了。Python会尝试从本地环境获取b。后面调用f2(3)时,发 的定义提会获取并打印局部变量a的值,但是尝试获取局部变量b的值时,发现b没有绑定值。 Python不要求声明变量,但是假定在函数定义体中赋值的变量是局部变量,如果在函数赋值中想让解释器把b当成全局变量,需要使用global声明。

闭包

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

>>> 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

averager函数中,series是自由变量。这是一个技术术语,指未在本地作用域中绑定的变量。继续查看下述代码:

>>> avg.__code__.co_varnames
('new_value', 'total')
>>> avg.__code__.co_freevars
('series',)
>>> avg.__closure__
(<cell at 0x10bb59828: list object at 0x10bbb5b88>,)
>>> avg.__closure__[0].cell_contents
[10, 11, 12]

series的绑定在返回的avg函数的__closure__(函数闭包)属性中.avg.__closure__中的各个元素对应于avg.__code__.co_freevars中的一个名称。 综上,闭包是一种函数,它会保留定义函数时存在的自由变量的绑定,这样调用函数时,虽然定义作用域不可用了,但仍然能使用那些绑定。 注意,只有嵌套在其他函数中但函数才可能需要处理不在全局作用域中但外部变量。

nonlocal声明

参考下述代码段:

>>> def make_averager():
...     count = 0
...     total = 0
...     
>>> 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):
  File "<input>", line 1, in <module>
  File "<input>", line 5, in averager
UnboundLocalError: local variable 'count' referenced before assignment

在当count是数组或任何不可变类型时,count+=1语句作用其实与count=count+1一样,因此我们在averager但定义体中为count赋值了,这会把count变成局部变量。total变量也受这个问题的影响。
在我们上个版本中没有这个问题,因为我们没有给series赋值,只是调用了series.append并没有传给sumlen,也就是说,我们利用了列表是可变对象的这一事实。 但是对于数字、字符串、元组等不可变类型来说,只能读取,不能更新。如果尝试重新绑定的话,它就不是自由变量了,因此不会被保存在闭包中。
python3中引入的nolocal解决了这个问题,它的作用是把变量标记为自由变量,即使在函数中为其赋新值了,也会变成自由变量。

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

装饰器实现

参考如下示例,打印出被转世函数的执行时间,传入参数及结果。

>>> import time
...
...
... def clock(func):
...     def clocked(*args):
...         t0 = time.perf_counter()
...         result = func(*args)
...         elapsed = time.perf_counter() - t0
...         name = func.__name__
...         arg_str = ', '.join(repr(arg) for arg in args)
...         print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result))
...         return result
...
...     return clocked
...
...
... @clock
... def snooze(seconds):
...     time.sleep(seconds)
...
>>> snooze(4)
[4.00239920s] snooze(4) -> None

上述代码相当于:

clock(snooze(4))

这是装饰器的典型行为:把被装饰的函数替换成新函数,二者接受相同的参数,而且返回被装饰的函数本该返回的值,同时还会做写额外操作。
上述装饰器有个缺点,遮盖了被装饰函数的__name____doc__属性。可以使用functools.wraps装饰器把相关属性从func复制到clocked中,如下:

import time
import functools


def clock(func):
    @functools.wraps(func)
    def clocked(*args):
        t0 = time.perf_counter()
        result = func(*args)
        elapsed = time.perf_counter() - t0
        name = func.__name__
        arg_str = ', '.join(repr(arg) for arg in args)
        print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result))
        return result

    return clocked

参数化装饰器

可以创建一个装饰器工厂函数,把参数传给它,返回一个装饰器,然后再把它应用到要装饰的函数上,如下所示:

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()')


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

其中:

  • decorate这个内部函数是真正的装饰器,它的参数是一个函数。
  • register是装饰器的工厂函数,因此返回decorate
  • @register工厂函数必须作为函数调用,并且传入所需的参数。

这里的关键是,register()要返回decorate,然后把它应用到被装饰的函数上。 或者如下方式:

import logging


def use_logging(level):
    def decorator(func):
        def wrapper(*args, **kwargs):
            if level == "warn":
                logging.warn("%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)