PySnooper

不要再使用 print 进行调试。(Never use print for debugging again)

Github stars Tracking Chart

PySnooper -- 永远不要再使用 print 进行调试

PySnooper 是一个穷人的调试器。

您试图弄清楚为什么您的 Python 代码没有按照您认为的那样做。您希望使用带有断点和 watches 的成熟调试器,但是现在还不想设置这样的调试器。

您想知道哪些行正在运行,哪些没有运行,以及局部变量的值是什么。

大多数人会在关键部分使用打印行(print lines),其中一些显示变量的值。

PySnooper 允许您执行相同的操作,只是您只需向感兴趣的函数添加一个装饰器行,而不是小心地创建正确的打印行。您将得到函数的详细日志,包括运行了哪些行、何时运行以及何时更改了局部变量。

是什么让 PySnooper 从所有其他代码智能工具中脱颖而出?您可以在糟糕、庞大的企业代码库中使用它,而无需进行任何设置。只需打开装饰器,如下所示,并通过将其路径指定为第一个参数,将输出重定向到专用日志文件。

示例

我们正在编写一个函数,通过返回一个位列表将数字转换为二进制。让我们通过添加@pysnooper.snoop() 装饰器来窥探它:

import pysnooper
@pysnooper.snoop()
def number_to_bits(number):
    if number:
        bits = []
        while number:
            number, remainder = divmod(number, 2)
            bits.insert(0, remainder)
        return bits
    else:
        return [0]
number_to_bits(6)

stderr 的输出是

Source path:... /my_code/foo.py
Starting var:.. number = 6
15:29:11.327032 call         4 def number_to_bits(number):
15:29:11.327032 line         5     if number:
15:29:11.327032 line         6         bits = []
New var:....... bits = []
15:29:11.327032 line         7         while number:
15:29:11.327032 line         8             number, remainder = divmod(number, 2)
New var:....... remainder = 0
Modified var:.. number = 3
15:29:11.327032 line         9             bits.insert(0, remainder)
Modified var:.. bits = [0]
15:29:11.327032 line         7         while number:
15:29:11.327032 line         8             number, remainder = divmod(number, 2)
Modified var:.. number = 1
Modified var:.. remainder = 1
15:29:11.327032 line         9             bits.insert(0, remainder)
Modified var:.. bits = [1, 0]
15:29:11.327032 line         7         while number:
15:29:11.327032 line         8             number, remainder = divmod(number, 2)
Modified var:.. number = 0
15:29:11.327032 line         9             bits.insert(0, remainder)
Modified var:.. bits = [1, 1, 0]
15:29:11.327032 line         7         while number:
15:29:11.327032 line        10         return bits
15:29:11.327032 return      10         return bits
Return value:.. [1, 1, 0]

或者,如果不想跟踪整个函数,可以将相关部分封装在一个 with 块中

import pysnooper
import random
def foo():
    lst = []
    for i in range(10):
        lst.append(random.randrange(1, 1000))
    with pysnooper.snoop():
        lower = min(lst)
        upper = max(lst)
        mid = (lower + upper) / 2
        print(lower, mid, upper)
foo()

输出类似于

New var:....... i = 9
New var:....... lst = [681, 267, 74, 832, 284, 678, ...]
09:37:35.881721 line        10         lower = min(lst)
New var:....... lower = 74
09:37:35.882137 line        11         upper = max(lst)
New var:....... upper = 832
09:37:35.882304 line        12         mid = (lower + upper) / 2
74 453.0 832
New var:....... mid = 453.0
09:37:35.882486 line        13         print(lower, mid, upper)

特性

如果您无法轻松访问 stderr,则可以将输出重定向到文件:

@pysnooper.snoop('/my/log/file.log')

你也可以传递一个流或一个可调用的,它们将被使用。

查看一些非局部变量的表达式的值:

@pysnooper.snoop(watch=('foo.bar', 'self.x["whatever"]'))

展开值以查看其所有属性或列表/词典项:

@pysnooper.snoop(watch_explode=('foo', 'self'))

这将输出如下行:

Modified var:.. foo[2] = 'whatever'
New var:....... self.baz = 8

(有关更多控制,请参阅高级用法

显示函数调用的函数的 snoop 行:

@pysnooper.snoop(depth=2)

使用前缀启动所有 snoop 行,以便轻松地为它们 grep:

@pysnooper.snoop(prefix='ZZZ ')

在多线程应用程序上,识别输出中窥探的线程:

@pysnooper.snoop(thread_info=True)

PySnooper 支持装饰生成器。

您还可以自定义对象的 repr:

def large(l):
    return isinstance(l, list) and len(l) > 5

def print_list_size(l):
    return 'list(size={})'.format(len(l))

def print_ndarray(a):
    return 'ndarray(shape={}, dtype={})'.format(a.shape, a.dtype)

@pysnooper.snoop(custom_repr=((large, print_list_size), (numpy.ndarray, print_ndarray)))
def sum_to_x(x):
    l = list(range(x))
    a = numpy.zeros((10,10))
    return sum(l)

sum_to_x(10000)

您将获得列表的 l = list(size=10000),以及 ndarray 的 a = ndarray(shape=(10, 10), dtype=float64)。 custom_repr 按顺序匹配,如果一个条件匹配,则不会检查其他条件。

安装

您可以通过以下方式安装PySnooper:

  • pip:
$ pip install pysnooper
  • conda with conda-forge channel:
$ conda install -c conda-forge pysnooper

高级使用

watch_explode 将根据其类自动猜测如何扩展传递给它的表达式。 您可以使用以下类之一来更加具体:

import pysnooper

@pysnooper.snoop(watch=(
    pysnooper.Attrs('x'),    # attributes
    pysnooper.Keys('y'),     # mapping (e.g. dict) items
    pysnooper.Indices('z'),  # sequence (e.g. list/tuple) items
))

使用 exclude 参数排除特定键/属性/索引,例如 Attrs('x', exclude=('_foo', '_bar'))。

在“索引”后添加切片,仅查看该切片中的值,例如 Indices('z')[-3:]。

$ export PYSNOOPER_DISABLED=1 # 这使 PySnooper 不做任何窥探

许可

版权所有(c)2019 Ram Rachum 和合作者,根据 MIT 许可证发布。

我在 Python 和 Django 中提供开发服务,并且我提供 Python 研讨会来教授人们 Python 和相关主题。

媒体报道

Hacker News thread and /r/Python Reddit thread (22 April 2019)

(First edition: vz edited at 2019.08.24)

Main metrics

Overview
Name With Ownercool-RR/PySnooper
Primary LanguagePython
Program languagePython (Language Count: 2)
PlatformLinux, Mac, Windows
License:MIT License
所有者活动
Created At2019-04-18 13:55:43
Pushed At2025-04-10 20:56:25
Last Commit At2025-04-10 23:56:22
Release Count20
Last Release Name1.2.2 (Posted on )
First Release Name0.1.0 (Posted on )
用户参与
Stargazers Count16.5k
Watchers Count230
Fork Count0.9k
Commits Count325
Has Issues Enabled
Issues Count135
Issue Open Count27
Pull Requests Count72
Pull Requests Open Count2
Pull Requests Close Count47
项目设置
Has Wiki Enabled
Is Archived
Is Fork
Is Locked
Is Mirror
Is Private

PySnooper - Never use print for debugging again

Travis CI

PySnooper is a poor man's debugger.

You're trying to figure out why your Python code isn't doing what you think it should be doing. You'd love to use a full-fledged debugger with breakpoints and watches, but you can't be bothered to set one up right now.

You want to know which lines are running and which aren't, and what the values of the local variables are.

Most people would use print lines, in strategic locations, some of them showing the values of variables.

PySnooper lets you do the same, except instead of carefully crafting the right print lines, you just add one decorator line to the function you're interested in. You'll get a play-by-play log of your function, including which lines ran and when, and exactly when local variables were changed.

What makes PySnooper stand out from all other code intelligence tools? You can use it in your shitty, sprawling enterprise codebase without having to do any setup. Just slap the decorator on, as shown below, and redirect the output to a dedicated log file by specifying its path as the first argument.

Example

We're writing a function that converts a number to binary, by returning a list of bits. Let's snoop on it by adding the @pysnooper.snoop() decorator:

import pysnooper

@pysnooper.snoop()
def number_to_bits(number):
    if number:
        bits = []
        while number:
            number, remainder = divmod(number, 2)
            bits.insert(0, remainder)
        return bits
    else:
        return [0]

number_to_bits(6)

The output to stderr is:

Source path:... /my_code/foo.py
Starting var:.. number = 6
15:29:11.327032 call         4 def number_to_bits(number):
15:29:11.327032 line         5     if number:
15:29:11.327032 line         6         bits = []
New var:....... bits = []
15:29:11.327032 line         7         while number:
15:29:11.327032 line         8             number, remainder = divmod(number, 2)
New var:....... remainder = 0
Modified var:.. number = 3
15:29:11.327032 line         9             bits.insert(0, remainder)
Modified var:.. bits = [0]
15:29:11.327032 line         7         while number:
15:29:11.327032 line         8             number, remainder = divmod(number, 2)
Modified var:.. number = 1
Modified var:.. remainder = 1
15:29:11.327032 line         9             bits.insert(0, remainder)
Modified var:.. bits = [1, 0]
15:29:11.327032 line         7         while number:
15:29:11.327032 line         8             number, remainder = divmod(number, 2)
Modified var:.. number = 0
15:29:11.327032 line         9             bits.insert(0, remainder)
Modified var:.. bits = [1, 1, 0]
15:29:11.327032 line         7         while number:
15:29:11.327032 line        10         return bits
15:29:11.327032 return      10         return bits
Return value:.. [1, 1, 0]

Or if you don't want to trace an entire function, you can wrap the relevant part in a with block:

import pysnooper
import random

def foo():
    lst = []
    for i in range(10):
        lst.append(random.randrange(1, 1000))

    with pysnooper.snoop():
        lower = min(lst)
        upper = max(lst)
        mid = (lower + upper) / 2
        print(lower, mid, upper)

foo()

which outputs something like:

New var:....... i = 9
New var:....... lst = [681, 267, 74, 832, 284, 678, ...]
09:37:35.881721 line        10         lower = min(lst)
New var:....... lower = 74
09:37:35.882137 line        11         upper = max(lst)
New var:....... upper = 832
09:37:35.882304 line        12         mid = (lower + upper) / 2
74 453.0 832
New var:....... mid = 453.0
09:37:35.882486 line        13         print(lower, mid, upper)

Features

If stderr is not easily accessible for you, you can redirect the output to a file:

@pysnooper.snoop('/my/log/file.log')

You can also pass a stream or a callable instead, and they'll be used.

See values of some expressions that aren't local variables:

@pysnooper.snoop(watch=('foo.bar', 'self.x["whatever"]'))

Show snoop lines for functions that your function calls:

@pysnooper.snoop(depth=2)

See Advanced Usage for more options. <------

Installation

You can install PySnooper by:

  • pip:
$ pip install pysnooper
  • conda with conda-forge channel:
$ conda install -c conda-forge pysnooper

License

Copyright (c) 2019 Ram Rachum and collaborators, released under the MIT license.

I provide Development services in Python and Django and I give Python workshops to teach people
Python and related topics.

Media Coverage

Hacker News thread
and /r/Python Reddit thread (22 April 2019)