什么是闭包

闭包(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. 使用闭包时,不要返回任何循环变量或后续会发生变化的变量。

彻底理解Python中的闭包和装饰器(上)的更多相关文章

  1. 轻松理解python中的闭包和装饰器 (下)

    在 上篇 我们讲了python将函数做为返回值和闭包的概念,下面我们继续讲解函数做参数和装饰器,这个功能相当方便实用,可以极大地简化代码,就让我们go on吧! 能接受函数做参数的函数我们称之为高阶函 ...

  2. 轻松理解python中的闭包和装饰器(上)

    继面向对象编程之后函数式编程逐渐火起来了,在python中也同样支持函数式编程,我们平时使用的map, reduce, filter等都是函数式编程的例子.在函数式编程中,函数也作为一个变量存在,对应 ...

  3. python中的闭包和装饰器

    重新学习完了函数,是时候将其中的一些重点重新捋一捋了,本次总结的东西只有闭包和装饰器 1.闭包 闭包是python函数中的一个比较重要功能,一般闭包都是用在装饰器上,一般学完闭包就会去学习装饰器,这俩 ...

  4. 21.python中的闭包和装饰器

    python中的闭包从表现形式上定义(解释)为:如果在一个内部函数里,对在外部作用域(但不是在全局作用域)的变量进行引用,那么内部函数就被认为是闭包(closure). 以下说明主要针对 python ...

  5. Python 中的闭包与装饰器

    闭包(closure)是函数式编程的重要的语法结构.闭包也是一种组织代码的结构,它同样提高了代码的可重复使用性. 如果在一个内嵌函数里,对在外部函数内(但不是在全局作用域)的变量进行引用,那么内嵌函数 ...

  6. python中的闭包与装饰器

    #原创,转载请留言联系 装饰器的本质就是闭包,所以想知道装饰器是什么,首先要理解一下什么是闭包. 闭包 1. 外部函数返回内部函数的引用.2. 内部函数使用外部函数的变量或者参数. def outer ...

  7. 聊聊Python中的闭包和装饰器

    1. 闭包 首先我们明确一下函数的引用,如下所示: def test1(): print("--- in test1 func----") # 调用函数 test1() # 引用函 ...

  8. python中函数总结之装饰器闭包

    1.前言 函数也是一个对象,从而可以增加属性,使用句点来表示属性. 如果内部函数的定义包含了在外部函数中定义的对象的引用(外部对象可以是在外部函数之外),那么内部函数被称之为闭包. 2.装饰器 装饰器 ...

  9. 理解Python中的闭包

    1.定义 闭包是函数式编程的一个重要的语法结构,函数式编程是一种编程范式 (而面向过程编程和面向对象编程也都是编程范式).在面向过程编程中,我们见到过函数(function):在面向对象编程中,我们见 ...

  10. 第十七篇 Python函数之闭包与装饰器

    一. 装饰器 装饰器:可以拆解来看,器本质就是函数,装饰就是修饰的意思,所以装饰器的功能就是为其他函数添加附加功能. 装饰器的两个原则: 1. 不修改被修饰函数的源代码 2. 不修改被修饰函数的调用方 ...

随机推荐

  1. google浏览器个人常用快捷键

    分享一下个人常用快捷键. 说明:字母排序规则遵循字母表(a->z) 快捷键 介绍 ctrl+0 恢复页面到100% ctrl+数字(1~9) 切换至序号对应的标签页 ctrl+d 将当前标签页添 ...

  2. 字符串反码A

    while True: try: string=input() if string!="!": res="" for i in string: if i.isu ...

  3. 原生Redis跨数据中心双向同步优化实践

    一.背景 公司基于业务发展以及战略部署,需要实现在多个数据中心单元化部署,一方面可以实现多数据中心容灾,另外可以提升用户请求访问速度.需要保证多数据中心容灾或者实现用户就近访问的话,需要各个数据中心拥 ...

  4. 浅谈 Golang 插件机制

    我们知道类似 Java 等半编译半解释型语言编译生成的都是类似中间态的字节码,所以在 Java 里面我们想要实现程序工作的动态扩展,可以通过 Java 的字节码编辑技术([[动态代理#ASM]]/[[ ...

  5. go使用JWT进行跨域认证最全教学

    JWT前言 JWT是JSON Web Token的缩写.JWT本身没有定义任何技术实现,它只是定义了一种基于Token的会话管理的规则,涵盖Token需要包含的标准内容和Token的生成过程. JWT ...

  6. 利用POI遍历出层级结构的excel表格

    import java.util.ArrayList; import java.util.List; import org.apache.poi.ss.util.CellRangeAddress; p ...

  7. 批量查询hive库中所有表的count

    一.准备文件 mkdir /query_hive_table_count touch query_db_name_table touch query_table_result.txt 二.编辑文件 2 ...

  8. 【Odoo】Odoo16-性能优化提升

    上海序说科技,专注于基于Odoo项目实施,实现企业数智化,助力企业成长. 老韩头的开发日常,博客园分享(2022年前博文) 10月12日,Odoo16版本正式发布,本文将就Odoo官方在性能方面做的优 ...

  9. .Net Core redis 调用报错 '6000 Redis requests per hour' 解决 6000 此调用限制

    问题描述 redis 是一种基于内存,性能高效的 NoSQL 数据库,性能高主要就体现在数据交互耗时较短,能够段时快速的对用户的请求做出反应,所以在业务比较复杂或交互量需求大时,必然会超过 6000次 ...

  10. 六、Kubernetes节点与 Pod 亲和性

    Kubernetes节点与 Pod 亲和性 一.节点亲和性策略介绍 ​pod.spec.nodeAffinity preferredDuringSchedulingIgnoredDuringExecu ...