我想分享一些非常酷的东西我一直在和你学习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多了。我保证。
大约一年前,在提交我的字节码 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) }
感谢您考虑该代码。
我们现在支持嵌套函数调用!发生了什么变化?
让我们看看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)) } } }
这里有几个关键的行动原则:
今天我要留给您的最后一件事是看看这些变量名称是如何映射的。在下面的代码片段中,您会注意到在 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,并知道分别查看全局命名空间字典或本地堆栈。
伙计!古腾堡先生打电话说我们不能再按了。
好的!美好的!我得到它!我们发货吧。 ?
嘘!印刷工不知道我在写结论,所以我会简短地说。
通过固定的变量作用域和函数调用,我逐渐将注意力转向堆栈跟踪和异步支持等功能。如果您喜欢深入了解字节码或者对构建自己的解释器有疑问,我很乐意听取您的意见 - 发表评论!
订阅并节省[无任何]
如果您想将更多类似的帖子直接发送到您的收件箱,您可以在这里订阅!
和我一起工作
我指导软件工程师在一个有时有些愚蠢的支持性环境中应对技术挑战和职业发展。如果您有兴趣,可以在这里预约课程。
其他地方
除了指导之外,我还写了我在自营职业和晚期诊断自闭症方面的经验。更少的代码和相同数量的笑话。
以上是我如何在 Python 字节码中添加对嵌套函数的支持的详细内容。更多信息请关注PHP中文网其他相关文章!