Wlgls 冲鸭!

第七章 函数装饰器和闭包


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

在了解装饰器的时候,我们可以先了解一下闭包。

闭包

闭包这个概念是只有涉及嵌套函数的时候才存在,其实闭包延伸了作用域的函数,在了解闭包前,我们不妨先了解一下变量作用域规则。

变量作用域规则

Python的作用域一种有四种,分别是:

  • L(Local) 局部作用域
  • E(Enclosing) 嵌套函数外的函数中
  • G(Global) 全局作用域
  • B(Built-in) 内置作用域

以LEGB的规则查找, 即,局部中找不到,去局部外的局部找,再找不到就去全局作用域,再着去内建中找。

>>> b = 1
>>> def foo(a):
...     print(a)
...     print(b)
... 
>>> foo(1)
1
1

在上述的代码中,我们就在foo函数中声明了a变量,在函数体外声明了b变量。在函数中输出b时,会首先在函数中查找,查找不到,便在函数体外的全局变量中查找,并且查找成功。

需要注意的是,在函数体内重新声明之后,解释器就会将其变成局部变量。

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

global关键字

如果在函数中需要修改全局变量,那么我们就可以使用global关键字,在函数中声明全局变量。

>>> b=1
>>> def foo(a):
...     global b
...     print(a)
...     print(b)
...     b=2
... 
>>> foo(1)
1
1
>>> b
2

闭包

在普通的函数中,我们查找了局部变量之后,会查找全局变量。但是在嵌套函数中,我们会查找函数外的局部变量。

>>> def foo():
...     b = 2
...     def foo1():
...             print(b)
...     return foo1
...

>>> foo1 = foo()
>>> foo1()
2

在上述例子中,b会首先在foo1函数中查找,如果找不到,则会在foo函数中查找。

2019-10-16-23-27-21.png

在红框中的内容就是一个闭包,其中bfoo的局部变量

但是我们可以看出,我们在使用foo1=foo()时,foo函数就已经返回了,而他所带有的局部作用域也就不存在了。

可是我们依然可以在foo1函数中使用b变量。这里存在一个技术术语:此时在foo1函数中,b自由变量(free variable)

所以,我们可以认为闭包是一种函数,它会保留定义函数时存在的自由变量的绑定,这样调用函数时,虽然定义作用域不可用了,但是仍能使用那些绑定。

注意: 只有嵌套在其他函数中的函数才有可能需要处理不在全局作用域中的外部变量

nonlocal声明

同一般的函数一样,我们可能在函数中重新声明全局变量,我们在闭包中也有可能重新声明自由变量。

>>> def foo():
...     a = 1
...     def foo1():
...             print(a)
...             a=2
...     return foo1
...
>>> foo1 = foo()
>>> foo1()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in foo1
UnboundLocalError: local variable 'a' referenced before assignment

处理这种错误,就是使用nonlocal关键字声明。

>>> def foo():
...     a = 1
...     def foo1():
...             nonlocnonlocal aal a
...             print(a)
...             a = 2
...     return foo1
... 
>>> foo1 = foo()
>>> foo1()
1

为什么使用闭包

假如有一个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)

然后我们的历史值就可以放在self.series这个列表中。然后我们就可以计算平均值了。

>>> from test import Averager
>>> avg = Averager()
>>> avg(9)
9.0
>>> avg(10)
9.5
>>> avg.series
[9, 10]
>>> exit()

同时,我们也可以使用闭包使用函数式实现

def Averager():
    series = []

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

>>> from test import Averager
>>> avg = Averager()
>>> avg(9)
9.0
>>> avg(10)
9.5

这和第一个例子十分相近,他们都能达到我们想要的效果。这是我们存储的历史值就是存储在Averager函数的局部变量series中。同时series也是averager自由变量

装饰器

对于下述两个片段的代码

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

其效果是一致的,我们知道python中函数也是一等公民所以,函数是可以作为参数传递给另一个参数。

对于上面两个代码,第一个就是使用了装饰器。第二个则和闭包中,我们使用函数时,十分相像,只是多了参数而已。

简单实现一个装饰器

严格说,装饰器只是语法糖。我们可以通过装饰器,将被装饰的函数替换成其他函数。

我们首先实现一个简单的装饰器。

def foo(func):

    def inner():
        print("begin running inner()")
        func()
        print("end")
    return inner

inner的闭包中包含自由变量func,之后,我们就可以返回内部函数,用于替代被装饰的函数

现在我们可以再定义一个函数,并将这个函数作为参数传入这个上面那个嵌套函数中:

>>> from test import foo

>>> def f():
...     print("running")
... 
>>> f = foo(f)
>>> f
<function foo.<locals>.inner at 0x7f4674b02e18>
>>> f()
begin running inner()
running
end
>>>

可以看出,我们运行了内部函数inner。事实上,我们可以更加简便的使用上述代码,也就是使用装饰器。

>>> @foo
... def f1():
...     print('running f1')
... 
>>> f1()
begin running inner()
running f1
end

这段代码的效果与上面的代码效果是一致的。只是书写的更加简单。

在这段代码里,@foo的作用就是与f=foo(f)的效果是一致的,我们使用@foo将我们的f函数变成了foo的内部函数inner函数。也就是说,装饰器的一大特性就是,能把被装饰的函数替换成其他函数。

Python何时执行装饰器

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

我们不妨再次创建一个装饰器

registery = []
def register(func):
    print('running register(%s)' % func)
    registery.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()

里面的register装饰器,将func函数不发生改变,但是却预先将函数放在了registery列表中。

当我们将其当作脚本运行是,得到的结果是:

$: python test.py
running register(<function f1 at 0x7fb1872710d0>)
running register(<function f2 at 0x7fb188ca2e18>)
running main
registry-> [<function f1 at 0x7fb1872710d0>, <function f2 at 0x7fb188ca2e18>]
running f1
running f2
running f3

但是当我们将其当作模块被导入时,我们可以发现:

>>> import test
running register(<function f1 at 0x7f5516c80e18>)
running register(<function f2 at 0x7f5516c80ae8>)

我们也可以看一下我们在registry中存储的内容:

>>> test.registry
[<function f1 at 0x7f5516c80e18>, <function f2 at 0x7f5516c80ae8>]

可见,函数装饰器在导入模块的时候立即被执行

我们只是为了讲解所以才这么写,实际上,

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