python series_流畅的python :嵌套函数与闭包

闭包

1、变量作用域

在开始之前,我们先看下面的一道选择题:

以下的三段代码中哪些会正常打印,而不会报错?

# 代码A
def f(a):
    print(a)
    print(b)
f(3)
# 代码B
b = 6
def f(a):
    print(a)
    print(b)
f(3)
# 代码C
b = 6
def f(a):
    print(a)
    print(b)
    b = 3
f(3)

对于代码A来说,只有有点编程基础的都应该知道肯定会报错,因为并没有定义b变量。

NameError: name 'b' is not defined

代码B可以正确执行,因为b是一个定义在函数体外的全局变量,函数体内可以读取到他的值。

但是代码C就很奇怪了,同样会报错:

UnboundLocalError: local variable 'b' referenced before assignment

一开始我觉得会打印出来6,因为函数体外我们已经定义了一个全局变量b=6了。可事实上,Python编译函数的定义体时,它判断b是局部变量,因为在函数中给它赋值了。所以python在获取并打印局部变量a的值,但是尝试获取局部变量b的值时,发现b没有绑定值并报错。

请注意,这不是缺陷,而是设计选择:Python不要求声明变量,但是假定在函数定义体中赋值的变量是局部变量。如果在函数中赋值时想让解释器把b当成全局变量,要使用global声明:

b = 6
def f(a):
    global b
    print(a)
    print(b)
    b = 3
f(3)
# 返回
3
6
>>>b
3

2、闭包(这个不太好理解,但是后面有用)

闭包指延伸了作用域的函数,其中包含函数定义体中引用、但是不在定义体中定义的非全局变量。

这句话实在是晦涩难懂,让我们用一个例子来说明它:

假如有个名为avg的函数,它的作用是计算不断增加的系列值的均值;例如,整个历史中某个商品的平均收盘价。每天都会增加新价格,因此平均值要考虑至目前为止所有的价格。

这个程序的主要难点是保存历史值,你可能会想到利用类来实现:

class Averager():
    def __init__(self):
        self.series = []
    def __call__(self, new_value):
        self.series.append(new_value)
        return sum(self.series) / len(self.series)
avg = Averager()

还记得__call__魔法方法吗?只要一个对象实现了这个模仿方法,就可以表现得像个函数。不记得的话参考一等函数。利用类实现的实例avg利用self.series来存储历史数据。

但是也可以使用函数进行实现:

def make_averager():
    series = []
    def averager(new_value):
        series.append(new_value)
        return sum(series) / len(series)
    return averager # 注意,make_averager返回的是一个函数对象
avg2 = make_averager()

但是它在哪儿存储历史值呢?注意,series是make_averager函数的局部变量,因为那个函数的定义体中初始化了series:series=[]。可是,调用avg()时,make_averager函数已经返回了,而它的本地作用域也一去不复返了。此时,对于内部嵌套的averager函数来说,series摆脱了make_averager的限制,它自由了,称为自由变量

>>>avg2.__code__.co_varnames # 查询函数内的局部变量
('new_value',)
>>>avg2.__code__.co_freevars # 查询函数内的自由变量
('series',)

综上,闭包出现在嵌套函数中,它会保留外部函数所创建的局部变量(对内部函数来说就是自由变量),这样调用内部函数时尽管局部变量的定义作用域不可用了,但是仍能使用对应的自由变量。

希望我解释明白了,说的更明白一点,就是嵌套函数里面外层函数定义的局部变量内层函数仍然能在后续调用时持续更新与使用。

3、nonlocal声明

前面实现make_averager函数的方法效率不高,事实上,只要存储目前的总值和元素个数,然后使用这两个数计算均值即可,不用使用序列增加程序的复杂度。很好修改,就像这样:

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

>>>avg = make_averager()
>>>avg(121)
UnboundLocalError: local variable 'count' referenced before assignment

然而,报错了。见鬼了,刚不是说count是free var, 咋又成local variable,如果你看过我的另一篇博文:对象引用、可变性、垃圾回收你会很容易理解这一切。因为对于count,total原始所绑定的对象是数字不可变对象,在外层函数的作用域失去以后,没错,变成了自由变量,但是但执行count += 1时,python 会创建一个count+1新的对象并分配给count,明白了吗?原来的自由变量已经不存在了。最终结果是,python会隐式创建局部变量count,新出现的count也就不是自由变量了,因此不会保存在闭包中。

为了解决这个问题,Python 3引入了nonlocal声明。它的作用是把变量标记为自由变量,即使在函数中为变量赋予新对象了,也会变成自由变量,闭包中保存的绑定也会更新。

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

>>>avg = make_averager()
>>>avg(121)
121.0
>>>avg(3)
62.0

——本章完—— 

欢迎关注我的微信公众号72df52df870f84e65f8824291f578a64.png

如果此文章对您有帮助,请点击在看(Wow)。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值