彻底理解Python中的闭包和装饰器(上)

什么是闭包

闭包(Closure)其实并不是Python独有的特性,很多语言都有对闭包的支持。(当然,因为Python是笔者除C/C++之外学习的第二门语言,所以也是第一次遇到闭包。)简而言之,闭包实际上就是——函数中定义的函数。

这种程序结构的主要作用是:使得函数中的局部变量可以常驻内存,即使在函数返回之后(函数生命期结束后)。在这个意义上它的作用与C++中的static静态变量类似,当然不完全相同。

Python中闭包的定义和使用

在Python中,一个典型的闭包可以这样定义:

def outer(arg):
    temp = 10
    def inner():
        _sum = temp + arg   # 内函数引用了外函数的局部变量
        print('_sum =', _sum)
        return _sum
    return inner    # 外函数返回了内函数的引用

在这里有两个嵌套的函数,不妨叫他们外函数和内函数。可以看到闭包有两个显著的特点:

  1. 内函数引用了外函数的局部变量。
  2. 外函数返回了内函数的引用(函数名)。

符合以上两点,Python解释器会认为这是一个闭包。这时如果外函数的生命期结束了,在外函数中创建的局部变量并不会像通常一样被销毁,而是会留在内存中。这样当下次调用内函数时,就能够继续使用这些局部变量。

通过下面的分析可以看到,调用内函数正是通过外函数返回的函数指针(Python中没有指针变量,出于C++习惯笔者认为把它称作指针比较易于理解,没有学过C/C++的读者理解成返回了内函数的地址即可)。

闭包代码分析

我们来仔细分析上面的代码。

如果读者有C/C++经验,那么理解起来将会轻松许多。C++严格的语法要求函数必须先定义再调用,在Python并没有不同。因此需要牢记一点:在代码段中,函数的定义是不会被执行的,在理解代码时def下的所有内容都先跳过,到调用函数时再回来看它。

按照这种阅读顺序,在外函数outer()中实际上只做了三件事情:

  1. 定义局部变量temp
  2. 定义内函数inner()
  3. 返回内函数inner,实际上是返回了内函数的指针。

调用这个闭包时,首先用一个变量保存函数对象(的指针):

f = outer(2)

执行这句话时,就完成了上面所说的1~3条,f实际上是outer()返回的inner()的指针。注意,第2条只做了函数的定义,第3条只返回了函数的引用。完成这两件事的时候,实际上都还没有执行内函数inner()。所以执行这句代码后的输出为:

>

对,啥都没有。因为任何shell中进行输出的语句还没有被执行。这是透彻理解闭包非常重要的一点。忽略这一点很容易造成所谓的“闭包陷阱”。

那么如何调用内函数呢?就要用刚刚用来保存函数指针的变量f:

x = f()
print('x = ', x)

上面的两句代码,实际上通过函数指针f执行了内函数inner()。执行上面的所有代码,输出为:

_sum = 12
x = 12

再次强调:

直到使用函数指针调用内函数,内函数才会被执行。

需要说明,虽然在闭包中定义的局部变量常驻内存中,但在闭包外这些变量仍然是不可访问的。如上面的temp变量,只有通过函数指针f才可以访问,在函数外引用该变量会报错变量不存在。这与C++中的静态变量相同,即生命期比局部变量长,但可见性与局部变量相同。

修改闭包的局部变量

外函数中的局部变量虽然在内函数中可以引用(使用),但不能够重新赋值。

执行如下闭包函数:

def outer(arg):
    temp = 10
    def inner():
        _sum = temp + arg
        temp += 1   #在内函数中尝试改变temp的值
        print('_sum = ', _sum)
        return _sum
    return inner

会报如下错误:

UnboundLocalError: local variable 'temp' referenced before assignment

这意味着对于内函数来说,外函数中的局部变量只是一个可以使用的常量,它不能被修改。如果在内函数中重新定义一个同名变量,那么它会屏蔽掉外函数中的变量,即优先使用“更局部”的变量。

这实际上是由Python本身的语法特性造成的。在Python中,一个函数可以任意读取全局数据,但要修改时必须符合如下条件之一:

  1. 全局变量使用global声明
  2. 全局变量是可变类型数据

在闭包中这一点是类似的。如果想要修改外函数中的变量,可以使用以下两种方法之一:

  1. 使用nonlocal声明变量
def outer(arg):
    temp = 10
    def inner():
        nonlocal  temp  #用nonlocal声明变量,表示要到上一层变量空间寻找该变量
        _sum = temp + arg
        temp += 1   #此处修改temp的值,不会报错
        print('_sum = ', _sum)
        return _sum
    return inner

f = outer(2)
x = f()
print('x = ', x)
x = f()
print('x = ', x)

代码执行输出为:

_sum =  12
x =  12
_sum =  13
x =  13
  1. 将变量改为可变类型数据,如list
def outer(arg):
    temp = [10]
    def inner():
        # nonlocal  temp
        _sum = temp[0] + arg
        temp[0] += 1
        print('_sum = ', _sum)
        return _sum
    return inner

f = outer(2)
x = f()
print('x = ', x)
x = f()
print('x = ', x)

输出结果相同。

从以上代码也可以看出,闭包中常驻内存的局部变量只有一份。当重复调用内函数时,访问的是同一处变量。

闭包的参数

闭包的外函数和内函数都是函数,因此都可以接受参数,区别只在于参数是创建函数指针时传入,还是实际调用内函数时传入。

如果在创建函数指针时传入,那么该参数在之后的调用中都会保持原值。以本文最开始的闭包代码为例,传给外函数的参数arg,与在外函数中定义的局部变量temp地位是完全相同的。

相应地,传给内函数的参数则可以在每次调用的时候都不一样。执行如下代码:

def outer():
    temp = 10
    def inner(arg):
        _sum = temp + arg
        print('_sum = ', _sum)
        return _sum
    return inner

f = outer()
x = f(2)
print('x = ', x)
x = f(5)
print('x = ', x)

输出为:

_sum =  12
x =  12
_sum =  15
x =  15

闭包陷阱

引用廖雪峰教程中的例子:

def count():
    fs = []
    for i in range(1, 4):
        def func():
             return i*i
        fs.append(func)
    return fs

f1, f2, f3 = count()
print(f1())
print(f2())
print(f3())

上面的闭包创建了一个函数的list,并将这个list返回。这样会造成闭包陷阱,编写者也许原来希望返回的是1、2、3的平方值,但实际上执行的结果是:

9
9
9

原因就是之前强调的,内函数的指针被创建时,它实际上还没有被执行。

在上面内函数的循环中,每次循环只做了一件事,创建一个函数func()的指针并放入list。当真正调用三个内函数时,局部变量i已经变成3了,因此三个函数的返回值都是3。

使用闭包时必须牢记:

不要返回任何循环变量,或者后续会发生变化的变量。

如果一定要引用循环变量怎么办?这时候只能再嵌套一个函数并立即执行它,将函数参数绑定到循环变量的当前值。代码如下:

def count():
    def f(j):
        def g():
            return j*j
        return g
    fs = []
    for i in range(1, 4):
        fs.append(f(i)) # f(i)立刻被执行,因此i的当前值被传入f()
    return fs

上面的代码实际上是两层嵌套的闭包。每次循环里,都使用当前的循环变量i立即调用了函数f(i),它的意义是创建了函数指针并放入list。具体来说,是调用内层闭包的外函数,返回内层闭包的内函数指针。

当各个函数指针被创建时,已经将当前循环变量传入闭包。对于后续的操作来说,每一个内层闭包拥有独立且不变的局部变量。当外层闭包返回函数list时,也就避免了闭包陷阱。

小结

  1. 闭包的两个特征:内函数引用外函数的局部变量,外函数返回内函数的指针。
  2. 外函数指针被创建时,内函数未被执行,直到使用函数指针调用内函数才会被执行。
  3. 使用闭包时,不要返回任何循环变量或后续会发生变化的变量。
posted @ 2022-12-06 23:28  MidoQ  阅读(334)  评论(0编辑  收藏  举报