前言
在程序开发过程中,代码的运行往往会和我们预期的结果有所差别。于是,我们需要清楚代码运行过程中到底发生了什么?代码哪些模块运行了,哪些模块没有运行?输出的局部变量是什么样的?很多时候,我们会想到选择使用成熟、常用的IDE使用断点和watches调试程序,或者更为基础的print函数、log打印出局部变量来查看是否符合我们预期的执行效果。但是这些方法都有一个共同的弱点--效率低且繁琐,本文就介绍一个堪称神器的Python调试工具PySnooper,能够大大减少调试过程中的工作量。
装饰器
装饰器(Decorators)是Python里一个很重要的概念,它能够使得Python代码更加简洁,用一句话概括:装饰器是修改其他函数功能的函数。PySnooper的调用主要依靠装饰器的方式,所以,了解装饰器的基本概念和使用方法更有助于理解PySnooper的使用。在这里,我先简单介绍一下装饰器的使用,如果精力有限,了解装饰器的调用方式即可。
对于Python,一切都是对象,一个函数可以作为一个对象在不同模块之间进行传递,举个例子,
1def one(func): 2 print("now you are in function one.") 3 func() 4 5 6def two(): 7 print("now you are in function two") 8 9 10one(two) 11 12# 输出 13>>> now you are in function one. 14>>> now you are in function two
其实这就是装饰器的核心所在,它们封装一个函数,可以用这样或那样的方式来修改它。换一种方式表达上述调用,可以用@+函数名来装饰一个函数。
1defone(func):
2 print("now you are in function one.")
3 defwarp():
4 func()
5 return warp
6
7
8@one
9deftwo():
10 print("now you are in function two.")
11
12
13two()
14
15# 输出
16>>> now you are in function one.
17>>> now you are in function two.
此外,在调用装饰器时还可以给函数传入参数:
1def one(func): 2 print("now you are in function one.") 3 def warp(*args): 4 func(*args) 5 return warp 6 7 8@one 9def two(x, y): 10 print("now you are in function two.") 11 print("x value is %d, y value is %d" % (x, y)) 12 13 14two(5, 6) 15 16# 输出 17>>> now you are in function one. 18>>> now you are in function two. 19>>> x value is 5, y value is 6
另外,装饰器本身也可以接收参数,
1def three(text): 2 def one(func): 3 print("now you are in function one.") 4 def warp(*args): 5 func(*args) 6 return warp 7 print("input params is {}".format(text)) 8 return one 9 10 11@three(text=5) 12def two(x, y): 13 print("now you are in function two.") 14 print("x value is %d, y value is %d" % (x, y)) 15 16 17two(5, 6) 18 19# 输出 20>>> input params is 5 21>>> now you are in function one. 22>>> now you are in function two. 23>>> x value is 5, y value is 6
上面讲述的就是Python装饰器的一些常用方法。
PySnooper
调试程序对于大多数开发者来说是一项必不可少的工作,当我们想要知道代码是否按照预期的效果在执行时,我们会想到去输出一下局部变量与预期的进行比对。目前大多数采用的方法主要有以下几种:
- Print函数
- Log日志
- IDE调试器
但是这些方法有着无法忽视的弱点:
- 繁琐
- 过度依赖工具
"PySnooper is a poor man's debugger."
有了PySnooper,上述问题都迎刃而解,因为PySnooper实在太简洁了,目前在github已经10k+star。
前面花了一些篇幅讲解装饰器其实就是为了PySnooper做铺垫,PySnooper的调用就是通过装饰器的方式进行使用,非常简洁。
PySnooper的调用方式就是通过@pysnooper.snoop的方式进行使用,该装饰器可以传入一些参数来实现一些目的,具体如下:
参数 | 描述 |
None | 输出日志到控制台 |
filePath | 输出到日志文件,例如'log/file.log' |
prefix | 给调试的行加前缀,便于识别 |
watch | 查看一些非局部变量表达式的值 |
watch_explode | 展开值用以查看列表/字典的所有属性或项 |
depth | 显示函数调用的函数的snoop行 |
安装
使用pip安装,
pip install pysnooper
或者使用conda安装,
conda install -c conda-forge pysnooper
使用
先写一个简单的例子,
1import numpy as np 2import pysnooper 3 4 5@pysnooper.snoop() 6def one(number): 7 mat = [] 8 while number: 9 mat.append(np.random.normal(0, 1)) 10 number -= 1 11 return mat 12 13 14one(3) 15 16# 输出 17 18Starting var:.. number = 3 1922:17:10.634566 call 6 def one(number): 2022:17:10.634566 line 7 mat = [] 21New var:....... mat = [] 2222:17:10.634566 line 8 while number: 2322:17:10.634566 line 9 mat.append(np.random.normal(0, 1)) 24Modified var:.. mat = [-0.4142847169210746] 2522:17:10.634566 line 10 number -= 1 26Modified var:.. number = 2 2722:17:10.634566 line 8 while number: 2822:17:10.634566 line 9 mat.append(np.random.normal(0, 1)) 29Modified var:.. mat = [-0.4142847169210746, -0.479901983375219] 3022:17:10.634566 line 10 number -= 1 31Modified var:.. number = 1 3222:17:10.634566 line 8 while number: 3322:17:10.634566 line 9 mat.append(np.random.normal(0, 1)) 34Modified var:.. mat = [-0.4142847169210746, -0.479901983375219, 1.0491540468063252] 3522:17:10.634566 line 10 number -= 1 36Modified var:.. number = 0 3722:17:10.634566 line 8 while number: 3822:17:10.634566 line 11 return mat 3922:17:10.634566 return 11 return mat 40Return value:.. [-0.4142847169210746, -0.479901983375219, 1.0491540468063252]
这是最简单的使用方法,从上述输出结果可以看出,PySnooper输出信息主要包括以下几个部分:
- 局部变量值
- 代码片段
- 局部变量所在行号
- 返回结果
也就是说,把我们日常DEBUG所需要的主要信息都被输出了。
上述结果输出到控制台,还可以把日志输出到文件,
1@pysnooper.snoop("debug.log")
在函数调用过程中,PySnooper还能够显示函数的层次关系,使得一目了然,
1@pysnooper.snoop() 2def two(x, y): 3 z = x + y 4 return z 5 6 7@pysnooper.snoop() 8def one(number): 9 k = 0 10 while number: 11 k = two(k, number) 12 number -= 1 13 return number 14 15 16one(3) 17 18# 输出 19Starting var:.. number = 3 2022:26:08.185204 call 12 def one(number): 2122:26:08.186202 line 13 k = 0 22New var:....... k = 0 2322:26:08.186202 line 14 while number: 2422:26:08.186202 line 15 k = two(k, number) 25 Starting var:.. x = 0 26 Starting var:.. y = 3 27 22:26:08.186202 call 6 def two(x, y): 28 22:26:08.186202 line 7 z = x + y 29 New var:....... z = 3 30 22:26:08.186202 line 8 return z 31 22:26:08.186202 return 8 return z 32 Return value:.. 3 33Modified var:.. k = 3 3422:26:08.186202 line 16 number -= 1 35Modified var:.. number = 2 3622:26:08.186202 line 14 while number: 3722:26:08.186202 line 15 k = two(k, number) 38 Starting var:.. x = 3 39 Starting var:.. y = 2 40 22:26:08.186202 call 6 def two(x, y): 41 22:26:08.186202 line 7 z = x + y 42 New var:....... z = 5 43 22:26:08.186202 line 8 return z 44 22:26:08.186202 return 8 return z 45 Return value:.. 5 46Modified var:.. k = 5 4722:26:08.186202 line 16 number -= 1 48Modified var:.. number = 1 4922:26:08.186202 line 14 while number: 5022:26:08.186202 line 15 k = two(k, number) 51 Starting var:.. x = 5 52 Starting var:.. y = 1 53 22:26:08.186202 call 6 def two(x, y): 54 22:26:08.186202 line 7 z = x + y 55 New var:....... z = 6 56 22:26:08.186202 line 8 return z 57 22:26:08.186202 return 8 return z 58 Return value:.. 6 59Modified var:.. k = 6 6022:26:08.186202 line 16 number -= 1 61Modified var:.. number = 0 6222:26:08.186202 line 14 while number: 6322:26:08.186202 line 17 return number 6422:26:08.186202 return 17 return number 65Return value:.. 0
从上述输出结果可以看出,函数one与函数two的输出缩进层次一目了然。
除了缩进之外,PySnooper还提供了参数prefix给debug信息添加前缀的方式便于识别,
1@pysnooper.snoop(prefix="funcTwo ") 2def two(x, y): 3 z = x + y 4 return z 5 6 7@pysnooper.snoop(prefix="funcOne ") 8def one(number): 9 k = 0 10 while number: 11 k = two(k, number) 12 number -= 1 13 return number 14 15 16one(3) 17 18# 输出 19funcOne Starting var:.. number = 3 20funcOne 22:28:14.259212 call 12 def one(number): 21funcOne 22:28:14.260211 line 13 k = 0 22funcOne New var:....... k = 0 23funcOne 22:28:14.260211 line 14 while number: 24funcOne 22:28:14.260211 line 15 k = two(k, number) 25funcTwo Starting var:.. x = 0 26funcTwo Starting var:.. y = 3 27funcTwo 22:28:14.260211 call 6 def two(x, y): 28funcTwo 22:28:14.260211 line 7 z = x + y 29funcTwo New var:....... z = 3 30funcTwo 22:28:14.260211 line 8 return z 31funcTwo 22:28:14.260211 return 8 return z 32funcTwo Return value:.. 3 33funcOne Modified var:.. k = 3 34funcOne 22:28:14.260211 line 16 number -= 1 35funcOne Modified var:.. number = 2 36funcOne 22:28:14.260211 line 14 while number: 37funcOne 22:28:14.260211 line 15 k = two(k, number) 38funcTwo Starting var:.. x = 3 39funcTwo Starting var:.. y = 2 40funcTwo 22:28:14.260211 call 6 def two(x, y): 41funcTwo 22:28:14.260211 line 7 z = x + y 42funcTwo New var:....... z = 5 43funcTwo 22:28:14.260211 line 8 return z 44funcTwo 22:28:14.260211 return 8 return z 45funcTwo Return value:.. 5 46funcOne Modified var:.. k = 5 47funcOne 22:28:14.260211 line 16 number -= 1 48funcOne Modified var:.. number = 1 49funcOne 22:28:14.260211 line 14 while number: 50funcOne 22:28:14.260211 line 15 k = two(k, number) 51funcTwo Starting var:.. x = 5 52funcTwo Starting var:.. y = 1 53funcTwo 22:28:14.260211 call 6 def two(x, y): 54funcTwo 22:28:14.260211 line 7 z = x + y 55funcTwo New var:....... z = 6 56funcTwo 22:28:14.260211 line 8 return z 57funcTwo 22:28:14.260211 return 8 return z 58funcTwo Return value:.. 6 59funcOne Modified var:.. k = 6 60funcOne 22:28:14.260211 line 16 number -= 1 61funcOne Modified var:.. number = 0 62funcOne 22:28:14.260211 line 14 while number: 63funcOne 22:28:14.260211 line 17 return number 64funcOne 22:28:14.260211 return 17 return number 65funcOne Return value:.. 0
参数watch可以用于查看一些非局部变量,例如,
1class Test(): 2 t = 10 3 4 5test = Test() 6 7 8@pysnooper.snoop(watch=("test.t", "x")) 9 10# 输出 11Starting var:.. number = 3 12Starting var:.. test.t = 10 13Starting var:.. x = 10
参数watch_explode可以展开字典或者列表显示它的所有属性值,对比一下它和watch的区别,
1#### watch_explode #### 2d = { 3 "one": 1, 4 "two": 1 5} 6 7 8@pysnooper.snoop(watch_explode="d") 9 10# 输出 11Starting var:.. number = 3 12Starting var:.. d = {'one': 1, 'two': 1} 13Starting var:.. d['one'] = 1 14Starting var:.. d['two'] = 1 15 16#### watch #### 17d = { 18 "one": 1, 19 "two": 1 20} 21 22 23@pysnooper.snoop(watch="d") 24 25# 输出 26Starting var:.. d = {'one': 1, 'two': 1}
可以看出watch_explode能够展开字典的属性值。
另外还有参数depth显示函数中调用函数的snoop行,默认值为1,参数值需要大于或等于1。