(点击
上方蓝字
,快速关注我们)
译文:伯乐在线专栏作者 - Nikkko
英文:flowerhack.dreamwidth.org
如有好文章投稿,请点击 → 这里了解详情
如需转载,发送「转载」二字查看说明
我最近花了一些时间在探索CPython,并且我想要在这里分享我的一些冒险经历。Allison Kaptur的excellent guide to getting started with Python internals 有一点啰嗦,我想逐步介绍我自己的探索过程会更加有条理性,这样也许其他好奇的Python使用者可以跟着一起做。
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
=