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使用小结:

  1. 在要跟踪的代码前使用@装饰语句或者with语句;
  2. 可以将提示信息显示到标准输出或者写到文件中;
  3. 用depth参数设定跟踪的深度。

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注