利用一个简单的例子窥探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 MD5文件生成码
Jan 12 Python
Python使用Mechanize模块编写爬虫的要点解析
Mar 31 Python
python之文件的读写和文件目录以及文件夹的操作实现代码
Aug 28 Python
利用ctypes提高Python的执行速度
Sep 09 Python
python去掉行尾的换行符方法
Jan 04 Python
Python算法应用实战之栈详解
Feb 04 Python
Python基于sklearn库的分类算法简单应用示例
Jul 09 Python
Python3.6中Twisted模块安装的问题与解决
Apr 15 Python
PYQT5实现控制台显示功能的方法
Jun 25 Python
python3 selenium自动化测试 强大的CSS定位方法
Aug 23 Python
pandas条件组合筛选和按范围筛选的示例代码
Aug 26 Python
python二元表达式用法
Dec 04 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
DedeCms模板安装/制作概述
2007/03/11 PHP
FirePHP 推荐一款PHP调试工具
2011/04/23 PHP
thinkphp3.2.2实现生成多张缩略图的方法
2014/12/19 PHP
PHP基于GD库的图像处理方法小结
2016/09/27 PHP
HTML-CSS群中单选引发的“事件”
2007/03/05 Javascript
用document.documentElement取代document.body的原因分析
2009/11/12 Javascript
jQuery控制cookie过期时间的方法
2015/04/07 Javascript
AngularJS 路由和模板实例及路由地址简化方法(必看)
2016/06/24 Javascript
jQuery可见性过滤选择器用法示例
2016/09/09 Javascript
JS简单判断函数是否存在的方法
2017/02/13 Javascript
nodejs实现大文件(在线视频)的读取
2020/10/16 NodeJs
Vue.Draggable拖拽功能的配置使用方法
2020/07/29 Javascript
[57:59]EG vs Secret 2018国际邀请赛淘汰赛BO3 第一场 8.22
2018/08/23 DOTA
sqlalchemy对象转dict的示例
2014/04/22 Python
Python实现Mysql数据库连接池实例详解
2017/04/11 Python
Python常见工厂函数用法示例
2018/03/21 Python
python用pandas数据加载、存储与文件格式的实例
2018/12/07 Python
Python学习笔记之Break和Continue用法分析
2019/08/14 Python
Python assert关键字原理及实例解析
2019/12/13 Python
Win10下安装并使用tensorflow-gpu1.8.0+python3.6全过程分析(显卡MX250+CUDA9.0+cudnn)
2020/02/17 Python
Python操作Jira库常用方法解析
2020/04/10 Python
Tensorflow中批量读取数据的案列分析及TFRecord文件的打包与读取
2020/06/30 Python
利用python制作拼图小游戏的全过程
2020/12/04 Python
举例详解CSS3中的Transition
2015/07/15 HTML / CSS
html5中为audio标签增加停止按钮动作实现方法
2013/01/04 HTML / CSS
雅诗兰黛美国官网:Estee Lauder美国
2016/07/21 全球购物
消防安全汇报材料
2014/02/08 职场文书
销售人员求职的自我评价分享
2014/03/15 职场文书
实习推荐信
2014/05/10 职场文书
2014大学生职业生涯规划书最新范文
2014/09/13 职场文书
党员先进性教育整改措施
2014/09/18 职场文书
2014年计生工作总结
2014/11/21 职场文书
幼儿园大班毕业评语
2014/12/31 职场文书
大学生活委员竞选稿
2015/11/21 职场文书
幼儿园教师培训心得体会
2016/01/21 职场文书
JAVA API 实用类 String详解
2021/10/05 Java/Android