Como fazer uma cadeia de decoradores de funções?

2756

Como posso criar dois decoradores em Python que fazem o seguinte?

@makebold
@makeitalic
def say():
   return "Hello"

... que deve retornar:

"<b><i>Hello</i></b>"

Não estou tentando fazer HTMLisso em um aplicativo real - apenas tentando entender como os decoradores e o encadeamento de decoradores funcionam.

Imran
fonte

Respostas:

2927

Confira a documentação para ver como os decoradores funcionam. Aqui está o que você pediu:

from functools import wraps

def makebold(fn):
    @wraps(fn)
    def wrapped(*args, **kwargs):
        return "<b>" + fn(*args, **kwargs) + "</b>"
    return wrapped

def makeitalic(fn):
    @wraps(fn)
    def wrapped(*args, **kwargs):
        return "<i>" + fn(*args, **kwargs) + "</i>"
    return wrapped

@makebold
@makeitalic
def hello():
    return "hello world"

@makebold
@makeitalic
def log(s):
    return s

print hello()        # returns "<b><i>hello world</i></b>"
print hello.__name__ # with functools.wraps() this returns "hello"
print log('hello')   # returns "<b><i>hello</i></b>"
Paolo Bergantino
fonte
261
Considere usar functools.wraps ou, melhor ainda, o módulo decorador do PyPI : eles preservam certos metadados importantes (como __name__e, falando sobre o pacote decorador, assinatura da função).
Marius Gedminas 11/03
31
*argse **kwargsdeve ser adicionado na resposta. A função decorada pode ter argumentos e eles serão perdidos se não forem especificados.
Blusky 02/04
3
Embora esta resposta tenha a grande vantagem de usar apenas o stdlib e funcione para este exemplo simples, onde não há argumentos do decorador nem argumentos da função decorada , ela possui três limitações principais: (1) nenhum suporte simples para argumentos opcionais do decorador (2) não assinatura de preservação (3) nenhuma maneira simples de extrair um argumento nomeado a partir *args, **kwargs. Uma maneira fácil de resolver esses três problemas de uma vez é usar decopatchcomo explicado aqui . Você também pode usar decoratorcomo já mencionado por Marius Gedminas, para resolver os pontos 2 e 3.
smarie
4209

Se você não tiver longas explicações, consulte a resposta de Paolo Bergantino .

Noções básicas do decorador

As funções do Python são objetos

Para entender os decoradores, você deve primeiro entender que funções são objetos em Python. Isso tem consequências importantes. Vamos ver o porquê com um exemplo simples:

def shout(word="yes"):
    return word.capitalize()+"!"

print(shout())
# outputs : 'Yes!'

# As an object, you can assign the function to a variable like any other object 
scream = shout

# Notice we don't use parentheses: we are not calling the function,
# we are putting the function "shout" into the variable "scream".
# It means you can then call "shout" from "scream":

print(scream())
# outputs : 'Yes!'

# More than that, it means you can remove the old name 'shout',
# and the function will still be accessible from 'scream'

del shout
try:
    print(shout())
except NameError as e:
    print(e)
    #outputs: "name 'shout' is not defined"

print(scream())
# outputs: 'Yes!'

Mantenha isso em mente. Voltaremos a ele em breve.

Outra propriedade interessante das funções do Python é que elas podem ser definidas dentro de outra função!

def talk():

    # You can define a function on the fly in "talk" ...
    def whisper(word="yes"):
        return word.lower()+"..."

    # ... and use it right away!
    print(whisper())

# You call "talk", that defines "whisper" EVERY TIME you call it, then
# "whisper" is called in "talk". 
talk()
# outputs: 
# "yes..."

# But "whisper" DOES NOT EXIST outside "talk":

try:
    print(whisper())
except NameError as e:
    print(e)
    #outputs : "name 'whisper' is not defined"*
    #Python's functions are objects

Referências de funções

Ok, ainda está aqui? Agora a parte divertida ...

Você viu que funções são objetos. Portanto, funções:

  • pode ser atribuído a uma variável
  • pode ser definido em outra função

Isso significa que uma função pode returnoutra função .

def getTalk(kind="shout"):

    # We define functions on the fly
    def shout(word="yes"):
        return word.capitalize()+"!"

    def whisper(word="yes") :
        return word.lower()+"...";

    # Then we return one of them
    if kind == "shout":
        # We don't use "()", we are not calling the function,
        # we are returning the function object
        return shout  
    else:
        return whisper

# How do you use this strange beast?

# Get the function and assign it to a variable
talk = getTalk()      

# You can see that "talk" is here a function object:
print(talk)
#outputs : <function shout at 0xb7ea817c>

# The object is the one returned by the function:
print(talk())
#outputs : Yes!

# And you can even use it directly if you feel wild:
print(getTalk("whisper")())
#outputs : yes...

Tem mais!

Se você pode returnuma função, pode passar uma como parâmetro:

def doSomethingBefore(func): 
    print("I do something before then I call the function you gave me")
    print(func())

doSomethingBefore(scream)
#outputs: 
#I do something before then I call the function you gave me
#Yes!

Bem, você só tem tudo o necessário para entender os decoradores. Veja, decoradores são "invólucros", o que significa que permitem executar o código antes e depois da função que decoram sem modificar a própria função.

Decoradores artesanais

Como você faria isso manualmente:

# A decorator is a function that expects ANOTHER function as parameter
def my_shiny_new_decorator(a_function_to_decorate):

    # Inside, the decorator defines a function on the fly: the wrapper.
    # This function is going to be wrapped around the original function
    # so it can execute code before and after it.
    def the_wrapper_around_the_original_function():

        # Put here the code you want to be executed BEFORE the original function is called
        print("Before the function runs")

        # Call the function here (using parentheses)
        a_function_to_decorate()

        # Put here the code you want to be executed AFTER the original function is called
        print("After the function runs")

    # At this point, "a_function_to_decorate" HAS NEVER BEEN EXECUTED.
    # We return the wrapper function we have just created.
    # The wrapper contains the function and the code to execute before and after. It’s ready to use!
    return the_wrapper_around_the_original_function

# Now imagine you create a function you don't want to ever touch again.
def a_stand_alone_function():
    print("I am a stand alone function, don't you dare modify me")

a_stand_alone_function() 
#outputs: I am a stand alone function, don't you dare modify me

# Well, you can decorate it to extend its behavior.
# Just pass it to the decorator, it will wrap it dynamically in 
# any code you want and return you a new function ready to be used:

a_stand_alone_function_decorated = my_shiny_new_decorator(a_stand_alone_function)
a_stand_alone_function_decorated()
#outputs:
#Before the function runs
#I am a stand alone function, don't you dare modify me
#After the function runs

Agora, você provavelmente deseja que toda vez que ligar a_stand_alone_function, a_stand_alone_function_decoratedseja chamado. Isso é fácil, basta substituir a_stand_alone_functiona função retornada por my_shiny_new_decorator:

a_stand_alone_function = my_shiny_new_decorator(a_stand_alone_function)
a_stand_alone_function()
#outputs:
#Before the function runs
#I am a stand alone function, don't you dare modify me
#After the function runs

# That’s EXACTLY what decorators do!

Decoradores desmistificados

O exemplo anterior, usando a sintaxe do decorador:

@my_shiny_new_decorator
def another_stand_alone_function():
    print("Leave me alone")

another_stand_alone_function()  
#outputs:  
#Before the function runs
#Leave me alone
#After the function runs

Sim, é tudo, é simples assim. @decoratoré apenas um atalho para:

another_stand_alone_function = my_shiny_new_decorator(another_stand_alone_function)

Decoradores são apenas uma variante pitônica do padrão de design do decorador . Existem vários padrões de design clássicos incorporados no Python para facilitar o desenvolvimento (como iteradores).

Obviamente, você pode acumular decoradores:

def bread(func):
    def wrapper():
        print("</''''''\>")
        func()
        print("<\______/>")
    return wrapper

def ingredients(func):
    def wrapper():
        print("#tomatoes#")
        func()
        print("~salad~")
    return wrapper

def sandwich(food="--ham--"):
    print(food)

sandwich()
#outputs: --ham--
sandwich = bread(ingredients(sandwich))
sandwich()
#outputs:
#</''''''\>
# #tomatoes#
# --ham--
# ~salad~
#<\______/>

Usando a sintaxe do decorador Python:

@bread
@ingredients
def sandwich(food="--ham--"):
    print(food)

sandwich()
#outputs:
#</''''''\>
# #tomatoes#
# --ham--
# ~salad~
#<\______/>

A ordem em que você define os decoradores é IMPORTANTE:

@ingredients
@bread
def strange_sandwich(food="--ham--"):
    print(food)

strange_sandwich()
#outputs:
##tomatoes#
#</''''''\>
# --ham--
#<\______/>
# ~salad~

Agora: para responder à pergunta ...

Como conclusão, você pode ver facilmente como responder à pergunta:

# The decorator to make it bold
def makebold(fn):
    # The new function the decorator returns
    def wrapper():
        # Insertion of some code before and after
        return "<b>" + fn() + "</b>"
    return wrapper

# The decorator to make it italic
def makeitalic(fn):
    # The new function the decorator returns
    def wrapper():
        # Insertion of some code before and after
        return "<i>" + fn() + "</i>"
    return wrapper

@makebold
@makeitalic
def say():
    return "hello"

print(say())
#outputs: <b><i>hello</i></b>

# This is the exact equivalent to 
def say():
    return "hello"
say = makebold(makeitalic(say))

print(say())
#outputs: <b><i>hello</i></b>

Agora você pode simplesmente sair feliz ou queimar seu cérebro um pouco mais e ver usos avançados de decoradores.


Levando os decoradores para o próximo nível

Passando argumentos para a Função Decorada

# It’s not black magic, you just have to let the wrapper 
# pass the argument:

def a_decorator_passing_arguments(function_to_decorate):
    def a_wrapper_accepting_arguments(arg1, arg2):
        print("I got args! Look: {0}, {1}".format(arg1, arg2))
        function_to_decorate(arg1, arg2)
    return a_wrapper_accepting_arguments

# Since when you are calling the function returned by the decorator, you are
# calling the wrapper, passing arguments to the wrapper will let it pass them to 
# the decorated function

@a_decorator_passing_arguments
def print_full_name(first_name, last_name):
    print("My name is {0} {1}".format(first_name, last_name))

print_full_name("Peter", "Venkman")
# outputs:
#I got args! Look: Peter Venkman
#My name is Peter Venkman

Métodos de decoração

Uma coisa bacana do Python é que métodos e funções são realmente os mesmos. A única diferença é que os métodos esperam que o primeiro argumento seja uma referência ao objeto atual ( self).

Isso significa que você pode criar um decorador para métodos da mesma maneira! Lembre-se de levar selfem consideração:

def method_friendly_decorator(method_to_decorate):
    def wrapper(self, lie):
        lie = lie - 3 # very friendly, decrease age even more :-)
        return method_to_decorate(self, lie)
    return wrapper


class Lucy(object):

    def __init__(self):
        self.age = 32

    @method_friendly_decorator
    def sayYourAge(self, lie):
        print("I am {0}, what did you think?".format(self.age + lie))

l = Lucy()
l.sayYourAge(-3)
#outputs: I am 26, what did you think?

Se você estiver criando um decorador de uso geral - um que você aplicará a qualquer função ou método, independentemente dos argumentos -, basta usar *args, **kwargs:

def a_decorator_passing_arbitrary_arguments(function_to_decorate):
    # The wrapper accepts any arguments
    def a_wrapper_accepting_arbitrary_arguments(*args, **kwargs):
        print("Do I have args?:")
        print(args)
        print(kwargs)
        # Then you unpack the arguments, here *args, **kwargs
        # If you are not familiar with unpacking, check:
        # http://www.saltycrane.com/blog/2008/01/how-to-use-args-and-kwargs-in-python/
        function_to_decorate(*args, **kwargs)
    return a_wrapper_accepting_arbitrary_arguments

@a_decorator_passing_arbitrary_arguments
def function_with_no_argument():
    print("Python is cool, no argument here.")

function_with_no_argument()
#outputs
#Do I have args?:
#()
#{}
#Python is cool, no argument here.

@a_decorator_passing_arbitrary_arguments
def function_with_arguments(a, b, c):
    print(a, b, c)

function_with_arguments(1,2,3)
#outputs
#Do I have args?:
#(1, 2, 3)
#{}
#1 2 3 

@a_decorator_passing_arbitrary_arguments
def function_with_named_arguments(a, b, c, platypus="Why not ?"):
    print("Do {0}, {1} and {2} like platypus? {3}".format(a, b, c, platypus))

function_with_named_arguments("Bill", "Linus", "Steve", platypus="Indeed!")
#outputs
#Do I have args ? :
#('Bill', 'Linus', 'Steve')
#{'platypus': 'Indeed!'}
#Do Bill, Linus and Steve like platypus? Indeed!

class Mary(object):

    def __init__(self):
        self.age = 31

    @a_decorator_passing_arbitrary_arguments
    def sayYourAge(self, lie=-3): # You can now add a default value
        print("I am {0}, what did you think?".format(self.age + lie))

m = Mary()
m.sayYourAge()
#outputs
# Do I have args?:
#(<__main__.Mary object at 0xb7d303ac>,)
#{}
#I am 28, what did you think?

Passando argumentos para o Decorador

Ótimo, agora o que você diria sobre passar argumentos para o próprio decorador?

Isso pode ficar um pouco distorcido, pois um decorador deve aceitar uma função como argumento. Portanto, você não pode passar os argumentos da função decorada diretamente para o decorador.

Antes de correr para a solução, vamos escrever um pequeno lembrete:

# Decorators are ORDINARY functions
def my_decorator(func):
    print("I am an ordinary function")
    def wrapper():
        print("I am function returned by the decorator")
        func()
    return wrapper

# Therefore, you can call it without any "@"

def lazy_function():
    print("zzzzzzzz")

decorated_function = my_decorator(lazy_function)
#outputs: I am an ordinary function

# It outputs "I am an ordinary function", because that’s just what you do:
# calling a function. Nothing magic.

@my_decorator
def lazy_function():
    print("zzzzzzzz")

#outputs: I am an ordinary function

É exatamente o mesmo. " my_decorator" é chamado. Então, quando você @my_decoratorestá dizendo ao Python para chamar a função 'rotulada pela variável " my_decorator"'.

Isso é importante! A etiqueta que você fornecer pode apontar diretamente para o decorador - ou não .

Vamos ficar mal. ☺

def decorator_maker():

    print("I make decorators! I am executed only once: "
          "when you make me create a decorator.")

    def my_decorator(func):

        print("I am a decorator! I am executed only when you decorate a function.")

        def wrapped():
            print("I am the wrapper around the decorated function. "
                  "I am called when you call the decorated function. "
                  "As the wrapper, I return the RESULT of the decorated function.")
            return func()

        print("As the decorator, I return the wrapped function.")

        return wrapped

    print("As a decorator maker, I return a decorator")
    return my_decorator

# Let’s create a decorator. It’s just a new function after all.
new_decorator = decorator_maker()       
#outputs:
#I make decorators! I am executed only once: when you make me create a decorator.
#As a decorator maker, I return a decorator

# Then we decorate the function

def decorated_function():
    print("I am the decorated function.")

decorated_function = new_decorator(decorated_function)
#outputs:
#I am a decorator! I am executed only when you decorate a function.
#As the decorator, I return the wrapped function

# Let’s call the function:
decorated_function()
#outputs:
#I am the wrapper around the decorated function. I am called when you call the decorated function.
#As the wrapper, I return the RESULT of the decorated function.
#I am the decorated function.

Nenhuma surpresa aqui.

Vamos fazer exatamente a mesma coisa, mas pular todas as variáveis ​​intermediárias traquinas:

def decorated_function():
    print("I am the decorated function.")
decorated_function = decorator_maker()(decorated_function)
#outputs:
#I make decorators! I am executed only once: when you make me create a decorator.
#As a decorator maker, I return a decorator
#I am a decorator! I am executed only when you decorate a function.
#As the decorator, I return the wrapped function.

# Finally:
decorated_function()    
#outputs:
#I am the wrapper around the decorated function. I am called when you call the decorated function.
#As the wrapper, I return the RESULT of the decorated function.
#I am the decorated function.

Vamos torná-lo ainda mais curto :

@decorator_maker()
def decorated_function():
    print("I am the decorated function.")
#outputs:
#I make decorators! I am executed only once: when you make me create a decorator.
#As a decorator maker, I return a decorator
#I am a decorator! I am executed only when you decorate a function.
#As the decorator, I return the wrapped function.

#Eventually: 
decorated_function()    
#outputs:
#I am the wrapper around the decorated function. I am called when you call the decorated function.
#As the wrapper, I return the RESULT of the decorated function.
#I am the decorated function.

Ei, você viu isso? Usamos uma chamada de função com a @sintaxe " "! :-)

Então, voltando aos decoradores com argumentos. Se podemos usar funções para gerar o decorador em tempo real, podemos passar argumentos para essa função, certo?

def decorator_maker_with_arguments(decorator_arg1, decorator_arg2):

    print("I make decorators! And I accept arguments: {0}, {1}".format(decorator_arg1, decorator_arg2))

    def my_decorator(func):
        # The ability to pass arguments here is a gift from closures.
        # If you are not comfortable with closures, you can assume it’s ok,
        # or read: /programming/13857/can-you-explain-closures-as-they-relate-to-python
        print("I am the decorator. Somehow you passed me arguments: {0}, {1}".format(decorator_arg1, decorator_arg2))

        # Don't confuse decorator arguments and function arguments!
        def wrapped(function_arg1, function_arg2) :
            print("I am the wrapper around the decorated function.\n"
                  "I can access all the variables\n"
                  "\t- from the decorator: {0} {1}\n"
                  "\t- from the function call: {2} {3}\n"
                  "Then I can pass them to the decorated function"
                  .format(decorator_arg1, decorator_arg2,
                          function_arg1, function_arg2))
            return func(function_arg1, function_arg2)

        return wrapped

    return my_decorator

@decorator_maker_with_arguments("Leonard", "Sheldon")
def decorated_function_with_arguments(function_arg1, function_arg2):
    print("I am the decorated function and only knows about my arguments: {0}"
           " {1}".format(function_arg1, function_arg2))

decorated_function_with_arguments("Rajesh", "Howard")
#outputs:
#I make decorators! And I accept arguments: Leonard Sheldon
#I am the decorator. Somehow you passed me arguments: Leonard Sheldon
#I am the wrapper around the decorated function. 
#I can access all the variables 
#   - from the decorator: Leonard Sheldon 
#   - from the function call: Rajesh Howard 
#Then I can pass them to the decorated function
#I am the decorated function and only knows about my arguments: Rajesh Howard

Aqui está: um decorador com argumentos. Os argumentos podem ser definidos como variáveis:

c1 = "Penny"
c2 = "Leslie"

@decorator_maker_with_arguments("Leonard", c1)
def decorated_function_with_arguments(function_arg1, function_arg2):
    print("I am the decorated function and only knows about my arguments:"
           " {0} {1}".format(function_arg1, function_arg2))

decorated_function_with_arguments(c2, "Howard")
#outputs:
#I make decorators! And I accept arguments: Leonard Penny
#I am the decorator. Somehow you passed me arguments: Leonard Penny
#I am the wrapper around the decorated function. 
#I can access all the variables 
#   - from the decorator: Leonard Penny 
#   - from the function call: Leslie Howard 
#Then I can pass them to the decorated function
#I am the decorated function and only know about my arguments: Leslie Howard

Como você pode ver, você pode passar argumentos para o decorador como qualquer função usando esse truque. Você pode até usar *args, **kwargsse desejar. Mas lembre-se de que os decoradores são chamados apenas uma vez . Apenas quando o Python importa o script. Você não pode definir dinamicamente os argumentos depois. Quando você "importa x", a função já está decorada , portanto você não pode alterar nada.


Vamos praticar: decorar um decorador

Certo, como bônus, darei um trecho para que qualquer decorador aceite genericamente qualquer argumento. Afinal, para aceitar argumentos, criamos nosso decorador usando outra função.

Envolvemos o decorador.

Mais alguma coisa que vimos recentemente nessa função embrulhada?

Oh sim, decoradores!

Vamos nos divertir e escrever um decorador para os decoradores:

def decorator_with_args(decorator_to_enhance):
    """ 
    This function is supposed to be used as a decorator.
    It must decorate an other function, that is intended to be used as a decorator.
    Take a cup of coffee.
    It will allow any decorator to accept an arbitrary number of arguments,
    saving you the headache to remember how to do that every time.
    """

    # We use the same trick we did to pass arguments
    def decorator_maker(*args, **kwargs):

        # We create on the fly a decorator that accepts only a function
        # but keeps the passed arguments from the maker.
        def decorator_wrapper(func):

            # We return the result of the original decorator, which, after all, 
            # IS JUST AN ORDINARY FUNCTION (which returns a function).
            # Only pitfall: the decorator must have this specific signature or it won't work:
            return decorator_to_enhance(func, *args, **kwargs)

        return decorator_wrapper

    return decorator_maker

Pode ser usado da seguinte maneira:

# You create the function you will use as a decorator. And stick a decorator on it :-)
# Don't forget, the signature is "decorator(func, *args, **kwargs)"
@decorator_with_args 
def decorated_decorator(func, *args, **kwargs): 
    def wrapper(function_arg1, function_arg2):
        print("Decorated with {0} {1}".format(args, kwargs))
        return func(function_arg1, function_arg2)
    return wrapper

# Then you decorate the functions you wish with your brand new decorated decorator.

@decorated_decorator(42, 404, 1024)
def decorated_function(function_arg1, function_arg2):
    print("Hello {0} {1}".format(function_arg1, function_arg2))

decorated_function("Universe and", "everything")
#outputs:
#Decorated with (42, 404, 1024) {}
#Hello Universe and everything

# Whoooot!

Eu sei, a última vez que você teve esse sentimento, foi depois de ouvir um cara dizendo: "antes de entender a recursão, você deve primeiro entender a recursão". Mas agora, você não se sente bem em dominar isso?


Práticas recomendadas: decoradores

  • Os decoradores foram introduzidos no Python 2.4, portanto, certifique-se de que seu código seja executado em> = 2.4.
  • Decoradores diminuem a velocidade da chamada de função. Tenha isso em mente.
  • Você não pode cancelar a decoração de uma função. (Não são hacks para criar decoradores que podem ser removidos, mas ninguém usa-los.) Então, quando uma função é decorada, é decorado para todo o código .
  • Decoradores agrupam funções, o que pode dificultar a depuração. (Isso melhora com o Python> = 2.5; veja abaixo.)

O functoolsmódulo foi introduzido no Python 2.5. Inclui a função functools.wraps(), que copia o nome, o módulo e a sequência de caracteres da função decorada para seu invólucro.

(Curiosidade: functools.wraps()é um decorador! ☺)

# For debugging, the stacktrace prints you the function __name__
def foo():
    print("foo")

print(foo.__name__)
#outputs: foo

# With a decorator, it gets messy    
def bar(func):
    def wrapper():
        print("bar")
        return func()
    return wrapper

@bar
def foo():
    print("foo")

print(foo.__name__)
#outputs: wrapper

# "functools" can help for that

import functools

def bar(func):
    # We say that "wrapper", is wrapping "func"
    # and the magic begins
    @functools.wraps(func)
    def wrapper():
        print("bar")
        return func()
    return wrapper

@bar
def foo():
    print("foo")

print(foo.__name__)
#outputs: foo

Como os decoradores podem ser úteis?

Agora a grande pergunta: para que posso usar decoradores?

Parece legal e poderoso, mas um exemplo prático seria ótimo. Bem, existem 1000 possibilidades. Os usos clássicos estão estendendo o comportamento de uma função de uma biblioteca externa (você não pode modificá-la) ou para depuração (você não deseja modificá-la porque é temporária).

Você pode usá-los para estender várias funções no modo DRY, como:

def benchmark(func):
    """
    A decorator that prints the time a function takes
    to execute.
    """
    import time
    def wrapper(*args, **kwargs):
        t = time.clock()
        res = func(*args, **kwargs)
        print("{0} {1}".format(func.__name__, time.clock()-t))
        return res
    return wrapper


def logging(func):
    """
    A decorator that logs the activity of the script.
    (it actually just prints it, but it could be logging!)
    """
    def wrapper(*args, **kwargs):
        res = func(*args, **kwargs)
        print("{0} {1} {2}".format(func.__name__, args, kwargs))
        return res
    return wrapper


def counter(func):
    """
    A decorator that counts and prints the number of times a function has been executed
    """
    def wrapper(*args, **kwargs):
        wrapper.count = wrapper.count + 1
        res = func(*args, **kwargs)
        print("{0} has been used: {1}x".format(func.__name__, wrapper.count))
        return res
    wrapper.count = 0
    return wrapper

@counter
@benchmark
@logging
def reverse_string(string):
    return str(reversed(string))

print(reverse_string("Able was I ere I saw Elba"))
print(reverse_string("A man, a plan, a canoe, pasta, heros, rajahs, a coloratura, maps, snipe, percale, macaroni, a gag, a banana bag, a tan, a tag, a banana bag again (or a camel), a crepe, pins, Spam, a rut, a Rolo, cash, a jar, sore hats, a peon, a canal: Panama!"))

#outputs:
#reverse_string ('Able was I ere I saw Elba',) {}
#wrapper 0.0
#wrapper has been used: 1x 
#ablE was I ere I saw elbA
#reverse_string ('A man, a plan, a canoe, pasta, heros, rajahs, a coloratura, maps, snipe, percale, macaroni, a gag, a banana bag, a tan, a tag, a banana bag again (or a camel), a crepe, pins, Spam, a rut, a Rolo, cash, a jar, sore hats, a peon, a canal: Panama!',) {}
#wrapper 0.0
#wrapper has been used: 2x
#!amanaP :lanac a ,noep a ,stah eros ,raj a ,hsac ,oloR a ,tur a ,mapS ,snip ,eperc a ,)lemac a ro( niaga gab ananab a ,gat a ,nat a ,gab ananab a ,gag a ,inoracam ,elacrep ,epins ,spam ,arutaroloc a ,shajar ,soreh ,atsap ,eonac a ,nalp a ,nam A

É claro que o bom dos decoradores é que você pode usá-los imediatamente em quase tudo sem precisar reescrever. SECO, eu disse:

@counter
@benchmark
@logging
def get_random_futurama_quote():
    from urllib import urlopen
    result = urlopen("http://subfusion.net/cgi-bin/quote.pl?quote=futurama").read()
    try:
        value = result.split("<br><b><hr><br>")[1].split("<br><br><hr>")[0]
        return value.strip()
    except:
        return "No, I'm ... doesn't!"


print(get_random_futurama_quote())
print(get_random_futurama_quote())

#outputs:
#get_random_futurama_quote () {}
#wrapper 0.02
#wrapper has been used: 1x
#The laws of science be a harsh mistress.
#get_random_futurama_quote () {}
#wrapper 0.01
#wrapper has been used: 2x
#Curse you, merciful Poseidon!

O próprio Python fornece vários decoradores: property, staticmethod, etc.

  • O Django usa decoradores para gerenciar o cache e visualizar permissões.
  • Torcido para falsificar chamadas de funções assíncronas inline.

Este é realmente um grande parque infantil.

e-satis
fonte
15
"Você não pode cancelar a decoração de uma função." - Embora normalmente seja verdade, é possível acessar o retorno da função dentro do fechamento por um decorador (ou seja, por meio de seu __closure__atributo) para retirar a função não decorada original. Um exemplo de uso está documentado nesta resposta, que aborda como é possível injetar uma função decoradora em um nível inferior em circunstâncias limitadas.
metatoaster 22/10/2015
8
Embora essa seja uma ótima resposta, acho que é um pouco enganadora em alguns aspectos. A @decoratorsintaxe do Python é provavelmente usada com mais frequência para substituir uma função por um fechamento de wrapper (como a resposta descreve). Mas também pode substituir a função por outra coisa. O embutido property, classmethode staticmethoddecoradores substituir a função com um descritor, por exemplo. Um decorador também pode fazer algo com uma função, como salvar uma referência a ele em um registro de algum tipo e depois devolvê-lo, sem modificação, sem nenhum invólucro.
precisa saber é o seguinte
3
O fato de que "funções são objetos", embora inteiramente verdadeiro em Python, é um pouco enganador. Armazenar funções em variáveis, transmiti-las como argumentos e retorná-las como resultados, tudo é possível sem que as funções sejam objetos, e existem vários idiomas que possuem funções de primeira classe, mas sem objetos.
0000
1
esta é uma resposta épica bem ali ... Muito obrigado! Como os argumentos padrão de uma função não aparecem como args / kwargs no wrapper do decorador?
Naz
Rolei de volta até o início desta resposta para votar porque o "Como os decoradores podem ser úteis?" seção foi tão útil.
Noumenon
145

Como alternativa, você pode escrever uma função de fábrica que retorne um decorador que agrupe o valor de retorno da função decorada em um tag passado para a função de fábrica. Por exemplo:

from functools import wraps

def wrap_in_tag(tag):
    def factory(func):
        @wraps(func)
        def decorator():
            return '<%(tag)s>%(rv)s</%(tag)s>' % (
                {'tag': tag, 'rv': func()})
        return decorator
    return factory

Isso permite que você escreva:

@wrap_in_tag('b')
@wrap_in_tag('i')
def say():
    return 'hello'

ou

makebold = wrap_in_tag('b')
makeitalic = wrap_in_tag('i')

@makebold
@makeitalic
def say():
    return 'hello'

Pessoalmente, eu teria escrito ao decorador um pouco diferente:

from functools import wraps

def wrap_in_tag(tag):
    def factory(func):
        @wraps(func)
        def decorator(val):
            return func('<%(tag)s>%(val)s</%(tag)s>' %
                        {'tag': tag, 'val': val})
        return decorator
    return factory

o que produziria:

@wrap_in_tag('b')
@wrap_in_tag('i')
def say(val):
    return val
say('hello')

Não se esqueça da construção para a qual a sintaxe do decorador é uma abreviação:

say = wrap_in_tag('b')(wrap_in_tag('i')(say)))
Trevor
fonte
5
Na minha opinião, é melhor evitar mais de um decorador, tanto quanto possível. Se eu tivesse que escrever uma função fábrica Gostaria de codificá-lo com kwargs * como def wrap_in_tag(*kwargs)então@wrap_in_tag('b','i')
guneysus
120

Parece que as outras pessoas já lhe disseram como resolver o problema. Espero que isso ajude você a entender o que são decoradores.

Decoradores são apenas açúcar sintático.

este

@decorator
def func():
    ...

expande para

def func():
    ...
func = decorator(func)
Desconhecido
fonte
3
Isso é tão elegante, simples e fácil de entender. 10000 votos para você, Sir Ockham.
eric
2
Ótima e simples resposta. Gostaria de acrescentar que ao usar @decorator()(em vez de @decorator) é açúcar sintático para func = decorator()(func). Esta é também uma prática comum quando você precisa para gerar decoradores "on the fly"
Omer Dagan
64

E é claro que você pode retornar lambdas também a partir de uma função decoradora:

def makebold(f): 
    return lambda: "<b>" + f() + "</b>"
def makeitalic(f): 
    return lambda: "<i>" + f() + "</i>"

@makebold
@makeitalic
def say():
    return "Hello"

print say()
Rune Kaagaard
fonte
12
E um passo adiante:makebold = lambda f : lambda "<b>" + f() + "</b>"
Robᵩ
13
@ Robᵩ: Para ser sintaticamente correto:makebold = lambda f: lambda: "<b>" + f() + "</b>"
martineau
11
Atrasado para a festa, mas eu realmente gostaria de sugerirmakebold = lambda f: lambda *a, **k: "<b>" + f(*a, **k) + "</b>"
seequ
Isso é necessário functools.wrapspara não descartar o docstring / assinatura / nome desay
Eric
Bem, o que importa é se isso é mencionado na sua resposta. Tendo @wrapsem outro lugar nessa página não vai me ajudar quando eu imprimir help(say)e obter "Ajuda na função <lambda>` em vez de 'Ajuda na função digamos' .
Eric
61

Decoradores Python adicionam funcionalidade extra a outra função

Um decorador em itálico pode ser como

def makeitalic(fn):
    def newFunc():
        return "<i>" + fn() + "</i>"
    return newFunc

Observe que uma função é definida dentro de uma função. O que basicamente faz é substituir uma função pela recém-definida. Por exemplo, eu tenho essa classe

class foo:
    def bar(self):
        print "hi"
    def foobar(self):
        print "hi again"

Agora diga, quero que ambas as funções imprimam "---" depois e antes de serem concluídas. Eu poderia adicionar uma impressão "---" antes e depois de cada instrução de impressão. Mas como não gosto de me repetir, farei um decorador

def addDashes(fn): # notice it takes a function as an argument
    def newFunction(self): # define a new function
        print "---"
        fn(self) # call the original function
        print "---"
    return newFunction
    # Return the newly defined function - it will "replace" the original

Então agora eu posso mudar minha classe para

class foo:
    @addDashes
    def bar(self):
        print "hi"

    @addDashes
    def foobar(self):
        print "hi again"

Para obter mais informações sobre decoradores, consulte http://www.ibm.com/developerworks/linux/library/l-cpdecor.html

Abhinav Gupta
fonte
Observe tão elegante quanto as funções lambda propostas por @Rune Kaagaard
rds
1
@ Phoenix: O selfargumento é necessário porque o newFunction()definido em addDashes()foi especificamente projetado para ser um decorador de método e não um decorador de função geral. O selfargumento representa a instância da classe e é passado aos métodos da classe, independentemente de serem usados ​​ou não - consulte a seção intitulada Decorando métodos na resposta do @ e-satis.
martineau
1
Imprima a saída também, por favor.
User1767754
Em faltafunctools.wraps
Eric
39

Você pode criar dois decoradores separados que fazem o que você deseja, conforme ilustrado diretamente abaixo. Observe o uso de *args, **kwargsna declaração dowrapped() função que suporta a função decorada com vários argumentos (o que não é realmente necessário para a say()função de exemplo , mas está incluído para generalidade).

Por razões semelhantes, o functools.wrapsdecorador é usado para alterar os meta atributos da função agrupada para serem aqueles da que está sendo decorada. Isso faz com que as mensagens de erro e a documentação da função incorporada ( func.__doc__) sejam aquelas da função decorada em vez de wrapped()'s.

from functools import wraps

def makebold(fn):
    @wraps(fn)
    def wrapped(*args, **kwargs):
        return "<b>" + fn(*args, **kwargs) + "</b>"
    return wrapped

def makeitalic(fn):
    @wraps(fn)
    def wrapped(*args, **kwargs):
        return "<i>" + fn(*args, **kwargs) + "</i>"
    return wrapped

@makebold
@makeitalic
def say():
    return 'Hello'

print(say())  # -> <b><i>Hello</i></b>

Refinamentos

Como você pode ver, há muitos códigos duplicados nesses dois decoradores. Dada essa semelhança, seria melhor você criar uma genérica que na verdade fosse uma fábrica de decoradores - em outras palavras, uma função decoradora que produz outros decoradores. Dessa forma, haveria menos repetição de código - e permitiria que o princípio DRY fosse seguido.

def html_deco(tag):
    def decorator(fn):
        @wraps(fn)
        def wrapped(*args, **kwargs):
            return '<%s>' % tag + fn(*args, **kwargs) + '</%s>' % tag
        return wrapped
    return decorator

@html_deco('b')
@html_deco('i')
def greet(whom=''):
    return 'Hello' + (' ' + whom) if whom else ''

print(greet('world'))  # -> <b><i>Hello world</i></b>

Para tornar o código mais legível, você pode atribuir um nome mais descritivo aos decoradores gerados de fábrica:

makebold = html_deco('b')
makeitalic = html_deco('i')

@makebold
@makeitalic
def greet(whom=''):
    return 'Hello' + (' ' + whom) if whom else ''

print(greet('world'))  # -> <b><i>Hello world</i></b>

ou até combiná-los assim:

makebolditalic = lambda fn: makebold(makeitalic(fn))

@makebolditalic
def greet(whom=''):
    return 'Hello' + (' ' + whom) if whom else ''

print(greet('world'))  # -> <b><i>Hello world</i></b>

Eficiência

Enquanto os exemplos acima fazem todo o trabalho, o código gerado envolve uma boa quantidade de sobrecarga na forma de chamadas de funções estranhas quando vários decoradores são aplicados ao mesmo tempo. Isso pode não importar, dependendo do uso exato (que pode ser vinculado à E / S, por exemplo).

Se a velocidade da função decorada é importante, a sobrecarga pode ser mantida em uma única chamada de função extra, escrevendo uma função de fábrica decoradora ligeiramente diferente que implementa a adição de todas as tags de uma só vez, para gerar código que evita as chamadas de função adicionais incorridas usando decoradores separados para cada tag.

Isso requer mais código no próprio decorador, mas isso só é executado quando está sendo aplicado às definições de função, e não mais tarde quando elas próprias são chamadas. Isso também se aplica ao criar nomes mais legíveis usando lambdafunções como ilustrado anteriormente. Amostra:

def multi_html_deco(*tags):
    start_tags, end_tags = [], []
    for tag in tags:
        start_tags.append('<%s>' % tag)
        end_tags.append('</%s>' % tag)
    start_tags = ''.join(start_tags)
    end_tags = ''.join(reversed(end_tags))

    def decorator(fn):
        @wraps(fn)
        def wrapped(*args, **kwargs):
            return start_tags + fn(*args, **kwargs) + end_tags
        return wrapped
    return decorator

makebolditalic = multi_html_deco('b', 'i')

@makebolditalic
def greet(whom=''):
    return 'Hello' + (' ' + whom) if whom else ''

print(greet('world'))  # -> <b><i>Hello world</i></b>
Martineau
fonte
2
upvote para referindo-se DRY :-)
nitin3685
Obrigado pela explicação "@wraps (diversão)" :)
notas de rodapé
20

Outra maneira de fazer a mesma coisa:

class bol(object):
  def __init__(self, f):
    self.f = f
  def __call__(self):
    return "<b>{}</b>".format(self.f())

class ita(object):
  def __init__(self, f):
    self.f = f
  def __call__(self):
    return "<i>{}</i>".format(self.f())

@bol
@ita
def sayhi():
  return 'hi'

Ou, de maneira mais flexível:

class sty(object):
  def __init__(self, tag):
    self.tag = tag
  def __call__(self, f):
    def newf():
      return "<{tag}>{res}</{tag}>".format(res=f(), tag=self.tag)
    return newf

@sty('b')
@sty('i')
def sayhi():
  return 'hi'
qed
fonte
Necessidades functools.update_wrapperpara mantersayhi.__name__ == "sayhi"
Eric
19

Como posso criar dois decoradores em Python que fazem o seguinte?

Você deseja a seguinte função, quando chamada:

@makebold
@makeitalic
def say():
    return "Hello"

Para retornar:

<b><i>Hello</i></b>

Solução simples

Para simplificar, faça decoradores que retornem lambdas (funções anônimas) que fechem sobre a função (closures) e chame-a:

def makeitalic(fn):
    return lambda: '<i>' + fn() + '</i>'

def makebold(fn):
    return lambda: '<b>' + fn() + '</b>'

Agora use-os conforme desejado:

@makebold
@makeitalic
def say():
    return 'Hello'

e agora:

>>> say()
'<b><i>Hello</i></b>'

Problemas com a solução simples

Mas parece que quase perdemos a função original.

>>> say
<function <lambda> at 0x4ACFA070>

Para encontrá-lo, precisamos cavar o fechamento de cada lambda, um dos quais está enterrado no outro:

>>> say.__closure__[0].cell_contents
<function <lambda> at 0x4ACFA030>
>>> say.__closure__[0].cell_contents.__closure__[0].cell_contents
<function say at 0x4ACFA730>

Portanto, se colocarmos a documentação nessa função, ou desejarmos decorar funções que levam mais de um argumento, ou apenas quisermos saber qual função estávamos observando em uma sessão de depuração, precisamos fazer um pouco mais com o nosso embrulho.

Solução completa - superando a maioria desses problemas

Temos o decorador wrapsdo functoolsmódulo na biblioteca padrão!

from functools import wraps

def makeitalic(fn):
    # must assign/update attributes from wrapped function to wrapper
    # __module__, __name__, __doc__, and __dict__ by default
    @wraps(fn) # explicitly give function whose attributes it is applying
    def wrapped(*args, **kwargs):
        return '<i>' + fn(*args, **kwargs) + '</i>'
    return wrapped

def makebold(fn):
    @wraps(fn)
    def wrapped(*args, **kwargs):
        return '<b>' + fn(*args, **kwargs) + '</b>'
    return wrapped

É lamentável que ainda exista algum boilerplate, mas isso é o mais simples que podemos fazer.

No Python 3, você também obtém __qualname__e__annotations__ atribui por padrão.

Então agora:

@makebold
@makeitalic
def say():
    """This function returns a bolded, italicized 'hello'"""
    return 'Hello'

E agora:

>>> say
<function say at 0x14BB8F70>
>>> help(say)
Help on function say in module __main__:

say(*args, **kwargs)
    This function returns a bolded, italicized 'hello'

Conclusão

Então nós vemos isso wraps faz com a função de empacotamento faça quase tudo, exceto nos diga exatamente o que a função usa como argumentos.

Existem outros módulos que podem tentar resolver o problema, mas a solução ainda não está na biblioteca padrão.

Aaron Hall
fonte
14

Para explicar o decorador de uma maneira simples:

Com:

@decor1
@decor2
def func(*args, **kwargs):
    pass

Quando fazer:

func(*args, **kwargs)

Você realmente faz:

decor1(decor2(func))(*args, **kwargs)
changyuheng
fonte
13

Um decorador pega a definição da função e cria uma nova função que executa essa função e transforma o resultado.

@deco
def do():
    ...

é equivalente a:

do = deco(do)

Exemplo:

def deco(func):
    def inner(letter):
        return func(letter).upper()  #upper
    return inner

este

@deco
def do(number):
    return chr(number)  # number to letter

é equivalente a isso

def do2(number):
    return chr(number)

do2 = deco(do2)

65 <=> 'a'

print(do(65))
print(do2(65))
>>> B
>>> B

Para entender o decorador, é importante notar que esse decorador criou uma nova função que é interna que executa a função e transforma o resultado.

Davoud Taghawi-Nejad
fonte
A saída de print(do(65))e print(do2(65))be Ae não A?
Treefish Zhang
8
#decorator.py
def makeHtmlTag(tag, *args, **kwds):
    def real_decorator(fn):
        css_class = " class='{0}'".format(kwds["css_class"]) \
                                 if "css_class" in kwds else ""
        def wrapped(*args, **kwds):
            return "<"+tag+css_class+">" + fn(*args, **kwds) + "</"+tag+">"
        return wrapped
    # return decorator dont call it
    return real_decorator

@makeHtmlTag(tag="b", css_class="bold_css")
@makeHtmlTag(tag="i", css_class="italic_css")
def hello():
    return "hello world"

print hello()

Você também pode escrever decorador em Class

#class.py
class makeHtmlTagClass(object):
    def __init__(self, tag, css_class=""):
        self._tag = tag
        self._css_class = " class='{0}'".format(css_class) \
                                       if css_class != "" else ""

    def __call__(self, fn):
        def wrapped(*args, **kwargs):
            return "<" + self._tag + self._css_class+">"  \
                       + fn(*args, **kwargs) + "</" + self._tag + ">"
        return wrapped

@makeHtmlTagClass(tag="b", css_class="bold_css")
@makeHtmlTagClass(tag="i", css_class="italic_css")
def hello(name):
    return "Hello, {}".format(name)

print hello("Your name")
nickleefly
fonte
1
A razão para gostar de uma classe aqui é que há um comportamento claramente relacionado, com duas instâncias. Você pode realmente obter seus dois decoradores atribuindo as classes construídas aos nomes desejados, em vez de reiterar os parâmetros. Isso é mais difícil de fazer com uma função. Adicioná-lo ao exemplo indicaria por que isso não é apenas redundante.
JavaScript é necessário
8

Esta resposta já foi respondida há muito tempo, mas pensei em compartilhar minha classe Decorator, o que torna a redação de novos decoradores fácil e compacta.

from abc import ABCMeta, abstractclassmethod

class Decorator(metaclass=ABCMeta):
    """ Acts as a base class for all decorators """

    def __init__(self):
        self.method = None

    def __call__(self, method):
        self.method = method
        return self.call

    @abstractclassmethod
    def call(self, *args, **kwargs):
        return self.method(*args, **kwargs)

Por um lado, acho que isso torna o comportamento dos decoradores muito claro, mas também facilita a definição de novos decoradores de maneira muito concisa. Para o exemplo listado acima, você pode resolvê-lo como:

class MakeBold(Decorator):
    def call():
        return "<b>" + self.method() + "</b>"

class MakeItalic(Decorator):
    def call():
        return "<i>" + self.method() + "</i>"

@MakeBold()
@MakeItalic()
def say():
   return "Hello"

Você também pode usá-lo para realizar tarefas mais complexas, como, por exemplo, um decorador que automaticamente faz com que a função seja aplicada recursivamente a todos os argumentos em um iterador:

class ApplyRecursive(Decorator):
    def __init__(self, *types):
        super().__init__()
        if not len(types):
            types = (dict, list, tuple, set)
        self._types = types

    def call(self, arg):
        if dict in self._types and isinstance(arg, dict):
            return {key: self.call(value) for key, value in arg.items()}

        if set in self._types and isinstance(arg, set):
            return set(self.call(value) for value in arg)

        if tuple in self._types and isinstance(arg, tuple):
            return tuple(self.call(value) for value in arg)

        if list in self._types and isinstance(arg, list):
            return list(self.call(value) for value in arg)

        return self.method(arg)


@ApplyRecursive(tuple, set, dict)
def double(arg):
    return 2*arg

print(double(1))
print(double({'a': 1, 'b': 2}))
print(double({1, 2, 3}))
print(double((1, 2, 3, 4)))
print(double([1, 2, 3, 4, 5]))

Que imprime:

2
{'a': 2, 'b': 4}
{2, 4, 6}
(2, 4, 6, 8)
[1, 2, 3, 4, 5, 1, 2, 3, 4, 5]

Observe que este exemplo não incluiu o listtipo na instanciação do decorador; portanto, na declaração de impressão final, o método é aplicado à própria lista, não aos elementos da lista.

v4gil
fonte
7

Aqui está um exemplo simples de encadear decoradores. Observe a última linha - mostra o que está acontecendo debaixo das cobertas.

############################################################
#
#    decorators
#
############################################################

def bold(fn):
    def decorate():
        # surround with bold tags before calling original function
        return "<b>" + fn() + "</b>"
    return decorate


def uk(fn):
    def decorate():
        # swap month and day
        fields = fn().split('/')
        date = fields[1] + "/" + fields[0] + "/" + fields[2]
        return date
    return decorate

import datetime
def getDate():
    now = datetime.datetime.now()
    return "%d/%d/%d" % (now.day, now.month, now.year)

@bold
def getBoldDate(): 
    return getDate()

@uk
def getUkDate():
    return getDate()

@bold
@uk
def getBoldUkDate():
    return getDate()


print getDate()
print getBoldDate()
print getUkDate()
print getBoldUkDate()
# what is happening under the covers
print bold(uk(getDate))()

A saída se parece com:

17/6/2013
<b>17/6/2013</b>
6/17/2013
<b>6/17/2013</b>
<b>6/17/2013</b>
resignado
fonte
6

Falando no exemplo do contador - como indicado acima, o contador será compartilhado entre todas as funções que usam o decorador:

def counter(func):
    def wrapped(*args, **kws):
        print 'Called #%i' % wrapped.count
        wrapped.count += 1
        return func(*args, **kws)
    wrapped.count = 0
    return wrapped

Dessa forma, seu decorador pode ser reutilizado para diferentes funções (ou usado para decorar a mesma função várias vezes:) func_counter1 = counter(func); func_counter2 = counter(func), e a variável do contador permanecerá privada para cada uma.

marcado
fonte
6

Decore funções com diferentes números de argumentos:

def frame_tests(fn):
    def wrapper(*args):
        print "\nStart: %s" %(fn.__name__)
        fn(*args)
        print "End: %s\n" %(fn.__name__)
    return wrapper

@frame_tests
def test_fn1():
    print "This is only a test!"

@frame_tests
def test_fn2(s1):
    print "This is only a test! %s" %(s1)

@frame_tests
def test_fn3(s1, s2):
    print "This is only a test! %s %s" %(s1, s2)

if __name__ == "__main__":
    test_fn1()
    test_fn2('OK!')
    test_fn3('OK!', 'Just a test!')

Resultado:

Start: test_fn1  
This is only a test!  
End: test_fn1  


Start: test_fn2  
This is only a test! OK!  
End: test_fn2  


Start: test_fn3  
This is only a test! OK! Just a test!  
End: test_fn3  
rabin utam
fonte
1
Isso poderia ser facilmente mais versátil, fornecendo também suporte para argumentos de palavras-chave via def wrapper(*args, **kwargs):e fn(*args, **kwargs).
27515
5

A resposta de Paolo Bergantino tem a grande vantagem de usar apenas o stdlib e funciona para este exemplo simples, onde não há argumentos de decorador nem argumentos de função decorados .

No entanto, há três grandes limitações se você quiser lidar com casos mais gerais:

  • como já observado em várias respostas, você não pode modificar facilmente o código para adicionar argumentos opcionais do decorador . Por exemplo, criando ummakestyle(style='bold') decorador não é trivial.
  • além disso, os wrappers criados com @functools.wraps não preservam a assinatura ; portanto, se forem fornecidos argumentos incorretos, eles começarão a ser executados e poderão gerar um tipo de erro diferente do habitual TypeError.
  • finalmente, é bastante difícil em embalagens criadas com @functools.wrapsa acessar um argumento baseado em seu nome . De fato, o argumento pode aparecer em *args, dentro **kwargsou pode não aparecer (se for opcional).

Escrevi decopatchpara resolver o primeiro problema e escrevi makefun.wrapspara resolver os outros dois. Observe que makefunutiliza o mesmo truque que o famosodecorator lib.

É assim que você criaria um decorador com argumentos, retornando invólucros que preservam verdadeiramente a assinatura:

from decopatch import function_decorator, DECORATED
from makefun import wraps

@function_decorator
def makestyle(st='b', fn=DECORATED):
    open_tag = "<%s>" % st
    close_tag = "</%s>" % st

    @wraps(fn)
    def wrapped(*args, **kwargs):
        return open_tag + fn(*args, **kwargs) + close_tag

    return wrapped

decopatchfornece outros dois estilos de desenvolvimento que ocultam ou mostram os vários conceitos de python, dependendo de suas preferências. O estilo mais compacto é o seguinte:

from decopatch import function_decorator, WRAPPED, F_ARGS, F_KWARGS

@function_decorator
def makestyle(st='b', fn=WRAPPED, f_args=F_ARGS, f_kwargs=F_KWARGS):
    open_tag = "<%s>" % st
    close_tag = "</%s>" % st
    return open_tag + fn(*f_args, **f_kwargs) + close_tag

Nos dois casos, você pode verificar se o decorador funciona conforme o esperado:

@makestyle
@makestyle('i')
def hello(who):
    return "hello %s" % who

assert hello('world') == '<b><i>hello world</i></b>'    

Por favor, consulte a documentação para obter detalhes.

smarie
fonte