Come creare un decoratore Python che può essere utilizzato con o senza parametri?


90

Vorrei creare un decoratore Python che può essere utilizzato con i parametri:

@redirect_output("somewhere.log")
def foo():
    ....

o senza di loro (ad esempio per reindirizzare l'output a stderr per impostazione predefinita):

@redirect_output
def foo():
    ....

È possibile?

Nota che non sto cercando una soluzione diversa al problema del reindirizzamento dell'output, è solo un esempio della sintassi che vorrei ottenere.


L'aspetto predefinito @redirect_outputè notevolmente disinformativo. Suggerirei che sia una cattiva idea. Usa la prima forma e semplifica molto la tua vita.
S.Lott

domanda interessante però - fino a quando non l'ho visto e ho guardato attraverso la documentazione, avrei supposto che @f fosse lo stesso di @f (), e penso ancora che dovrebbe esserlo, ad essere onesto (qualsiasi argomento fornito sarebbe stato semplicemente appiccicato sull'argomento della funzione)
rog

Risposte:


68

So che questa domanda è vecchia, ma alcuni commenti sono nuovi e, sebbene tutte le soluzioni praticabili siano essenzialmente le stesse, la maggior parte di esse non sono molto chiare o facili da leggere.

Come dice la risposta di thobe, l'unico modo per gestire entrambi i casi è controllare entrambi gli scenari. Il modo più semplice è semplicemente controllare se c'è un singolo argomento ed è callabe (NOTA: saranno necessari controlli extra se il tuo decoratore accetta solo 1 argomento e sembra essere un oggetto richiamabile):

def decorator(*args, **kwargs):
    if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):
        # called as @decorator
    else:
        # called as @decorator(*args, **kwargs)

Nel primo caso, fai quello che fa un normale decoratore, restituisci una versione modificata o incartata della funzione passata.

Nel secondo caso, restituisci un "nuovo" decoratore che in qualche modo utilizza le informazioni passate con * args, ** kwargs.

Va bene tutto, ma doverlo scrivere per ogni decoratore che fai può essere piuttosto fastidioso e non così pulito. Invece, sarebbe bello poter modificare automaticamente i nostri decoratori senza doverli riscrivere ... ma è a questo che servono i decoratori!

Utilizzando il seguente decoratore decoratore, possiamo deocratizzare i nostri decoratori in modo che possano essere utilizzati con o senza argomenti:

def doublewrap(f):
    '''
    a decorator decorator, allowing the decorator to be used as:
    @decorator(with, arguments, and=kwargs)
    or
    @decorator
    '''
    @wraps(f)
    def new_dec(*args, **kwargs):
        if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):
            # actual decorated function
            return f(args[0])
        else:
            # decorator arguments
            return lambda realf: f(realf, *args, **kwargs)

    return new_dec

Ora possiamo decorare i nostri decoratori con @doublewrap, e lavoreranno con e senza argomenti, con un avvertimento:

Ho notato sopra ma dovrei ripetere qui, il controllo in questo decoratore fa un'ipotesi sugli argomenti che un decoratore può ricevere (vale a dire che non può ricevere un singolo argomento richiamabile). Dal momento che lo stiamo rendendo applicabile a qualsiasi generatore ora, deve essere tenuto presente o modificato se sarà contraddetto.

Quanto segue dimostra il suo utilizzo:

def test_doublewrap():
    from util import doublewrap
    from functools import wraps    

    @doublewrap
    def mult(f, factor=2):
        '''multiply a function's return value'''
        @wraps(f)
        def wrap(*args, **kwargs):
            return factor*f(*args,**kwargs)
        return wrap

    # try normal
    @mult
    def f(x, y):
        return x + y

    # try args
    @mult(3)
    def f2(x, y):
        return x*y

    # try kwargs
    @mult(factor=5)
    def f3(x, y):
        return x - y

    assert f(2,3) == 10
    assert f2(2,5) == 30
    assert f3(8,1) == 5*7

31

Usare argomenti di parole chiave con valori predefiniti (come suggerito da kquinn) è una buona idea, ma richiederà di includere le parentesi:

@redirect_output()
def foo():
    ...

Se desideri una versione che funzioni senza le parentesi sul decoratore, dovrai tenere conto di entrambi gli scenari nel tuo codice decoratore.

Se stavi usando Python 3.0 potresti usare solo argomenti di parole chiave per questo:

def redirect_output(fn=None,*,destination=None):
  destination = sys.stderr if destination is None else destination
  def wrapper(*args, **kwargs):
    ... # your code here
  if fn is None:
    def decorator(fn):
      return functools.update_wrapper(wrapper, fn)
    return decorator
  else:
    return functools.update_wrapper(wrapper, fn)

In Python 2.x questo può essere emulato con i trucchi varargs:

def redirected_output(*fn,**options):
  destination = options.pop('destination', sys.stderr)
  if options:
    raise TypeError("unsupported keyword arguments: %s" % 
                    ",".join(options.keys()))
  def wrapper(*args, **kwargs):
    ... # your code here
  if fn:
    return functools.update_wrapper(wrapper, fn[0])
  else:
    def decorator(fn):
      return functools.update_wrapper(wrapper, fn)
    return decorator

Ognuna di queste versioni ti consentirebbe di scrivere codice come questo:

@redirected_output
def foo():
    ...

@redirected_output(destination="somewhere.log")
def bar():
    ...

1
Cosa ci metti dentro your code here? Come si chiama la funzione decorata? fn(*args, **kwargs)non funziona.
lum

Penso che ci sia una risposta molto più semplice, creare una classe che sarà il decoratore con argomenti opzionali. crea un'altra funzione con gli stessi argomenti con i valori di default e restituisce una nuova istanza delle classi decorator. dovrebbe assomigliare a qualcosa del genere: def f(a = 5): return MyDecorator( a = a) e class MyDecorator( object ): def __init__( self, a = 5 ): .... scusa è difficile scriverlo in un commento, ma spero che sia abbastanza semplice da capire
Omer Ben Haim

17

So che questa è una vecchia domanda, ma davvero non mi piace nessuna delle tecniche proposte, quindi ho voluto aggiungere un altro metodo. Ho visto che django usa un metodo molto pulito nel suo login_requireddecoratore indjango.contrib.auth.decorators . Come si può vedere nella documentazione del decoratore , può essere usato da solo come @login_requiredo con argomenti, @login_required(redirect_field_name='my_redirect_field').

Il modo in cui lo fanno è abbastanza semplice. Aggiungono una kwarg( function=None) prima degli argomenti del decoratore. Se il decoratore viene utilizzato da solo, functionsarà la funzione effettiva che sta decorando, mentre se viene chiamato con argomenti, functionlo sarà None.

Esempio:

from functools import wraps

def custom_decorator(function=None, some_arg=None, some_other_arg=None):
    def actual_decorator(f):
        @wraps(f)
        def wrapper(*args, **kwargs):
            # Do stuff with args here...
            if some_arg:
                print(some_arg)
            if some_other_arg:
                print(some_other_arg)
            return f(*args, **kwargs)
        return wrapper
    if function:
        return actual_decorator(function)
    return actual_decorator

@custom_decorator
def test1():
    print('test1')

>>> test1()
test1

@custom_decorator(some_arg='hello')
def test2():
    print('test2')

>>> test2()
hello
test2

@custom_decorator(some_arg='hello', some_other_arg='world')
def test3():
    print('test3')

>>> test3()
hello
world
test3

Trovo che questo approccio utilizzato da django sia più elegante e più facile da capire rispetto a qualsiasi altra tecnica qui proposta.


Sì, mi piace questo metodo. Fare nota che si deve usare kwargs quando si chiama il decoratore altrimenti la prima arg posizionale è assegnato a functione poi le cose si rompono perché i tentativi decoratore di chiamare quel primo arg posizionale come se fosse la vostra funzione decorato.
Dustin Wyatt

12

È necessario rilevare entrambi i casi, ad esempio utilizzando il tipo del primo argomento, e di conseguenza restituire il wrapper (se utilizzato senza parametro) o un decoratore (se utilizzato con argomenti).

from functools import wraps
import inspect

def redirect_output(fn_or_output):
    def decorator(fn):
        @wraps(fn)
        def wrapper(*args, **args):
            # Redirect output
            try:
                return fn(*args, **args)
            finally:
                # Restore output
        return wrapper

    if inspect.isfunction(fn_or_output):
        # Called with no parameter
        return decorator(fn_or_output)
    else:
        # Called with a parameter
        return decorator

Quando si utilizza la @redirect_output("output.log")sintassi, redirect_outputviene chiamato con un singolo argomento "output.log"e deve restituire un decoratore che accetta la funzione da decorare come argomento. Quando viene utilizzato come @redirect_output, viene chiamato direttamente con la funzione da decorare come argomento.

O in altre parole: la @sintassi deve essere seguita da un'espressione il cui risultato è una funzione che accetta una funzione da decorare come suo unico argomento e restituisce la funzione decorata. L'espressione stessa può essere una chiamata di funzione, come nel caso di @redirect_output("output.log"). Contorto, ma vero :-)


9

Diverse risposte qui affrontano già bene il tuo problema. Per quanto riguarda lo stile, tuttavia, preferisco risolvere questo problema del decoratore usando functools.partial, come suggerito nel Python Cookbook 3 di David Beazley :

from functools import partial, wraps

def decorator(func=None, foo='spam'):
    if func is None:
         return partial(decorator, foo=foo)

    @wraps(func)
    def wrapper(*args, **kwargs):
        # do something with `func` and `foo`, if you're so inclined
        pass

    return wrapper

Mentre sì, puoi semplicemente farlo

@decorator()
def f(*args, **kwargs):
    pass

senza stravaganti soluzioni alternative, lo trovo strano e mi piace avere la possibilità di decorare semplicemente con @decorator.

Per quanto riguarda l'obiettivo della missione secondaria, il reindirizzamento dell'output di una funzione è affrontato in questo post di Stack Overflow .


Se vuoi approfondire, dai un'occhiata al Capitolo 9 (Metaprogrammazione) in Python Cookbook 3 , che è liberamente disponibile per essere letto online .

Parte di quel materiale è dimostrata dal vivo (e molto altro!) Nel fantastico video YouTube di Beazley Python 3 Metaprogramming .

Buona codifica :)


8

Un decoratore Python viene chiamato in un modo fondamentalmente diverso a seconda che tu gli dia argomenti o meno. La decorazione è in realtà solo un'espressione (sintatticamente limitata).

Nel tuo primo esempio:

@redirect_output("somewhere.log")
def foo():
    ....

la funzione redirect_outputviene chiamata con l'argomento dato, che dovrebbe restituire una funzione decoratore, che a sua volta viene chiamata con foocome argomento, che (finalmente!) dovrebbe restituire la funzione decorata finale.

Il codice equivalente è simile a questo:

def foo():
    ....
d = redirect_output("somewhere.log")
foo = d(foo)

Il codice equivalente per il tuo secondo esempio ha il seguente aspetto:

def foo():
    ....
d = redirect_output
foo = d(foo)

Quindi puoi fare ciò che desideri ma non in modo totalmente fluido:

import types
def redirect_output(arg):
    def decorator(file, f):
        def df(*args, **kwargs):
            print 'redirecting to ', file
            return f(*args, **kwargs)
        return df
    if type(arg) is types.FunctionType:
        return decorator(sys.stderr, arg)
    return lambda f: decorator(arg, f)

Questo dovrebbe essere ok a meno che tu non voglia usare una funzione come argomento per il tuo decoratore, nel qual caso il decoratore presumerà erroneamente di non avere argomenti. Fallirà anche se questa decorazione viene applicata a un'altra decorazione che non restituisce un tipo di funzione.

Un metodo alternativo consiste semplicemente nel richiedere che la funzione decorator sia sempre chiamata, anche se non ha argomenti. In questo caso, il tuo secondo esempio sarebbe simile a questo:

@redirect_output()
def foo():
    ....

Il codice della funzione decoratore sarebbe simile a questo:

def redirect_output(file = sys.stderr):
    def decorator(file, f):
        def df(*args, **kwargs):
            print 'redirecting to ', file
            return f(*args, **kwargs)
        return df
    return lambda f: decorator(file, f)

2

In effetti, il caso di avvertenza nella soluzione di @ bj0 può essere verificato facilmente:

def meta_wrap(decor):
    @functools.wraps(decor)
    def new_decor(*args, **kwargs):
        if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):
            # this is the double-decorated f. 
            # Its first argument should not be a callable
            doubled_f = decor(args[0])
            @functools.wraps(doubled_f)
            def checked_doubled_f(*f_args, **f_kwargs):
                if callable(f_args[0]):
                    raise ValueError('meta_wrap failure: '
                                'first positional argument cannot be callable.')
                return doubled_f(*f_args, **f_kwargs)
            return checked_doubled_f 
        else:
            # decorator arguments
            return lambda real_f: decor(real_f, *args, **kwargs)

    return new_decor

Ecco alcuni casi di test per questa versione a prova di errore di meta_wrap.

    @meta_wrap
    def baddecor(f, caller=lambda x: -1*x):
        @functools.wraps(f)
        def _f(*args, **kwargs):
            return caller(f(args[0]))
        return _f

    @baddecor  # used without arg: no problem
    def f_call1(x):
        return x + 1
    assert f_call1(5) == -6

    @baddecor(lambda x : 2*x) # bad case
    def f_call2(x):
        return x + 1
    f_call2(5)  # raises ValueError

    # explicit keyword: no problem
    @baddecor(caller=lambda x : 100*x)
    def f_call3(x):
        return x + 1
    assert f_call3(5) == 600

1
Grazie. Questo è utile!
Pragy Agarwal

0

Per dare una risposta più completa rispetto a quanto sopra:

"C'è un modo per costruire un decoratore che possa essere usato con e senza argomenti?"

No, non esiste un modo generico perché attualmente manca qualcosa nel linguaggio Python per rilevare i due diversi casi d'uso.

Tuttavia Sì, come già sottolineato da altre risposte come bj0s , c'è una goffa soluzione alternativa che consiste nel controllare il tipo e il valore del primo argomento posizionale ricevuto (e controllare se nessun altro argomento ha un valore non predefinito). Se hai la certezza che gli utenti non passeranno mai un callable come primo argomento del tuo decoratore, puoi utilizzare questa soluzione alternativa. Nota che questo è lo stesso per i decoratori di classe (sostituisci chiamabile con classe nella frase precedente).

Per essere sicuro di quanto sopra, ho fatto un bel po 'di ricerche là fuori e ho persino implementato una libreria denominata decopatchche utilizza una combinazione di tutte le strategie sopra citate (e molte altre, inclusa l'introspezione) per eseguire "qualunque sia la soluzione più intelligente" a seconda in base alle tue necessità.

Ma francamente la cosa migliore sarebbe non aver bisogno di alcuna libreria qui e ottenere quella funzionalità direttamente dal linguaggio Python. Se, come me, pensi che sia un peccato che il linguaggio python non sia oggi in grado di fornire una risposta chiara a questa domanda, non esitare a supportare questa idea nel bugtracker python : https: //bugs.python .org / issue36553 !

Grazie mille per il tuo aiuto nel rendere Python un linguaggio migliore :)


0

Questo fa il lavoro senza problemi:

from functools import wraps

def memoize(fn=None, hours=48.0):
  def deco(fn):
    @wraps(fn)
    def wrapper(*args, **kwargs):
      return fn(*args, **kwargs)
    return wrapper

  if callable(fn): return deco(fn)
  return deco

0

Poiché nessuno lo ha menzionato, esiste anche una soluzione che utilizza la classe richiamabile che trovo più elegante, specialmente nei casi in cui il decoratore è complesso e si potrebbe desiderare di dividerlo in più metodi (funzioni). Questa soluzione utilizza il __new__metodo magico per fare essenzialmente ciò che altri hanno sottolineato. Per prima cosa rileva come è stato utilizzato il decoratore, quindi regola il ritorno in modo appropriato.

class decorator_with_arguments(object):

    def __new__(cls, decorated_function=None, **kwargs):

        self = super().__new__(cls)
        self._init(**kwargs)

        if not decorated_function:
            return self
        else:
            return self.__call__(decorated_function)

    def _init(self, arg1="default", arg2="default", arg3="default"):
        self.arg1 = arg1
        self.arg2 = arg2
        self.arg3 = arg3

    def __call__(self, decorated_function):

        def wrapped_f(*args):
            print("Decorator arguments:", self.arg1, self.arg2, self.arg3)
            print("decorated_function arguments:", *args)
            decorated_function(*args)

        return wrapped_f

@decorator_with_arguments(arg1=5)
def sayHello(a1, a2, a3, a4):
    print('sayHello arguments:', a1, a2, a3, a4)

@decorator_with_arguments()
def sayHello(a1, a2, a3, a4):
    print('sayHello arguments:', a1, a2, a3, a4)

@decorator_with_arguments
def sayHello(a1, a2, a3, a4):
    print('sayHello arguments:', a1, a2, a3, a4)

Se il decoratore viene utilizzato con argomenti, questo equivale a:

result = decorator_with_arguments(arg1=5)(sayHello)(a1, a2, a3, a4)

Si può vedere che gli argomenti arg1vengono passati correttamente al costruttore e la funzione decorata viene passata a__call__

Ma se il decoratore viene utilizzato senza argomenti, questo equivale a:

result = decorator_with_arguments(sayHello)(a1, a2, a3, a4)

Vedete che in questo caso la funzione decorata viene passata direttamente al costruttore e la chiamata a __call__è completamente omessa. Ecco perché dobbiamo impiegare la logica per occuparci di questo caso con il __new__metodo magico.

Perché non possiamo usare al __init__posto di __new__? Il motivo è semplice: python proibisce di restituire qualsiasi altro valore diverso da Nessuno da__init__

AVVERTIMENTO

Questo approccio ha un effetto collaterale. Non manterrà la firma della funzione!


-1

Hai provato argomenti di parole chiave con valori predefiniti? Qualcosa di simile a

def decorate_something(foo=bar, baz=quux):
    pass

-2

Generalmente puoi fornire argomenti predefiniti in Python ...

def redirect_output(fn, output = stderr):
    # whatever

Non sono sicuro che funzioni anche con i decoratori. Non so per quale motivo non lo sarebbe.


2
Se dici @dec (abc) la funzione non viene passata direttamente a dec. dec (abc) restituisce qualcosa e questo valore restituito viene utilizzato come decoratore. Quindi dec (abc) deve restituire una funzione, che quindi ottiene la funzione decorata passata come parametro. (Vedi anche codice thobes)
sth

-2

Basandosi sulla risposta di vartec:

imports sys

def redirect_output(func, output=None):
    if output is None:
        output = sys.stderr
    if isinstance(output, basestring):
        output = open(output, 'w') # etc...
    # everything else...

questo non può essere usato come decoratore come @redirect_output("somewhere.log") def foo()nell'esempio nella domanda.
ehabkost
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.