分享

UC头条:编译型or解释型? Python运行机制浅析

 flyk0tcfb46p9f 2020-04-18

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

1.举个栗子

首先看一个简单的例子:

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

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

a+b =3 Traceback (most recent call last): File '/demo.py', line 4,inc=NotDefinedValue

NameError: name 'NotDefinedValue'isnot defined

Process finished with exit code 1

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

#!/usr/bin/python3# file name :demo2.py a=1 b=2print('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.pyimport dis import marshal import sys defshow_file(fname:str)- >None:withopen(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

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.参考资料:

Inside The Python Virtual Machine

海纳.自己动手写python虚拟机.北京航空航天大学出版社

Reading pyc file

python文档

[注]

本文用的python均为Cpython ↩︎

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

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

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

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

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

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多