重新认识 Python(7)CPython初探 CPython 是 Python 社区的标准,其他版本的 Python,比如 pypy,都会遵行 CPython 的标准API实现。想要更深入的认识 Python,就需要了解 CPython 实现。这一期,我就带大家认识一下 CPython 源代码,为日后改造 Python 做准备。首先,展示如何本地编译最新的 CPython代码。然后,展示如何 debug 编译好的解释器。最后我们会利用 CPython 实现简单的函数调用。
本文会为你揭开 CPython 的面纱,带你进入 C + Python 的世界。文章的最后,你也会对 Python 中最重要的概念:一切皆对象(object)有更深刻的认识;你还会发现一点点线索,为什么Python用起来比其他语言,比如 C 慢很多。
请打开编辑器和命令行,我们开始。(本文主要使用MacOS,Windows 不一定可以直接实现)。
当我们再说 Python,我们在说什么? 在我还是小小白的时候,我经常会想:当我们说 Python 的时候,我们到底在说什么?后来当我学了编译原理,我变成了一个小白,我知道了,当我们说 Python 的时候,我们其实再说两个东西:语言的语法和语言的运行时。
语法主要规定了如何表达程序,比如我们写 a = 1
是合法的,且有语义;而当我写 for i in 1:
的时候,是不合法的,语义也就无法确定。运行时(run time)是指运行我们写好的代码的另一个“程序”。也就是当我们输入 python test.py
的时候,这里面的 python 就是我们的“程序”,这个运行时负责执行我们的代码。
而这个 runtime,可以有很多不同的实现,比如 CPython 实现就是一个用 C 语言实现运行时,而 pypy 则是用 RPython 实现的运行时等等。这个 runtime 可以理解我们写的 python 代码,并且把它编译成 python 的字节码,然后执行。比如:
上面的代码在屏幕打印1。如何实现的呢?其实是我们的运行时“程序” python
实现的。首先,python 会把上面的源代码翻译成字节码:
1 2 3 4 5 6 7 8 9 10 11 12 13 import dis dis.dis("a=1;print(1)") # 这一行会把源代码编译成字节码,就是 python 认识的操作。 ## 结果 1 0 LOAD_CONST 0 (1) 2 STORE_NAME 0 (a) 4 LOAD_NAME 1 (print) 6 LOAD_NAME 0 (a) 8 CALL_FUNCTION 1 10 POP_TOP 12 LOAD_CONST 1 (None) 14 RETURN_VALUE
上面的字节码,背后就是 python 程序提供的一些基本操作。比如 LOAD_CONST
,加载常数 1 进入内存。你可以在这里 找到所有的字节码。
一句话总结,CPython
就是一个可以执行你写的python代码的另一个由 C 语言写的程序。
编译 CPython 首先,我们要去 github fork CPython 的代码到自己的账户下:https://github.com/python/cpython 。然后我们作如下操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 xcode-select --install brew install git brew install openssl brew install readline git clone git@github.com:{你的账户。。}/cpython.git cd cpythongit remote add upstream git@github.com:python/cpython.git PKG_CONFIG_PATH="$(brew --prefix openssl) /lib/pkgconfig" \ CPPFLAGS="-I$(brew --prefix readline) /include" \ LDFLAGS="-L$(brew --prefix readline) /lib" \ ./configure --with-pydebug \ && make
很快编译就会成功,此时你的当前目录下会出现一个 python.exe
的可执行文件,这就是我们编译好的 Python!
输入 ./python.exe
就会看见熟悉的python解释器了提示符了。
1 2 3 Python 3.10.0a6+ (heads/master:f00e82f8b8, Mar 13 2021, 23:26:48) [Clang 11.0.3 (clang-1103.0.32.59)] on darwin Type "help", "copyright", "credits" or "license" for more information. >>>
Debug CPython 编译完成后,我们就可以来看看 CPython 内部的“秘密”了。这里我用的 lldb
因为 Mac 的安装 gdb 比较复杂。基本步骤是一样的。
输入 lldb python.ext
。接下来,我们需要在 Programs/python.c
的 main 函数处设置一个断点。这个函数就整个解释器的入口。
1 2 3 4 5 6 7 8 9 10 11 12 13 (lldb) b main Breakpoint 1: where = python.exe`main + 22 at python.c:15:25, address = 0x0000000100000856 (lldb) r Process 43373 launched: '/Users/zhewang/Projects/cpython/python.exe' (x86_64) Process 43373 stopped * thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1 frame #0: 0x0000000100000856 python.exe`main(argc=1, argv=0x00007ffeefbff330) at python.c:15:25 12 int 13 main(int argc, char **argv) 14 { -> 15 return Py_BytesMain(argc, argv); 16 } 17 #endif
好,现在我们就停留在了 CPython 世界的入口了!
简单的加法! 现在我们在 debug 环境下,用 CPython 来实现一个整数的加法。
首先,我们来看一下 int 这个 object。我把部分 int object 的代码放在下面。可以看出,python 里面连最基本的 int 对象都别有洞天!要知道python运行的时候,每一个整数在内存里其实都包含了下面结构体所有的变量。具体文件可以在 Objects/longobject.c
找到。
PyVarObject_HEAD_INIT
定义了 int 的类型,int 的类型其实叫 PyLong_Type
,他的基类是一个叫 PyType_Type
的类型。比如在python中,type(1)
会返回 int
,即下面的 tp_name,而 type(type(1))
则返回 type
,即PyType_Type
。当然 python 的整数做的这么复杂主要是为了保持所有对象类型协调统一,即无论是复杂的类型还是简单的类型,他们的内存结构都是样的!( 这里插一嘴,python的int是long,也就是说可以是任意大的整数,不会溢出!)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 PyTypeObject PyLong_Type = { PyVarObject_HEAD_INIT(&PyType_Type, 0 ) "int" , offsetof(PyLongObject, ob_digit), sizeof (digit), 0 , 0 , 0 , 0 , 0 , long_to_decimal_string, &long_as_number, 0 , 0 , (hashfunc)long_hash, 0 , 0 , PyObject_GenericGetAttr, 0 , 0 , Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_LONG_SUBCLASS | _Py_TPFLAGS_MATCH_SELF, long_doc, 0 , 0 , long_richcompare, 0 , 0 , 0 , long_methods, 0 , long_getset, 0 , 0 , 0 , 0 , 0 , 0 , 0 , long_new, PyObject_Del, };
我们现在在debug 环境里面可以尝试创造一个 int 对象!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 (lldb) b pymain_import_readline Breakpoint 1: where = python.exe`pymain_import_readline + 12 at main.c:205:9, address = 0x000000010032ad1c (lldb) r Process 54697 launched: '/Users/zhewang/Projects/cpython/python.exe' (x86_64) Python 3.10.0a6+ (heads/master:f00e82f8b8, Mar 20 2021, 21:50:27) [Clang 11.0.3 (clang-1103.0.32.59)] on darwin Type "help", "copyright", "credits" or "license" for more information. Process 54697 stopped * thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1 frame #0: 0x000000010032ad1c python.exe`pymain_import_readline(config=0x00000001007503d0) at main.c:205:9 202 static void 203 pymain_import_readline(const PyConfig *config) 204 { -> 205 if (config->isolated) { 206 return; 207 } 208 if (!config->inspect && config_run_code(config))
我们停在这里,Python已经完成一些初始化设置,但是还没有进入 >>> 交互环境。这时,我们使用 CPython 的底层函数创建两个 int 对象。
1 2 3 4 (lldb) call PyLong_FromLong(10) (PyObject *) $0 = 0x00000001007a8500 (lldb) call PyLong_FromLong(20) (PyObject *) $1 = 0x00000001007a8780
可以看到,返回的是一个指向 PyObject
的指针。其实这个就是一切皆对象中的对象啦,就是这个 PyObject
,Python中所有类型实例化后都是这个这个 PyObject。来看一下这两对象的内存:
1 2 3 4 5 6 7 8 9 10 (lldb) p *$0 (PyObject) $2 = { ob_refcnt = 10 ob_type = 0x00000001004936f0 } (lldb) p *$1 (PyObject) $3 = { ob_refcnt = 8 ob_type = 0x00000001004936f0 }
可以看到,他们有两个field,一个是 refcnt 用来管理垃圾回收,另一个是 ob_type 指向对象类型的内存。通过访问 ob_type 可以看到该类型的内存:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 (lldb) p *$1->ob_type (PyTypeObject) $5 = { ob_base = { ob_base = { ob_refcnt = 68 ob_type = 0x000000010049a620 } ob_size = 0 } tp_name = 0x0000000100407b51 "int" tp_basicsize = 24 tp_itemsize = 4 tp_dealloc = 0x00000001001707c0 (python.exe`object_dealloc at typeobject.c:4062) tp_vectorcall_offset = 0 。。。省略
接下来我们看看我们的 PyLong_Type 支持哪些运算:
1 2 3 4 5 6 7 8 9 10 11 (lldb) p *PyLong_Type->tp_as_number (PyNumberMethods) $9 = { nb_add = 0x000000010011b180 (python.exe`long_add at longobject.c:3065) nb_subtract = 0x000000010011ae00 (python.exe`long_sub at longobject.c:3099) nb_multiply = 0x000000010011dac0 (python.exe`long_mul at longobject.c:3533) nb_remainder = 0x000000010011dda0 (python.exe`long_mod at longobject.c:3966) nb_divmod = 0x000000010011dee0 (python.exe`long_divmod at longobject.c:3982) nb_power = 0x000000010011e0b0 (python.exe`long_pow at longobject.c:4098) nb_negative = 0x000000010011aca0 (python.exe`long_neg at longobject.c:4318) nb_positive = 0x000000010011c570 (python.exe`long_long at longobject.c:4692) ...
我们来试一下加法!
1 2 (lldb) p *PyLong_Typp.tp_as_number->nb_add (PyObject *(*)(PyObject *, PyObject *)) $10 = 0x000000010011b180 (python.exe`long_add at longobject.c:3065)
可以看到,加法的函数签名是:(PyObject *(*)(PyObject *, PyObject *))
。
1 2 (lldb) p PyLong_Type.tp_as_number->nb_add((PyObject *) 0x00000001007a8500,(PyObject *) 0x00000001007a8780) (PyObject *) $15 = 0x00000001007a8a00
我们得到了另一个 PyObject 的指针,0x00000001007a8a00。这个理论上就是 10+20 的结果,30。我们来验证一下。
1 2 3 4 5 6 7 8 9 10 11 (lldb) p *(PyLongObject *) 0x00000001007a8a00 (PyLongObject) $30 = { ob_base = { ob_base = { ob_refcnt = 8 ob_type = 0x00000001004936f0 } ob_size = 1 } ob_digit = ([0] = 30) # <--- 这里就是我们的答案。。 }
总结 这一篇 CPython 初探就到这里,我们学会了如何编译、debug我们的Python解释器,用CPython的API实现了简答的加法。有了这些基础知识我们就可以开始进一步探索CPython的世界了!