Ipython: IPythonShellEmbed 无法识别局部变量

创建于 2010-07-19  ·  18评论  ·  资料来源: ipython/ipython

问题

嵌入式 IPython shell 可能会丢失对局部变量的跟踪。

测试用例

最小测试用例:

class Foo(object):
    """ Container-like object """
    def __setattr__(self, obj, val):
        self.__dict__[obj] = val

    def __getattr__(self, obj, val):
        return self.__dict__[obj]

f = Foo()
f.indices = set([1,2,3,4,5])
f.values = {}
for x in f.indices:
    f.values[x] = x

def bar(foo):
    import IPython
    IPython.Shell.IPShellEmbed()()
    return sum(foo.values[x] for x in foo.indices)

print bar(f)

要查看错误,首先在 Python(或 IPython)中运行代码并退出生成的 shell; 最终的打印语句正确显示“15”。 再次运行代码,但这次在生成的 shell 中键入sum(foo.values[x] for x in foo.indices) ,我们收到错误

“NameError:全局名称‘foo’未定义”。

最有用的评论

我稍微调查了这个问题,它绝对是可以修复的(尽管为 Python 2 和 3 维护修复程序可能很麻烦)。

ChainMap 解决方案最容易包含到 IPython 中。 但是,有一个小问题,即 eval/exec 要求全局变量为dict 。 创建一个class MyChainMap(ChainMap, dict): pass可以解决这个问题。

我还基于模拟闭包单元并强制 python 编译器发出正确的字节码来处理它们的不同策略编写了一个 Python 3.5+ 修复程序。 相关文件在这里,我的xdbg 演示的一部分。 它的工作原理是替换get_ipython().run_ast_nodes

据我所知,这两种方法仅在处理闭包方面有所不同。 当xdbg嵌入到已经关闭了某些变量的作用域中时,它可以正确地通过引用访问这些变量并对其进行变异。 此外,如果在交互式解释器中创建了任何函数,它们将关闭所需的任何局部变量,同时允许对局部作用域的其余部分进行垃圾收集。

所有18条评论

嗯,你在哪里定义'foo'?

如果您只是在函数栏之外运行 call foo,则 foo 不应该存在(显然它不存在)。

当您改为运行“sum(f.values[x] for x in f.indices)”时,您再次得到 15 ......

相当正确。 但是,我指的是使用foo _inside_ 生成的 IPython shell,它又在函数定义中生成,其中foo是局部变量。

这可能是与#62 相同的问题吗?

不, @takluyver :这是一个单独的问题,确实是我们嵌入代码中的一个真正的错误。 我希望你最近在命名空间方面的工作能够修复它,但它没有。 作为参考,以下是使用当前嵌入 api 运行的示例代码:

class Foo(object):
    """ Container-like object """
    def __setattr__(self, obj, val):
        self.__dict__[obj] = val

    def __getattr__(self, obj, val):
        return self.__dict__[obj]

f = Foo()
f.indices = set([1,2,3,4,5])
f.values = {}
for x in f.indices:
    f.values[x] = x

def bar(foo):
    import IPython
    IPython.embed()
    return sum(foo.values[x] for x in foo.indices)

print bar(f)

然后,在生成的嵌入式 IPython 中,这失败了:

In [1]: sum(foo.values[x] for x in foo.indices)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
/home/fperez/tmp/junk/ipython/foo.py in <module>()
----> 1 sum(foo.values[x] for x in foo.indices)

/home/fperez/tmp/junk/ipython/foo.py in <genexpr>((x,))
----> 1 sum(foo.values[x] for x in foo.indices)

NameError: global name 'foo' is not defined

即使我们明确地传递给嵌入调用user_ns=locals()也不起作用,但在这种情况下,我们还会在退出时发生崩溃:

Error in atexit._run_exitfuncs:
Traceback (most recent call last):
  File "/usr/lib/python2.7/atexit.py", line 24, in _run_exitfuncs
    func(*targs, **kargs)
  File "/home/fperez/usr/lib/python2.7/site-packages/IPython/core/interactiveshell.py", line 2702, in atexit_operations
    self.reset(new_session=False)
  File "/home/fperez/usr/lib/python2.7/site-packages/IPython/core/interactiveshell.py", line 1100, in reset
    self.displayhook.flush()
  File "/home/fperez/usr/lib/python2.7/site-packages/IPython/core/displayhook.py", line 319, in flush
    self.shell.user_ns['_oh'].clear()
KeyError: '_oh'

看起来我们的嵌入机器状态很糟糕......

把这个分配给自己看。

太好了,谢谢。

不幸的是,我认为这是 Python 本身的限制。 看起来动态编译的代码不能定义闭包,这本质上就是我们在这里使用生成器表达式所做的。 这是一个最小的测试用例:

def f():
   x = 1
   exec "def g(): print x\ng()"

f()

这使:

Traceback (most recent call last):
  File "scopetest.py", line 5, in <module>
    f()
  File "scopetest.py", line 3, in f
    exec "def g(): print x\ng()"
  File "<string>", line 2, in <module>
  File "<string>", line 1, in g
NameError: global name 'x' is not defined

请注意,您仍然可以在 IPython 中看到局部变量 - 在给出的示例中,普通的print foo有效。 但是你不能对它们关闭一个新的范围。

我认为可以使用 Python 3.3 中的collections.ChainMap来完成这项工作,以便 IPython 看到它作为全局变量嵌入的局部和全局变量。 但是,由于过去两年对此没有任何意见,我认为这不是高优先级,因此我相应地重新标记,并希望在 1.0 之后的某个时间进行此操作。

投票可以接受吗,:+1: ? 这对我也有影响。 如果需要,我可以添加我的用例。

如果此修复程序仅适用于 Python 3,我将 100% 没问题。

我在 Python 3 下也有同样的问题。感谢您重新打开。

同样的问题:+1:在 Python 2 和 3 中。

我在 Python 2 和 3 中都有这个问题。它每天都在影响我。

我稍微调查了这个问题,它绝对是可以修复的(尽管为 Python 2 和 3 维护修复程序可能很麻烦)。

ChainMap 解决方案最容易包含到 IPython 中。 但是,有一个小问题,即 eval/exec 要求全局变量为dict 。 创建一个class MyChainMap(ChainMap, dict): pass可以解决这个问题。

我还基于模拟闭包单元并强制 python 编译器发出正确的字节码来处理它们的不同策略编写了一个 Python 3.5+ 修复程序。 相关文件在这里,我的xdbg 演示的一部分。 它的工作原理是替换get_ipython().run_ast_nodes

据我所知,这两种方法仅在处理闭包方面有所不同。 当xdbg嵌入到已经关闭了某些变量的作用域中时,它可以正确地通过引用访问这些变量并对其进行变异。 此外,如果在交互式解释器中创建了任何函数,它们将关闭所需的任何局部变量,同时允许对局部作用域的其余部分进行垃圾收集。

IPython 6.0 及更高版本仅适用于 Python 3,因此如果@nikitakit或其他任何人想要打开带有测试用例和修复程序的 pulll 请求,那将是受欢迎的。

我上次在这里发表评论已经一年了,在这段时间里,我对这个问题的理解发生了一些变化。

交互式检查局部作用域对我来说仍然是一个重要的特征,但实际上有许多关于局部变量和嵌入机制的相互关联的问题。 例如,在嵌入式 shell 中修改局部变量不起作用:

>>> import IPython
>>> def test():
...     x = 5
...     IPython.embed()
...     print('x is', x)
...
>>> test()
Python 3.5.1 |Continuum Analytics, Inc.| (default, Dec  7 2015, 11:24:55)
Type 'copyright', 'credits' or 'license' for more information
IPython 6.1.0 -- An enhanced Interactive Python. Type '?' for help.

In [1]: x
Out[1]: 5

In [2]: x = 6

In [3]:
Do you really want to exit ([y]/n)? y

x is 5

在这种情况下,基于ChainMap的方法无济于事。

另一个问题是当在嵌入式 shell 中定义的闭包泄漏到全局范围时会发生什么。 考虑运行与上面相同的代码,但在嵌入式 IPython shell 中沿着IPython.get_my_x = lambda: x行输入一些内容。 在这种情况下,基于ChainMap的解决方案将避免导致NameError ,代价是可能引入x两个同时副本,它们彼此独立存在(一个是ChainMap ,另一个是python解释器使用的局部变量/闭包单元)。

鉴于情况的复杂性,我决定将精力集中在解决问题的更全面的方法上(这也更符合我自己对 IPython 的使用)。 这导致了xdbg的开发,它本质上是一个与 IPython 集成的调试器。 关键思想是通过魔法提供调试器命令来扩展 IPython shell(例如%break来设置断点)。 事实上,断点是在外部设置的,而不是通过调用函数embed就地,这使得实现可以解决许多与局部变量有关的问题。

我目前不打算向核心 IPython 请求一个针对性极强的错误修复。 但是,我很想知道 IPython 用户和开发人员对使用受调试器启发的接口进行本地代码检查有何看法。 IPython(现在还有 Jupyter)对在 Python 中进行交互式编码的能力产生了巨大影响,但在它如何与使用类/函数/模块的重封装交互方面仍有许多改进要做。

我已经被这个错误烧了很多次了。 例如

[mcgibbon<strong i="6">@xps13</strong>:~]$ cat test.py 
x = 1
def func():    
    x = 2
    import IPython
    IPython.embed()

if __name__ == "__main__":
    func()
[mcgibbon<strong i="9">@xps13</strong>:~]$ python test.py 
Python 3.7.6 (default, Dec 18 2019, 19:23:55) 
Type 'copyright', 'credits' or 'license' for more information
IPython 7.12.0 -- An enhanced Interactive Python. Type '?' for help.
In [1]: [x for _ in range(2)]                                                                                                                                                                                                                 
Out[1]: [1, 1]  # lol whoops. looked up in wrong scope.

我不确定可以做什么,但只是将我的声音添加到合唱团中。

基于https://bugs.python.org/issue13557以及将 locals() 和 globals() 显式传递给 exec 修复了@takluyver对 bug

x = 1

def func():    
    x = 2
    exec("def g():\n print(x)\ng()")  # prints 1 :(
    exec("def g():\n print(x)\ng()", locals(), globals())  # prints 2 yay!


if __name__ == "__main__":
    func()
此页面是否有帮助?
0 / 5 - 0 等级