Python调试利器:告别Print,使用PySnooper高效追踪代码执行

七月
七月
发布时间:2026-10-14| 5人浏览过
原创

本文介绍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代码调试的资料,请关注本站其他相关文章!

相关标签:

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。