利用一个简单的例子窥探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 相关文章推荐
压缩包密码破解示例分享(类似典破解)
Jan 17 Python
Python中Proxypool库的安装与配置
Oct 19 Python
python中logging模块的一些简单用法的使用
Feb 22 Python
Python中按值来获取指定的键
Mar 04 Python
用pycharm开发django项目示例代码
Jun 13 Python
python3+PyQt5 使用三种不同的简便项窗口部件显示数据的方法
Jun 17 Python
PowerBI和Python关于数据分析的对比
Jul 11 Python
matplotlib实现显示伪彩色图像及色度条
Dec 07 Python
python应用Axes3D绘图(批量梯度下降算法)
Mar 25 Python
TensorFlow2.1.0最新版本安装详细教程
Apr 08 Python
基于python实现坦克大战游戏
Oct 27 Python
解决selenium+Headless Chrome实现不弹出浏览器自动化登录的问题
Jan 09 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中mysql与mysqli的区别分析
2013/06/10 PHP
关于JSON以及JSON在PHP中的应用技巧
2013/11/27 PHP
php递归删除目录下的文件但保留的实例分享
2014/05/10 PHP
PHP中Memcache操作类及用法实例
2014/12/12 PHP
php生成mysql的数据字典
2016/07/07 PHP
PHP实现多图上传和单图上传功能
2018/05/17 PHP
Centos7.7 64位利用本地完整安装包安装lnmp/lamp套件教程
2021/03/09 Servers
一段批量给页面上的控件赋值js
2010/06/19 Javascript
ECMAScript 创建自己的js类库
2012/11/22 Javascript
nodejs开发微博实例
2015/03/25 NodeJs
Avalon中文长字符截取、关键字符隐藏、自定义过滤器
2016/05/18 Javascript
JS常用字符串方法(推荐)
2021/01/15 Javascript
Vue.js父与子组件之间传参示例
2017/02/28 Javascript
javascript+html5+css3自定义提示窗口
2017/06/21 Javascript
详解vue项目构建与实战
2017/06/27 Javascript
javascript 取小数点后几位几种方法总结
2017/08/02 Javascript
原生js的ajax和解决跨域的jsonp(实例讲解)
2017/10/16 Javascript
深入理解移动前端开发之viewport
2018/10/19 Javascript
jQuery ajax仿Google自动提示SearchSuggess功能示例
2019/03/28 jQuery
vue获取data数据改变前后的值方法
2019/11/07 Javascript
微信小程序开发之获取用户手机号码(php接口解密)
2020/05/17 Javascript
Vue实现鼠标经过文字显示悬浮框效果的示例代码
2020/10/14 Javascript
解读Python编程中的命名空间与作用域
2015/10/16 Python
Python中浅拷贝copy与深拷贝deepcopy的简单理解
2018/10/26 Python
Python2和Python3的共存和切换使用
2019/04/12 Python
python简单实现矩阵的乘,加,转置和逆运算示例
2019/07/10 Python
python导入不同目录下的自定义模块过程解析
2019/11/18 Python
基于python 等频分箱qcut问题的解决
2020/03/03 Python
10款最佳Python开发工具推荐,每一款都是神器
2020/10/15 Python
Python jieba结巴分词原理及用法解析
2020/11/05 Python
美国和加拿大计算机和电子产品购物网站:TigerDirect.com
2019/09/13 全球购物
五一家具促销方案
2014/01/10 职场文书
幼儿园教师岗位职责
2014/03/17 职场文书
工伤赔偿协议书
2014/04/15 职场文书
2015年酒店销售部工作总结
2015/07/24 职场文书
解析Java异步之call future
2021/06/14 Java/Android