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

67voto

Abhinav Gupta Punkte 4450

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

Ein Kursiv-Dekorator könnte wie folgt aussehen

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

Beachten Sie, dass eine Funktion innerhalb einer Funktion definiert ist. Im Grunde wird eine Funktion durch die neu definierte Funktion ersetzt. Ein Beispiel: Ich habe diese Klasse

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

Angenommen, ich möchte, dass beide Funktionen nach und vor ihrer Ausführung "---" ausgeben. Ich könnte ein print "---" vor und nach jeder print-Anweisung einfügen. Aber da ich mich nicht gerne wiederhole, werde ich einen Dekorator erstellen

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 also meine Klasse ändern in

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

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

Mehr über Dekorateure finden Sie unter http://www.ibm.com/developerworks/linux/library/l-cpdecor.html

0 Stimmen

Hinweis so elegant wie die von @Rune Kaagaard vorgeschlagenen Lambda-Funktionen

1 Stimmen

@Phoenix: Die self ist erforderlich, weil das Argument newFunction() definiert in addDashes() wurde speziell entwickelt, um eine Methode Dekorator und nicht ein allgemeiner Funktionsdekorator. Der self Argument stellt die Klasseninstanz dar und wird an Klassenmethoden weitergegeben, unabhängig davon, ob sie es verwenden oder nicht -- siehe den Abschnitt Dekorationsmethoden in der Antwort von @e-satis.

1 Stimmen

Drucken Sie bitte auch die Ausgabe aus.

44voto

martineau Punkte 110783

Usted könnte zwei separate Dekoratoren erstellen, die das tun, was Sie wollen, wie direkt unten dargestellt. Beachten Sie die Verwendung von *args, **kwargs in der Erklärung des wrapped() Funktion, die die dekorierte Funktion mit mehreren Argumenten unterstützt (was für das Beispiel nicht wirklich notwendig ist say() Funktion, wird aber aus Gründen der Allgemeinheit aufgenommen).

Aus ähnlichen Gründen ist die functools.wraps Dekorator wird verwendet, um die Meta-Attribute der umhüllten Funktion in die der dekorierten Funktion zu ändern. Dies macht Fehlermeldungen und eingebettete Funktionsdokumentation ( func.__doc__ ) die 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, gibt es eine Menge doppelten Code in diesen beiden Dekoratoren. Angesichts dieser Ähnlichkeit wäre es besser für Sie, stattdessen einen generischen zu machen, der eigentlich ein Dekorationsmittelfabrik -mit anderen Worten, eine Dekoratorfunktion, die andere Dekoratoren erzeugt. Auf diese Weise gäbe es weniger Code-Wiederholungen und die DRY Grundsatz zu befolgen.

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 lesbarer zu machen, können Sie den werkseitig erzeugten Dekoratoren einen aussagekräftigeren Namen geben:

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 sie sogar auf diese Weise kombinieren:

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>

Wirkungsgrad

Während die obigen Beispiele alle funktionieren, beinhaltet der erzeugte Code eine Menge Overhead in Form von fremden Funktionsaufrufen, wenn mehrere Dekoratoren gleichzeitig angewendet werden. Je nach der genauen Verwendung (die z.B. I/O-gebunden sein könnte) ist dies vielleicht nicht von Bedeutung.

Wenn die Geschwindigkeit der dekorierten Funktion wichtig ist, kann der Overhead auf einen einzigen zusätzlichen Funktionsaufruf beschränkt werden, indem man eine etwas andere Dekorator-Factory-Funktion schreibt, die das Hinzufügen aller Tags auf einmal implementiert, so dass sie Code erzeugen kann, der die zusätzlichen Funktionsaufrufe vermeidet, die durch die Verwendung separater Dekoratoren für jedes Tag entstehen.

Dies erfordert mehr Code im Dekorator selbst, aber dieser läuft nur, wenn er auf Funktionsdefinitionen angewendet wird, nicht später, wenn sie selbst aufgerufen werden. Dies gilt auch für die Erstellung von besser lesbaren Namen durch die Verwendung von lambda funktioniert wie zuvor beschrieben. Beispiel:

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>

22voto

qed Punkte 20934

Eine andere Art, das Gleiche 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 noch 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'

0 Stimmen

Benötigt functools.update_wrapper um zu erhalten sayhi.__name__ == "sayhi"

21voto

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

Sie möchten, dass die folgende Funktion aufgerufen wird:

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

Zur Rückkehr:

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

Einfache Lösung

Um dies am einfachsten zu tun, machen Sie Dekoratoren, die Lambdas (anonyme Funktionen) zurückgeben, die über die Funktion (Closures) schließen und sie aufrufen:

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

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

Verwenden Sie sie nun nach Belieben:

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

und jetzt:

>>> 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 sie zu finden, müssten wir in die Schließung jedes Lambdas eindringen, von denen das eine im anderen verborgen 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 ausschmücken wollen, die mehr als ein Argument annehmen, oder wenn wir einfach nur wissen wollen, welche Funktion wir in einer Debugging-Sitzung betrachten, müssen wir etwas mehr mit unserem Wrapper machen.

Umfassende Lösung, die die meisten dieser Probleme überwindet

Wir haben den Dekorateur wraps von der functools Modul 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 immer noch einige Standardformulierungen gibt, aber das ist so einfach, wie wir es machen können.

In Python 3 erhalten Sie außerdem __qualname__ y __annotations__ standardmäßig zugewiesen.

Also jetzt:

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

Und jetzt:

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

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

Schlussfolgerung

Wir sehen also, dass wraps lässt die Wrapping-Funktion fast alles tun, außer uns genau zu sagen, was die Funktion als Argumente nimmt.

Es gibt andere Module, die versuchen, das Problem zu lösen, aber die Lösung ist noch nicht in der Standardbibliothek enthalten.

14voto

Davoud Taghawi-Nejad Punkte 14966

Ein Dekorator nimmt die Funktionsdefinition und erstellt eine neue Funktion, die diese Funktion ausführt und das Ergebnis umwandelt.

@deco
def do():
    ...

ist gleichbedeutend mit:

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 mit

def do2(number):
    return chr(number)

do2 = deco(do2)

65 <=> 'a'

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

Um den Dekorator zu verstehen, ist es wichtig zu wissen, dass der Dekorator eine neue Funktion do erstellt, die eine innere Funktion ist, die die Funktion ausführt und das Ergebnis umwandelt.

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