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

StackOverflow https://stackoverflow.com/questions/653368

  •  19-08-2019
  •  | 
  •  

Frage

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

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

oder ohne sie (zum Beispiel, um die Ausgabe standardmäßig nach 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 Ausgabeumleitung suche, sondern lediglich ein Beispiel für die Syntax ist, die ich erreichen möchte.

War es hilfreich?

Lösung

Ich weiß, diese Frage ist alt, aber einige der Kommentare sind neu, und während all die tragfähige Lösungen im Wesentlichen die gleichen sind, die meisten von ihnen sind nicht sehr sauber und leicht zu lesen.

Wie thobe Antwort sagt, der einzige Weg, beide Fälle zu behandeln, ist für beide Szenarien zu überprüfen. Der einfachste Weg ist einfach zu sehen, zu prüfen, ob ein einziges Argument ist, und es ist callabe (Hinweis: zusätzliche Überprüfungen werden notwendig sein, wenn Ihr Dekorateur dauert nur 1 Argument und es geschieht ein aufrufbare Objekt sein):

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, Sie tun, was jeder normale Dekorateur der Fall ist, geben eine modifizierte oder wickelte Version der in Funktion übergeben.

Im zweiten Fall, Sie geben einen 'neuen' Dekorateur, die irgendwie die Informationen mit * args in verwendet, ** kwargs.

Das ist in Ordnung und alles, aber mit es für jeden Dekorateur Sie kann ziemlich lästig sein, machen zu schreiben, und nicht so sauber. Stattdessen wäre es schön zu können, werden automatisch in unseren Dekorateure modifizieren, ohne sie neu zu schreiben ... aber das ist, was Dekorateure sind!

Bei Verwendung des folgenden Dekorateur Dekorateur, können wir unsere Dekorateure deocrate, so dass sie mit oder ohne Argumente verwendet werden:

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 einer Einschränkung mit und ohne Argumente, arbeiten:

ich oben erwähnt, soll aber hier wiederholen, die Prüfung in diesem Dekorateur macht eine Annahme über die Argumente, die ein Dekorateur empfangen kann (nämlich dass es ein einziges, aufrufbar Argument nicht empfangen kann). Da wir es für jeden Generator jetzt machen, muss man sich vor Augen, oder modifizierte gehalten werden, wenn sie im Widerspruch wird.

Im Folgenden zeigt 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

Andere Tipps

Schlüsselwort-Argumente mit Standardwerten (wie von kquinn vorgeschlagen) ist eine gute Idee, aber benötigen Sie die Klammer schließen ein:

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

Wenn Sie eine Version mögen, die ohne die Klammer auf dem Dekorateur arbeiten Sie beiden Szenarien in Ihrem Dekorateur Code-Konto haben.

Wenn Sie mit Python wurden 3,0 Sie Schlüsselwort nur Argumente für diese 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

Jede dieser Versionen Ihnen erlauben würde, Code wie folgt zu schreiben:

@redirected_output
def foo():
    ...

@redirected_output(destination="somewhere.log")
def bar():
    ...

Sie müssen beide Fälle erfassen, zum Beispiel den Typ des ersten Arguments verwenden und entsprechend zurückgeben entweder den Wrapper (wenn ohne Parameter verwendet wird) oder ein Dekorateur (wenn sie mit Argumenten verwendet wird).

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, redirect_output mit einem einzigen Argumente "output.log" genannt, und es muss einen Dekorateur zurückgeben, die Funktion zu akzeptieren als Argument eingerichtet werden. Wenn sie als @redirect_output verwendet wird, wird es direkt mit der Funktion als Argument eingerichtet werden.

Oder mit anderen Worten: die @ Syntax muss durch einen Ausdruck, dessen Ergebnis eine Funktion eine Funktion als das einzige Argument ist dekoriert zu akzeptieren folgen, und die eingerichtete Funktion zurückkehrt. Der Ausdruck selbst kann ein Funktionsaufruf sein, was der Fall mit @redirect_output("output.log") ist. Verworren, aber wahr: -)

Ein Python-Dekorator wird grundsätzlich unterschiedlich 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_output wird mit dem angegebenen Argument aufgerufen, der eine Dekorationsfunktion zurückgibt, die selbst aufgerufen wird foo Als Argument wird erwartet, dass (endlich!) die endgültige dekorierte Funktion zurückgibt.

Der entsprechende Code sieht so aus:

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

Der entsprechende Code für Ihr zweites Beispiel sieht so aus:

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

Also du dürfen Machen Sie, was Sie möchten, aber nicht auf völlig nahtlose Weise:

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 wird der Dekorateur fälschlicherweise annehmen, dass es keine Argumente hat.Es wird auch fehlschlagen, wenn diese Dekoration auf eine andere Dekoration angewendet wird, die keinen Funktionstyp zurückgibt.

Eine alternative Methode besteht nur darin, dass die Dekorationsfunktion immer aufgerufen wird, auch wenn sie keine Argumente entspricht.In diesem Fall würde Ihr zweites Beispiel so aussehen:

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

Der Dekorator-Funktionscode würde so 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)

Ich weiß, dass dies eine alte Frage ist, aber ich weiß nicht wirklich wie eine der Techniken vorgeschlagen, so wollte ich eine andere Methode hinzuzufügen. Ich sah, dass django verwendet eine wirklich saubere Methode in ihr login_required Dekorateur in django.contrib.auth.decorators . Wie Sie in dem Dekorateur docs sehen , kann er allein als @login_required oder mit Argumenten verwendet werden, @login_required(redirect_field_name='my_redirect_field').

Die Art, wie sie es tun, ist ganz einfach. Sie fügen hinzu, eine kwarg (function=None) vor ihrer Dekorateur Argumente. Wenn der Dekorateur allein verwendet wird, wird function die eigentliche Funktion sei es die Dekoration ist, während, wenn sie mit Argumenten aufgerufen wird, wird function None werden.

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

Das finde ich Ansatz, eleganter und leichter zu verstehen als alle anderen Techniken vorgeschlagen hier django verwendet.

Mehrere Antworten hier schon Ihr Problem gut ansprechen. Im Hinblick auf Stil, aber ich ziehe die Lösung dieses Dekorateur misslichen functools.partial verwenden, wie vorgeschlagen in David Beazley Python-Kochbuch 3 :

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 flippige Abhilfen, ich finde es seltsam suchen, und Ich mag mit der Möglichkeit, einfach mit @decorator Dekoration.

Wie für das sekundäre Missionsziel, ist eine Funktion der Ausgabe Umleitung in diesem adressiert Stapelüberlauf Post .


Wenn Sie tiefer tauchen, finden Sie in Kapitel 9 (Metaprogrammierung) in Python-Kochbuch 3 , die frei verfügbar ist online lesen .

Ein Teil dieses Material ist Live demoed (plus mehr!) In Beazleys ehrfürchtig YouTube Video Python 3 Metaprogrammierung .

Happy-Codierung:)

In der Tat, die Einschränkung Fall in-Lösung @ bj0 leicht überprüft werden kann:

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 ein paar 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

Haben Sie Keyword-Argumente mit Standardwerten versucht? So etwas wie

def decorate_something(foo=bar, baz=quux):
    pass

Generell können Sie Standardargumente in Python geben ...

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

Nicht sicher, ob das mit Dekorateure funktioniert auch, wenn. Ich weiß nicht, von irgendeinem Grund, warum es nicht würde.

Aufbauend auf vartec Antwort:

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...
Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top