源码编译
要看懂源码,肯定要学会编译源码。先去官网下载源码文件,然后解压编译安装。linux环境下进解压目录依次运行:
- ./configure -prefix=<安装路径>
- make
- make install
至此一个可运行python就已经编译完成,之后便可以愉快地改源码了。
python运行流程
当我们运行python命令时,代码流向是怎样的呢?makefile中target指向Modules文件夹的python.c,
执行里面的Py_Main方法,Py_Main方法实现在main.c,主要是判断命令参数执行不同操作,整个方法可以简化成初始化环境、执行、清理环境:
Py_Initialize();
sts = PyRun_AnyFileExFlags(
fp,
filename == NULL ? "" : filename,
filename != NULL, &cf) != 0;
}
Py_Finalize();
python有两种运行模式,命令行模式和文件执行,最终都会调用PyRun_AnyFileFlags方法,参数里面的filename就对应着运行python abc.py这种模式。
PyRun_AnyFileFlags在Python目录的pythonrun.c:
int
PyRun_AnyFileExFlags(FILE *fp, const char *filename, int closeit,
PyCompilerFlags *flags)
{
if (filename == NULL)
filename = "???";
if (Py_FdIsInteractive(fp, filename)) {
int err = PyRun_InteractiveLoopFlags(fp, filename, flags);
if (closeit)
fclose(fp);
return err;
}
else
return PyRun_SimpleFileExFlags(fp, filename, closeit, flags);
}
PyRun_InteractiveLoopFlags和PyRun_SimpleFileExFlags分别对应交互式命令行执行和文件执行,但两者最终执行流程会汇合在一起。
我们先来看下Py_Initialize方法,初始化环境初始化了些什么。
python运行环境初始化
Py_Initialize也在pythonrun.c里,实际调用了Py_InitializeEx方法,初始化顺序依次如下:
- hash算法 随机数初始化
_PyRandom_Init();
这里只是初始化hash算法需要用到的随机数环境,使得 dict 对象中 key 的顺序每次启动时随机;而python的random模块在Lib文件夹的random.py,random.py内部引用的是C生成python模块,位于Modules的_randommodule.c,其中一个随机数基础方法random_random挺有意思,对应random(),把注释也贴出来:
/* random_random is the function named genrand_res53 in the original code;
* generates a random number on [0,1) with 53-bit resolution; note that
* 9007199254740992 == 2**53; I assume they're spelling "/2**53" as
* multiply-by-reciprocal in the (likely vain) hope that the compiler will
* optimize the division away at compile-time. 67108864 is 2**26. In
* effect, a contains 27 random bits shifted left 26, and b fills in the
* lower 26 bits of the 53-bit numerator.
* The orginal code credited Isaku Wada for this algorithm, 2002/01/09.
*/
static PyObject *
random_random(RandomObject *self)
{
unsigned long a=genrand_int32(self)>>5, b=genrand_int32(self)>>6;
return PyFloat_FromDouble((a*67108864.0+b)*(1.0/9007199254740992.0));
}
PyFloat_FromDouble表示从double类型转换成PyFloat对象,其中genrand_int32方法也是这种魔数风格,感觉很geek。
- 进程环境初始化
interp = PyInterpreterState_New();
这里只是初始化一个进程状态链表,进程实现在Lib文件夹的multiprocessing和Modules文件夹的_multiprocessing,底层使用的是系统的进程,根据系统不同实现不同
- 线程环境初始化
tstate = PyThreadState_New(interp);
这里只是初始化一个线程状态链表,线程实现在Modules的threadmodule.c,底层使用的是系统的线程,根据系统不同实现不同
- 类型初始化
_Py_ReadyTypes();
里面关键的方法PyType_Ready实现在Objects的typeobject.c,根据类型不同进行了初始化
- int、long等对象初始化
_PyFrame_Init()
_PyInt_Init()
_PyLong_Init()
PyByteArray_Init()
_PyFloat_Init()
_PyUnicode_Init()
除了_PyFrame_Init代表builtin_object,其他方法看名字应该就清楚其用途
- __builtin__ module初始化
bimod = _PyBuiltin_Init();
点进去就会发现都是些熟悉的面孔
- sys module初始化
sysmod = _PySys_Init();
- 初始化import环境
_PyImport_Init();
- 初始化内建异常
_PyExc_Init();
- 在sys module中添加一些对象用于import
_PyImportHooks_Init();
- 初始化信号机制
initsigs();
- 初始化warning机制
_PyWarnings_Init();
- 初始化main
initmain();
- 这个应该和GIL有关,具体要看下GIL
_PyGILState_Init(interp, tstate);
- site module初始化
initsite();
就是import Lib文件夹里site.py,目的是加载site-packages里的包,不同平台情况不同
字节码运行
初始完环境,就可以开始执行自己写的代码了。之前提到PyRun_InteractiveLoopFlags和PyRun_SimpleFileExFlags两者最终执行流程会汇合在一起,这里把两者代码简化一下:
PyRun_InteractiveLoopFlags
for (;;) {
ret = PyRun_InteractiveOneFlags(fp, filename, flags);
_PyDebug_PrintTotalRefs();
if (ret == E_EOF)
return 0;
}
内部PyRun_InteractiveOneFlags方法
arena = PyArena_New();
mod = PyParser_ASTFromFile(fp, filename,
Py_single_input, ps1, ps2,
flags, &errcode, arena);
m = PyImport_AddModule("__main__");
d = PyModule_GetDict(m);
v = run_mod(mod, filename, d, d, flags, arena);
PyArena_Free(arena);
PyRun_SimpleFileExFlags
m = PyImport_AddModule("__main__");
d = PyModule_GetDict(m);
if (maybe_pyc_file(fp, filename, ext, closeit)) {
v = run_pyc_file(fp, filename, d, d, flags);
} else {
v = PyRun_FileExFlags(fp, filename, Py_file_input, d, d,
closeit, flags);
}
内部run_pyc_file方法
magic = PyMarshal_ReadLongFromFile(fp);
if (magic != PyImport_GetMagicNumber()) {
PyErr_SetString(PyExc_RuntimeError,
"Bad magic number in .pyc file");
return NULL;
}
(void) PyMarshal_ReadLongFromFile(fp);
v = PyMarshal_ReadLastObjectFromFile(fp);
co = (PyCodeObject *)v;
v = PyEval_EvalCode(co, globals, locals);
内部PyRun_FileExFlags方法
PyArena *arena = PyArena_New();
mod = PyParser_ASTFromFile(fp, filename, start, 0, 0,
flags, NULL, arena);
ret = run_mod(mod, filename, globals, locals, flags, arena);
PyArena_Free(arena);
看了代码,可以发现只有两种模式:根据pyc文件执行和调用run_mod方法。pyc概念就不多说了,pyc文件开头会记录magic数,用于兼容python版本问题;而调用run_mod之前的arena是python的内存管理单位,run_mod的代码如下:
co = PyAST_Compile(mod, filename, flags, arena);
v = PyEval_EvalCode(co, globals, locals);
这样所有的执行都汇合到了一起,调用PyEval_EvalCode方法。PyEval_EvalCode接收一个PyCodeObject对象,PyCodeObject对象是python代码编译结果,由此可见pyc文件其实是个PyCodeObject缓存,pyc从名字看也可以看出是简称嘛。PyEval_EvalCode内部调用了PyEval_EvalFrameEx方法,PyEval_EvalFrameEx就是最后的大魔王了。PyEval_EvalFrameEx是个很庞大的方法,还是先从PyCodeObject入手,使用dis模块就可以知道PyCodeObject的结构了,比如print('hello,world')的输出如下:
('const : ', ('hello,world', None))
('name : ', ())
2 0 LOAD_CONST 0 ('hello,world')
3 PRINT_ITEM
4 PRINT_NEWLINE
5 LOAD_CONST 1 (None)
8 RETURN_VALUE
第一个是静态变量,第二个是变量,第三个是字节码,这个字节码就是执行的核心。PyEval_EvalFrameEx中有个大switch块,里面的case条件就对应着字节码(所有的字节码在这个篇文章可以看到)。
最后的Py_Finalize方法可以看成是Py_Initialize反过程。
本文到这里就结束了,python源码内容丰富庞大,这篇文章只是浅显地过了一下,目的是了解代码执行原理、了解整个脉络,具体细节可以参考《Python源码剖析》这本书。