Python 闭包 (closure)深入解析

1. 闭包介绍

闭包概念: 在一个内部函数中,对外部作用域的变量进行引用,并且一般外部函数的返回值为内部函数,那么内部函数就被认为是闭包。

闭包作用: 1.装饰器 2.面向对象 3.实现单利模式

闭包创建:

  • 闭包函数必须有内嵌函数
  • 内嵌函数需要引用该嵌套函数上一级中的变量
  • 闭包函数必须返回内嵌函数

第一个案例:

def start(x):
    def inner(y):
        return x + y
    return inner

# 根据闭包这里的 test 是一个函数,这也是 Python 的一个特性
test = start(1)

print(test)

print(test(2))

执行结果:
在这里插入图片描述

start() 函数使用数值 1 进行调用,返回的函数被绑定到另一个名称。 在调用 test() 时,尽管我们已经完成了 start() 函数的执行,但仍然记住了这个消息

第二个案例:
在这里我们调用外函数传入参数 5,此时外函数两个临时变量 a 是 5,b 是10 ,并创建了内函数,然后把内函数的引用返回存给了demo,外函数结束的时候发现内部函数将会用到自己的临时变量,这两个临时变量就不会释放,会绑定给这个内部函数


# outer 是外部函数 a 和 b 都是外函数的临时变量
def outer( a ):
    b = 10
    # inner是内函数
    def inner():
        # 在内函数中 用到了外函数的临时变量
        print(a+b)
    # 外函数的返回值是内函数的引用
    return inner

demo = outer(5) # a = 5, b = 10,绑定到了内部函数 inner 中

# demo 存了外函数的返回值,也就是inner函数的引用,这里相当于执行inner函数
demo() # 15

demo2 = outer(7)
demo2() #17

执行结果:
在这里插入图片描述

1.1 外函数返回了内函数的引用:

引用是什么?在 python 中一切皆对象,包括整型数据 1, 2, 3 等,函数其实也是对象。

当我们进行 a = 1 的时候,实际上在内存当中有一个地方存了值 1,然后用 a 这个变量名存了 1 所在内存位置的引用。引用就好像 c 语言里的指针,大家可以把引用理解成地址。a 只不过是一个变量名字,a 里面存的是 1 这个数值所在的地址,就是 a 里面存了数值 1 的引用。

相同的道理,当我们在 python 中定义一个函数 def demo(): 的时候,内存当中会开辟一些空间,存下这个函数的代码、内部的局部变量等等。这个 demo 只不过是一个变量名字,它里面存了这个函数所在位置的引用而已。我们还可以进行 x = demo, y = demo, 这样的操作就相当于,把 demo 里存的东西赋值给 xy,这样 xy 都指向了demo 函数所在的引用,在这之后我们可以用 x() 或者 y() 来调用我们自己创建的demo() ,调用的实际上根本就是一个函数,x、ydemo 三个变量名存了同一个函数的引用。

有了上面的解释,我们可以继续说,返回内函数的引用是怎么回事了。对于闭包,在外函数 outer 中 最后 return inner,我们在调用外函数 demo = outer() 的时候,outer 返回了 innerinner 是一个函数的引用,这个引用被存入了 demo 中。所以接下来我们再进行 demo() 的时候,相当于运行了 inner 函数。

同时我们发现,一个函数,如果函数名后紧跟一对括号,相当于现在我就要调用这个函数,如果不跟括号,相当于只是一个函数的名字,里面存了函数所在位置的引用。

1.2 外函数把临时变量绑定给内函数:

按照我们正常的认知,一个函数结束的时候,会把自己的临时变量都释放还给内存,之后变量都不存在了。一般情况下,确实是这样的。但是闭包是一个特别的情况。外部函数发现,自己的临时变量会在将来的内部函数中用到,自己在结束的时候,返回内函数的同时,会把外函数的临时变量送给内函数绑定在一起。所以外函数已经结束了,调用内函数的时候仍然能够使用外函数的临时变量。

见下列案例,当外部函数 outer(5) 结束之后返回了 innerreturn 应该是把 outer 函数给关闭了,它的本地作用域也随之消失,为什么

  • inner() 还能再次进入outer
  • 并且还能再次从outer 的本地作用域调用 a + b

其实当 python 程序运行时,编译的结果是保存在位于内存中的 PyCodeObject 里,当python 运行结束时,Python 解释器则将 PycodeObject 写回到 pyc 文件中。pyc文件是PyCodeObject 的一种持久化方式。

函数.__code__ 属性可以访问 PyCodeObject ,具体信息看下面的博客:Python 中的代码对象 code object 与 code 属性

使用的关键属性为:

  • co_cellvars:外层函数的哪些变量被内层函数所引用 (不仅仅适用于闭包,下同)
  • co_freevars:内层函数引用了外层函数的哪些变量
  • co_consts:在函数中用到的所有常量,比如整数、字符串、布尔值等等。
  • co_varnames:函数所有的局部变量名称(包括函数参数)组成的元组,这里被判定为自由变量的局部变量就不被包含在内了
def outer(args):
    a = 10
    b = 15
    c = 25
    def inner():
        name = '闭包'
        return  a + b + args
    return inner

#查看outer的代码对象
print(outer.__code__.co_varnames) # a, b 为自由变量,并未打印出
print(outer.__code__.co_consts)

#查看inner的局部变量
inner = outer(5)
print(inner.__code__.co_varnames)

#查看inner的代码对象
inner = outer(5)
print(outer.__code__.co_cellvars) # outer 被内层函数引用变量
print(inner.__code__.co_freevars) # 内层inner引用了外层的哪些变量--自由变量

执行结果:
在这里插入图片描述

  • 疑惑二:outer 的本地作用域调用 a + b + args

    print(inner.__closure__)
    print(inner.__closure__[0].cell_contents)
    print(inner.__closure__[1].cell_contents)
    print(inner.__closure__[2].cell_contents)
    
  • 执行结果:
    在这里插入图片描述

  • 有没有发现在上面访问代码对象的时候,仅仅用了 inner = outer(5) + inner.__code__ ,根本没有再次进入outer 的内部,仍然可以通过 code_obj.co_freevars 查看到 inner 引用的变量是啥?并且还能通过__closure__ 查看自已引用的外部变量是哪些值。

  • 也就是说在返回 inner 之后并且再次进入 outer 之前,这些被引用的自由变量( outer 的变量)已经归 inner 所有了,官方一点就是闭包函数 inner 引用 的自由变量在 inner 被定义的时候就别存到了一个叫 Cell 的对象中,如果后续闭包函数引用这些自由变量,就直接从 Cell 中取。

  • 疑惑一: inner(3) 还能再次进入 outer
    这种特性不是闭包函数特有的而是所有嵌套函数在被外层函数返回函数对象后都有的特性。

在编写的实例中,两次调用外部函数 outer,分别传入的值是 57。内部函数只定义了一次,我们发现调用的时候,内部函数是能识别外函数的临时变量是不一样的。python 中一切都是对象,虽然函数我们只定义了一次,但是外函数在运行的时候,实际上是按照里面代码执行的,外函数里创建了一个函数,我们每次调用外函数,它都创建一个内函数,虽然代码一样,但是却创建了不同的对象,并且把每次传入的临时变量数值绑定给内函数,再把内函数引用返回。虽然内函数代码是一样的,但其实,我们每次调用外函数,都返回不同的实例对象的引用,他们的功能是一样的,但是它们实际上不是同一个函数对象。

1.3 自由变量

自由变量: 没有在某代码块中定义,但却在该代码块中使用,也就是引用的外部的变量。

def outer():
	a = 10
	def inner():
		print(a) #  a 就是自由变量

1.4 判断是否为闭包函数

  • 通过__closure__ 属性来判断

  • Python 闭包的 __closure__ 属性

    def nth_power(exponent):
        def exponent_of(base):
            return base ** exponent
        return exponent_of
    square = nth_power(2)
    #查看 __closure__ 的值
    print(square.__closure__)
    
  • 执行结果
    在这里插入图片描述
    闭包函数和嵌套函数的区别在于闭包函数有一个 __closure__ 属性,返回的是一个元组,每一项都是闭包函数引用的外部变量。可以通过cell_contents 将被引用的变量打印出来

    def nth_power(exponent):
        def exponent_of(base):
            return base ** exponent
    
        return exponent_of
    
    
    square = nth_power(2)
    
    # 查看 __closure__ 的值
    for line in square.__closure__:
        print(line.cell_contents)
    
  • 执行结果:
    在这里插入图片描述

1.5 不满足闭包的情况

  • 没有返回闭包函数名
def outer(a):
    b = 5
    def inner():
        print(a + b)
    inner()

print(outer(9).__closure__)

执行结果:
在这里插入图片描述

  • 没有引用外部函数作用域的变量或者形参
def outer(a):
    b = 10
    def inner():
        print(10)
    return inner

print(outer(9).__closure__)

执行结果:
在这里插入图片描述

2 常见错误

2.1 闭包无法修改外部函数的局部变量

def outerFunc():
    x = 0
    def innerFunc():
        x = 1
        print ("inner x:", x)
    
    print('outer x before call inner:', x)
    innerFunc()
    print('outer x after call inner:', x)

outerFunc()

执行结果:
在这里插入图片描述
innerFuncx 的值发生了改变,但是在 outerFuncx 的值并未发生变化。

闭包中内函数修改外函数局部变量

在闭包内函数中,我们可以随意使用外函数绑定来的临时变量,但是如果我们想修改外函数临时变量数值的时候发现出问题了!

在基本的 python 语法当中,一个函数可以随意读取全局数据,但是要修改全局数据的时候有两种方法:1 global 声明全局变量 2 全局变量是可变类型数据的时候可以修改。

在闭包内函数也是类似的情况。在内函数中想修改闭包变量(外函数绑定给内函数的局部变量)的时候:

  • python3 中,可以用 nonlocal 关键字声明 一个变量, 表示这个变量不是局部变量空间的变量,需要向上一层变量空间找这个变量。
  • python2 中,没有 nonlocal 这个关键字,我们可以把闭包变量改成可变类型数据进行修改,比如列表。
# 修改闭包变量的实例
# outer是外部函数 a 和 b 都是外函数的临时变量
def outer( a ):
    b = 10  # a 和 b 都是闭包变量
    c = [a] # 这里对应修改闭包变量的方法2
    # inner 是内函数
    def inner():
        # 内函数中想修改闭包变量
        # 方法1 nonlocal 关键字声明
        nonlocal  b
        b += 1
        # 方法二,把闭包变量修改成可变数据类型 比如列表
        c[0] += 1
        print(c[0])
        print(b)
    # 外函数的返回值是内函数的引用
    return inner

demo = outer(5)
demo() # 6  11

执行结果:
在这里插入图片描述
注意

还有一点需要注意:使用闭包的过程中,一旦外函数被调用一次返回了内函数的引用,虽然每次调用内函数,是开启一个函数执行过后消亡,但是闭包变量实际上只有一份,每次开启内函数都在使用同一份闭包变量。

def outer(x):
    def inner(y):
        nonlocal x
        x += y
        return x
    return inner


a = outer(10)

print(a(1)) # 11
print(a(3)) # 这里本来是 11, 结果却是 14,因为用的是同一个闭包

执行结果:
在这里插入图片描述

2.2 python 循环中不包含域的概念

flist = []
for i in range(3):
    def func(x):
        return x * i
    flist.append(func)

for f in flist :
    print(f(2))

执行结果:
在这里插入图片描述
按照正常的理解,应该输出的是 0, 2, 4,但实际输出的结果是:4, 4, 4

原因是什么呢?

loop (循环)在 python 中是没有域的概念的,for 循环中出现的变量包括循环变量都是和 for 循环在同一个作用域下的即 iouter 的局部变量。因此这里 inner 引用了 iflist 在向列表中添加 func 的时候,并没有保存 i 的值,而是当执行 f(2) 的时候才去取,这时候循环已经结束,i 的值是 2,所以结果都是 4。也就是保存在每个 innerCell 中的 i 都是 4(从这句话可以体会出,每次保存到 Cell 中的自由变量是 return 之后的值也就是 inner 被创建时的自由变量的值,只要没有 return,闭包函数就没有被创建,在这之前自由变量可以被改变)

解决方案:让 func 形成闭包

flist = []
for i in range(3):
    def makefunc(i):
        def func(x):
            return x * i
        return func
    flist.append(makefunc(i))

for f in flist :
    print(f(2))

第二种形式:

def outer():
    list = []
    for i in range(1, 4):
        def inner():
            return i * i
        list.append(inner)
    return list
for f in outer():
    print(f())

执行结果:
在这里插入图片描述

3.案例

  • 计算一个数的 n 次幂,用闭包可以写成下面的代码:

    #闭包函数,其中 exponent 称为自由变量
    def nth_power(exponent):
        def exponent_of(base):
            return base ** exponent
        return exponent_of # 返回值是 exponent_of 函数
    square = nth_power(2) # 计算一个数的平方
    cube = nth_power(3) # 计算一个数的立方
    print(square(2))  # 计算 2 的平方
    print(cube(2)) # 计算 2 的立方
    
  • 执行结果:
    在这里插入图片描述

Reference

1. python中闭包详解

2. Python 闭包 (Closure)

3. 深入浅出python闭包

4. python之闭包详解

  • 4
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值