Wie erstelle ich eine Kette von Funktionsdekorateuren?

2755

Wie kann ich in Python zwei Dekorateure erstellen, die Folgendes tun?

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

... was zurückkehren sollte:

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

Ich versuche nicht, HTMLdies in einer realen Anwendung zu tun - ich versuche nur zu verstehen, wie Dekorateure und Dekorationsketten funktionieren.

Imran
quelle

Antworten:

2926

Lesen Sie in der Dokumentation nach, wie Dekorateure arbeiten. Folgendes haben Sie gefragt:

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
quelle
261
Erwägen Sie die Verwendung von functools.wraps oder, noch besser, des Decorator-Moduls von PyPI : Sie bewahren bestimmte wichtige Metadaten auf (z. B. __name__und, was das Decorator-Paket betrifft , die Funktionssignatur).
Marius Gedminas
31
*argsund **kwargssollte in der Antwort hinzugefügt werden. Dekorierte Funktionen können Argumente haben und gehen verloren, wenn sie nicht angegeben werden.
Blusky
3
Obwohl diese Antwort den großen Vorteil hat, nur die stdlib zu verwenden, und für dieses einfache Beispiel funktioniert , in dem es keine Dekorationsargumente oder dekorierten Funktionsargumente gibt , gibt es drei Hauptbeschränkungen: (1) keine einfache Unterstützung für optionale Dekorationsargumente (2) nicht Signaturerhaltung (3) Keine einfache Möglichkeit, ein benanntes Argument aus *args, **kwargs. Eine einfache Möglichkeit, diese drei Probleme gleichzeitig zu lösen, ist die hierdecopatch erläuterte Verwendung . Sie können auch, wie bereits von Marius Gedminas erwähnt, verwenden, um die Punkte 2 und 3 zu lösen.decorator
smarie
4209

Wenn Sie keine langen Erklärungen haben, lesen Sie die Antwort von Paolo Bergantino .

Grundlagen des Dekorateurs

Pythons Funktionen sind Objekte

Um Dekoratoren zu verstehen, müssen Sie zunächst verstehen, dass Funktionen Objekte in Python sind. Dies hat wichtige Konsequenzen. Mal sehen, warum mit einem einfachen Beispiel:

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

Denken Sie daran. Wir werden in Kürze darauf zurückkommen.

Eine weitere interessante Eigenschaft von Python-Funktionen ist, dass sie in einer anderen Funktion definiert werden können!

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

Funktionsreferenzen

Okay, immer noch hier? Nun der lustige Teil ...

Sie haben gesehen, dass Funktionen Objekte sind. Daher Funktionen:

  • kann einer Variablen zugeordnet werden
  • kann in einer anderen Funktion definiert werden

Das heißt, eine Funktion kann eine returnandere Funktion haben .

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

Es gibt mehr!

Wenn Sie returneine Funktion können, können Sie eine als Parameter übergeben:

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!

Nun, Sie haben einfach alles, was Sie brauchen, um Dekorateure zu verstehen. Sie sehen, Dekoratoren sind "Wrapper", was bedeutet, dass Sie Code vor und nach der Funktion ausführen können, die sie dekorieren, ohne die Funktion selbst zu ändern.

Handgefertigte Dekorateure

So würden Sie es manuell machen:

# 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

Jetzt möchten Sie wahrscheinlich, dass jedes Mal, wenn Sie anrufen a_stand_alone_function, a_stand_alone_function_decoratedstattdessen aufgerufen wird. Das ist einfach, überschreiben Sie einfach a_stand_alone_functionmit der Funktion, die zurückgegeben wird von 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!

Dekorateure entmystifiziert

Das vorherige Beispiel mit der Decorator-Syntax:

@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

Ja, das ist alles, so einfach ist das. @decoratorist nur eine Abkürzung zu:

another_stand_alone_function = my_shiny_new_decorator(another_stand_alone_function)

Dekorateure sind nur eine pythonische Variante des Dekorationsmuster . In Python sind mehrere klassische Entwurfsmuster eingebettet, um die Entwicklung zu vereinfachen (z. B. Iteratoren).

Natürlich können Sie Dekorateure ansammeln:

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

Verwenden der Python Decorator-Syntax:

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

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

Die Reihenfolge, in der Sie die Dekorateure einstellen, ist wichtig:

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

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

Nun: um die Frage zu beantworten ...

Als Fazit können Sie leicht sehen, wie die Frage zu beantworten ist:

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

Sie können jetzt einfach glücklich gehen oder Ihr Gehirn ein bisschen mehr verbrennen und die fortgeschrittene Verwendung von Dekorateuren sehen.


Dekorateure auf die nächste Stufe bringen

Argumente an die dekorierte Funktion übergeben

# 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

Dekorationsmethoden

Eine nette Sache an Python ist, dass Methoden und Funktionen wirklich gleich sind. Der einzige Unterschied besteht darin, dass Methoden erwarten, dass ihr erstes Argument auf das aktuelle Objekt verweist ( self).

Das heißt, Sie können auf die gleiche Weise einen Dekorateur für Methoden erstellen! Denken Sie daran, Folgendes selfzu berücksichtigen:

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?

Wenn Sie einen Allzweckdekorateur erstellen - einen, den Sie auf jede Funktion oder Methode anwenden, unabhängig von ihren Argumenten -, verwenden Sie einfach *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?

Argumente an den Dekorateur weitergeben

Großartig, was würden Sie jetzt über die Weitergabe von Argumenten an den Dekorateur selbst sagen?

Dies kann etwas verdreht werden, da ein Dekorateur eine Funktion als Argument akzeptieren muss. Daher können Sie die Argumente der dekorierten Funktion nicht direkt an den Dekorator übergeben.

Bevor wir zur Lösung eilen, schreiben wir eine kleine Erinnerung:

# 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

Es ist genau das gleiche. " my_decorator" heißt. Wenn Sie @my_decoratoralso Python anweisen, die durch die Variable " my_decorator" gekennzeichnete Funktion "aufzurufen .

Das ist wichtig! Das Etikett, das Sie geben, kann direkt auf den Dekorateur verweisen - oder auch nicht .

Lass uns böse werden. ☺

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.

Kein Wunder hier.

Lassen Sie uns genau das Gleiche tun, aber alle lästigen Zwischenvariablen überspringen:

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.

Machen wir es noch kürzer :

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

Hey, hast du das gesehen? Wir haben einen Funktionsaufruf mit der @Syntax " " verwendet! :-)

Also zurück zu den Dekorateuren mit Argumenten. Wenn wir Funktionen verwenden können, um den Dekorator im laufenden Betrieb zu generieren, können wir dieser Funktion Argumente übergeben, oder?

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

Hier ist es: ein Dekorateur mit Argumenten. Argumente können als Variable festgelegt werden:

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

Wie Sie sehen, können Sie mit diesem Trick wie jede andere Funktion Argumente an den Dekorateur übergeben. Sie können sogar verwenden, *args, **kwargswenn Sie möchten. Aber denken Sie daran, Dekorateure werden nur einmal angerufen . Nur wenn Python das Skript importiert. Sie können die Argumente danach nicht dynamisch festlegen. Wenn Sie "x importieren", ist die Funktion bereits dekoriert , sodass Sie nichts ändern können.


Lassen Sie uns üben: Dekorieren eines Dekorateurs

Okay, als Bonus gebe ich Ihnen einen Ausschnitt, damit jeder Dekorateur generisch jedes Argument akzeptiert. Um Argumente zu akzeptieren, haben wir unseren Dekorator mit einer anderen Funktion erstellt.

Wir haben den Dekorateur eingewickelt.

Gibt es noch etwas, das wir kürzlich gesehen haben?

Oh ja, Dekorateure!

Lass uns Spaß haben und einen Dekorateur für die Dekorateure schreiben:

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

Es kann wie folgt verwendet werden:

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

Ich weiß, das letzte Mal, als Sie dieses Gefühl hatten, war es, nachdem Sie einem Mann zugehört hatten, der sagte: "Bevor Sie die Rekursion verstehen, müssen Sie zuerst die Rekursion verstehen." Aber jetzt, fühlst du dich nicht gut dabei, das zu meistern?


Best Practices: Dekorateure

  • Dekorateure wurden in Python 2.4 eingeführt. Stellen Sie daher sicher, dass Ihr Code auf> = 2.4 ausgeführt wird.
  • Dekorateure verlangsamen den Funktionsaufruf. Merk dir das.
  • Sie können die Dekoration einer Funktion nicht aufheben. (Es gibt Hacks zum Erstellen von Dekoratoren, die entfernt werden können, aber niemand verwendet sie.) Sobald eine Funktion dekoriert ist, wird sie für den gesamten Code dekoriert .
  • Dekorateure wickeln Funktionen ein, die das Debuggen erschweren können. (Dies wird besser von Python> = 2.5; siehe unten.)

Das functoolsModul wurde in Python 2.5 eingeführt. Es enthält die Funktionfunctools.wraps() , die den Namen, das Modul und die Dokumentzeichenfolge der dekorierten Funktion in ihren Wrapper kopiert.

(Lustige Tatsache: functools.wraps()ist ein Dekorateur! ☺)

# 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

Wie können die Dekorateure nützlich sein?

Nun die große Frage: Wofür kann ich Dekorateure verwenden?

Scheint cool und kraftvoll, aber ein praktisches Beispiel wäre großartig. Nun, es gibt 1000 Möglichkeiten. Klassische Anwendungen sind das Erweitern eines Funktionsverhaltens von einer externen Bibliothek (Sie können es nicht ändern) oder das Debuggen (Sie möchten es nicht ändern, weil es nur vorübergehend ist).

Sie können sie verwenden, um mehrere Funktionen auf DRY-Weise zu erweitern, wie zum Beispiel:

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

Das Gute an Dekorateuren ist natürlich, dass Sie sie sofort für fast alles verwenden können, ohne sie neu zu schreiben. TROCKEN, sagte ich:

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

Python bietet sich mehrere Dekorateure: property, staticmethodusw.

  • Django verwendet Dekoratoren, um das Caching zu verwalten und Berechtigungen anzuzeigen.
  • Verdreht, um asynchrone Inlining-Funktionsaufrufe zu fälschen.

Dies ist wirklich ein großer Spielplatz.

E-Satis
quelle
15
"Sie können eine Funktion nicht aufheben." - Während dies normalerweise zutrifft, ist es möglich, durch einen Dekorateur (dh über sein __closure__Attribut) in den Verschluss in der Funktionsrückgabe zu gelangen, um die ursprüngliche nicht dekorierte Funktion herauszuziehen. In dieser Antwort wird eine Beispielverwendung dokumentiert , die beschreibt, wie es möglich ist, eine Dekorationsfunktion unter bestimmten Umständen auf einer niedrigeren Ebene zu injizieren.
Metatoaster
8
Dies ist zwar eine großartige Antwort, aber ich denke, dass sie in gewisser Weise etwas irreführend ist. Die @decoratorSyntax von Python wird wahrscheinlich am häufigsten verwendet, um eine Funktion durch einen Wrapper-Abschluss zu ersetzen (wie in der Antwort beschrieben). Es kann aber auch die Funktion durch etwas anderes ersetzen. Die eingebaute property, classmethodund staticmethodDekoratore ersetzen die Funktion mit einem Deskriptor, zum Beispiel. Ein Dekorateur kann auch etwas mit einer Funktion tun, z. B. einen Verweis darauf in einer Registrierung speichern und ihn dann unverändert ohne Wrapper zurückgeben.
Blckknght
3
Die Tatsache, dass "Funktionen Objekte sind", obwohl sie in Python völlig zutreffen, ist ein wenig irreführend. Das Speichern von Funktionen in Variablen, das Übergeben als Argumente und das Zurückgeben als Ergebnisse sind alle möglich, ohne dass Funktionen tatsächlich Objekte sind, und es gibt verschiedene Sprachen, die erstklassige Funktionen, aber keine Objekte haben.
00dani
1
Dies ist eine epische Antwort genau dort ... Vielen Dank! Wie kommt es jedoch, dass Standardargumente für eine Funktion im Wrapper des Dekorateurs nicht als args / kwargs angezeigt werden?
Naz
Scrollte den ganzen Weg zurück zum Anfang dieser Antwort, um zu stimmen, weil das "Wie können die Dekorateure nützlich sein?" Abschnitt war so hilfreich.
Noumenon
145

Alternativ können Sie eine Factory-Funktion schreiben, die einen Dekorator zurückgibt, der den Rückgabewert der dekorierten Funktion in ein Tag einschließt, das an die Factory-Funktion übergeben wird. Zum Beispiel:

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

So können Sie schreiben:

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

oder

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

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

Persönlich hätte ich den Dekorateur etwas anders geschrieben:

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

was ergeben würde:

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

Vergessen Sie nicht die Konstruktion, für die die Dekoratorsyntax eine Abkürzung ist:

say = wrap_in_tag('b')(wrap_in_tag('i')(say)))
Trevor
quelle
5
Meiner Meinung nach ist es besser, mehr als einen Dekorateur so weit wie möglich zu meiden. Wenn ich eine Factory-Funktion schreiben müsste, würde ich sie wie def wrap_in_tag(*kwargs)damals mit * kwargs codieren@wrap_in_tag('b','i')
guneysus
120

Es sieht so aus, als hätten Ihnen die anderen Leute bereits gesagt, wie Sie das Problem lösen können. Ich hoffe, das hilft Ihnen zu verstehen, was Dekorateure sind.

Dekorateure sind nur syntaktischer Zucker.

Diese

@decorator
def func():
    ...

erweitert sich auf

def func():
    ...
func = decorator(func)
Unbekannt
quelle
3
Das ist so elegant, einfach, leicht zu verstehen. 10000 positive Stimmen für Sie, Sir Ockham.
Eric
2
Tolle und einfache Antwort. Ich möchte hinzufügen, dass bei der Verwendung @decorator()(anstelle von @decorator) syntaktischer Zucker für func = decorator()(func). Dies ist auch gängige Praxis, wenn Sie Dekorateure "on the fly" generieren müssen
Omer Dagan
64

Und natürlich können Sie Lambdas auch von einer Dekorationsfunktion zurückgeben:

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
quelle
12
Und noch einen Schritt weiter:makebold = lambda f : lambda "<b>" + f() + "</b>"
Robᵩ
13
@ Robᵩ: Um syntaktisch korrekt zu sein:makebold = lambda f: lambda: "<b>" + f() + "</b>"
Martineau
11
Spät zur Party, aber ich würde wirklich vorschlagenmakebold = lambda f: lambda *a, **k: "<b>" + f(*a, **k) + "</b>"
siehe
Dies muss functools.wraps, um die Dokumentzeichenfolge / Signatur / den Namen vonsay
Eric
Nun, was zählt, ist, ob es in Ihrer Antwort erwähnt wird. Wenn ich @wrapsirgendwo anders auf dieser Seite bin, hilft es mir nicht, wenn ich drucke help(say)und "Hilfe zur Funktion <lambda>" anstelle von "Hilfe zur Funktion sagen" erhalte .
Eric
61

Python-Dekoratoren fügen einer anderen Funktion zusätzliche Funktionen hinzu

Ein kursiver Dekorateur könnte so sein

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

Beachten Sie, dass eine Funktion innerhalb einer Funktion definiert ist. Grundsätzlich wird eine Funktion durch die neu definierte ersetzt. Zum Beispiel habe ich diese Klasse

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

Sagen wir nun, ich möchte, dass beide Funktionen "---" drucken, nachdem und bevor sie fertig sind. Ich könnte vor und nach jeder Druckanweisung ein Druck "---" hinzufügen. Aber weil ich mich nicht gerne wiederhole, werde ich einen Dekorateur machen

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

Jetzt kann ich meine Klasse ändern

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

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

Weitere Informationen zu Dekorateuren finden Sie unter http://www.ibm.com/developerworks/linux/library/l-cpdecor.html

Abhinav Gupta
quelle
Beachten Sie so elegant wie die Lambda-Funktionen von @Rune Kaagaard
rds
1
@Phoenix: Das selfArgument wird benötigt, da das newFunction()definierte in addDashes()speziell als Methodendekorateur und nicht als allgemeiner Funktionsdekorator konzipiert wurde. Das selfArgument stellt die Klasseninstanz dar und wird an Klassenmethoden übergeben, unabhängig davon, ob sie diese verwenden oder nicht - siehe Abschnitt Dekorationsmethoden in der Antwort von @ e-satis.
Martineau
1
Drucken Sie bitte auch die Ausgabe aus.
user1767754
Vermisstfunctools.wraps
Eric
39

Sie können zwei separate Dekorateure erstellen, die das tun, was Sie möchten, wie direkt unten dargestellt. Beachten Sie die Verwendung von *args, **kwargsin der Deklaration der wrapped()Funktion, die die dekorierte Funktion mit mehreren Argumenten unterstützt (was für die Beispielfunktion nicht wirklich erforderlich ist say(), aber der Allgemeinheit halber enthalten ist).

Aus ähnlichen Gründen wird der functools.wrapsDekorator verwendet, um die Metaattribute der umschlossenen Funktion in die der zu dekorierenden zu ändern. Dies macht Fehlermeldungen und eingebettete Funktionsdokumentation ( func.__doc__) zu denen der dekorierten Funktion anstelle von 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>

Verfeinerungen

Wie Sie sehen können, enthält diese beiden Dekorateure viel doppelten Code. Angesichts dieser Ähnlichkeit wäre es für Sie besser, stattdessen eine generische zu erstellen, die eigentlich eine Dekorationsfabrik war - mit anderen Worten, eine Dekorationsfunktion, die andere Dekorateure herstellt. Auf diese Weise würde weniger Code wiederholt - und das DRY- Prinzip könnte eingehalten werden.

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>

Um den Code besser lesbar zu machen, können Sie den werkseitig generierten Dekorateuren einen aussagekräftigeren Namen zuweisen:

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>

oder kombinieren Sie sie sogar so:

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>

Effizienz

Während die obigen Beispiele alle Arbeiten ausführen, ist der generierte Code mit einem erheblichen Aufwand in Form von externen Funktionsaufrufen verbunden, wenn mehrere Dekorateure gleichzeitig angewendet werden. Dies spielt möglicherweise keine Rolle, abhängig von der genauen Verwendung (die beispielsweise E / A-gebunden sein kann).

Wenn die Geschwindigkeit der dekorierten Funktion wichtig ist, kann der Overhead auf einen einzelnen zusätzlichen Funktionsaufruf beschränkt werden, indem eine etwas andere Dekorations-Factory-Funktion geschrieben wird, die das gleichzeitige Hinzufügen aller Tags implementiert, sodass Code generiert werden kann, der die zusätzlichen Funktionsaufrufe vermeidet durch Verwendung separater Dekoratoren für jedes Tag.

Dies erfordert mehr Code im Dekorator selbst, wird jedoch nur ausgeführt, wenn er auf Funktionsdefinitionen angewendet wird, nicht später, wenn sie selbst aufgerufen werden. Dies gilt auch, wenn Sie besser lesbare Namen mithilfe der lambdazuvor dargestellten Funktionen erstellen . Stichprobe:

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
quelle
2
Gegenstimme für die Bezugnahme auf DRY :-)
nitin3685
Vielen Dank für die Erklärung "@wraps (fun)" :)
walknotes
20

Eine andere Möglichkeit, dasselbe zu tun:

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'

Oder flexibler:

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
quelle
Bedürfnisse, functools.update_wrapperum zu haltensayhi.__name__ == "sayhi"
Eric
19

Wie kann ich in Python zwei Dekorateure erstellen, die Folgendes tun?

Wenn Sie aufgerufen werden, möchten Sie die folgende Funktion:

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

Zurückgeben:

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

Einfache Lösung

Um dies am einfachsten zu tun, erstellen Sie Dekorateure, die Lambdas (anonyme Funktionen) zurückgeben, die über der Funktion (Verschlüsse) schließen, und nennen Sie sie:

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

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

Verwenden Sie sie nun wie gewünscht:

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

und nun:

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

Probleme mit der einfachen Lösung

Aber wir scheinen die ursprüngliche Funktion fast verloren zu haben.

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

Um es zu finden, müssten wir in die Schließung jedes Lambdas graben, von denen eines im anderen begraben ist:

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

Wenn wir also diese Funktion dokumentieren oder Funktionen dekorieren möchten, die mehr als ein Argument enthalten, oder nur wissen möchten, welche Funktion wir in einer Debugging-Sitzung betrachtet haben, müssen wir etwas mehr mit unserer tun Verpackung.

Voll funktionsfähige Lösung - Überwindung der meisten dieser Probleme

Wir haben den Dekorateur wrapsaus dem functoolsModul in der Standardbibliothek!

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

Es ist bedauerlich, dass es noch einige Boilerplate gibt, aber das ist ungefähr so ​​einfach, wie wir es machen können.

In Python 3 erhalten Sie auch standardmäßig __qualname__und __annotations__zugewiesen.

Also jetzt:

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

Und nun:

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

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

Fazit

Wir sehen also, wrapsdass die Wrapping-Funktion fast alles macht, außer uns genau zu sagen, was die Funktion als Argumente verwendet.

Es gibt andere Module, die möglicherweise versuchen, das Problem zu lösen, aber die Lösung befindet sich noch nicht in der Standardbibliothek.

Aaron Hall
quelle
14

Um den Dekorateur auf einfache Weise zu erklären:

Mit:

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

Wann tun:

func(*args, **kwargs)

Das tust du wirklich:

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

Ein Dekorateur übernimmt die Funktionsdefinition und erstellt eine neue Funktion, die diese Funktion ausführt und das Ergebnis transformiert.

@deco
def do():
    ...

ist äquivalent zu:

do = deco(do)

Beispiel:

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

Diese

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

ist gleichbedeutend damit

def do2(number):
    return chr(number)

do2 = deco(do2)

65 <=> 'a'

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

Um den Dekorateur zu verstehen, ist es wichtig zu beachten, dass der Dekorateur eine neue Funktion do erstellt hat, die inner ist und die Funktion ausführt und das Ergebnis transformiert.

Davoud Taghawi-Nejad
quelle
Sollte nicht die Ausgabe von print(do(65))und print(do2(65))sein Aund 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()

Sie können auch Dekorateur in Klasse schreiben

#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
quelle
1
Der Grund, eine Klasse hier zu mögen, ist, dass es ein eindeutig verwandtes Verhalten mit zwei Instanzen gibt. Sie können Ihre beiden Dekorateure tatsächlich erhalten, indem Sie die erstellten Klassen den gewünschten Namen zuweisen, anstatt die Parameter erneut zu wiederholen. Dies ist mit einer Funktion schwieriger zu tun. Das Hinzufügen zum Beispiel würde darauf hinweisen, warum dies nicht nur redundant ist.
Jon Jay Obermark
8

Diese Antwort wurde schon lange beantwortet, aber ich dachte, ich würde meine Dekorationsklasse teilen, was das Schreiben neuer Dekorateure einfach und kompakt macht.

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)

Zum einen macht dies das Verhalten von Dekorateuren sehr deutlich, aber es macht es auch einfach, neue Dekorateure sehr präzise zu definieren. Für das oben aufgeführte Beispiel können Sie es dann wie folgt lösen:

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"

Sie können es auch verwenden, um komplexere Aufgaben auszuführen, z. B. einen Dekorator, bei dem die Funktion automatisch rekursiv auf alle Argumente in einem Iterator angewendet wird:

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]))

Welche Drucke:

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

Beachten Sie, dass dieses Beispiel den listTyp nicht in die Instanziierung des Dekorateurs einbezog. In der endgültigen Druckanweisung wird die Methode also auf die Liste selbst angewendet, nicht auf die Elemente der Liste.

v4gil
quelle
7

Hier ist ein einfaches Beispiel für die Verkettung von Dekorateuren. Beachten Sie die letzte Zeile - sie zeigt, was unter der Decke vor sich geht.

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

Die Ausgabe sieht aus wie:

17/6/2013
<b>17/6/2013</b>
6/17/2013
<b>6/17/2013</b>
<b>6/17/2013</b>
resigniert
quelle
6

Apropos Zählerbeispiel - wie oben angegeben, wird der Zähler von allen Funktionen geteilt, die den Dekorator verwenden:

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

Auf diese Weise kann Ihr Dekorator für verschiedene Funktionen wiederverwendet werden (oder verwendet werden, um dieselbe Funktion mehrmals zu dekorieren :) func_counter1 = counter(func); func_counter2 = counter(func), und die Zählervariable bleibt für jede privat.

marqueed
quelle
6

Dekorieren Sie Funktionen mit unterschiedlicher Anzahl von Argumenten:

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!')

Ergebnis:

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
quelle
1
Dies könnte leicht noch vielseitiger gestaltet werden, indem auch Keyword-Argumente über def wrapper(*args, **kwargs):und unterstützt werden fn(*args, **kwargs).
Martineau
5

Die Antwort von Paolo Bergantino hat den großen Vorteil, nur die stdlib zu verwenden, und funktioniert für dieses einfache Beispiel, bei dem es weder Dekorationsargumente noch dekorierte Funktionsargumente gibt .

Es gibt jedoch drei Hauptbeschränkungen, wenn Sie allgemeinere Fälle angehen möchten:

  • Wie bereits in mehreren Antworten erwähnt, können Sie den Code nicht einfach ändern, um optionale Dekorationsargumente hinzuzufügen . Zum Beispiel ist das Erstellen eines makestyle(style='bold')Dekorateurs nicht trivial.
  • Außerdem behalten Wrapper, die mit erstellt wurden, @functools.wraps die Signatur nicht bei . Wenn also schlechte Argumente angegeben werden, werden sie ausgeführt und können einen anderen Fehler als den üblichen auslösen TypeError.
  • Schließlich ist es in Wrappern, die mit erstellt wurden, ziemlich schwierig @functools.wraps, auf ein Argument zuzugreifen, das auf seinem Namen basiert . In der Tat kann das Argument in *args, in **kwargsoder überhaupt nicht erscheinen (wenn es optional ist).

Ich schrieb decopatch, um das erste Problem zu lösen, und schrieb makefun.wraps, um die beiden anderen zu lösen. Beachten Sie, dass makefunder gleiche Trick wie die berühmte decoratorBibliothek verwendet wird.

Auf diese Weise würden Sie einen Dekorateur mit Argumenten erstellen und wirklich signaturerhaltende Wrapper zurückgeben:

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

decopatchbietet Ihnen zwei weitere Entwicklungsstile, mit denen die verschiedenen Python-Konzepte je nach Ihren Vorlieben ausgeblendet oder angezeigt werden. Der kompakteste Stil ist der folgende:

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

In beiden Fällen können Sie überprüfen, ob der Dekorateur wie erwartet funktioniert:

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

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

Weitere Informationen finden Sie in der Dokumentation .

smarie
quelle