Python-闭包与自由变量

闭包

闭包(Closure)是Python中的高级概念,它只出现在嵌套函数中,但和嵌套函数有很大的不同,那就是闭包使用了一个叫自由变量的东西。

实际中,闭包就是一个函数,只不过这个函数可以引用没有在该函数内部定义的变量,这个函数本身可以是匿名函数也可以是非匿名函数。

光说不练假把式,下面通过一个例子理解一下。

def make_averager():
    series = []
    def averager(new_value):
        series.append(new_value)
        print("series in averager: ", series)
        total = sum(series)
        return total / len(series)
    return averager

上面定义了一个函数 make_averager,这个函数的返回值是一个函数对象 averager,谨记“Python中一切皆可对象”的真理。当每次 averager 被调用的时候,它就会接收一个新的值,并把这个值添加到 series 数组中,并计算此时数组的平均值。这个函数到底有什么威力呢?再来看下面的使用例子。

avg = make_averager()
print(avg(10))
print(avg(11))

输出结果如下:

series in averager:  [10]
10.0
series in averager:  [10, 11]
10.5

看出端倪了吗? 通过调用 avg(n) 就可以实现更新历史 series 并且计算当前 series 的平均值。那么到底是如何实现更新 series 呢?讲道理,series 是 make_averager 函数中定义的一个局部变量,并且当通过 avg(n)调用时,明显 make_averager已经被执行过了,应该就访问不到 series 变量了。

实际上在嵌套函数 averager 中,series 是一个自由变量(free variable),表明这个变量没有被绑定在局部作用域中,同时 averager函数的闭包行为使得它可以访问到这个自由变量。
在这里插入图片描述
这里的自由变量 series 保存在函数的只读属性 __closure__ 中,我们可以打印看一下:

print(avg.__closure__)
(<cell at 0x11822a8e8: list object at 0x1166541c8>,)

print("####:",avg.__closure__[0].cell_contents)
####: [10, 11]

可以看到 __closure__存储的是一个 list 对象,通过第二个 print 也可以看到当前 series 中的两个元素。

如果这个函数仅仅是嵌套函数,那么它的 __closure__ 应该是 None。

nonlocal

上面代码中的 series 是可变变量,在内部函数中使用时没有问题,但如果是不可变变量,如 string、number、tuple 等,在内部函数引用则会报错。还是上面的例子,将它稍微改写一下,变成下面的代码,好处是不用每次都重新计算 series 的 sum。

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

avg = make_averager()
print(avg(10))

这时运行代码则会报错,报错信息如下:

UnboundLocalError                         Traceback (most recent call last)
<ipython-input-323-5c1ff34d8622> in <module>
      9 
     10 avg = make_averager()
---> 11 print(avg(10))
     12 
     13 print(avg.__closure__)

<ipython-input-323-5c1ff34d8622> in averager(new_value)
      3     count = 0
      4     def averager(new_value):
----> 5         count += 1
      6         total += new_value
      7         return total / count

UnboundLocalError: local variable 'count' referenced before assignment

问题出在 count+=1 这行代码,为什么呢?因为在Python中并没有要求先声明一个变量,所以Python解释器任务在函数体内,只要对一个变量进行赋值操作,那么这个变量就是局部变量。而 count+=1相当于 count=count+1,对 count 进行了赋值操作,所以Python解释器认为 count 是函数内的局部变量,但是从执行顺序上来说,count+1则是先执行的,先引用了局部变量 count,即使外面有一个同名的 count 变量。并且此时 count 就不再是自由变量了,也不会存储在 __closure__中。

那么如何解决这个问题呢?Python3中引入了 nonlocal关键字,它可以使得一个被赋值的局部变量变为自由变量,并且 nonlocal声明的变量发生变化时,__closure__中存储的值也会发生变化。所以只要对 count 及 total 变量加上 nonlocal的声明就可以了,即:

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

avg = make_averager()
print(avg(10))
print("####:",avg.__closure__[0].cell_contents, avg.__closure__[1].cell_contents)

print(avg(11))
print("####:",avg.__closure__[0].cell_contents, avg.__closure__[1].cell_contents)

运行结果为:

10.0
####: 1 10
10.5
####: 2 21

打印 __closure__ 的值,也可以发现确实发生了变化。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值