Ipython: IPythonShellEmbed ne reconnaît pas les variables locales

Créé le 19 juil. 2010  ·  18Commentaires  ·  Source: ipython/ipython

Problème

Les shells IPython intégrés peuvent perdre la trace des variables locales.

Cas de test

Cas de test minimal :

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)

Pour voir l'erreur, exécutez d'abord le code en Python (ou IPython) et quittez le shell généré ; l'instruction d'impression finale affiche correctement '15'. Exécutez à nouveau le code, mais cette fois, tapez sum(foo.values[x] for x in foo.indices) dans le shell généré, et nous recevons l'erreur

" NameError : le nom global 'foo' n'est pas défini".

bug

Commentaire le plus utile

J'ai examiné un peu ce problème, et il est définitivement réparable (bien que le maintien des correctifs pour Python 2 et 3 puisse être compliqué).

La solution ChainMap serait la plus simple à inclure dans IPython proprement dit. Cependant, il y a un petit problème que eval/exec nécessite que les globales soient un dict . Créer un class MyChainMap(ChainMap, dict): pass peut contourner ce problème.

J'ai également écrit un correctif Python 3.5+ basé sur une stratégie différente consistant à simuler des cellules de fermeture et à forcer le compilateur python à émettre le bytecode correct pour fonctionner avec elles. Le fichier correspondant est ici , faisant partie de ma démo xdbg . Cela fonctionne en remplaçant get_ipython().run_ast_nodes .

Pour autant que je sache, les deux approches ne diffèrent que par leur gestion des fermetures. Lorsque xdbg est intégré à une portée qui s'est fermée sur certaines variables, il peut accéder correctement à ces variables par référence et les muter. De plus, si des fonctions sont créées dans l'interpréteur interactif, elles fermeront toutes les variables locales dont elles ont besoin tout en permettant au reste de la portée locale d'être récupérés.

Tous les 18 commentaires

Hmm, où avez-vous défini « foo » ?

Si vous exécutez simplement l'appel foo en dehors de la barre de fonctions, foo ne devrait pas exister (et évidemment il n'existe pas).

Lorsque vous exécutez à la place "sum(f.values[x] for x in f.indices)", vous obtenez à nouveau 15 ...

Tout à fait correct. Cependant, je fais référence à l'utilisation de foo _inside_ le shell IPython généré, qui est à son tour généré dans la définition de la fonction où foo est une variable locale.

Est-ce que c'est probablement le même problème que #62?

Non, @takluyver : il s'agit d'un problème distinct et en fait d'un véritable bug dans notre code d'intégration. J'espérais que votre travail récent avec les espaces de noms l'aurait corrigé, mais ce n'est pas le cas. Pour référence, voici l'exemple de code à exécuter avec l'API d'intégration actuelle :

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)

Ensuite, dans l'IPython généré et intégré, cela échoue :

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

Et ça ne marche pas même si on passe à l'appel d'intégration user_ns=locals() explicitement, mais dans ce cas on obtient en plus un plantage à la sortie :

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'

On dirait que nos machines d'enrobage sont en assez mauvais état...

Attribué cela à moi-même à regarder.

Excellent, merci.

Malheureusement, je pense que c'est une limitation de Python lui-même. Il semble que le code compilé dynamiquement ne puisse pas définir une fermeture, ce qui est essentiellement ce que nous faisons ici avec une expression génératrice. Voici un cas de test minimal :

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

f()

Qui donne:

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

Notez que vous pouvez toujours voir les variables locales dans IPython - dans l'exemple donné, le simple print foo fonctionne. Mais vous ne pouvez pas fermer une nouvelle étendue sur eux.

Je pense qu'il est peut-être possible de faire fonctionner cela en utilisant collections.ChainMap de Python 3.3 afin qu'IPython voie à la fois les variables locales et globales où elles sont intégrées en tant que globales. Cependant, vu l'absence de bruit à ce sujet au cours des deux dernières années, je ne pense pas que ce soit une priorité élevée, donc je re-marque en conséquence, et j'espère y arriver quelque temps après la 1.0.

Est-il acceptable de voter pour, :+1: ? Cela m'affecte aussi. Je peux ajouter mon cas d'utilisation si demandé.

Je serais d'accord à 100% si le correctif ne fonctionnait que sur Python 3.

J'ai également le même problème sous Python 3. Merci pour la réouverture.

Même problème ici :+1 : à la fois en Python 2 et 3.

J'ai ce problème dans Python 2 et 3. Cela m'affecte quotidiennement.

J'ai examiné un peu ce problème, et il est définitivement réparable (bien que le maintien des correctifs pour Python 2 et 3 puisse être compliqué).

La solution ChainMap serait la plus simple à inclure dans IPython proprement dit. Cependant, il y a un petit problème que eval/exec nécessite que les globales soient un dict . Créer un class MyChainMap(ChainMap, dict): pass peut contourner ce problème.

J'ai également écrit un correctif Python 3.5+ basé sur une stratégie différente consistant à simuler des cellules de fermeture et à forcer le compilateur python à émettre le bytecode correct pour fonctionner avec elles. Le fichier correspondant est ici , faisant partie de ma démo xdbg . Cela fonctionne en remplaçant get_ipython().run_ast_nodes .

Pour autant que je sache, les deux approches ne diffèrent que par leur gestion des fermetures. Lorsque xdbg est intégré à une portée qui s'est fermée sur certaines variables, il peut accéder correctement à ces variables par référence et les muter. De plus, si des fonctions sont créées dans l'interpréteur interactif, elles fermeront toutes les variables locales dont elles ont besoin tout en permettant au reste de la portée locale d'être récupérés.

IPython 6.0 et les versions ultérieures ne fonctionnent qu'en Python 3, donc si @nikitakit ou quelqu'un d'autre veut ouvrir une requête pulll avec un cas de test et un correctif pour cela, ce serait le bienvenu.

Cela fait un an depuis mon dernier commentaire ici, et pendant ce temps ma compréhension de la question a un peu changé.

L'inspection interactive de la portée locale reste une caractéristique importante pour moi, mais il existe en fait un certain nombre de problèmes interdépendants concernant les variables locales et les machines d'intégration. Par exemple, la modification de variables locales à l'intérieur d'un shell intégré ne fonctionne pas :

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

Une approche basée sur ChainMap n'aiderait pas dans cette situation.

Une autre question est de savoir ce qui se passe lorsque des fermetures définies à l'intérieur d'un shell intégré sont divulguées dans la portée globale. Envisagez d'exécuter le même code que ci-dessus, mais en saisissant quelque chose du genre IPython.get_my_x = lambda: x dans le shell IPython intégré. Une solution basée sur ChainMap évitera de provoquer un NameError dans cette situation, au détriment de l'introduction potentielle de deux copies simultanées de x qui existent indépendamment l'une de l'autre (l'une étant la ChainMap , et l'autre la variable locale/cellule de fermeture utilisée par l'interpréteur python).

Compte tenu de la complexité de la situation, j'ai décidé de concentrer mes efforts sur une approche plus globale du problème (qui correspond également mieux à ma propre utilisation d'IPython). Cela a conduit au développement de xdbg , qui est essentiellement un débogueur qui s'intègre à IPython. L'idée clé est d'étendre le shell IPython en proposant des commandes de débogueur via des magies (par exemple %break pour définir des points d'arrêt). Le fait que les points d'arrêt soient définis en externe, plutôt qu'en appelant la fonction embed sur place, a permis une implémentation qui résout bon nombre de ces problèmes avec des variables locales.

Je ne prévois actuellement pas de demander une correction de bogue étroitement ciblée au noyau IPython. Cependant, je suis très intéressé de savoir ce que les utilisateurs et les développeurs d'IPython pensent de l'utilisation d'une interface inspirée du débogueur pour l'inspection du code local. IPython (et maintenant aussi Jupyter) ont eu un impact énorme sur la capacité de faire du codage interactif en Python, mais il reste encore de nombreuses améliorations à apporter dans la façon dont il interagit avec une encapsulation lourde à l'aide de classes/fonctions/modules.

J'ai été brûlé un tas de fois par ce bug. Par exemple

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

Je ne suis pas sûr de ce qui peut être fait, mais j'ajoute juste ma voix à la chorale.

Sur la base de https://bugs.python.org/issue13557 et du fait que le fait de passer explicitement locals() et globals() à un exec corrige la reproduction du bogue par @takluyver , il semble que cela _devrait_ être possible à corriger dans IPython en passant les espaces de noms locaux et globaux corrects.

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()
Cette page vous a été utile?
0 / 5 - 0 notes