什么是闭包

闭包(Closure)其实并不是Python独有的特性,很多语言都有对闭包的支持。(当然,因为Python是笔者除C/C++之外学习的第二门语言,所以也是第一次遇到闭包。)简而言之,闭包实际上就是——函数中定义的函数。

这种程序结构的主要作用是:使得函数中的局部变量可以常驻内存,即使在函数返回之后(函数生命期结束后)。在这个意义上它的作用与C++中的static静态变量类似,当然不完全相同。

Python中闭包的定义和使用

在Python中,一个典型的闭包可以这样定义:

def outer(arg):
temp = 10
def inner():
_sum = temp + arg # 内函数引用了外函数的局部变量
print('_sum =', _sum)
return _sum
return inner # 外函数返回了内函数的引用

在这里有两个嵌套的函数,不妨叫他们外函数和内函数。可以看到闭包有两个显著的特点:

  1. 内函数引用了外函数的局部变量。
  2. 外函数返回了内函数的引用(函数名)。

符合以上两点,Python解释器会认为这是一个闭包。这时如果外函数的生命期结束了,在外函数中创建的局部变量并不会像通常一样被销毁,而是会留在内存中。这样当下次调用内函数时,就能够继续使用这些局部变量。

通过下面的分析可以看到,调用内函数正是通过外函数返回的函数指针(Python中没有指针变量,出于C++习惯笔者认为把它称作指针比较易于理解,没有学过C/C++的读者理解成返回了内函数的地址即可)。

闭包代码分析

我们来仔细分析上面的代码。

如果读者有C/C++经验,那么理解起来将会轻松许多。C++严格的语法要求函数必须先定义再调用,在Python并没有不同。因此需要牢记一点:在代码段中,函数的定义是不会被执行的,在理解代码时def下的所有内容都先跳过,到调用函数时再回来看它。

按照这种阅读顺序,在外函数outer()中实际上只做了三件事情:

  1. 定义局部变量temp
  2. 定义内函数inner()
  3. 返回内函数inner,实际上是返回了内函数的指针。

调用这个闭包时,首先用一个变量保存函数对象(的指针):

f = outer(2)

执行这句话时,就完成了上面所说的1~3条,f实际上是outer()返回的inner()的指针。注意,第2条只做了函数的定义,第3条只返回了函数的引用。完成这两件事的时候,实际上都还没有执行内函数inner()。所以执行这句代码后的输出为:

>

对,啥都没有。因为任何shell中进行输出的语句还没有被执行。这是透彻理解闭包非常重要的一点。忽略这一点很容易造成所谓的“闭包陷阱”。

那么如何调用内函数呢?就要用刚刚用来保存函数指针的变量f:

x = f()
print('x = ', x)

上面的两句代码,实际上通过函数指针f执行了内函数inner()。执行上面的所有代码,输出为:

_sum = 12
x = 12

再次强调:

直到使用函数指针调用内函数,内函数才会被执行。

需要说明,虽然在闭包中定义的局部变量常驻内存中,但在闭包外这些变量仍然是不可访问的。如上面的temp变量,只有通过函数指针f才可以访问,在函数外引用该变量会报错变量不存在。这与C++中的静态变量相同,即生命期比局部变量长,但可见性与局部变量相同。

修改闭包的局部变量

外函数中的局部变量虽然在内函数中可以引用(使用),但不能够重新赋值。

执行如下闭包函数:

def outer(arg):
temp = 10
def inner():
_sum = temp + arg
temp += 1 #在内函数中尝试改变temp的值
print('_sum = ', _sum)
return _sum
return inner

会报如下错误:

UnboundLocalError: local variable 'temp' referenced before assignment

这意味着对于内函数来说,外函数中的局部变量只是一个可以使用的常量,它不能被修改。如果在内函数中重新定义一个同名变量,那么它会屏蔽掉外函数中的变量,即优先使用“更局部”的变量。

这实际上是由Python本身的语法特性造成的。在Python中,一个函数可以任意读取全局数据,但要修改时必须符合如下条件之一:

  1. 全局变量使用global声明
  2. 全局变量是可变类型数据

在闭包中这一点是类似的。如果想要修改外函数中的变量,可以使用以下两种方法之一:

  1. 使用nonlocal声明变量
def outer(arg):
temp = 10
def inner():
nonlocal temp #用nonlocal声明变量,表示要到上一层变量空间寻找该变量
_sum = temp + arg
temp += 1 #此处修改temp的值,不会报错
print('_sum = ', _sum)
return _sum
return inner f = outer(2)
x = f()
print('x = ', x)
x = f()
print('x = ', x)

代码执行输出为:

_sum =  12
x = 12
_sum = 13
x = 13
  1. 将变量改为可变类型数据,如list
def outer(arg):
temp = [10]
def inner():
# nonlocal temp
_sum = temp[0] + arg
temp[0] += 1
print('_sum = ', _sum)
return _sum
return inner f = outer(2)
x = f()
print('x = ', x)
x = f()
print('x = ', x)

输出结果相同。

从以上代码也可以看出,闭包中常驻内存的局部变量只有一份。当重复调用内函数时,访问的是同一处变量。

闭包的参数

闭包的外函数和内函数都是函数,因此都可以接受参数,区别只在于参数是创建函数指针时传入,还是实际调用内函数时传入。

如果在创建函数指针时传入,那么该参数在之后的调用中都会保持原值。以本文最开始的闭包代码为例,传给外函数的参数arg,与在外函数中定义的局部变量temp地位是完全相同的。

相应地,传给内函数的参数则可以在每次调用的时候都不一样。执行如下代码:

def outer():
temp = 10
def inner(arg):
_sum = temp + arg
print('_sum = ', _sum)
return _sum
return inner f = outer()
x = f(2)
print('x = ', x)
x = f(5)
print('x = ', x)

输出为:

_sum =  12
x = 12
_sum = 15
x = 15

闭包陷阱

引用廖雪峰教程中的例子:

def count():
fs = []
for i in range(1, 4):
def func():
return i*i
fs.append(func)
return fs f1, f2, f3 = count()
print(f1())
print(f2())
print(f3())

上面的闭包创建了一个函数的list,并将这个list返回。这样会造成闭包陷阱,编写者也许原来希望返回的是1、2、3的平方值,但实际上执行的结果是:

9
9
9

原因就是之前强调的,内函数的指针被创建时,它实际上还没有被执行。

在上面内函数的循环中,每次循环只做了一件事,创建一个函数func()的指针并放入list。当真正调用三个内函数时,局部变量i已经变成3了,因此三个函数的返回值都是3。

使用闭包时必须牢记:

不要返回任何循环变量,或者后续会发生变化的变量。

如果一定要引用循环变量怎么办?这时候只能再嵌套一个函数并立即执行它,将函数参数绑定到循环变量的当前值。代码如下:

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

上面的代码实际上是两层嵌套的闭包。每次循环里,都使用当前的循环变量i立即调用了函数f(i),它的意义是创建了函数指针并放入list。具体来说,是调用内层闭包的外函数,返回内层闭包的内函数指针。

当各个函数指针被创建时,已经将当前循环变量传入闭包。对于后续的操作来说,每一个内层闭包拥有独立且不变的局部变量。当外层闭包返回函数list时,也就避免了闭包陷阱。

小结

  1. 闭包的两个特征:内函数引用外函数的局部变量,外函数返回内函数的指针。
  2. 外函数指针被创建时,内函数未被执行,直到使用函数指针调用内函数才会被执行。
  3. 使用闭包时,不要返回任何循环变量或后续会发生变化的变量。

最新文章

  1. 用DirectX实现魔方(一)
  2. MySQL之ALTER
  3. timus 1022 Genealogical Tree(拓扑排序)
  4. java初探native
  5. 8 C#中的字符串输出
  6. python 操作word文档
  7. android.content.res.Resources$NotFoundException:String resource ID #ffffffff
  8. 【高斯消元】BZOJ 1770: [Usaco2009 Nov]lights 燈
  9. java.lang.NoClassDefFoundError: javax/servlet/ServletContext
  10. MSSQL - 存储过程事物
  11. C++0x新特性
  12. MUI开发记录——我的考勤
  13. hdu5601 BestCoder Round #67 (div.2)
  14. Git实操
  15. Guess Number Game
  16. DedeCMS织梦文章页图片地址为绝对路径实现方法
  17. Cannot change version of project facet Dynamic Web Module to 3.0 异常问题处理
  18. [剑指Offer]快排
  19. ubuntu upstart启动流程分析
  20. (转)多种方法实现Loading(加载)动画效果

热门文章

  1. 第六章:Django 综合篇 - 8:信号 signal
  2. k8s安装常用软件的yaml文件
  3. Node.js(七)MySql+ajax
  4. GCC Arm 12.2编译提示 LOAD segment with RWX permissions 警告
  5. hibernate validation 手动参数校验 不经过spring
  6. Git、TortoiseGit中文安装教程,如何注册Gitee账号进行代码提交,上传代码后主页贡献度没显示绿点(详解)
  7. Response对象页面重定向、时间的动态显示
  8. 后端框架的学习----mybatis框架(5、分页)
  9. 基于docker安装jumpserver
  10. spring-ioc知识点