作者整理笔记,记录备查
文章目录
- 1、变量作用域
- 2、闭包的概念
- 3、闭包的作用
- 3.1 读取函数内部的变量
- 3.2 让函数内部的局部变量始终保持在内存中
- 4、注意事项
- 4.1 内存消耗
- 4.2 使用场景
- 4.3 闭包无法改变外部函数局部变量指向的内存地址
- 4.4 返回函数引用循环变量
- 5、闭包的__closure__属性
1、变量作用域
还是以局部函数中的例子做参考,假设要在函数外部得到函数内的局部变量a,由于python作用于的搜索顺序,无法通过调用outfuc直接实现,但是通过在内部定义infunc函数,并通过返回该函数对象可以实现对局部变量a的访问。通过局部函数,可以在函数外部得到函数内的局部变量
这时outfunc 内部的所有局部变量,对infunc都是可见的。但是infunc内部的局部变量,对outfunc 是不可见的
#全局函数
def outfunc ():
a = 'outvar'
#局部函数
def infunc():
print(a)
#调用局部函数
return infunc
#调用全局函数
get_a = outfunc()
get_a() ## 访问变量a的值
以上infunc函数就形成了闭包,外部函数返回的不是一个具体的值,而是一个函数。一般情况下,返回的函数会赋值给一个变量,这个变量可以在后面被继续执行调用。
2、闭包的概念
维基百科:在一些语言中,在函数中可以(嵌套)定义另一个函数时,如果内部的函数引用了外部的函数的变量,则可能产生闭包。闭包可以用来在一个函数与一组“私有”变量之间创建关联关系。在给定函数被多次调用的过程中,这些私有变量能够保持其持久性。
维基百科的定义要点如下:
- 定义:闭包就是能够读取外部函数内的变量的函数。
- 作用1:闭包将外层函数内的局部变量和外层函数的外部连接起来。
- 作用2:将外层函数的变量持久地保存在内存中。
计算一个数的 n 次幂,使用闭包实现:
#闭包函数,其中 exponent 称为自由变量
def nth_power(exponent):
def exponent_of(base):
return base ** exponent
return exponent_of # 返回值是 exponent_of 函数
square = nth_power(2) # 计算一个数的平方
cube = nth_power(3) # 计算一个数的立方
print(square(2)) # 计算 2 的平方
print(cube(2)) # 计算 2 的立方
在上面程序中,外部函数 nth_power() 的返回值是函数 exponent_of(),而不是一个具体的数值。
需要注意的是,在执行完 square = nth_power(2) 和 cube = nth_power(3) 后,外部函数 nth_power() 的参数 exponent 会和内部函数 exponent_of 一起赋值给 squre 和 cube,这样在之后调用 square(2) 或者 cube(2) 时,程序就能顺利地输出结果,而不会报错说参数 exponent 没有定义。
3、闭包的作用
3.1 读取函数内部的变量
借用以下案例,实现给content加tag的功能:
def tag(tag_name):
def add_tag(content):
return "<{0}>{1}</{0}>".format(tag_name, content)
return add_tag
content = 'Hello'
add_tag = tag('a')
print add_tag(content)
# <a>Hello</a>
add_tag = tag('b')
print add_tag(content)
# <b>Hello</b>
具体使用什么tag_name要根据实际需求来定,对外部调用的接口已经确定,就是add_tag(content),如果使用面向接口的方式,可以这样写:
def add_tag(tag_name,content):
return "<{0}>{1}</{0}>".format(tag_name, content)
add_tag('a','Hello') # <a>Hello</a>
上面程序确实可以实现相同的功能,不过使用闭包,在添加大量同类型的tag_name时,更加易读和方便,只需要传入content参数
3.2 让函数内部的局部变量始终保持在内存中
一般来说,函数内部的局部变量在这个函数运行完以后,就会被Python的垃圾回收机制从内存中清除掉。如果希望这个局部变量能够长久的保存在内存中,可以用闭包来实现这个功能。
以棋盘游戏的为例,假设棋盘大小为50*50,左上角为坐标系原点(0,0),建立一个函数,接收2个参数,分别为方向(direction),步长(step),该函数控制棋子的运动。 注:每次运动的起点都是上次运动结束的终点
def create(pos=[0,0]):
def go(direction, step):
new_x = pos[0]+direction[0]*step
new_y = pos[1]+direction[1]*step
pos[0] = new_x
pos[1] = new_y
return pos
return go
player = create()
print(player([1,0],10))
print(player([0,1],20))
print(player([-1,0],10))
##
[10, 0]
[10, 20]
[0, 20]
这段代码中,player实际上就是闭包go函数的一个实例对象。
它一共运行了三次,第一次是沿X轴前进了10来到[10,0],第二次是沿Y轴前进了20来到 [10, 20],第三次是反方向沿X轴退了10来到[0, 20]。
这证明了,函数create中的局部变量pos一直保存在内存中,并没有在create调用后被自动清除。
为什么会这样呢?原因就在于create是go的父函数,而go被赋给了一个全局变量,这导致go始终在内存中,而go的存在依赖于create,因此create也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。
这个时候,闭包使得函数的实例对象的内部变量,变得很像一个类的实例对象的属性,可以一直保存在内存中,并不断的对其进行运算。
4、注意事项
使用闭包,需要注意几点:
4.1 内存消耗
由于闭包会使得函数中的变量都被保存在内存中,会增加内存消耗,所以不能滥用闭包,否则会造成程序的性能问题,可能导致内存泄露。
解决方法是,在退出函数之前,将不使用的局部变量全部删除。
4.2 使用场景
当要实现的功能比较简单的时候,可以用闭包。例如:
- 当我们的代码中函数比较少的时候,可以使用闭包。但是如果我们要实现很多功能,还是要使用类(OOP)
- 如果我们的对象中只有一个方法时,使用闭包是会比用类来实现更优雅。
这有点类似于,如果我们要实现比较简单的函数功能,通常使用 lambda 匿名函数比定义一个完整的function更加优雅,而且几乎不会损失可读性。类似的还有用列表解析式代替 for 循环。
4.3 闭包无法改变外部函数局部变量指向的内存地址
def outfunc ():
x = 'outvar'
#局部函数
def infunc():
x = 'invar'
print('outer x before call inner:', x, 'at', id(x))
#调用局部函数
print('outer x before call inner:', x, 'at', id(x))
infunc()
print('outer x before call inner:', x, 'at', id(x))
outfunc()
##
outer x before call inner: outvar at 2176116975280 # outvar
inner x before call inner: invar at 2176116989104 # invar
outer x before call inner: outvar at 2176116975280 # outvar
可以看到,局部函数并没有改变外部函数中变量x的地址,这与函数的作用域有关。如果要让内层函数不仅可以访问,还可以修改外层函数的变量,那么需要用到nonlocal声明,使得内层函数不要在自己的命名空间创建新的x,而是操作外层函数命名空间的x
def outfunc ():
x = 'outvar'
#局部函数
def infunc():
nonlocal x
x = 'invar'
print('inner x before call inner:', x, 'at', id(x))
#调用局部函数
print('outer x before call inner:', x, 'at', id(x))
infunc()
print('outer x before call inner:', x, 'at', id(x))
outfunc()
##
outer x before call inner: outvar at 2176116975280 # outvar
inner x before call inner: invar at 2176116989104 # outvar
outer x before call inner: invar at 2176116989104 # outvar地址改变
infunc改变了 outfunc 中变量x的内存地址
4.4 返回函数引用循环变量
返回闭包时,返回函数不要引用任何循环变量,或者后续会发生变化的变量
以下循环想得到1,4,9,但结果显然没有实现
def count():
fs = []
for i in range(1, 4):
def f():
return i*i
fs.append(f)
return fs
f1, f2, f3 = count()
print(f1(),f2(),f3()) ## 9 9 9
返回的函数并没有立刻执行,而是直到调用了f()才执行。因为在向列表中添加 func 的时候,i 的值没有固定到f的实例对象中,而仅是将计算公式固定到了实例对象中。等到了调用f1()、f2()、f3()的时候才去取 i的值,这时候循环已经结束,i 的值是3,所以结果都是9。
如果一定要使用循环,需要再创建一个函数,用该函数的参数绑定循环变量当前的值,无论该循环变量后续如何更改,已绑定到函数参数的值不变
def count():
def f(j):
def g():
return j*j
return g
fs = []
for i in range(1, 4):
fs.append(f(i)) # f(i)立刻被执行,因此i的当前值被传入f()
return fs
f1, f2, f3 = count() ## 1 4 9
5、闭包的__closure__属性
闭包比普通的函数多了一个 closure 属性,该属性记录着自由变量的地址。当闭包被调用时,系统就会根据该地址找到对应的自由变量,完成整体的函数调用。
def nth_power(exponent):
def exponent_of(base):
return base ** exponent
return exponent_of
square = nth_power(2)
#查看 __closure__ 的值
print(square.__closure__) # (<cell at 0x000001FAAB272C70: int object at 0x00007FFAB3332740>,)
参考:
[1] Wayne:用最简单的语言解释Python的闭包是什么? [2] 千山飞雪:深入浅出python闭包 [3] Python中作用域的搜索顺序