笔记|CPython|Python 程序运行原理

Python 是一种解释性语言(虽然该定义由于字节码编译器的存在而有所模糊),即不需要在运行前就编译成机器语言,而是在运行时才编译为机器语言。这意味着源文件可以直接运行而不必显式地创建可执行文件再运行。

资料 1:Python Documentation > glossary > interpreted

概括地说,Python 脚本的执行可以简化概括为如下两个步骤:

  1. Python 编译器:将 Python 代码编译为字节码
  2. Python 虚拟机:逐行执行字节码

Python 代码运行原理

下面,让我们以一个计算黄金分割的函数脚本为例,Python 脚本是如何被编译为字节码,字节码又是如何被运行的:

GOLD = 0.618

def get_golden_ratio(x):
    """计算黄金分割值"""
    return GOLD * x

print(get_golden_ratio(3))

我们首先将上述 Python 代码存入字符串 script,然后使用内置函数 compile 编译它,可以得到代码对象 code

>>> script = ("GOLD = 0.618\n"
...           "def get_golden_ratio(x):\n"
...           "    return GOLD * x\n"
...           "print(get_golden_ratio(3))")
>>> code = compile(script, "test.py", "exec")
>>> code
<code object <module> at 0x000002BCD0AE2290, file "test.py", line 1>

code 对象常用的属性及含义如下:

属性名称 属性含义
co_filename 创建 code 对象的文件名
co_firstlineno Python 源代码中第一行的行号
co_name code 对象的名称
co_code 字符串形式的原始字节码
co_consts 字节码中使用的常量元组
co_varnames 参数名和局部变量的标识符组成的元组
co_names 除参数和函数局部变量之外的标识符组成的元组
co_cellvars 单元变量的标识符的元组(通过包含作用域引用)
co_freevars 自由变量的标识符组成的元组(通过函数闭包引用)
co_stacksize 需要虚拟机的堆栈空间

资料 2:Python Documentation > inspect

例如,我们可以通过 co_code 属性查看这段代码的字节码(基于 Python 3.10):

>>> code.co_code
b'd\x00Z\x00d\x01d\x02\x84\x00Z\x01e\x02e\x01d\x03\x83\x01\x83\x01\x01\x00d\x04S\x00'
>>> [ch for ch in code.co_code]
[100, 0, 90, 0, 100, 1, 100, 2, 132, 0, 90, 1, 101, 2, 101, 1, 100, 3, 131, 1, 131, 1, 1, 0, 100, 4, 83, 0]

在 Python 3.6 及以上,每条字节码指令包含 2 个字节(即上述列表中每 2 个 0 - 255 之间的整数构成一条字节码指令),第 1 个字节是字节码指令,第 2 个字节是字节码指令的参数,如果字节码指令没有参数则使用 0 占位。需要注意的是,字节码是 CPython 解释器的实现细节,不保证不会在 Python 版本之间添加、删除或更改字节码。

资料 3:Python Documentation > glossary > bytecode

资料 4:Python Documentation > dis

也可以通过 co_names 属性查看这段代码使用的所有标识符,或通过 co_consts 属性查看这段代码使用的所有常量:

>>> code.co_names
('GOLD', 'get_golden_ratio', 'print')
>>> code.co_consts
(0.618, <code object get_golden_ratio at 0x000002BCD0AE3E10, file "test.py", line 2>, 'get_golden_ratio', 3, None)

通过这段代码使用的所有常量可以发现,函数 get_gold_ratio 被编译为另一个 code 对象,在这里被作为常量被引用。因此,我们可以进一步查看 get_gold_ratio 函数对应的字节码(基于 Python 3.10):

>>> code.co_consts[1].co_code
b't\x00|\x00\x14\x00S\x00'
>>> [ch for ch in code.co_consts[1].co_code]
[116, 0, 124, 0, 20, 0, 83, 0]

Python 虚拟机是一台完全通过软件定义的计算机,可执行字节码编译器所生成的字节码。

资料 5:Python Documentation > glossary > virtual machine

我们除通过 co_code 属性通过字符串形式的原始字节码分析 Python 虚拟机中的执行过程外,也可以结合标准库 dis,反编译上述 Python 代码,分析字节码在 Python 虚拟机中的执行过程(基于 Python 3.10):

>>> import dis
>>> dis.dis(script)
  1           0 LOAD_CONST               0 (0.618)
              2 STORE_NAME               0 (GOLD)
  2           4 LOAD_CONST               1 (<code object get_golden_ratio at 0x000002BCBEECFAA0, file "<dis>", line 2>)
              6 LOAD_CONST               2 ('get_golden_ratio')
              8 MAKE_FUNCTION            0
             10 STORE_NAME               1 (get_golden_ratio)
  4          12 LOAD_NAME                2 (print)
             14 LOAD_NAME                1 (get_golden_ratio)
             16 LOAD_CONST               3 (3)
             18 CALL_FUNCTION            1
             20 CALL_FUNCTION            1
             22 POP_TOP
             24 LOAD_CONST               4 (None)
             26 RETURN_VALUE
Disassembly of <code object get_golden_ratio at 0x000002BCBEECFAA0, file "<dis>", line 2>:
  3           0 LOAD_GLOBAL              0 (GOLD)
              2 LOAD_FAST                0 (x)
              4 BINARY_MULTIPLY
              6 RETURN_VALUE

按引用顺序自下而上,首先说明第 3 行(return GOLD * x)对应字节码的含义:

  • LOAD_GLOBAL(116), 0:读取上一层 code 对象的 co_names 中的第 0 个标识符(GOLD)的引用,并推入栈顶;此时栈中有 1 个元素;
  • LOAD_FAST(124), 0:读取当前 code 对象的 co_varnames 中的第 0 个标识符(x)的引用,并推入栈顶;此时栈中有 2 个元素;
  • BINARY_MULTIPLY(20), 0:连续出栈两个栈顶元素(分别是 GOLDx 的引用),执行 * 运算符,并将结果推入栈顶;此时栈中有 1 个元素;
  • RETURN_VALUE(83), 0:出栈栈顶元素,返回给调用者;此时栈中没有元素,第 3 行结束。

第 1 行(GOLD = 0.618)对应字节码的含义:

  • LOAD_CONST(100), 0:读取当前 code 对象的 co_consts 中第 0 个值(0.618)的引用,并推入栈顶;此时栈中有 1 个元素;
  • STORE_NAME(90), 0:出栈栈顶元素(0.618 的引用),并赋值给当前 code 对象的 co_names 中第 0 个标识符(GOLD);此时栈中没有元素,第 1 行结束。

第 2 行(def get_golden_ratio(x):)对应字节码的含义:

  • LOAD_CONST(100), 1:读取当前 code 对象的 co_consts 中第 1 个值(get_golden_ratiocode 对象)的引用;此时栈中有 1 个元素;
  • LOAD_CONST(100), 2:读取当前 code 对象的 co_consts 中第 2 个值(字符串 "get_golden_ratio")的引用;此时栈中有 2 个元素;
  • MAKE_FUNCTION(132), 0:出栈栈顶元素(字符串 "get_golden_ratio" 的引用)作为函数的名称;接着出栈栈顶元素(get_golden_ratiocode 对象的引用)作为函数关联的代码;构造新函数对象并推入栈顶;此时栈中有 1 个元素;
  • STORE_NAME(90), 1:出栈栈顶元素(新构造的 get_golden_ratio 函数代码对象),并赋值给当前 code 对象的 co_names 中第 1 个标识符(get_golden_ratio);此时栈中没有元素,第 2 行结束。

第 4 行(print(get_golden_ratio(3)))对应字节码的含义:

  • LOAD_NAME(101), 2:读取当前 code 对象的 co_names 中第 2 个标识符(内置函数 print 的代码对象)的引用,并推入栈顶;此时栈中有 1 个元素;
  • LOAD_NAME(101), 1:读取当前 code 对象的 co_names 中第 1 个标识符(get_golden_ratio 函数代码对象)的引用,并推入栈顶;此时栈中有 2 个元素;
  • LOAD_CONST(100), 3:读取当前 code 对象的 co_consts 中第 3 个值(整数 3)的引用,并推入栈顶;此时栈中有 3 个元素;
  • CALL_FUNCTION(131), 1:出栈 1 个栈顶元素(整数 3 的引用)作为函数的参数;接着出栈栈顶元素(get_golden_ratio 函数代码对象)作为被调用的可调用对象;然后附带着参数调用该可调用函数,将可调用对象所返回的返回值(get_golden_ratio 函数的返回值)推入栈顶;此时栈中有 2 个元素;
  • CALL_FUNCTION(131), 1:出栈 1 个栈顶元素(get_golden_ratio 函数的返回值的引用)作为函数的参数,接着出栈栈顶元素(内置函数 print 的代码对象)作为被调用的可调用对象;然后附带着参数调用该可调用函数,将可调用对象所返回的返回值(内置函数 print 的返回值 None)推入栈顶;此时栈中有 1 个元素;
  • POP_TOP(1), 0:出栈栈顶元素并删除;此时栈中没有元素;
  • LOAD_CONST(100), 4:读取当前 code 对象的 co_consts 中第 4 个值(None)的引用,并推入栈顶;此时栈中有 1 个元素;
  • RETURN_VALUE(83), 0:出栈栈顶元素,返回给调用者;此时栈中没有元素,第 4 行结束。

猜你喜欢

转载自blog.csdn.net/Changxing_J/article/details/129779161
今日推荐