2009-08-14 5 views
2

Supposons que je veuille exécuter du code, par exempleexec() bytecode avec des locales arbitraires?

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 fonction exec(), mais je dois passer une chaîne là:

exec('value += 5', mydict) 

et déclarations 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 était exec(block.__code__, mydict), mais pas de chance: il soulève UnboundLocalError environ value. Je crois qu'il exécute essentiellement block(), pas le code à l'intérieur du bloc, donc les affectations ne sont pas faciles - est-ce exact?

Bien sûr, une autre solution possible serait de démonter block.__code__ ...

Pour votre information, je suis arrivé à la question en raison de this thread. 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) 
+0

Quel est le problème avec les définitions de classes simples? –

+0

Je ne suis pas sûr de ce que vous voulez dire à propos des cours. Je vais essayer de poster une réponse décrivant une idée possible que vous pourriez vouloir dire, mais n'hésitez pas à me corriger. –

Répondre

6

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 compile les rend si, alors que ...:

>>> 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 une fonction est optimisée pour utiliser les versions _FAST, et celles-ci ne fonctionnent pas sur une dict passée à exec. Si vous avez démarré d'une manière ou d'une autre avec un bytecode en utilisant les instructions _FAST, vous pouvez le patcher pour utiliser le type _NAME à la place, par ex. avec bytecodehacks ou une approche similaire.

+0

On dirait que pirater le bytecode est une bonne idée dans ce cas, d'autant plus que la sémantique de ce que je veux, la fonction 'apply_code_to_dict' est simple. –

3

Utilisez le mot-clé global pour forcer la portée dynamique sur toutes les variables que vous souhaitez modifier à l'intérieur du bloc:

def block(): 
    global value 
    value += 5 

mydict = {"value": 42} 
exec(block.__code__, mydict) 
print(mydict["value"]) 
0

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 de nouvelles classe.

class _(__metaclass__ = change(mydict)): 
    value += 1 
    ... 

change est un métaclasse dont __prepare__ lit le dictionnaire et dont le dictionnaire __new__ mises à jour.

Pour la réutilisation, l'extrait ci-dessous fonctionnerait, mais il est le genre de laid:

def increase_value(d): 
    class _(__metaclass__ = change(d)): 
     value += 1 
     ... 

increase_value(mydict) 
3

est ici un décorateur fou de créer un tel bloc qui utilise des « locaux personnalisés ». En réalité, c'est un hack rapide pour transformer tous les accès variables à l'intérieur de la fonction en accès global, et évaluer le résultat avec le dictionnaire des locals personnalisés en tant qu'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 decorator

+0

plutôt que d'un décorateur, une utilisation plus saine pourrait faire juste une fonction appel 'withlocals (bloc, d)' – u0b34a0f6ae

+0

Wow, c'est bien. –

+0

merci. Je suspecte que Martelli en sache beaucoup plus que moi, je suis juste parvenu à assembler quelque chose qui "marche" (honte de l'appeler) ... peut-être que LOAD_NAME vaut mieux que LOAD_GLOBAL? – u0b34a0f6ae

Questions connexes