#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)的,下次見!

点赞

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *