函数装饰器和闭包

在23中设计模式中,给出关于装饰器模式的定义是:就是对已经存在的某些类进行装饰,以此来扩展或者说增强函数的一些功能;然而在python中,装饰器使用一种语法糖来实现。不知道你是否知道nonlocal关键字以及闭包,但是如果你想自己实现函数装饰器,那就必须了解闭包的方方面面,因此也就需要知道 nonlocal
要理解装饰器,就要知道下面问题:

  • Python 如何计算装饰器句法
  • Python 如何判断变量是不是局部的
  • 闭包存在的原因和工作原理
  • nonlocal 能解决什么问题

一、装饰器基本知识

装饰器是可调用的对象,其参数是另一个函数(被装饰的函数)。 装饰器可能会处理被装饰的函数,然后把它返回,或者将其替换成另一个函数或可调用对象。
活不多说来看下面例子:

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

❶ deco 返回 inner 函数对象。
❷ 使用 deco 装饰 target。
❸ 调用被装饰的 target 其实会运行 inner。
❹ 审查对象,发现 target 现在是 inner 的引用。
如果看完这个例子你可能会有疑问,明明上面说的是,装饰器是增强函数的功能,但是为什么这里却改变了函数的打印结果,这是因为在deco(func)函数传入的形参是func,但是后来我们在func这个函数中却没有用到func而是用inner,因此target的行为改变了。
:如果你对这里的操纵台的操作 target 输出结果有疑问,或者说不知道为什么不是 target() 的话,可以参考博主的另一篇博文 一等函数毕竟函数在Python中也是一种对象

装饰器的一大特性是,能把被装饰的函数替换成其他函数。第二个特性是,装饰器在加载模块时立即执行,下面我们来看一下。

二、Python何时执行装饰器

1、作为脚本执行时

装饰器的一个关键特性是,它们在被装饰的函数定义之后立即运行。这通常是在导入时(即 Python 加载模块时)

registry = [] ➊

def register(func): ➋
    print('running register(%s)' % func) ➌
    registry.append(func) ➍
    return func ➎

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

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

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

def main(): ➑
    print('running main()')
    print('registry ->', registry)
    f1()
    f2()
    f3()

if __name__=='__main__':
    main() ➒

❶ registry 保存被 @register 装饰的函数引用。
❷ register 的参数是一个函数。
❸ 为了演示,显示被装饰的函数。
❹ 把 func 存入 registry。
❺ 返回 func:必须返回函数;这里返回的函数与通过参数传入的一样。
❻ f1 和 f2 被 @register 装饰。
❼ f3 没有装饰。
❽ main 显示 registry,然后调用 f1()、f2() 和 f3()。
❾ 只有把 registration.py 当作脚本运行时才调用 main()

把 registration.py 当作脚本运行得到的输出如下:

$ python3 registration.py
running register(<function f1 at 0x100631bf8>)
running register(<function f2 at 0x100631c80>)
running main()
registry -> [<function f1 at 0x100631bf8>, <function f2 at 0x100631c80>]
running f1()
running f2()
running f3()

通过执行脚本可以看出,在执行main()函数之前,装饰器都已经被调用过二次了,装饰器的参数分别是被装饰的两个函数对象f1f2

2、作为模块导入时

如果导入 registration.py 模块(不作为脚本运行),输出如下:

>>> import registration
running register(<function f1 at 0x10063b1e0>)
running register(<function f2 at 0x10063b268>)

此时查看 registry 的值,得到的输出如下:

>>> registration.registry
[<function f1 at 0x10063b1e0>, <function f2 at 0x10063b268>]

函数装饰器在导入模块时立即执行,而被装饰的函数只在明确调用时运行。这突出了 Python 程序员所说的导入时和运行时之间的区别。

3、总结

  • 装饰器函数与被装饰的函数在同一个模块中定义。实际情况是,装饰器通常在一个模块中定义,然后应用到其他模块中的函数上。
  • register 装饰器返回的函数与通过参数传入的相同。实际上,大多数装饰器会在内部定义一个函数,然后将其返回。

三、变量作用域规则

如果你了解全局变量与局部变量的区别,那就很容易明白下面的问题

>>> def f1(a):
... print(a)
... print(b)
...
>>> f1(3)
3
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in f1
NameError: global name 'b' is not defined

错误提示给出了,变量b未定义。那如果这样呢,我们事先给变量b定义好

>>> b = 6
>>> f1(3)
3
6

就不会出错

在来看下面这段代码,如果你对变量作用域不够清楚的话就会出错。

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

我刚开始看的时候,也是一脸懵逼,但是看到流畅的Python关于这个解释才明白。在函数体编译的时候,由于b=9赋值语句是在函数f2内部,所以python判断b是一个局部变量,但是在执行函数的print(b)的时候,b变量还没有绑定任何值,所以出错。

想要解决这一问题,就要告诉python,b变量是一个全局变量,在打印的时候才不会出错。要使用 global 声明:

>>> b = 6
>>> def f3(a):
... global b
... print(a)
... print(b)
... b = 9
...
>>> f3(3)
3
6
>>> b
9
>>> f3(3)
3
9
>>> b = 30
>>> b
30

四、闭包

闭包指延伸了作用域的函数,其中包含函数定义体中引用、但是不在定义体中定义的非全局变量。函数是不是匿名的没有关系,关键是它能访问定义体之外定义的非全局变量。

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)

:这里的call特殊方法使得Averager函数对象成为了一个可以调用的对象。如果还有疑问可以参考博主的另一篇博文 一等函数
假如有个名为 avg 的函数,它的作用是计算不断增加的系列值的均值;例如,整个历史中某个商品的平均收盘价。每天都会增加新价格,因此平均值要考虑至目前为止所有的价格。
操纵台输入输出:Averager 的实例是可调用对象:

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

这样就实现了计算平均值。当然这样写没有问题,但是还有另一种写法。

def make_averager():
    series = []

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

调用make_averager 时,返回一个 averager 函数对象。每次调用averager 时,它会把参数添加到系列值中,然后计算当前平均值。
:这里make_averager扮演者一个高阶函数的角色,所谓高阶函数就是把其他函数对象作为参数传入自身的函数对象。

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

注意,这两个示例有共通之处:调用 Averager() 或make_averager() 得到一个可调用对象 avg,它会更新历史值,然后计算当前均值。在第一个示例 ,avg 是 Averager 的实例;在第二个示例中是内部函数 averager。不管怎样,我们都只需调用 avg(n),把 n放入系列值中,然后重新计算均值。
Averager 类的实例 avg 在哪里存储历史值很明显:self.series 实例属性。但是第二个示例中的 avg 函数在哪里寻找 series 呢?
注意,series 是 make_averager 函数的局部变量,因为那个函数的定义体中初始化了 series:series = []。可是,调用 avg(10)时,make_averager 函数已经返回了,而它的本地作用域也一去不复返了。

在 averager 函数中,series 是自由变量(free variable)。这是一个技术术语,指未在本地作用域中绑定的变量

在图中:averager 的闭包延伸到那个函数的作用域之外,包含自由变量 series 的绑定。

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

五、nonlocal声明

在上面的例子你仔细看可能会觉得别扭,不舒服,因为原本是想计算平均值,而又要用一个series记录下列表,然后在列表中记录下所有的值,并且计算总和在求平均值,这样做也可以,只是不符合习惯,并且不高,要知道申请列表空间可比整型要浪费多了,正确的思路应该是做累加,并记录下数的个数,最后求平均值。

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

这可能跟上面的局部变量问题有些相似,甚至错误类型都是相同的,UnboundLocalError: local variable 'count' referenced before assignment,当 count 是数字或任何不可变类型时,count += 1 语句的作用其实与 count = count + 1 一样。因此,我们在 averager 的定义体中为 count 赋值了,这会把 count 变成局部变量。total 变量也受这个问题影响。
上面的series中没遇到这个问题,因为我们没有给 series 赋值,我们只是调用 series.append,并把它传给 sum 和 len。也就是说,我们利用了列表是可变的对象这一事实。

所以对不可变类型来说,进行+=或者*=操作的时候,其实是又创建了一个相同的类型并赋值,而不是像列表那样进行追加,这一点也可以通过id()查看。而在上面的例子中,counttotal都作为不可变类型而在averager函数内部的局部变量,而不是自由变量。为了解决这一问题,Python3引入nonlocal声明。
它的作用是把变量标记为自由变量,即使在函数中为变量赋予新值了,也会变成自由变量。如果为 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 Version < 2.4,2004年以前),为一个函数添加额外功能的写法是这样的。

def debug(func):
    def wrapper():
        print "[DEBUG]: enter {}()".format(func.__name__)
        return func()
    return wrapper

def say_hello():
    print "hello!"

say_hello = debug(say_hello)  # 添加功能并保持原函数名不变

上面的debug函数其实已经是一个装饰器了,它对原函数做了包装并返回了另外一个函数,额外添加了一些功能。因为这样写实在不太优雅,在后面版本的Python中支持了@语法糖,下面代码等同于早期的写法。

def debug(func):
    def wrapper():
        print "[DEBUG]: enter {}()".format(func.__name__)
        return func()
    return wrapper

@debug
def say_hello():
    print "hello!"

这是最简单的装饰器,但是有一个问题,如果被装饰的函数需要传入参数,那么这个装饰器就坏了。因为返回的函数并不能接受参数,你可以指定装饰器函数wrapper接受和原函数一样的参数,比如:

def debug(func):
    def wrapper(something):  # 指定一毛一样的参数
        print "[DEBUG]: enter {}()".format(func.__name__)
        return func(something)
    return wrapper  # 返回包装过函数

@debug
def say(something):
    print "hello {}!".format(something)

这样你就解决了一个问题,但又多了N个问题。因为函数有千千万,你只管你自己的函数,别人的函数参数是什么样子,鬼知道?还好Python提供了可变参数*args和关键字参数**kwargs,有了这两个参数,装饰器就可以用于任意目标函数了。

def debug(func):
    def wrapper(*args, **kwargs):  # 指定宇宙无敌参数
        print "[DEBUG]: enter {}()".format(func.__name__)
        print 'Prepare and say...',
        return func(*args, **kwargs)
    return wrapper  # 返回

@debug
def say(something):
    print "hello {}!".format(something)
原文地址:https://www.cnblogs.com/welan/p/9860269.html