2025-10-01 Python不基础 1——字节码和虚拟机

文章目录

  • [1 `code object`和`frame`](#1 code objectframe)
  • [2 栈虚拟机](#2 栈虚拟机)
  • [3 CPython源码中的实现](#3 CPython源码中的实现)
  • [4 总结](#4 总结)

1 code objectframe

Python代码在执行前,并非直接被CPU理解和运行。它经历了一个编译和解释的过程。

  1. 编译成code object:

    编写的所有Python代码,无论是脚本文件还是一个函数,首先都会被编译器(Compiler)转换成一种中间形式,称为字节码(Bytecode)。这些字节码被打包存储在一个名为code object的结构中。这个对象不仅包含了字节码指令,还包含了执行这段代码所需的各种元信息,如常量、变量名等。

  2. 创建执行环境frame:

    当Python解释器准备执行一段字节码时,它会首先创建一个帧(frame)。可以把frame理解为一个独立的、用于执行代码的"沙盒"或上下文环境。

    • 何时创建
      • 程序启动时会创建一个顶层frame
      • 每当调用一个函数时,都会为该函数的执行创建一个新的frame
    • frame包含什么
      • 局部变量(f_locals):函数内部定义的变量。
      • 全局变量(f_globals):在全局作用域中可访问的变量。
      • 代码对象引用(f_code) :指向需要在此frame中执行的那个code object
      • 指令指针(f_lasti):记录上一条执行过的字节码指令位置,控制代码的分支和流程。
      • 一个数据栈(f_back):这是虚拟机执行字节码操作的核心区域,我们稍后会详细介绍。

2 栈虚拟机

Python虚拟机的一个核心设计思想是**"基于栈"(Stack-Based)**。这意味着几乎所有的字节码指令都在做两类事情之一:

  1. 操作数据栈:将数据(如常量、变量的值)压入栈顶(Push),或者从栈顶弹出数据(Pop)。
  2. 进行计算:从栈顶取出(Pop)一个或多个数据,进行计算(如加法、比较),然后将计算结果再压回(Push)栈顶。

这个"栈"是一个后进先出(LIFO, Last-In, First-Out)的数据结构,就像一摞盘子,你最后放上去的盘子总是最先被取走。

实例:一个加法函数

假设有这样一个简单的Python函数:

Python 复制代码
def add(a, b):
    return a + b

使用dis模块(disassembler,反汇编器)可以查看它对应的字节码:

复制代码
  2           0 LOAD_FAST                0 (a)
              2 LOAD_FAST                1 (b)
              4 BINARY_ADD
              6 RETURN_VALUE

下面我们来逐步分解虚拟机的执行流程:

  1. LOAD_FAST 0 (a)
    • LOAD_FAST是一条加载局部变量的指令。
    • 0是变量acode object变量名列表中的索引。
    • 动作 :虚拟机找到局部变量a的值,并将其压入(Push)栈顶 。此时,栈里只有一个元素:a的值。
  2. LOAD_FAST 1 (b)
    • 1是变量b的索引。
    • 动作 :虚拟机找到局部变量b的值,并将其压入(Push)栈顶 。现在,栈从底到顶依次是:a的值,b的值。
  3. BINARY_ADD
    • 这是一条二元加法指令。
    • 动作
      1. 从栈顶**弹出(Pop)**一个元素,得到b的值。
      2. 再次从栈顶**弹出(Pop)**一个元素,得到a的值。
      3. 计算这两个值的和(a + b)。
      4. 将计算出的结果压回(Push)栈顶
    • 执行完毕后,栈里只有一个元素:a + b的和。
  4. RETURN_VALUE
    • 这是一条函数返回指令。
    • 动作 :从栈顶**弹出(Pop)**唯一的元素(即a + b的和),并将其作为整个函数的返回值。这个frame的生命周期至此结束。

3 CPython源码中的实现

如果我们想知道这些字节码指令究竟是如何实现的,就需要查看CPython(官方的Python实现)的C语言源代码。

  • 核心文件ceval.c
  • 核心函数_PyEval_EvalFrameDefault

这个函数是CPython的"心脏",它负责解释和执行字节码。其内部结构大致如下:

  1. 一个巨大的主循环(main_loop) :这个循环会不断地读取并执行code object中的下一条字节码指令,直到没有指令可执行。

  2. 一个庞大的switch语句 :在循环内部,有一个switch语句,它根据当前读取到的字节码指令类型(Opcode),跳转到相应的C代码块去执行。

例如:

  • LOAD_FAST的实现 :对应的C代码会通过GETLOCAL(oparg)获取局部变量,然后通过PUSH()宏将其压入虚拟机栈。
  • BINARY_ADD的实现 :对应的C代码会POP两次栈来获取操作数,调用C语言的加法函数,然后SET_TOP()(相当于PUSH)将结果放回栈顶。
  • RETURN_VALUE的实现 :它的C代码除了POP返回值外,还会做一些额外工作,比如将当前frame的状态设置为FRAME_RETURNED(已返回),然后通过goto exiting跳出主执行循环,完成函数的返回流程。

4 总结

Python程序的运行是一个层次分明、逻辑清晰的过程:

  1. 创建Frame :每次函数调用或程序启动时,都会创建一个新的执行环境frame
  2. 顺序执行字节码 :在frame的环境下,Python虚拟机会像CPU执行汇编指令一样,一条一条地执行code object中的字节码。
  3. 与Stack交互:绝大多数字节码指令都是围绕一个内部数据栈(Stack)进行操作,包括加载数据到栈、从栈中取数据计算、将结果存回栈等。
  4. C语言实现 :每一条字节码指令都在CPython的底层(主要是ceval.c)有对应的C语言代码实现。

这个机制与汇编语言在硬件上运行的模式非常相似,不同之处在于,Python的字节码是运行在一个用C语言编写的软件虚拟机之上,而不是直接运行在物理硬件上。理解了这个流程,就能对Python的运行原理有一个更深刻的认识。

相关推荐
lly2024063 小时前
PostgreSQL LIMIT 语句详解
开发语言
山,离天三尺三3 小时前
线程中互斥锁和读写锁相关区别应用示例
linux·c语言·开发语言·面试·职场和发展
讓丄帝愛伱3 小时前
阿里开源 Java 诊断神器Arthas
java·linux·开发语言·开源
扑克中的黑桃A3 小时前
Python快速入门专业版(十一):布尔值与None:Python逻辑判断的基石(深度解析真值、假值与空状态处理)
python
扑克中的黑桃A4 小时前
Python快速入门专业版(十二):数据类型转换:int/str/float/bool之间的转换规则(避坑指南)
python
魂尾ac4 小时前
Django + Vue3 前后端分离技术实现自动化测试平台从零到有系列 <第三章> 之 基础架构搭建
python·架构·django
大模型真好玩4 小时前
深入浅出LangGraph AI Agent智能体开发教程(九)—LangGraph长短期记忆管理
人工智能·python·agent
好开心啊没烦恼4 小时前
图数据库:基于历史学科的全球历史知识图谱构建,使用Neo4j图数据库实现中国历史与全球历史的关联查询。
大数据·数据库·python·数据挖掘·数据分析·知识图谱·neo4j
李宥小哥4 小时前
C#基础09-面向对象关键字
开发语言·c#