编译型or解释型?Python运行机制浅析

Python语言通常被看作是解释型语言,不同于像C语言那样的编译型。但实际上,如果说Python是编译型语言,也未尝不可。我们来一起看一下1

1.举个栗子

首先看一个简单的例子:

#!/usr/bin/python3
# file name :demo1.py
a=1
b=2
print("a+b = ",a+b)
c=NotDefinedValue
print(c)

这里第四行有个赋值的错误,但python在运行前不会进行类型检查,所以该程序仍可正常运行,直至遇到错误,运行结果与预想的一致:

a+b =  3
Traceback (most recent call last):
  File "/demo.py", line 4, in <module>
    c=NotDefinedValue
NameError: name 'NotDefinedValue' is not defined

Process finished with exit code 1

现在稍微改动一下,使最后一行有个语法错误(少个括号):

#!/usr/bin/python3
# file name :demo2.py
a=1
b=2
print("a+b = ",a+b)
c=NotDefinedValue
print(c

按照对python语言的理解,程序应该会逐行执行,直至遇到第一个赋值语句的错误,然后抛出异常。执行结果应该和上面的例子一样。是不是这样呢,我们试着执行,结果如下:

  File "/demo.py", line 6
SyntaxError: unexpected EOF while parsing

可见没有像预想的一样,而是直接抛出语法错误。

那么问题来了,前三行代码没错误,为什么不能正常执行呢?python作为解释性语言,应该是“一边执行一边转换”的,后面的“错误”按理说不会影响前面正确的代码的啊?2 那可能有同学要说了,python在运行之前会检查语法!但“检查语法”是个怎样的过程呢?要知道答案,需要了解python底层的运作。

2. python运行机制

我们都知道python “解释器”(interpreter)这个东西,就是负责执行python源码的,大体的过程是这样:
在这里插入图片描述
对于解释器内部,可以分成两部分:编译器(compiler)和虚拟机(virtual machine),编译器负责将源码编译成字节码(byte code),字节码交给虚拟机运行,虚拟机会调用CPU内存等硬件资源,进行计算,最后产生结果。

在这里插入图片描述
可见编译器做了很多事情:生成语法树(parse tree generation),生成AST(Abstract Syntax Tree),字节码生成与优化,最后产生字节码对象。字节码对象交给虚拟机后,虚拟机会读取其中的指令,逐步执行。了解java的同学会发现,这个过程与java很类似,java源码也是先编译成字节码(.class文件),后由JVM执行。

这里能看到,语法分析在编译阶段就会进行,此时若有语法错误,则编译不通过,抛出错误;既然没有编译产生的字节码,虚拟机自然也就不会执行指令,也就不会有输出结果。 所以上面第一个例子是可以正常编译的,生成字节码并交给虚拟机,虚拟机执行指令时会检查类型,正确的指令会执行,不对的就报错;对于第二个例子,编译器在分析语法的时候就发现错误,停止编译。所以两个程序中断的原因有着本质不同。

3. *.pyc文件

看到python也有“字节码”,可能又有同学有问题了:Java的字节码存在于*.class文件中,那pyhton的字节码在哪呢?答案就是 *.pyc文件。

我们有时候会在python源码的文件夹中发现__pycache__文件夹3,里面就有*.pyc文件,这可能是自动生成的。我们也可以手动编译源码,生成*.pyc:

python -m py_compile filename.py

我们先对第一个例子进行编译:

python -m py_compile demo1.py

编译通过,并在该文件目录下有个__pycache__文件夹,进入会发现demo1.cpython-37.pyc文件,这就是字节码文件 4。这是供机器读的二进制文件(虽然这里是虚拟机),可以用hexdump(在Linux环境下)打开,结果以16进制显示:
在这里插入图片描述

emm,虽然看上去很复杂,实际上确实很复杂。不过没关系,可以尝试着解读一下。字节码的前两个4字节是魔术数,是有关于版本号的,第三个4字节是时间戳,第四个4字节是源文件大小。在本例中,前两个字节是420d0d0a000000005,略过;第三个4字节是d75a915e,因为是小端模式,实际是5e915ad7,转换成十进制就是1586584279,这就很眼熟了,就是UNIX时间戳(时间为2020/4/11 13:51:19);接着后面的4字节是37000000,实际是00000037,十进制就是55,也就是说源文件大小为55字节。通过查看文件属性,也确实如此。

至于后面的部分,我们可以通过python的dis工具来查看:

#/usr/bin/python3
#file name:read_file.py
import dis
import marshal
import sys


def show_file(fname: str) -> None:
    with open(fname, 'rb') as f:
        f.read(16)  # pop the first 16 bytes
        dis.disassemble(marshal.loads(f.read()))


if __name__ == '__main__':
    show_file(sys.argv[1])

这段代码我们只输出字节码中的指令,其余部分略过。运行,传入pyc文件,结果如下:

$ python3 read_file.py ./demo1.cpython-37.pyc 
  1           0 LOAD_CONST               0 (1)
              2 STORE_NAME               0 (a)
  2           4 LOAD_CONST               1 (2)
              6 STORE_NAME               1 (b)
  3           8 LOAD_NAME                2 (print)
             10 LOAD_CONST               2 ('a+b = ')
             12 LOAD_NAME                0 (a)
             14 LOAD_NAME                1 (b)
             16 BINARY_ADD
             18 CALL_FUNCTION            2
             20 POP_TOP
  4          22 LOAD_NAME                3 (NotDefinedValue)
             24 STORE_NAME               4 (c)
  5          26 LOAD_NAME                2 (print)
             28 LOAD_NAME                4 (c)
             30 CALL_FUNCTION            1
             32 POP_TOP
             34 LOAD_CONST               3 (None)
             36 RETURN_VALUE

看起来有点像汇编?那就对了,因为dis模块就是反汇编(disassemble),将(虚拟机的)机器码反汇编成汇编。这里展示的是指令部分6,能看到源代码的变量,值,函数等在栈上的压入与弹出。具体每个指令什么意思这里就不再展开。

.pyc文件是交给虚拟机执行的,所以我们可以运行pyc文件,就像运行普通py文件一样:

$ python3 demo1.cpython-37.pyc 
a+b =  3
Traceback (most recent call last):
  File "demo1.py", line 4, in <module>
NameError: name 'NotDefinedValue' is not defined

没问题,跟第一个例子运行结果完全一样。
至于第二个例子,编译时就会出错:

$ python3 -m py_compile demo2.py 
  File "demo1.py", line 5
    print(c
          ^
SyntaxError: unexpected EOF while parsing

4.小结

通过对python运行机制的简单探讨,可以发现python其实并不是严格意义上的解释型语言。实际上,解释型与编译型本身就没有严格的定义,现在很多语言也在模糊这两者的界限。

我们也没必要纠结于具体是哪种类型的语言,这根本不重要。了解语言背后的机制,知道从输入到输出中间发生了什么,这才是更有意义的。

5.参考资料:

  1. Inside The Python Virtual Machine
  2. 海纳.自己动手写python虚拟机.北京航空航天大学出版社
  3. Reading pyc file
  4. python文档

[注]


  1. 本文用的python均为Cpython ↩︎

  2. 比如像shell script,后面的错误确实不会影响前面代码的执行 ↩︎

  3. 什么时候生成__pycache文件有一定的规则,这里不赘述 ↩︎

  4. 准确来说,pyc文件是字节码对象在磁盘中持久化的结果 ↩︎

  5. 由于是16进制,所以两位就是2进制的8位,也就是一个字节 ↩︎

  6. 实际上pyc文件中有很多内容,这里为简单起见只查看了指令相关的内容 ↩︎

发布了1 篇原创文章 · 获赞 3 · 访问量 98

猜你喜欢

转载自blog.csdn.net/weixin_42281402/article/details/105400111