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

python源码是如何运行起来的

为什么要了解底层原理

  • 写出高质量代码

  • 问题定位

  • 满足好奇心

  • 机械通感

开始

当我们编写并运行一行 print('Hello, World!') 时,背后究竟发生了什么?Python 代码是如何从我们可读的文本,变成计算机可以执行的指令的呢?

很多人将 Python 称为一门“解释型语言”,但这其实不完全准确。更精确地说,Python 是一门先编译后解释的语言,这点与java其实非常相似。

它的运行过程涉及两个核心阶段:

  1. 编译阶段:Python 解释器首先将我们编写的源代码(.py 文件)编译成一种称为字节码 (Bytecode) 的中间形式。

  2. 解释阶段:编译产生的字节码随后由 Python 虚拟机 (Python Virtual Machine, PVM) 来解释执行。

Part.1 从 Python 源代码到字节码

Python 代码的生命周期始于我们编写的 .py 文件。当我们执行一个 Python 脚本时,解释器并不直接逐行解析这些文本。相反,它会先进行一个“预处理”步骤——编译成字节码。

什么是字节码?

字节码是一种低级、与平台无关的指令集。它不像机器码那样是给物理 CPU 执行的,而是专门为 Python 虚拟机 (PVM) 设计的。这个设计使得 Python 代码拥有了出色的可移植性——同一份 Python 代码可以在任何安装了 Python 解释器的操作系统上运行,因为字节码是标准化的。

这个过程主要分为两个核心步骤:词法分析 (Lexical Analysis) 和 语法分析 (Parsing)。可以将它类比为我们阅读句子的过程:首先识别出每个单词(词法分析),然后理解整个句子的语法结构(语法分析)。

核心流程:

源代码 -> [词法分析器] -> 令牌流 -> [语法分析器] -> 抽象语法树 (AST)->字节码

第 1 步:词法分析 (Lexical Analysis) - 将代码分解为“token”

当 Python 解释器拿到您的源代码(一串纯文本字符)时,它做的第一件事不是去理解代码的逻辑,而是先将其打碎成一个个有意义的最小单元。这些单元被称为 “令牌” (Token)。

一个令牌通常包含两部分信息:

类型:例如 NAME (名称/标识符), NUMBER (数字), OP (操作符), KEYWORD (关键字)。

值:这个令牌对应的具体文本,例如 result, 10, +。

举个例子,对于我们之前用的代码 result = a + b,词法分析器会扫描这段文本,并生成类似下面这样的一个“令牌流”:

NAME: 'result'
OP: '='
NAME: 'a'
OP: '+'
NAME: 'b'

您可以把这个阶段看作是给代码“断词”。Python 内置的 tokenize 模块可以让我们亲眼看到这个过程:

import tokenize
from io import BytesIOcode = """
result=a+b
"""tokens = tokenize.tokenize(BytesIO(code.encode('utf-8')).readline)for token in tokens:print(token)

结果

TokenInfo(type=63 (ENCODING), string='utf-8', start=(0, 0), end=(0, 0), line='')
TokenInfo(type=62 (NL), string='\n', start=(1, 0), end=(1, 1), line='\n')
TokenInfo(type=1 (NAME), string='result', start=(2, 0), end=(2, 6), line='result=a+b\n')
TokenInfo(type=54 (OP), string='=', start=(2, 6), end=(2, 7), line='result=a+b\n')
TokenInfo(type=1 (NAME), string='a', start=(2, 7), end=(2, 8), line='result=a+b\n')
TokenInfo(type=54 (OP), string='+', start=(2, 8), end=(2, 9), line='result=a+b\n')
TokenInfo(type=1 (NAME), string='b', start=(2, 9), end=(2, 10), line='result=a+b\n')
TokenInfo(type=4 (NEWLINE), string='\n', start=(2, 10), end=(2, 11), line='result=a+b\n')
TokenInfo(type=0 (ENDMARKER), string='', start=(3, 0), end=(3, 0), line='')

类型名

描述

NAME

变量名、关键字等

NUMBER

数值

STRING

字符串

OP

运算符

INDENT

缩进

DEDENT

取消缩进

NEWLINE

语句结束

ENCODING

文件编码(如 utf-8)

COMMENT

注释

NL

非逻辑新行

ENDMARKER

文件结束标记

第2步:语法分析

现在我们有了一个扁平的、线性的令牌列表,但解释器还不知道它们之间的关系。这时 解析器 (Parser) 就登场了。

解析器的任务是接收词法分析器生成的令牌流,并根据 Python 语言预设的语法规则 (Grammar),将这些令牌组织成一个具有层级结构的树。这个树就是抽象语法树 (AST)。

继续我们的例子 result = a + b:

解析器拿到 NAME, OP('='), NAME, OP('+'), NAME 这个令牌序列。

它根据 Python 的语法规则,识别出这是一个“赋值语句”的模式。

于是它创建一个 Assign (赋值) 节点作为根。

语法规定,= 左边的是赋值目标 (target),于是它把第一个 NAME('result') 令牌作为 Assign 节点的 targets 子节点。

= 右边的 a + b 是要赋的值 (value)。解析器接着分析 NAME('a'), OP('+'), NAME('b') 这部分。

它识别出这是一个“二元运算” (Binary Operation) 的模式。

于是它创建一个 BinOp 节点,并将其作为 Assign 节点的 value 子节点。

语法规定,+ 是操作符 (op),a 是左操作数 (left),b 是右操作数 (right)。解析器相应地创建 Add、Name('a') 和 Name('b') 节点,并将它们作为 BinOp 节点的子节点。

经过这一系列操作,一个扁平的令牌流就变成了一个能清晰反映代码逻辑和结构的树状数据结构——AST。这个 AST 完整地表达了“将变量 a 和 b 相加的结果,赋值给变量 result”这一操作,并且已经去除了所有不影响语法的细节(如空格)。 将 AST(抽象语法树)转化为字节码的过程,是由 Python 的编译器 (Compiler) 完成的。这个过程可以被理解为一个**“翻译”**工作:编译器读取结构化的 AST,并将其翻译成一个线性的、供 Python 虚拟机 (PVM) 执行的指令列表。

这个“翻译”过程的核心思想是 “树的遍历” (Tree Traversal)。

第3步:遍历 AST 并生成指令

编译器会从 AST 的根节点开始,以深度优先的方式递归地访问(或称“遍历”)树中的每一个节点。对于每一种类型的节点(如 AssignBinOpName 等),编译器都有一个专门的处理函数。这个设计模式通常被称为 访问者模式 (Visitor Pattern)。

让我们以我们熟悉的 result = a + b 为例,看看编译器是如何“走”过它的 AST 并生成字节码的。

import ast# 我们要分析的简单代码行
code_line = """
# 这是一个我们编写的简单函数
result = a + b
"""# 解析代码并获取 AST
tree = ast.parse(code_line)# ast.dump() 可以用一种开发者友好的方式打印出树的结构
print(ast.dump(tree, indent=4))

这是它的 AST 结构:

Assign(targets=[Name(id='result')],value=BinOp(left=Name(id='a'),op=Add(),right=Name(id='b'))
)

编译器的工作流程如下:

  1. 访问 Assign (赋值) 节点

    • 编译器的规则是:要完成一次赋值,必须先计算出右边的值。

    • 因此,它不会立即生成赋值指令,而是递归地去访问 Assign 节点的 value 子节点,也就是 BinOp 节点。

  2. 访问 BinOp (二元运算) 节点

    • 编译器的规则是:要计算一个二元运算,必须先得到左右两个操作数的值。

    • 它首先递归地访问 left 子节点,也就是 Name(id='a')

  3. 访问 Name(id='a') 节点

    • 编译器看到这是一个变量名,并且当前上下文是需要“加载”它的值。

    • 于是,它生成第一条字节码指令:LOAD_FAST a。这条指令的作用是:在程序运行时,从本地变量中找到 a 的值,并将其推到 PVM 的操作数堆栈顶部。

    • a 节点处理完毕,返回到 BinOp 节点的访问流程。

  4. 回到 BinOp 节点

    • 左边已经处理完。现在,编译器递归地访问 right 子节点,也就是 Name(id='b')

  5. 访问 Name(id='b') 节点

    • 和处理 a 一样,编译器生成第二条字节码指令:LOAD_FAST b。运行时,b 的值会被推到堆栈顶部。

    • b 节点处理完毕,返回到 BinOp 节点的访问流程。

  6. 再次回到 BinOp 节点

    • 现在,左右两个子节点都处理完了。编译器知道,在运行时,a 和 b 的值会依次位于操作数堆栈上。

    • 它查看操作符 op 是 Add()

    • 于是,它生成第三条字节码指令:BINARY_ADD。这条指令会从堆栈弹出顶部的两个值,将它们相加,然后把结果再推回堆栈顶部。

    • BinOp 节点处理完毕,返回到 Assign 节点的访问流程。

  7. 再次回到 Assign 节点

    • 现在,value 子节点(即 a + b)已经完全处理完。编译器知道,在运行时,a + b 的计算结果正位于操作数堆栈的顶部。

    • 它查看赋值目标 targets 是 Name(id='result')

    • 于是,它生成第四条字节码指令:STORE_FAST result。这条指令会从堆栈顶部弹出那个计算结果,并将其存入名为 result 的本地变量中。

    • Assign 节点处理完毕。

至此,result = a + b 这行代码的 AST 就被完整地遍历并转换成了字节码序列:

LOAD_FAST     a
LOAD_FAST     b
BINARY_ADD
STORE_FAST    result

Part.2 解释执行

当字节码准备就绪后,就轮到 Python 虚拟机 (PVM) 登场了。PVM 是 Python 的运行时引擎,是真正执行代码的地方。它是一个在您的操作系统上运行的软件程序,它模拟了一台“理想”的计算机,专门用来执行 Python 字节码。

PVM 的核心可以理解为一个巨大的 switch 循环,我们称之为评估循环 (Evaluation Loop) 或解释器循环。这个循环不断地做三件事:

  1. 读取下一条字节码指令。

  2. 根据指令的类型,执行相应的操作。

  3. 重复此过程,直到没有指令为止。

PVM 是一个堆栈机 (Stack-based machine)。这意味着它使用一种叫做“调用堆栈 (Call Stack)”的数据结构来管理所有的数据和操作。当一个函数被调用时,PVM 会为这个函数创建一个新的帧 (Frame),并将其推入调用堆栈的顶部。

一个帧包含了执行该函数所需的所有信息,主要包括:

  • 本地变量 (Local variables): 存储函数内部定义的变量,如 ab 和 result

  • 操作数堆栈 (Operand Stack): 也叫评估堆栈 (Evaluation Stack),用于执行字节码指令的临时工作区。例如 BINARY_ADD 就是在这个堆栈上完成计算的。

  • 指令指针 (Instruction Pointer): 指向当前正在执行的字节码指令。

  • 返回地址: 指向调用该函数的代码位置,以便函数执行完毕后可以返回。

CPython解释器核心源码

https://github.com/python/cpython/blob/3.9/Python/ceval.c

       case TARGET(LOAD_FAST): {
            PyObject *value = GETLOCAL(oparg);
            if (value == NULL) {
                format_exc_check_arg(tstate, PyExc_UnboundLocalError,
                                     UNBOUNDLOCAL_ERROR_MSG,
                                     PyTuple_GetItem(co->co_varnames, oparg));
                goto error;
            }
            Py_INCREF(value);
            PUSH(value);
            FAST_DISPATCH();
        }

        case TARGET(LOAD_CONST): {
            PREDICTED(LOAD_CONST);
            PyObject *value = GETITEM(consts, oparg);
            Py_INCREF(value);
            PUSH(value);
            FAST_DISPATCH();
        }

        case TARGET(STORE_FAST): {
            PREDICTED(STORE_FAST);
            PyObject *value = POP();
            SETLOCAL(oparg, value);
            FAST_DISPATCH();
        }

留个坑

GIL、JIT、GC

http://www.dtcms.com/a/320108.html

相关文章:

  • HTTPS是如何确保网站安全性的?
  • 【Apache Olingo】全面深入分析报告-OData
  • 使用Ollama本地部署DeepSeek、GPT等大模型
  • C++模拟法超超超详细指南
  • 连续最高天数的销售额(动态规划)
  • 如何让keil编译生成bin文件与反汇编文件?
  • 机器学习:线性回归
  • Win10桌面从默认C盘改到D盘
  • 小红书开源多模态视觉语言模型DOTS-VLM1
  • 深入剖析React框架原理:从虚拟DOM到Fiber架构
  • PCA9541调试记录
  • 软考中级【网络工程师】第6版教材 第2章 数据通信基础(下)
  • ansible 操作家族(ansible_os_family)信息
  • 网页中 MetaMask 钱包钱包交互核心功能详解
  • Redis缓存数据库深度剖析
  • ESXI7.0添加标准交换机过程
  • 通过CNN、LSTM、CNN-LSTM及SSA-CNN-LSTM模型对数据进行预测,并进行全面的性能对比与可视化分析
  • [Oracle] DECODE()函数
  • [Oracle] GREATEST()函数
  • GCC与NLP实战:编译技术赋能自然语言处理
  • Kubernetes(k8s)之Service服务
  • 【C语言】深入理解编译与链接过程
  • Java中的反射机制
  • 【AxureMost落葵网】企业ERP项目原型-免费
  • 上位机知识篇篇---驱动
  • Xvfb虚拟屏幕(Linux)中文入门篇1:(wikipedia摘要,适当改写)
  • 函数、方法和计算属性
  • 计网学习笔记第3章 数据链路层(灰灰题库)
  • [激光原理与应用-169]:测量仪器 - 能量型 - 光功率计(功率稳定性监测)
  • 记录:rk3568适配开源GPU驱动(panfrost)