Python_闭包

news2024/12/29 9:35:05

目录

1.概念介绍

2.闭包初探

3.闭包陷阱

4.闭包的应用

4.1 潜在的问题

5.闭包的实现


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

1.概念介绍

首先看一下维基上对闭包的解释

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

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

2.闭包初探

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

def outer_func():
    loc_list = []
    def inner_func(name):
        loc_list.append(len(loc_list) + 1)
        print '%s loc_list = %s' %(name, loc_list)
    return inner_func

clo_func_0 = outer_func()
clo_func_0('clo_func_0')
clo_func_0('clo_func_0')
clo_func_0('clo_func_0')
clo_func_1 = outer_func()
clo_func_1('clo_func_1')
clo_func_0('clo_func_0')
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)。

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

  • 闭包中的引用的自由变量只和具体的闭包有关联,闭包的每个实例引用的自由变量互不干扰。
  • 一个闭包实例对其自由变量的修改会被传递到下一次该闭包实例的调用。

由于这个概念理解起来并不是那么的直观,因此使用的时候很容易掉进陷阱。

3.闭包陷阱

def my_func(*args):
    fs = []
    for i in range(3):
        def func():
            return i * i
        fs.append(func)     # 注意此时append的是func()函数的引用
    return fs


fs = my_func()
fs1, fs2, fs3 = fs
print(fs)
print(fs1())
print(fs2())
print(fs3())

运行结果:

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

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

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

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

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

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

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

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

正确的写法

def my_func(*args):
    fs = []
    for i in xrange(3):
        def func(_i = i):
            return _i * _i
        fs.append(func)
    return fs

或者

def my_func(*args):
    fs = []
    for i in range(3):
        func = lambda _i = i : _i * _i
        fs.append(func)
    return fs

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

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

4.闭包的应用

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

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

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

  • 可能看不到函数或者类的定义
  • 会破坏原来的定义,导致原来对类的引用不兼容
  • 如果多人想在原来的基础上定制自己函数,很容易冲突

 使用闭包可以相对简单的解决上面的问题,下面看一个例子:

def func_dec(func):
    def wrapper(*args):
        if len(args) == 2:
            func(*args)
        else:
            print 'Error! Arguments = %s'%list(args)
    return wrapper

@func_dec
def add_sum(*args):
    print sum(args)

# add_sum = func_dec(add_sum)
args = range(1,3)
add_sum(*args)

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

4.1 潜在的问题

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

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

def counter(cls):
    obj_list = []
    def wrapper(*args, **kwargs):
        new_obj = cls(*args, **kwargs)
        obj_list.append(new_obj)
        print "class:%s'object number is %d" % (cls.__name__, len(obj_list))
        return new_obj
    return wrapper

@counter
class my_cls(object):
    STATIC_MEM = 'This is a static member of my_cls'
    def __init__(self, *args, **kwargs):
        print self, args, kwargs
        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来访问类的静态属性,答案是肯定的。

def counter(cls):
    obj_list = []
    @functools.wraps(cls)
    def wrapper(*args, **kwargs):
        ... ...
    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么,如果一切都从源码中学习,那成本的确有点高。

def outer_func():
    loc_var = "local variable"
    def inner_func():
        return loc_var
    return inner_func

import dis
dis.dis(outer_func)
clo_func = outer_func()
print clo_func()
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',)

尝试反汇编上面这个简单清晰的闭包例子,得到下面的结果:

0 LOAD_CONST               1 ('local variable')   # 将outer_func.func_code.co_consts[1]放到栈顶
STORE_DEREF              0 (loc_var)        # 将栈顶元素存放到cell对象的slot 0 
           6 LOAD_CLOSURE             0 (loc_var)        # 将outer_func.func_code.co_cellvars[0]对象的索引放到栈顶
BUILD_TUPLE              1              # 将栈顶1个元素取出,创建元组并将元组压入栈中
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]放到栈顶
MAKE_CLOSURE            0              # 创建闭包,此时栈顶是闭包函数代码段的入口,栈顶下面则是函数的free variables,也就是本例中的'local variable ',将闭包压入栈顶
STORE_FAST              0 (inner_func)       # 将栈顶存放入outer_func.func_code.co_varnames[0]
           21 LOAD_FAST               0 (inner_func)       # 将outer_func.func_code.co_varnames[0]的引用放入栈顶
RETURN_VALUE                       # Returns with TOS to the caller of the function.

local variable
           0 LOAD_DEREF               0 (loc_var)         # 将cell对象中的slot 0对象的引用压入栈顶
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.

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


-事必有法,然后有成- 最后祝大家早日达到测试的天花板!



以下是我收集到的比较好的学习教程资源,虽然不是什么很值钱的东西,如果你刚好需要,可以评论区,留言【777】直接拿走就好了

 

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

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

相关文章

面试又问到:工作中发现的最有价值的bug?答不好offer要飞了...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 这个问题&#xf…

SpringBoot--超时熔断器

需求背景 如果一个服务中有很多涉及需要服务间熔断的地方&#xff0c;就会出现N多下述代码&#xff1a; 1.N个fegnClient接口 FeignClient(name "hello-world-service", fallback HelloWorldFallback.class) public interface HelloWorldService {GetMapping(&q…

gma 2 教程(一)概述:3. 探索 GMA

组织方式 gma 整体按照库-模块-类/函数-&#xff08;方法/属性/子类&#xff09;的思路构建&#xff0c;详细思路如下所示&#xff1a; 整体架构 gma内主要模块与功能对应关系见下表&#xff1a; 模块名中文名对应主要功能io输入输出栅格/矢量数据输入输出模块crs坐标系统坐…

vim的使用方法及相关按键

目录 一、安装vim 二、vim的使用 1.打开vim 2.vim的四种模式使用 &#xff08;1&#xff09;命令模式&#xff08;快捷键的使用&#xff09; &#xff08;2&#xff09;编辑模式 &#xff08;3&#xff09;末行模式 &#xff08;4&#xff09;可视化模式 一、安装vim …

022:vue中tree结构数据变成扁平化table结构数据的示例

第022个 查看专栏目录: VUE — element UI vue在使用element UI tree的时候,有的时候是要做逆向处理的,即将树形结构的数据转化为table结构的数据,即扁平化的json数据。 如何处理呢? 效果图 原始tree结构数据: let newdata= [ {

redis pipeline

redis 执行多条连续的命令的时候为了减少网络开销RTT&#xff0c;可以使用pipeline技术。 pipeline 与 原生批命令(mset, mget) 对比&#xff1a; 原生批命令是原子性&#xff0c;pipeline是非原子性 (原子性概念:一个事务是一个不可分割的最小工作单位,要么都成功要么都失败…

基于Anime2Sketch算法那将图片转成素描

1.下载源码地址 https://github.com/Mukosame/Anime2Sketch下载项目依赖包&#xff0c;下载模型权重文件 运行看效果 python test.py --datarootE:\01_hjz\datas\00-hjz\pictures --load_size512调整自定义测试图片路径 """Test script for anime-to-sketch…

基于Java+Vue前后端分离网上书城系统设计实现(源码+lw+部署文档+讲解等)

博主介绍&#xff1a;✌全网粉丝30W,csdn特邀作者、博客专家、CSDN新星计划导师、Java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专…

ChatGPT+低代码,好用到飞起?

ChatGPT 凭借短短 2 个月&#xff0c;月活用户突破 1 亿&#xff0c;成为史上用户增长速度最快的消费级应用程序。ChatGPT 的爆火&#xff0c;在全球范围内掀起了一场关于 AI 技术革命的狂潮&#xff0c;AIGC 也迅速成为科技圈最火赛道。 更有国际咨询机构预测&#xff0c;203…

IIS安装配置和简单网站部署流程

IIS安装和网站配置 环境&#xff1a;win10 注意&#xff1a;这是在win10下部署iis&#xff0c;开发环境下部署&#xff0c;开发测试&#xff0c;非windows server IIS简介 Internet Information Services,简称IIS&#xff0c;是微软提供基于windows的互联网信息服务&#x…

微信小程序使用第三方组件wxParse加载富文本html

微信小程序使用第三方组件wxParse加载富文本html 微信小程序微信小程序加载富文本html微信小程序富文本第三方组件wxParsewxParse富文本html wxParse简介 wxParse 是一个微信小程序富文本解析组件&#xff0c;支持支持Html及markdown转wxml。 wxParse gitHub地址&#xff1…

亿发软件:玩具制造行业批发ERP系统解决方案,赋能传统制造商数字化

我国长期以来被公认为玩具制造大国&#xff0c;受益于其制造基础设施和成本优势。此外&#xff0c;可支配收入的增加和用户生活方式的改变增加了国内外对玩具的需求。然而&#xff0c;行业也面临着挑战和转型。随着数字技术的出现和用户偏好的变化&#xff0c;玩具ERP系统在确保…

TCP三次握手和自连接的条件和缺点

详解三次握手 为什么 SYN 段不携带数据却要消耗一个序列号呢&#xff1f; 记住&#xff1a; • 不占用序列号的段是不需要确认的&#xff0c;比如纯 ACK 包 • SYN 段需要对方的确认&#xff0c;需要占用一个序列号 • 凡是消耗序列号的 TCP 报文段&#xff0c;一定需要对端确认…

旅游宣传软文怎么写吸引人?纯干货

世界那么大&#xff0c;我想去看看&#xff0c;旅游是一种非常放松解压的方式&#xff0c;在旅行中放飞自我&#xff0c;在旅行中寻找自我&#xff0c;一个景点的客流量很大程度取决于其宣传效果&#xff0c;旅游宣传软文就是一种通过文字来吸引人们前往旅游目的地的宣传手段。…

嵌入式系统中详解 Modbus 通信协议(清晰易懂)

本文总结关于 Modbus 相关的知识&#xff0c;浅显易懂&#xff0c;旨在对 Modbus 有一个很直观的了解。如有错误&#xff0c;欢迎修改意见和建议。 什么是协议 在了解什么是Modbus之前&#xff0c;我们先来看下什么是协议。 协议是一个汉语词汇&#xff0c;读音为xi y&#…

Nftables栈溢出漏洞(CVE-2022-1015)复现

背景介绍 Nftables Nftables 是一个基于内核的包过滤框架&#xff0c;用于 Linux 操作系统中的网络安全和防火墙功能。nftables 的设计目标是提供一种更简单、更灵活和更高效的方式来管理网络数据包的流量。 钩子点&#xff08;Hook Point&#xff09; 钩子点的作用是拦截数…

Linux环境下配置安装RocketMQ

1.下载 官网下载&#xff1a;下载链接 根据需要下载自己需要的版本、本文使用下载的是:4.7.0版本 2.安装 创建目录&#xff0c;使用ftp工具上传下载的包到上面创建的目录下。 cd /usr/local mkdir rocketmq-all-4.7.0注意&#xff1a;rocketmq 需要 Linux 上安装JDK&…

7、卷积神经网络:基础部件+LeNet

1、图像卷积 1. 互相关运算 严格来说&#xff0c;卷积层是个错误的叫法&#xff0c;因为它所表达的运算其实是互相关运算&#xff08;cross-correlation&#xff09;&#xff0c;而不是卷积运算。在卷积层中&#xff0c;输入张量和核张量通过(互相关运算)产生输出张量。 首先…

【运维工程师学习】安装ubuntu20.04并配置SSH

【运维工程师学习】安装ubuntu20.04 1、镜像获取2、创建虚拟机3、开始安装4、配置SSH(1) 查看本地ssh版本(2) 安装ssh(3) 查看ssh运行状态(4) 设置开机自动启动(5) 重启(6) 安装net-tools(7) 查看ip5、SSH连接 1、镜像获取 https://next.itellyou.cn/Original/#cbpProduct?ID…

pdf如何导出为图片?分享三个方法PDF转图片!

将PDF文件转换为图片是在许多场景下都非常有用的操作&#xff0c;不仅能够保留原始文档的内容&#xff0c;还方便在各种平台上共享和展示。在本文中&#xff0c;我们将介绍三种简便的方法&#xff0c;帮助您将PDF文件快速转换为图片格式。 方法一&#xff1a;使用记灵在线工具…