iPython debugger raises `NameError: name ... is not defined`

iPython debugger raises `NameError: name ... is not defined`

我无法理解此 Python 调试器会话中引发的以下异常:

(Pdb) p [move for move in move_values if move[0] == max_value]
*** NameError: name 'max_value' is not defined
(Pdb) [move for move in move_values]
[(0.5, (0, 0)), (0.5, (0, 1)), (0.5, (0, 2)), (0.5, (1, 0)), (0.5, (1, 1)), (0.5, (1, 2)), (0.5, (2, 0)), (0.5, (2, 1)), (0.5, (2, 2))]
(Pdb) max_value
0.5
(Pdb) (0.5, (0, 2))[0] == max_value
True
(Pdb) [move for move in move_values if move[0] == 0.5]
[(0.5, (0, 0)), (0.5, (0, 1)), (0.5, (0, 2)), (0.5, (1, 0)), (0.5, (1, 1)), (0.5, (1, 2)), (0.5, (2, 0)), (0.5, (2, 1)), (0.5, (2, 2))]
(Pdb) [move for move in move_values if move[0] == max_value]
*** NameError: name 'max_value' is not defined

为什么它有时告诉我 max_value 未定义,有时又没有?

顺便说一句,这是调试器启动前的代码:

max_value = max(move_values)[0]
best_moves = [move for move in move_values if move[0] == max_value]
import pdb; pdb.set_trace()

我在 PyCharm 中使用 Python 3.6 运行。

更新更新:

经过更多测试后,当我从 iPython REPL 或 PyCharm 中执行以下操作时,局部变量似乎在 pdb 会话中的列表理解中不可见:

$ ipython
Python 3.6.5 | packaged by conda-forge | (default, Apr  6 2018, 13:44:09) 
Type 'copyright', 'credits' or 'license' for more information
IPython 6.4.0 -- An enhanced Interactive Python. Type '?' for help.

In [1]: import pdb; pdb.set_trace()
--Call--
> /Users/billtubbs/anaconda/envs/py36/lib/python3.6/site-packages/IPython/core/displayhook.py(247)__call__()
-> def __call__(self, result=None):
(Pdb) x = 1; [x for i in range(3)]
*** NameError: name 'x' is not defined

但在常规 Python REPL 中它有效:

$ python
Python 3.6.5 | packaged by conda-forge | (default, Apr  6 2018, 13:44:09) 
[GCC 4.2.1 Compatible Apple LLVM 6.1.0 (clang-602.0.53)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import pdb; pdb.set_trace()
--Return--
> <stdin>(1)<module>()->None
(Pdb) x = 1; [x for i in range(3)]
[1, 1, 1]

我在上面测试了 3.4、3.5、3.6 版本,所以它似乎不依赖于版本。

更新 2

请注意,上面的测试 ('AMENDED UPDATE') 是有问题的,因为它在交互式 REPL 中使用了 import pdb; pdb.set_trace()

还有,原来的问题不限于iPython。

请参阅下面的 以全面了解此处发生的情况。

抱歉,如果我造成任何混淆!

这里有两个核心问题。第一个是(当在 IPython 中交互调用 pdb.set_trace() 时)你正在调试 IPython 的内容而不是你想要的范围。第二个是列表理解作用域规则与无法静态确定封闭作用域中存在的变量的情况相互作用很差,例如在调试器或 class bodies.

第一个问题几乎只有在 IPython 交互式提示中键入 pdb.set_trace() 时才会发生,这不是一件非常有用的事情,因此避免该问题的最简单方法是只是不要那样做。如果您无论如何都想这样做,您可以输入几次 r 命令,直到 pdb 说您已经超出了 IPython 的范围。 (不要过度,否则你最终会进入 IPython 的不同部分。)

第二个问题是根深蒂固的语言设计决策之间不可避免的相互作用。不幸的是,它不太可能消失。调试器中的列表理解仅在全局范围内有效,在调试函数时无效。如果你想在调试函数的同时建立一个列表,最简单的方法可能是使用 interact 命令并编写一个 for 循环。


这里是完整的效果组合。

  1. pdb.set_trace() 在下一个 跟踪事件 上触发 pdb,而不是在 pdb.set_trace() 被调用时触发。

pdb 和其他 Python 调试器使用的 trace function 机制仅在某些特定事件上触发,不幸的是 "when a trace function is set" 不是这些事件之一。通常,下一个事件是下一行的 'line' 事件或当前代码对象执行结束的 'return' 事件,但这不是这里发生的情况。

  1. IPython 设置一个 displayhook 来自定义表达式语句处理。

Python 用于显示表达式语句结果的机制是 sys.displayhook。当您在交互式提示中执行 1+2 时:

>>> 1+2
3

sys.displayhook 是打印 3 而不是丢弃它的东西。它还设置 _。当表达式语句的结果是 None 时,例如表达式 pdb.set_trace()sys.displayhook 什么都不做,但它仍然被调用。

IPython 用自己的自定义处理程序替换 sys.displayhook,负责打印 Out[n]: 东西,设置 Out 记录中的条目,调用 IPython 自定义漂亮打印,以及各种其他 IPython 便利。对于我们的目的,重要的是 IPython 的 displayhook 是写在 Python 中的,因此下一个跟踪事件是 displayhook 的 'call' 事件。

pdb 在 IPython 的 displayhook.

中开始调试
In [1]: import pdb; pdb.set_trace()
--Call--
> /Users/billtubbs/anaconda/envs/py36/lib/python3.6/site-packages/IPython/core/displayhook.py(247)__call__()
-> def __call__(self, result=None):
  1. 列表解析创建一个新的作用域。

人们不喜欢列表理解如何将循环变量泄漏到 Python 2 中的包含范围,因此列表理解在 Python 3 中获得了自己的范围。

  1. pdb 使用 eval,它与闭包变量的交互非常差。

Python 的闭包变量机制依赖于静态范围分析,这与 eval 的工作方式完全不兼容。因此,在 eval 中创建的新作用域无法访问闭包变量;他们只能访问全局变量。


将它们放在一起,在 IPython 中,您最终调试的是 IPython displayhook,而不是您正在 运行ning 交互代码的范围。因为您在里面IPython 的 displayhook,您的 x = 1 分配进入 displayhook 的局部变量。随后的列表理解需要访问 displayhook 的局部变量才能访问 x,但这将是列表理解的闭包变量,它不适用于 eval.

IPython之外,sys.displayhook是用C写的,所以pdb进不去,也没有'call'事件。您最终调试了您打算调试的范围。由于您处于全局范围内,因此 x = 1 进入全局范围,并且列表理解可以访问它。

如果您在调试任何普通函数时尝试 运行 x = 1; [x for i in range(3)],您会看到相同的效果。

一个可能的solution/workaround是运行

 globals().update(locals())

在 运行 宁 (i)pdb 中的列表理解之前。

Kobi T's 的启发,如果您希望每次出现此问题时无需人工干预即可应用解决方法,您可以继承TerminalPdb(ipython的调试器,但它也应该通过子类化 Pdb 来工作)。关键是扩展 TerminalPdb 的(实际上,Pdb 的,因为它是继承的)default 方法。 default 方法是每当您从 Pdb 的调试器提示中 运行 一行代码时调用的方法。

正如 user2357112-supports-monica 的回答所描述的那样,此函数使用 exec 到 运行 代码,并且通常会捕获引发的任何错误并将它们打印出来而不是引发它们。您可以利用这一事实,即对于此处出现的列表理解(以及更普遍的闭包)的特定问题,会引发 NameError。具体来说,如果这是引发的错误,您可以调解应用 Kobi T 的解决方法(globals().update(locals()) 的一个版本),然后再次 exec 代码——如果 NameError 是因为此处描述的闭包问题,代码现在应该 运行 没有错误;如果不是,NameError 将再次引发,并且输出将与该错误的预期一致。

这里的另一个好处与全局变量的处理方式有关。出于 exec 代码 运行 的目的,它看到的 global/local 变量可以明确定义为 exec 的输入。因此,您可以只更新发送到 exec 的副本,而不是更新工作区的 globals(),当您想要 运行 这些列表推导式时,让您的工作区受到的影响较小.

import sys
import traceback
from IPython.terminal.debugger import TerminalPdb

class LcTerminalPdb(TerminalPdb):
    # the default method is what runs code written to the prompt of the debugger
    def default(self, line):
        # most of this method directly copies the original one, but there's no
        # good way to add the NameError handling separately from the original
        # code
        if line[:1] == '!': line = line[1:]
        locals = self.curframe_locals
        globals = self.curframe.f_globals
        try:
            code = compile(line + '\n', '<stdin>', 'single')
            save_stdout = sys.stdout
            save_stdin = sys.stdin
            save_displayhook = sys.displayhook
            try:
                sys.stdin = self.stdin
                sys.stdout = self.stdout
                sys.displayhook = self.displayhook
                exec(code, globals, locals)
            ''' BELOW IS THE CODE ADDED TO Pdb's default()'''
            except NameError:
                # NameError occurs when a list comprehension requires variables
                # to be bound in its closure, but isn't able to because of how
                # exec handles local variables; putting the variable in the
                # global dictionary works, and this code takes the sledgehammer
                # approach of assigning *all* locals to globals, so we don't
                # have to be picky about which variable exactly was needed
                try:
                    tempGlobal = globals.copy()
                    tempGlobal.update(locals)
                    exec(code, tempGlobal, locals)
                except:
                    raise
            '''ABOVE IS THE CODE ADDED TO Pdb's default()'''
            finally:
                sys.stdout = save_stdout
                sys.stdin = save_stdin
                sys.displayhook = save_displayhook
        except:
            self._error_exc()

    # TerminalPdb doesn't directly call _error_exc, which was originally defined
    # in Pdb, so we can't import it from there, and it's underscored, so it
    # isn't immediately accessible from TerminalPdb's parent Pdb. However, it's
    # a simple function so I'm just replicating it here.
    def _error_exc(self):
        exc_info = sys.exc_info()[:2]
        self.error(traceback.format_exception_only(*exc_info)[-1].strip())

这是我们想要的结果:

>> import pdb; pdb.set_trace()
(Pdb) x=5;[x for i in range(3)]
*** NameError: name 'x' is not defined
(Pdb) q
>> import LcTerminalPdb; LcTerminalPdb.set_trace()
ipdb> x=5;[x for i in range(3)]
[5, 5, 5] 

如果您调用 LcTerminalPdb.set_trace(),此解决方案将起作用。或者,您可以将其保存为 Python 路径上的模块,在该模块中定义调用 LcTerminalPdb.set_trace()set_trace 方法(如下),然后设置 PYTHONBREAKPOINT 环境变量指向它。

[appended to file with class code from above]
def set_trace(frame=None):
    """
    Start debugging from `frame`.
    If frame is not specified, debugging starts from caller's frame.
    """
    LcTerminalPdb().set_trace(frame or sys._getframe().f_back)

我快速 LcTerminalPdb git repo 使用此文件和有关如何让感兴趣的人使用该部分的说明。