# 9.7 【调试技巧】使用 PySnopper 调试疑难杂症
对于每个程序开发者来说,调试几乎是必备技能。
代码写到一半卡住了,不知道这个函数执行完的返回结果是怎样的?调试一下看看
代码运行到一半报错了,什么情况?怎么跟预期的不一样?调试一下看看
调试的方法多种多样,不同的调试方法适合不同的场景和人群。
- 如果你是刚接触编程的小萌新,对很多工具的使用还不是很熟练,那么 print 和 log 大法好
- 如果你在本地(Win或者Mac)电脑上开发,那么 IDE 的图形化界面调试无疑是最适合的;
- 如果你在服务器上排查BUG,那么使用 PDB 进行无图形界面的调试应该是首选;
- 如果你要在本地进行开发,但是项目的进行需要依赖复杂的服务器环境,那么可以了解下 PyCharm 的远程调试
除了以上,今天明哥再给你介绍一款非常好用的调试工具,它能在一些场景下,大幅度提高调试的效率, 那就是 `PySnooper`,它在 Github 上已经收到了 13k 的 star,获得大家的一致好评。
**有了这个工具后,就算是小萌新也可以直接无门槛上手,从此与 print 说再见~**
## 1. 快速安装
执行下面这些命令进行安装 PySnooper
```shell
$ python3 -m pip install pysnooper
# 或者
$ conda install -c conda-forge pysnooper
# 或者
$ yay -S python-pysnooper
```
## 2. 简单案例
下面这段代码,定义了一个 demo_func 的函数,在里面生成一个 profile 的字典变量,然后去更新它,最后返回。
代码本身没有什么实际意义,但是用来演示 PySnooper 已经足够。
```python
import pysnooper
@pysnooper.snoop()
def demo_func():
profile = {}
profile["name"] = "写代码的明哥"
profile["age"] = 27
profile["gender"] = "male"
return profile
def main():
profile = demo_func()
main()
```
现在我使用终端命令行的方式来运行它
```shell
[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. 详细使用
### 2.1 重定向到日志文件
`@pysnooper.snoop()` 不加任何参数时,会默认将调试的信息输出到标准输出。
对于单次调试就能解决的 BUG ,这样没有什么问题,但是有一些 BUG 只有在特定的场景下才会出现,需要你把程序放在后面跑个一段时间才能复现。
这种情况下,你可以将调试信息重定向输出到某一日志文件中,方便追溯排查。
```python
@pysnooper.snoop(output='/var/log/debug.log')
def demo_func():
...
```
### 2.2 跟踪非局部变量值
PySnooper 是以函数为单位进行调试的,它默认只会跟踪函数体内的局部变量,若想跟踪全局变量,可以给 `@pysnooper.snoop()` 加上 `watch` 参数
```python
out = {"foo": "bar"}
@pysnooper.snoop(watch=('out["foo"]'))
def demo_func():
...
```
如此一来,PySnooper 会在 `out["foo"]` 值有变化时,也将其打印出来
![](http://image.iswbm.com/20201114183018.png)
watch 参数,接收一个可迭代对象(可以是list 或者 tuple),里面的元素为字符串表达式,什么意思呢?看下面例子就知道了
```python
@pysnooper.snoop(watch=('out["foo"]', 'foo.bar', 'self.foo["bar"]'))
def demo_func():
...
```
和 `watch` 相对的,`pysnooper.snoop()` 还可以接收一个函数 `watch_explode`,表示除了这几个参数外的其他所有全局变量都监控。
```python
@pysnooper.snoop(watch_explode=('foo', 'bar'))
def demo_func():
...
```
### 2.3 设置跟踪函数的深度
当你使用 PySnooper 调试某个函数时,若该函数中还调用了其他函数,PySnooper 是不会傻傻的跟踪进去的。
如果你想继续跟踪该函数中调用的其他函数,可以通过指定 `depth` 参数来设置跟踪深度(不指定的话默认为 1)。
```python
@pysnooper.snoop(depth=2)
def demo_func():
...
```
### 2.4 设置调试日志的前缀
当你在使用 PySnooper 跟踪多个函数时,调试的日志会显得杂乱无章,不方便查看。
在这种情况下,PySnooper 提供了一个参数,方便你为不同的函数设置不同的标志,方便你在查看日志时进行区分。
```python
@pysnooper.snoop(output="/var/log/debug.log", prefix="demo_func: ")
def demo_func():
...
```
效果如下
![](http://image.iswbm.com/20201114193131.png)
### 2.5 设置最大的输出长度
默认情况下,PySnooper 输出的变量和异常信息,如果超过 100 个字符,被会截断为 100 个字符。
当然你也可以通过指定参数 进行修改
```python
@pysnooper.snoop(max_variable_length=200)
def demo_func():
...
```
您也可以使用max_variable_length=None它从不截断它们。
```python
@pysnooper.snoop(max_variable_length=None)
def demo_func():
...
```
### 2.6 支持多线程调试模式
PySnooper 同样支持多线程的调试,通过设置参数 `thread_info=True`,它就会在日志中打印出是在哪个线程对变量进行的修改。
```python
@pysnooper.snoop(thread_info=True)
def demo_func():
...
```
效果如下
![](http://image.iswbm.com/20201114194449.png)
### 2.7 自定义对象的格式输出
`pysnooper.snoop()` 函数有一个参数是 `custom_repr`,它接收一个元组对象。
在这个元组里,你可以指定特定类型的对象以特定格式进行输出。
这边我举个例子。
假如我要跟踪 person 这个 Person 类型的对象,由于它不是常规的 Python 基础类型,PySnooper 是无法正常输出它的信息的。
因此我在 `pysnooper.snoop()` 函数中设置了 `custom_repr` 参数,该参数的第一个元素为 Person,第二个元素为 `print_persion_obj` 函数。
PySnooper 在打印对象的调试信息时,会逐个判断它是否是 Person 类型的对象,若是,就将该对象传入 `print_persion_obj` 函数中,由该函数来决定如何显示这个对象的信息。
```python
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():
...
```
完整的代码如下
```python
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()
```
运行一下,观察一下效果。
![](http://image.iswbm.com/20201114201042.png)
如果你要自定义格式输出的有很多个类型,那么 `custom_repr` 参数的值可以这么写
```python
@pysnooper.snoop(custom_repr=((Person, print_person_obj), (numpy.ndarray, print_ndarray)))
def demo_func():
...
```
还有一点我提醒一下,元组的第一个元素可以是类型(如类名Person 或者其他基础类型 list等),也可以是一个判断对象类型的函数。
也就是说,下面三种写法是等价的。
```python
# 【第一种写法】
@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`) 的详细使用手册,是不是觉得还不错?
- 第一章:安装运行
- 1.1 【环境】快速安装 Python 解释器
- 1.2 【环境】Python 开发环境的搭建
- 1.3 【基础】两种运行 Python 程序方法
- 第二章:数据类型
- 2.1 【基础】常量与变量
- 2.2 【基础】字符串类型
- 2.3 【基础】整数与浮点数
- 2.4 【基础】布尔值:真与假
- 2.5 【基础】学会输入与输出
- 2.6 【基础】字符串格式化
- 2.6 【基础】运算符(超全整理)
- 第三章:数据结构
- 3.1 【基础】列表
- 3.2 【基础】元组
- 3.3 【基础】字典
- 3.4 【基础】集合
- 3.5 【基础】迭代器
- 3.6 【基础】生成器
- 第四章:控制流程
- 4.1 【基础】条件语句:if
- 4.2 【基础】循环语句:for
- 4.3 【基础】循环语句:while
- 4.4 【进阶】五种推导式
- 第五章:学习函数
- 5.1 【基础】普通函数
- 5.2 【基础】匿名函数
- 5.3 【基础】高阶函数
- 5.4 【基础】反射函数
- 5.5 【基础】偏函数
- 5.6 【进阶】泛型函数
- 5.7 【基础】变量的作用域
- 5.8 【进阶】上下文管理器
- 5.9 【进阶】装饰器的六种写法
- 第六章:错误异常
- 6.1 【基础】什么是异常?
- 6.2 【基础】如何抛出和捕获异常?
- 6.3 【基础】如何自定义异常?
- 6.4 【进阶】如何关闭异常自动关联上下文?
- 6.5 【进阶】异常处理的三个好习惯
- 第七章:类与对象
- 7.1 【基础】类的理解与使用
- 7.2 【基础】静态方法与类方法
- 7.3 【基础】私有变量与私有方法
- 7.4 【基础】类的封装(Encapsulation)
- 7.5 【基础】类的继承(Inheritance)
- 7.6 【基础】类的多态(Polymorphism)
- 7.7 【基础】类的 property 属性
- 7.8 【进阶】类的 Mixin 设计模式
- 7.9 【进阶】类的魔术方法(超全整理)
- 7.10 【进阶】神奇的元类编程(metaclass)
- 7.11 【进阶】深藏不露的描述符(Descriptor)
- 第八章:包与模块
- 8.1 【基础】什么是包、模块和库?
- 8.2 【基础】安装第三方包的八种方法
- 8.3 【基础】导入单元的构成
- 8.4 【基础】导入包的标准写法
- 8.5 【进阶】常规包与空间命名包
- 8.6 【进阶】花式导包的八种方法
- 8.7 【进阶】包导入的三个冷门知识点
- 8.8 【基础】pip 的超全使用指南
- 8.9 【进阶】理解模块的缓存
- 8.10 【进阶】理解查找器与加载器
- 8.11 【进阶】实现远程导入模块
- 8.12 【基础】分发工具:distutils和setuptools
- 8.13 【基础】源码包与二进制包有什么区别?
- 8.14 【基础】eggs与wheels 有什么区别?
- 8.15 【进阶】超详细讲解 setup.py 的编写
- 8.16 【进阶】打包辅助神器 PBR 是什么?
- 8.17 【进阶】开源自己的包到 PYPI 上
- 第九章:调试技巧
- 9.1 【调试技巧】超详细图文教你调试代码
- 9.2 【调试技巧】PyCharm 中指定参数调试程序
- 9.3 【调试技巧】PyCharm跑完后立即进入调试模式
- 9.4 【调试技巧】脚本报错后立即进入调试模式
- 9.5 【调试技巧】使用 PDB 进行无界面调试
- 9.6 【调试技巧】如何调试已经运行的程序?
- 9.7 【调试技巧】使用 PySnopper 调试疑难杂症
- 9.8 【调试技巧】使用 PyCharm 进行远程调试
- 第十章:并发编程
- 10.1 【并发编程】从性能角度初探并发编程
- 10.2 【并发编程】创建多线程的几种方法
- 10.3 【并发编程】谈谈线程中的“锁机制”
- 10.4 【并发编程】线程消息通信机制
- 10.5 【并发编程】线程中的信息隔离
- 10.6 【并发编程】线程池创建的几种方法
- 10.7 【并发编程】从 yield 开始入门协程
- 10.8 【并发编程】深入理解yield from语法
- 10.9 【并发编程】初识异步IO框架:asyncio 上篇
- 10.10 【并发编程】深入异步IO框架:asyncio 中篇
- 10.11 【并发编程】实战异步IO框架:asyncio 下篇
- 10.12 【并发编程】生成器与协程,你分清了吗?
- 10.14 【并发编程】浅谈线程安全那些事儿
- 第十二章:虚拟环境
- 12.1 【虚拟环境】为什么要有虚拟环境?
- 12.2 【虚拟环境】方案一:使用 virtualenv
- 12.3 【虚拟环境】方案二:使用 pipenv
- 12.4 【虚拟环境】方案三:使用 pipx
- 12.5 【虚拟环境】方案四:使用 poetry
- 第十三章:绝佳工具
- 13.1 【静态检查】mypy 的使用
- 13.2 【代码测试】pytest 的使用
- 13.3 【代码提交】pre-commit hook
- 13.4 【项目生成】cookiecutter 的使用
- 第十四章:数据可视化
- 14.1 【可视化之matplotlib】一图带你入门matplotlib
- 14.2 【可视化之matplotlib】详解六种可视化图表
- 14.3 【可视化之matplotlib】 绘制正余弦函数图象
- 14.4 【可视化之matplotlib】难点:子图与子区
- 14.5 【可视化之matplotlib】绘制酷炫的gif动态图
- 14.6 【可视化之matplotlib】自动生成图像视频
- 14.7 【可视化神器】最高级的可视化神器: plotly_express