Pytanie Jak stworzyć łańcuch dekoratorów funkcji?


Jak mogę utworzyć dwa Python w dwóch dekoratorów, które wykonają następujące czynności?

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

... który powinien powrócić:

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

Nie próbuję tworzyć HTML w ten sposób w prawdziwej aplikacji - po prostu próbując zrozumieć, jak działają dekoratory i łańcuch dekoratorski.


2382
2018-04-11 07:05


pochodzenie




Odpowiedzi:


Sprawdzić dokumentacja aby zobaczyć, jak działają dekoratory. Oto, o co prosiłeś:

def makebold(fn):
    def wrapped():
        return "<b>" + fn() + "</b>"
    return wrapped

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

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

print hello() ## returns "<b><i>hello world</i></b>"

2658
2018-04-11 07:16



Rozważ użycie functools.wraps lub, jeszcze lepiej, moduł dekoratora z PyPI: zachowują niektóre ważne metadane (np __name__ oraz mówiąc o pakiecie dekoratora, podpisie funkcji). - Marius Gedminas
*argsi **kwargs należy dodać w odpowiedzi. Udekorowana funkcja może mieć argumenty i zostaną one utracone, jeśli nie zostaną określone. - Blusky


Jeśli nie masz długich wyjaśnień, zobacz Odpowiedź Paolo Bergantino.

Podstawy dekoratora

Funkcje Pythona są obiektami

Aby zrozumieć dekoratory, musisz najpierw zrozumieć, że funkcje są obiektami w Pythonie. Ma to ważne konsekwencje. Zobaczmy, dlaczego na prostym przykładzie:

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, e:
    print(e)
    #outputs: "name 'shout' is not defined"

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

Pamiętaj o tym. Wkrótce się z tym obejrzymy.

Inną interesującą właściwością funkcji Pythona jest to, że można je zdefiniować w innej funkcji!

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, e:
    print(e)
    #outputs : "name 'whisper' is not defined"*
    #Python's functions are objects

Odnośniki funkcji

Okay, wciąż tu jesteś? Teraz zabawna część ...

Widziałeś, że funkcje są obiektami. Dlatego funkcje:

  • można przypisać do zmiennej
  • można zdefiniować w innej funkcji

Oznacza to, że funkcja może return inna funkcja.

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...

Jest więcej!

Jeśli możesz return funkcja, możesz przekazać jeden jako parametr:

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!

Cóż, masz wszystko, co potrzebne do zrozumienia dekoratorów. Widzisz, dekoratorzy są "owijkami", co oznacza, że umożliwiają wykonanie kodu przed i po funkcji, którą dekorują bez modyfikowania samej funkcji.

Ręcznie robione dekoratory

Jak zrobić to ręcznie:

# 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

Teraz prawdopodobnie chcesz tego za każdym razem, gdy dzwonisz a_stand_alone_function, a_stand_alone_function_decorated nazywa się zamiast tego. To proste, wystarczy je zastąpić a_stand_alone_function z funkcją zwróconą przez 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!

Dekoratory demystified

Poprzedni przykład, używając składni dekoratora:

@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

Tak, to wszystko, to takie proste. @decorator to skrót do:

another_stand_alone_function = my_shiny_new_decorator(another_stand_alone_function)

Dekoratory są tylko pytonowym wariantem dekorator wzór. W Pythonie wbudowanych jest kilka klasycznych wzorców ułatwiających rozwój (np. Iteratory).

Oczywiście można gromadzić dekoratorów:

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~
#<\______/>

Korzystanie ze składni dekoratora języka Python:

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

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

Kolejność, w jakiej ustawisz dekoratory ZGŁASZA:

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

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

Teraz: aby odpowiedzieć na pytanie ...

Podsumowując, możesz łatwo zobaczyć, jak odpowiedzieć na pytanie:

# 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>

Możesz teraz po prostu wyjść z radości lub spalić swój mózg trochę więcej i zobaczyć zaawansowane zastosowania dekoratorów.


Zabieranie dekoratorów na wyższy poziom

Przekazywanie argumentów do udekorowanej funkcji

# 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

Metody dekorowania

Jedną z najlepszych rzeczy na temat Pythona jest to, że metody i funkcje są naprawdę takie same. Jedyną różnicą jest to, że metody oczekują, że ich pierwszy argument będzie odwoływał się do bieżącego obiektu (self).

Oznacza to, że można zbudować dekorator dla metod w ten sam sposób! Tylko pamiętaj, aby wziąć self pod uwagę:

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?

Jeśli tworzysz dekorator ogólnego przeznaczenia - taki, który zastosujesz do dowolnej funkcji lub metody, bez względu na jej argumenty - po prostu użyj *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?

Przekazywanie argumentów dekoratorowi

Świetnie, co powiesz na przekazanie argumentów samemu dekoratorowi?

Może to być nieco zakręcone, ponieważ dekorator musi przyjąć funkcję jako argument. Dlatego nie można przekazać argumentów funkcji dekorowanej bezpośrednio do dekoratora.

Zanim przejdziemy do rozwiązania, napiszmy przypomnienie:

# 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

To jest dokładnie to samo. "my_decorator"jest wywoływane, więc kiedy ty @my_decorator, mówisz Pythonowi, aby wywołał funkcję "oznaczoną przez zmienną"my_decorator"".

To jest ważne! Etykieta, którą podasz, może wskazywać bezpośrednio dekoratorowialbo nie.

Złośmy.

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.

Nic dziwnego.

Zróbmy dokładnie to samo, ale pomiń wszystkie brzydkie zmienne pośrednie:

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.

Zróbmy to nawet krótszy:

@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.

Hej, widziałeś to? Użyliśmy wywołania funkcji z "@"Składnia! :-)

Wróćmy więc do dekoratorów argumentami. Jeśli możemy użyć funkcji do generowania dekoratora w locie, możemy przekazać argumenty do tej funkcji, prawda?

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: https://stackoverflow.com/questions/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

Oto ona: dekorator z argumentami. Argumenty można ustawić jako zmienne:

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

Jak widać, możesz przekazywać argumenty do dekoratora, jak każdą inną funkcję za pomocą tej sztuczki. Możesz nawet użyć *args, **kwargs Jeśli chcesz. Ale pamiętajcie, że dekoratorzy są nazywani tylko raz. Właśnie wtedy, gdy Python importuje skrypt. Nie można później dynamicznie ustawiać argumentów. Kiedy wykonujesz "import x", funkcja jest już udekorowana, więc nie możesz zmienić cokolwiek.


Ćwiczmy: dekorowanie dekoratora

Okay, jako bonus podam ci fragment, który sprawi, że każdy dekorator zaakceptuje generalnie wszelkie argumenty. Wszakże w celu przyjęcia argumentów, stworzyliśmy naszego dekoratora za pomocą innej funkcji.

Owinęliśmy dekoratora.

Czy cokolwiek innego widzieliśmy ostatnio, że zawijaliśmy funkcję?

Och tak, dekoratorzy!

Bawmy się i pisz dekoratora do dekoratorów:

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

Może być używany w następujący sposób:

# 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!

Wiem, kiedy ostatnio miałeś to uczucie, to po wysłuchaniu faceta mówiącego: "zanim zrozumiesz rekursję, musisz najpierw zrozumieć rekursję". Ale czy nie czujesz się dobrze z opanowaniem tego?


Najlepsze praktyki: dekoratorzy

  • Dekoratory zostały wprowadzone w Pythonie 2.4, więc upewnij się, że twój kod zostanie uruchomiony na> = 2.4.
  • Dekoratory spowalniają wywołanie funkcji. Miej to w pamięci.
  • Nie możesz anulować dekoracji funkcji. (Tam  hacki do tworzenia dekoratorów, które można usunąć, ale nikt ich nie używa.) Więc gdy funkcja jest dekorowana, jest dekorowana dla całego kodu.
  • Dekoratory zawijają funkcje, co może utrudnić ich debugowanie. (Lepiej z Python> = 2.5, patrz poniżej.)

The functools moduł został wprowadzony w Pythonie 2.5. Obejmuje funkcję functools.wraps(), który kopiuje nazwę, moduł i docstring udekorowanej funkcji do jej opakowania.

(Śmieszny fakt: functools.wraps() jest dekoratorem! )

# 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

W jaki sposób dekoratorzy mogą się przydać?

Teraz wielkie pytanie: Do czego mogę użyć dekoratorów?

Wydaje się fajne i potężne, ale praktyczny przykład byłby świetny. Cóż, istnieje 1000 możliwości. Klasyczne zastosowania rozszerzają działanie funkcji z zewnętrznej biblioteki (nie można jej modyfikować) lub debugowania (nie chcesz jej modyfikować, ponieważ jest tymczasowa).

Możesz ich użyć do rozszerzenia kilku funkcji w trybie DRY, na przykład:

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

Oczywiście dobrą rzeczą w przypadku dekoratorów jest to, że można z nich korzystać od razu na prawie wszystko bez przepisywania. DRY, powiedziałem:

@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!

Sam Python zapewnia kilka dekoratorów: property, staticmethoditp.

  • Django używa dekoratorów do zarządzania buforowaniem i uprawnień widoku.
  • Skręcony do fałszywych wywołań asynchronicznych wywołań funkcji.

To naprawdę duży plac zabaw.


3811
2018-04-11 08:00



"Nie możesz odszyfrować funkcji." - Chociaż normalnie jest to prawda, możliwe jest osiągnięcie wewnątrz zamknięcia powrotu funkcji przez dekorator (to znaczy za pośrednictwem jej __closure__ atrybut), aby wyciągnąć oryginalną, nie przygotowaną funkcję. Jeden przykład użycia jest udokumentowany w ta odpowiedź która dotyczy możliwości wstrzyknięcia funkcji dekoratora na niższym poziomie w ograniczonych okolicznościach. - metatoaster
Chociaż jest to świetna odpowiedź, myślę, że jest to trochę mylące pod pewnymi względami. Python @decorator składnia jest prawdopodobnie najczęściej używana do zastąpienia funkcji zamknięciem opakowania (jak opisuje odpowiedź). Ale może również zastąpić funkcję czymś innym. Wbudowany property, classmethod i staticmethod dekoratory zastępują na przykład funkcję deskryptorem. Dekorator może również zrobić coś z funkcją, taką jak zapisanie odniesienia do niej w rejestrze, a następnie zwrócić ją, bez modyfikacji, bez żadnego opakowania. - Blckknght
Fakt, że "funkcje są obiektami", podczas gdy w Pythonie jest całkowicie prawdziwy, jest trochę mylący. Przechowywanie funkcji w zmiennych, przekazywanie ich jako argumentów i zwracanie ich jako wyników jest możliwe bez funkcji, które w rzeczywistości są obiektami, i są różne języki, które mają pierwszorzędne funkcje, ale nie obiekty. - 00dani
Z łatwością najlepsze wytłumaczenie w internecie na temat dekoratorów, z bardzo dobrze wytłumaczonym, wypróbowanym krokiem na każdym kroku. Naprawdę doceniam dbałość o szczegóły. - Abhi Tk
Bardzo pomocne, dogłębne wyjaśnienie. Przykłady bardzo pomagają! - Charles


Alternatywnie, możesz napisać funkcję fabryczną, która zwróci dekorator, który zawinie wartość zwracaną przez dekorowaną funkcję w znaczniku przekazanym do funkcji fabrycznej. Na przykład:

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

Dzięki temu możesz napisać:

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

lub

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

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

Osobiście napisałbym dekoratora nieco inaczej:

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

co dałoby:

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

Nie zapomnij o konstrukcji, dla której składnia dekoratora jest skrótem:

say = wrap_in_tag('b')(wrap_in_tag('i')(say)))

132
2017-10-25 06:18



Moim zdaniem lepiej unikać jak najwyżej jednego dekoratora. Gdybym miał napisać funkcję fabryczną, zakodowałbym ją za pomocą * kwargs def wrap_in_tag(*kwargs) następnie @wrap_in_tag('b','i') - guneysus


Wygląda na to, że inni już ci powiedzieli, jak rozwiązać problem. Mam nadzieję, że to pomoże ci zrozumieć, czym są dekoratorzy.

Dekoratory są po prostu syntaktycznym cukrem.

To

@decorator
def func():
    ...

rozwija się do

def func():
    ...
func = decorator(func)

101
2018-04-11 07:19



To jest takie eleganckie, proste, łatwe do zrozumienia. 10000 przegranych dla ciebie, Sir Ockham. - neuronet
Świetna i prosta odpowiedź. Chciałbym dodać, że podczas korzystania @decorator() (zamiast @decorator) to jest cukier syntaktyczny dla func = decorator()(func). Jest to również powszechna praktyka, gdy trzeba generować dekoratory "w locie" - Omer Dagan


I oczywiście możesz zwrócić lambdas także z funkcji dekoratora:

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

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

print say()

59
2018-05-17 03:26



I jeszcze jeden krok dalej: makebold = lambda f : lambda "<b>" + f() + "</b>" - Robᵩ
@ Robᵩ: Aby być poprawnym składniowo: makebold = lambda f: lambda: "<b>" + f() + "</b>" - martineau
Późno na imprezę, ale naprawdę bym to zasugerował makebold = lambda f: lambda *a, **k: "<b>" + f(*a, **k) + "</b>" - seequ


Dekoratory Python dodają dodatkową funkcjonalność do innej funkcji

Może to być dekorator kursywa

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

Zauważ, że funkcja jest zdefiniowana wewnątrz funkcji. W zasadzie to zastępuje funkcję nowo zdefiniowaną. Na przykład mam tę klasę

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

Teraz mówię, że obie funkcje mają być drukowane "---" po i przed ich wykonaniem. Mogłabym dodać wydruk "---" przed i po każdym poleceniu print. Ale ponieważ nie lubię się powtarzać, zrobię dekoratora

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

Teraz mogę zmienić moją klasę na

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

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

Aby uzyskać więcej informacji na temat dekoratorów, sprawdź http://www.ibm.com/developerworks/linux/library/l-cpdecor.html


56
2017-12-26 06:13



Uwaga tak elegancka, jak funkcje lambda zaproponowane przez @Rune Kaagaard - rds
Jakie jest tutaj działanie słowa kluczowego self? - asit_dhal
@Phoenix: The self argument jest potrzebny, ponieważ newFunction() określone w addDashes() został specjalnie zaprojektowany, aby być metoda dekorator nie jest dekoratorem ogólnej funkcji. The self argument reprezentuje instancję klasy i jest przekazywany do metod klasowych, niezależnie od tego, czy go używają, czy nie - patrz sekcja zatytułowana Metody dekorowania w odpowiedzi @ e-satis. - martineau
Wydrukuj również dane wyjściowe. - user1767754


ty mógłby zrobić dwa oddzielne dekoratory, które robią to, co chcesz, jak pokazano bezpośrednio poniżej. Zwróć uwagę na użycie *args, **kwargs w deklaracji wrapped() funkcja, która obsługuje udekorowaną funkcję mającą wiele argumentów (co nie jest konieczne dla przykładu say() funkcja, ale jest uwzględniona w ogólności).

Z podobnych powodów, functools.wraps dekorator służy do zmiany atrybutów meta funkcji zawijanej na tę z dekorowanego. W ten sposób powstają komunikaty o błędach i dokumentacja funkcji wbudowanych (func.__doc__) to te z funkcji zdobionej zamiast wrapped().

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>

Udoskonalenia

Jak widać, w tych dwóch dekoratorach jest dużo duplikatów kodu. Biorąc pod uwagę to podobieństwo, byłoby lepiej, gdybyś zamiast tego zrobił rodzajowy, który faktycznie był fabryka dekoratorówInnymi słowy dekorator, który tworzy inne dekoratory. W ten sposób będzie mniej powtórzeń kodu - i zezwól na SUCHY zasada, której należy przestrzegać.

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>

Aby kod był bardziej czytelny, możesz przypisać bardziej opisową nazwę do generatorów generowanych fabrycznie:

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>

lub nawet połączyć je w ten sposób:

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>

Wydajność

Chociaż powyższe przykłady działają, wygenerowany kod wiąże się z dużą ilością narzutów w postaci zewnętrznych wywołań funkcji, gdy stosuje się jednocześnie wiele dekoratorów. To może nie mieć znaczenia, w zależności od dokładnego użycia (na przykład, związanego z I / O).

Jeśli prędkość dekorowanej funkcji jest ważna, narzut może zostać utrzymany w jednym dodatkowym wywołaniu funkcji przez napisanie nieco innej fabrycznej funkcji dekoratora, która implementuje dodawanie wszystkich znaczników na raz, aby mógł wygenerować kod, który uniknie dodatkowych wywołań funkcji wywołanych za pomocą oddzielnych dekoratorów dla każdej etykiety.

Wymaga to więcej kodu w samym dekoratorze, ale działa tylko wtedy, gdy jest zastosowany do definicji funkcji, a nie później, gdy sami są wywoływani. Dotyczy to również tworzenia bardziej czytelnych nazw za pomocą lambda funkcje jak wcześniej pokazano. Próba:

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>

25
2017-12-03 18:09





Inny sposób robienia tego samego:

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'

Lub bardziej elastycznie:

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'

18
2017-07-26 16:11





Jak mogę utworzyć dwa Python w dwóch dekoratorów, które wykonają następujące czynności?

Jeśli chcesz wywołać następującą funkcję:

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

Wracać:

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

Proste rozwiązanie

Najprościej to uczyńmy, tworząc dekoratory, które zwracają lambdas (anonimowe funkcje), które zamykają się nad funkcją (zamknięciami) i nazywają ją:

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

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

Teraz użyj ich zgodnie z życzeniem:

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

i teraz:

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

Problemy z prostym rozwiązaniem

Ale wydaje się, że prawie straciliśmy pierwotną funkcję.

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

Aby go znaleźć, musielibyśmy zagłębić się w zamknięcie każdego lambda, z których jeden jest pochowany w drugim:

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

Jeśli więc umieścimy dokumentację na temat tej funkcji lub chcemy mieć możliwość dekorowania funkcji, które wymagają więcej niż jednego argumentu, lub po prostu chcieliśmy wiedzieć, jakiej funkcji szukaliśmy w sesji debugowania, musimy zrobić coś więcej z naszą obwoluta.

W pełni funkcjonalne rozwiązanie - pokonanie większości z tych problemów

Mamy dekoratora wraps z functools moduł w standardowej bibliotece!

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

To niefortunne, że wciąż jest jakiś plan, ale jest to mniej więcej tak proste, jak to tylko możliwe.

W Pythonie 3 również __qualname__ i __annotations__ przypisane domyślnie.

Więc teraz:

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

I teraz:

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

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

Wniosek

Widzimy to wraps sprawia, że ​​funkcja owijająca robi prawie wszystko, z wyjątkiem tego, że dokładnie mówi nam, co funkcja przyjmuje jako argumenty.

Istnieją inne moduły, które mogą próbować rozwiązać problem, ale rozwiązanie nie znajduje się jeszcze w standardowej bibliotece.


15
2018-03-20 09:48