前言
好久不见,大家最近可好😏。通过前几节的学习,相信你已经掌握了面向对象的大量知识,但是光知道是不够的,需要自己多写、多看,学一门语言无非不过这两种秘诀嘛。因此本篇博文带着大家剖析一次源代码,剖析对象为代码调试模块: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)的,下次见!