#6 ipdb模块源代码解读

前言

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

一、ipdb介绍

1.1 ipdb介绍

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

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

1.2 ipdb安装

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

pip install ipdb

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

python -m ipdb

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

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安装或者换用其他方法,之前介绍过,这里就不列举了

二、源代码剖析

2.1 源代码位置

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

pip show ipdb

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

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 

2.2 源代码文件剖析

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

ipdb 文件夹

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

 __init__.py    __main__.py  

 __pycache__    stdout.py

ipdb-0.11-py3.5.egg-info 文件夹

从名称上就可以看出这是一个存放信息的文件夹,里面有 

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:这个文件我也不清楚🤯

『防抄袭:读者请忽略这段文字,文章作者是博客园的MinuteSheep

2.3 源代码剖析

__init__.py

 1 # Copyright (c) 2007-2016 Godefroid Chapelle and ipdb development team
 2 #
 3 # This file is part of ipdb.
 4 # Redistributable under the revised BSD license
 5 # https://opensource.org/licenses/BSD-3-Clause
 6 
 7 from ipdb.__main__ import set_trace, post_mortem, pm, run             # noqa
 8 from ipdb.__main__ import runcall, runeval, launch_ipdb_on_exception  # noqa
 9 
10 from ipdb.stdout import sset_trace, spost_mortem, spm                 # noqa
11 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中导入了许多方法

 

__main__.py

这个文件就是整个模块的主程序了,源代码就放在这个文件中

  1 # Copyright (c) 2011-2016 Godefroid Chapelle and ipdb development team
  2 #
  3 # This file is part of ipdb.
  4 # Redistributable under the revised BSD license
  5 # https://opensource.org/licenses/BSD-3-Clause
  6 
  7 
  8 from IPython.terminal.embed import InteractiveShellEmbed
  9 from IPython.terminal.ipapp import TerminalIPythonApp
 10 from IPython.core.debugger import BdbQuit_excepthook
 11 from IPython import get_ipython
 12 import os
 13 import sys
 14 
 15 from contextlib import contextmanager
 16 
 17 __version__ = "0.10.3"
 18 
 19 
 20 shell = get_ipython()
 21 if shell is None:
 22     # Not inside IPython
 23     # Build a terminal app in order to force ipython to load the
 24     # configuration
 25     ipapp = TerminalIPythonApp()
 26     # Avoid output (banner, prints)
 27     ipapp.interact = False
 28     ipapp.initialize([])
 29     shell = ipapp.shell
 30 else:
 31     # Running inside IPython
 32 
 33     # Detect if embed shell or not and display a message
 34     if isinstance(shell, InteractiveShellEmbed):
 35         sys.stderr.write(
 36             "\nYou are currently into an embedded ipython shell,\n"
 37             "the configuration will not be loaded.\n\n"
 38         )
 39 
 40 # Let IPython decide about which debugger class to use
 41 # This is especially important for tools that fiddle with stdout
 42 debugger_cls = shell.debugger_cls
 43 def_colors = shell.colors
 44 
 45 
 46 def _init_pdb(context=3, commands=[]):
 47     try:
 48         p = debugger_cls(def_colors, context=context)
 49     except TypeError:
 50         p = debugger_cls(def_colors)
 51     p.rcLines.extend(commands)
 52     return p
 53 
 54 
 55 def wrap_sys_excepthook():
 56     # make sure we wrap it only once or we would end up with a cycle
 57     #  BdbQuit_excepthook.excepthook_ori == BdbQuit_excepthook
 58     if sys.excepthook != BdbQuit_excepthook:
 59         BdbQuit_excepthook.excepthook_ori = sys.excepthook
 60         sys.excepthook = BdbQuit_excepthook
 61 
 62 
 63 def set_trace(frame=None, context=3):
 64     wrap_sys_excepthook()
 65     if frame is None:
 66         frame = sys._getframe().f_back
 67     p = _init_pdb(context).set_trace(frame)
 68     if p and hasattr(p, 'shell'):
 69         p.shell.restore_sys_module_state()
 70 
 71 
 72 def post_mortem(tb=None):
 73     wrap_sys_excepthook()
 74     p = _init_pdb()
 75     p.reset()
 76     if tb is None:
 77         # sys.exc_info() returns (type, value, traceback) if an exception is
 78         # being handled, otherwise it returns None
 79         tb = sys.exc_info()[2]
 80     if tb:
 81         p.interaction(None, tb)
 82 
 83 
 84 def pm():
 85     post_mortem(sys.last_traceback)
 86 
 87 
 88 def run(statement, globals=None, locals=None):
 89     _init_pdb().run(statement, globals, locals)
 90 
 91 
 92 def runcall(*args, **kwargs):
 93     return _init_pdb().runcall(*args, **kwargs)
 94 
 95 
 96 def runeval(expression, globals=None, locals=None):
 97     return _init_pdb().runeval(expression, globals, locals)
 98 
 99 
100 @contextmanager
101 def launch_ipdb_on_exception():
102     try:
103         yield
104     except Exception:
105         e, m, tb = sys.exc_info()
106         print(m.__repr__(), file=sys.stderr)
107         post_mortem(tb)
108     finally:
109         pass
110 
111 
112 _usage = """\
113 usage: python -m ipdb [-c command] ... pyfile [arg] ...
114 
115 Debug the Python program given by pyfile.
116 
117 Initial commands are read from .pdbrc files in your home directory
118 and in the current directory, if they exist.  Commands supplied with
119 -c are executed after commands from .pdbrc files.
120 
121 To let the script run until an exception occurs, use "-c continue".
122 To let the script run up to a given line X in the debugged file, use
123 "-c 'until X'"
124 ipdb version %s.""" % __version__
125 
126 
127 def main():
128     import traceback
129     import sys
130     import getopt
131 
132     try:
133         from pdb import Restart
134     except ImportError:
135         class Restart(Exception):
136             pass
137 
138     opts, args = getopt.getopt(sys.argv[1:], 'hc:', ['--help', '--command='])
139 
140     if not args:
141         print(_usage)
142         sys.exit(2)
143 
144     commands = []
145     for opt, optarg in opts:
146         if opt in ['-h', '--help']:
147             print(_usage)
148             sys.exit()
149         elif opt in ['-c', '--command']:
150             commands.append(optarg)
151 
152     mainpyfile = args[0]     # Get script filename
153     if not os.path.exists(mainpyfile):
154         print('Error:', mainpyfile, 'does not exist')
155         sys.exit(1)
156 
157     sys.argv = args     # Hide "pdb.py" from argument list
158 
159     # Replace pdb's dir with script's dir in front of module search path.
160     sys.path[0] = os.path.dirname(mainpyfile)
161 
162     # Note on saving/restoring sys.argv: it's a good idea when sys.argv was
163     # modified by the script being debugged. It's a bad idea when it was
164     # changed by the user from the command line. There is a "restart" command
165     # which allows explicit specification of command line arguments.
166     pdb = _init_pdb(commands=commands)
167     while 1:
168         try:
169             pdb._runscript(mainpyfile)
170             if pdb._user_requested_quit:
171                 break
172             print("The program finished and will be restarted")
173         except Restart:
174             print("Restarting", mainpyfile, "with arguments:")
175             print("\t" + " ".join(sys.argv[1:]))
176         except SystemExit:
177             # In most cases SystemExit does not warrant a post-mortem session.
178             print("The program exited via sys.exit(). Exit status: ", end='')
179             print(sys.exc_info()[1])
180         except:
181             traceback.print_exc()
182             print("Uncaught exception. Entering post mortem debugging")
183             print("Running 'cont' or 'step' will restart the program")
184             t = sys.exc_info()[2]
185             pdb.interaction(None, t)
186             print("Post mortem debugger finished. The " + mainpyfile +
187                   " will be restarted")
188 
189 
190 if __name__ == '__main__':
191     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..........

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

 

stdout.py

 1 import sys
 2 from contextlib import contextmanager
 3 from IPython.utils import io
 4 from .__main__ import set_trace
 5 from .__main__ import post_mortem
 6 
 7 
 8 def update_stdout():
 9     # setup stdout to ensure output is available with nose
10     io.stdout = sys.stdout = sys.__stdout__
11 
12 
13 def sset_trace(frame=None, context=3):
14     update_stdout()
15     if frame is None:
16         frame = sys._getframe().f_back
17     set_trace(frame, context)
18 
19 
20 def spost_mortem(tb=None):
21     update_stdout()
22     post_mortem(tb)
23 
24 
25 def spm():
26     spost_mortem(sys.last_traceback)
27 
28 
29 @contextmanager
30 def slaunch_ipdb_on_exception():
31     try:
32         yield
33     except Exception:
34         e, m, tb = sys.exc_info()
35         print(m.__repr__(), file=sys.stderr)
36         spost_mortem(tb)
37     finally:
38         pass

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

 

__pycache__

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

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

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

 

结语

以上就是ipdb模块源代码的剖析,相信你已经有了分析源代码的能力了!下一篇博文将会记录Python是如何调试代码(debug)的,下次见!

posted @ 2019-03-15 21:00  MinuteSheep  阅读(914)  评论(0编辑  收藏  举报