Python调试利器:告别Print,使用PySnooper高效追踪代码执行
本文介绍Python调试工具PySnooper,一款基于装饰器的高效代码追踪库。通过简单的@pysnooper.snoop()装饰器,开发者无需手动添加print语句即可自动记录函数调用、变量变更、返回值及耗时。文章详细演示了如何重定向日志、监控全局变量、设置跟踪深度、自定义输出格式及支持多线程调试,帮助开发者快速定位Bug,提升Python开发效率。
推荐一本由作者编写的电子书《PyCharm中文指南》,书中通过GIF动图直观展示了各种PyCharm高效使用技巧。感兴趣的朋友可以参考其在线文档:http://pycharm.iswbm.com

对于每一位程序开发人员而言,调试代码几乎是不可或缺的核心技能。
当代码编写中途遇到瓶颈,不确定某个函数执行后的返回结果时,通过调试查看具体状态是一个有效手段。
若代码运行至中途抛出异常,且实际表现与预期不符,通过调试排查问题原因也是常见做法。
调试手段丰富多样,不同的调试方法适用于不同的应用场景及开发者群体。
- 如果是刚入门编程的新手,对各类专业调试工具尚不熟悉,那么使用print和log打印大法往往是首选方案。
- 若在本地Windows或Mac电脑上开发项目,利用IDE提供的图形化界面进行调试无疑是最为便捷的选择;
- 若需在服务器上排查Bug,使用PDB进行无图形界面的命令行调试则更为合适;
- 若在本地开发但项目依赖复杂的服务器环境,可以尝试了解PyCharm的远程调试功能。
除了上述方法,今天再推荐一款能显著提升特定场景下调试效率的工具,即PySnooper。该工具在Github上已获得13k的star,备受开发者好评。
借助这款工具,即使是编程新手也能轻松上手,从此告别繁琐的print调试方式。
1. 快速安装
执行以下命令即可安装PySnooper:
$ python3 -m pip install pysnooper
# 或者
$ conda install -c conda-forge pysnooper
# 或者
$ yay -S python-pysnooper
2. 简单案例
以下代码定义了一个名为demo_func的函数,内部生成一个名为profile的字典变量,对其进行更新操作后返回。
该代码虽无实际业务意义,但足以用于演示PySnooper的功能。
import pysnooper
@pysnooper.snoop()
def demo_func():
profile = {}
profile["name"] = "写代码的明哥"
profile["age"] = 27
profile["gender"] = "male"
return profile
def main():
profile = demo_func()
main()
现在通过终端命令行方式运行该脚本。
[root@iswbm ~]# python3 demo.py
Source path:... demo.py
17:52:49.624943 call 4 def demo_func():
17:52:49.625124 line 5 profile = {}
New var:....... profile = {}
17:52:49.625156 line 6 profile["name"] = "写代码的明哥"
Modified var:.. profile = {'name': '写代码的明哥'}
17:52:49.625207 line 7 profile["age"] = 27
Modified var:.. profile = {'name': '写代码的明哥', 'age': 27}
17:52:49.625254 line 8 profile["gender"] = "male"
Modified var:.. profile = {'name': '写代码的明哥', 'age': 27, 'gender': 'male'}
17:52:49.625306 line 10 return profile
17:52:49.625344 return 10 return profile
Return value:.. {'name': '写代码的明哥', 'age': 27, 'gender': 'male'}
Elapsed time: 00:00:00.000486
可以看到,PySnooper完整记录了函数运行的全过程,包括:
代码片段、行号等信息,以及每一行代码的具体调用时间。
函数内部局部变量的值是如何变化的?何时新增变量,何时修改变量,均被清晰记录。
函数的最终返回值是什么?
运行该函数消耗了多少时间?
作为开发者,要获取如此详细的调试信息,操作极其简单:只需在目标函数上添加一个装饰器——@pysnooper.snoop()即可。
3. 详细使用
3.1 重定向到日志文件
当@pysnooper.snoop()不加任何参数时,默认将调试信息输出到标准输出(控制台)。
对于单次调试即可解决的Bug,这种方式没有问题;但有些Bug仅在特定场景下出现,需要将程序运行一段时间才能复现。
这种情况下,可以将调试信息重定向输出到指定的日志文件中,便于后续追溯和排查。
@pysnooper.snoop(output='/var/log/debug.log')
def demo_func():
...
3.2 跟踪非局部变量值
PySnooper以函数为单位进行调试,默认仅跟踪函数体内的局部变量。若想跟踪全局变量,可以给@pysnooper.snoop()加上watch参数。
out = {"foo": "bar"}
@pysnooper.snoop(watch=('out["foo"]'))
def demo_func():
...
如此一来,当out["foo"]的值发生变化时,PySnooper也会将其打印出来。

watch参数接收一个可迭代对象(如list或tuple),其中的元素为字符串表达式,具体含义见下方示例。
@pysnooper.snoop(watch=('out["foo"]', 'foo.bar', 'self.foo["bar"]'))
def demo_func():
...
与watch相对,pysnooper.snoop()还可以接收一个函数watch_explode,表示除了指定参数外的其他所有全局变量都将受到监控。
@pysnooper.snoop(watch_explode=('foo', 'bar'))
def demo_func():
...
3.3 设置跟踪函数的深度
当使用PySnooper调试某个函数时,若该函数内部调用了其他函数,PySnooper默认不会深入跟踪这些被调用的函数。
若想继续跟踪函数内部调用的其他函数,可以通过指定depth参数来设置跟踪深度(不指定时默认为1)。
@pysnooper.snoop(depth=2)
def demo_func():
...
3.4 设置调试日志的前缀
当使用PySnooper跟踪多个函数时,调试日志可能会显得杂乱无章,不便查阅。
为此,PySnooper提供了一个参数,允许为不同的函数设置不同的前缀标志,便于在查看日志时进行区分。
@pysnooper.snoop(output="/var/log/debug.log", prefix="demo_func: ")
def demo_func():
...
效果如下所示。

3.5 设置最大的输出长度
默认情况下,若PySnooper输出的变量或异常信息超过100个字符,将被截断为100个字符。
当然,你也可以通过指定参数来修改这一限制。
@pysnooper.snoop(max_variable_length=200)
def demo_func():
...
您也可以使用max_variable_length=None来设置从不截断输出内容。
@pysnooper.snoop(max_variable_length=None)
def demo_func():
...
3.6 支持多线程调试模式
PySnooper同样支持多线程调试,通过设置参数thread_info=True,日志中将打印出是哪个线程对变量进行了修改。
@pysnooper.snoop(thread_info=True)
def demo_func():
...
效果如下所示。

3.7 自定义对象的格式输出
pysnooper.snoop()函数包含一个参数custom_repr,它接收一个元组对象。
在该元组中,你可以指定特定类型的对象以特定的格式进行输出。
这里举一个例子。
假设我要跟踪一个名为person的Person类型对象,由于它不是常规的Python基础类型,PySnooper无法直接正常输出其信息。
因此我在pysnooper.snoop()函数中设置了custom_repr参数,该参数的第一个元素为Person类,第二个元素为print_persion_obj函数。
PySnooper在打印对象调试信息时,会逐个判断对象是否为Person类型,若是,则将该对象传入print_persion_obj函数,由该函数决定如何显示该对象的信息。
class Person:pass
def print_person_obj(obj):
return f"<Person {obj.name} {obj.age} {obj.gender}>"
@pysnooper.snoop(custom_repr=(Person, print_person_obj))
def demo_func():
...
完整代码如下所示。
import pysnooper
class Person:pass
def print_person_obj(obj):
return f"<Person {obj.name} {obj.age} {obj.gender}>"
@pysnooper.snoop(custom_repr=(Person, print_person_obj))
def demo_func():
person = Person()
person.name = "写代码的明哥"
person.age = 27
person.gender = "male"
return person
def main():
profile = demo_func()
main()
运行一下,观察输出效果。

若需要自定义格式输出的类型较多,那么custom_repr参数的值可以按如下方式编写。
@pysnooper.snoop(custom_repr=((Person, print_person_obj), (numpy.ndarray, print_ndarray)))
def demo_func():
...
另外提醒一点,元组的第一个元素可以是类型(如类名Person或其他基础类型list等),也可以是一个用于判断对象类型的函数。
也就是说,以下三种写法是等价的。
# 【第一种写法】
@pysnooper.snoop(custom_repr=(Person, print_persion_obj))
def demo_func():
...
# 【第二种写法】
def is_persion_obj(obj):
return isinstance(obj, Person)
@pysnooper.snoop(custom_repr=(is_persion_obj, print_persion_obj))
def demo_func():
...
# 【第三种写法】
@pysnooper.snoop(custom_repr=(lambda obj: isinstance(obj, Person), print_persion_obj))
def demo_func():
...
以上就是今天介绍的调试神器(PySnooper)的详细使用手册,是否觉得非常实用?
更多关于Python代码调试的资料,请关注本站其他相关文章!
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













