深入理解Python装饰器与闭包之看不懂你来打我

最初学习Python时,了解到装饰器与闭包的概念,在网上看了很多博客与教程,总觉得自己的理解还是不那么透彻,最近开始学习《流畅的Python》一书,书中对与闭包和装饰器有详细的解释,我觉得写的非常到位,现在把我的理解分享出来与大家共同探讨。

装饰器

函数装饰器用于在源码中“标记”函数,以某种方式增强函数的行为。装饰器是一个可调用的对象,其参数是另一个函数(即被装饰的函数)。装饰器可能会处理被装饰的函数,然后把它返回,或者将其替换为另一个函数或可调用对象。

假如有一个名为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() # 运行target函数
print(target) # 输入target对象,可以看到target现在其实已经是inner的引用了

输出结果如下:

running inner()
<function deco.<locals>.inner at 0x0000024EF95041F8>

上述代码中,我们执行了如下操作:

  1. deco返回inner函数对象
  2. 使用deco装饰target
  3. 调用被装饰的target其实会运行inner
  4. 审查对象,发现target现在是inner的引用

事实上,多数装饰器会修改被装饰的函数。通常,它们会定义一个内部函数,然后返回它以替换被装饰的函数。使用内部函数的代码几乎都要靠闭包才能正确的运作。为了理解闭包,我们先退一步,先了解Python中变量的作用域。

变量作用规则

在下述代码中,我们定义了一个函数,它读取两个变量的值:一个局部变量a,是函数的参数;另一个是变量b,在这个函数中我们没有定义它。

def f1(a):
    print(a)
    print(b)

f1(3)

运行结果如下:

Traceback (most recent call last):
  File "E:/Coding/PyProject/Practices/Day1/Part4.py", line 5, in <module>
    f1(3)
  File "E:/Coding/PyProject/Practices/Day1/Part4.py", line 3, in f1
    print(b)
NameError: name 'b' is not defined
3

事实上,出现这个错误我们不会感到奇怪,稍稍了解编程规则的同学都能很清楚的知道这个错误为何。我们只需要在调用f1之前给全局变量b赋值,这个函数就不会出现任何错误,像下面这样:

def f1(a):
    print(a)
    print(b)

b = 1
f1(3)
3
1

很好的输出了a,b的值,符合我们的预期,我们再来看下下面的代码,可能结果会让我们吃惊:

b = 1
def f1(a):
    print(a)
    print(b)
    b = 9

f1(3)

运行结果如下:

Traceback (most recent call last):
  File "E:/Coding/PyProject/Practices/Day1/Part4.py", line 8, in <module>
    f1(3)
  File "E:/Coding/PyProject/Practices/Day1/Part4.py", line 4, in f1
    print(b)
UnboundLocalError: local variable 'b' referenced before assignment
3

我们在print(b)之前对b进行了定义,但是当执行完第一个print(a)之后,明明应该打印b的值1,但是控制台却出现了报错。

事实上,Python编译函数的定义体时,它判断b是局部变量,因为在函数中给它赋值了,于是Python会尝试从局部空间中获取b。当我们调用f1(3)的时候,f1的定义体会获取并打印局部变量a的值,但是尝试获取局部变量b的值时,发现b没有绑定值,于是就出现了报错。

Python不要求声明变量,但是假定在函数定义体中赋值的变量时局部变量。因此,如果在函数中赋值时想让解释器把b当作全局变量,要使用global声明:

b = 1
def f1(a):
    global b
    print(a)
    print(b)
    b = 9

f1(3)

这样,我们再去运行这段代码时,就不会出现任何的错误信息。

闭包

在《流畅的Python》一书中,作者对闭包的定义是这样的,“闭包指延伸了作用域的函数,其中包含函数定义体中引用、但是不在定义体中定义的非全局变量”,意思就是值闭包函数可以访问函数定义体之外的非全局变量。看起来很难理解,请看下面的例子:

我们定义一个avg函数,用来返回不断增加的一系列数的平均值。
我们可以很简单的这样来实现这个功能:

class averager():

    def __init__(self):
        self._data_list = []

    '''
        在自定义类中实现__call__()函数可以将实例变成可调用的对象,
        在接下来的调用中我们可以很直观的看到该函数作用
    '''
    def __call__(self, new_value):
        self._data_list.append(new_value)
        total = sum(self._data_list)
        return total / len(self._data_list)

# 得益于__call__()函数,我们可以直接调用该类的实例
avg = averager()
print(avg(5))
print(avg(6))
print(avg(7))

输出如下:

5.0
5.5
6.0

那么,用函数式编程如何实现呢,我们看下面的示例:

def averagerPlus():
    data_list = []

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

    return averager

avg = averagerPlus()
print(avg(5))
print(avg(6))
print(avg(7))

输出结果如下:

5.0
5.5
6.0

上面两个例子有共同之处:调用averager()或者averagerPlus()时,都会得到一个可调用的对象avg,它会更新历史值,然后计算当前的平均值。

averager类的实例avg如何存储历史值我们可以很明显的看到:self._data_list实例属性。但是averagerPlus中的avg函数如何去寻找data_list呢?

注意,data_list是函数averagerPlus的局部变量,因为在averagerPlus的定义体中初始化了data_list == []。但是,当我们调用avg(5)时,averagerPlus函数已经返回了,而它的本地作用域也一去不复返了。

在averagerPlus函数中,data_list是自由变量,即指的是在本地作用域中未绑定的变量。如下图所示:
在这里插入图片描述
因此,在内部函数averager中,我们实际上绑定了averagerPlus中初始化的自由变量data_list。

综上,闭包是一种函数,它会保留定义函数时存在的自由变量的绑定,这样调用函数时,虽然定义域不可用了,但是仍然能够使用这些绑定,这也是闭包的灵魂所在。

nonlocal声明

实际上,我们在上面实现的averagerPlus函数的效率并不高,因为每次我们都会去重新计算一遍整个数组的sum值。更好的做法是,我们只用两个值来保存当前的元素综合和当前元素的总个数。

重新实现averagerPlus函数如下:

def averagerPlus():
    count = 0
    total = 0

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

    return averager

avg = averagerPlus()
print(avg(5))
print(avg(6))
print(avg(7))

但是,这个函数显然没有像我们想象中那样得到运行结果,而是出现了如下运行结果:

Traceback (most recent call last):
  File "E:/Coding/PyProject/Practices/Day1/Part5.py", line 13, in <module>
    print(avg(5))
  File "E:/Coding/PyProject/Practices/Day1/Part5.py", line 6, in averager
    count += 1
UnboundLocalError: local variable 'count' referenced before assignment

这又是为什么呢?

这就牵扯到Python中可变与不可变对象的问题,我们知道,在Python中,int型其实是属于不可变的类型,在我们执行 count += 1实际就相当于执行了 count = count + 1 。因此,我们在averager的定义体中为count赋值了,这会把count变成局部变量,total也是如此。

那么在之前的示例中为什么没有这个问题呢?因此在之前,我们并没有给data_list赋值,我们只是调用了append()方法不断的往数组中追加元素,并把它传给sum和len,也就是说,我们利用了列表是可变的对象这一事实。

但是对数字、字符串、元组等不可变类型来说,只能读取,不能更新。如果尝试重新绑定,例如上面的count += 1这样的操作,其实会导致Python解释器隐式的创建局部变量count。这样,count就不是自由变量了,因此不能保存在闭包中。

为了解决这个问题,Python引入了nonlocal声明。它的作用是把变量标记为自由变量,即使在函数中为变量赋予新值了,也会变为自由变量。如果nonlocal声明的变量赋予新值,闭包中保存的绑定会更新。我们利用nonlocal重新实现averagerPlus函数如下:

def averagerPlus():
    count = 0
    total = 0

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

    return averager

avg = averagerPlus()
print(avg(5))
print(avg(6))
print(avg(7))

完美运行。

总结

闭包和装饰器是很好用的特性,能让我们轻松的实现对函数功能的拓展而无需去改动原函数。初学者可能刚理解起来会困难一点,但是,看完本文之后,一定会豁然开朗!

猜你喜欢

转载自blog.csdn.net/airenKKK/article/details/104497434