首页 > 后端开发 > Python教程 > 我如何在 Python 字节码中添加对嵌套函数的支持

我如何在 Python 字节码中添加对嵌套函数的支持

Susan Sarandon
发布: 2024-12-31 18:58:18
原创
880 人浏览过

How I added support for nested functions in Python bytecode

我想分享一些非常酷的东西我一直在和你学习Python字节码,包括我如何添加对嵌套的支持函数,但我的印刷工说我需要将其控制在 500 字以内。

这是一个假期周,他耸耸肩。 你希望我做什么?

不包括代码片段,我讨价还价了。

好吧,他让出。

你知道我们为什么要使用字节码吗?

我只是操作印刷机,不过我相信你。

很公平。让我们开始吧。

为什么我们首先使用字节码

Memphis,我用 Rust 编写的 Python 解释器,有两个执行引擎。两者都不能运行所有代码,但都可以运行部分代码。

我的treewalk 解释器 是您在不知道自己在做什么的情况下构建的。 ?‍♂️ 对输入的 Python 代码进行标记,生成抽象语法树 (AST),然后遍历树并评估每个节点。表达式返回值和语句修改符号表,符号表被实现为一系列遵守 Python 作用域规则的作用域。只要记住简单的肺炎 LEGB:局部、封闭、全局、内置。

我的字节码虚拟机是如果你不知道自己在做什么但想像你一样行事时你会构建的。还有?‍♂️。对于这个引擎,令牌和 AST 的工作方式相同,但我们不是步行,而是冲刺。我们将 AST 编译为中间表示(IR),以下称为字节码。然后,我们创建一个基于堆栈的虚拟机 (VM),它在概念上类似于 CPU,按顺序执行字节码指令,但它完全由软件实现。

(对于这两种方法的完整指南,没有漫无目的,《Crafting Interpreters》非常好。)

我们首先为什么要这样做?只要记住两个 P:便携性和性能。还记得在 2000 年代初期,没有人会对 Java 字节码的可移植性闭嘴吗? 您只需要一个 JVM,就可以运行在任何机器上编译的 Java 程序!出于技术和营销原因,Python 选择不采用这种方法,但理论上同样的原则适用。 (实际上,编译步骤是不同的,我很遗憾打开了这罐蠕虫。)

性能才是最重要的。编译后的 IR 是一种更有效的表示形式,而不是在程序的生命周期内多次遍历 AST。我们看到,由于避免了重复遍历 AST 的开销,性能得到了提高,而且其扁平结构通常会在运行时带来更好的分支预测和缓存局部性。

(如果你没有计算机架构背景,我不会责怪你没有考虑缓存——哎呀,我的职业生涯是从这个行业开始的,我对缓存的考虑远远少于我对如何避免的考虑编写同一行代码两次。所以请相信我的性能部分。这就是我的领导风格:盲目信任。)

嘿伙计,这是 500 字。我们需要加载框架并让其撕裂。

已经?!您排除了代码片段吗?

没有代码片段,老兄。

好吧好吧。只剩下500多了。我保证。

上下文对于 Python 变量很重要

大约一年前,在提交我的字节码 VM 实现之前,我已经了解了很多:我可以定义 Python 函数和类,调用这些函数并实例化这些类。我通过一些测试限制了这种行为。但我知道我的实现很混乱,在添加更多有趣的东西之前我需要重新审视基础知识。现在是圣诞节周,我想添加一些有趣的东西。

考虑使用此代码片段来调用函数,并密切关注 TODO。

fn compile_function_call(
    &mut self,
    name: &str,
    args: &ParsedArguments)
) -> Result<Bytecode, CompileError> {
    let mut opcodes = vec![];

    // We push the args onto the stack in reverse call order so that we will pop
    // them off in call order.
    for arg in args.args.iter().rev() {
        opcodes.extend(self.compile_expr(arg)?);
    }

    let (_, index) = self.get_local_index(name);

    // TODO how does this know if it is a global or local index? this may not be the right
    // approach for calling a function
    opcodes.push(Opcode::Call(index));

    Ok(opcodes)
}
登录后复制
登录后复制

你考虑好了吗?我们将函数参数加载到堆栈上并“调用函数”。在字节码中,所有名称都会转换为索引(因为在虚拟机运行时索引访问速度更快),但我们实际上没有办法知道我们在这里处理的是本地索引还是全局索引。

现在考虑改进版本。

fn compile_function_call(
    &mut self,
    name: &str,
    args: &ParsedArguments)
) -> Result<Bytecode, CompileError> {
    let mut opcodes = vec![self.compile_load(name)];

    // We push the args onto the stack in reverse call order so that we will pop
    // them off in call order.
    for arg in args.args.iter().rev() {
        opcodes.extend(self.compile_expr(arg)?);
    }

    let argc = opcodes.len() - 1;
    opcodes.push(Opcode::Call(argc));

    Ok(opcodes)
}
登录后复制

感谢您考虑该代码。

我们现在支持嵌套函数调用!发生了什么变化?

  1. Call 操作码现在采用多个位置参数,而不是函数的索引。这指示 VM 在调用函数之前从堆栈中弹出多少个参数。
  2. 将参数从堆栈中弹出后,函数本身将留在堆栈上,并且compile_load已经为我们处理了本地与全局范围

LOAD_GLOBAL 与 LOAD_FAST

让我们看看compile_load 做了什么。

fn compile_load(&mut self, name: &str) -> Opcode {
    match self.ensure_context() {
        Context::Global => Opcode::LoadGlobal(self.get_or_set_nonlocal_index(name)),
        Context::Local => {
            // Check locals first
            if let Some(index) = self.get_local_index(name) {
                return Opcode::LoadFast(index);
            }

            // If not found locally, fall back to globals
            Opcode::LoadGlobal(self.get_or_set_nonlocal_index(name))
        }
    }
}
登录后复制

这里有几个关键的行动原则:

  1. 我们根据当前上下文进行匹配。遵循 Python 语义,我们可以认为 Context::Global 位于任何模块的顶层(而不仅仅是脚本的入口点),而 Context::Local 位于任何块内(即函数定义或类定义)。
  2. 我们现在区分本地索引和非本地索引。 (因为我疯狂地试图破译索引 0 在不同地方所指的内容,所以我引入了类型化整数。LocalIndex 和 NonlocalIndex 为其他非类型化无符号整数提供类型安全。我将来可能会写这个!)
  3. 我们可以在字节码编译时判断是否存在具有给定名称的局部变量,如果不存在,则在运行时我们将搜索全局变量。这说明了 Python 内置的动态性:只要在函数执行时变量存在于该模块的全局范围内,它的值就可以在运行时解析。然而,这种动态分辨率会带来性能损失。虽然局部变量查找已优化为使用堆栈索引,但全局查找需要搜索全局命名空间字典,速度较慢。该字典是名称到对象的映射,对象本身可能存在于堆上。谁知道有句话“放眼全球,行动本地”。实际上指的是 Python 作用域?

varname 中有什么?

今天我要留给您的最后一件事是看看这些变量名称是如何映射的。在下面的代码片段中,您会注意到在 code.varnames 中找到本地索引,在 code.names 中找到非本地索引。两者都存在于 CodeObject 上,其中包含 Python 字节码块的元数据,包括其变量和名称映射。

fn compile_function_call(
    &mut self,
    name: &str,
    args: &ParsedArguments)
) -> Result<Bytecode, CompileError> {
    let mut opcodes = vec![];

    // We push the args onto the stack in reverse call order so that we will pop
    // them off in call order.
    for arg in args.args.iter().rev() {
        opcodes.extend(self.compile_expr(arg)?);
    }

    let (_, index) = self.get_local_index(name);

    // TODO how does this know if it is a global or local index? this may not be the right
    // approach for calling a function
    opcodes.push(Opcode::Call(index));

    Ok(opcodes)
}
登录后复制
登录后复制

varnames 和 name 之间的区别折磨了我好几个星期(CPython 称这些为 co_varnames 和 co_names),但它实际上相当简单。 varnames 保存给定范围内所有局部变量的变量名称,names 保存所有非局部变量的变量名称。

一旦我们正确跟踪这一点,其他一切都会正常工作。在运行时,VM 会看到 LOAD_GLOBAL 或 LOAD_FAST,并知道分别查看全局命名空间字典或本地堆栈。

伙计!古腾堡先生打电话说我们不能再按了。

好的!美好的!我得到它!我们发货吧。 ?

孟菲斯的下一步是什么?

嘘!印刷工不知道我在写结论,所以我会简短地说。

通过固定的变量作用域和函数调用,我逐渐将注意力转向堆栈跟踪和异步支持等功能。如果您喜欢深入了解字节码或者对构建自己的解释器有疑问,我很乐意听取您的意见 - 发表评论!


订阅并节省[无任何]

如果您想将更多类似的帖子直接发送到您的收件箱,您可以在这里订阅!

和我一起工作

我指导软件工程师在一个有时有些愚蠢的支持性环境中应对技术挑战和职业发展。如果您有兴趣,可以在这里预约课程。

其他地方

除了指导之外,我还写了我在自营职业和晚期诊断自闭症方面的经验。更少的代码和相同数量的笑话。

  • 湖效应咖啡,第 2 章 - 来自 Scratch dot org

以上是我如何在 Python 字节码中添加对嵌套函数的支持的详细内容。更多信息请关注PHP中文网其他相关文章!

来源:dev.to
本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
作者最新文章
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板