利用一个简单的例子窥探CPython内核的运行机制


Posted in Python onMarch 30, 2015

我最近花了一些时间在探索CPython,并且我想要在这里分享我的一些冒险经历。Allison Kaptur的excellent guide to getting started with Python internals 有一点??拢?蚁胫鸩浇樯芪易约旱奶剿鞴?袒岣?佑刑趵硇裕?庋?残砥渌?闷娴?ython使用者可以跟着一起做。

1.注意到了一些奇怪的事情

一开始,我只是设置好Nose对一些我写的Python 3代码进行测试。当我运行这些测试的时候,我得到了一个不可思议的错误信息:”TypeError: bad argument type for built-in operation”,这是我之前在这个程序里没有见到过的。

最终造成这个错误的原因有一点显而易见——我不小心在程序里留了一个PDB断点(`import pdb; pdb.set_trace()`)。当我把它去掉后,测试正常运行了。

但是,我曾经使用Nose在Python 2 repos上进行测试,并且在那种情况下,错误留下的断点并不会导致Nose崩溃,而是看上去像是“挂起”了。程序并不是真的挂起了——它仅仅是不显示东西到stdout(标准输出)了。Nose是故意这样做的,而当我正在运行一套测试的时候这样做是有意义的。我可能仅仅是想看测试的结果,而不是一大堆程序自己打印出来的状态。如果你在这个脚本里面敲击“c”,Nose仅仅像通常那样经过这个断点。

通常情况下,我可能只是耸耸肩,移除掉这个断点,然后继续我的工作。但是!我在一个黑客学校并且有时间深入研究任何抓住我兴趣的东西,所以我决定利用这次机会去窥探一下Python的内核。

2.制造一个最简单的测试样例。

结果这次的问题研究起来有一点复杂——我并不能确定问题是在Nose,还是PDB或者CPython自己的代码里面。并且,我当然不能使用任何断点,因为这些断点会导致我的程序崩溃。

最终,在验证了一些假设后,看上去PDB对`input()`的调用导致了崩溃。所以:在Python2和Python3里面,input的实现有什么不同吗?或者是其他的某些东西不同?

我和Jesse一起进行调试,最后我们意识到Nose以一种有趣的方式处理标准输出:
 

self._buf = StringIO()
sys.stdout = self._buf

这里用sys.stout表示Python中的所有标准输出,即表示所有向终端输出的内容都会发送到这里。但由于我们可以像访问其他Python变量那样访问sys.stout,所以我们可以改变这个sys.stout。而Nose将sys.stoud设置为StringIO(),而这只是任意一个字符串。

如果你这么做,print函数就不会工作了!
 

import sys, io
sys.stdout = io.StringIO()
print(“Hello”)
# Oh no, nothing printed!

我们怀疑是否那一行就是问题所在,所以我们构造了一个简单的测试样例:
 

import sys, io
sys.stdout = io.StringIO()
print("Hello!") # Nothing will appear
input("Input: ") # Raises a TypeError

在Python 3 里面运行这个会出现我们之前看到过的”bad argument for built-in operation”。所以现在我们知道该调查哪里了!当你试图改变sys.stdout的时候,内建函数`input()`以一种奇怪的方式中断下来。

3.了解一点CPython!

所以我们想要看下‘input'是怎样实现的。Python有一个非常酷的模块叫做'inspect',能让你检查源代码,像这样:
 

>>> from collections import namedtuple
>>> import inspect; print(inspect.getsource(namedtuple))
def namedtuple(typename, field_names, verbose=False, rename=False):
"""Returns a new subclass of tuple with named fields.
.....

然而当你想要对'input'调用'inspect.getsource'的时候,结果会是:“TypeError: is not a module, class, method, function, traceback, frame, or code object.”这意味着我们的函数不是用Python实现的——它是用C语言实现的,因此'inspect;模块不能为我们显示它的代码。

……但是,利用cinspect模块的魔力,我们能查看C源代码!
 

>>> import cinspect; print(cinspect.getsource(input))
static PyObject *
builtin_input(PyObject *self, PyObject *args)
{
PyObject *line;
char *str;
.....

很好,现在我们知道我们想要找的函数叫做'builtin_input'。这时,我们将要开始浏览C代码了,而不仅仅是Python代码,我们将要在中端调试而不是在Python的解释器里。你不需要一定是一个C语言专家才能看明白接下来的东西——我大多数时候会以根据函数名称进行推测的方式进行。

那么,让我们来检索一下Cpython的源代码,然后我们将发现'builtin_input'是'builtin_input_impl'的封装,而'builtin_input_impl'是一个在bltinmodule.c里面实现的一个方法。让我们尝试将Python载入到lldb C语言调试器里面并在那个方法的开头设置一个断点:
 

flowerhack$ lldb -- /Users/flowerhack/cpython/python.exe
flowerhack$ breakpoint set --file bltinmodule.c --line 2337

当单步步过源代码的时候(这个过程和你在PDB里面做的事情很像——不停敲击”n”来运行下一行代码),我们发现问题第一次出现的那点代码:
 

stdout_encoding_str = _PyUnicode_AsString(stdout_encoding);
stdout_errors_str = _PyUnicode_AsString(stdout_errors);
if (!stdout_encoding_str || !stdout_errors_str)
goto _readline_errors; // "throws" an exception

第三行误导了我:“如果编码字符串是空或者错误字符串是空,那么我们会得到一个错误”。但是,请等一下,难道一个空的错误字符串不是意味着没有错误被发现吗?

因为这个,我进一步查看了_PyUnicode_AsString的定义(另一个C函数):
 

#define _PyUnicode_AsString PyUnicode_AsUTF8

那仅仅是一个宏:“嘿,当我们调用_PyUnicode_AsString的时候,去调用PyUnicode_AsUTF8。”所以我们真正想要找的是PyUnicode_AsUTF8的定义:
 

char*
PyUnicode_AsUTF8(PyObject *unicode)
{
return PyUnicode_AsUTF8AndSize(unicode, NULL);
}

……看上去这个函数所做的所有的事情是调用PyUnicode_AsUTF8AndSize,而这正是我们真正想要去阅读的。

在PyUnicode_AsUTF8AndSize函数里面有若干个错误情况,每一个都返回NULL。在错误情况里面返回NULL而不是返回像-1这样的错误代码对我来说很奇怪。也许这里有其他我不熟悉的约定的考虑?

不管怎么样,为了显示出我究竟陷入了哪一个错误情况,我进行了“打印调试”——我在每一个可能的错误情况后面加入了一个打印语句,然后运行程序——这样我们就能发现当我们调用PyUnicode_Check到底错在了哪里。

那么,是否有在Python3里面进行了而没有在Python2里面进行过的的检查呢?嗯,我们能比较两个版本的源代码来找出这个答案。最后显示出,Python 2 的源代码没有进行类似的编码检查,然而Python 3做了。所以,如果sys.stdout被错误编码的东西代替了,它会在3里面运行失败,在2里面就不会。

4.收获!

看上去仅仅是找出一个非常普通的固定的BUG后面的原因,就做了非常多的工作。并且也许确实是这样。但是!我们在这个过程中学到了一些很酷的东西。当我在验证一些假设的时候我发现了很多Python处理标准输入输出的方式。我学到了更多如何阅读大型的、很多宏的C工程的经验。我学到了GOTO语句仍然在使用,这让我感到很吃惊。但是在连贯性上这样做是有意义的——看上去如果不用GOTO在C里面做一些像是异常处理的事情的时候将变的很繁琐。并且浏览bltinmodule.c的input 函数在Python2 和Python3中的不同真的是一件很酷的事情——严格上来说,是检查。他们重构和清理东西看上去很简洁。

声明:设置cinspect有一点复杂。在这个项目的README里面的介绍会有一些帮助,但是注意“indexing your sources”这一步将会花很多时间。

如果你之前习惯使用gdb,那么你仅仅需要知道的是lldb和它非常相似。如果你两个都没有用过,他们在调试上有一点像PDB。

Python 相关文章推荐
在Python的Django框架中加载模版的方法
Jul 16 Python
Python设计模式之观察者模式简单示例
Jan 10 Python
Python实现迭代时使用索引的方法示例
Jun 05 Python
python生成多个只含0,1元素的随机数组或列表的实例
Nov 12 Python
程序员写Python时的5个坏习惯,你有几条?
Nov 26 Python
python生成带有表格的图片实例
Feb 03 Python
深入了解Python iter() 方法的用法
Jul 11 Python
Pycharm中出现ImportError:DLL load failed:找不到指定模块的解决方法
Sep 17 Python
怎么解决pycharm license Acti的方法
Oct 28 Python
python爬虫判断招聘信息是否存在的实例代码
Nov 20 Python
Python脚本调试工具安装过程
Jan 11 Python
PyTorch 实现L2正则化以及Dropout的操作
May 27 Python
30分钟搭建Python的Flask框架并在上面编写第一个应用
Mar 30 #Python
编写同时兼容Python2.x与Python3.x版本的代码的几个示例
Mar 30 #Python
以Python的Pyspider为例剖析搜索引擎的网络爬虫实现方法
Mar 30 #Python
在树莓派2或树莓派B+上安装Python和OpenCV的教程
Mar 30 #Python
Python中利用函数装饰器实现备忘功能
Mar 30 #Python
利用Python绘制MySQL数据图实现数据可视化
Mar 30 #Python
Python面向对象编程中的类和对象学习教程
Mar 30 #Python
You might like
PHP的5个安全措施小结
2012/07/17 PHP
PHP 面向对象程序设计(oop)学习笔记 (二) - 静态变量的属性和方法及延迟绑定
2014/06/12 PHP
phpstudy默认不支持64位php的解决方法
2017/02/20 PHP
php 多进程编程父进程的阻塞与非阻塞实例分析
2020/02/22 PHP
jquery imgareaselect 使用利用js与程序结合实现图片剪切
2009/07/30 Javascript
javascript闭包的理解
2015/04/01 Javascript
JavaScript编写页面半透明遮罩效果的简单示例
2016/05/09 Javascript
jQuery插件实现图片轮播特效
2016/06/16 Javascript
有关文件上传 非ajax提交 得到后台数据问题
2016/10/12 Javascript
详解webpack-dev-server的简单使用
2018/04/02 Javascript
vue 做移动端微信公众号采坑经验记录
2018/04/26 Javascript
Angular Renderer (渲染器)的具体使用
2018/05/03 Javascript
JavaScript实现多态和继承的封装操作示例
2018/08/20 Javascript
vue通过style或者class改变样式的实例代码
2018/10/30 Javascript
使用webpack搭建vue项目实现脚手架功能
2019/03/15 Javascript
jQuery zTree插件快速实现目录树
2019/08/16 jQuery
layui问题之模拟table表格中的选中按钮选中事件的方法
2019/09/20 Javascript
微信小程序getLocation 需要在app.json中声明permission字段
2020/03/03 Javascript
在Python程序中操作文件之flush()方法的使用教程
2015/05/24 Python
Python在Console下显示文本进度条的方法
2016/02/14 Python
Golang与python线程详解及简单实例
2017/04/27 Python
python 使用xlsxwriter循环向excel中插入数据和图片的操作
2021/01/01 Python
Python解析m3u8拼接下载mp4视频文件的示例代码
2021/03/03 Python
Canvas实现放大镜效果完整案例分析(附代码)
2020/11/26 HTML / CSS
迪拜领先运动补剂零售品牌中文站:Sporter商城
2019/08/20 全球购物
得到Class的三个过程是什么
2012/08/10 面试题
OLEDBConnection和SQLConnection有什么区别
2013/05/31 面试题
介绍一下mysql的日期和时间函数
2013/03/28 面试题
师范生实习自我鉴定
2013/11/01 职场文书
主持人婚宴答谢词
2014/01/28 职场文书
幼儿园保育员责任书
2014/07/22 职场文书
单方离婚协议书范本2014
2014/10/28 职场文书
2014年法院工作总结
2014/11/24 职场文书
逃课检讨书
2015/01/26 职场文书
2015年重阳节主持词
2015/07/04 职场文书
python和anaconda的区别
2022/05/06 Python