深入理解 python 虚拟机:原来虚拟机是这么实现闭包的
深入理解 python 虚拟机:原来虚拟机是这么实现闭包的
在本篇文章当中主要从虚拟机层面讨论函数闭包是如何实现的,当能够从设计者的层面去理解闭包就再也不用死记硬背一些闭包的概念了,因为如果你理解闭包的设计原理之后,这些都是非常自然的。
根据 wiki 的描述,a closure is a record storing a function together with an environment。所谓闭包就是将函数和环境存储在一起的记录。这里有三个重点一个是函数,一个是环境(简单说来就是程序当中变量),最后一个需要将两者组合在一起所形成的东西,才叫做闭包。
Python 中的闭包
我们现在用一种更加直观的方式描述一下闭包:闭包是指在函数内部定义的函数,它可以访问外部函数的局部变量,并且可以在外部函数执行完后继续使用这些变量。这是因为闭包在创建时会捕获其所在作用域的变量,然后保持对这些变量的引用。下面是一个详细的Python闭包示例:
def outer_function(x):# 外部函数定义了一个局部变量 xdef inner_function(y):# 内部函数可以访问外部函数的局部变量 xreturn x + y# 外部函数返回内部函数的引用,形成闭包return inner_function# 创建两个闭包实例,分别使用不同的 x 值
closure1 = outer_function(10)
closure2 = outer_function(20)# 调用闭包,它们仍然可以访问其所在外部函数的 x 变量
result1 = closure1(5) # 计算 10 + 5,结果是 15
result2 = closure2(5) # 计算 20 + 5,结果是 25print(result1)
print(result2)
在上面的示例中,outer_function 是外部函数,它接受一个参数 x,然后定义了一个内部函数 inner_function,它接受另一个参数 y,并返回 x + y 的结果。当我们调用 outer_function 时,它返回了一个对 inner_function 的引用,形成了一个闭包。这个闭包可以保持对 x 的引用,即使 outer_function 已经执行完毕。
在上面的例子当中 outer_function 的返回值就是闭包,这个闭包包含函数和环境,函数是 inner_function ,环境就是 x,从程序语义的层面来说返回值是一个闭包,但是如果直接从 Python 层面来看,返回值也是一个函数,现在我们打印两个闭包看一下结果:
>>> print(closure1)
<function outer_function.<locals>.inner_function at 0x102e17a60>
>>> print(closure2)
<function outer_function.<locals>.inner_function at 0x1168bc430>
从上面的输出结果可以看到两个闭包(从 Python 层面来说也是函数)所在的内存地址是不一样的,因此每次调用都会返回一个不同的函数(闭包),因此两个闭包相互不影响。
再来看下面的程序,他们的执行结果是什么?
def outer_function(x):def inner_function(y):nonlocal xx += 1return x + yreturn inner_functionclosure1 = outer_function(10)
closure2 = outer_function(20)result1 = closure1(5)
print(result1)
result1 = closure1(5)
print(result1)
result2 = closure2(5)
print(result2)
输出结果为:
16
17
26
根据上面的分析 closure1 和 closure2 分别是两个不同的闭包,两个闭包的 x 也是各自的 x ,因此前一个闭包的 x 变化并不会影响第二个闭包,所以 result2 的输出结果为 26。
闭包相关的字节码
在正式了解闭包相关的字节码之前我们首先来重新回顾一下 CodeObject 当中的字段:
def outer_function(x):def inner_function(y):nonlocal xx += 1return x + yprint(inner_function.__code__.co_freevars) # ('x',)print(inner_function.__code__.co_cellvars) # ()return inner_functionif __name__ == '__main__':out = outer_function(1)print(outer_function.__code__.co_freevars) # ()print(outer_function.__code__.co_cellvars) # ('x', )
cellvars 表示在其他函数当中会使用本地定义的变量,freevars 表示本地会使用其他函数定义的变量。在上面的例子当中,outer_function 当中的变量 x 会被 inner_function 使用,而cellvars 表示在其他函数当中会使用本地定义的变量,所以 outer_function 的这个字段为 (‘x’, )。如果要了解详细的信息可以参考这篇文章 深入理解 python 虚拟机:字节码灵魂——Code obejct 。
上面的内容我们简要回顾了一下 CodeObject 当中的两个非常重要的字段,这两个字段在进行传递参数的时候非常重要,当我们在进行函数调用的时候,虚拟机会新建一个栈帧,在进行新建栈帧的过程当中,如果发现 co_cellvars 存储的字符串变量也是函数参数的时候,除了会在局部变量当中保存一份参数之外,还会将传递过来的参数保存到栈帧对象的其他位置当中(这里需要注意一下,CodeObject 当中的 co_freevars 保存的是字符串,也就是变量名,栈帧当中保存的是变量名字对应的真实对象,也就是函数参数),这么做的目的是为了方面后面字节码 LOAD_CLOSURE 的操作,因为实际虚拟机存储的是指向对象的指针,因此浪费不了多少空间。
实际在虚拟机的栈帧对象当中 freevars 是一个数组,后续的字节码都是会根据数组下标对这些变量进行操作。
下面我们分析一下和闭包相关的字节码操作
def outer_function(x):def inner_function(y):nonlocal xx += 1return x + yreturn inner_functionif __name__ == '__main__':import disdis.dis(outer_function)
上面的代码回输出 outer_function 和 inner_function 对应的字节码:
2 0 LOAD_CLOSURE 0 (x)2 BUILD_TUPLE 14 LOAD_CONST 1 (<code object inner_function at 0x100757a80, file "closure_bytecode.py", line 2>)6 LOAD_CONST 2 ('outer_function.<locals>.inner_function')8 MAKE_FUNCTION 8 (closure)10 STORE_FAST 1 (inner_function)7 12 LOAD_FAST 1 (inner_function)14 RETURN_VALUEDisassembly of <code object inner_function at 0x100757a80, file "closure_bytecode.py", line 2>:4 0 LOAD_DEREF 0 (x)2 LOAD_CONST 1 (1)4 INPLACE_ADD6 STORE_DEREF 0 (x)5 8 LOAD_DEREF 0 (x)10 LOAD_FAST 0 (y)12 BINARY_ADD14 RETURN_VALUE
我们现在来详细解释一下上面的字节码含义:
- LOAD_CLOSURE:这个就是从栈帧对象当中加载指定下标的 cellvars 变量,在上面的字节码当中就是加载栈帧对象 cellvars 当中下标为 0 的对象,对应的参数就是 x 。也就是将参数 x 加载到栈帧上。
- BUILD_TUPLE:从栈帧当中弹出 oparg (字节码参数) 个参数,并且将这些参数封装成元祖,在上面的程序当中 oparg = 1 。
- LOAD_CONST:加载对应的常量到栈帧当中,这里是会加载两个常量,分别是函数对应的 CodeObject 和函数名。
在执行完上的字节码之后栈帧当中 valuestack 如下所示:

- MAKE_FUNCTION:这条字节码的主要作用是根据上面三个栈里面的对象创建一个函数,其中最重要的字段就是 CodeObject 这里面保存了函数最重要的代码,最下面的元祖就是 inner_function 的 freevars,当虚拟机在创建函数的时候就已经把这个对象保存下来了,然后在创建栈帧的时候会将这个对象保存到栈帧。需要注意的是这里所保存的变量就是函数参数 x,他们是同一个对象。这就使得内部函数每次调用的时候都可以使用参数 x 。
我们再来看一下函数 inner_function 的字节码
- LOAD_DEREF:这个字节码会从栈帧的 freevars 数组当中加载下标为 oparg 的对象,freevars 就是刚刚在创建函数的时候所保存的,也就是 outter_function 传递给 inner_function 的元祖。直观的来说就是将外部函数的 x 加载到 valuestack 当中。
- STORE_DEREF:就是将栈顶的元素弹出,保存到 cellvars 数组对应的下标 (oparg) 当中。
后续的字节码就很简单了,这里不做详细分析了。
如果上面的过程太复杂,我们在这里从整体的角度再叙述一下,简单说来就是当有代码调用 outer_function 的时候,传递进来的参数,会在 outer_function 创建函数 inner_function 的时候当作闭包参数传递给 inner_function,这样 inner_function 就能够使用 outer_function 的参数了,因此这也不难理解,每次我们调用函数 outer_function 都会返回一个新的闭包(实际就是返回的新创建的函数),因为我们每次调用函数 outer_function 时,它都会创建一个新的函数,而这些被创建的函数唯一的区别就是他们的闭包参数不同。这也就解释了再之前的例子当中为什么两个闭包他们互不影响,因为函数 outer_function 创建了两个不同的函数。
总结
在本篇文章当中详细介绍了闭包的使用例子和使用原理,理解闭包最重要的一点就是函数和环境,也就是和函数绑定在一起的变量。当进行函数调用的时候函数就会创建一个新的内部函数,也就是闭包。在虚拟机内部实现闭包主要是通过函数参数传递和函数生成实现的,当执行 MAKE_FUNCTION 创建新函数的时候,会将外部函数的闭包变量 (在文章中就是 x ) 传递给内部函数,然后保存在内部函数当中,之后的每一次调用都是用这个变量,从而实现闭包的效果。
本篇文章是深入理解 python 虚拟机系列文章之一,文章地址:https://github.com/Chang-LeHung/dive-into-cpython
更多精彩内容合集可访问项目:https://github.com/Chang-LeHung/CSCore
关注公众号:一无是处的研究僧,了解更多计算机(Java、Python、计算机系统基础、算法与数据结构)知识。
相关文章:
深入理解 python 虚拟机:原来虚拟机是这么实现闭包的
深入理解 python 虚拟机:原来虚拟机是这么实现闭包的 在本篇文章当中主要从虚拟机层面讨论函数闭包是如何实现的,当能够从设计者的层面去理解闭包就再也不用死记硬背一些闭包的概念了,因为如果你理解闭包的设计原理之后,这些都是…...
【数据结构-哈希表 一】【原地哈希】:缺失的第一个正整数
废话不多说,喊一句号子鼓励自己:程序员永不失业,程序员走向架构!本篇Blog的主题是【原地哈希】,使用【数组】这个基本的数据结构来实现,这个高频题的站点是:CodeTop,筛选条件为&…...
【C++设计模式之迭代器模式】分析及示例
简介 迭代器模式是一种行为型设计模式,它提供了一种顺序访问聚合对象元素的方法,而又不需要暴露聚合对象的内部结构。迭代器模式通过将遍历算法封装在迭代器对象中,可以使得遍历过程更简洁、灵活,并且符合开闭原则。 描述 迭代…...
【代码随想录】LC 27. 移除元素
文章目录 前言一、题目1、原题链接2、题目描述 二、解题报告1、思路分析2、时间复杂度3、代码详解 三、知识风暴 前言 本专栏文章为《代码随想录》书籍的刷题题解以及读书笔记,如有侵权,立即删除。 一、题目 1、原题链接 27. 移除元素 2、题目描述 二、…...
crash工具分析dma设备内存踩踏(一)
背景介绍 我们的客户在利用我们提供的SDK参考方案开发相关产品时,在产品方案上进行一些基础老化测试时,极低概率出现kernel随机panic问题,由于场景复杂,无法单独针对特定模块或功能进行拆解来进行实验排查,只能基于已…...
C#上位机——根据命令发送
C#上位机——根据命令发送 第一步:设置窗口的布局 第二步:设置各个属性 第三步:编写各个模块之间的关系...
BEVFormer代码跑通
1 环境配置 1.1 环境安装 # 1 拉取源码 github加速代理https://ghproxy.com/ git clone https://github.com/fundamentalvision/BEVFormer.git# 2 创建虚拟环境 conda create -n bev python3.8 -y# 3 激活虚拟环境 conda activate bev# 4.1 安装torch,torchvision,torchaud…...
kafka安装
kafka安装 1 kafka概念 1.1 kafka介绍 kafka是最初有Linkedin公司开发的,是一个分布式,分区,多副本,多生产者,多订阅者,基于zookeeper协调的分布式日志系统。具有高吞吐量,可扩展性和可容错性…...
Mac上安装Java的JDK多版本管理软件jEnv
JDK的多版本管理软件主要有以下三种: jEnv jEnv 是一个命令行工具,可以帮助您管理和切换不同版本的 Java 环境。它可以让您在不同的项目之间轻松切换 Java 版本。您可以使用 jenv global 命令设置全局 Java 版本,也可以使用 jenv local 命令…...
linux常见命令以及jdk,tomcat环境搭建
目录 Is pwd cd touch cat echo vim 复制粘贴 mkdir rm cp jdk部署 1. yum list | grep jdk进行查找编辑 2.安装编辑 3.再次确认 4.判断是否安装成功 tomcat安装 1.下载压缩包,把压缩包上传至linux(可能需要yum install lrzsz) 2.解压缩unzip 压缩包名&…...
将表情存入数据库
概念: 表情是一种比较特殊的字符串,为unicode编码,unicode编码要存入数据库一般情况下,是存不了的,有两种解决方式,一种将数据表编码方式改为unicode编码方式,但是这种情况适用于功能刚开始设计…...
H桥级联型五电平三相逆变器Simulink仿真模型
💥💥💞💞欢迎来到本博客❤️❤️💥💥 🏆博主优势:🌞🌞🌞博客内容尽量做到思维缜密,逻辑清晰,为了方便读者。 ⛳️座右铭&a…...
后端解决跨域(极速版)
header(Access-Control-Allow-Origin: *); header(Access-Control-Allow-Methods:*); 代表接收全部的请求,"POST,GET"//允许访问的方式 指定域,如http://172.20.0.206//宝塔的域名,注意不是:http://wang.jingyi.icu等…...
数据结构与算法-前缀树
数据结构与算法-前缀树详解 1 何为前缀树 2 前缀树的代码表示及相关操作 1 何为前缀树 前缀树 又称之为字典树,是一种多路查找树,多路树形结构,是哈希树的变种,和hash效率有一拼,是一种用于快速检索的多叉树结构。 性质:不同字符串的相同…...
DirectX12_Windows_GameDevelop_3:Direct3D的初始化
引言 查看龙书时发现,第四章介绍预备知识的代码不太利于学习。因为它不像是LearnOpenGL那样从头开始一步一步教你敲代码,导致你没有一种整体感。如果你把它当作某一块的代码进行学习,你跟着敲会发现,总有几个变量是没有定义的。这…...
基于粒子群优化算法、鲸鱼算法、改进的淘沙骆驼模型算法(PSO/SSA/tGSSA)的微电网优化调度(Matlab代码实现)
💥💥💞💞欢迎来到本博客❤️❤️💥💥 🏆博主优势:🌞🌞🌞博客内容尽量做到思维缜密,逻辑清晰,为了方便读者。 ⛳️座右铭&a…...
数据分析篇-数据认知分析
一简介 数据认知分析,实际是对数据的整体结构和分布特征进行分析,是对整个数据外在的认识,也是数据分析的第一步。对于数据认知的分析,一般会考虑分散性、位置特性、变量的相关性等,一般会考虑平均数、方差、极差、峰…...
【力扣-每日一题】714. 买卖股票的最佳时机含手续费
class Solution { public:int maxProfit(vector<int>& prices, int fee) {//[i][0]-不持有 [i][1]-持有int mprices.size();vector<vector<int>> dp(m,vector<int>(2));dp[0][0]0; //初始状态dp[0][1]-prices[0];for(int i1;i<m;i){dp[i]…...
【代码实践】HAT代码Window平台下运行实践记录
HAT是CVPR2023上的自然图像超分辨率重建论文《activating More Pixels in Image Super-Resolution Transformer》所提出的模型。本文旨在记录在Window系统下运行该官方代码(https://github.com/XPixelGroup/HAT)的过程,中间会遇到一些问题&am…...
机器学习-Pytorch基础
Numpy和Pytorch可以相互转换,前者CPU上,后者GPU上,都是对矩阵进行运算,Pytorch的基本单位是张量。torch 可以初始化全为0、全为1、符合正态分布的矩阵确定性初始化 torch.tensor()torch.arrange()torch.linspace()torch.logspace…...
接口测试中缓存处理策略
在接口测试中,缓存处理策略是一个关键环节,直接影响测试结果的准确性和可靠性。合理的缓存处理策略能够确保测试环境的一致性,避免因缓存数据导致的测试偏差。以下是接口测试中常见的缓存处理策略及其详细说明: 一、缓存处理的核…...
【力扣数据库知识手册笔记】索引
索引 索引的优缺点 优点1. 通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。2. 可以加快数据的检索速度(创建索引的主要原因)。3. 可以加速表和表之间的连接,实现数据的参考完整性。4. 可以在查询过程中,…...
最新SpringBoot+SpringCloud+Nacos微服务框架分享
文章目录 前言一、服务规划二、架构核心1.cloud的pom2.gateway的异常handler3.gateway的filter4、admin的pom5、admin的登录核心 三、code-helper分享总结 前言 最近有个活蛮赶的,根据Excel列的需求预估的工时直接打骨折,不要问我为什么,主要…...
如何理解 IP 数据报中的 TTL?
目录 前言理解 前言 面试灵魂一问:说说对 IP 数据报中 TTL 的理解?我们都知道,IP 数据报由首部和数据两部分组成,首部又分为两部分:固定部分和可变部分,共占 20 字节,而即将讨论的 TTL 就位于首…...
Element Plus 表单(el-form)中关于正整数输入的校验规则
目录 1 单个正整数输入1.1 模板1.2 校验规则 2 两个正整数输入(联动)2.1 模板2.2 校验规则2.3 CSS 1 单个正整数输入 1.1 模板 <el-formref"formRef":model"formData":rules"formRules"label-width"150px"…...
USB Over IP专用硬件的5个特点
USB over IP技术通过将USB协议数据封装在标准TCP/IP网络数据包中,从根本上改变了USB连接。这允许客户端通过局域网或广域网远程访问和控制物理连接到服务器的USB设备(如专用硬件设备),从而消除了直接物理连接的需要。USB over IP的…...
Hive 存储格式深度解析:从 TextFile 到 ORC,如何选对数据存储方案?
在大数据处理领域,Hive 作为 Hadoop 生态中重要的数据仓库工具,其存储格式的选择直接影响数据存储成本、查询效率和计算资源消耗。面对 TextFile、SequenceFile、Parquet、RCFile、ORC 等多种存储格式,很多开发者常常陷入选择困境。本文将从底…...
LINUX 69 FTP 客服管理系统 man 5 /etc/vsftpd/vsftpd.conf
FTP 客服管理系统 实现kefu123登录,不允许匿名访问,kefu只能访问/data/kefu目录,不能查看其他目录 创建账号密码 useradd kefu echo 123|passwd -stdin kefu [rootcode caozx26420]# echo 123|passwd --stdin kefu 更改用户 kefu 的密码…...
C++:多态机制详解
目录 一. 多态的概念 1.静态多态(编译时多态) 二.动态多态的定义及实现 1.多态的构成条件 2.虚函数 3.虚函数的重写/覆盖 4.虚函数重写的一些其他问题 1).协变 2).析构函数的重写 5.override 和 final关键字 1&#…...
【JVM】Java虚拟机(二)——垃圾回收
目录 一、如何判断对象可以回收 (一)引用计数法 (二)可达性分析算法 二、垃圾回收算法 (一)标记清除 (二)标记整理 (三)复制 (四ÿ…...
