理解Python的神器PySnooper调试程序

Posted by locus on 2020-01-05

PySnooper介绍

print对很多人来说算是最常用的debug神器了,只需要在合适的地方插入print打印变量的值,就能判断代码在这个地方是不是还按照你的预期在运行。不过这也带来一些麻烦:首先需要找准位置,然后写出对应的print语句。当函数复杂、变量多的时候确实挺烦的,通常需要多个地方插入多个变量的print。

最近一个刚刚上线的Python包“PySnooper”更优雅地解决了这一需求——只需要对关注的函数前加上一个装饰器@pysnooper.snoop(),就可以将这个函数运行的时间,行号、运行过程中变量的数值以及代码等内容输出到stderr。项目已经有超过13.1K个star,可以说是相当火爆了。

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

PySnooper有以下两个作用:

首先,学习与理解Python执行逻辑,在新手学习中,经过困扰的是一段代码不知道背后机器是如何进行执行的,PySnooper可以解决这个问题。

其次,程序debug调试,这个就是开头的作用,用于程序调试。

使用方法

安装很简单,直接使用pip就行:

1
$ pip install pysnooper

使用也依然简单,我们这里写个简单的运算斐波那契数列的函数进行测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from __future__ import print_function
import pysnooper

@pysnooper.snoop()
def fib(n):
a, b = 0, 1
for i in xrange(n+1):
a, b = b, a + b
return a

def main():
for n in xrange(3):
print(fib(n), end=' ')

if __name__ == '__main__':
main()

可以看到PySnooper输出了函数每条语句的运行时间(方便做性能优化测试)、变量的初始化赋值和修改后的值、行号、代码都进行了输出。最后输出的则是程序运行的结果。

或者,如果您不想追踪整个函数,则可以将相关部分包装在一个with块中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
导入 pysnooper
随机导入

def foo():对于范围(10)中的i
lst = []

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会输出到stderr。如果想要输出到文件,可以在命令行使用2> /my/log/file.log重定向到文件。也可以给装饰器增加一个参数:

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

如果想检查一些非局部变量,也可以使用参数variables:

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

通过参数depth设定调用函数的深度:

1
@pysnooper.snoop(depth=2)

如果stderr和stdout一起输出可能会看不太清楚,可以通过prefix参数给PySnooper的输出结果添加一个前缀,比如:

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

高级用法

使用watch_explode扩展值,以查看他们的所有列表/字典的属性或项目:

@ pysnooper.snoop(watch_explode =(' foo ',' self '))

watch_explode会自动猜测如何根据其类扩展传递给它的表达式。通过使用以下类别之一,您可以更加具体:

1
2
3
4
5
6
7
import pysnooper

@pysnooper.snoop(watch=(
pysnooper.Attrs('x'), # attributes
pysnooper.Keys('y'), # mapping (e.g. dict) items
pysnooper.Indices('z'), # sequence (e.g. list/tuple) items
))

使用exclude参数排除特定的键/属性/索引,例如Attrs(‘x’, exclude=(’_foo’, ‘_bar’))。

在其后添加一个切片,Indices以仅查看该切片中的值,例如Indices(‘z’)[-3:]。

$ export PYSNOOPER_DISABLED = 1 #这使得PySnooper不做任何监听
这将输出如下行:

1
2
Modified var:.. foo[2] = 'whatever'
New var:....... self.baz = 8

以前缀开头所有侦听线,以便为它们轻松grep:

@ pysnooper.snoop(前缀= ' ZZZ ')

删除所有与机器相关的数据(路径,时间戳,内存地址),以便与其他跟踪轻松进行比较:

@ pysnooper.snoop(normalize = True)

在多线程应用程序上,确定在输出中监听到哪个线程:

@ pysnooper.snoop(thread_info = True)

PySnooper支持装饰生成器。

如果您使用来修饰一个类snoop,它将自动将修饰符应用于所有方法。(不包括属性和其他特殊情况。)

您还可以自定义对象的代表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def large(l):
return isinstance(l, list) and len(l) > 5

def print_list_size(l):
return 'list(size={})'.format(len(l))

def print_ndarray(a):
return 'ndarray(shape={}, dtype={})'.format(a.shape, a.dtype)

@pysnooper.snoop(custom_repr=((large, print_list_size), (numpy.ndarray, print_ndarray)))
def sum_to_x(x):
l = list(range(x))
a = numpy.zeros((10,10))
return sum(l)

sum_to_x(10000)

您将获得l = list(size=10000)该列表和a = ndarray(shape=(10, 10), dtype=float64)ndarray。的custom_repr顺序匹配,如果一个条件匹配,将不检查其他条件。

默认情况下,变量和异常被截断为100个字符。您可以自定义:

@ pysnooper.snoop(max_variable_length = 200)

您也可以使用max_variable_length=None它从不截断它们。