什么是闭包

闭包(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. linux系统排查数据包常用命令

    1.查看当前系统中生效的所有参数 sysctl -a 2.统计处于TIME_WAIT状态的TCP连接数 netstat -ant|grep TIME_WAIT|wc -l 3.统计TCP连接数 net ...

  2. 华为 Quidway S3700-28TP-SI-AC Routing Switch 配置时间(ntp)

    设置ntp服务器: [SW03] ntp unicast-server x.x.x.x 记住一定要退出特权模式之后再设置时区 <SW03>clock timezone beijing ad ...

  3. 关于 LOCATE vs LIKE vs INSTR 性能分析

    网上很多流传关于Mysql字符串对比的函数性能说法是  INSTR >> LOCATE >> LIKE 字符串,所以今天我自己测一下看看真假. 这是在字符串较长的情况下测试的结 ...

  4. MYSQL-->函数与约束条件

    函数 用法 函数最常用的地方就是查询语句处 select 函数(字段) from 表名; select 字段列表 from 表名 group by 分组字段 having 函数(字段); 字符串函数( ...

  5. Java8新特性之Stream流(含具体案例)

    一.概述   Stream 流是 Java 8 新提供给开发者的一组操作集合的 API,将要处理的元素集合看作一种流, 流在管道中传输, 并且可以在管道的节点上进行处理, 比如筛选.排序.聚合等.元素 ...

  6. Hive之权限控制

    Hive之权限 一.库级的权限 -- 授予所有权限给某个用户 grant all on msta to user {userName}; -- 授权admin privilege权限 grant AD ...

  7. 你的哪些骚操作会导致Segmentation Fault😂

    你的哪些骚操作会导致Segmentation Fault 前言 如果你是一个写过一些C程序的同学,那么很大可能你会遇到魔幻的segmentation fault,可能一时间抓耳挠腮,本篇文章主要介绍一 ...

  8. python学习笔记----必备知识

    一.必备知识 二.流程控制 https://blog.csdn.net/weixin_43304253/article/details/120778228 1.1语法特点: 1.1.1 代码注释 单行 ...

  9. 动词时态=>2.动作的时间状态结合

    动作和时间结合 现在的四种时态 现在进行时态 对于 现在这个时间点,这个 动作 还在进行当中 例如:我现在正在喝水 现在完成时态 对于 现在这个时间点,这个 动作 已然完成 例子:我现在已经喝完了水 ...

  10. LcdToos如何实现PX01自动调Flicker及VCOM烧录

    准备工作: LcdTools+PX01点亮需调Flicker的屏:F118 Flicker探头,用于自动Flicker校准测量,F118连接PX01上电后,探头屏会提示零点校准,此时需盖住探头窗口再按 ...