如何在pdb中查看变量

78

我正在调试一个Python脚本,想要监视一个变量是否发生变化(类似于在gdb中观察内存地址)。有没有方法可以实现这个目的?


@WayneWerner:https://dev59.com/questions/P6bja4cB1Zd3GeqPaxIM - Jeff Widman
6个回答

44

使用 PDB 进行数据断点调试

... 类似于在 GDB 中观察内存地址 ...

  • GDB 使用数据断点进行调试,这得益于硬件级别的支持(即硬件监视点),通常涉及将内存页面标记为只读,这样就会在内存访问时触发异常处理程序。如果没有硬件监视点,则使用软件监视点,但它们只对单线程有用,而且速度要慢得多。
  • PDB 不支持数据断点,所以简短的回答是:不,你不能直接用 PDB 实现数据断点调试

在 pdb 的断点处打印变量

要在命中 断点 时查看变量,可以使用 commands 命令。例如,在命中第一个断点时打印 some_variable(可以参考 PDB 文档的典型示例)。

(Pdb) commands 1
(com) print(some_variable)
(com) end
(Pdb)

此外,您可以使用condition命令来确保断点仅在变量取特定值时被触发。

例如:

(Pdb) condition 1 some_variable==some_value

其他解决方案

您可以使用追踪/分析功能,通过使用sys.settrace查看正在执行的操作码,逐步检查事物。

以下是一些代码,可供您开始使用:

import sys
import dis


def tracefn(frame, event, arg):
    if event == 'call':
        print("## CALL", frame)
        frame.f_trace_opcodes = True
    elif event == 'opcode':
        opcode = frame.f_code.co_code[frame.f_lasti]
        opname = dis.opname[opcode]
        print("## OPCODE", opname)
    return tracefn


watchme = 123

def foo():
    global watchme
    watchme = 122

sys.settrace(tracefn)

foo()

你可能需要监视所有的STORE_*操作码。https://docs.python.org/3/library/dis.html


1
这应该是最佳答案。简单、直接,教你一个pdb的好用功能——断点命令。 - Christian Aichinger
12
虽然它可能有用,但它并没有直接回答问题,该问题想要观察变量的变化,并在发生这些变化时中断,而不仅仅是在预设断点查看变量的状态。 - kmantel
有些变量像 j 似乎被用作 jump 的缩写。我想打印出 j,我可以这样做吗或者需要改变变量名? - Ego ren

32

对于Python 3

您可以使用pdb的显示功能。

一旦触发断点,只需键入

ipdb> display expression

示例:

ipdb> display instance
display instance: <AppUser: dmitry4>
ipdb> display instance.id
display instance.id: 9
ipdb> display instance.university
display instance.university: <University: @domain.com>

ipdb> display

Currently displaying:
instance.university: <University: @domain.com>
instance.id: 9
instance: <AppUser: dmitry4>
ipdb> 

正如您所看到的,每次键入"display",它都会打印出所有手表(表达式)。 您可以使用内置函数undisplay删除某些观察记录。

您还可以使用pp expression来美化表达式(非常有用)


2
如果你正在循环中观察变化,请添加一个 break <行号>,每次 continue 时都会显示你的显示。显示非常方便。 - rafaelpivato

25

这里有一个非常hacky的方法使用 pdb 进行调试。这些命令可以放在你的 ~/.pdbrc 文件中,每次使用 pdb 时都会自动加载。

!global __currentframe, __stack; from inspect import currentframe as __currentframe, stack as __stack
!global __copy; from copy import copy as __copy
!global __Pdb; from pdb import Pdb as __Pdb
!global __pdb; __pdb = [__framerec[0].f_locals.get("pdb") or __framerec[0].f_locals.get("self") for __framerec in __stack() if (__framerec[0].f_locals.get("pdb") or __framerec[0].f_locals.get("self")).__class__ == __Pdb][-1]

alias _setup_watchpoint !global __key, __dict, __val; __key = '%1'; __dict = __currentframe().f_locals if __currentframe().f_locals.has_key(__key) else __currentframe().f_globals; __val = __copy(%1)

alias _nextwatch_internal next;; !if __dict[__key] == __val: __pdb.cmdqueue.append("_nextwatch_internal %1")
alias _stepwatch_internal step;; !if __dict[__key] == __val: __pdb.cmdqueue.append("_stepwatch_internal %1")

alias nextwatch __pdb.cmdqueue.extend(["_setup_watchpoint %1", "_nextwatch_internal"])
alias stepwatch __pdb.cmdqueue.extend(["_setup_watchpoint %1", "_stepwatch_internal"])

这将添加两个命令,nextwatchstepwatch,分别以变量名varname作为参数。如果可能的话,它们将为当前帧的本地变量varname创建一个浅复制,并继续执行nextstep,直到该名称所指向的内容发生更改。

这在CPython 2.7.2中可以工作,但依赖于一些pdb内部实现,因此在其他地方可能会出现问题。


我将宏复制到了我的~/.pdbrc文件中。然而,pdb告诉我:NameError'nextwatch'未定义。我像这样调用它:“nextwatch'(var)”。 - ArchLinuxTux
我正在使用Python 3.5和Mac,似乎.pdbrc无法正常工作。即使我将上面的代码粘贴到(pdb):环境中,当我运行stepwatchnextwatch时,它会显示__dict和其他内容未定义。你能否更新你的代码以适用于Python 3.5?谢谢。 - Daniel
如上所述,这可能在其他版本的Python中无法正常工作。欢迎您将Python 3.5版本发布为新答案。 - Michael Hoffman

18

一个可能的解决方案是使用pdb++

pip install pdbpp

然后,使用修饰符@pdb.break_on_setattr来“标记”您想要观察的对象:

from pdb import break_on_setattr
@break_on_setattr('bar')
class Foo(object):
    pass

f = Foo()
f.bar = 42    # the program breaks here

在任何Foo对象的属性bar发生更改时,pdb都会中断。

注意事项
只有对基础__setattr__方法的调用才会触发断点。这意味着f.bar = 'XYZ'setattr(f, 'XYZ')将起作用,但操纵bar对象不会触发断点:

f.bar = []
f.bar.append(7) # will NOT trigger breakpoint

f.bar = 2
f.bar += 5      # will trigger breakpoint

注意:@break_on_setattr不是标准的pdb模块的一部分。 pdbpdbpp包覆盖/猴子补丁。

pdb.set_trace()之后,您还可以通过其类来包装现有对象:

(Pdb++) import pdb
(Pdb++) pdb.break_on_setattr('tree_id')(self.__class__)
(Pdb++) continue

这应该是被接受的答案。运行起来非常好,超级简单。 - Nick K9

4

这是我对 Michael Hoffman 的解决方案的版本,适用于ipdb和Python 3.7(更改涉及检查类类型和字典的has_key):

!global __currentframe, __stack; from inspect import currentframe as __currentframe, stack as __stack
!global __copy; from copy import copy as __copy
!global __Pdb; from IPython.terminal.debugger import TerminalPdb as __Pdb
!global __pdb_list; __pdb_list = [__fr[0].f_locals.get("pdb") or __fr[0].f_locals.get("self") for __fr in __stack() if ((type(__fr[0].f_locals.get("pdb")) is __Pdb) or (type(__fr[0].f_locals.get("self")) is __Pdb))]
!global __pdb; __pdb = __pdb_list[0]
alias _setup_watchpoint !global __key, __dict, __val; __key = '%1'; __dict = __currentframe().f_locals if (__key in __currentframe().f_locals) else __currentframe().f_globals; __val = __copy(%1)
alias _nextwatch_internal next;; !if __dict[__key] == __val: __pdb.cmdqueue.append("_nextwatch_internal %1")
alias _stepwatch_internal step;; !if __dict[__key] == __val: __pdb.cmdqueue.append("_stepwatch_internal %1")
alias nextwatch __pdb.cmdqueue.extend(["_setup_watchpoint %1", "_nextwatch_internal"])
alias stepwatch __pdb.cmdqueue.extend(["_setup_watchpoint %1", "_stepwatch_internal"])

如果您要在Python 3.7和pdb中使用它,请按照以下方式更改__Pdb的定义:

!global __Pdb; from pdb import Pdb as __Pdb

3

两个现有的答案都存在一些基本问题。

直接在pdb中执行,或者使用一些技巧来实现观察点,在你处于相同作用域时才能正常工作。当你跳出该帧时,它将不起作用。

def change(var):
    var.append(1)

a = []
change(a)

它不能捕获函数内发生的事情,只有在change(a)之后才知道变量a已经被更改。这在函数很简单的情况下有效,但实际上,函数可能会非常深入,有价值的东西就在里面。或者更糟糕的是,a可能被返回并传递,但在当前范围内永远不会改变。那么你就永远无法捕获它。

__setattr__方法仅适用于您可以创建自己的类的情况。它无法处理我上面提到的简单情况。对于许多情况而言,拥有一个新类型的对象是不可接受的。例如,如果您正在使用内置的dictlist

我建议使用一个叫做watchpoints的库。

它比pdb(如果您还不熟悉它)更容易使用,并且覆盖的情况比本文中任何一种解决方案都要多得多。

您唯一需要做的就是watch您想要监视的变量。

from watchpoints import watch

a = []
watch(a)
a.append(1)  # will print the changes

输出将是:
> <module> (my_script.py:5):
>     a = 1
a:
0
->
1

它可以与内置类型(包括不可变的类型)、自定义类以及属性和索引一起使用。例如,您可以执行watch(my_obj.my_attr),它会正常工作。

您提到了pdb,因此可能有一些额外的东西您想要实现,而不仅仅是知道变量已更改。 watchpoints 支持 pdb 拉起,就像 breakpoints() 一样工作。

您需要通过以下方式配置 watchpoints

watch.config(pdb=True)

然后像上面那样 watch 变量。当该变量发生改变时,将弹出 pdb。你可以使用 q(uit) 命令退出 pdb 并在下一次变量改变时再次弹出。

您可以参考github页面了解更多有关该库的用法。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接