如果下述问题也困扰着你,又或仍在使用“新手做法”,那么本文值得一读。
常见问题 | 新手做法 | debuglib分享方法 | 标准处理方法 |
---|---|---|---|
程序计时,速度性能分析 | t0 = time.clock() … print(‘xxx用时%.2f\n’ % (time.clock() - t0)) |
timer = Timer(‘xxx’, start_now=True) … timer.stop_and_report() |
timeit模块 IDE的Profile功能 |
查看程序是否运行到某个位置 | print(123) … print(321) |
dprint() | IDE断点功能 |
查看变量值,监控过程 | print(a) print(‘a=’, a) |
dprint(a, b, c) | IDE监控变量 log日志模块 |
异常警告 | 不处理特殊情况或者直接raise报错 | dprint(a, b) # 异常信息 | warnings模块 |
表格等数据的对齐输出 | 靠火眼金睛看print的输出 | chrome() | pprint模块 pandas模块 |
查看对象的类型和成员变量、方法 | 不知道变量是什么类型, 吐槽动态语言不好用 |
showdir(ob) | 跳转查源码Structure 查官方文档 |
上面第4列给出了这些问题工程上比较规范的做法,有不清楚的读者自己找资料了解,网上的资料很多本文不过多介绍,重点是要分享我的debuglib.py工具。如果把标准做法看成是PhotoShop软件,那么我所要分享的工具就是美图秀秀,论专业和严谨我的工具自然是无法和IDE的调试功能相比的,但有时候我们只是遇到一个小问题,杀鸡焉用牛刀?我的工具有一些精巧的设计封装,一切设计都是为了便捷、方便快速,又比“新手做法”的“画图软件”强大的多。
后文的介绍依赖debuglib.py源码 (访问密码:sEg8Cj),不过也不需要用里面的所有代码,读者有感兴趣的功能可以做代码摘选而不是复制整个脚本文件来使用。有些函数接口支持非常灵活的扩展功能,本文无法面面俱到地介绍,有需要使用的读者可以读源码,源码中也有很完善的文档注释。
当然,毕竟不是标准的调试操作,这些东西必然存在争议,也欢迎大家的评论和优化。
希望我的代码至少能给大家带来调试效率方面的思索和灵感。
这些技巧并不局限于Python语言,这是用任何编程语言开发都值得思考的问题。
作者:陈坤泽
time.clock()是有bug的,不同的平台应该使用的计时器不同,最好用timeit中的配置:timeit.default_timer。考虑到还有输出计时标签、多次运行计算平均时间等需求,我们可以封装好一个Timer类更方便使用。
def demo_timer():
"""该函数也可以用来测电脑性能"""
print('1、普通用法(循环5*1000万次用时)')
timer = Timer('循环', start_now=True)
for _ in range(5):
for _ in range(10 ** 7):
pass
timer.stop_and_report()
print('2、循环多轮计时(循环5*1000万次用时)')
timer = Timer()
for _ in range(5):
timer.start()
for _ in range(10 ** 7):
pass
timer.stop_and_report()
print('3、with上下文用法')
with Timer('循环'):
for _ in range(5):
for _ in range(10 ** 6):
pass
# 1、普通用法(循环5*1000万次用时)
# 循环 CumuTime: 0.834s, #run: 1, AvgTime: 0.834s
# 2、循环多轮计时(循环5*1000万次用时)
# CumuTime: 0.157s, #run: 1, AvgTime: 0.157s
# CumuTime: 0.314s, #run: 2, AvgTime: 0.157s
# CumuTime: 0.470s, #run: 3, AvgTime: 0.157s
# CumuTime: 0.626s, #run: 4, AvgTime: 0.157s
# CumuTime: 0.787s, #run: 5, AvgTime: 0.157s
# 3、上下文用法
# 循环 CumuTime: 0.080s, #run: 1, AvgTime: 0.080s
假设我们现在要开发一个计算 a b m o d m a^b \mod m abmodm的程序,从一个有问题的代码来举例如何用dprint工具进行排查解决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,不过更方便的做法是使用dprint函数:
from debuglib import dprint
a, b, m = map(int, input().split()) # 从控制台输入一行字符串,例如`2 2 3`,读取成3个整数值
dprint()
if m is True:
dprint()
n = a ^ b
a = n % m
print(a)
输入2 2 3
,得到的输出是:
test.py//4:
2
dprint借鉴了标注库pprint的命名方式,其中的p是pretty美化输出的意思,我这里的d是debug调试的意思。
输出第1行,是被执行代码所在位置,有三部分组成,第一部分test.py
是表示所在文件,第二部分
表示所在函数,未进行函数封装时,会输出
表示是test.py模块的代码,第3部分4
是test.py文件第4行的意思。
输出第2行,是程序最后运行的print(a)。
从这里可以得知程序有运行到第4行,却未进入if语句运行到第6行,检查后得知是if条件写错了,直接判断m的取值即可,参见真值测试,修改第3行if m is True:
为if m:
,输出变为:
test.py//4:
test.py//6:
0
跟普通的print(123)相比,dprint能输出自身代码所在位置,有了定位信息,就不用人为刻意写print(123)、print(321)来区别不同位置。
进入if
代码块后,程序结果0
仍然是错误答案,我们很容易把目光放到中间运算结果n=a^b
上,可以直接print(n)
检查n
的取值。这个小程序还好,但如果是大项目,控制台的输出那么多,调试的时候怎么分的清哪个结果是哪句代码输出的?所以要加修饰print('n=', n)
,但这样变量一多也很烦。dprint不仅仅有定位信息,也能传入参数监控变量值。使用dprint这类调试问题迎刃而解,修改代入如下:
from debuglib import dprint
a, b, m = map(int, input().split())
if m:
n = a ^ b
dprint(a, b, n, m)
a = n % m
print(a)
输入2 2 3
,得到的输出是:
test.py//6: 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)
上述代码因为有做if m:
的判断,所以输入m=0
并不会抛出异常,但也没有任何提示信息告诉用户输入错误,如果要加else提示,有些人可能会写print('除数m=', m, '非法')
,或者raise直接抛出错误ZeroDivisionError('除数m=' + str(m) + '非法')
。但这种监控变量数值的方法又绕回要自己添加变量m标记的老路了,其实print警告性质的错误也可以用dprint来提示,只需在正常dprint后面加上注释代码:
a, b, m = map(int, input().split())
if m:
n = a ** b
a = n % m
print(a)
else:
dprint(m) # 除数m数值非法
输入2 2 0
会得到非常全面的提示信息,包括这句提示信息所在代码行,所监控变量值,所出现的错误类型:
test.py//9: m=0 # 除数m数值非法
这个例子因为只有一个变量m
要监控,以及显然出现异常的时候m
的值是0,所以并不是特别好地看出dprint作用。读者可以想想如果出现异常的值是不确定的,且有多个变量要关联的时候,dprint就非常简洁方便了。
注意dprint可以监控一个表达式的值,而不仅仅是变量,例如:
from debuglib import dprint
dprint(12+34, '12'+'34')
# 输出:test.py//2: 12+34=46 '12'+'34'='1234'
【后记补充】当前dprint的输出格式由fmt='{filename}/{funcname}/{lineno}: {argmsg} {comment}'
改为fmt='[{depth:02}]{filename}/{lineno}: {argmsg} {comment}'
,即去掉了函数名,前缀增加了调用的函数堆栈深度:
from code4101py.util.debuglib import dprint
def factorial(n):
if n > 0:
dprint(n)
return n * factorial(n - 1)
else:
return 1
factorial(3)
#[05]test.py/5: n=3
#[06]test.py/5: n=2
#[07]test.py/5: n=1
我觉得这样设计更方便实际调试中的需求,读者也可以调整pformat的fmt参数设置适合自己的自定义格式。
关于字符串格式控制,推荐大家了解下f字符串,这个python3.6新增的功能大大简化了格式控制代码量:
a, b = 12, 34
print(f'{a}+{b}={a+b}')
# 12+34=46
对于一些复杂的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']]
有兴趣的可以了解更多解包、压包操作,这在复杂函数接口的传参中也有非常巧妙的应用。通过解包可以对一些线性表类型的数据一行一行等更加直观的形式输出查看。
pprint能对一些嵌套结构等进行美化输出
import pprint
arr = {'name': 'Arrietty', 'email': '[email protected]',
'phone': {'home': '08947 000000', 'mobile': '9999999999', 'test': {'home': '08947 000000', 'mobile': '9999999999'}}}
pprint.pprint(arr)
# {'email': '[email protected]',
# 'name': 'Arrietty',
# 'phone': {'home': '08947 000000',
# 'mobile': '9999999999',
# 'test': {'home': '08947 000000', 'mobile': '9999999999'}}}
分析类的数据,不应该程序输出一个死的报告,而是生成一个二维表,放到excel来研究。excel的筛选、设置颜色格式、数据透视表等都能给分析带来极大的帮助。
比较简单的关联方式是输出文本的时候使用“Tab”隔开,再将文本复制到Excel就能形成二维数据表了。
考虑到不一定每个人都安装有excel软件,但肯定有浏览器,所以我写了一个chrome()函数,会将二维表格数据转成Html表格,用谷歌或系统默认浏览器展示数据:
from debuglib import chrome
ls = [['①', 3], ['呵呵', 123]]
chrome(ls) # 支持DataFrame的参数控制,例如指定列名:chrome(ls, columns=('key', 'value'))
chrome()也能将dict、pandas.DataFrame等多种数据以更适合浏览器展示的效果文本化后,用浏览器打开。
动态语言由于每个变量的具体类型没有静态语言直观,使用起来可能有些不便,通过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
使用type、dir功能还是不太方便,所以我写了一个showdir进行功能扩展,能输出任意对象(模块、变量、类、函数)的成员清单:
>>> import re
>>> from debuglib import showdir
>>> m = re.search(r'(\d{6})-(\d{6})', '2018暑假高数高二选修2-1(教师版) 180526-091500.pdf')
>>> showdir(m, to_html=False) # 默认调用浏览器打开,这里要控制参数才会输出到控制台
==== 对象名称:m,类继承关系:(<class '_sre.SRE_Match'>, <class 'object'>),内存消耗:136(递归子类总大小:136)Byte ====
成员变量 描述
0 __doc__ str,The result of re.match() and re.search()....
1 endpos int,38
2 lastgroup NoneType,None
3 lastindex int,2
4 pos int,0
5 re _sre.SRE_Pattern,re.compile('(\\d{6})-(\\d{6}...
6 regs tuple,((21, 34), (21, 27), (28, 34))
7 string str,2018暑假高数高二选修2-1(教师版) 180526-...
成员函数 描述
0 __class__ <class '_sre.SRE_Match'>
1 __copy__ <built-in method __copy__ of _sre.SRE_Match ob...
2 __deepcopy__ <built-in method __deepcopy__ of _sre.SRE_Matc...
......
22 __sizeof__ <built-in method __sizeof__ of _sre.SRE_Match ...
23 __str__ <method-wrapper '__str__' of _sre.SRE_Match ob...
24 __subclasshook__ <built-in method __subclasshook__ of type obje...
25 end <built-in method end of _sre.SRE_Match object ...
26 expand <built-in method expand of _sre.SRE_Match obje...
27 group <built-in method group of _sre.SRE_Match objec...
28 groupdict <built-in method groupdict of _sre.SRE_Match o...
29 groups <built-in method groups of _sre.SRE_Match obje...
30 span <built-in method span of _sre.SRE_Match object...
31 start <built-in method start of _sre.SRE_Match objec...
>>> showdir(m) # 直接使用,会打开浏览器查看,效果如下图:
Python中所有东西本质上都是类,包括模块、整数浮点数字符串、函数等所有对象。
这个功能在了解一个陌生库时非常方便,能迅速知道所有接口功能,也能用来整理文档。将浏览器器里的表格数据复制到excel或OneNote等笔记软件就行了,还可以加上自己的注解:
初学者遇到错误,很容易忽略raise抛出的错误信息,其实这些提示非常有用,通过其一般都能追溯到大概问题点。
就我个人经验而言,raise抛出错误阅读重要性排序(具体问题具体讨论,不绝对)
这种都是标准库里的代码位置,一般不用管,错误肯定不是出在标准库。但是引发了标准库里怎样的错误,还是要过一眼的,遇到能看懂的就看看,看不懂就跳过关系不大,除非等下仍然解决不了问题再细看标准库错误。
在PyCharm,点击蓝色定位信息,是可以直接跳转到对应代码位置的:
定位大概位置后,再利用“二分法”和前文所介绍的dprint等函数,就能一步步定位出具体出错代码位置了。
如果代码和自己预期结果不同且实在分析不出为什么,可以通过qq群等社群求助,但记得遵循WEM原则: