exec () bytecode avec les habitants arbitraires?
-
16-09-2019 - |
Question
Supposons que je veuille exécuter du code, par exemple
value += 5
à l'intérieur d'un espace de noms de mon propre (si le résultat est essentiellement mydict['value'] += 5
). Il y a une exec()
fonction, mais je dois passer une chaîne là:
exec('value += 5', mydict)
et les états passant comme des chaînes semble étrange (par exemple il n'est pas colorisé de cette façon). Peut-il être fait comme:
def block():
value += 5
???(block, mydict)
? Le candidat évident pour la dernière ligne a été exec(block.__code__, mydict)
, mais pas de chance: elle soulève UnboundLocalError
au sujet value
. Je crois qu'il exécute essentiellement block()
, pas le code dans le bloc , si les affectations ne sont pas faciles - est-ce pas
Bien sûr, une autre solution possible serait de démonter block.__code__
...
Pour votre information, je suis arrivé à la question en raison de ce fil . En outre, c'est pourquoi certains (moi indécis) appel à une nouvelle syntaxe
using mydict:
value += 5
Notez comment cela ne jette pas l'erreur, mais ne change pas mydict
soit:
def block(value = 0):
value += 5
block(**mydict)
La solution
Vous pouvez passer bytecode au lieu d'une chaîne à exec
, il vous suffit de faire le bon bytecode dans le but:
>>> bytecode = compile('value += 5', '<string>', 'exec')
>>> mydict = {'value': 23}
>>> exec(bytecode, mydict)
>>> mydict['value']
28
Plus précisément, ...:
>>> import dis
>>> dis.dis(bytecode)
1 0 LOAD_NAME 0 (value)
3 LOAD_CONST 0 (5)
6 INPLACE_ADD
7 STORE_NAME 0 (value)
10 LOAD_CONST 1 (None)
13 RETURN_VALUE
les instructions de chargement et de stockage doivent être de la persuasion _NAME, et ce qui les rend si compile
, alors ...:
>>> def f(): value += 5
...
>>> dis.dis(f.func_code)
1 0 LOAD_FAST 0 (value)
3 LOAD_CONST 1 (5)
6 INPLACE_ADD
7 STORE_FAST 0 (value)
10 LOAD_CONST 0 (None)
13 RETURN_VALUE
... code dans une fonction est optimisée pour utiliser les versions _FAST, et ceux qui ne fonctionnent pas sur un dict passé à exec
. Si vous avez commencé en quelque sorte avec un bytecode en suivant les instructions de _FAST, vous pouvez patcher d'utiliser le _NAME type au lieu, par exemple avec bytecodehacks ou une approche similaire.
Autres conseils
Utilisez le mot-clé global
pour forcer la portée dynamique sur toutes les variables que vous souhaitez modifier à partir du bloc:
def block():
global value
value += 5
mydict = {"value": 42}
exec(block.__code__, mydict)
print(mydict["value"])
Voici un décorateur fou de créer un tel bloc qui utilise des « locaux personnalisés ». En réalité, il est un hack pour transformer tous les accès variable à l'intérieur de la fonction d'accès global, et d'évaluer le résultat avec les sections locales de dictionnaire personnalisé comme environnement.
import dis
import functools
import types
import string
def withlocals(func):
"""Decorator for executing a block with custom "local" variables.
The decorated function takes one argument: its scope dictionary.
>>> @withlocals
... def block():
... counter += 1
... luckynumber = 88
>>> d = {"counter": 1}
>>> block(d)
>>> d["counter"]
2
>>> d["luckynumber"]
88
"""
def opstr(*opnames):
return "".join([chr(dis.opmap[N]) for N in opnames])
translation_table = string.maketrans(
opstr("LOAD_FAST", "STORE_FAST"),
opstr("LOAD_GLOBAL", "STORE_GLOBAL"))
c = func.func_code
newcode = types.CodeType(c.co_argcount,
0, # co_nlocals
c.co_stacksize,
c.co_flags,
c.co_code.translate(translation_table),
c.co_consts,
c.co_varnames, # co_names, name of global vars
(), # co_varnames
c.co_filename,
c.co_name,
c.co_firstlineno,
c.co_lnotab)
@functools.wraps(func)
def wrapper(mylocals):
return eval(newcode, mylocals)
return wrapper
if __name__ == '__main__':
import doctest
doctest.testmod()
Ceci est juste une adaptation de singe ragréage de recette brillante de quelqu'un pour une goto décorateur
Du commentaire de S. Lott ci-dessus, je pense que je reçois l'idée d'une réponse en utilisant la création d'une nouvelle catégorie.
class _(__metaclass__ = change(mydict)):
value += 1
...
où change
est un métaclasse dont __prepare__
lit le dictionnaire et dont le dictionnaire des mises à jour __new__
.
Pour la réutilisation, l'extrait ci-dessous fonctionnerait, mais il est un peu laid:
def increase_value(d):
class _(__metaclass__ = change(d)):
value += 1
...
increase_value(mydict)