Ipython: IPythonShellEmbed no reconoce las variables locales

Creado en 19 jul. 2010  ·  18Comentarios  ·  Fuente: ipython/ipython

Asunto

Los shells de IPython incrustados pueden perder el rastro de las variables locales.

Caso de prueba

Caso de prueba mínimo:

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)

Para ver el error, primero ejecute el código en Python (o IPython) y salga del shell generado; la declaración de impresión final muestra correctamente '15'. Ejecute el código nuevamente, pero esta vez escriba sum(foo.values[x] for x in foo.indices) en el shell generado y recibiremos el error

"NameError: el nombre global 'foo' no está definido".

bug

Comentario más útil

Investigué un poco este problema y definitivamente se puede solucionar (aunque mantener las correcciones para Python 2 y 3 puede ser complicado).

La solución ChainMap sería más fácil de incluir en IPython propiamente dicho. Sin embargo, hay una pequeña trampa de que eval / exec requiere que los globales sean dict . Crear un class MyChainMap(ChainMap, dict): pass puede solucionar esto.

También escribí una corrección de Python 3.5+ basada en una estrategia diferente de simular celdas de cierre y forzar al compilador de Python a emitir el código de bytes correcto para trabajar con ellas. El archivo relevante está aquí , parte de mi demostración de xdbg . Funciona reemplazando get_ipython().run_ast_nodes .

Por lo que puedo decir, los dos enfoques difieren solo en el manejo de los cierres. Cuando xdbg está incrustado en un ámbito que se ha cerrado sobre algunas variables, puede acceder correctamente a esas variables por referencia y mutarlas. Además, si se crean funciones en el intérprete interactivo, se cerrarán sobre las variables locales que necesiten y permitirán que el resto del ámbito local sea recolectado como basura.

Todos 18 comentarios

Hmm, ¿dónde definiste 'foo'?

Si simplemente ejecuta call foo fuera de la barra de funciones, foo no debería existir (y obviamente no lo hace).

Cuando, en cambio, ejecuta "sum (f.values ​​[x] para x en f.indices)", obtiene 15 nuevamente ...

Muy correcto. Sin embargo, me refiero al uso de foo _inside_ del shell IPython generado, que a su vez se genera dentro de la definición de la función donde foo es una variable local.

¿Es probable que sea el mismo problema que el número 62?

No, @takluyver : este es un problema separado y, de hecho, un error real en nuestro código de incrustación. Esperaba que su trabajo reciente con espacios de nombres lo hubiera solucionado, pero no fue así. Como referencia, aquí está el código de ejemplo para ejecutar con la API de incrustación actual:

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)

Luego, en el IPython incrustado generado, esto falla:

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

Y no funciona incluso si pasamos a la llamada de inserción user_ns=locals() explícitamente, pero en ese caso obtenemos además un bloqueo al salir:

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'

Parece que nuestra maquinaria de incrustación está en muy mal estado ...

Me asigné esto a mí mismo para verlo.

Excelente, gracias.

Desafortunadamente, creo que esta es una limitación de Python en sí. Parece que el código compilado dinámicamente no puede definir un cierre, que es esencialmente lo que estamos haciendo aquí con una expresión generadora. Aquí hay un caso de prueba mínimo:

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

f()

Lo que da:

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

Tenga en cuenta que todavía puede ver variables locales en IPython; en el ejemplo dado, funciona print foo . Pero no se puede cerrar un nuevo alcance sobre ellos.

Creo que puede ser posible hacer que esto funcione usando collections.ChainMap de Python 3.3 para que IPython vea las variables locales y globales donde está incrustado como globales. Sin embargo, debido a la falta de ruido sobre esto en los últimos dos años, no creo que sea de alta prioridad, así que estoy volviendo a marcar en consecuencia, y espero llegar a esto en algún momento después de 1.0.

¿Es aceptable votar a favor,: +1:? Esto también me afecta a mí. Puedo agregar mi caso de uso si se solicita.

Estaría 100% bien si la solución para esto solo funcionara en Python 3.

También tengo el mismo problema en Python 3. Gracias por reabrir.

El mismo problema aquí: +1: ambos en Python 2 y 3.

Tengo este problema tanto en Python 2 como en 3. Me afecta a diario.

Investigué un poco este problema y definitivamente se puede solucionar (aunque mantener las correcciones para Python 2 y 3 puede ser complicado).

La solución ChainMap sería más fácil de incluir en IPython propiamente dicho. Sin embargo, hay una pequeña trampa de que eval / exec requiere que los globales sean dict . Crear un class MyChainMap(ChainMap, dict): pass puede solucionar esto.

También escribí una corrección de Python 3.5+ basada en una estrategia diferente de simular celdas de cierre y forzar al compilador de Python a emitir el código de bytes correcto para trabajar con ellas. El archivo relevante está aquí , parte de mi demostración de xdbg . Funciona reemplazando get_ipython().run_ast_nodes .

Por lo que puedo decir, los dos enfoques difieren solo en el manejo de los cierres. Cuando xdbg está incrustado en un ámbito que se ha cerrado sobre algunas variables, puede acceder correctamente a esas variables por referencia y mutarlas. Además, si se crean funciones en el intérprete interactivo, se cerrarán sobre las variables locales que necesiten y permitirán que el resto del ámbito local sea recolectado como basura.

IPython 6.0 y versiones posteriores solo funcionan en Python 3, por lo que si @nikitakit o cualquier otra persona quiere abrir una solicitud pulll con un caso de prueba y una solución para esto, sería bienvenido.

Ha pasado un año desde mi último comentario aquí, y durante este tiempo mi comprensión del tema ha cambiado un poco.

La inspección interactiva del alcance local sigue siendo una característica importante para mí, pero en realidad hay una serie de cuestiones interrelacionadas con respecto a las variables locales y la maquinaria de integración. Por ejemplo, la modificación de variables locales dentro de un shell incrustado no funciona:

>>> 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

Un enfoque basado en ChainMap no ayudaría en esta situación.

Otra pregunta es qué sucede cuando los cierres definidos dentro de un shell incrustado se filtran al alcance global. Considere ejecutar el mismo código que el anterior, pero ingresando algo como IPython.get_my_x = lambda: x en el shell de IPython integrado. Una solución basada en ChainMap evitará causar un NameError en esta situación, a expensas de introducir potencialmente dos copias simultáneas de x que existen independientemente una de la otra (una es la ChainMap , y el otro la variable local / celda de cierre utilizada por el intérprete de Python).

Dada la complejidad de la situación, he decidido centrar mis esfuerzos en un enfoque más integral del problema (que también se alinea mejor con mi propio uso de IPython). Esto ha llevado al desarrollo de xdbg , que es esencialmente un depurador que se integra con IPython. La idea clave es extender el shell de IPython ofreciendo comandos de depuración mediante magia (por ejemplo, %break para establecer puntos de interrupción). El hecho de que los puntos de interrupción se establezcan externamente, en lugar de llamar a la función embed in situ, ha permitido una implementación que aborda muchos de estos problemas con variables locales.

Actualmente no planeo solicitar una extracción de una corrección de errores con un objetivo específico para el núcleo de IPython. Sin embargo, estoy muy interesado en saber qué piensan los usuarios y desarrolladores de IPython sobre el uso de una interfaz inspirada en el depurador para la inspección del código local. IPython (y ahora también Jupyter) ha tenido un gran impacto en la capacidad de hacer codificación interactiva en Python, pero aún quedan muchas mejoras por hacer en la forma en que interactúa con la encapsulación pesada usando clases / funciones / módulos.

Este error me ha quemado un montón de veces. P.ej

[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.

No estoy seguro de qué se puede hacer, pero solo agrego mi voz al coro.

Basado en https://bugs.python.org/issue13557 y el hecho de que la aprobación explícita locales () y globales (a) un ejecutivo correcciones @takluyver 's reproducción del insecto , parece que este _ought_ ser posible fijar en IPython pasando los espacios de nombres locales y globales correctos.

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()
¿Fue útil esta página
0 / 5 - 0 calificaciones