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
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.参考资料:
- Inside The Python Virtual Machine
- 海纳.自己动手写python虚拟机.北京航空航天大学出版社
- Reading pyc file
- python文档
[注]
- 本文用的python均为Cpython ↩︎
- 比如像shell script,后面的错误确实不会影响前面代码的执行 ↩︎
- 什么时候生成__pycache文件有一定的规则,这里不赘述 ↩︎
- 准确来说,pyc文件是字节码对象在磁盘中持久化的结果 ↩︎
- 由于是16进制,所以两位就是2进制的8位,也就是一个字节 ↩︎
- 实际上pyc文件中有很多内容,这里为简单起见只查看了指令相关的内容 ↩︎