python之闭包详解

如有转载请声明转载地址
如有侵权请联系本人

闭包

1.什么是闭包

  • 闭包的定义
  • 自由变量

先检验一下自己的作用域基础:LEGB

def outer():
    a = 10
    inner()
def inner():
    print(a)
a = 20
outer()
>>>
20

现在我们使用闭包来看一下

def outer():
    a = 10
    def inner():
        print(a)
    return inner
a = 20
outer()()
>>>
10

1.1闭包的定义

def outer(args):
    a = 10
    b = 15
    c = 25
    def inner():
        return  a + b + args
    return inner
outer(5)()

上面的例子就是一个闭包,那么形成闭包的条件有两点:

  • 函数的返回值必须是被包函数对象(函数名)
  • 闭包中必须引用了外层函数作用域内的变量或者形参

闭包是一种延伸了作用域的函数

Untitled%20976e075a1a5148d7b99ab1aa0d60a8b0/Untitled.png

1.2 自由变量(free variable)

知道了闭包之后还要知道一个名词:自由变量free variable的定义:

If a name is bound in a block, it is a local variable of that block. If a name is bound at the module level, it is a global variable. (The variables of the module code block are local and global.) If a variable is used in a code block but not defined there, it is a free variable.

翻译一些就是如果一个变量出现在一个代码块中,那么它就是这个代码块的局部变量;如果一个变量出现在一个模块层级的作用域(py文件的最外层)中,那么它就是全局变量(模块的变量分为全局变量和局部变量)。如果一个变量被用在一个代码块中并且没有在这个代码块定义这个变量,那么它就是一个自由变量。

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

def f1():
	a = 10
	def f2():
		print(a)
这就是一种自由变量的例子

1.3. 判断是否为闭包函数

通过__closure__ 属性来判断

def outer(args):
    a = 10
    b = 15
    c = 25
    def inner():
        return  a + b + args
    return inner
print(outer(5).__closure__)
>>>
(<cell at 0x00000276B79B01F8: int object at 0x00007FF8DE8EA2B0>, <cell at 0x00000276B97038E8: int object at 0x00007FF8DE8EA210>, <cell at 0x00000276B9833438: int object at 0x00007FF8DE8EA350>)

闭包函数和嵌套函数的区别在于闭包函数有一个 __closure__ 属性,返回的是一个元组,每一项都是闭包函数引用的外部变量。可以通过cell_contents 将被引用的变量打印出来。

for line in outer(5).__closure__:
    print(line.cell_contents)
>>
10 #a
5 #args
15 # b

看一下下面不满足闭包的情况以加深印象

#没有返回闭包函数名
def outer(args):
    a = 10
    def inner():
        print(a + args)
    inner()
print(outer(12).__closure__)
>>>
22
AttributeError: 'NoneType' object has no attribute '__closure__'
#没有引用外部函数作用域的变量或者形参
def outer(args):
    a = 10
    def inner():
        print(10)
    return inner
print(outer(12).__closure__)
>>>
None

2. 疑惑

懂得思考python解释器运行机制的小伙伴一定会产生这样的疑惑:

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

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

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

函数.__code__ 属性可以访问PyCodeObject ,具体信息看下面的博客。

Python 中的代码对象 code object 与 code 属性_团子大圆帅的博客-优快云博客_python 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)
print(outer.__code__.co_consts)
>>>
('args', 'c', 'inner')#因为a,b被当成了自由变量
(None, 10, 15, 25, <code object inner at 0x000001FBC73B46F0, file "F:/Code/GIThub_100/Test6.py", line 48>, 'outer.<locals>.inner')

#查看inner的局部变量
print(inner.__code__.co_varnames)
>>>#只有一个
('name',)
#查看inner的代码对象
inner = outer(5)
code_obj = inner.__code__
print(outer.__code__.co_cellvars)#outer被内层函数引用 变量
print(code_obj.co_freevars)#内层inner引用了外层的哪些变量--自由变量
>>>#结果一样的
('a', 'args', 'b')
('a', 'args', 'b')

至此不知道小伙伴们能否反映过来,这里已经解决了上面的两个疑惑。先将疑惑二,因为疑惑一不是闭包的特性而是嵌套函数的特性。

  • 疑惑二:并且还能再次从outer 的本地作用域调用a + b + args

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

    print(inner.__closure__)
    print(inner.__closure__[0].cell_contents)
    print(inner.__closure__[1].cell_contents)
    print(inner.__closure__[2].cell_contents)
    >>>
    10
    5
    15
    

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

  • 疑惑一:inner(3)还能再次进入outer
#一步走看不出来outer的return已经执行完毕。
outer(5)()
#分步来走,说明确实是outer的return语句执行完毕后inner又进入的
inner = outer(5)
print(inner())

答案:这种特性不是闭包函数特有的而是所有嵌套函数在被外层函数返回函数对象后都有的特性。

#inner没有引用外部参数,这不是闭包
def outer():
    a = 10
    b = 11
    def inner():
        print("nishi ")
    return inner
outer()()
>>>
nishi#一样运行成功

3.闭包的陷阱

由闭包作用域我们再进一步看一个例子,这个例子是闭包典型的陷阱。

def outer():
    f_list = []
    for i in range(14):
        def inner():
            return i * i
        f_list.append(inner)
    return f_list
for fun in outer():
    print(fun())
>>>
9
9
9
  • 首先判断这个函数是闭包
    • 返回了闭包函数对象,虽然这些对象包含在列表中,但是仍然满足闭包的条件。
    • 之前我们说过for语句不存在局部作用域的说法,for循环中出现的变量包括循环变量都是和for循环在同一个作用域下的即iouter的局部变量。因此这里inner 引用了i 所以满足第二个条件。
  • 但是结果却不是理想的1,4,9 也就是说虽然引用了外部变量i 但是却没有把每一次的值都保存下来。

原因是什么呢?

我们可以看出来所有返回的闭包函数使用的都是最后一次i 的值,这在for 循环中这种现象很常见很正规。我们来屡一下过程,每次for循环,都会执行def inner(): 这一句话,执行完这一句话也就是简单的创建了一个inner 的函数对象,一共创建三个inner 。只不过每一次的循环变量i都没有被保存下来,所以到最后outer的局部变量i(当然也可以称为自由变量,这里大家知道就行)的值为3,也就是保存在每个inner的Cell中的i都是3(从这句话可以体会出,每次保存到Cell中的自由变量是return之后的值也就是inner被创建时的自由变量的值,只要没有return,闭包函数就没有被创建,在这之前自由变量可以被改变)

总结一下 😇:在return f_list 之前没有形成闭包,即inner此时只是一个普通的嵌套函数但还不是闭包函数。所以没有保存下当前的自由变量,最后返回列表return f_list时 才形成闭包,将i=3 加入到了cell_contents 之中 。

根据这个原因,我们就知道了解决方法,就是在每一次准闭包函数在生成时,把自由变量的当前值给它保存下来,如何保存?这很基础啊!

#这不是闭包,仅仅是普通的for循环+普通的函数创建
for i in range(1,4):
    def inner(ii = i):
        return ii * ii
    print(inner())
>>>
1
4
9

就是通过将循环变量赋值给闭包函数的形参,当做默认形参,等到闭包函数调用时各自各自的默认形参。

总结:

  • 知道闭包形成的条件

  • 闭包是怎么进出外层函数的

  • 顺便理解一下__code__属性,以后会用到的。

  • 说了这么多,其实闭包没有什么高大上,如果你只是想知道怎么用,只需要套模板就行,但是关键是换个场景你就想不起来用闭包可以解决一个你认为很复杂的问题,这就是理解透彻的好处。

  • 码字不易,请尊重原创

  • 我这笔记是自己复盘用的,如果那里讲的啰嗦或者串片了,请大家谅解!也请大家批评指正!

  • 我也是自学python没多长时间,各位大佬嘴下留情 ❤️。

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值