python的编译和执行过程

58 阅读6分钟

python的编译和执行过程

python 是一种解释型的编程语言,所以不像编译型语言那样需要显式的编译过程。然而,在 Python 代码执行之前,它需要被解释器转换成字节码,这个过程就是 Python 的编译过程。

编译和执行全过程

257491068_3_20221214083632680_wm

假设我们有以下 Python 代码:

 def add_numbers(a, b):
     return a + b
  
 print(add_numbers(2, 3))

当我们执行Python代码的时候,Python解释器用四个过程“拆解”我们的代码,最终被CPU执行返回给用户。

  1. 词法分析(Lexical Analysis)

解释器会先将 Python 源代码分解成一系列的 Token(标记),每个 Token 包含了源代码中的一个词汇单元(例如变量名、关键字、操作符等)。这个过程又叫做扫描(Scanning),Python 解释器使用了一个名为 tokenizer 的模块来完成这个任务。Python 解释器将上面的代码分解成以下 Token:

 def, add_numbers, (, a, ,, b, ), :, return, a, +, b, print, (, add_numbers, (, 2, ,, 3, ), ), EOF

例如用户键入关键字或者当输入关键字有误时,都会被词法分析所触发,不正确的代码将不会被执行。

  1. 语法分析(Parsing)

解释器会使用 Token,构建语法树(Syntax Tree)或抽象语法树(Abstract Syntax Tree,AST),以此来表示代码的结构和语法。AST 包含了源代码中的所有关键信息,并为解释器提供了执行代码的指令。Python 解释器使用了一个名为 parser 的模块来完成这个任务。Python 解释器使用 上面词法分析的Token 生成以下 AST

 Module(body=[
     FunctionDef(name='add_numbers', args=arguments(args=[
         arg(arg='a', annotation=None),
         arg(arg='b', annotation=None)
     ], vararg=None, kwonlyargs=[], kw_defaults=[], kwarg=None, defaults=[]),
     body=[Return(value=BinOp(left=Name(id='a', ctx=Load()), op=Add(), right=Name(id='b', ctx=Load())))],
     decorator_list=[],
     returns=None
 ),
 Expr(value=Call(func=Name(id='print', ctx=Load()), args=[Call(func=Name(id='add_numbers', ctx=Load()), args=[Num(n=2), Num(n=3)], keywords=[])], keywords=[]))
 ])

例如当"for i in test:"中,test后面的冒号如果被写为其他符号,代码依旧不会被执行。

  1. 编译(Compilation)

在生成AST之后,解释器将使用它来生成字节码(Bytecode)。字节码是一种类似于汇编语言的中间代码,是 Python 解释器的一种低级表示方式。Python 解释器使用了一个名为 compiler 的模块来完成这个任务。Python 解释器使用上面得到的AST生成以下字节码:

 1           0 LOAD_CONST               0 (<code object add_numbers at 0x10d0db7c0, file "<stdin>", line 1>)
             2 LOAD_CONST               1 ('add_numbers')
             4 MAKE_FUNCTION            0
             6 STORE_NAME               0 (add_numbers)
 ​
 3           8 LOAD_NAME                1 (add_numbers)
            10 LOAD_CONST               2 (2)
            12 LOAD_CONST               3 (3)
            14 CALL_FUNCTION            2
            16 CALL_FUNCTION            1
            18 POP_TOP
            20 LOAD_CONST               4 (None)
            22 RETURN_VALUE

生成.pyc文件(字节码),简单来说就是在编译代码的过程中,首先会将代码中的函数、类等对象分类处理,然后生成字节码文件。字节码在Python虚拟机程序里对应的是PyCodeObject对象。.pyc文件是字节码在磁盘上的表现形式。

Python中有一个内置函数compile(),可以将源文件编译成codeobject,首先看这个函数的说明:

compile(…) compile(source, filename, mode[, flags[, dont_inherit]]) -> code object

参数1:源文件的内容字符串

参数2:源文件名称

参数3:exec-编译module,single-编译一个声明,eval-编译一个表达式 一般使用前三个参数就够了

  1. 执行(Execution)

最后,Python 解释器将执行字节码,并将其转换成机器码来完成具体的操作。执行过程是由解释器来完成的,解释器会根据字节码中存储的指令,按照一定的顺序来执行代码。Python 解释器按照上面的指令逐个执行字节码,最终输出 5

有了字节码文件,CPU可以直接识别字节码文件进行处理,接着Python就可执行了。

pyc文件

深度解密 Python 的字节码 (360doc.com)

257491068_4_20221214083632867_wm

Python 编译器负责将 Python 源代码编译成 PyCodeObject 对象,并且该对象还会被保存到 pyc 文件中,然后交给 Python 虚拟机来执行。

pyc文件,是python编译后的字节码(bytecode)文件。只要你运行了py文件,python编译器就会自动生成一个对应的pyc字节码文件。这个pyc字节码文件,经过python解释器,会生成机器码运行(这也是为什么pyc文件可以跨平台部署,类似于java的跨平台,java中JVM运行的字节码文件)。下次调用直接调用pyc,而不调用py文件。直到你这个py文件有改变。python解释器会检查pyc文件中的生成时间,对比py文件的修改时间,如果py更新,那么就生成新的pyc。

pyc文件生成时机:

在你 import 别的 py 文件时,那个 py 文件会被存一份 pyc 加速下次装载。而主文件因为只需要装载一次就没有存 pyc,你可以写两个 a.py 和 b.py,一个 import 另一个试试看。在你 import 别的 py 文件时,那个 py 文件会被存一份 pyc 加速下次装载。而主文件因为只需要装载一次就没有存 pyc,你可以写两个 a.py 和 b.py,一个 import 另一个试试看。

我们可以看到a.py 是被引用的文件,所以它会在__pycache__下生成a.py对应的pyc文件,若下次执行脚本时,若解释器发现你的 *.py 脚本没有变更,便会跳过编译一步,直接运行保存在 __pycache__ 目录下的 *.pyc 文件

特性

Python 是一种动态语言,它的特点是代码的执行过程中能够进行大量的动态操作。在 Python 编译过程中,生成字节码的过程和执行字节码的过程是同时进行的,这意味着 Python 解释器在执行代码时可以根据实际情况来进行优化,提高程序的性能。

例如,在运行时,Python 解释器会使用一些高级的优化技术,例如 JIT(Just-In-Time)编译、动态类型推断等,来提高代码的执行效率。这些优化技术在编译期间是不可用的,因为 Python 中很多类型和属性是在运行时才能确定的。

  1. 每次运行都要进行转换成字节码,然后再有虚拟机把字节码转换成机器语言,最后才能在硬件上运行。与编译性语言相比,每次多出了编译和链接的过程,性能肯定会受到影响;而Python并不是每次都需要转换字节码,解释器在转换之前会判断代码文件的修改时间是否与上一次转换后的字节码pyc文件的修改时间一致,若不一致才会重新转换。
  2. 由于不用关心程序的编译和库的链接等问题,开发的工作更加轻松。
  3. Python代码与机器底层更远了,Python程序更加易于移植,基本上无需改动就能在多平台上运行。