一、认识闭包
有时候需要在函数外部得到函数内部的局部变量,但是由于Python作用域的关系,这一点是无法实现的。
def f(): n = 22 print(n) #NameError:name 'n' is not defined
但是有一种方法是可以的,那就是在函数内部再定义一个函数,这样就可以引用到外层变量
def f(): n = 999 def f2(): print(n)
二、闭包概念
上一部分的f2函数就是闭包:在上面的实例中,有一个外层函数的局部变量 n,有一个内层函数 f2,f2 里面可以访问到 n 变量,那这f2就是一个闭包。
维基百科定义:
在一些语言中,在函数中可以(嵌套)定义另一个函数时,如果内部的函数引用了外部的函数的变量,则可能产生闭包。闭包可以用来在一个函数与一组“私有”变量之间创建关联关系。在给定函数被多次调用的过程中,这些私有变量能够保持其持久性。
从上面可以得出:
三、闭包的用途
维基百科的定义中已经提到的它的两个用处:① 可以读取函数内部的变量,②让这些变量的值始终保持在内存中。
(一)读取函数内部的变量
在第一部分中,我们讲到,有时候会为了保证命名空间的干净而把一些变量隐藏到函数内部,作为局部变量。但是由于Python中作用域的搜索顺序,函数内的变量不会被函数外的代码读取到。
如果这时候想要函数外部的代码能够读取函数内部的变量,那么就可以使用闭包。
这里再借用一下 Wayne的例子(Wayne:用最简单的语言解释Python的闭包是什么?)。
def tag(tag_name): def add_tag(content): print(tag_name,content) return "<{1}>{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>
在这个例子里,我们想要一个给content加tag的功能,但是具体的tag_name是什么样子的要根据实际需求来定,对外部调用的接口已经确定,就是add_tag(content)。如果按照面向接口方式实现,我们会先把add_tag写成接口,指定其参数和返回类型,然后分别去实现a和b的add_tag。
但是在闭包的概念中,add_tag
就是一个函数,它需要tag_name
和content
两个参数,只不过tag_name
这个参数是打包带走的。所以一开始时就可以告诉我怎么打包,然后带走就行。
(二)让函数内部的局部变量始终保持在内存中
这里借用 千山飞雪的例子(来自于:千山飞雪:深入浅出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)回收。
这个时候,闭包使得函数的实例对象的内部变量,变得很像一个类的实例对象的属性,可以一直保存在内存中,并不断的对其进行运算。
(三)总结
- 局部变量无法共享和长久的保存,而全局变量可能造成变量污染,闭包既可以长久的保存变量又不会造成全局污染。
- 闭包使得函数内局部变量的值始终保持在内存中,不会在外层函数调用后被自动清除。
- 当外层函数返回了内层函数后,外层函数的局部变量还被内层函数引用
- 带参数的装饰器,那么一般都会生成闭包。
- 闭包在爬虫以及web应用中都有很广泛的应用。
四、闭包使用注意点
(一)内存消耗
由于闭包会使得函数中的变量都被保存在内存中,会增加内存消耗,所以不能滥用闭包,否则会造成程序的性能问题,可能导致内存泄露。
(二)使用场景
闭包的两个作用,“读取函数内部的变量”和“让函数内部的局部变量始终保持在内存中”,都可以被 Python 中现成的对象“类”很好地实现。我认为,“闭包”在 Python 中确实是一个必要性不大的概念。
那么为什么还要在 Python 中引入“闭包”这个概念呢?
首先,我觉得最重要的理由是,理解清楚这个概念,对于理解 Python 中的一大利器“装饰器”有很大的帮助。因为装饰器本身就是闭包的一个应用。
其次,当我们要实现的功能比较简单的时候,可以用闭包。例如:
这有点类似于,如果我们要实现比较简单的函数功能,通常使用 lambda 匿名函数比定义一个完整的function更加优雅,而且几乎不会损失可读性。类似的还有用列表解析式代替 for 循环。
(三)闭包无法改变外部函数局部变量指向的内存地址,看如下例子:
def out_fun(): x = 0 def inner_fun(): x = 1 print("inner x:",x,"at",id(x)) print("outer x before call inner:",x,"at",id(x)) inner_fun() print("outer x before call inner:",x,"at",id(x)) out_fun()
如果 innerFunc 可以修改 x 的的内存地址的话,那么 x 首先在outer_fun中指向了一个储存着 0 的内存地址,后面又在 inner_fun中,x 会指向新的储存着 1 的内存地址(由于int是不可变类型),但结果是:
在 innerFunc 中 x 的值发生了改变,但是原因是重新创建了一个变量 x,指向了一个新的内存地址。而在 outerFunc 中 x 的值以及内存地址并未发生变化。
造成这一结果的原因的根源,还是前面第一部分讲的Python中作用域的搜索顺序。在 inner_fun 函数里面,有自己的命名空间,这个命名空间是独立于 outer_fun 的命名空间的。它里面的x是一个局部名称(local names),在执行 “x=1” 命令的时候,是重新在 inner_fun自己的命名空间里创建了一个新的变量 x ,而无法覆盖掉 outer_fun 的命名空间的 x。
如果要让内层函数不仅可以访问,还要可以修改外层函数的变量,那么需要用到nonlocal声明,使得内层函数不要在自己的命名空间创建新的x,而是操作外层函数命名空间的x。
def outer_fun(): x = 0 def inner_fun(): nonlocal x # 注意这里 x = 1 print('inner x:',x, 'at', id(x)) print('outer x before call inner:', x, 'at', id(x)) inner_fun() print('outer x before call inner:', x, 'at', id(x)) outer_fun()
我们可以发现,此时 inner_fun 改变了 outer_fun 中的变量的内存地址
同样地,在上文棋盘的例子中,外层函数的变量pos内的值虽然一直在改变,但是由于列表本身是可变类型的变量,虽然列表中的元素一直在变,但是列表本身的内存地址没有发生变化。
(四)返回闭包时,返回函数不要引用任何循环变量,或者后续会发生变化的变量
在Python中,如果要返回一个函数,那么返回函数不要引用任何循环变量,或者后续会发生变化的变量。
因为,返回的函数并没有立刻执行,而是直到调用了f()
才执行。我们来看一个例子:
def count(): fs = [] for i in range(1, 4): def f(): return i*i fs.append(f) return fs f1, f2, f3 = count()
在上面的例子中,每次循环,都创建了一个新的函数,然后,把创建的3个函数都放在列表中,通过列表整体返回了。
你可能认为调用f1()
,f2()
和f3()
结果应该是1
,4
,9
,但实际结果是:
>>> f1() 9 >>> f2() 9 >>> f3() 9
因为在向列表中添加 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() >>> f1() 1 >>> f2() 4 >>> f3() 9
判断一个函数是不是闭包,可以查看它的closure属性。如果该函数是闭包,查看该属性将会返回一个cell对象组成的tuple。如果我们分别对每个cell对象查看其cell_contents属性,返回的内容就是闭包引用的自由变量的值。
通过一个例子来展示:
def add(x,y): def f(z): return x+y+z return f d = add(5,6) d(9) d(1)
闭包的__closure__方法,可以展示出闭包储存了外部函数的两个变量,cell的内存地址是什么,在cell里面储存的对象类型是int,这个int储存的内存地址是什么。
d.__closure__
闭包的__closure__方法,可以查看每个cell对象的内容。
for i in d.__closure__: print(i.cell_contents)
cell_contents解释了局部变量在脱离函数后仍然可以在函数之外被访问的原因,因为变量被存储在cell_contents中了。
感谢::
https://zhuanlan.zhihu.com/p/453787908