一、认识闭包

有时候需要在函数外部得到函数内部的局部变量,但是由于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就是一个闭包。

维基百科定义:

在一些语言中,在函数中可以(嵌套)定义另一个函数时,如果内部的函数引用了外部的函数的变量,则可能产生闭包。闭包可以用来在一个函数与一组“私有”变量之间创建关联关系。在给定函数被多次调用的过程中,这些私有变量能够保持其持久性。

从上面可以得出:

  • 定义:闭包就是能够读取外部函数内的变量的函数。
  • 作用1:闭包是将外层函数内的局部变量和外层函数的外部连接起来的一座桥梁。
  • 作用2:将外层函数的变量持久的保存在内存中

三、闭包的用途

维基百科的定义中已经提到的它的两个用处:① 可以读取函数内部的变量,②让这些变量的值始终保持在内存中。

(一)读取函数内部的变量

在第一部分中,我们讲到,有时候会为了保证命名空间的干净而把一些变量隐藏到函数内部,作为局部变量。但是由于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_namecontent两个参数,只不过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 中的一大利器“装饰器”有很大的帮助。因为装饰器本身就是闭包的一个应用。

其次,当我们要实现的功能比较简单的时候,可以用闭包。例如:

  • 当我们的代码中函数比较少的时候,可以使用闭包。(但是如果我们要实现很多功能,还是要使用类(OOP))
  • 如果我们的对象中只有一个方法时,使用闭包是会比用类来实现更优雅。

这有点类似于,如果我们要实现比较简单的函数功能,通常使用 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()结果应该是149,但实际结果是:

>>> 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

Python-闭包(Closure)的更多相关文章

  1. python 闭包(closure)

    闭包的定义: 闭包就是一个函数,这个函数可以记住封闭作用域里的值,而不管封闭作用域是否还在内存中. 来一个例子: def happy_add(a): print 'id(a): %x' % id(a) ...

  2. python闭包closure

    在讨论闭包之前,先总结一下python的命名空间namespace,一般的语言都是通过namespace来识别名字标识,无论是变量,对象,函数等等.python划分3个名字空间层次,local:局部, ...

  3. Python闭包Closure 2

    由于Python中,变量作用域为LEGB,所以在函数内部可以读取外部变量,但是在函数外不能读取函数内的变量.但是出于种种原因,我们需要读取函数内的变量时候怎么办?那就是在函数内在加一个函数. def ...

  4. python 闭包 Closure 函数作为返回值

    一.函数作为返回值 高阶函数除了可以接受函数作为参数外,还可以把函数作为结果值返回. >>> def lazy_sum(*args): ... def sum(): ... ax = ...

  5. python 函数对象(函数式编程 lambda、map、filter、reduce)、闭包(closure)

    1.函数对象 作者:Vamei 出处:http://www.cnblogs.com/vamei 欢迎转载,也请保留这段声明.谢谢! 秉承着一切皆对象的理念,我们再次回头来看函数(function).函 ...

  6. Python 闭包(Closure)

    Python  闭包 (Closure) 这里介绍一下python 的闭包 基本概念 闭包(closure)是函数式编程的重要的语法结构. 函数式编程的一个特点就是,允许把函数本身作为参数传入另一个函 ...

  7. 【Python】闭包Closure

    原来这就是闭包啊... 还是上次面试,被问只不知掉js里面的闭包 闭包,没听过啊...什么是闭包 回来查了下,原来这货叫闭包啊...... —————————————————————————————— ...

  8. Python 闭包

    什么是闭包? 闭包(closure)是词法闭包(lexical closure)的简称.闭包不是新奇的概念,而是早在高级程序语言开始发展的年代就已产生. 对闭包的理解大致分为两类,将闭包视为函数或者是 ...

  9. Python闭包与javascript闭包比较

    实例一 python def line_conf(): def line(x): return 2*x+1 print(line(5)) # within the scope     line_con ...

  10. Python闭包与函数对象

    1. Python闭包是什么 在python中有函数闭包的概念,这个概念是什么意思呢,查看Wikipedia的说明如下: “ In programming languages, closures (a ...

随机推荐

  1. JAVA虚拟机16-方法的动态调用

    更详细:https://www.cnblogs.com/jthr/p/15762527.html 1.子类重写父类方法 1.1父类 public class Father { public int n ...

  2. Vue视频 | 【Vue2 + Vue3 前端教程】完整版

    目前大部分公司还是以vue.react技术为主的,而Vue中还是以Vue2为主流,但不可否认Vue3是未来所必须的且已有这个趋向了 今天给大家介绍一个Vue的教程 里面既有现在主流的Vue2 同时也存 ...

  3. C#写文本日志

    /*----------------------------------------------------------------- * 作 者(Author): Dennis * 日 期(Crea ...

  4. ros_navigation案列操作流程

    1. 启动仿真 source devel/setup.bash export TURTLEBOT3_MODEL=burger roslaunch turtlebot3_gazebo turtlebot ...

  5. 读写wav格式文件

    读写wav格式文件 本文所有相关代码(包括未来的)均可在该代码库找到 https://gitcode.net/PeaZomboss/learnaudios 本文代码在MinGW-w64 gcc/g++ ...

  6. 01Python变量的使用

    Python变量 变量的定义 变量:在程序运行过程中,值会发生变化的量 把程序运算的中间结果临时存到内存里,以备后面的代码继续调用,这几个名字的学名就叫做"变量". 变量的作用 我 ...

  7. Python arcpy创建栅格、批量拼接栅格

      本文介绍基于Python语言arcpy模块,实现栅格影像图层建立与多幅遥感影像数据批量拼接(Mosaic)的操作.   首先,相关操作所需具体代码如下: import os import arcp ...

  8. Django中多数据库的配置,实现分库分表,主从复制,读写分离

    在django项目中, 一个工程中存在多个APP应用很常见. 有时候希望不同的APP连接不同的数据库,这个时候需要建立多个数据库连接. 1. 修改项目的 settings 配置 在 settings. ...

  9. Docker 架构演进之路

    转载:https://developer.aliyun.com/article/673009 前言 Docker已经推出了5年,在这5年中它极大的改变了互联网产品的架构,推进了新的产品开发.测试和运维 ...

  10. rsut 字节数组和字符串转换

    一.字符串转换为字节数组 let s = String::from("str"); let v = s.as_bytes(); // &[u8] println!(&quo ...