Python调试神器–PySnooper
原文链接:http://www.juzicode.com/archives/921
在跟踪调试代码的时候,print()是个非常不错的选择,将要跟踪的变量打印在屏幕上就可以看到变量的变化过程,或者在分支中加入语句就可以看到代码的走向。下面这个例子就是对一个list求和,每加一个元素就将累加结果打印出来:
def add(list):
sum = 0
for l in list:
sum = sum+l
print('sum =',sum)
return sum
if __name__ == '__main__':
list = [1,2,3,4,5]
print('list:',list)
value = add(list)
print('list元素的和:',value)
=====结果:
list: [1, 2, 3, 4, 5]
sum = 1
sum = 3
sum = 6
sum = 10
sum = 15
list元素的和: 15
当代码行数增多,需要跟踪调试的对象增加时,print()语句也会相应地增加,另外在程序发布时过多的print()语句又不是必须的,在部署前就需要将print()语句注释掉,这一加一减的动作就显得有点“不优雅”了。今天介绍一款跟踪代码的神器:pysnooper ,只需要在待调试的代码前加入一行语句就可以跟踪代码的走向,观察变量的变化,甚至还可以跟踪到被调用的其他模块内部。
首先需要使用pip命令安装pysnooper:
pip install pysnooper
仍然参照前面的例子 ,想在累加过程中观察累加结果sum的变化过程,在add函数前加入一行“@pysnooper.snoop()”,然后运行看下效果:
print('-----www.juzicode.com')
print('-----公众号: juzicode/桔子code\n')
import pysnooper
@pysnooper.snoop()
def add(list):
sum = 0
for l in list:
sum = sum+l
#print('sum =',sum)
return sum
if __name__ == '__main__':
list = [1,2,3,4,5]
print('list:',list)
value = add(list)
print('list元素的和:',value)
运行结果:
-----www.juzicode.com
-----公众号: juzicode/桔子code
list: [1, 2, 3, 4, 5]
Source path:... debug-print.py
Starting var:.. list = [1, 2, 3, 4, 5]
01:10:34.336005 call 14 def add(list):
01:10:34.337053 line 15 sum = 0
New var:....... sum = 0
01:10:34.338630 line 16 for l in list:
New var:....... l = 1
01:10:34.340782 line 17 sum = sum+l
Modified var:.. sum = 1
01:10:34.341784 line 16 for l in list:
Modified var:.. l = 2
01:10:34.343776 line 17 sum = sum+l
Modified var:.. sum = 3
01:10:34.345768 line 16 for l in list:
Modified var:.. l = 3
01:10:34.350060 line 17 sum = sum+l
Modified var:.. sum = 6
01:10:34.351061 line 16 for l in list:
Modified var:.. l = 4
01:10:34.352057 line 17 sum = sum+l
Modified var:.. sum = 10
01:10:34.353055 line 16 for l in list:
Modified var:.. l = 5
01:10:34.354051 line 17 sum = sum+l
Modified var:.. sum = 15
01:10:34.355048 line 16 for l in list:
01:10:34.360058 line 19 return sum
01:10:34.361059 return 19 return sum
Return value:.. 15
Elapsed time: 00:00:00.027952
list元素的和: 15:
从上面的打印信息可以看出来,会提示当前运行的是哪个py文件,哪个时间点运行了哪一行代码,每行代码运行后会影响到哪些变量,执行完函数后,会提示函数的返回值是多少,整个函数运行花费的时间,可以说是非常详尽。
当然如果不想观察整个函数,只需要观察某个语句,比如这里的for循环语句,可以在for循环语句前使用with pysnooper.snoop()语句:
import pysnooper
def add(list):
sum = 0
with pysnooper.snoop() :
for l in list:
sum = sum+l
return sum
if __name__ == '__main__':
list = [1,2,3,4,5]
print('list:',list)
value = add(list)
print('list元素的和:',value)
=====结果:
Source path:... debug-pysnooper-with.py
New var:....... list = [1, 2, 3, 4, 5]
New var:....... sum = 0
01:23:39.865211 line 16 for l in list:
New var:....... l = 1
01:23:39.872437 line 17 sum = sum+l
Modified var:.. sum = 1
01:23:39.874431 line 16 for l in list:
Modified var:.. l = 2
01:23:39.875429 line 17 sum = sum+l
Modified var:.. sum = 3
01:23:39.879764 line 16 for l in list:
Modified var:.. l = 3
01:23:39.882048 line 17 sum = sum+l
Modified var:.. sum = 6
01:23:39.883049 line 16 for l in list:
Modified var:.. l = 4
01:23:39.884045 line 17 sum = sum+l
Modified var:.. sum = 10
01:23:39.889042 line 16 for l in list:
Modified var:.. l = 5
01:23:39.891038 line 17 sum = sum+l
Modified var:.. sum = 15
01:23:39.892024 line 16 for l in list:
Elapsed time: 00:00:00.027810
除了在标准输出上打印信息 ,pysnooper还可以将调试信息保存到文件,只需要在snoop()入参中传入保存的文件路径即可:
@pysnooper.snoop('log.txt')
也可以设置depth=n指定要显示或记录的函数调用层次,利用这个特性还可以跟踪到其他被调用模块内部:
@pysnooper.snoop(depth=2)
下面这个例子使用configparser模块读取config.ini文件内容,并获取到info section下drivername的值:
import pysnooper
import configparser
if __name__ == '__main__':
with pysnooper.snoop(depth=1):
config = configparser.ConfigParser() #新建configparser实例
config.read('config.ini')
info_obj = config['info'] #得到一个名为info的section对象
drivername = config['info']['drivername'] #读取key='drivername'的值
pass #如果没有这行pass,最后的drivername不会显示,pysnooper bug
ini文件的内容:
[info]
drivername=usbhub
symbolfile=usbperfsym.h
当depth=1的时候,提示的信息比较少,而且只局限于这个py文件本身:
Source path:... debug-pysnooper-depth.py
New var:....... __name__ = '__main__'
New var:....... __doc__ = '\nauthor: juzicode\naddress: www.juzicode.com\n公众号: juzicode/桔子code\ndate: 2020.7.15\n'
New var:....... __package__ = None
New var:....... __loader__ = <_frozen_importlib_external.SourceFileLoader object at 0x000001F378FD0850>
New var:....... __spec__ = None
New var:....... __annotations__ = {}
New var:....... __builtins__ = <module 'builtins' (built-in)>
New var:....... __file__ = 'debug-pysnooper-depth.py'
New var:....... __cached__ = None
New var:....... pysnooper = <module 'pysnooper' from 'D:\\Python\\Python38\\lib\\site-packages\\pysnooper\\__init__.py'>
New var:....... configparser = <module 'configparser' from 'D:\\Python\\Python38\\lib\\configparser.py'>
01:50:32.927914 line 16 config = configparser.ConfigParser() #新建configparser实例
New var:....... config = <configparser.ConfigParser object at 0x000001F37ABB2190>
01:50:32.949789 line 17 config.read('config.ini')
01:50:32.956771 line 18 info_obj = config['info'] #得到一个名为info的section对象
New var:....... info_obj = <Section: info>
01:50:32.956771 line 19 drivername = config['info']['drivername'] #读取key='drivername'的值
New var:....... drivername = 'usbhub'
01:50:32.959763 line 20 pass
Elapsed time: 00:00:00.037324
当depth=2的时候,就可以看到已经跟踪到 configparser 模块内部了,如果在加大depth的值,可以看到更多的提示信息:
Source path:... debug-pysnooper-depth.py
New var:....... __name__ = '__main__'
New var:....... __doc__ = '\nauthor: juzicode\naddress: www.juzicode.com\n公众号: juzicode/桔子code\ndate: 2020.7.15\n'
New var:....... __package__ = None
New var:....... __loader__ = <_frozen_importlib_external.SourceFileLoader object at 0x0000029B60290850>
New var:....... __spec__ = None
New var:....... __annotations__ = {}
New var:....... __builtins__ = <module 'builtins' (built-in)>
New var:....... __file__ = 'debug-pysnooper-depth.py'
New var:....... __cached__ = None
New var:....... pysnooper = <module 'pysnooper' from 'D:\\Python\\Python38\\lib\\site-packages\\pysnooper\\__init__.py'>
New var:....... configparser = <module 'configparser' from 'D:\\Python\\Python38\\lib\\configparser.py'>
01:53:31.987969 line 16 config = configparser.ConfigParser() #新建configparser实例
Source path:... D:\Python\Python38\lib\configparser.py
Starting var:.. self = <configparser.ConfigParser object at 0x0000029B60362190>
Starting var:.. defaults = None
Starting var:.. dict_type = <class 'dict'>
Starting var:.. allow_no_value = False
Starting var:.. delimiters = ('=', ':')
Starting var:.. comment_prefixes = ('#', ';')
Starting var:.. inline_comment_prefixes = None
Starting var:.. strict = True
Starting var:.. empty_lines_in_values = True
Starting var:.. default_section = 'DEFAULT'
Starting var:.. interpolation = <object object at 0x0000029B60224F20>
Starting var:.. converters = <object object at 0x0000029B60224F20>
01:53:31.992568 call 601 def __init__(self, defaults=None, dict_type=_default_dict,
01:53:32.014033 line 608 self._dict = dict_type
01:53:32.015032 line 609 self._sections = self._dict()
01:53:32.017033 line 610 self._defaults = self._dict()
01:53:32.019045 line 611 self._converters = ConverterMapping(self)
01:53:32.021015 line 612 self._proxies = self._dict()
01:53:32.025003 line 613 self._proxies[default_section] = SectionProxy(self, default_section)
01:53:32.026001 line 614 self._delimiters = tuple(delimiters)
01:53:32.026998 line 615 if delimiters == ('=', ':'):
01:53:32.027995 line 616 self._optcre = self.OPTCRE_NV if allow_no_value else self.OPTCRE
01:53:32.028993 line 625 self._comment_prefixes = tuple(comment_prefixes or ())
01:53:32.035974 line 626 self._inline_comment_prefixes = tuple(inline_comment_prefixes or ())
01:53:32.036971 line 627 self._strict = strict
01:53:32.037974 line 628 self._allow_no_value = allow_no_value
01:53:32.038966 line 629 self._empty_lines_in_values = empty_lines_in_values
01:53:32.039964 line 630 self.default_section=default_section
01:53:32.041958 line 631 self._interpolation = interpolation
01:53:32.046953 line 632 if self._interpolation is _UNSET:
01:53:32.047960 line 633 self._interpolation = self._DEFAULT_INTERPOLATION
01:53:32.048943 line 634 if self._interpolation is None:
01:53:32.049937 line 636 if converters is not _UNSET:
01:53:32.052928 line 638 if defaults:
01:53:32.056918 return 638 if defaults:
Return value:.. None
Source path:... debug-pysnooper-depth.py
New var:....... config = <configparser.ConfigParser object at 0x0000029B60362190>
01:53:32.057930 line 17 config.read('config.ini')
Source path:... D:\Python\Python38\lib\configparser.py
Starting var:.. self = <configparser.ConfigParser object at 0x0000029B60362190>
Starting var:.. filenames = 'config.ini'
Starting var:.. encoding = None
01:53:32.061904 call 679 def read(self, filenames, encoding=None):
01:53:32.071880 line 691 if isinstance(filenames, (str, bytes, os.PathLike)):
01:53:32.076864 line 692 filenames = [filenames]
Modified var:.. filenames = ['config.ini']
01:53:32.077871 line 693 read_ok = []
New var:....... read_ok = []
01:53:32.079856 line 694 for filename in filenames:
New var:....... filename = 'config.ini'
01:53:32.087816 line 695 try:
01:53:32.088810 line 696 with open(filename, encoding=encoding) as fp:
New var:....... fp = <_io.TextIOWrapper name='config.ini' mode='r' encoding='cp936'>
01:53:32.092803 line 697 self._read(fp, filename)
01:53:32.093796 line 700 if isinstance(filename, os.PathLike):
01:53:32.098783 line 702 read_ok.append(filename)
Modified var:.. read_ok = ['config.ini']
01:53:32.098783 line 694 for filename in filenames:
01:53:32.100777 line 703 return read_ok
01:53:32.102772 return 703 return read_ok
Return value:.. ['config.ini']
Source path:... debug-pysnooper-depth.py
01:53:32.108755 line 18 info_obj = config['info'] #得到一个名为info的section对象
Source path:... D:\Python\Python38\lib\configparser.py
Starting var:.. self = <configparser.ConfigParser object at 0x0000029B60362190>
Starting var:.. key = 'info'
01:53:32.115745 call 958 def __getitem__(self, key):
01:53:32.124190 line 959 if key != self.default_section and not self.has_section(key):
01:53:32.128778 line 961 return self._proxies[key]
01:53:32.135776 return 961 return self._proxies[key]
Return value:.. <Section: info>
Source path:... debug-pysnooper-depth.py
New var:....... info_obj = <Section: info>
01:53:32.137753 line 19 drivername = config['info']['drivername'] #读取key='drivername'的值
Source path:... D:\Python\Python38\lib\configparser.py
Starting var:.. self = <configparser.ConfigParser object at 0x0000029B60362190>
Starting var:.. key = 'info'
01:53:32.145733 call 958 def __getitem__(self, key):
01:53:32.156703 line 959 if key != self.default_section and not self.has_section(key):
01:53:32.157700 line 961 return self._proxies[key]
01:53:32.158698 return 961 return self._proxies[key]
Return value:.. <Section: info>
Starting var:.. self = <Section: info>
Starting var:.. key = 'drivername'
01:53:32.161690 call 1252 def __getitem__(self, key):
01:53:32.170668 line 1253 if not self._parser.has_option(self._name, key):
01:53:32.171663 line 1255 return self._parser.get(self._name, key)
01:53:32.172662 return 1255 return self._parser.get(self._name, key)
Return value:.. 'usbhub'
Source path:... debug-pysnooper-depth.py
New var:....... drivername = 'usbhub'
01:53:32.177647 line 20 pass
Elapsed time: 00:00:00.197664
pysnooper使用小结:
- 在要跟踪的代码前使用@装饰语句或者with语句;
- 可以将提示信息显示到标准输出或者写到文件中;
- 用depth参数设定跟踪的深度。
更多精彩在这里: