理解Python闭包概念

news2024/11/18 6:38:46

闭包并不只是一个python中的概念,在函数式编程语言中应用较为广泛。理解python中的闭包一方面是能够正确的使用闭包,另一方面可以好好体会和思考闭包的设计思想。

1.概念介绍
首先看一下维基上对闭包的解释:

在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。

简单来说就是一个函数定义中引用了函数外定义的变量,并且该函数可以在其定义环境外被执行。这样的一个函数我们称之为闭包。实际上闭包可以看做一种更加广义的函数概念。因为其已经不再是传统意义上定义的函数。

根据我们对编程语言中函数的理解,大概印象中的函数是这样的:

程序被加载到内存执行时,函数定义的代码被存放在代码段中。函数被调用时,会在栈上创建其执行环境,也就是初始化其中定义的变量和外部传入的形参以便函数进行下一步的执行操作。当函数执行完成并返回函数结果后,函数栈帧便会被销毁掉。函数中的临时变量以及存储的中间计算结果都不会保留。下次调用时唯一发生变化的就是函数传入的形参可能会不一样。函数栈帧会重新初始化函数的执行环境。

C++中有static关键字,函数中的static关键字定义的变量独立于函数之外,而且会保留函数中值的变化。函数中使用的全局变量也有类似的性质。

但是闭包中引用的函数定义之外的变量是否可以这么理解呢?但是如果函数中引用的变量既不是全局的,也不是静态的(python中没有这个概念)。应该怎么正确的理解呢?

建议先参考一下我的另一篇博文(Python  UnboundLocalError和NameError错误根源解析 ),了解一下变量可见性和绑定相关的概念非常有必要。

2.闭包初探
为了说明闭包中引用的变量的性质,可以看一下下面的这个例子:

def outer_func():
 2     loc_list = []
 3     def inner_func(name):
 4         loc_list.append(len(loc_list) + 1)
 5         print '%s loc_list = %s' %(name, loc_list)
 6     return inner_func
 7 
 8 clo_func_0 = outer_func()
 9 clo_func_0('clo_func_0')
10 clo_func_0('clo_func_0')
11 clo_func_0('clo_func_0')
12 clo_func_1 = outer_func()
13 clo_func_1('clo_func_1')
14 clo_func_0('clo_func_0')
15 clo_func_1('clo_func_1')

程序的运行结果:

clo_func_0 loc_list = [1]
clo_func_0 loc_list = [1, 2]
clo_func_0 loc_list = [1, 2, 3]
clo_func_1 loc_list = [1]
clo_func_0 loc_list = [1, 2, 3, 4]
clo_func_1 loc_list = [1, 2]

从上面这个简单的例子应该对闭包有一个直观的理解了。运行的结果也说明了闭包函数中引用的父函数中local variable既不具有C++中的全局变量的性质也没有static变量的行为。

在python中我们称上面的这个loc_list为闭包函数inner_func的一个自由变量(free variable)。

If a name is bound in a block, it is a local variable of that block. If a name is bound at the module level, it is a global variable. (The variables of the module code block are local and global.) If a variable is used in a code block but not defined there, it is a free variable.

在这个例子中我们至少可以对闭包中引用的自由变量有如下的认识:

闭包中的引用的自由变量只和具体的闭包有关联,闭包的每个实例引用的自由变量互不干扰。
一个闭包实例对其自由变量的修改会被传递到下一次该闭包实例的调用。
由于这个概念理解起来并不是那么的直观,因此使用的时候很容易掉进陷阱。

3.闭包陷阱
下面先来看一个例子:

1 def my_func(*args):
 2     fs = []
 3     for i in xrange(3):
 4         def func():
 5             return i * i
 6         fs.append(func)
 7     return fs
 8 
 9 fs1, fs2, fs3 = my_func()
10 print fs1()
11 print fs2()
12 print fs3()

上面这段代码可谓是典型的错误使用闭包的例子。程序的结果并不是我们想象的结果0,1,4。实际结果全部是4。

这个例子中,my_func返回的并不是一个闭包函数,而是一个包含三个闭包函数的一个list。这个例子中比较特殊的地方就是返回的所有闭包函数均引用父函数中定义的同一个自由变量。

但这里的问题是为什么for循环中的变量变化会影响到所有的闭包函数?尤其是我们上面刚刚介绍的例子中明明说明了同一闭包的不同实例中引用的自由变量互相没有影响的。而且这个观点也绝对的正确。

那么问题到底出在哪里?应该怎样正确的分析这个错误的根源。

其实问题的关键就在于在返回闭包列表fs之前for循环的变量的值已经发生改变了,而且这个改变会影响到所有引用它的内部定义的函数。因为在函数my_func返回前其内部定义的函数并不是闭包函数,只是一个内部定义的函数。

当然这个内部函数引用的父函数中定义的变量也不是自由变量,而只是当前block中的一个local variable。

1 def my_func(*args):
2     fs = []
3     j = 0
4     for i in xrange(3):
5         def func():
6             return j * j
7         fs.append(func)
8     j = 2
9     return fs

上面的这段代码逻辑上与之前的例子是等价的。这里或许更好理解一点,因为在内部定义的函数func实际执行前,对局部变量j的任何改变均会影响到函数func的运行结果。

函数my_func一旦返回,那么内部定义的函数func便是一个闭包,其中引用的变量j成为一个只和具体闭包相关的自由变量。后面会分析,这个自由变量存放在Cell对象中。

使用lambda表达式重写这个例子:

1 def my_func(*args):
2     fs = []
3     for i in xrange(3):
4         func = lambda : i * i
5         fs.append(func)
6     return fs

经过上面的分析,我们得出下面一个重要的经验:返回闭包中不要引用任何循环变量,或者后续会发生变化的变量。

这条规则本质上是在返回闭包前,闭包中引用的父函数中定义变量的值可能会发生不是我们期望的变化。

正确的写法:

1 def my_func(*args):
2     fs = []
3     for i in xrange(3):
4         def func(_i = i):
5             return _i * _i
6         fs.append(func)
7     return fs

或者:


1 def my_func(*args):
2     fs = []
3     for i in xrange(3):
4         func = lambda _i = i : _i * _i
5         fs.append(func)
6     return fs

正确的做法便是将父函数的local variable赋值给函数的形参。函数定义时,对形参的不同赋值会保留在当前函数定义中,不会对其他函数有影响。

另外注意一点,如果返回的函数中没有引用父函数中定义的local variable,那么返回的函数不是闭包函数。

4.闭包的应用
自由变元可以记录闭包函数被调用的信息,以及闭包函数的一些计算结果中间值。而且被自由变量记录的值,在下次调用闭包函数时依旧有效。

根据闭包函数中引用的自由变量的一些特性,闭包的应用场景还是比较广泛的。后面会有文章介绍其应用场景之一——单例模式,限于篇幅,此处以装饰器为例介绍一下闭包的应用。

如果我们想对一个函数或者类进行修改重定义,最简单的方法就是直接修改其定义。但是这种做法的缺点也是显而易见的:

可能看不到函数或者类的定义
会破坏原来的定义,导致原来对类的引用不兼容
如果多人想在原来的基础上定制自己函数,很容易冲突
 使用闭包可以相对简单的解决上面的问题,下面看一个例子

1 def func_dec(func):
 2     def wrapper(*args):
 3         if len(args) == 2:
 4             func(*args)
 5         else:
 6             print 'Error! Arguments = %s'%list(args)
 7     return wrapper
 8 
 9 @func_dec
10 def add_sum(*args):
11     print sum(args)
12 
13 # add_sum = func_dec(add_sum)
14 args = range(1,3)
15 add_sum(*args)

对于上面的这个例子,并没有破坏add_sum函数的定义,只不过是对其进行了一层简单的封装。如果看不到函数的定义,也可以对函数对象进行封装,达到相同的效果(即上面注释掉的13行),而且装饰器是可以叠加使用的。

4.1 潜在的问题
但闭包的缺点也是很明显的,那就是经过装饰器装饰的函数或者类不再是原来的函数或者类了。这也是使用装饰器改变函数或者类的行为与直接修改定义最根本的差别。

实际应用的时候一定要注意这一点,下面看一个使用装饰器导致的一个很隐蔽的问题。

1 def counter(cls):
 2     obj_list = []
 3     def wrapper(*args, **kwargs):
 4         new_obj = cls(*args, **kwargs)
 5         obj_list.append(new_obj)
 6         print "class:%s'object number is %d" % (cls.__name__, len(obj_list))
 7         return new_obj
 8     return wrapper
 9 
10 @counter
11 class my_cls(object):
12     STATIC_MEM = 'This is a static member of my_cls'
13     def __init__(self, *args, **kwargs):
14         print self, args, kwargs
15         print my_cls.STATIC_MEM

这个例子中我们尝试使用装饰器来统计一个类创建的对象数量。当我们创建my_cls的对象时,会发现something is wrong!

Traceback (most recent call last):
  File "G:\Cnblogs\Alpha Panda\Main.py", line 360, in <module>
    my_cls(1,2, key = 'shijun')
  File "G:\Cnblogs\Alpha Panda\Main.py", line 347, in wrapper
    new_obj = cls(*args, **kwargs)
  File "G:\Cnblogs\Alpha Panda\Main.py", line 358, in __init__
    print my_cls.STATIC_MEM
AttributeError: 'function' object has no attribute 'STATIC_MEM'

如果对装饰器不是特别的了解,可能会对这个错误感到诧异。经过装饰器修饰后,我们定义的类my_cls已经成为一个函数。

my_cls.__name__ == 'wrapper' and type(my_cls) is types.FunctionType
 my_cls被装饰器counter修饰,等价于 my_cls = counter(my_cls)。

显然在上面的例子中,my_cls.STATIC_MEM是错误的,正确的用法是self.STATIC_MEM。

对象中找不到属性的话,会到类空间中寻找,因此被装饰器修饰的类的静态属性是可以通过其对象进行访问的。虽然my_cls已经不是类,但是其调用返回的值却是被装饰之前的类的对象。

该问题同样适用于staticmethod。那么有没有方法得到原来的类呢?当然可以,my_cls().__class__便是被装饰之前的类的定义。

那有没有什么方法能让我们还能通过my_cls来访问类的静态属性,答案是肯定的。

1 def counter(cls):
2     obj_list = []
3     @functools.wraps(cls)
4     def wrapper(*args, **kwargs):
5         ... ...
6     return wrapper

改写装饰器counter的定义,主要是对wrapper使用functools进行了一次包裹更新,使经过装饰的my_cls看起来更像装饰之前的类或者函数。该过程的主要原理就是将被装饰类或者函数的部分属性直接赋值到装饰之后的对象。如WRAPPER_ASSIGNMENTS(__name__, __module__ and __doc__, )和WRAPPER_UPDATES(__dict__)等。但是该过程不会改变wrapper是函数这样一个事实。

my_cls.__name__ == 'my_cls' and type(my_cls) is types.FunctionType
5.闭包的实现
本着会用加理解的原则,可以从应用层的角度来稍微深入的理解一下闭包的实现。毕竟要先会用python么,如果一切都从源码中学习,那成本的确有点高。

1 def outer_func():
 2     loc_var = "local variable"
 3     def inner_func():
 4         return loc_var
 5     return inner_func
 6 
 7 import dis
 8 dis.dis(outer_func)
 9 clo_func = outer_func()
10 print clo_func()
11 dis.dis(clo_func)

为了更加清楚理解上述过程,我们先尝试给出outer_func.func_code中的部分属性:

outer_func.func_code.co_consts: (None, 'local variable', <code object inner_func at 025F7770, file "G:\Cnblogs\Alpha Panda\Main.py", line 207>)
outer_func.func_code.co_cellvars:('loc_var',)
outer_func.func_code.co_varnames:('inner_func',)
尝试反汇编上面这个简单清晰的闭包例子,得到下面的结果:

2            0 LOAD_CONST               1 ('local variable')   # 将outer_func.func_code.co_consts[1]放到栈顶
             3 STORE_DEREF              0 (loc_var)        # 将栈顶元素存放到cell对象的slot 0 

3            6 LOAD_CLOSURE             0 (loc_var)        # 将outer_func.func_code.co_cellvars[0]对象的索引放到栈顶
             9 BUILD_TUPLE              1              # 将栈顶1个元素取出,创建元组并将元组压入栈中
             12 LOAD_CONST              2 (<code object inner_func at 02597770, file "G:\Cnblogs\Alpha Panda\Main.py", line 207>) # 将outer_func.func_code.co_consts[2]放到栈顶
             15 MAKE_CLOSURE            0              # 创建闭包,此时栈顶是闭包函数代码段的入口,栈顶下面则是函数的free variables,也就是本例中的'local variable ',将闭包压入栈顶
             18 STORE_FAST              0 (inner_func)       # 将栈顶存放入outer_func.func_code.co_varnames[0]

5            21 LOAD_FAST               0 (inner_func)       # 将outer_func.func_code.co_varnames[0]的引用放入栈顶
             24 RETURN_VALUE                       # Returns with TOS to the caller of the function.
local variable
4            0 LOAD_DEREF               0 (loc_var)         # 将cell对象中的slot 0对象的引用压入栈顶
             3 RETURN_VALUE                          # Returns with TOS to the caller of the function

这个结果中,我们反汇编了外层函数及其返回的闭包函数(为了便于查看,修改了部分行号)。从对上面两个函数的反汇编的注释可以大致了解闭包实现的步骤。

python闭包中引用的自由变量实际存放在一个Cell对象中,当自由变元被闭包引用时,便将Cell中存放的自由变量的引用放入栈顶。

本例中Cell对象及其存放的自由变量分别为:

clo_func.func_closure[0]    #Cell Object
clo_func.func_closure[0].cell_contents == 'local variable'    # Free Variable


闭包实现的一个关键的地方是Cell Object,下面是官方给出的解释:

“Cell” objects are used to implement variables referenced by multiple scopes. For each such variable, a cell object is created to store the value; the local variables of each stack frame that references the value contains a reference to the cells from outer scopes which also use that variable. When the value is accessed, the value contained in the cell is used instead of the cell object itself. This de-referencing of the cell object requires support from the generated byte-code; these are not automatically de-referenced when accessed. Cell objects are not likely to be useful elsewhere.

好了,限于篇幅就先介绍到这里。重要的是理解的基础上灵活的应用解决实际的问题并避免陷阱,希望本文能让你对闭包有一个不一样的认识。

欢迎转载博客文章,转载请标明出处!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2177972.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【动态规划-分组背包】【hard】力扣2218. 从栈中取出 K 个硬币的最大面值和

一张桌子上总共有 n 个硬币 栈 。每个栈有 正整数 个带面值的硬币。 每一次操作中&#xff0c;你可以从任意一个栈的 顶部 取出 1 个硬币&#xff0c;从栈中移除它&#xff0c;并放入你的钱包里。 给你一个列表 piles &#xff0c;其中 piles[i] 是一个整数数组&#xff0c;分…

OpenEuler虚拟机安装保姆级教程 | 附可视化界面

0x00 系统介绍 在 2019 年 7 月 19 日&#xff0c;华为宣布要在年底正式开源 openEuler 操作系统&#xff1b;在半年后的 12 月 31 日&#xff0c;华为正式开源了 openEuler 操作系统&#xff0c;邀请社区开发者共同来贡献。 一年后&#xff0c;截止到 2020 年12 月 25日&…

注册中心Eureka

Eureka 文章目录 前言一、Eureka是什么&#xff1f;二、使用步骤1.搭建一个SpringCloud项目2.启动类上增加注解 EnableEurekaServer3.启动项目 访问 配置文件里定义的端口号4.启动成功访问 localhost:70005.以同样的方式创建子项目 eureka-client-xx 作为 服务客户端 然后向eur…

实用SQL小总结

WHERE 条件 column 为纯英文字符 或 不包含任何字符 语法&#xff1a; SELECT * FROM your_table WHERE REGEXP(your_column,^[A-Za-z]$); SELECT * FROM your_table WHERE NOT REGEXP(your_column,^[A-Za-z]$);例&#xff1a; SELECT DISTINCT t.pldlibho FROM kibb_pldlyw…

gRPC基础讲解

一、gRPC原理 1、什么是RPC RPC 即远程过程调用协议&#xff08;Remote Procedure Call Protocol&#xff09;&#xff0c;可以让我们像调用本地函数一样发起远程调用。RPC 凭借其强大的治理功能&#xff0c;成为解决分布式系统通信问题的一大利器。 gRPC是一个现代的、高性能…

电路学习——反激电源(2024.09.29)

参考链接1: XXX 在此感谢各位前辈大佬的总结&#xff0c;写这个只是为了记录学习大佬资料的过程&#xff0c;内容基本都是搬运的大佬博客&#xff0c;觉着有用自己搞过来自己记一下&#xff0c;如果有大佬觉着我搬过来不好&#xff0c;联系我删。 电路学习——反激电源&#xf…

基于大数据的高校新生数据可视化分析系统

作者&#xff1a;计算机学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等&#xff0c;“文末源码”。 专栏推荐&#xff1a;前后端分离项目源码、SpringBoot项目源码、Vue项目源码、SSM项目源码、微信小程序源码 精品专栏&#xff1a;…

生信科研,教授(优青)团队一站式指导:高通量测序技术--农业植物基因组分析、组蛋白甲基化修饰、DNA亲和纯化测序、赖氨酸甲基化

组蛋白甲基化修饰工具(H3K4me3 ChIP-seq) 组蛋白甲基化类型也有很多种&#xff0c;包括赖氨酸甲基化位点H3K4、H3K9、H3K27、H3K36、H3K79和H4K20等。组蛋白H3第4位赖氨酸的甲基化修饰(H3K4)在进化上高度保守&#xff0c;是被研究最多的组蛋白修饰之一。 DNA亲和纯化测序 DNA亲…

QT设计中文输入法软键盘DLL给到C#开发步骤

开发目的&#xff1a;本文提供解决触摸屏C#程序中无法输入中文问题&#xff0c;中文拼音采用开源的谷歌输入法程序、使用QT编译中文输入法界面和中文输入法接口给到C#使用。 开发步骤&#xff1a; 1、QT中设计字母和字符输入界面 2、QT中设计数字输入界面 3、QT中封装调用谷歌…

神秘“蓝莓“模型横空出世:AI文生图界的新霸主还是营销噱头?

Ai 智能办公利器 - Ai-321.com AI文生图领域近日风起云涌&#xff0c;“蓝莓”模型横空出世&#xff0c;迅速占领行业制高点&#xff0c;成为万众瞩目的焦点。这个神秘而强大的模型&#xff0c;在众多AI竞技高手中脱颖而出&#xff0c;一举超越了OpenAI的“草莓”、Flux.1等强…

基于springboot+小程序的儿童预防接种预约管理系统(疫苗1)(源码+sql脚本+视频导入教程+文档)

&#x1f449;文末查看项目功能视频演示获取源码sql脚本视频导入教程视频 1、项目介绍 本儿童预防接种预约微信小程序可以实现管理员和用户。 1、管理员功能有个人中心&#xff0c;用户管理&#xff0c;儿童信息管理&#xff0c;疫苗信息管理&#xff0c;儿童接种管理&#x…

C++中的多态(详细讲解)

目录 一、多态的概念 1、多态的概念 二、多态的定义以及实现 1、多态的构成条件 2、虚函数 3、虚函数的重写 1&#xff09; 虚函数重写的理解 2&#xff09;虚函数重写的两个例外 1. 协变(基类与派生类虚函数返回值类型不同) 2. 析构函数的重写(基类与派生类析构函数的…

个人获取Wiley 、ScienceDirect、SpringerLink三个数据库文献的方法

在同学们的求助文献中经常出现Wiley 、ScienceDirect、SpringerLink这三个数据库文献。本文下面就讲解一下个人如何不用求助他人自己搞定这三个数据库文献下载的方法。 个人下载文献首先要先获取数据库资源&#xff0c;小编平时下载文献是通过科研工具——文献党下载器获取的数…

情感短视频素材上哪里找?推荐几个热门情感视频素材资源网站

抖音上热门的情感短视频都是怎么做的&#xff1f;情感视频素材都在哪里可以下载呢&#xff1f;作为一名资深的视频剪辑师&#xff0c;今天就跟大家聊聊那些可以下载情感素材高清无水印的网站&#xff0c;如果你也在苦苦找寻情感素材&#xff0c;快来看看吧&#xff5e; 1. 稻虎…

AI预测福彩3D采取888=3策略+和值012路或胆码测试9月28日新模型预测第101弹

经过100多期的测试&#xff0c;当然有很多彩友也一直在观察我每天发的预测结果&#xff0c;得到了一个非常有价值的信息&#xff0c;那就是9码定位的命中率非常高&#xff0c;100多期一共只错了12次&#xff0c;这给喜欢打私房菜的朋友提供了极高价值的预测结果~当然了&#xf…

黑神话热潮,能引发GPU狂欢的才是杀手级应用

黑神话与GPU技术的共舞&#xff1a;游戏推动硬件创新的新时代 ©作者|Steven 来源|神州问学 黑神话掀起游戏风暴 《黑神话&#xff1a;悟空》自上线以来&#xff0c;可谓是在游戏界掀起了一场前所未有的风暴。上线后就蝉联 Steam 热销榜首&#xff0c;晚间同时在线人数超…

打造自己的解析大模型:模型的安装与推理

RAG系统中要快速构建AI助理&#xff0c;首先要高效、准确地建立知识库&#xff0c;而实现这一点的关键便是具备一个功能强大的文档解析器。在上一篇中&#xff0c;我们介绍了PdfParser&#xff0c;本篇将深入讨论该解析器所依赖的模型&#xff0c;以及如何在Windows环境中安装并…

深度学习新手必备:Easy-PyTorch 助你轻松入门 PyTorch

深度学习初学者必备工具&#xff0c;简化代码编写&#xff0c;提升学习效率&#xff01; 对于刚入门深度学习的小伙伴&#xff0c;PyTorch 是一个强大的工具&#xff0c;但同时也伴随着大量复杂的代码和不小的学习曲线。你是否曾因为不知道如何快速实现模型而感到困惑&#xff…

4. 将pycharm本地项目同步到(Linux)服务器上——深度学习·科研实践·从0到1

目录 前序工作 1. 服务器项目名和本地一致 2. pycharm连接服务器 3. 本地项目对应到服务器项目 4. 简单测试一下同步效果 同步成功 前序工作 在同步到服务器之前&#xff0c;得确保已经完成以下几个前置步骤&#xff1a; 1. 租一个云服务器&#xff0c;可参考&#xff1a…

如何在云端地球建模云平台利用无人机航拍照片进行三维建模?

第一步&#xff1a;导入照片 进入云端地球工作台&#xff0c;选择【场景建模】将航拍的照片组导入。 输入模型名称&#xff08;若无则无法上传&#xff09;&#xff0c;点击【上传】&#xff0c;将照片上传到云端服务器。 第二步&#xff1a;创建任务 上传成功后点击开始处理…