0%

ipdb模块源代码解读

#6 ipdb模块源代码解读

好久不见,大家最近可好😏。通过前几节的学习,相信你已经掌握了面向对象的大量知识,但是光知道是不够的,需要自己多写、多看,学一门语言无非不过这两种秘诀嘛。因此本篇博文带着大家剖析一次源代码,剖析对象为代码调试模块:ipdb。为什么选择这个模块呢?因为下一次的博文计划写Python代码调试的啦~~Go!!!

一、ipdb介绍

ipdb介绍

ipdb是一款调试代码的第三方模块

我想这一句话就给出了ipdb的所有信息了哇

ipdb安装

既然是第三方模块,那么就需要自己来安装,使用pip即可,在命令行输入:

1
pip install ipdb

测试安装是否成功,在命令行输入:

1
python -m ipdb

如果安装成功则会输出以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
usage: python -m ipdb [-c command] ... pyfile [arg] ...

Debug the Python program given by pyfile.

Initial commands are read from .pdbrc files in your home directory
and in the current directory, if they exist. Commands supplied with
-c are executed after commands from .pdbrc files.

To let the script run until an exception occurs, use "-c continue".
To let the script run up to a given line X in the debugged file, use
"-c 'until X'"
ipdb version 0.10.3.

如果安装失败请重新pip安装或者换用其他方法,之前介绍过,这里就不列举了

二、源代码剖析

源代码位置

想要剖析这一个模块,首先应该找到源代码的位置,由于模块是由pip安装的,所以可以使用pip查看模块的详细信息,在命令行中输入:

1
pip show ipdb

输出的详细信息中,有一行Location信息,每个人的位置可能不同,以自己的为准,这里输出我自己的位置:

1
Location: /Users/minutesheep/.pyenv/versions/3.5.2/Python.framework/Versions/3.5/lib/python3.5/site-packages

进入上面👆所示的目录中,会发现site-packages目录里有许多模块,ipdb模块的源代码有两个,一个是 ipdb ,另一个是 ipdb-0.11-py3.5.egg-info

源代码文件剖析

如果你仔细观察的话,你会发现每一个模块基本是都是两个文件夹,一个文件夹是模块本身,另一个是以info结尾的文件夹,下面以ipdb模块讲解:

ipdb 文件夹 这个文件夹里面存放着ipdb模块的源代码,里面有

1
2
__init__.py    __main__.py  
__pycache__ stdout.py

ipdb-0.11-py3.5.egg-info 文件夹 从名称上就可以看出这是一个存放信息的文件夹,里面有

1
2
3
PKG-INFO   dependency_links.txt  installed-files.txt  

top_level.txt SOURCES.txt entry_points.txt requires.txt zip-safe

PKG-INFO:内容是模块的信息,包括模块名、版本、依赖、作者、代码地址、许可、描述、历史版本变更信息

dependency_links.txt:内容是依赖模块链接

installed-files.txt:内容是安装这个模块时安装的文件

top_level.txt:内容是父亲模块

SOURCES.TXT:内容是整个模块所有的文件

entry_points.txt:内容是程序入口语句

requires.txt:本模块需要的其他模块

zip-safe:这个文件我也不清楚🤯

源代码剖析

1
2
3
4
5
6
7
8
9
10
11
12
# __init__.py
# Copyright (c) 2007-2016 Godefroid Chapelle and ipdb development team
#
# This file is part of ipdb.
# Redistributable under the revised BSD license
# https://opensource.org/licenses/BSD-3-Clause

from ipdb.__main__ import set_trace, post_mortem, pm, run # noqa
from ipdb.__main__ import runcall, runeval, launch_ipdb_on_exception # noqa

from ipdb.stdout import sset_trace, spost_mortem, spm # noqa
from ipdb.stdout import slaunch_ipdb_on_exception # noqa

__init__.py到底是个什么文件呢?为什么Python项目中总是会出现这个诡异的文件呢?

__init__.py其实是将这个文件夹变成一个Python模块,方便以后导入。

每当我们使用import语句时,其实导入的就是这个模块的__init__.py文件。

通常一个模块的许多方法并不会写在同一个文件中,而是会有分类的写入不同的文件中,最后将这个模块的所有方法都一次性写入__init__.py文件中(相当于为所有方法提供一个公共接口),导入的时候将会方便许多。

本模块的__init__.py文件中,前5行是注释信息,这里就不翻译了;第7行开始,进入正式代码,可以看到从__main__.py文件中导入了许多种方法,之后又从stdout.py中导入了许多方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
# __main__.py
# 这个文件就是整个模块的主程序了,源代码就放在这个文件中


# Copyright (c) 2011-2016 Godefroid Chapelle and ipdb development team
#
# This file is part of ipdb.
# Redistributable under the revised BSD license
# https://opensource.org/licenses/BSD-3-Clause


from IPython.terminal.embed import InteractiveShellEmbed
from IPython.terminal.ipapp import TerminalIPythonApp
from IPython.core.debugger import BdbQuit_excepthook
from IPython import get_ipython
import os
import sys

from contextlib import contextmanager

__version__ = "0.10.3"


shell = get_ipython()
if shell is None:
# Not inside IPython
# Build a terminal app in order to force ipython to load the
# configuration
ipapp = TerminalIPythonApp()
# Avoid output (banner, prints)
ipapp.interact = False
ipapp.initialize([])
shell = ipapp.shell
else:
# Running inside IPython

# Detect if embed shell or not and display a message
if isinstance(shell, InteractiveShellEmbed):
sys.stderr.write(
"\nYou are currently into an embedded ipython shell,\n"
"the configuration will not be loaded.\n\n"
)

# Let IPython decide about which debugger class to use
# This is especially important for tools that fiddle with stdout
debugger_cls = shell.debugger_cls
def_colors = shell.colors


def _init_pdb(context=3, commands=[]):
try:
p = debugger_cls(def_colors, context=context)
except TypeError:
p = debugger_cls(def_colors)
p.rcLines.extend(commands)
return p


def wrap_sys_excepthook():
# make sure we wrap it only once or we would end up with a cycle
# BdbQuit_excepthook.excepthook_ori == BdbQuit_excepthook
if sys.excepthook != BdbQuit_excepthook:
BdbQuit_excepthook.excepthook_ori = sys.excepthook
sys.excepthook = BdbQuit_excepthook


def set_trace(frame=None, context=3):
wrap_sys_excepthook()
if frame is None:
frame = sys._getframe().f_back
p = _init_pdb(context).set_trace(frame)
if p and hasattr(p, 'shell'):
p.shell.restore_sys_module_state()


def post_mortem(tb=None):
wrap_sys_excepthook()
p = _init_pdb()
p.reset()
if tb is None:
# sys.exc_info() returns (type, value, traceback) if an exception is
# being handled, otherwise it returns None
tb = sys.exc_info()[2]
if tb:
p.interaction(None, tb)


def pm():
post_mortem(sys.last_traceback)


def run(statement, globals=None, locals=None):
_init_pdb().run(statement, globals, locals)


def runcall(*args, **kwargs):
return _init_pdb().runcall(*args, **kwargs)


def runeval(expression, globals=None, locals=None):
return _init_pdb().runeval(expression, globals, locals)


@contextmanager
def launch_ipdb_on_exception():
try:
yield
except Exception:
e, m, tb = sys.exc_info()
print(m.__repr__(), file=sys.stderr)
post_mortem(tb)
finally:
pass


_usage = """\
usage: python -m ipdb [-c command] ... pyfile [arg] ...

Debug the Python program given by pyfile.

Initial commands are read from .pdbrc files in your home directory
and in the current directory, if they exist. Commands supplied with
-c are executed after commands from .pdbrc files.

To let the script run until an exception occurs, use "-c continue".
To let the script run up to a given line X in the debugged file, use
"-c 'until X'"
ipdb version %s.""" % __version__


def main():
import traceback
import sys
import getopt

try:
from pdb import Restart
except ImportError:
class Restart(Exception):
pass

opts, args = getopt.getopt(sys.argv[1:], 'hc:', ['--help', '--command='])

if not args:
print(_usage)
sys.exit(2)

commands = []
for opt, optarg in opts:
if opt in ['-h', '--help']:
print(_usage)
sys.exit()
elif opt in ['-c', '--command']:
commands.append(optarg)

mainpyfile = args[0] # Get script filename
if not os.path.exists(mainpyfile):
print('Error:', mainpyfile, 'does not exist')
sys.exit(1)

sys.argv = args # Hide "pdb.py" from argument list

# Replace pdb's dir with script's dir in front of module search path.
sys.path[0] = os.path.dirname(mainpyfile)

# Note on saving/restoring sys.argv: it's a good idea when sys.argv was
# modified by the script being debugged. It's a bad idea when it was
# changed by the user from the command line. There is a "restart" command
# which allows explicit specification of command line arguments.
pdb = _init_pdb(commands=commands)
while 1:
try:
pdb._runscript(mainpyfile)
if pdb._user_requested_quit:
break
print("The program finished and will be restarted")
except Restart:
print("Restarting", mainpyfile, "with arguments:")
print("\t" + " ".join(sys.argv[1:]))
except SystemExit:
# In most cases SystemExit does not warrant a post-mortem session.
print("The program exited via sys.exit(). Exit status: ", end='')
print(sys.exc_info()[1])
except:
traceback.print_exc()
print("Uncaught exception. Entering post mortem debugging")
print("Running 'cont' or 'step' will restart the program")
t = sys.exc_info()[2]
pdb.interaction(None, t)
print("Post mortem debugger finished. The " + mainpyfile +
" will be restarted")


if __name__ == '__main__':
main()

一下子看到这么长的代码是不是蒙圈了🤪,遇到这种长的代码,第一步就是在心理上战胜自己!要想成长,就要多看这种标准代码,学习代码思想,模仿代码风格,这样一步一步脚踏实地走下去,你自己写出这样优秀的代码指日可待!

拿到这么长的代码,先大致浏览一下:

1-5行;注释信息;8-15行:导入模块;17行:定义版本变量;20-43行:运行一小段程序(通常是程序的配置);46-109行:定义若干函数;112-124行:定义字符串变量;127-187行:main函数;190-191:判断主程序并运行

通过上面这种方法将程序分解掉,整个程序一下子清晰明了,瞬间感觉so easy~~~

来跟着我稍微详细的走一遍整个程序的运行过程(具体的内容就不做介绍了,因为许多内容需要详细的掌握IPython):

1.从IPthon导入四种方法,导入os和sys模块,从contextlib导入contextmanager(这是一个装饰器)

2.定义当前版本为:0.10.3

3.获得一个ipython的shell环境

4.判断这个shell是否存在:如果不存在,强制性的创建一个ipython环境;如果存在,则检测其是否为InteractiveShellEmbed的一个对象,如果是,则输出标准错误语句“You are currently into an embedded ipython shell""the configuration will not be loaded."

5.使用当前IPython的主题和颜色

6.执行第112行语句,定义_usage字符串

7.执行第190行语句,判断是否为__main__,是的话运行main函数

8.执行127行语句,运行main函数

9..........

以上就是稍微详细的运行过程,感兴趣的小伙伴可以继续深入到每一步是如何运行的,由于篇幅关系,我就不再深入了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# stdout.py

import sys
from contextlib import contextmanager
from IPython.utils import io
from .__main__ import set_trace
from .__main__ import post_mortem


def update_stdout():
# setup stdout to ensure output is available with nose
io.stdout = sys.stdout = sys.__stdout__


def sset_trace(frame=None, context=3):
update_stdout()
if frame is None:
frame = sys._getframe().f_back
set_trace(frame, context)


def spost_mortem(tb=None):
update_stdout()
post_mortem(tb)


def spm():
spost_mortem(sys.last_traceback)


@contextmanager
def slaunch_ipdb_on_exception():
try:
yield
except Exception:
e, m, tb = sys.exc_info()
print(m.__repr__(), file=sys.stderr)
spost_mortem(tb)
finally:
pass

这个文件是ipdb模块的另一个文件,编写项目时,不会将所有方法都写入同一个文件中的,而是将不同的方法分类放入不同的文件中,这个文件的内容就不做详细讲解了。

__pycache__

这是一个文件夹,里面存放着许多以.pyc结尾的文件,这些文件时什么呢?

其实从文件夹的名称就可以看出这些是缓存文件。

Python程序为了加快程序的运行速度,在第一次导入模块后,会在本模块目录中生成__pycache__的缓存文件夹,里面存放着编译过的文件;下一次再次导入这个模块时,直接执行pyc文件,大大加快了程序的运行速度;每当模块里的py文件的修改时间发生变化时,就会重新生成pyc文件。