Python是由3个主要部分组成,运行语句的解释器、将源代码编译成中间字节码的编译器和运行.pyc文件的python虚拟机PVM组成。
当python脚本运行时,在代码开始运行处理之前,python会执行一些步骤,第一是编译成所谓的字节码,第二是将其转发到所谓的虚拟机中。
程序执行时,pyhton通过把每一条源语句分解为单一步骤来将这些源语句翻译成一组字节码指令,编译只是一个简单的翻译步骤,这些字节码指令是源代码底层的与平台无关的表现形式。如果python进程在机器上有写入权限,那么它将把程序的字节码保存为一个.pyc为扩展名的文件中(经过编译的.py源代码)。Python这样做的目的是作为一种启动速度的优化,下一次运行程序的时候,如果在上次保存字节码之后没有修改过源代码的话,python将会加载.pyc文件并跳过编译这个步骤。Python会自动检查源文件和字节码文件的时间戳,如果又重新保存了源代码,则字节码将自动重新创建。
一旦程序被编译成字节码,之后字节码被发送到称为PVM的python虚拟机上来执行。PVM是python的运行引擎,它时常表现为python系统的一部分,是实际运行脚本的组件。实际上,PVM就是迭代运行字节码指令的一个大循环,一个接一个地完成操作。
Python 的虚拟机是Python 的核心,在.py 源代码被编译器编译为字节码指令序列之后,就将由Python 的虚拟机接手整个工作。Python 的虚拟机会从编译得到的PyCodeObject对象(Python代码的编译结果就是PyCodeObject对象)中依次读入每一条字节码指令,并在当前的上下文环境中执行这条字节码指令。如此反复运行,所有由Python 源代码所规定的动作都会如期望一样,一一展开。
字节码在Python虚拟机程序里对应的是PyCodeObject对象。.pyc文件是字节码在磁盘上的表现形式。Python 源代码经过编译之后,所有的字节码指令以及程序的其他静态信息都存放在PyCodeObject 对象中。
typedef struct {
PyObject_HEAD
int co_argcount; /* 位置参数个数*/
int co_nlocals; /* 局部变量个数*/
int co_stacksize; /* 栈大小*/
int co_flags;
PyObject *co_code; /* 字节码指令序列*/
PyObject *co_consts; /* 所有常量集合*/
PyObject *co_names; /* 所有符号名称集合*/
PyObject *co_varnames; /* 局部变量名称集合*/
PyObject *co_freevars; /* 闭包用的的变量名集合*/
PyObject *co_cellvars; /* 内部嵌套函数引用的变量名集合*/
/* The rest doesn’t count for hash/cmp */
PyObject *co_filename; /* 代码所在文件名*/
PyObject *co_name; /* 模块名|函数名|类名*/
int co_firstlineno; /* 代码块在文件中的起始行号*/
PyObject *co_lnotab; /* 字节码指令和行号的对应关系*/
void *co_zombieframe; /* for optimization only (see frameobject.c) */
} PyCodeObject;
PyCodeObject对象的创建时机是模块加载的时候。
*.py文件在作为模块导入的时候,运行的时候会被编译成.pyc文件。如下执行mytest.py文件会对mytest.py进行编译成字节码并解释执行,但是不会生成mytest.pyc。但如果mytest.py加载了其他模块,如import otherfile,Python会对otherfile.py进行编译成字节码,生成otherfile.pyc,然后对字节码解释执行。
[root@localhost newtest]# ll
total 28
-rw-r--r-- 1 root root 20 May 31 11:39 a.py
-rw-r--r-- 1 root root 446 May 31 11:39 mytest.py #该文件中导入a.py
[root@localhost newtest]# pythontest.py
[root@localhost newtest]# ll
total 32
-rw-r--r-- 1 root root 20 May 31 11:39 a.py
-rw-r--r-- 1 root root 121 May 31 11:40 a.pyc
-rw-r--r-- 1 root root 446 May 31 11:39 test.py
[root@localhost newtest]#
如果想生成mytest.pyc,我们可以使用Python内置模块py_compile来编译(Python代码的编译结果就是PyCodeObject对象)。加载模块时,如果同时存在.py和.pyc,Python会尝试使用.pyc,如果.pyc的编译时间早于.py的修改时间,则重新编译.py并更新.pyc。如
[root@localhostnewtest]# python -m py_compile mytest.py # -m py_compile相当于import py_compile
[root@localhostnewtest]# ll
total32
-rw-r--r-- 1 root root 20 May 31 11:39 a.py
-rw-r--r-- 1 root root 121 May 31 11:54 a.pyc
-rw-r--r-- 1 root root 446 May 31 11:39 mytest.py
-rw-r--r-- 1 root root 587 May 31 11:56 mytest.pyc
[root@localhost newtest]# pythonmytest.pyc
in module A
[root@localhostnewtest]#
编译源代码有以下作用:
1、源代码保护(算法保护)/ 防止用户篡改源代码
2、解释器加载代码速度加快
如果用python3执行该脚本,还会在当前目录下生成一个__pycache__文件夹。
To speed up loading modules, Python caches the
compiled version of each module in the __pycache__ directory under
the name module.version.pyc, where the version encodes the format of the
compiled file; it generally contains the Python version number. For example, in
CPython release 3.7the compiled version of spam.py would be cached
as __pycache__/spam.cpython-37.pyc.
[root@localhost newtest]# cd__pycache__
[root@localhost __pycache__]# ll
total 4
-rw-r--r-- 1 root root 125 May 3112:55 a.cpython-37.pyc
python程序的运行并不单单靠PyCodeObject对象维护的静态环境,也需要一些关于程序运行的其他动态信息执行环境。如下一段python代码
#[environment.py]
i ='Python'
deff():
i = 999
print i #1
f()
print i #2
在上面代码中的1 和2 两个地方,都进行了同样的动作,即print i。显然,它们所对应的字节码指令肯定是相同的,但是这两条语句的执行效果是不同的。这样的结果正是在执行环境的影响下产生的。在执行“1”处的print 时,执行环境中,i 的值为999;而在执行2 处的print 时,执行环境中i 的值为“Python”。像这种同样的符号在程序运行的不同时刻对应不同的值,甚至不同类型的情况,必须在运行时动态地被捕捉和维护。这些信息是不可能在PyCodeObject 对象中被静态地存储的。
我们都知道C语言使用栈帧保存函数中的变量信息,我们可以用这样的机理来定性地解释environment.py 的执行过程。Python 的虚拟机实际上是在模拟操作系统运行可执行文件的过程,当Python 开始执行environment.py 中的第一条表达式时,Python 已经建立起了一个执行环境A,所有的字节码指令都会在这个执行环境中执行。Python 可以从这个执行环境中获取变量的值,也可以根据字节码指令的指示修改执行环境中某个变量的值,以影响后续的字节码指令。这样的过程会一直持续下去,直到发生了函数的调用行为。
当Python 在执行环境A 中执行调用函数f 的字节码指令时,会在当前的执行环境A之外重新创建一个新的执行环境B,在这个新的执行环境B 中,有一个新的名字为“i”的对象。这个新的执行环境B 实际上是一个新的栈帧。而Python 正是在其虚拟机中通过不同的实现方式模拟了这一原理,从而完成了Python 字节码指令序列的执行。
当发生函数调用时,创建新的栈帧,对应Python的实现就是PyFrameObject对象。Python 源码中对PyFrame-Object的定义:
typedef struct _frame {
PyObject_VAR_HEAD
struct _frame *f_back; //执行环境链上的前一个frame
PyCodeObject *f_code;
//PyCodeObject 对象
PyObject *f_builtins; //builtin 名字空间
PyObject *f_globals; //global 名字空间
PyObject *f_locals; //local 名字空间
PyObject **f_valuestack; //运行时栈的栈底位置
PyObject **f_stacktop; //运行时栈的栈顶位置
……
int f_lasti; //上一条字节码指令在f_code 中的偏移位置
int f_lineno; //当前字节码对应的源代码行
……
//动态内存,维护(局部变量+cell 对象集合+free
对象集合+运行时栈)所需要的空间
PyObject *f_localsplus[1];
} PyFrameObject;
从f_back 我们可以看出一点,在Python 实际的执行中,会产生很多PyFrameObject对象,而这些对象会被链接起来,形成一条执行环境链表。这正是对x86 机器上栈帧间关系的模拟。
在f_code 中存放的是一个待执行的PyCodeObject对象,而接下来的f_builtins、f_globals、f_locals 是3 个独立的名字空间,在这里我们看到了名字空间和执行环境之间的关系。名字空间实际上是维护着变量名和变量值之间关系的PyDictObject对象,所以,在这3 个PyDictObject 中,分别维护了builtin 的name、global 的name,以及local 的name 与对应值之间的映射关系。
尽管PyFrameObject对象是一个用于Python虚拟机实现的极为隐秘的内部对象,但是Python还是提供了某种途径可以访问到PyFrameObject对象。在Python中,有一种frame
object,它是对C一级的PyFrameObject的包装。非常幸运的是,Python提供的一个方法能方便地获得当前处于活动状态的frame
object。这个方法就是sys模块中的_getframe方法。
下面的caller.py演示了如何利用获得当前活动的frame
object,进而获取调用当前函数的函数的信息:
importsys
value= 3
defg():
frame = sys._getframe()
print 'current function is : ',frame.f_code.co_name
caller = frame.f_back
print 'caller function is : ',caller.f_code.co_name
print "caller's local namespace :", caller.f_locals
print "caller's global namespace :",
print caller.f_globals.keys()
deff():
a = 1
b = 2
g()
defshow():
f()
>>>show()
currentfunction is : g
callerfunction is : f
caller'slocal namespace : {'a': 1, 'b': 2}
caller'sglobal namespace : ['g', 'f','__builtins__', 'show', 'value', 'sys', '__name__', '__doc__']
>>>
从执行的结果可以看到,从函数f 中我们完全获得了其调用者——函数g 的一切信息,甚至包括函数g 的各个名字空间。
所以说在Python 真正执行的时候,它的虚拟机实际上面对的并不是一个PyCodeObject对象,而是另一个对象——PyFrameObject。它就是我们所说的执行环境。
网友评论