11 votes

Pourquoi les modifications apportées par Python 3 à exec ont-elles cassé ce code ?

J'ai parcouru les innombrables fils de discussion sur l'exécution de Python sur SO, mais je n'ai pas trouvé de réponse à mon problème. Je suis terriblement désolé si cette question a déjà été posée. Voici mon problème :

# Python 2.6: prints 'it is working'
# Python 3.1.2: "NameError: global name 'a_func' is not defined"
class Testing(object):
  def __init__(self):
    exec("""def a_func():
      print('it is working')""")
    a_func()

Testing()

# Python 2.6: prints 'it is working'
# Python 3.1.2: prints 'it is working'
class Testing(object):
  def __init__(self):
    def a_func():
      print('it is working')
    a_func()

Testing()

Comme la définition de la fonction standard fonctionne dans les deux versions de Python, je suppose que le problème doit venir d'un changement dans la façon dont exec fonctionne. J'ai lu les docs de l'API pour 2.6 et 3 pour exec J'ai également lu la page "What's New In Python 3.0" et je n'ai pas vu de raison pour laquelle le code serait cassé.

11voto

JBernardo Points 14772

Vous pouvez voir le bytecode généré pour chaque version de Python avec :

>>> from dis import dis

Et, pour chaque interprète :

#Python 3.2
>>> dis(Testing.__init__)
...
  5          10 LOAD_GLOBAL              1 (a_func)
...

#Python 2.7
>>> dis(Testing.__init__)
...
  5           8 LOAD_NAME                0 (a_func)
...

Comme vous pouvez le voir, Python 3.2 recherche une valeur globale (LOAD_GLOBAL) nommée a_func et 2.7 recherche d'abord l'étendue locale (LOAD_NAME) avant de rechercher l'étendue globale.

Si vous le faites print(locals()) après le exec vous verrez que a_func est créé à l'intérieur du __init__ fonction.

Je ne sais pas vraiment pourquoi c'est fait de cette façon, mais ça semble être un changement sur la façon dont tables de symboles sont traitées.

Par ailleurs, si vous voulez créer un a_func = None sur le dessus de votre __init__ pour que l'interpréteur sache qu'il s'agit d'une variable locale, cela ne fonctionnera pas puisque le bytecode sera maintenant LOAD_FAST et qui ne font pas de recherche mais récupèrent directement la valeur d'une liste.

La seule solution que je vois est d'ajouter globals() comme deuxième argument de exec donc cela va créer a_func comme une fonction globale à laquelle on peut accéder par l'option LOAD_GLOBAL opcode.

Modifier

Si vous retirez le exec Python2.7 modifie le bytecode de l'instruction LOAD_NAME a LOAD_GLOBAL . Ainsi, en utilisant exec votre code sera toujours plus lent sous Python2.x car il doit rechercher les changements dans la portée locale.

Comme l'a fait Python3 exec n'est pas un mot clé, l'interpréteur ne peut pas être sûr s'il exécute réellement un nouveau code ou s'il fait autre chose... Donc le bytecode ne change pas.

Par exemple

>>> exec = len
>>> exec([1,2,3])
3

tl;dr

exec('...', globals()) peut résoudre le problème si vous ne vous souciez pas que le résultat soit ajouté à l'espace de nom global

6voto

Armin Rigo Points 4754

Je complète la réponse ci-dessus, juste au cas où. Si le exec est dans une fonction quelconque, je recommande d'utiliser la version à trois arguments comme suit :

def f():
    d = {}
    exec("def myfunc(): ...", globals(), d)
    d["myfunc"]()

C'est la solution la plus propre, car elle ne modifie aucun espace de nom sous vos pieds. Au lieu de cela, myfunc est stocké dans le dictionnaire explicite d .

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X