利用一个简单的例子窥探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 相关文章推荐
使用wxpython实现的一个简单图片浏览器实例
Jul 10 Python
一个超级简单的python web程序
Sep 11 Python
TensorFlow安装及jupyter notebook配置方法
Sep 08 Python
Python+selenium实现截图图片并保存截取的图片
Jan 05 Python
Python使用Django实现博客系统完整版
Sep 29 Python
python解决js文件utf-8编码乱码问题(推荐)
May 02 Python
python  创建一个保留重复值的列表的补码
Oct 15 Python
Python3视频转字符动画的实例代码
Aug 29 Python
浅谈Django中的QueryDict元素为数组的坑
Mar 31 Python
Jupyter 无法下载文件夹如何实现曲线救国
Apr 22 Python
Python操作word文档插入图片和表格的实例演示
Oct 25 Python
详解python定时简单爬取网页新闻存入数据库并发送邮件
Nov 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 clearstatcache()函数详解
2010/03/02 PHP
PHP对MongoDB[NoSQL]数据库的操作
2013/03/01 PHP
thinkphp的c方法使用示例
2014/02/24 PHP
用prototype实现的简单小巧的多级联动菜单
2007/03/24 Javascript
教你如何在 Javascript 文件里使用 .Net MVC Razor 语法
2014/07/23 Javascript
Javascript中的匿名函数与封装介绍
2015/03/15 Javascript
BootStrap中的table实现数据填充与分页应用小结
2016/05/26 Javascript
jQuery UI Bootstrap是什么?
2016/06/17 Javascript
JavaScript禁止用户多次提交的两种方法
2016/07/24 Javascript
有趣的bootstrap走动进度条
2016/12/01 Javascript
前端面试知识点锦集(JavaScript篇)
2016/12/28 Javascript
js以及jquery实现手风琴效果
2020/04/17 Javascript
vuejs响应用户事件(如点击事件)
2017/03/14 Javascript
一个简易的js图片轮播效果
2017/07/22 Javascript
微信小程序实现富文本图片宽度自适应的方法
2019/01/20 Javascript
Vue——解决报错 Computed property "****" was assigned to but it has no setter.
2020/12/19 Vue.js
python实现人脸识别代码
2017/11/08 Python
Python使用matplotlib和pandas实现的画图操作【经典示例】
2018/06/13 Python
详解Python requests 超时和重试的方法
2018/12/18 Python
如何利用pygame实现简单的五子棋游戏
2019/12/29 Python
关于python的缩进规则的知识点详解
2020/06/22 Python
Python暴力破解Mysql数据的示例
2020/11/09 Python
检测浏览器是否支持html5视频的代码
2013/03/28 HTML / CSS
Agoda台湾官网:国内外订房2折起
2018/03/20 全球购物
英国在线汽车和面包车零件商店:Car Parts 4 Less
2018/08/15 全球购物
英国在线药房:Express Chemist
2019/03/28 全球购物
Marc O’Polo俄罗斯官方在线商店:德国高端时尚品牌
2019/12/26 全球购物
波兰汽车配件网上商店:iParts.pl
2020/09/08 全球购物
股份合作协议书范本
2014/04/14 职场文书
中秋手机店促销方案
2014/06/16 职场文书
房屋买卖协议样本
2014/11/16 职场文书
怎样写离婚协议书
2015/01/26 职场文书
学生会生活部工作总结2015
2015/03/31 职场文书
课题研究阶段性总结
2015/08/13 职场文书
2016年“六一儿童节”校园广播稿
2015/12/17 职场文书
慰问信(范文3篇)
2019/10/23 职场文书