Decoratori con parametri?


401

Ho un problema con il trasferimento della variabile "modalità_assicurazione" da parte del decoratore. Lo farei con la seguente dichiarazione del decoratore:

 @execute_complete_reservation(True)
 def test_booking_gta_object(self):
     self.test_select_gta_object()

ma purtroppo questa affermazione non funziona. Forse forse c'è un modo migliore per risolvere questo problema.

def execute_complete_reservation(test_case,insurance_mode):
    def inner_function(self,*args,**kwargs):
        self.test_create_qsf_query()
        test_case(self,*args,**kwargs)
        self.test_select_room_option()
        if insurance_mode:
            self.test_accept_insurance_crosseling()
        else:
            self.test_decline_insurance_crosseling()
        self.test_configure_pax_details()
        self.test_configure_payer_details

    return inner_function

3
Il tuo esempio non è sintatticamente valido. execute_complete_reservationaccetta due parametri, ma tu lo passi uno. I decoratori sono solo zucchero sintattico per avvolgere le funzioni all'interno di altre funzioni. Vedi docs.python.org/reference/compound_stmts.html#function per la documentazione completa.
Brian Clapper,

Risposte:


687

La sintassi per i decoratori con argomenti è un po 'diversa: il decoratore con argomenti dovrebbe restituire una funzione che prenderà una funzione e restituirà un'altra funzione. Quindi dovrebbe davvero restituire un normale decoratore. Un po 'confuso, vero? Ciò che voglio dire è:

def decorator_factory(argument):
    def decorator(function):
        def wrapper(*args, **kwargs):
            funny_stuff()
            something_with_argument(argument)
            result = function(*args, **kwargs)
            more_funny_stuff()
            return result
        return wrapper
    return decorator

Qui puoi leggere di più sull'argomento: è anche possibile implementarlo usando oggetti richiamabili e che è anche spiegato lì.


56
Mi chiedo perché GVR non l'abbia implementato passando i parametri come argomenti decoratore successivi dopo 'funzione'. 'Yo dawg ti ho sentito come chiusure ...' eccetera.
Michel Müller,

3
> La funzione sarebbe il primo o l'ultimo argomento? Ovviamente prima di tutto, poiché i parametri sono un elenco di parametri di lunghezza variabile. > È anche strano che tu "chiami" la funzione con una firma diversa da quella nella definizione. Come hai sottolineato, in realtà si adatterebbe molto bene - è praticamente analogo a come viene chiamato un metodo di classe. Per renderlo più chiaro, potresti avere qualcosa di simile alla convenzione decorator (self_func, param1, ...). Ma nota: non sto sostenendo alcun cambiamento qui, Python è troppo lontano per questo e possiamo vedere come hanno funzionato i cambiamenti di rottura ..
Michel Müller,

21
hai dimenticato functools.wraps molto utile per decorare involucro :)
socketpair

10
Ti sei dimenticato di tornare quando hai chiamato la funzione, ad es. return function(*args, **kwargs)
formiaczek il

36
Forse ovvio, ma per ogni evenienza: devi usare questo decoratore come @decorator()e non solo @decorator, anche se hai solo argomenti opzionali.
Patrick Mevzek il

327

Modifica : per una comprensione approfondita del modello mentale dei decoratori, dai un'occhiata a questo fantastico Pycon Talk. vale i 30 minuti.

Un modo di pensare ai decoratori con argomenti è

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

si traduce in

foo = decorator(foo)

Quindi se il decoratore avesse delle discussioni,

@decorator_with_args(arg)
def foo(*args, **kwargs):
    pass

si traduce in

foo = decorator_with_args(arg)(foo)

decorator_with_args è una funzione che accetta un argomento personalizzato e che restituisce il decoratore effettivo (che verrà applicato alla funzione decorata).

Uso un semplice trucco con i parziali per semplificare i miei decoratori

from functools import partial

def _pseudo_decor(fun, argument):
    def ret_fun(*args, **kwargs):
        #do stuff here, for eg.
        print ("decorator arg is %s" % str(argument))
        return fun(*args, **kwargs)
    return ret_fun

real_decorator = partial(_pseudo_decor, argument=arg)

@real_decorator
def foo(*args, **kwargs):
    pass

Aggiornare:

Sopra, foodiventareal_decorator(foo)

Un effetto della decorazione di una funzione è che il nome fooviene sovrascritto dalla dichiarazione del decoratore. fooviene "ignorato" da qualsiasi cosa venga restituita da real_decorator. In questo caso, un nuovo oggetto funzione.

Tutti fooi metadati sono sovrascritti, in particolare docstring e nome della funzione.

>>> print(foo)
<function _pseudo_decor.<locals>.ret_fun at 0x10666a2f0>

functools.wraps ci fornisce un metodo conveniente per "sollevare" il docstring e il nome della funzione restituita.

from functools import partial, wraps

def _pseudo_decor(fun, argument):
    # magic sauce to lift the name and doc of the function
    @wraps(fun)
    def ret_fun(*args, **kwargs):
        #do stuff here, for eg.
        print ("decorator arg is %s" % str(argument))
        return fun(*args, **kwargs)
    return ret_fun

real_decorator = partial(_pseudo_decor, argument=arg)

@real_decorator
def bar(*args, **kwargs):
    pass

>>> print(bar)
<function __main__.bar(*args, **kwargs)>

4
La tua risposta ha spiegato perfettamente l'ortogonalità intrinseca del decoratore, grazie
zsf222,

Potresti aggiungere @functools.wraps?
Mr_and_Mrs_D,

1
@Mr_and_Mrs_D, ho aggiornato il post con un esempio con functool.wraps. Aggiungerlo nell'esempio può confondere ulteriormente i lettori.
srj,

7
Cosa c'è argqui !?
displayname

1
Come passerai l'argomento passato barall'argomento di real_decorator?
Chang Zhao,

85

Mi piacerebbe mostrare un'idea che è IMHO piuttosto elegante. La soluzione proposta da t.dubrownik mostra uno schema che è sempre lo stesso: hai bisogno dell'involucro a tre strati indipendentemente da ciò che fa il decoratore.

Quindi ho pensato che questo fosse un lavoro per un meta-decoratore, cioè un decoratore per decoratori. Poiché un decoratore è una funzione, in realtà funziona come un normale decoratore con argomenti:

def parametrized(dec):
    def layer(*args, **kwargs):
        def repl(f):
            return dec(f, *args, **kwargs)
        return repl
    return layer

Questo può essere applicato a un normale decoratore per aggiungere parametri. Ad esempio, supponiamo di avere il decoratore che raddoppia il risultato di una funzione:

def double(f):
    def aux(*xs, **kws):
        return 2 * f(*xs, **kws)
    return aux

@double
def function(a):
    return 10 + a

print function(3)    # Prints 26, namely 2 * (10 + 3)

Con @parametrizedpossiamo costruire un @multiplydecoratore generico con un parametro

@parametrized
def multiply(f, n):
    def aux(*xs, **kws):
        return n * f(*xs, **kws)
    return aux

@multiply(2)
def function(a):
    return 10 + a

print function(3)    # Prints 26

@multiply(3)
def function_again(a):
    return 10 + a

print function(3)          # Keeps printing 26
print function_again(3)    # Prints 39, namely 3 * (10 + 3)

Convenzionalmente il primo parametro di un decoratore parametrizzato è la funzione, mentre gli argomenti rimanenti corrisponderanno al parametro del decoratore parametrizzato.

Un esempio di utilizzo interessante potrebbe essere un decoratore assertivo sicuro:

import itertools as it

@parametrized
def types(f, *types):
    def rep(*args):
        for a, t, n in zip(args, types, it.count()):
            if type(a) is not t:
                raise TypeError('Value %d has not type %s. %s instead' %
                    (n, t, type(a))
                )
        return f(*args)
    return rep

@types(str, int)  # arg1 is str, arg2 is int
def string_multiply(text, times):
    return text * times

print(string_multiply('hello', 3))    # Prints hellohellohello
print(string_multiply(3, 3))          # Fails miserably with TypeError

Un'ultima nota: qui non sto usando functools.wrapsle funzioni wrapper, ma consiglierei di usarlo tutte le volte.


3
Non l'ho usato esattamente, ma mi ha aiutato a capire meglio il concetto :) Grazie!
mouckatron,

Ho provato questo e ho avuto alcuni problemi .
Jeff,

@Jeff potresti condividere con noi il tipo di problemi che hai avuto?
Dacav,

L'avevo collegato alla mia domanda, e l'ho capito ... Avevo bisogno di chiamare il @wrapsmio per il mio caso particolare.
Jeff,

4
Oh ragazzo, ho perso un giorno intero su questo. Per fortuna, ho trovato questa risposta (che per inciso potrebbe essere la migliore risposta mai creata su tutta Internet). Anche loro usano il tuo @parametrizedtrucco. Il problema che ho avuto è che ho dimenticato che la @sintassi equivale alle chiamate effettive (in qualche modo lo sapevo e non lo sapevo allo stesso tempo in cui puoi raccogliere dalla mia domanda). Quindi, se vuoi tradurre la @sintassi in chiamate banali per verificare come funziona, è meglio commentarla temporaneamente prima o finiresti per chiamarla due volte e ottenere risultati
mumbojumbo

79

Ecco una versione leggermente modificata della risposta di t.dubrownik . Perché?

  1. Come modello generale, è necessario restituire il valore restituito dalla funzione originale.
  2. Ciò modifica il nome della funzione, che potrebbe influire su altri decoratori / codice.

Quindi usa @functools.wraps():

from functools import wraps

def decorator(argument):
    def real_decorator(function):
        @wraps(function)
        def wrapper(*args, **kwargs):
            funny_stuff()
            something_with_argument(argument)
            retval = function(*args, **kwargs)
            more_funny_stuff()
            return retval
        return wrapper
    return real_decorator

37

Presumo che il tuo problema sia passare argomenti al tuo decoratore. Questo è un po 'complicato e non semplice.

Ecco un esempio di come eseguire questa operazione:

class MyDec(object):
    def __init__(self,flag):
        self.flag = flag
    def __call__(self, original_func):
        decorator_self = self
        def wrappee( *args, **kwargs):
            print 'in decorator before wrapee with flag ',decorator_self.flag
            original_func(*args,**kwargs)
            print 'in decorator after wrapee with flag ',decorator_self.flag
        return wrappee

@MyDec('foo de fa fa')
def bar(a,b,c):
    print 'in bar',a,b,c

bar('x','y','z')

stampe:

in decorator before wrapee with flag  foo de fa fa
in bar x y z
in decorator after wrapee with flag  foo de fa fa

Vedi l'articolo di Bruce Eckel per maggiori dettagli.


20
Fai attenzione alle lezioni di decoratore. Non funzionano sui metodi a meno che non si reinventi manualmente la logica dei descrittori del metodo di istanza.

9
delnan, ti interessa elaborare? Ho dovuto usare questo schema solo una volta, quindi non ho ancora colto nessuna delle insidie.
Ross Rogers,

2
@RossRogers La mia ipotesi è che @delnan si riferisca a cose come quelle __name__che un'istanza della classe decoratrice non avrà?
Jamesc,

9
@jamesc Anche quello, sebbene sia relativamente facile da risolvere. Il caso specifico a cui mi riferivo era class Foo: @MyDec(...) def method(self, ...): blahche non funziona perché Foo().methodnon sarà un metodo associato e non passerà selfautomaticamente. Anche questo può essere risolto, creando MyDecun descrittore e creando metodi associati __get__, ma è più coinvolto e molto meno ovvio. Alla fine, le lezioni di decoratore non sono così comode come sembrano.

2
@delnan Mi piacerebbe vedere questo avvertimento in primo piano. Lo sto colpendo e sono interessato a vedere una soluzione che FUNZIONA (più coinvolto e meno ovvio che possa essere).
HaPsantran,

12
def decorator(argument):
    def real_decorator(function):
        def wrapper(*args):
            for arg in args:
                assert type(arg)==int,f'{arg} is not an interger'
            result = function(*args)
            result = result*argument
            return result
        return wrapper
    return real_decorator

Uso del decoratore

@decorator(2)
def adder(*args):
    sum=0
    for i in args:
        sum+=i
    return sum

Poi il

adder(2,3)

produce

10

ma

adder('hi',3)

produce

---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-143-242a8feb1cc4> in <module>
----> 1 adder('hi',3)

<ipython-input-140-d3420c248ebd> in wrapper(*args)
      3         def wrapper(*args):
      4             for arg in args:
----> 5                 assert type(arg)==int,f'{arg} is not an interger'
      6             result = function(*args)
      7             result = result*argument

AssertionError: hi is not an interger

8

Questo è un modello per un decoratore di funzioni che non richiede ()se non si devono fornire parametri:

import functools


def decorator(x_or_func=None, *decorator_args, **decorator_kws):
    def _decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kws):
            if 'x_or_func' not in locals() \
                    or callable(x_or_func) \
                    or x_or_func is None:
                x = ...  # <-- default `x` value
            else:
                x = x_or_func
            return func(*args, **kws)

        return wrapper

    return _decorator(x_or_func) if callable(x_or_func) else _decorator

un esempio di ciò è riportato di seguito:

def multiplying(factor_or_func=None):
    def _decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            if 'factor_or_func' not in locals() \
                    or callable(factor_or_func) \
                    or factor_or_func is None:
                factor = 1
            else:
                factor = factor_or_func
            return factor * func(*args, **kwargs)
        return wrapper
    return _decorator(factor_or_func) if callable(factor_or_func) else _decorator


@multiplying
def summing(x): return sum(x)

print(summing(range(10)))
# 45


@multiplying()
def summing(x): return sum(x)

print(summing(range(10)))
# 45


@multiplying(10)
def summing(x): return sum(x)

print(summing(range(10)))
# 450

Si noti, inoltre, che factor_or_func(o qualsiasi altro parametro) non dovrebbe mai viene riassegnata a wrapper().
norok2,

1
Perché devi effettuare il check-in locals()?
Shital Shah,

@ShitalShah che copre il caso in cui il decoratore viene utilizzato senza ().
norok2

4

Nel mio caso, ho deciso di risolverlo tramite una lambda a una riga per creare una nuova funzione di decorazione:

def finished_message(function, message="Finished!"):

    def wrapper(*args, **kwargs):
        output = function(*args,**kwargs)
        print(message)
        return output

    return wrapper

@finished_message
def func():
    pass

my_finished_message = lambda f: finished_message(f, "All Done!")

@my_finished_message
def my_func():
    pass

if __name__ == '__main__':
    func()
    my_func()

Quando eseguito, questo stampa:

Finished!
All Done!

Forse non estensibile come altre soluzioni, ma ha funzionato per me.


Questo funziona Anche se sì, questo rende difficile impostare il valore sul decoratore.
Arindam Roychowdhury,

3

Scrivere un decoratore che funziona con e senza parametro è una sfida perché Python si aspetta un comportamento completamente diverso in questi due casi! Molte risposte hanno cercato di aggirare questo problema e di seguito è riportato un miglioramento della risposta di @ norok2. In particolare, questa variazione elimina l'uso dilocals() .

Seguendo lo stesso esempio dato da @ norok2:

import functools

def multiplying(f_py=None, factor=1):
    assert callable(f_py) or f_py is None
    def _decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            return factor * func(*args, **kwargs)
        return wrapper
    return _decorator(f_py) if callable(f_py) else _decorator


@multiplying
def summing(x): return sum(x)

print(summing(range(10)))
# 45


@multiplying()
def summing(x): return sum(x)

print(summing(range(10)))
# 45


@multiplying(factor=10)
def summing(x): return sum(x)

print(summing(range(10)))
# 450

Gioca con questo codice .

Il problema è che l'utente deve fornire chiave, coppie di valori di parametri anziché parametri posizionali e il primo parametro è riservato.


2

È noto che i seguenti due pezzi di codice sono quasi equivalenti:

@dec
def foo():
    pass    foo = dec(foo)

############################################
foo = dec(foo)

Un errore comune è pensare che @nasconda semplicemente l'argomento più a sinistra.

@dec(1, 2, 3)
def foo():
    pass    
###########################################
foo = dec(foo, 1, 2, 3)

Sarebbe molto più facile scrivere decoratori se quanto sopra è come ha @funzionato. Sfortunatamente, non è così che vanno le cose.


Prendi in considerazione un decoratore Waitche interrompe l'esecuzione del programma per alcuni secondi. Se non passi un tempo di attesa, il valore predefinito è 1 secondo. I casi d'uso sono indicati di seguito.

##################################################
@Wait
def print_something(something):
    print(something)

##################################################
@Wait(3)
def print_something_else(something_else):
    print(something_else)

##################################################
@Wait(delay=3)
def print_something_else(something_else):
    print(something_else)

Quando Waitha un argomento, ad esempio @Wait(3), la chiamata Wait(3) viene eseguita prima che accada qualsiasi altra cosa.

Cioè, i seguenti due pezzi di codice sono equivalenti

@Wait(3)
def print_something_else(something_else):
    print(something_else)

###############################################
return_value = Wait(3)
@return_value
def print_something_else(something_else):
    print(something_else)

Questo è un problema.

if `Wait` has no arguments:
    `Wait` is the decorator.
else: # `Wait` receives arguments
    `Wait` is not the decorator itself.
    Instead, `Wait` ***returns*** the decorator

Una soluzione è mostrata di seguito:

Cominciamo creando la seguente classe DelayedDecorator:

class DelayedDecorator:
    def __init__(i, cls, *args, **kwargs):
        print("Delayed Decorator __init__", cls, args, kwargs)
        i._cls = cls
        i._args = args
        i._kwargs = kwargs
    def __call__(i, func):
        print("Delayed Decorator __call__", func)
        if not (callable(func)):
            import io
            with io.StringIO() as ss:
                print(
                    "If only one input, input must be callable",
                    "Instead, received:",
                    repr(func),
                    sep="\n",
                    file=ss
                )
                msg = ss.getvalue()
            raise TypeError(msg)
        return i._cls(func, *i._args, **i._kwargs)

Ora possiamo scrivere cose come:

 dec = DelayedDecorator(Wait, delay=4)
 @dec
 def delayed_print(something):
    print(something)

Nota che:

  • dec non accetta più argomenti.
  • dec accetta solo la funzione da racchiudere.

    import inspect class PolyArgDecoratorMeta (type): def call (Wait, * args, ** kwargs): try: arg_count = len (args) if (arg_count == 1): if callable (args [0]): SuperClass = inspect. getmro (PolyArgDecoratorMeta) [1] r = SuperClass. chiamata (Wait, args [0]) else: r = DelayedDecorator (Wait, * args, ** kwargs) else: r = DelayedDecorator (Wait, * args, ** kwargs) infine: passa return r

    classe temporale di importazione Wait (metaclass = PolyArgDecoratorMeta): def init (i, func, delay = 2): i._func = func i._delay = delay

    def __call__(i, *args, **kwargs):
        time.sleep(i._delay)
        r = i._func(*args, **kwargs)
        return r 

I seguenti due pezzi di codice sono equivalenti:

@Wait
def print_something(something):
     print (something)

##################################################

def print_something(something):
    print(something)
print_something = Wait(print_something)

Possiamo stampare "something"sulla console molto lentamente, come segue:

print_something("something")

#################################################
@Wait(delay=1)
def print_something_else(something_else):
    print(something_else)

##################################################
def print_something_else(something_else):
    print(something_else)

dd = DelayedDecorator(Wait, delay=1)
print_something_else = dd(print_something_else)

##################################################

print_something_else("something")

Note finali

Potrebbe sembrare un sacco di codice, ma non devi scrivere le classi DelayedDecoratore PolyArgDecoratorMetaogni volta. L'unico codice che devi scrivere personalmente è simile al seguente, che è piuttosto breve:

from PolyArgDecoratorMeta import PolyArgDecoratorMeta
import time
class Wait(metaclass=PolyArgDecoratorMeta):
 def __init__(i, func, delay = 2):
     i._func = func
     i._delay = delay

 def __call__(i, *args, **kwargs):
     time.sleep(i._delay)
     r = i._func(*args, **kwargs)
     return r

1

definire questa "funzione decoratrice" per generare la funzione decoratrice personalizzata:

def decoratorize(FUN, **kw):
    def foo(*args, **kws):
        return FUN(*args, **kws, **kw)
    return foo

usalo in questo modo:

    @decoratorize(FUN, arg1 = , arg2 = , ...)
    def bar(...):
        ...

1

Grandi risposte sopra. Questo illustra anche @wraps, che prende la stringa doc e il nome della funzione dalla funzione originale e lo applica alla nuova versione con wrapping:

from functools import wraps

def decorator_func_with_args(arg1, arg2):
    def decorator(f):
        @wraps(f)
        def wrapper(*args, **kwargs):
            print("Before orginal function with decorator args:", arg1, arg2)
            result = f(*args, **kwargs)
            print("Ran after the orginal function")
            return result
        return wrapper
    return decorator

@decorator_func_with_args("foo", "bar")
def hello(name):
    """A function which prints a greeting to the name provided.
    """
    print('hello ', name)
    return 42

print("Starting script..")
x = hello('Bob')
print("The value of x is:", x)
print("The wrapped functions docstring is:", hello.__doc__)
print("The wrapped functions name is:", hello.__name__)

stampe:

Starting script..
Before orginal function with decorator args: foo bar
hello  Bob
Ran after the orginal function
The value of x is: 42
The wrapped functions docstring is: A function which prints a greeting to the name provided.
The wrapped functions name is: hello

0

Nel caso in cui sia la funzione che il decoratore debbano accettare argomenti, puoi seguire l'approccio seguente.

Ad esempio c'è un decoratore chiamato decorator1che accetta un argomento

@decorator1(5)
def func1(arg1, arg2):
    print (arg1, arg2)

func1(1, 2)

Ora, se l' decorator1argomento deve essere dinamico o passato durante la chiamata della funzione,

def func1(arg1, arg2):
    print (arg1, arg2)


a = 1
b = 2
seconds = 10

decorator1(seconds)(func1)(a, b)

Nel codice sopra

  • seconds è l'argomento per decorator1
  • a, b sono gli argomenti di func1
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.