使用 PySnooper 优雅的对 Python 进行 DeBug

简介

PySnooper 是一个极简的调试器。
当你试图弄清楚为什么你的 Python 代码没有按照你认为的那样运行并得到你想要的结果时,大多数时候,你会选择用assert 打断点 或者使用 print 打印出某一行或某几行的变量值,你想知道哪些行正在运行,哪些不是,以及局部变量的值是什么。

PySnooper 让你做同样的事情, 你只需要添加一个装饰器到你感兴趣的功能。你将获得你的功能函数的逐行变化日志,包括哪些行在运行、何时更改局部变量以及局部变量值的变化。

是什么让PySnooper从所有其他代码智能工具中脱颖而出?
您可以在糟糕庞大的企业代码库中使用它,而无需进行任何设置。只需打开装饰器,如下所示,并通过将其路径指定为第一个参数,将输出重定向到专用日志文件。

PySnooper 案例

整个函数 Debug

项目作者写了一个函数以将数值转换为二进制码,该函数返回的是一个二进制列表。
将装饰器 @pysnooper.snoop() 加到该函数上,运行程序就 OK 了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import pysnooper

@pysnooper.snoop()
def number_to_bits(number):
if number:
bits = []
while number:
number, remainder = divmod(number, 2)
bits.insert(0, remainder)
return bits
else:
return [0]

number_to_bits(6)

该函数返回的日志如下,我们可以看到在调用 number_to_bits 函数时,赋予参数 number 的初始值为 6。然后,PySnooper 对着源代码一行行分析了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Starting var:.. number = 6
15:29:11.327032 call 4 def number_to_bits(number):
15:29:11.327032 line 5 if number:
15:29:11.327032 line 6 bits = []
New var:....... bits = []
15:29:11.327032 line 7 while number:
15:29:11.327032 line 8 number, remainder = divmod(number, 2)
New var:....... remainder = 0
Modified var:.. number = 3
15:29:11.327032 line 9 bits.insert(0, remainder)
Modified var:.. bits = [0]
15:29:11.327032 line 7 while number:
15:29:11.327032 line 8 number, remainder = divmod(number, 2)
Modified var:.. number = 1
Modified var:.. remainder = 1
15:29:11.327032 line 9 bits.insert(0, remainder)
Modified var:.. bits = [1, 0]
15:29:11.327032 line 7 while number:
15:29:11.327032 line 8 number, remainder = divmod(number, 2)
Modified var:.. number = 0
15:29:11.327032 line 9 bits.insert(0, remainder)
Modified var:.. bits = [1, 1, 0]
15:29:11.327032 line 7 while number:
15:29:11.327032 line 10 return bits
15:29:11.327032 return 10 return bits
Return value:.. [1, 1, 0]

如上分析所示,函数每创建一个新变量,那么这个变量的值、这个变量的变化都会展示出来。而且 PySnooper 还将循环展开,因此变化的细节更加明确。最终 6 的二进制版本应该是 [1, 1, 0],它的变化过程也展示在 bits 变量中。

现在通过这些详细信息,PySnooper 再也不用担心我们用 print 函数强行 deBug 了。

局部 Debug

可以将相关部分包含在with块中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import pysnooper
import random

def foo():
lst = []
for i in range(10):
lst.append(random.randrange(1, 1000))

with pysnooper.snoop():
lower = min(lst)
upper = max(lst)
mid = (lower + upper) / 2
print(lower, mid, upper)

foo()

该函数返回的日志如下

1
2
3
4
5
6
7
8
9
10
New var:....... i = 9
New var:....... lst = [681, 267, 74, 832, 284, 678, ...]
09:37:35.881721 line 10 lower = min(lst)
New var:....... lower = 74
09:37:35.882137 line 11 upper = max(lst)
New var:....... upper = 832
09:37:35.882304 line 12 mid = (lower + upper) / 2
74 453.0 832
New var:....... mid = 453.0
09:37:35.882486 line 13 print(lower, mid, upper)

PySnooper 部分详细特征

如果标准错误输出难以获得,或者太长了,那么可以将输出定位到本地文件:

1
@pysnooper.snoop('/my/log/file.log')

查看一些非本地变量的值:

1
@pysnooper.snoop(variables=('foo.bar', 'self.whatever'))

展示我们函数中调用函数的 snoop 行:

1
@pysnooper.snoop(depth=2)

将所有 snoop 行以某个前缀开始,更容易定位和找到:

1
@pysnooper.snoop(prefix='ZZZ ')

PySnooper 安装

pip:

pip install pysnooper

conda with conda-forge channel:

conda install -c conda-forge pysnooper

PySnooper 项目地址

https://github.com/cool-RR/pysnooper

工具是为生产进行服务,工具是为了提高效率而存在,请选择最适合你的工具,无论是 assert 、print 、还是pysnooper。