Wie erstelle ich einen Python-Dekorator, der mit oder ohne Parameter verwendet werden kann?

84

Ich möchte einen Python-Dekorator erstellen, der entweder mit Parametern verwendet werden kann:

@redirect_output("somewhere.log")
def foo():
    ....

oder ohne sie (zum Beispiel um die Ausgabe standardmäßig auf stderr umzuleiten):

@redirect_output
def foo():
    ....

Ist das überhaupt möglich?

Beachten Sie, dass ich nicht nach einer anderen Lösung für das Problem der Umleitung von Ausgaben suche, sondern nur ein Beispiel für die Syntax, die ich erreichen möchte.

Elifiner
quelle
Das Standard-Aussehen @redirect_outputist bemerkenswert wenig aussagekräftig. Ich würde vorschlagen, dass es eine schlechte Idee ist. Verwenden Sie die erste Form und vereinfachen Sie Ihr Leben sehr.
S.Lott
Interessante Frage - bis ich sie gesehen und die Dokumentation durchgesehen habe, hätte ich angenommen, dass @f dasselbe ist wie @f (), und ich denke immer noch, dass es so sein sollte, um ehrlich zu sein (alle angegebenen Argumente würden nur angegangen weiter zum Funktionsargument)
Rog

Antworten:

61

Ich weiß, dass diese Frage alt ist, aber einige der Kommentare sind neu, und obwohl alle praktikablen Lösungen im Wesentlichen gleich sind, sind die meisten von ihnen nicht sehr sauber oder leicht zu lesen.

Wie die Antwort von thobe sagt, besteht die einzige Möglichkeit, beide Fälle zu behandeln, darin, nach beiden Szenarien zu suchen. Der einfachste Weg ist einfach zu überprüfen, ob es ein einzelnes Argument gibt und es callabe ist (HINWEIS: Zusätzliche Überprüfungen sind erforderlich, wenn Ihr Dekorateur nur 1 Argument akzeptiert und es sich zufällig um ein aufrufbares Objekt handelt):

def decorator(*args, **kwargs):
    if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):
        # called as @decorator
    else:
        # called as @decorator(*args, **kwargs)

Im ersten Fall tun Sie das, was ein normaler Dekorateur tut, und geben eine modifizierte oder umschlossene Version der übergebenen Funktion zurück.

Im zweiten Fall geben Sie einen 'neuen' Dekorateur zurück, der die mit * args, ** kwargs übergebenen Informationen irgendwie verwendet.

Das ist in Ordnung und alles, aber es für jeden Dekorateur, den Sie machen, aufschreiben zu müssen, kann ziemlich nervig und nicht so sauber sein. Stattdessen wäre es schön, unsere Dekorateure automatisch modifizieren zu können, ohne sie neu schreiben zu müssen ... aber dafür sind Dekorateure da!

Mit dem folgenden Dekorateur können wir unsere Dekorateure so dekrieren, dass sie mit oder ohne Argumente verwendet werden können:

def doublewrap(f):
    '''
    a decorator decorator, allowing the decorator to be used as:
    @decorator(with, arguments, and=kwargs)
    or
    @decorator
    '''
    @wraps(f)
    def new_dec(*args, **kwargs):
        if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):
            # actual decorated function
            return f(args[0])
        else:
            # decorator arguments
            return lambda realf: f(realf, *args, **kwargs)

    return new_dec

Jetzt können wir unsere Dekorateure mit @doublewrap dekorieren und sie werden mit und ohne Argumente mit einer Einschränkung arbeiten:

Ich habe oben erwähnt, sollte aber hier wiederholen, dass die Prüfung in diesem Dekorateur eine Annahme über die Argumente macht, die ein Dekorateur erhalten kann (nämlich dass er kein einziges, aufrufbares Argument empfangen kann). Da wir es jetzt auf jeden Generator anwendbar machen, muss es berücksichtigt oder modifiziert werden, wenn es widersprochen wird.

Das Folgende demonstriert seine Verwendung:

def test_doublewrap():
    from util import doublewrap
    from functools import wraps    

    @doublewrap
    def mult(f, factor=2):
        '''multiply a function's return value'''
        @wraps(f)
        def wrap(*args, **kwargs):
            return factor*f(*args,**kwargs)
        return wrap

    # try normal
    @mult
    def f(x, y):
        return x + y

    # try args
    @mult(3)
    def f2(x, y):
        return x*y

    # try kwargs
    @mult(factor=5)
    def f3(x, y):
        return x - y

    assert f(2,3) == 10
    assert f2(2,5) == 30
    assert f3(8,1) == 5*7
bj0
quelle
31

Die Verwendung von Schlüsselwortargumenten mit Standardwerten (wie von kquinn vorgeschlagen) ist eine gute Idee, erfordert jedoch die Angabe der Klammer:

@redirect_output()
def foo():
    ...

Wenn Sie eine Version wünschen, die ohne die Klammer auf dem Dekorator funktioniert, müssen Sie beide Szenarien in Ihrem Dekorationscode berücksichtigen.

Wenn Sie Python 3.0 verwenden, können Sie hierfür nur Argumente mit Schlüsselwörtern verwenden:

def redirect_output(fn=None,*,destination=None):
  destination = sys.stderr if destination is None else destination
  def wrapper(*args, **kwargs):
    ... # your code here
  if fn is None:
    def decorator(fn):
      return functools.update_wrapper(wrapper, fn)
    return decorator
  else:
    return functools.update_wrapper(wrapper, fn)

In Python 2.x kann dies mit varargs Tricks emuliert werden:

def redirected_output(*fn,**options):
  destination = options.pop('destination', sys.stderr)
  if options:
    raise TypeError("unsupported keyword arguments: %s" % 
                    ",".join(options.keys()))
  def wrapper(*args, **kwargs):
    ... # your code here
  if fn:
    return functools.update_wrapper(wrapper, fn[0])
  else:
    def decorator(fn):
      return functools.update_wrapper(wrapper, fn)
    return decorator

Mit jeder dieser Versionen können Sie Code wie folgt schreiben:

@redirected_output
def foo():
    ...

@redirected_output(destination="somewhere.log")
def bar():
    ...
thobe
quelle
1
Was steckst du ein your code here? Wie nennt man die Funktion, die dekoriert ist? fn(*args, **kwargs)funktioniert nicht
Lum
Ich denke, es gibt eine viel einfachere Antwort, erstellen Sie eine Klasse, die der Dekorateur mit optionalen Argumenten. Erstellen Sie eine andere Funktion mit denselben Argumenten mit Standardwerten und geben Sie eine neue Instanz der Decorator-Klassen zurück. sollte ungefähr so ​​aussehen: def f(a = 5): return MyDecorator( a = a) und es class MyDecorator( object ): def __init__( self, a = 5 ): .... tut mir leid, dass es schwer ist, es in einen Kommentar zu schreiben, aber ich hoffe, das ist einfach genug zu verstehen
Omer Ben Haim
15

Ich weiß, dass dies eine alte Frage ist, aber ich mag keine der vorgeschlagenen Techniken, deshalb wollte ich eine andere Methode hinzufügen. Ich habe gesehen, dass Django eine wirklich saubere Methode in ihrem login_requiredDekorateur verwendetdjango.contrib.auth.decorators . Wie Sie in den Dokumenten des Dekorateurs sehen können , kann es allein als @login_requiredoder mit Argumenten verwendet werden @login_required(redirect_field_name='my_redirect_field').

Die Art und Weise, wie sie es tun, ist ganz einfach. Sie fügen vor den Dekorationsargumenten ein kwarg( function=None) hinzu. Wenn der Dekorator alleine verwendet wird, functionist dies die eigentliche Funktion, die er dekoriert, während dies der Fall ist, wenn er mit Argumenten aufgerufen functionwird None.

Beispiel:

from functools import wraps

def custom_decorator(function=None, some_arg=None, some_other_arg=None):
    def actual_decorator(f):
        @wraps(f)
        def wrapper(*args, **kwargs):
            # Do stuff with args here...
            if some_arg:
                print(some_arg)
            if some_other_arg:
                print(some_other_arg)
            return f(*args, **kwargs)
        return wrapper
    if function:
        return actual_decorator(function)
    return actual_decorator

@custom_decorator
def test1():
    print('test1')

>>> test1()
test1

@custom_decorator(some_arg='hello')
def test2():
    print('test2')

>>> test2()
hello
test2

@custom_decorator(some_arg='hello', some_other_arg='world')
def test3():
    print('test3')

>>> test3()
hello
world
test3

Ich finde diesen Ansatz, den Django verwendet, eleganter und leichter zu verstehen als alle anderen hier vorgeschlagenen Techniken.

dgel
quelle
Ja, ich mag diese Methode. Zu beachten ist, dass Sie haben kwargs verwenden beim Aufruf der Dekorateur ansonsten wird die erste Positions arg zugewiesen wird functionund dann die Dinge zu brechen , weil die Dekorateur versucht so zu nennen ersten Positions arg , als ob es Ihr dekoriert Funktion waren.
Dustin Wyatt
12

Sie müssen beide Fälle erkennen, z. B. anhand des Typs des ersten Arguments, und dementsprechend entweder den Wrapper (bei Verwendung ohne Parameter) oder einen Dekorator (bei Verwendung mit Argumenten) zurückgeben.

from functools import wraps
import inspect

def redirect_output(fn_or_output):
    def decorator(fn):
        @wraps(fn)
        def wrapper(*args, **args):
            # Redirect output
            try:
                return fn(*args, **args)
            finally:
                # Restore output
        return wrapper

    if inspect.isfunction(fn_or_output):
        # Called with no parameter
        return decorator(fn_or_output)
    else:
        # Called with a parameter
        return decorator

Wenn die @redirect_output("output.log")Syntax verwendet wird, redirect_outputwird sie mit einem einzelnen Argument aufgerufen "output.log"und muss einen Dekorateur zurückgeben, der die zu dekorierende Funktion als Argument akzeptiert. Bei Verwendung als @redirect_outputwird es direkt mit der Funktion aufgerufen, die als Argument dekoriert werden soll.

Oder mit anderen Worten: die @ Syntax muss ein Ausdruck folgen, dessen Ergebnis eine Funktion ist, die eine zu dekorierende Funktion als einziges Argument akzeptiert und die dekorierte Funktion zurückgibt. Der Ausdruck selbst kann ein Funktionsaufruf sein, was bei der Fall ist @redirect_output("output.log"). Gewunden, aber wahr :-)

Remy Blank
quelle
9

Einige Antworten hier sprechen Ihr Problem bereits gut an. In Bezug auf den Stil ziehe ich es jedoch vor, diese Dekorationsproblematik mithilfe von zu lösen functools.partial, wie in David Beazleys Python Cookbook 3 vorgeschlagen :

from functools import partial, wraps

def decorator(func=None, foo='spam'):
    if func is None:
         return partial(decorator, foo=foo)

    @wraps(func)
    def wrapper(*args, **kwargs):
        # do something with `func` and `foo`, if you're so inclined
        pass

    return wrapper

Während ja, können Sie einfach tun

@decorator()
def f(*args, **kwargs):
    pass

Ohne funky Workarounds finde ich es seltsam und ich mag die Möglichkeit, einfach mit zu dekorieren @decorator.

Was das sekundäre Missionsziel betrifft, wird in diesem Beitrag zum Stapelüberlauf die Umleitung der Ausgabe einer Funktion behandelt .


Wenn Sie tiefer eintauchen möchten, lesen Sie Kapitel 9 (Metaprogrammierung) in Python Cookbook 3 , das frei verfügbar ist und online gelesen werden kann .

Ein Teil dieses Materials wird in Beazleys großartigem YouTube-Video Python 3 Metaprogramming live (und noch mehr!) Vorgeführt .

Viel Spaß beim Codieren :)

Henrywallace
quelle
8

Ein Python-Dekorateur wird grundlegend anders aufgerufen, je nachdem, ob Sie ihm Argumente geben oder nicht. Die Dekoration ist eigentlich nur ein (syntaktisch eingeschränkter) Ausdruck.

In Ihrem ersten Beispiel:

@redirect_output("somewhere.log")
def foo():
    ....

Die Funktion redirect_outputwird mit dem angegebenen Argument aufgerufen, von dem erwartet wird, dass es eine Dekorationsfunktion zurückgibt, die selbst fooals Argument aufgerufen wird , und von der (schließlich!) erwartet wird, dass sie die endgültige dekorierte Funktion zurückgibt.

Der entsprechende Code sieht folgendermaßen aus:

def foo():
    ....
d = redirect_output("somewhere.log")
foo = d(foo)

Der entsprechende Code für Ihr zweites Beispiel sieht folgendermaßen aus:

def foo():
    ....
d = redirect_output
foo = d(foo)

So Sie können tun , was Sie aber nicht in einem völlig nahtlos mögen:

import types
def redirect_output(arg):
    def decorator(file, f):
        def df(*args, **kwargs):
            print 'redirecting to ', file
            return f(*args, **kwargs)
        return df
    if type(arg) is types.FunctionType:
        return decorator(sys.stderr, arg)
    return lambda f: decorator(arg, f)

Dies sollte in Ordnung sein, es sei denn, Sie möchten eine Funktion als Argument für Ihren Dekorateur verwenden. In diesem Fall geht der Dekorateur fälschlicherweise davon aus, dass er keine Argumente enthält. Es schlägt auch fehl, wenn diese Dekoration auf eine andere Dekoration angewendet wird, die keinen Funktionstyp zurückgibt.

Eine alternative Methode besteht lediglich darin, dass die Dekorationsfunktion immer aufgerufen wird, auch wenn sie keine Argumente enthält. In diesem Fall würde Ihr zweites Beispiel folgendermaßen aussehen:

@redirect_output()
def foo():
    ....

Der Dekorationsfunktionscode würde folgendermaßen aussehen:

def redirect_output(file = sys.stderr):
    def decorator(file, f):
        def df(*args, **kwargs):
            print 'redirecting to ', file
            return f(*args, **kwargs)
        return df
    return lambda f: decorator(file, f)
Rog
quelle
2

Tatsächlich kann der Vorbehaltsfall in der Lösung von @ bj0 leicht überprüft werden:

def meta_wrap(decor):
    @functools.wraps(decor)
    def new_decor(*args, **kwargs):
        if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):
            # this is the double-decorated f. 
            # Its first argument should not be a callable
            doubled_f = decor(args[0])
            @functools.wraps(doubled_f)
            def checked_doubled_f(*f_args, **f_kwargs):
                if callable(f_args[0]):
                    raise ValueError('meta_wrap failure: '
                                'first positional argument cannot be callable.')
                return doubled_f(*f_args, **f_kwargs)
            return checked_doubled_f 
        else:
            # decorator arguments
            return lambda real_f: decor(real_f, *args, **kwargs)

    return new_decor

Hier sind einige Testfälle für diese ausfallsichere Version von meta_wrap.

    @meta_wrap
    def baddecor(f, caller=lambda x: -1*x):
        @functools.wraps(f)
        def _f(*args, **kwargs):
            return caller(f(args[0]))
        return _f

    @baddecor  # used without arg: no problem
    def f_call1(x):
        return x + 1
    assert f_call1(5) == -6

    @baddecor(lambda x : 2*x) # bad case
    def f_call2(x):
        return x + 1
    f_call2(5)  # raises ValueError

    # explicit keyword: no problem
    @baddecor(caller=lambda x : 100*x)
    def f_call3(x):
        return x + 1
    assert f_call3(5) == 600
Ainz Titor
quelle
1
Vielen Dank. Das ist hilfreich!
Pragy Agarwal
0

Um eine vollständigere Antwort als die oben genannten zu geben:

"Gibt es eine Möglichkeit, einen Dekorateur zu bauen, der sowohl mit als auch ohne Argumente verwendet werden kann?"

Nein, es gibt keinen generischen Weg, da derzeit in der Python-Sprache etwas fehlt, um die beiden unterschiedlichen Anwendungsfälle zu erkennen.

Jedoch Ja , wie bereits von anderen Antworten wie darauf hingewiesen , bj0s , gibt es eine klobig Abhilfe , die die Art und Wert des ersten Positions Arguments zu überprüfen ist , empfängt (und zu überprüfen , ob keine andere Argumente Nicht-Standardwert). Wenn Sie garantiert sind, dass Benutzer nie sicher einen Aufruf als erstes Argument Ihres Dekorateurs übergeben, können Sie diese Problemumgehung verwenden. Beachten Sie, dass dies auch für Klassendekorateure gilt (ersetzen Sie callable durch class im vorherigen Satz).

Um sicherzugehen, habe ich da draußen einiges recherchiert und sogar eine Bibliothek mit dem Namen implementiert decopatch, die eine Kombination aller oben genannten Strategien (und viele weitere, einschließlich Selbstbeobachtung) verwendet, um "was auch immer die intelligenteste Problemumgehung ist" durchzuführen auf Ihre Bedürfnisse.

Aber ehrlich gesagt wäre es das Beste, hier keine Bibliothek zu benötigen und diese Funktion direkt aus der Python-Sprache zu erhalten. Wenn Sie wie ich der Meinung sind, dass es schade ist, dass die Python-Sprache bis heute keine ordentliche Antwort auf diese Frage liefern kann, zögern Sie nicht, diese Idee im Python-Bugtracker zu unterstützen : https: //bugs.python .org / issue36553 !

Vielen Dank für Ihre Hilfe, Python zu einer besseren Sprache zu machen :)

smarie
quelle
0

Dies macht den Job ohne viel Aufhebens:

from functools import wraps

def memoize(fn=None, hours=48.0):
  def deco(fn):
    @wraps(fn)
    def wrapper(*args, **kwargs):
      return fn(*args, **kwargs)
    return wrapper

  if callable(fn): return deco(fn)
  return deco
j4hangir
quelle
-1

Haben Sie Keyword-Argumente mit Standardwerten ausprobiert? Etwas wie

def decorate_something(foo=bar, baz=quux):
    pass
Kquinn
quelle
-2

Im Allgemeinen können Sie in Python Standardargumente angeben ...

def redirect_output(fn, output = stderr):
    # whatever

Ich bin mir nicht sicher, ob das auch mit Dekorateuren funktioniert. Ich kenne keinen Grund, warum es nicht so wäre.

David Z.
quelle
2
Wenn Sie @dec (abc) sagen, wird die Funktion nicht direkt an dec übergeben. dec (abc) gibt etwas zurück und dieser Rückgabewert wird als Dekorator verwendet. Dec (abc) muss also eine Funktion zurückgeben, die dann die dekorierte Funktion als Parameter übergibt. (Siehe auch Thobes Code)
etw
-2

Aufbauend auf der Antwort von vartec:

imports sys

def redirect_output(func, output=None):
    if output is None:
        output = sys.stderr
    if isinstance(output, basestring):
        output = open(output, 'w') # etc...
    # everything else...
Muhuk
quelle
Dies kann nicht wie im @redirect_output("somewhere.log") def foo()Beispiel in der Frage als Dekorateur verwendet werden .
Ehabkost