2013-07-25 14 views
33

[Disclaimer: ci possono essere modi più divinatorio di fare quello che voglio fare, ma voglio sapere come scoping del pitone lavora qui]Come iniettare variabile in ambito con un decoratore in python

sto cercando trovare un modo per fare un decoratore che faccia qualcosa come iniettare un nome nell'ambito di un'altra funzione (in modo che il nome non perda fuori dall'ambito del decoratore). Ad esempio, se ho una funzione che dice di stampare una variabile denominata var che non è stata definita, vorrei definirla all'interno di un decoratore dove viene chiamata. Ecco un esempio che rompe:

c = 'Message' 

def decorator_factory(value): 
    def msg_decorator(f): 
     def inner_dec(*args, **kwargs): 
      var = value 
      res = f(*args, **kwargs) 
      return res 
     return inner_dec 
    return msg_decorator 

@decorator_factory(c) 
def msg_printer(): 
    print var 

msg_printer() 

Vorrei per stampare 'Messaggio', ma dà:

NameError: global name 'var' is not defined 

traceback anche punti per wher var è definito:

<ipython-input-25-34b84bee70dc> in inner_dec(*args, **kwargs) 
     8   def inner_dec(*args, **kwargs): 
     9    var = value 
---> 10    res = f(*args, **kwargs) 
    11    return res 
    12   return inner_dec 

quindi non capisco perché non riesca a trovare var.

C'è un modo per fare qualcosa di simile?

risposta

35

Non è possibile. I nomi con scope (chiusure) sono determinati al momento della compilazione, non è possibile aggiungerne altri in fase di runtime.

Il meglio che si può sperare di raggiungere è quello di aggiungere nomi globali, utilizzando proprio namespace globale della funzione:

def decorator_factory(value): 
    def msg_decorator(f): 
     def inner_dec(*args, **kwargs): 
      g = f.__globals__ # use f.func_globals for py < 2.6 
      sentinel = object() 

      oldvalue = g.get('var', sentinel) 
      g['var'] = value 

      try: 
       res = f(*args, **kwargs) 
      finally: 
       if oldvalue is sentinel: 
        del g['var'] 
       else: 
        g['var'] = oldvalue 

      return res 
     return inner_dec 
    return msg_decorator 

f.__globals__ è il namespace globale per la funzione di avvolto, quindi questo funziona, anche se il decoratore vive in un modulo diverso. Se var è già stato definito come globale, viene sostituito con il nuovo valore e, dopo aver chiamato la funzione, vengono ripristinati i globals.

Questo funziona perché qualsiasi nome in una funzione a cui non è assegnato e che non si trova in un ambito circostante, è contrassegnato come globale.

Demo:

>>> c = 'Message' 
>>> @decorator_factory(c) 
... def msg_printer(): 
...  print var 
... 
>>> msg_printer() 
Message 
>>> 'var' in globals() 
False 

Ma invece di decorazione, ho potuto solo così ho definito var nell'ambito globale direttamente.

Si noti che la modifica dei globali non è thread-safe, e qualsiasi chiamata temporanea ad altre funzioni nello stesso modulo vedrà comunque questo stesso globale.

+0

Quindi, se faccio 'def msg_printer(): var' stampa e provare a chiamare' msg_printer', ottengo lo stesso errore di nome, ma se poi mi definisco ' var = 'Ciao' e chiamalo, lo stampa bene. In questo esempio, 'var' non viene definito in fase di esecuzione, dopo che' msg_printer' è stato compilato? – beardc

+0

Poiché 'var' non è definito nella funzione o negli ambiti parent,' var' è segnato, al momento della compilazione, come nome globale. Ma se ci fosse un ambito genitore, allora al momento della compilazione, 'var' verrebbe invece contrassegnato come un nome con scope, a quel punto il trucco decorator non funzionerebbe più neanche. –

+0

Non capisco. 'func_globals' dovrebbe essere di sola lettura, secondo la bella tabella del modello di dati: http://docs.python.org/2/reference/datamodel.html – n611x007

5

Non è possibile. Python ha lo scope lessicale . Ciò significa che il significato di un identificatore è determinato esclusivamente in base agli ambiti che lo circondano fisicamente quando si guarda il codice sorgente.

0

Ecco una semplice dimostrazione dell'utilizzo di un decoratore per aggiungere una variabile nell'ambito di una funzione.

>>> def add_name(name): 
...  def inner(func): 
...   # Same as defining name within wrapped 
...   # function. 
...   func.func_globals['name'] = name 
... 
...   # Simply returns wrapped function reference. 
...   return func 
... 
...  return inner 
... 
>>> @add_name("Bobby") 
... def say_hello(): 
...  print "Hello %s!" % name 
... 
>>> print say_hello() 
Hello Bobby! 
>>> 
+4

Nota che stai manipolando un dizionario condiviso qui. * Altre funzioni nello stesso modulo vedranno anche questa modifica * e la modifica del dizionario non è thread-safe. –

+0

@MartijnPieters È un problema anche se il valore modificato dal decoratore non viene più modificato e viene letto solo dopo che è stato restituito il decoratore? – stackoverflowwww

+1

@stackoverflowwww: modifica le globali del modulo, ogni volta che viene richiamata la funzione. –

3

Python è lessicalmente ambito, quindi ho paura non c'è modo pulito di fare ciò che si vuole senza alcuni effetti collaterali potenzialmente brutto. Raccomando di passare var alla funzione tramite il decoratore.

c = 'Message' 

def decorator_factory(value): 
    def msg_decorator(f): 
     def inner_dec(*args, **kwargs): 
      res = f(value, *args, **kwargs) 
      return res 
     inner_dec.__name__ = f.__name__ 
     inner_dec.__doc__ = f.__doc__ 
     return inner_dec 
    return msg_decorator 

@decorator_factory(c) 
def msg_printer(var): 
    print var 

msg_printer() # prints 'Message' 
+1

Che cosa sono 'msg_decorator .__ name__ = f .__ nome__' e' msg_decorator .__ doc__ = f .__ doc__' per? È necessario? – stackoverflowwww

+2

@stackoverflowwww Ogni funzione in python ha un nome (a meno che non sia stata creata con lambda) e molti hanno docstring. Entrambi sono importanti per generare documentazione, quindi li copiamo sulla funzione wrapper. Ho fatto un errore nella mia risposta, dovrebbero essere copiati su 'inner_dec'. –

+1

Vedere anche 'functools.wraps' per fare questo genere di cose – killthrush

1

C'è un modo pulito di fare quello che vuoi senza usare la variabile globale. Se vuoi essere apolidi e le discussioni sicure, non hai davvero la scelta.

Utilizzare la variabile "kwargs":

c = 'Message' 

def decorator_factory(value): 
    def msg_decorator(f): 
    def inner_dec(*args, **kwargs): 
     kwargs["var"] = value 
     res = f(*args, **kwargs) 
     return res 
    return inner_dec 
return msg_decorator 

@decorator_factory(c) 
def msg_printer(*args, **kwargs): 
    print kwargs["var"] 

msg_printer() 
+0

Come è concettualmente diverso dal passare un argomento posizionale? –

+0

Non è così diverso, ma come è scritto per argomento posizionale dovresti conoscere la posizione del tuo argomento. Dato che è un decoratore, non lo sai. ** kwargs ** è il modo più sicuro nel punto di vista perché controlli il nome del tuo argomento. – M07

+0

Questa è sicuramente una spiegazione abbastanza buona per me per rimuovere il mio downvote. Non sono sicuro di poter sopravvalutare questo nel contesto della risposta accettata. –

Problemi correlati