Python程序执行过程与字节码

凡事预则立,不预则废。

—— 《礼记·中庸》

我们每天都要编写一些 Python 程序,或者用来处理一些文本,或者是做一些系统管理工作。程序写好后,只需敲下 python 命令,便可将程序启动起来并开始执行:

1
$ python some-program.py

那么,一个文本形式的 .py 文件,是如何一步步转换为能够被 CPU 执行的机器指令的呢?此外,程序执行过程中可能会有 .pyc 文件生成,这些文件又有什么作用呢?带着这些问题我们开始本节的探索。

Python程序执行过程

你也许听过这样的说法: Python 是一种解释性语言。这意味着 Python 程序不用编译,只需要用一个解释器来执行。事实真的是这样吗?

虽然从行为上看 Python 更像 Shell 脚本这样的解释性语言,但实际上 Python 程序执行原理本质上跟 Java 或者 C# 一样,都可以归纳为 虚拟机 和 字节码 。 Python 执行程序分为两步:先将程序代码编译成字节码,然后启动虚拟机执行字节码:

虽然 python 命令也叫做 Python 解释器 ( Interpreter ),但跟其他脚本语言解释器有本质区别。实际上, Python 解释器包含 编译器 以及 虚拟机 两部分。当 Python 解释器启动后,主要执行以下两个步骤:

  1. 编译器 将 .py 文件中的 Python 源码编译成 字节码 ;
  2. 虚拟机 逐行执行编译器生成的 字节码 ;

因此, .py 文件中的 Python 语句并没有直接转换成机器指令,而是转换成 Python 字节码 。

字节码

好了,我们知道 Python 程序的 编译结果 是字节码,里面应该藏着不少 Python 运行的秘密。因此,不管是为了更深入理解 Python 虚拟机运行机制,还是为了调优 Python 程序运行效率,字节码都是绕不过去的一关。那么, Python 字节码到底长啥样呢?我们如何才能获得一个 Python 程序的字节码呢?

为了回答以上问题,我们需要深入 Python 解释器源码,研究 Python 编译器 。但出于几方面考虑,我不打算深入介绍 Python 编译器:① Python 编译器工作原理与其他任何语言类似,市面上任何一本编译原理均有介绍;② 编译原理是计算机基础学科,不是 Python 特有的,不在本专栏的篇幅内;③ 能够影响 Python 编译过程的手段非常有限,研究 Python 编译器对开发工作帮助不大。因此,我们只需要知道 Python 解释器背后有一个编译器负责将源码编译成字节码即可, 字节码以及虚拟机才是我们重点研究的对象 。

那我们还怎么研究字节码呀?别急, Python 提供了一个内置函数 compile 用于即时编译源码。我们只需将待编译源码作为参数调用 compile 函数,即可获得源码的编译结果。

源码编译

接下来,我们调用 compile 函数编译一个例子程序,以此演示该函数的用法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
PI = 3.14

def circle_area(r):
    return PI * r ** 2

class Dog(object):

    def __init__(self, name):
        self.name = name

    def yelp(self):
        print('woof, i am', self.name)

假设这段源码保存于 demo.py 文件,开始编译之前需要将源码从文件中读取出来:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
>>> text = open('demo.py').read()
>>> print(text)
PI = 3.14

def circle_area(r):
    return PI * r ** 2

class Dog(object):

    def __init__(self, name):
        self.name = name

    def yelp(self):
        print('woof, i am', self.name)

接着,调用 compile 函数编译源码:

1
>>> result = compile(text, 'demo.py', 'exec')

compile 函数必填的参数有 3 个:

  • source ,待编译 源码 ;
  • filename ,源码所在 文件名 ;
  • mode , 编译模式 , exec 表示将源码当做一个模块来编译;

顺便提一下, compile 函数有 3 种不同的 编译模式 可供选择:

  • exec ,用于编译模块源码;
  • single ,用于编译一个单独的 Python 语句(交互式下);
  • eval ,用于编译一个 eval 表达式;

compile 详细用法请参考 Python 文档,运行 help 内建函数可快速查看:

1
>>> help(compile)

我们接着看源码编译结果到底是个什么东西:

1
2
3
4
>>> result
<code object <module> at 0x103d21150, file "demo.py", line 1>
>>> result.__class__
<class 'code'>

看上去我们得到了一个 代码对象 ,代码对象有什么特别的呢?字节码又是藏身何处呢?想要获取更多详情,请点击 阅读原文

【Python源码剖析】系列文章首发于公众号【小菜学编程】,敬请关注:

【Python源码剖析】系列文章首发于公众号【小菜学编程】,敬请关注: