当前位置: 首页 > news >正文

深入理解 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 虚拟机&#xff1a;原来虚拟机是这么实现闭包的 在本篇文章当中主要从虚拟机层面讨论函数闭包是如何实现的&#xff0c;当能够从设计者的层面去理解闭包就再也不用死记硬背一些闭包的概念了&#xff0c;因为如果你理解闭包的设计原理之后&#xff0c;这些都是…...

【数据结构-哈希表 一】【原地哈希】:缺失的第一个正整数

废话不多说&#xff0c;喊一句号子鼓励自己&#xff1a;程序员永不失业&#xff0c;程序员走向架构&#xff01;本篇Blog的主题是【原地哈希】&#xff0c;使用【数组】这个基本的数据结构来实现&#xff0c;这个高频题的站点是&#xff1a;CodeTop&#xff0c;筛选条件为&…...

【C++设计模式之迭代器模式】分析及示例

简介 迭代器模式是一种行为型设计模式&#xff0c;它提供了一种顺序访问聚合对象元素的方法&#xff0c;而又不需要暴露聚合对象的内部结构。迭代器模式通过将遍历算法封装在迭代器对象中&#xff0c;可以使得遍历过程更简洁、灵活&#xff0c;并且符合开闭原则。 描述 迭代…...

【代码随想录】LC 27. 移除元素

文章目录 前言一、题目1、原题链接2、题目描述 二、解题报告1、思路分析2、时间复杂度3、代码详解 三、知识风暴 前言 本专栏文章为《代码随想录》书籍的刷题题解以及读书笔记&#xff0c;如有侵权&#xff0c;立即删除。 一、题目 1、原题链接 27. 移除元素 2、题目描述 二、…...

crash工具分析dma设备内存踩踏(一)

背景介绍 我们的客户在利用我们提供的SDK参考方案开发相关产品时&#xff0c;在产品方案上进行一些基础老化测试时&#xff0c;极低概率出现kernel随机panic问题&#xff0c;由于场景复杂&#xff0c;无法单独针对特定模块或功能进行拆解来进行实验排查&#xff0c;只能基于已…...

C#上位机——根据命令发送

C#上位机——根据命令发送 第一步&#xff1a;设置窗口的布局 第二步&#xff1a;设置各个属性 第三步&#xff1a;编写各个模块之间的关系...

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公司开发的&#xff0c;是一个分布式&#xff0c;分区&#xff0c;多副本&#xff0c;多生产者&#xff0c;多订阅者&#xff0c;基于zookeeper协调的分布式日志系统。具有高吞吐量&#xff0c;可扩展性和可容错性…...

Mac上安装Java的JDK多版本管理软件jEnv

JDK的多版本管理软件主要有以下三种&#xff1a; jEnv jEnv 是一个命令行工具&#xff0c;可以帮助您管理和切换不同版本的 Java 环境。它可以让您在不同的项目之间轻松切换 Java 版本。您可以使用 jenv global 命令设置全局 Java 版本&#xff0c;也可以使用 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.下载压缩包&#xff0c;把压缩包上传至linux(可能需要yum install lrzsz) 2.解压缩unzip 压缩包名&…...

将表情存入数据库

概念&#xff1a; 表情是一种比较特殊的字符串&#xff0c;为unicode编码&#xff0c;unicode编码要存入数据库一般情况下&#xff0c;是存不了的&#xff0c;有两种解决方式&#xff0c;一种将数据表编码方式改为unicode编码方式&#xff0c;但是这种情况适用于功能刚开始设计…...

H桥级联型五电平三相逆变器Simulink仿真模型

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…...

后端解决跨域(极速版)

header(Access-Control-Allow-Origin: *); header(Access-Control-Allow-Methods:*); 代表接收全部的请求&#xff0c;"POST,GET"//允许访问的方式 指定域&#xff0c;如http://172.20.0.206//宝塔的域名&#xff0c;注意不是&#xff1a;http://wang.jingyi.icu等…...

数据结构与算法-前缀树

数据结构与算法-前缀树详解 1 何为前缀树 2 前缀树的代码表示及相关操作 1 何为前缀树 前缀树 又称之为字典树,是一种多路查找树,多路树形结构,是哈希树的变种&#xff0c;和hash效率有一拼&#xff0c;是一种用于快速检索的多叉树结构。 性质&#xff1a;不同字符串的相同…...

DirectX12_Windows_GameDevelop_3:Direct3D的初始化

引言 查看龙书时发现&#xff0c;第四章介绍预备知识的代码不太利于学习。因为它不像是LearnOpenGL那样从头开始一步一步教你敲代码&#xff0c;导致你没有一种整体感。如果你把它当作某一块的代码进行学习&#xff0c;你跟着敲会发现&#xff0c;总有几个变量是没有定义的。这…...

基于粒子群优化算法、鲸鱼算法、改进的淘沙骆驼模型算法(PSO/SSA/tGSSA)的微电网优化调度(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…...

数据分析篇-数据认知分析

一简介 数据认知分析&#xff0c;实际是对数据的整体结构和分布特征进行分析&#xff0c;是对整个数据外在的认识&#xff0c;也是数据分析的第一步。对于数据认知的分析&#xff0c;一般会考虑分散性、位置特性、变量的相关性等&#xff0c;一般会考虑平均数、方差、极差、峰…...

【力扣-每日一题】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系统下运行该官方代码&#xff08;https://github.com/XPixelGroup/HAT&#xff09;的过程&#xff0c;中间会遇到一些问题&am…...

机器学习-Pytorch基础

Numpy和Pytorch可以相互转换&#xff0c;前者CPU上&#xff0c;后者GPU上&#xff0c;都是对矩阵进行运算&#xff0c;Pytorch的基本单位是张量。torch 可以初始化全为0、全为1、符合正态分布的矩阵确定性初始化 torch.tensor()torch.arrange()torch.linspace()torch.logspace…...

大话软工笔记—需求分析概述

需求分析&#xff0c;就是要对需求调研收集到的资料信息逐个地进行拆分、研究&#xff0c;从大量的不确定“需求”中确定出哪些需求最终要转换为确定的“功能需求”。 需求分析的作用非常重要&#xff0c;后续设计的依据主要来自于需求分析的成果&#xff0c;包括: 项目的目的…...

突破不可导策略的训练难题:零阶优化与强化学习的深度嵌合

强化学习&#xff08;Reinforcement Learning, RL&#xff09;是工业领域智能控制的重要方法。它的基本原理是将最优控制问题建模为马尔可夫决策过程&#xff0c;然后使用强化学习的Actor-Critic机制&#xff08;中文译作“知行互动”机制&#xff09;&#xff0c;逐步迭代求解…...

大语言模型如何处理长文本?常用文本分割技术详解

为什么需要文本分割? 引言:为什么需要文本分割?一、基础文本分割方法1. 按段落分割(Paragraph Splitting)2. 按句子分割(Sentence Splitting)二、高级文本分割策略3. 重叠分割(Sliding Window)4. 递归分割(Recursive Splitting)三、生产级工具推荐5. 使用LangChain的…...

OpenLayers 分屏对比(地图联动)

注&#xff1a;当前使用的是 ol 5.3.0 版本&#xff0c;天地图使用的key请到天地图官网申请&#xff0c;并替换为自己的key 地图分屏对比在WebGIS开发中是很常见的功能&#xff0c;和卷帘图层不一样的是&#xff0c;分屏对比是在各个地图中添加相同或者不同的图层进行对比查看。…...

Unity | AmplifyShaderEditor插件基础(第七集:平面波动shader)

目录 一、&#x1f44b;&#x1f3fb;前言 二、&#x1f608;sinx波动的基本原理 三、&#x1f608;波动起来 1.sinx节点介绍 2.vertexPosition 3.集成Vector3 a.节点Append b.连起来 4.波动起来 a.波动的原理 b.时间节点 c.sinx的处理 四、&#x1f30a;波动优化…...

深度学习习题2

1.如果增加神经网络的宽度&#xff0c;精确度会增加到一个特定阈值后&#xff0c;便开始降低。造成这一现象的可能原因是什么&#xff1f; A、即使增加卷积核的数量&#xff0c;只有少部分的核会被用作预测 B、当卷积核数量增加时&#xff0c;神经网络的预测能力会降低 C、当卷…...

4. TypeScript 类型推断与类型组合

一、类型推断 (一) 什么是类型推断 TypeScript 的类型推断会根据变量、函数返回值、对象和数组的赋值和使用方式&#xff0c;自动确定它们的类型。 这一特性减少了显式类型注解的需要&#xff0c;在保持类型安全的同时简化了代码。通过分析上下文和初始值&#xff0c;TypeSc…...

Cilium动手实验室: 精通之旅---13.Cilium LoadBalancer IPAM and L2 Service Announcement

Cilium动手实验室: 精通之旅---13.Cilium LoadBalancer IPAM and L2 Service Announcement 1. LAB环境2. L2公告策略2.1 部署Death Star2.2 访问服务2.3 部署L2公告策略2.4 服务宣告 3. 可视化 ARP 流量3.1 部署新服务3.2 准备可视化3.3 再次请求 4. 自动IPAM4.1 IPAM Pool4.2 …...

HybridVLA——让单一LLM同时具备扩散和自回归动作预测能力:训练时既扩散也回归,但推理时则扩散

前言 如上一篇文章《dexcap升级版之DexWild》中的前言部分所说&#xff0c;在叠衣服的过程中&#xff0c;我会带着团队对比各种模型、方法、策略&#xff0c;毕竟针对各个场景始终寻找更优的解决方案&#xff0c;是我个人和我司「七月在线」的职责之一 且个人认为&#xff0c…...

Java 与 MySQL 性能优化:MySQL 慢 SQL 诊断与分析方法详解

文章目录 一、开启慢查询日志&#xff0c;定位耗时SQL1.1 查看慢查询日志是否开启1.2 临时开启慢查询日志1.3 永久开启慢查询日志1.4 分析慢查询日志 二、使用EXPLAIN分析SQL执行计划2.1 EXPLAIN的基本使用2.2 EXPLAIN分析案例2.3 根据EXPLAIN结果优化SQL 三、使用SHOW PROFILE…...