3152 Stimmen

Wie kann man Funktionsdekoratoren erstellen und sie miteinander verketten?

Wie kann ich zwei Dekoratoren in Python erstellen, die folgendes tun?

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

...der zurückkehren sollte:

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

11voto

v4gil Punkte 733

Diese Frage wurde schon lange beantwortet, aber ich dachte, ich würde meine Decorator-Klasse mit Ihnen teilen, die das Schreiben neuer Dekoratoren 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 denke ich, dass dies das Verhalten der Dekoratoren sehr klar macht, aber es macht es auch einfach, neue Dekoratoren sehr prägnant zu definieren. Für das oben aufgeführte Beispiel könnte man 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 für komplexere Aufgaben verwenden, wie zum Beispiel einen Dekorator, der die Funktion automatisch rekursiv auf alle Argumente in einem Iterator anwendet:

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 nicht die list Typ in der Instanziierung des Dekorators, so dass in der abschließenden Druckanweisung die Methode auf die Liste selbst und nicht auf die Elemente der Liste angewendet wird.

9voto

nickleefly Punkte 3663
#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 den Dekorator auch in die Klasse

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

1 Stimmen

Der Grund, eine Klasse hier zu mögen, ist, dass es ein eindeutig verwandtes Verhalten gibt, mit zwei Instanzen. Sie können Ihre beiden Dekoratoren tatsächlich erhalten, indem Sie die konstruierten Klassen den gewünschten Namen zuweisen, anstatt die Parameter zu wiederholen. Dies ist bei einer Funktion schwieriger. Das Hinzufügen zu dem Beispiel würde zeigen, warum dies nicht nur redundant ist.

7voto

smarie Punkte 3528

Antwort von Paolo Bergantino hat den großen Vorteil, dass es nur die stdlib verwendet und für dieses einfache Beispiel funktioniert, bei dem es keine Tapezierer Argumente noch dekorierte Funktion Argumente.

Es hat jedoch 3 wesentliche Einschränkungen, wenn man allgemeinere Fälle angehen will:

  • wie bereits in mehreren Antworten erwähnt, können Sie den Code nicht einfach ändern, um optionale Dekorator-Argumente hinzufügen . Zum Beispiel die Erstellung einer makestyle(style='bold') Dekorateur ist nicht trivial.
  • Außerdem können Wrapper, die mit @functools.wraps die Signatur nicht erhalten Wenn also falsche Argumente angegeben werden, beginnen sie mit der Ausführung und können eine andere Art von Fehler auslösen als die üblichen TypeError .
  • schließlich ist es ziemlich schwierig, in Wrappern, die mit @functools.wraps a Zugriff auf ein Argument anhand seines Namens . In der Tat kann das Argument erscheinen in *args , in **kwargs oder gar nicht erscheinen (wenn sie fakultativ 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 makefun nutzt denselben Trick wie das berühmte decorator lib.

Auf diese Weise würden Sie einen Dekorator mit Argumenten erstellen, der wirklich signaturerhaltende Umhüllungen zurückgibt:

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

decopatch stellt Ihnen zwei weitere Entwicklungsstile zur Verfügung, die die verschiedenen Python-Konzepte je nach Ihren Vorlieben ein- oder ausblenden. 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 Dekorator wie erwartet funktioniert:

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

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

Bitte beachten Sie die Dokumentation für Details.

7voto

resigned Punkte 1002

Hier ist ein einfaches Beispiel für die Verkettung von Dekoratoren. Beachten Sie die letzte Zeile - sie zeigt, was unter der Haube 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 wie folgt aus:

17/6/2013
<b>17/6/2013</b>
6/17/2013
<b>6/17/2013</b>
<b>6/17/2013</b>

6voto

rabin utam Punkte 12108

Funktionen mit unterschiedlicher Anzahl von Argumenten dekorieren:

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

1 Stimmen

Dies könnte leicht noch vielseitiger gemacht werden, indem man auch Unterstützung für Schlüsselwortargumente über def wrapper(*args, **kwargs): y fn(*args, **kwargs) .

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X