2014-11-04 10 views
7

Permettetemi innanzitutto di riconoscere che quello che voglio fare può essere considerato qualsiasi cosa da sciocco a cattivo, ma voglio scoprire se posso farlo comunque in Python.Come può un decoratore passare le variabili in una funzione senza cambiarne la firma?

Diciamo che ho un decoratore di funzioni che accetta gli argomenti delle parole chiave che definiscono le variabili e voglio accedere a quelle variabili nella funzione di avvolgimento. Potrei fare qualcosa di simile:

def more_vars(**extras): 
    def wrapper(f): 
     @wraps(f) 
     def wrapped(*args, **kwargs): 
      return f(extras, *args, **kwargs) 
     return wrapped 
    return wrapper 

Ora posso fare qualcosa di simile:

@more_vars(a='hello', b='world') 
def test(deco_vars, x, y): 
    print(deco_vars['a'], deco_vars['b']) 
    print(x, y) 

test(1, 2) 
# Output: 
# hello world 
# 1 2 

La cosa che non mi piace di questo è che quando si utilizza questo decoratore, è necessario cambia la firma della chiamata della funzione, aggiungendo la variabile extra oltre allo schiaffo sul decoratore. Inoltre, se si guarda al l'aiuto per la funzione, si vede una variabile in più che non ci si aspetta di utilizzare quando si chiama la funzione:

help(test) 
# Output: 
# Help on function test in module __main__: 
# 
# test(deco_vars, x, y) 

Questo lo fa apparire come l'utente deve chiamare la funzione con 3 parametri, ma ovviamente non funzionerà. Quindi dovresti anche aggiungere un messaggio alla docstring che indica che il primo parametro non fa parte dell'interfaccia, è solo un dettaglio di implementazione e dovrebbe essere ignorato. Questo è un po 'schifoso, però. C'è un modo per farlo senza appendere queste variabili su qualcosa nella portata globale? Idealmente, mi piacerebbe farlo sembrare come il seguente:

@more_vars(a='hello', b='world') 
def test(x, y): 
    print(a, b) 
    print(x, y) 

test(1, 2) 
# Output: 
# hello world 
# 1 2 
help(test) 
# Output: 
# Help on function test in module __main__: 
# 
# test(x, y) 

io sono contento di un Python 3 unica soluzione se ne esiste uno.

+1

Forse dovresti descrivere di più la tua necessità. Avrai solo una possibilità di decorare la funzione, quindi qual è il vantaggio di mettere 'a = 'hello'' nel decoratore, invece di metterlo come prima riga della funzione? –

+0

Sembra che tu stia chiedendo come inserire "extra" nel test di chiusura dopo che è già stato compilato, senza dover modificare in alcun modo la definizione di "test". Se è così, non penso che sarà possibile senza alcuni terribili hack. – abarnert

+0

Ma usando una macro [MacroPy] (https://github.com/lihaoyi/macropy) invece di un decoratore, immagino che sarebbe facile ... Sarebbe una risposta accettabile? – abarnert

risposta

0

Non mi piace l'idea di iniettare i locali nello spazio dei nomi. Sebbene sia probabilmente possibile, sarebbe brutto gestire le collisioni tra i nuovi locali e i nomi che sono già presenti nella funzione.

È accettabile archiviarli come attrs sull'oggetto funzione? In questo modo sono assegnati ai nomi.

from functools import wraps 

def more_vars(**extras): 
    def wrapper(f): 
     @wraps(f) 
     def wrapped(*args, **kwargs): 
      return f(*args, **kwargs) 
     for k,v in extras.items(): 
      setattr(wrapped, k, v) 
     return wrapped 
    return wrapper 

@more_vars(a='hello', b='world') 
def test(x, y): 
    print(test.a, test.b) 
    print(x, y) 
+0

Per prima cosa ho pensato di archiviarli come funzioni attrs, ma il mio problema è che rende le definizioni delle funzioni non più "autonome" in un certo senso. Se copi e incolli la definizione della funzione di test, rinominandola "test2", devi anche assicurarti di rinominare tutte le istanze di "test" nella definizione. Questo può portare a errori di copia e incolla che preferisco evitare. – user108471

+0

Ciò è facilmente risolvibile .. il nome della funzione è disponibile dinamicamente tramite 'inspect.currentframe() .f_code.co_name' o' inspect.stack', quindi è possibile ottenere un handle sull'oggetto funzione senza dover codificare il nome della funzione . – wim

+0

Questa non è una cattiva idea, tranne per il fatto che aggiunge ancora il codice boilerplate a tutte le funzioni che vogliono utilizzare il decoratore. Sarebbe bello se tutti gli shenanigans rilevanti potessero essere gestiti dallo stesso decoratore in modo che l'utente del decoratore non debba cambiare la funzione affatto oltre l'aggiunta del decoratore. – user108471

2

Si potrebbe fare questo con qualche inganno che inserisce le variabili passate per il decoratore in variabili locali della funzione:

import sys 
from functools import wraps 
from types import FunctionType 


def is_python3(): 
    return sys.version_info >= (3, 0) 


def more_vars(**extras): 
    def wrapper(f): 
     @wraps(f) 
     def wrapped(*args, **kwargs): 
      fn_globals = {} 
      fn_globals.update(globals()) 
      fn_globals.update(extras) 
      if is_python3(): 
       func_code = '__code__' 
      else: 
       func_code = 'func_code' 
      call_fn = FunctionType(getattr(f, func_code), fn_globals) 
      return call_fn(*args, **kwargs) 
     return wrapped 
    return wrapper 


@more_vars(a="hello", b="world") 
def test(x, y): 
    print("locals: {}".format(locals())) 
    print("x: {}".format(x)) 
    print("y: {}".format(y)) 
    print("a: {}".format(a)) 
    print("b: {}".format(b)) 


if __name__ == "__main__": 
    test(1, 2) 

può fare questo? Sicuro! Dovresti farlo? Probabilmente no!

(Codice disponibili here.)

+0

Questa sembra una soluzione decente, ma penso di vedere un problema. Se si tenta di assegnare una delle variabili iniettate con more_vars, verrà creata una variabile di ambito locale con lo stesso nome che ombreggia il "falso" globale immesso ... a meno che non si aggiungano anche dichiarazioni globali nella funzione. C'è un modo per aggirare questo? – user108471

+0

Questo è esattamente il motivo per cui il tuo design è una cattiva idea, in primo luogo. Perché vuoi rompere lo spazio dei nomi? Per quale possibile caso d'uso questa potrebbe essere una buona idea? Hai preso in considerazione l'utilizzo di una classe richiamabile e l'iniezione di nuovi oggetti in modo semplice ed esplicito, piuttosto che gironzolare con i decoratori? – wim

+0

Se qualcosa è difficile da attuare, c'è sempre una tendenza a rispondere con un istante "non dovresti farlo in primo luogo" senza capire le motivazioni che ci sono dietro. Ho capito, i problemi difficili sono sicuramente più facili da risolvere quando li si cambia in quelli diversi e più facili. Ma non sto chiedendo altre strategie per ottenere la funzionalità che sto cercando, sto cercando di capire se questa strategia può essere implementata. Se non conosci alcun modo per farlo con i decoratori, va bene, ma per favore non chiedere un saggio che spieghi tutte le mie motivazioni che portano a questo punto. – user108471

0

Sembra che il tuo unico problema è che help sta mostrando la firma del grezzo test come la firma della funzione avvolto, e non si desidera.

L'unica ragione che sta accadendo è che wraps (o, meglio, update_wrapper, che wraps chiamate) esplicitamente copie questo dal wrappee per l'involucro.

È possibile decidere esattamente cosa si fa e non si desidera copiare. Se quello che vuoi fare in modo diverso è abbastanza semplice, è solo questione di filtrare le cose fuori dallo standard WRAPPER_ASSIGNMENTS e WRAPPER_UPDATES.Se vuoi cambiare altre cose, potresti dover inserire il numero update_wrapper e utilizzare la tua versione, ma lo functools è uno di quei moduli che ha un collegamento a the source proprio nella parte superiore della documentazione, perché è pensato per essere usato come esempio leggibile codice.

Nel tuo caso, può essere solo una questione di wraps(f, updated=[]), oppure si può decidere di fare qualcosa di fantasia, come l'uso inspect.signature per ottenere la firma di f, e modificarlo per rimuovere il primo parametro, e costruire un involucro in modo esplicito intorno a questo per ingannare anche il modulo inspect.

+0

Questo non è l'unico problema. Ho anche scritto nella mia descrizione iniziale del problema: "La cosa che non mi piace di questo è che quando usi questo decoratore, devi cambiare la firma di chiamata della funzione, aggiungendo la variabile extra oltre allo slapping sul decoratore." Il problema con "aiuto" segue direttamente quella frase, ma si tratta di due problemi separati. Nell'ultimo snippet di codice, mostro cosa mi piacerebbe che fosse. – user108471

Problemi correlati