Je suis curieux de savoir s'il est possible de exec une chaîne dans une fonction comme si la chaîne était directement substituée à exec (avec l'indentation appropriée) . Je comprends que dans 99,9% des cas, vous ne devriez pas utiliser exec mais je suis plus intéressé à savoir si cela peut être fait plutôt que si cela devrait être fait.
Le comportement que je souhaite est équivalent à:
def exec_and_extract(exec_str, var_name):
locals().update(globals())
exec(EXEC_STR, locals()) # equivalent to exec(EXEC_STR, locals(), locals())
return locals()[var_name]
Mais on me donne à la place:
def exec_and_extract(exec_str, var_name):
exec(EXEC_STR, globals()) # equivalent to exec(EXEC_STR, globals(), globals())
return globals()[var_name]
def exec_and_extract(exec_str, var_name):
exec(EXEC_STR, locals()) # equivalent to exec(EXEC_STR, locals(), locals())
return locals()[var_name]
NameError: le nom 'A' n'est pas défini lors de l'appel de func () depuis A et B existe dans les locals () de exec_and_extract mais le contexte d'exécution lors de l'exécution de A ou B code> est les globals() de exec_and_extract .
def exec_and_extract(exec_str, var_name):
exec(EXEC_STR) # equivalent to exec(EXEC_STR, globals(), locals())
return locals()[var_name]
NameError: name 'GLOBAL_CONSTANT 'n'est pas défini lors de l'appel de A depuis func () car le contexte d'exécution de A est exec_and_extract locals () de code> qui ne contient pas GLOBAL_CONSTANT.
GLOBAL_CONSTANT = 1
EXEC_STR = """
def A():
return GLOBAL_CONSTANT
def B():
return A()
"""
def exec_and_extract(exec_str, var_name):
# Insert code here
func = exec_and_extract(EXEC_STR, 'B')
assert func() == 1
Fonctionne mais pollue l'espace de noms global , pas équivoque nt.
GLOBAL_CONSTANT = 1
def test_func():
def A():
return GLOBAL_CONSTANT
def B():
return A()
return B
func = test_func()
assert func() == 1
Fonctionne mais nécessite de copier l'intégralité du contenu des globals () de exec_and_extract dans son locals () ce qui est une perte de temps si globals () est grand (bien sûr non applicable dans cet exemple artificiel). De plus, ce n'est subtilement pas la même chose que la version "coller dans le code" car si l'un des arguments de exec_and_extract se trouvait être GLOBAL_CONSTANT (un nom d'argument terrible), le comportement serait différent (la version "coller dans" utiliserait la valeur de l'argument tandis que ce code utiliserait la valeur de la constante globale).
Essayer de couvrir les éventuelles "failles" dans le énoncé du problème:
exec_str doit représenter un code arbitraire pouvant accéder à des variables de portée globales ou locales. exec_str . exec_and_extract (dans l'espace de noms global ou autrement). c'est-à-dire que dans cet exemple, l'exécution de EXEC_STR ne doit pas laisser A dans les parages pour pouvoir être référencé dans les futurs appels à exec_and_extract . 3 Réponses :
C'est impossible. exec interagit mal avec les mécanismes de portée des variables locales, et il est beaucoup trop restreint pour que quelque chose comme ça fonctionne. En fait, littéralement, toute opération de liaison de variable locale dans la chaîne exécutée est un comportement non défini , y compris l'affectation simple, les définitions de fonction, les définitions de classe, les importations, et plus encore, si vous appelez exec avec les locaux par défaut. Citant la documentation :
Les locals par défaut agissent comme décrit pour la fonction locals () ci-dessous: les modifications du dictionnaire des locals par défaut ne doivent pas être tentées. Passez un dictionnaire de locals explicite si vous avez besoin de voir les effets du code sur les locals après le retour de la fonction exec ().
De plus, le code exécuté par exec ne peut pas retourner , casser , yield , ou effectuer autre flux de contrôle au nom de l'appelant. Il peut casser les boucles qui font partie du code exécuté, ou retourner à partir des fonctions définies dans le code exécuté, mais il ne peut pas interagir avec le flux de contrôle de son appelant.
Si vous êtes prêt à sacrifier l'exigence de pouvoir interagir avec les locaux de la fonction appelante (comme vous l'avez mentionné dans les commentaires), et que vous ne vous souciez pas d'interagir avec le flux de contrôle de l'appelant, alors vous pourrait insérer l'AST du code dans le corps d'une nouvelle définition de fonction et l'exécuter:
import ast
import sys
def exec_and_extract(code_string, var):
original_ast = ast.parse(code_string)
new_ast = ast.parse('def f(): return ' + var)
fdef = new_ast.body[0]
fdef.body = original_ast.body + fdef.body
code_obj = compile(new_ast, '<string>', 'exec')
gvars = sys._getframe(1).f_globals
lvars = {}
exec(code_obj, gvars, lvars)
return lvars['f']()
J'ai utilisé une approche basée sur AST au lieu du formatage de chaîne pour éviter des problèmes comme accidentellement insérer une indentation supplémentaire dans des chaînes entre guillemets triples dans l'entrée.
inspect nous permet d'utiliser les globaux de celui qui a appelé exec_and_extract , plutôt que exec_and_extract 's propres globals, même si l'appelant est dans un module différent.
Les fonctions définies dans le code exécuté voient les globals réels plutôt qu'une copie.
Le supplément la fonction wrapper dans l'AST modifié évite certains problèmes de portée qui se produiraient autrement; en particulier, B ne pourrait pas voir la définition de A dans votre exemple de code.
Merci pour la réponse rapide! Et si je relâche les exigences pour que ce ne soit pas équivalent mais qu'il soit toujours exécutable. En particulier, je n'ai pas besoin que la chaîne exec ed accède ou modifie les locals () de la fonction exec_and_extract mais je veux quand même qu'elle puisse accéder aux variables de portée globale et aux autres variables définies dans la chaîne exec ed (comme B accédant à A ). Par conséquent, il n'est plus obligatoire d'utiliser les locaux () par défaut.
@ nj3: Il y a encore des conflits. Si vous voulez que le code exécuté voie la portée de la variable globale d'origine plutôt qu'une copie, vous devez passer les globals () réels à exec plutôt qu'une copie. Si vous ne voulez pas que quelque chose de défini dans le code exécuté pollue les globaux, vous devez passer un dict local (non par défaut). Cela, à son tour, signifie que les fonctions définies dans le code exécuté ne peuvent voir que les globaux, pas les locaux (cela est dû à l'interaction des mécanismes d'exécution et de fermeture). En particulier, les fonctions définies dans le code exécuté ne peuvent pas s'appeler.
Je vois, donc la conclusion est que la sémantique exacte de la définition ajustée du problème (voir les variables globales et autres variables définies dans le exec_str ) n'est pas possible. Comme indiqué par les autres, si vous ne voyez pas les modifications futures des variables de portée globale, il est possible de faire du dict local (non par défaut) une copie de globals () (ce qui devrait être rapide) et passez cela à exec . Merci encore pour votre aide!
@ user2357112supportsMonica putain, je pensais peut-être que quelque chose comme passer collections.ChainMap ({}, globals ()) pour ne pas polluer l'espace de noms global mais y avoir toujours un accès en lecture serait un hack intelligent , mais hélas, l'argument globals doit être un dict
@ juanpa.arrivillaga Haha, j'envisageais également une sorte de solution de carte de chaîne pour la même raison mais malheureusement pas possible.
Une chose qui pourrait vous rapprocher: demandez à la fonction de modifier la chaîne exécutée pour placer tout le code exécuté dans un nouveau corps de fonction. Python serait alors capable d'analyser les locaux de cette nouvelle fonction, évitant ainsi le dernier problème.
Vous pouvez vous frayer un chemin autour du problème de "la carte de la chaîne n'est pas un dict" en créant une nouvelle classe qui hérite des deux: class ChainMapDict (collections.ChainMap, dict): pass . De toute évidence, ChainMap n'a pas été conçu avec ce type d'utilisation à l'esprit, mais après avoir un peu joué avec ChainMapDict, il semble fonctionner de la même manière qu'un ChainMap régulier pour autant que je sache, et exec () l'accepte.
@RoadrunnerWMC: La documentation pour exec dit "Si seuls les globals sont fournis, ce doit être un dictionnaire ( et non une sous-classe de dictionnaire ), qui sera utilisé à la fois pour le global et les variables locales. ", de sorte que cela peut être encore moins fiable qu'il n'y paraît.
Fonctionne mais pollue l'espace de noms global, pas équivalent.
Alors, que diriez-vous de faire une copie du dict
globals ()et de récupérerBà partir de celui-ci?def exec_and_extract(exec_str, var_name): env = dict(globals()) env.update(locals()) exec(EXEC_STR, env) return env[var_name]Cela fonctionne toujours et ne pollue pas l'espace de noms global.
Cela se rapproche de l'objectif (modifié), mais n'est toujours pas équivalent. Un problème est que les fonctions définies dans le code exécuté ne peuvent pas voir les mises à jour des variables globales d'origine.
Merci! Voir la dernière tentative infructueuse. Essentiellement équivalent sauf copie les globaux dans les locaux au lieu d'un nouveau dictionnaire (cette solution est certainement meilleure si je ne veux pas jouer avec locals () comme l'a dit l'autre commentateur). Cela dit, il présente le même inconvénient d'exiger une copie de toutes les variables globales. Edit: également d'accord avec ^, très bon point sur les changements apportés aux globaux. Merci!
J'ai modifié ma réponse pour répondre à la question "Et si GLOBAL_CONSTANT était un nom d'argument". En ce qui concerne le code exécuté pouvant voir les changements dans l'espace de noms global (en raison d'autres threads ou quelque chose) ... oui, je doute que cela puisse être complètement résolu, malheureusement.
Merci pour les commentaires. Ya il semble que le problème ne peut pas être résolu complètement. De plus, les changements globaux pourraient simplement être modifiés après l'appel à exec_and_extract avant d'exécuter la fonction retournée, pas besoin de concurrence.
@ user2357112supportsMonica (Répondant au commentaire dans le fil car il contient un bloc de code)
On dirait que quelque chose comme ça pourrait fonctionner:
def exec_and_extract(exec_str, var_name):
env = {}
modified_exec_str = """def wrapper():
{body}
return {var_name}
""".format(body=textwrap.indent(exec_str, ' '), var_name=var_name)
exec(modified_exec_str, globals(), env)
return env['wrapper']()
Cela permet d'accéder à une portée globale, y compris les modifications futures ainsi que l'accès à d'autres variables définies dans exec_str.
J'aurais opté pour le module ast au lieu du formatage de chaîne, pour éviter des problèmes tels que l'insertion accidentelle d'une indentation parasite dans des chaînes triples dans l'entrée. (En fait, j'écris actuellement une approche basée sur ast .)
"ce qui est une perte de temps si globals () est grand" Pas vraiment, non. Je veux dire, vous vous rendez compte, aucun des objets n'est réellement copié.
@ juanpa.arrivillaga Oui bien sûr, ne copie que les valeurs des primitives et les "références" d'objets (je ne sais pas quel est le terme correct) pour tout le reste. Cela n'est peut-être pas aussi cher que je le pensais, mais cela pose également le problème que
user2357112 prend en charge Monicaindiqué ci-dessous où il prend un instantané deglobals ()et ne voit aucun futures mises à jour de ces variables.Non, il ne copie pas les valeurs des "primitives", python n'a pas de "primitives", tout est un objet, et tout utilise la sémantique de référence ici . Mais oui, le problème des instantanés globaux persiste. Je dis simplement que ce n'est certainement pas une opération coûteuse. Au plus, une milliseconde? Et c'est probablement pour un
globals ()inhabituellement grand, sinon vous êtes de l'ordre de centaines de nanosecondes sur n'importe quelle machine moderne.Merci d'avoir clarifié la sémantique de la copie python.