闭包总结

一、闭包的本质

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

 

二、举例理解概念

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

>>>avg(10)
10.0 
>>>avg(20) 
15.0
>>>avg(30)
20.0 

 

2、实现方式1:使用类实现

class Averager():

    def __init__(self):
        self.series = []

    def __call__(self, new_value):
        self.series.append(new_value)
        total = sum(self.series)
        return total/len(self.series)

avg = Averager()

print(avg(10)) #10.0
print(avg(20)) #15.0
print(avg(30)) #20.0

 函数说明:

  1、定义一个类变量保存价格,avg = Averager() 类的实例化,生成实例对象时,创建self.series,是一个数组。

  2、__call__ 魔法方法:使得 Averager类的 avg 实例对象变成了可调用对象。

  3、每次调用  avg(arg) 时,类变量的列表都增加一个 arg 的元素,返回 列表总和的平均值。

 

3、实现方式2:使用高阶函数实现(把函数作为参数传入,这样的函数称为高阶函数)

def make_averager():
    series = []

    def averager(new_value):
        series.append(new_value)
        total = sum(series)
        return total/len(series)

    return averager

avg = make_averager()
print(avg.__code__.co_varnames) #('new_value', 'total')
print(avg.__code__.co_freevars) #('series',)
print(avg(10)) #10.0
print(avg(20)) #15.0
print(avg(30)) #20.0

print(avg.__closure__) #(<cell at 0x00000151B2C415E8: list object at 0x00000151B2DA7AC8>,)
print(avg.__closure__[0].cell_contents) #[10, 20, 30]

 函数说明:

  1. 调用 make_averager() 时,返回一个 averager 函数对象。每次调用 averager 时,它会把参数添加到系列值中,然后计算平均值。

  2. series 是 make_averager 函数的局部变量

 

4、方法1和方法2的相通之处和不同之处

 相通:

  调用 Averager() 或 make_averager() 都得到一个可调用对象 avg,它会更新历史值,然后计算当前均值。

  我们只需要调用 avg(n) ,把n放入到系列值中,然后重新计算均值。

 不同:

  Averager 类的实例 avg 在 self.series 中存储历史值;

  make_averager 函数在哪里寻找 series ?series 是 make_averager 函数的局部变量,因为那个函数的定义体中初始化了series, series = [] 。可是调用avg(10)时,make_averager 函数已经返回了,它的本地作用域也一去不复返了。

 

三、闭包和自由变量

1、闭包和自由变量:在实现方式2的基础上

# 整个部分称为闭包
    series = []

    def averager(new_value):
        series.append(new_value) # series 称为自由变量
        total = sum(series)
        return total/len(series)

 说明:

  averager 的闭包延伸到那个函数的作用域之外,包含自由变量的绑定。

 

2、func.__code__

  print(avg.__code__.co_varnames) #('new_value', 'total') 将函数参数和局部变量以元组的形式返回

  print(avg.__code__.co_freevars) #('series',) 返回内部函数中引用外部函数参数,即自由变量(通过函数闭包引用)

 

更详细的说明参考:

https://blog.csdn.net/jpch89/article/details/86764245

 

3、func.__closure__

# 示例2中
print(avg.__closure__) #(<cell at 0x00000151B2C415E8: list object at 0x00000151B2DA7AC8>,)
print(avg.__closure__[0].cell_contents) #[10, 20, 30]

或者遍历得到:
[cell.cell_contents for cell in func.__closure__]

 

 说明:

  1. Python 3 已将名为func_X 的函数属性重命名为使用 __X__ 形式,从而在函数属性名称空间中为用户定义的属性释放了这些名称。

  例如,func_closure,func_code,func_defaults,func_dict,func_doc,func_globals,func_name
  分别重命名为__closure__,__code__,__defaults__,__dict__,__doc__,__globals__,__name__。

 

  2. 其实闭包函数相对于普通函数会多出一个closure的属性,里面定义了一个元组用于存放所有的cell对象,每个cell对象一一保存了这个闭包中所有的外部变量。

  data model将__closure__定义为:None或包含该函数的自由变量绑定的单元格元组。

  如果__closure__不是None,它将创建一个字典,将__code__.co_freevars中的每个单元格名称映射到元组中的相应cell.cell_contents。

 

  3. “Cell”对象用于实现由多个作用域引用的变量。

  对于每个这样的变量,一个“Cell”对象为了存储该值而被创建;引用该值的每个堆栈框架的局部变量包含同样使用该变量的对外部作用域的“Cell”引用。

  访问该值时,将使用“Cell”中包含的值而不是单元格对象本身。 这种对“Cell”对象的非关联化的引用需要支持生成的字节码;访问时不会自动非关联化这些内容。

  “Cell”对象在其他地方可能不太有用。

  引用自:https://docs.python.org/zh-cn/3.7/c-api/cell.html

 

4、总结

  闭包是一种函数,它会保留定义函数时存在的自由变量的绑定,这样调用函数时,虽然定义作用域不可用了,但是仍能使用那些绑定。

  注意:只有嵌套在其他函数中的函数,才可能需要处理不在全局作用域中的外部变量。

 

四、nonlocal 声明

1、背景:

  实现方式2中 make_averager 函数的效率并不高;有缺陷。

 

2、缺陷说明举例

>>> 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(10)
Traceback (most recent call last):
  File "<pyshell#5>", line 1, in <module>
    avg(10)
  File "<pyshell#3>", line 6, in averager
    count += 1
UnboundLocalError: local variable 'count' referenced before assignment
>>> 未绑定的局部错误:赋值前引用了局部变量“count”

 代码说明:

  当count 是数字,字符串,元组等不可变类型时,count += 1 语句的作用和 count = count+1 一样。因此我们在averager的定义体中为count赋值了,这会把count变成局部变量。total变量同理。

  在 实现方法2中没有这个问题是因为没有给 series 赋值,我们只是调用 series.append ,并把它传给sum 和 len。也就是说我们利用了列表是可变对象这一事实。

  但是对于不可变类型,只能读取不可更新。如果尝试重新绑定,其实会隐式创建局部变量 count。这样count就不是自由变量了,因此不会保存在闭包中。

  为了解决这个问题,Pyhon 引入了 nonlocal 声明,它的作用是把变量标记为自由变量,即使在函数中为变量赋予了新值,也会变成自由变量。

  如果为 nonlocal 声明的变量赋予了新值,闭包中保存的绑定就会更新。

 

3、实现方式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()
print(avg(10)) # 10.0
print(avg(20)) # 15.0
print(avg(30)) # 20.0

 

posted @ 2021-02-19 00:42  hqq的进阶日记  阅读(147)  评论(0编辑  收藏  举报