摘自<流畅的python> 第七章 函数装饰器和闭包
实现一个函数(可以不是函数)avg,计算不断增加的系列值的平均值,效果如下
def avg(...):
pass
avg(10) =>返回10
avg(20) =>返回10+20的平均值15
avg(30) =>返回10+20+30的平均值20
跟Python常见面试题015.请实现一个如下功能的函数有点类似,但又不太一样
关键是你需要有个变量来存储历史值
参考代码
class Average():
def __init__(self):
self.series = []
def __call__(self, value):
self.series.append(value)
return sum(self.series)/len(self.series)
avg = Average()
print(avg(10))
print(avg(20))
print(avg(30))
avg是个Average的实例
avg有个属性series,一开始是个空列表
__call__
使得avg对象可以像函数一样调用
调用的时候series会保留,因为series只在第一次初始化的时候置为空列表
下面的事情就变得简单了
参考代码
def make_average():
series = []
def averager(value):
series.append(value)
return sum(series)/len(series)
return averager
avg = make_average()
print(avg(10))
print(avg(20))
print(avg(30))
仔细对比2个代码,你会发现相似度是极高的
一个是类,一个是函数
类中存储历史值的是self.series,函数中的是series局部变量
类实例能调用是实现了__call__
,函数的实现中,avg是make_average()的返回值averager,是个函数名,所以它也能调用
闭包closure定义:
外函数
中定义了一个内函数
临时变量
内函数的引用
以上面的为例
def make_average(): # 外函数
series = [] # 临时变量(局部变量)
def averager(value): # 内函数
series.append(value)
return sum(series)/len(series)
return averager # 返回内函数的引用
下面这些话你可能听的云里雾里的,姑且听一下。
series 是 make_averager 函数的局部变量,因为那个函数的定义体中初始化了series:series = []
调用 avg(10) 时,make_averager 函数已经返回了,而它的本地作用域也一去不复返了
在 averager 函数中,series 是自由变量(free variable)。这是一个技术术语,指未在本地作用域中绑定的变量
averager 的闭包延伸到那个函数的作用域之外,包含自由变量 series 的绑定
from dis import dis
dis(make_average)
2 0 BUILD_LIST 0
2 STORE_DEREF 0 (series)
3 4 LOAD_CLOSURE 0 (series)
6 BUILD_TUPLE 1
8 LOAD_CONST 1 (<code object averager at 0x000002225DD1CBE0, file "<ipython-input-1-a43a8601eedd>", line 3>)
10 LOAD_CONST 2 ('make_average.<locals>.averager')
12 MAKE_FUNCTION 8 (closure)
14 STORE_FAST 0 (averager)
6 16 LOAD_FAST 0 (averager)
18 RETURN_VALUE
Disassembly of <code object averager at 0x000002225DD1CBE0, file "<ipython-input-1-a43a8601eedd>", line 3>:
4 0 LOAD_DEREF 0 (series)
2 LOAD_METHOD 0 (append)
4 LOAD_FAST 0 (value)
6 CALL_METHOD 1
8 POP_TOP
5 10 LOAD_GLOBAL 1 (sum)
12 LOAD_DEREF 0 (series)
14 CALL_FUNCTION 1
16 LOAD_GLOBAL 2 (len)
18 LOAD_DEREF 0 (series)
20 CALL_FUNCTION 1
22 BINARY_TRUE_DIVIDE
24 RETURN_VALUE
读懂上面的,不是人干的事情,不过你依然有可能
https://docs.python.org/zh-cn/3/library/dis.html#bytecodes
怎么样不云里雾里呢
查看avg.__code__
属性
[_ for _ in dir(avg.__code__) if _[:2]=='co']
['co_argcount',
'co_cellvars',
'co_code',
'co_consts',
'co_filename',
'co_firstlineno',
'co_flags',
'co_freevars',
'co_kwonlyargcount',
'co_lnotab',
'co_name',
'co_names',
'co_nlocals',
'co_posonlyargcount',
'co_stacksize',
'co_varnames']
官方解释
属性 | 描述 |
---|---|
co_argcount | 参数数量(不包括仅关键字参数、* 或 ** 参数) |
co_code | 原始编译字节码的字符串 |
co_cellvars | 单元变量名称的元组(通过包含作用域引用) |
co_consts | 字节码中使用的常量元组 |
co_filename | 创建此代码对象的文件的名称 |
co_firstlineno | 第一行在Python源码的行号 |
co_flags | CO_* 标志的位图,详见 此处 |
co_lnotab | 编码的行号到字节码索引的映射 |
co_freevars | 自由变量的名字组成的元组(通过函数闭包引用) |
co_posonlyargcount | 仅限位置参数的数量 |
co_kwonlyargcount | 仅限关键字参数的数量(不包括 ** 参数) |
co_name | 定义此代码对象的名称 |
co_names | 局部变量名称的元组 |
co_nlocals | 局部变量的数量 |
co_stacksize | 需要虚拟机堆栈空间 |
co_varnames | 参数名和局部变量的元组 |
通过__code__
分析
def make_average():
series = []
def averager(value):
series.append(value)
total = sum(series)
return total/len(series)
return averager
avg = make_average()
avg.__code__.co_varnames # 参数名和局部变量的元组
# ('value', 'total') # value是参数,total是局部变量名
avg.__code__.co_freevars
# ('series',) # 自由变量的名字组成的元组(通过函数闭包引用)
结合avg.__closure__
avg.__closure__
# (<cell at 0x000002225FA4DC70: list object at 0x000002225EE35600>,)
# 这是个cell对象,list对象
len(avg.__closure__) # 1
avg.__closure__[0].cell_contents # [] 因为你还没调用
avg(10)
avg(20)
avg(30)
avg.__closure__[0].cell_contents # [10, 20, 30] 保存着真正的值
闭包是一种函数,它会保留定义函数时存在的自由变量的绑定,这样调用函数时,虽然定义作用域不可用了,但是仍能使用那些绑定。
只有嵌套在其他函数中的函数才可能需要处理不在全局作用域中的外部变量
前面的make_averager 函数的方法效率不高
因为我们把所有值存储在历史数列中,然后在每次调用 averager 时使用 sum 求和
更好的实现方式是,只存储目前的总值和元素个数,然后使用这两个数计算均值
所以你可能这样实现
def make_average():
total = 0
length = 0
def averager(value):
total = total + value
length = length + 1
return total/length
return averager
avg = make_average()
执行avg(10)的时候你就会报错
UnboundLocalError Traceback (most recent call last)
<ipython-input-11-ace390caaa2e> in <module>
----> 1 avg(10)
<ipython-input-9-eaa25222e808> in averager(value)
4 def averager(value):
5 # nonlocal total,length
----> 6 total = total + value
7 length = length + 1
8 return total/length
UnboundLocalError: local variable 'total' referenced before assignment
这个问题你应该看到过,在前面的面试题002中看到过这样的错误
关键的错误是在于
total = total + value
length = length + 1
这样的赋值会把total和length都变成局部变量
from dis import dis
dis(make_average)
2 0 LOAD_CONST 1 (0)
2 STORE_FAST 0 (total)
3 4 LOAD_CONST 1 (0)
6 STORE_FAST 1 (length)
4 8 LOAD_CONST 2 (<code object averager at 0x0000026A8ED0E660, file "<ipython-input-12-12a610cc685c>", line 4>)
10 LOAD_CONST 3 ('make_average.<locals>.averager')
12 MAKE_FUNCTION 0
14 STORE_FAST 2 (averager)
8 16 LOAD_FAST 2 (averager)
18 RETURN_VALUE
Disassembly of <code object averager at 0x0000026A8ED0E660, file "<ipython-input-12-12a610cc685c>", line 4>:
5 0 LOAD_FAST 1 (total)
2 LOAD_FAST 0 (value)
4 BINARY_ADD
6 STORE_FAST 1 (total)
6 8 LOAD_FAST 2 (length)
10 LOAD_CONST 1 (1)
12 BINARY_ADD
14 STORE_FAST 2 (length)
7 16 LOAD_FAST 1 (total) #此处 LOAD_FAST 加载局部变量
18 LOAD_FAST 2 (length)
20 BINARY_TRUE_DIVIDE
22 RETURN_VALUE
是对数字、字符串、元组等不可变类型来说,只能读取,不能更新。如果尝试重新绑定,例如 count = count + 1,其实会隐式创建局部变量 count。这样,count 就不是自由变量了,因此不会保存在闭包中
为了解决这个问题,Python 3 引入了 nonlocal 声明。它的作用是把变量标记为自由变量,即使在函数中为变量赋予新值了,也会变成自由变量。如果为 nonlocal 声明的变量赋予新值,闭包中保存的绑定会更新。
解决的代码
def make_average():
total = 0
length = 0
def averager(value):
nonlocal total,length
total = total + value
length = length + 1
return total/length
return averager
avg = make_average()
# 你就可以avg(10)这样了~