【Python】二、debuglib.py,除錯利器
0 序
如果下述問題也困擾著你,又或仍在使用“新手做法”, 那麼本文值得一讀。
常見問題 | 新手做法 | debuglib分享方法 | 標準處理方法 |
---|---|---|---|
程式計時,速度效能分析 | t0 = time.clock()…print(‘xxx用時%.2f\n’ % (time.clock() - t0)) | tic()…toc(‘xxx’) | timeit模組IDE的Profile功能 |
檢視程式是否執行到某個位置 | print(123)…print(321) | dbvar() | IDE斷點功能 |
檢視變數值,監控過程 | print(a)print(‘a=’, a) | dbvar(a, b, c) | IDE監控變數log日誌模組 |
異常警告 | 不處理特殊情況或者直接raise報錯 | dbvar(a, b) # 異常資訊 | warnings模組 |
表格等資料的對齊輸出 | 靠火眼金睛看print的輸出 | arralign()excel() | pprint模組pandas模組 |
檢視物件的型別和成員變數、方法 | 不知道變數是什麼型別,吐槽動態語言不好用 | mydir(ob) | 跳轉查原始碼Structure查官方文件 |
上面第4列給出了這些問題工程上比較規範的做法,有不清楚的讀者自己找資料瞭解,網上的資料很多本文不過多介紹,重點是要分享我的debuglib.py工具。如果把標準做法看成是PhotoShop軟體,那麼我所要分享的工具就是美圖秀秀,論專業和嚴謹我的工具自然是無法和IDE的除錯功能相比的,但有時候我們只是遇到一個小問題,殺雞焉用牛刀?我的工具有一些精巧的設計封裝,一切設計都是為了便捷、方便快速,又比“新手做法”的“畫圖軟體”強大的多。
後文的介紹依賴debuglib.py原始碼 (訪問密碼:sEg8Cj),不過也不需要用裡面的所有程式碼,讀者有感興趣的功能可以做程式碼摘選而不是複製整個指令碼檔案來使用。有些函式介面支援非常靈活的擴充套件功能,本文無法面面俱到地介紹,有需要使用的讀者可以讀原始碼,原始碼中也有很完善的文件註釋。
當然,畢竟不是標準的除錯操作,這些東西必然存在爭議, 也歡迎大家的評論和優化。 希望我的程式碼至少能給大家帶來除錯效率方面的思索和靈感。 這些技巧並不侷限於Python語言,這是用任何程式語言開發都值得思考的問題。
作者:陳坤澤
1 程式計時
對一段程式碼計時,我們可以先封裝好兩個函式:tic、toc(命令名稱參考自Matlab語言)。這樣呼叫tic時可以重置計時器,呼叫toc時自動輸出從上一個tic到目前的執行時間:
T = 10**8
tic() # 從此處開始計時
for _ in range(T): pass
toc() # 輸出執行時長
tic() # 重置計時器
for _ in range(T): pass
toc('第二個迴圈') # 輸出第二段迴圈執行時長
toc第1個引數可以輸入一個字串控制字首,例如這段程式碼輸出:
用時2.81秒
第二個迴圈 用時2.80秒
當需要巢狀計時,在一個大計時過程中記錄一個子過程的時間,要設定tic和toc的key引數,以區別不同的計時器:
T = 10**7
def func():
for _ in range(T): pass
tic(key=1)
for _ in range(T): pass
toc('func內部計時', key=1)
for _ in range(T): pass
tic()
func()
toc('func')
程式輸出:
func內部計時 用時0.17秒
func 用時0.51秒
2 過程與變數監控
2.1 檢視程式是否執行到某個位置
假設我們現在要開發一個計算$a^b \mod m$
的程式,從一個有問題的程式碼來舉例如何用dbvar工具進行排查解決bug。
初始test.py程式碼如下:
a, b, m = map(int, input().split()) # 從控制檯輸入一行字串,例如`2 2 3`,讀取成3個整數值
if m is True:
n = a^b
a = n % m
print(a)
輸入2 2 3
,會發現程式輸出是2
,測試更多的輸入值,會發現程式輸出的值永遠是輸入的a原始值,於是可以猜測沒有進入if語句,此時可以在n前面寫一句print(123),來測試是否有進入if,不過更方便的做法是使用dbvar函式:
a, b, m = map(int, input().split())
dbvar()
if m is True:
dbvar()
n = a^b
a = n % m
print(a)
輸入2 2 3
,得到的輸出是:
test.py/<module>/2:
2
dbvar是debug variable的縮寫。
輸出第1行,是被執行程式碼所在位置,有三部分組成,第一部分test.py
是表示所在檔案,第二部分<module>
表示所在函式,未進行函式封裝時,會輸出<module>
表示是test.py模組的程式碼,第3部分2
是test.py檔案第2行的意思。
輸出第2行,是程式最後執行的print(a)。
從這裡可以得知程式有執行到第2行,卻未進入if語句執行到第4行,檢查後得知是if條件寫錯了,直接判斷m的取值即可,參見真值測試,修改第3行if m is True:
為if m:
,輸出變為:
test.py/<module>/2:
test.py/<module>/4:
0
跟普通的print(123)相比,dbvar能輸出自身程式碼所在位置,有了定位資訊,就不用人為刻意寫print(123)、print(321)來區別不同位置。
2.2 檢視變數值,監控過程
進入if
程式碼塊後,程式結果0
仍然是錯誤答案,我們很容易把目光放到中間運算結果n=a^b
上,可以直接print(n)
檢查n
的取值。這個小程式還好,但如果是大專案,控制檯的輸出那麼多,除錯的時候怎麼分的清哪個結果是哪句程式碼輸出的?所以要加修飾print('n=', n)
,但這樣變數一多也很煩。dbvar不僅僅有定位資訊,也能傳入引數監控變數值。使用dbvar這類除錯問題迎刃而解,修改代入如下:
a, b, m = map(int, input().split())
if m:
n = a^b
dbvar(a, b, n, m)
a = n % m
print(a)
輸入2 2 3
,得到的輸出是:
test.py/<module>/5: a=2, b=2, n=0, m=3
0
發現n
的計算結果不對,檢查python語法,a^b
實際是兩個整數的異或運算,指數運算應該用a**b
。修改程式碼後就能正常輸出1
了:
a, b, m = map(int, input().split())
if m:
n = a**b
a = n % m
print(a)
2.3 異常警告
上述程式碼因為有做if m:
的判斷,所以輸入m=0
並不會丟擲異常,但也沒有任何提示資訊告訴使用者輸入錯誤,如果要加else提示,有些人可能會寫print('除數m=', m, '非法')
,或者raise直接丟擲錯誤ZeroDivisionError('除數m=' + str(m) + '非法')
。但這種監控變數數值的方法又繞回要自己新增變數m標記的老路了,其實print警告性質的錯誤也可以用dbvar來提示,只需在正常dbvar後面加上註釋程式碼:
a, b, m = map(int, input().split())
if m:
n = a**b
a = n % m
print(a)
else:
dbvar(m) # 除數m數值非法
輸入2 2 0
會得到非常全面的提示資訊,包括這句提示資訊所在程式碼行,所監控變數值,所出現的錯誤型別:
test.py/<module>/7: m=0, # 除數m數值非法
這個例子因為只有一個變數m
要監控,以及顯然出現異常的時候m
的值是0,所以並不是特別好地看出dbvar作用。讀者可以想想如果出現異常的值是不確定的,且有多個變數要關聯的時候,dbvar就非常簡潔方便了。
2.4 dbvart
dbvart和dbvar功能差不多,但增加了監控變數型別的功能,字尾t是type的縮寫。 注意它們都可以監控一個表示式的值,而不僅僅是變數,例如:
dbvart(12+34, '12'+'34')
# 輸出:test.py/<module>/1: 12+34<int>=46, '12'+'34'<str>=1234
3 對齊輸出
3.1 解包操作
對於一些複雜的list,直接print可能看不清楚元素:
ls = [1, [2, 'a', 'b', ['b', 'c']], ['a', ['b', 'c']]]
print(ls) # [1, [2, 'a', 'b', ['b', 'c']], ['a', ['b', 'c']]]
此時可以用解包語法,詳見:
ls = [1, [2, 'a', 'b', ['b', 'c']], ['a', ['b', 'c']]]
print(*ls, sep='\n')
# 1
# [2, 'a', 'b', ['b', 'c']]
# ['a', ['b', 'c']]
有興趣的可以瞭解更多解包、壓包操作,這在複雜函式介面的傳參中也有非常巧妙的應用。通過解包可以對一些線性表型別的資料一行一行等更加直觀的形式輸出檢視。
3.2 arralign
對於二維陣列,用print解包輸出仍然不夠直觀:
arr = [[1, 1], [20], [3, 3, 3]]
print(*arr, sep='\n')
# [1, 1]
# [20]
# [3, 3, 3]
可以用專門進行二維陣列資料對齊的arralign函式:
arr = [[1, 1], [20], [3, 3, 3]]
print(arralign(arr))
# 1 1
# 20
# 3 3 3
arralign有大量的引數可以靈活調整輸出效果。可以控制每列的對齊方式,可以增加一個序號列,可以增加每列的標籤名(甚至採用Excel的列標籤編號格式),可以控制列間隔,可以控制每個元素最長顯示長度等,讀者可以讀原始碼瞭解詳細用法。其更復雜的應用效果見後文mydir的輸出。
這個對齊函式的實現有點複雜,因為要考慮中文情況下len函式算的是字串長度並不是“所佔域寬”,這個問題pandas在某些特殊字元下也會域寬計算錯誤導致對不齊,這是我決定自己開發arralign而不用pandas的原因之一。
import pandas as pd
with pd.option_context('display.unicode.east_asian_width', True): # 中文輸出必備選項,用來控制正確的域寬
print(pd.DataFrame({'name': ['哈', '呵呵'], 'score': [45, 123]})) # 這個輸出正常
print(pd.DataFrame({'name': ['①', '呵呵'], 'score': [45, 123]})) # 遇到①等字元域寬就計算錯誤了
# name score
# 0 哈 45
# 1 呵呵 123
# name score
# 0 ① 45
# 1 呵呵 123
print(arralign([['①', 45], ['呵呵', 123]], col_tag=('name', 'score'), row_tag=1))
# name score
# 1 ① 45
# 2 呵呵 123
arralign具體實現原理有興趣的可以讀我的該篇部落格:py3 中文字串對齊問題。
arralign中呼叫了debuglib.py的shorten函式,控制字串長度的函式也是自己寫的而不是呼叫textwrap.shorten,因為我在textwrap.shorten使用中也發現了一個bug:
>>> textwrap.shorten('0123456789 0123456789', 11) # 全部字元都被摺疊了
'[...]'
>>> shorten('0123456789 0123456789', 11) # 自己寫的shorten
'0123456789 '
說這些是想解釋我程式碼寫的那麼長是有原因的,說來話長啊(笑哭.gif)。
3.3 excel
不過換個角度,為什麼一定要文字形式對齊輸出呢?很多表格資料其實關聯到Excel是最方便的。分析類的東西,不應該程式輸出一個死的報告,而是生成一個二維表,放到excel來研究。excel的篩選、設定顏色格式、資料透視表等都能給分析帶來極大的幫助。
比較簡單的關聯方式是輸出文字的時候使用“Tab”隔開,再將文字複製到Excel就能形成二維資料表了。這裡我進行了封裝,用excel()函式做了關聯簡化操作:
關於字串格式控制,最後再推薦大家瞭解下f字串,這個python3.6新增的功能大大簡化了格式控制程式碼量:
a = 12
b = 34
print(f'{a}+{b}={a+b}')
# 12+34=46
4 檢視物件型別與成員
4.1 動態語言
動態語言由於每個變數的具體型別沒有靜態語言直觀,使用起來可能有些不便,通過Python支援的一些功能函式:type、dir、help,可以適當減輕影響。
>>> s = 'string'
>>> type(s) # 檢視一個變數的型別
<class 'str'>
>>> dir(s) # 檢視一個變數的所有成員(變數、方法)
['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mod__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'capitalize', 'casefold', 'center', 'count', 'encode', 'endswith', 'expandtabs', 'find', 'format', 'format_map', 'index', 'isalnum', 'isalpha', 'isdecimal', 'isdigit', 'isidentifier', 'islower', 'isnumeric', 'isprintable', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'maketrans', 'partition', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']
>>> help(s.replace) # 檢視一個函式的使用方法
Help on built-in function replace:
replace(...) method of builtins.str instance
S.replace(old, new[, count]) -> str
Return a copy of S with all occurrences of substring
old replaced by new. If the optional argument count is
given, only the first count occurrences are replaced.
不過另一方面,沒有型別也是一種方便,可以傳入任意型別,我們可以非常簡單地寫出所有型別的“加”操作,而不用像C語言要給所有整數、浮點數、字串等的加法單獨寫一個函式:
def add(a, b):
return a + b
甚至傳入一個“函式”作為引數,這就是“高階函式”,函數語言程式設計的概念了。下下節要講的re.sub函式第2個輸入引數就利用這個技巧讓使用者可以及其便利地對正則操作進行非常靈活的功能擴充套件。
如果要明確變數型別,python也是支援型別標註語法的(Type Hints),不過注意這種標註只是方便讀者,並不影響實際功能,雖然標註int但還是能輸入str等型別,並沒有改變python本身的功能,但這種標註也能給IDE帶來便利,如果出現型別矛盾,IDE會出現相應的提示警告。
# 顯式地宣告輸入引數型別、函式返回值型別
def add(a: int, b: int) -> int:
return a + b
4.2 mydir
使用type、dir功能還是不太方便,所以我寫了一個mydir進行功能擴充套件,能輸出任意物件(模組、變數、類、函式)的成員清單:
>>> import re
>>> m = re.search(r'(\d{6})-(\d{6})', '2018暑假高數高二選修2-1(教師版) 180526-091500.pdf')
>>> print(mydir(m))
==== 物件名稱:m,類繼承關係:(<class '_sre.SRE_Match'>, <class 'object'>),記憶體消耗:136(遞迴子類總大小:136)Byte ====
成員變數:
1 __doc__ str,The result of re.match() and re.search(). Match objects always have a boolean value of True.
2 endpos int,38
3 lastgroup NoneType,None
4 lastindex int,2
5 pos int,0
6 re _sre.SRE_Pattern,re.compile('(\\d{6})-(\\d{6})')
7 regs tuple,((21, 34), (21, 27), (28, 34))
8 string str,2018暑假高數高二選修2-1(教師版) 180526-091500.pdf
成員方法:
1 __class__ <class '_sre.SRE_Match'>
2 __copy__ <built-in method __copy__ of _sre.SRE_Match object at 0x000002E090387470>
3 __deepcopy__ <built-in method __deepcopy__ of _sre.SRE_Match object at 0x000002E090387470>
4 __delattr__ <method-wrapper '__delattr__' of _sre.SRE_Match object at 0x000002E090387470>
5 __dir__ <built-in method __dir__ of _sre.SRE_Match object at 0x000002E090387470>
6 __eq__ <method-wrapper '__eq__' of _sre.SRE_Match object at 0x000002E090387470>
7 __format__ <built-in method __format__ of _sre.SRE_Match object at 0x000002E090387470>
8 __ge__ <method-wrapper '__ge__' of _sre.SRE_Match object at 0x000002E090387470>
9 __getattribute__ <method-wrapper '__getattribute__' of _sre.SRE_Match object at 0x000002E090387470>
10 __getitem__ <method-wrapper '__getitem__' of _sre.SRE_Match object at 0x000002E090387470>
11 __gt__ <method-wrapper '__gt__' of _sre.SRE_Match object at 0x000002E090387470>
12 __hash__ <method-wrapper '__hash__' of _sre.SRE_Match object at 0x000002E090387470>
13 __init__ <method-wrapper '__init__' of _sre.SRE_Match object at 0x000002E090387470>
14 __init_subclass__ <built-in method __init_subclass__ of type object at 0x00000000514E1F70>
15 __le__ <method-wrapper '__le__' of _sre.SRE_Match object at 0x000002E090387470>
16 __lt__ <method-wrapper '__lt__' of _sre.SRE_Match object at 0x000002E090387470>
17 __ne__ <method-wrapper '__ne__' of _sre.SRE_Match object at 0x000002E090387470>
18 __new__ <built-in method __new__ of type object at 0x00000000514F45C0>
19 __reduce__ <built-in method __reduce__ of _sre.SRE_Match object at 0x000002E090387470>
20 __reduce_ex__ <built-in method __reduce_ex__ of _sre.SRE_Match object at 0x000002E090387470>
21 __repr__ <method-wrapper '__repr__' of _sre.SRE_Match object at 0x000002E090387470>
22 __setattr__ <method-wrapper '__setattr__' of _sre.SRE_Match object at 0x000002E090387470>
23 __sizeof__ <built-in method __sizeof__ of _sre.SRE_Match object at 0x000002E090387470>
24 __str__ <method-wrapper '__str__' of _sre.SRE_Match object at 0x000002E090387470>
25 __subclasshook__ <built-in method __subclasshook__ of type object at 0x00000000514E1F70>
26 end <built-in method end of _sre.SRE_Match object at 0x000002E090387470>
27 expand <built-in method expand of _sre.SRE_Match object at 0x000002E090387470>
28 group <built-in method group of _sre.SRE_Match object at 0x000002E090387470>
29 groupdict <built-in method groupdict of _sre.SRE_Match object at 0x000002E090387470>
30 groups <built-in method groups of _sre.SRE_Match object at 0x000002E090387470>
31 span <built-in method span of _sre.SRE_Match object at 0x000002E090387470>
32 start <built-in method start of _sre.SRE_Match object at 0x000002E090387470>
Python中所有東西本質上都是類,包括模組、整數浮點數字符串、函式等所有物件。
- mydir輸出第一行,給出了這個類的繼承關係,記憶體開銷。
- 接下來列舉了類具有的成員變數,變數值型別,當前取值情況。
- 再然後列舉了類具有的成員方法。
這個功能在瞭解一個陌生庫時非常方便,能迅速知道所有介面功能,也能用來整理文件。使用mydir(m, useexcel=True)
就能用excel來開啟兩個表格資料:
然後整理到OneNote等筆記軟體,加上自己的註解: