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文件中有很多内容,这里为简单起见只查看了指令相关的内容 ↩︎ |
|
来自: flyk0tcfb46p9f > 《AI》