Chciałbym stworzyć dekorator Pythona, którego można używać z parametrami:
@redirect_output("somewhere.log")
def foo():
....
lub bez nich (na przykład, aby domyślnie przekierować wyjście na stderr):
@redirect_output
def foo():
....
Czy to w ogóle możliwe?
Zwróć uwagę, że nie szukam innego rozwiązania problemu przekierowania wyjścia, to tylko przykład składni, którą chciałbym osiągnąć.
@redirect_output
jest wyjątkowo mało informacyjny. Sugerowałbym, że to zły pomysł. Skorzystaj z pierwszej formy i znacznie uprość sobie życie.Odpowiedzi:
Wiem, że to pytanie jest stare, ale niektóre komentarze są nowe i chociaż wszystkie realne rozwiązania są zasadniczo takie same, większość z nich nie jest zbyt przejrzysta ani łatwa do odczytania.
Jak mówi odpowiedź thobe, jedynym sposobem rozwiązania obu przypadków jest sprawdzenie obu scenariuszy. Najłatwiej jest po prostu sprawdzić, czy jest pojedynczy argument i jest on wywoływany (UWAGA: dodatkowe sprawdzenia będą konieczne, jeśli dekorator przyjmuje tylko 1 argument i jest to obiekt możliwy do wywołania):
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)
W pierwszym przypadku robisz to, co robi każdy normalny dekorator, zwracając zmodyfikowaną lub opakowaną wersję przekazanej funkcji.
W drugim przypadku zwracasz „nowy” dekorator, który w jakiś sposób wykorzystuje informacje przekazane przez * args, ** kwargs.
To wszystko w porządku, ale napisanie tego dla każdego dekoratora, którego tworzysz, może być dość denerwujące i nie tak czyste. Zamiast tego byłoby miło móc automagicznie modyfikować nasze dekoratory bez konieczności ich ponownego pisania ... ale po to są dekoratorzy!
Korzystając z następującego dekoratora dekoratora, możemy zdekokrować nasze dekoratory, aby mogły być używane z argumentami lub bez:
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
Teraz możemy ozdobić nasze dekoratory @doublewrap i będą one działać z argumentami i bez nich, z jednym zastrzeżeniem:
Zauważyłem powyżej, ale powinienem tutaj powtórzyć, sprawdzenie w tym dekoratorze zakłada, że argumenty, które może otrzymać dekorator (a mianowicie, że nie może otrzymać pojedynczego, wywoływalnego argumentu). Ponieważ teraz sprawiamy, że ma on zastosowanie do dowolnego generatora, należy o tym pamiętać lub zmodyfikować, jeśli będzie się temu zaprzeczać.
Poniżej przedstawiono jego zastosowanie:
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
źródło
Używanie argumentów słów kluczowych z wartościami domyślnymi (zgodnie z sugestią kquinn) jest dobrym pomysłem, ale będzie wymagało umieszczenia nawiasów:
@redirect_output() def foo(): ...
Jeśli chcesz, aby wersja działała bez nawiasów na dekoratorze, musisz uwzględnić oba scenariusze w swoim kodzie dekoratora.
Jeśli używasz Pythona 3.0, możesz użyć do tego argumentów zawierających tylko słowa kluczowe:
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)
W Pythonie 2.x można to emulować za pomocą sztuczek Varargs:
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
Każda z tych wersji pozwoliłaby na napisanie takiego kodu:
@redirected_output def foo(): ... @redirected_output(destination="somewhere.log") def bar(): ...
źródło
your code here
? Jak nazywasz dekorowaną funkcję?fn(*args, **kwargs)
nie działa.def f(a = 5): return MyDecorator( a = a)
iclass MyDecorator( object ): def __init__( self, a = 5 ): ....
przepraszam, że trudno to napisać w komentarzu, ale mam nadzieję, że jest to wystarczająco proste do zrozumieniaWiem, że to stare pytanie, ale żadna z proponowanych technik mi się nie podoba, więc chciałem dodać inną metodę. Widziałem, że django używa naprawdę czystej metody w swoim
login_required
dekoratorze wdjango.contrib.auth.decorators
. Jak można zobaczyć w docs dekoratorów , może być stosowany samodzielnie jako@login_required
albo z argumentów@login_required(redirect_field_name='my_redirect_field')
.Sposób, w jaki to robią, jest dość prosty. Dodają
kwarg
(function=None
) przed argumentami dekoratora. Jeśli dekorator jest używany samodzielnie,function
będzie rzeczywistą funkcją, którą dekoruje, natomiast jeśli zostanie wywołany z argumentami,function
będzieNone
.Przykład:
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
Uważam, że to podejście, którego używa django, jest bardziej eleganckie i łatwiejsze do zrozumienia niż jakiekolwiek inne proponowane tutaj techniki.
źródło
function
, a następnie rzeczy złamać, ponieważ stara dekorator zadzwonić tego pierwszego argumentu pozycyjnym jakby to była twoja funkcja urządzone.Musisz wykryć oba przypadki, na przykład używając typu pierwszego argumentu i odpowiednio zwrócić wrapper (gdy jest używany bez parametru) lub dekorator (gdy jest używany z argumentami).
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
Przy używaniu
@redirect_output("output.log")
składniredirect_output
wywoływana jest z pojedynczym argumentem"output.log"
i musi zwrócić dekorator akceptujący funkcję, która ma być dekorowana jako argument. Gdy jest używany jako@redirect_output
, jest wywoływany bezpośrednio z funkcją, która ma być dekorowana jako argument.Innymi słowy: po
@
składni musi następować wyrażenie, którego wynikiem jest funkcja przyjmująca funkcję do dekoracji jako jedyny argument i zwracająca dekorowaną funkcję. Samo wyrażenie może być wywołaniem funkcji, co ma miejsce w przypadku@redirect_output("output.log")
. Zagmatwane, ale prawdziwe :-)źródło
Kilka odpowiedzi tutaj już ładnie rozwiązuje Twój problem. Jednak jeśli chodzi o styl, wolę rozwiązywać tę kłopotliwą sytuację dekoratora za pomocą
functools.partial
, jak zasugerowano w Python Cookbook 3 Davida Beazleya :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
Chociaż tak, możesz po prostu to zrobić
@decorator() def f(*args, **kwargs): pass
bez ciekawych rozwiązań wydaje mi się to dziwne i lubię mieć możliwość prostego dekorowania
@decorator
.Jeśli chodzi o drugorzędny cel misji, o przekierowaniu wyników funkcji opisano w tym poście o przepełnieniu stosu .
Jeśli chcesz zanurkować głębiej, zapoznaj się z rozdziałem 9 (Metaprogramowanie) w Python Cookbook 3 , który jest bezpłatnie dostępny do czytania online .
Część tego materiału jest demonstrowana na żywo (i nie tylko!) W niesamowitym wideo Beazleya w YouTube Python 3 Metaprogramming .
Miłego kodowania :)
źródło
Dekorator Pythona jest wywoływany w zasadniczo inny sposób, w zależności od tego, czy podajesz mu argumenty, czy nie. Dekoracja jest w rzeczywistości tylko wyrażeniem (ograniczonym składniowo).
W pierwszym przykładzie:
@redirect_output("somewhere.log") def foo(): ....
funkcja
redirect_output
jest wywoływana z podanym argumentem, co powinno zwrócić funkcję dekoratora, która sama jest wywoływanafoo
jako argument, która (ostatecznie!) ma zwrócić ostatnią dekorowaną funkcję.Odpowiedni kod wygląda następująco:
def foo(): .... d = redirect_output("somewhere.log") foo = d(foo)
Odpowiedni kod dla drugiego przykładu wygląda następująco:
def foo(): .... d = redirect_output foo = d(foo)
Więc może robić, co chcesz, ale nie w sposób całkowicie bezproblemową:
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)
To powinno być w porządku, chyba że chcesz użyć funkcji jako argumentu dla swojego dekoratora. W takim przypadku dekorator błędnie założy, że nie ma argumentów. Nie powiedzie się również, jeśli ta dekoracja zostanie zastosowana do innej dekoracji, która nie zwraca typu funkcji.
Alternatywną metodą jest po prostu wymaganie, aby funkcja dekoratora była zawsze wywoływana, nawet jeśli nie ma argumentów. W takim przypadku Twój drugi przykład wyglądałby następująco:
@redirect_output() def foo(): ....
Kod funkcji dekoratora wyglądałby następująco:
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)
źródło
W rzeczywistości, zastrzeżenie w rozwiązaniu @ bj0 można łatwo sprawdzić:
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
Oto kilka przypadków testowych dla tej niezawodnej wersji programu
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
źródło
Aby udzielić pełniejszej odpowiedzi niż powyższe:
Nie, nie ma ogólnego sposobu, ponieważ obecnie w języku Python brakuje czegoś do wykrywania dwóch różnych przypadków użycia.
Jednak tak, jak już wskazano w innych odpowiedziach, takich jak
bj0
s , istnieje niezgrabne obejście polegające na sprawdzeniu typu i wartości pierwszego otrzymanego argumentu pozycyjnego (i sprawdzeniu, czy żadne inne argumenty nie mają wartości innej niż domyślna). Jeśli masz gwarancję, że użytkownicy nigdy nie przekażą wywołania jako pierwszego argumentu dekoratora, możesz zastosować to obejście. Zauważ, że to samo dotyczy dekoratorów klas (zastąp wywoływalne przez klasę w poprzednim zdaniu).Aby mieć pewność co do powyższego, przeprowadziłem sporo badań, a nawet wdrożyłem bibliotekę o nazwie,
decopatch
która wykorzystuje kombinację wszystkich strategii wymienionych powyżej (i wielu innych, w tym introspekcji), aby wykonać „najbardziej inteligentne obejście” w zależności od na twoje potrzeby.Ale szczerze mówiąc, najlepiej byłoby nie potrzebować tutaj żadnej biblioteki i uzyskać tę funkcję bezpośrednio z języka Python. Jeśli tak jak ja myślisz, że szkoda, że język pythona nie jest na dzień dzisiejszy w stanie udzielić zgrabnej odpowiedzi na to pytanie, nie wahaj się wesprzeć tego pomysłu w pythonie bugtracker : https: //bugs.python .org / issue36553 !
Wielkie dzięki za pomoc w uczynieniu Pythona lepszym językiem :)
źródło
To działa bez problemów:
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
źródło
Ponieważ nikt o tym nie wspomniał, istnieje również rozwiązanie wykorzystujące klasę wywoływaną, które uważam za bardziej eleganckie, szczególnie w przypadkach, gdy dekorator jest złożony i można chcieć podzielić go na wiele metod (funkcji). To rozwiązanie wykorzystuje
__new__
magiczną metodę, aby zasadniczo zrobić to, co wskazali inni. Najpierw wykryj, w jaki sposób użyto dekoratora, a następnie odpowiednio dostosuj powrót.class decorator_with_arguments(object): def __new__(cls, decorated_function=None, **kwargs): self = super().__new__(cls) self._init(**kwargs) if not decorated_function: return self else: return self.__call__(decorated_function) def _init(self, arg1="default", arg2="default", arg3="default"): self.arg1 = arg1 self.arg2 = arg2 self.arg3 = arg3 def __call__(self, decorated_function): def wrapped_f(*args): print("Decorator arguments:", self.arg1, self.arg2, self.arg3) print("decorated_function arguments:", *args) decorated_function(*args) return wrapped_f @decorator_with_arguments(arg1=5) def sayHello(a1, a2, a3, a4): print('sayHello arguments:', a1, a2, a3, a4) @decorator_with_arguments() def sayHello(a1, a2, a3, a4): print('sayHello arguments:', a1, a2, a3, a4) @decorator_with_arguments def sayHello(a1, a2, a3, a4): print('sayHello arguments:', a1, a2, a3, a4)
Jeśli dekorator jest używany z argumentami, to jest równe:
result = decorator_with_arguments(arg1=5)(sayHello)(a1, a2, a3, a4)
Widać, że argumenty
arg1
są poprawnie przekazywane do konstruktora, a dekorowana funkcja jest przekazywana do__call__
Ale jeśli dekorator jest używany bez argumentów, to jest równe:
Widzisz, że w tym przypadku dekorowana funkcja jest przekazywana bezpośrednio do konstruktora i wywoływana
__call__
jest całkowicie pomijane. Dlatego musimy posłużyć się logiką, aby zająć się tym przypadkiem w__new__
metodzie magicznej.Dlaczego nie możemy użyć
__init__
zamiast__new__
? Powód jest prosty: python zabrania zwracania jakichkolwiek innych wartości niż None from__init__
OSTRZEŻENIE
Takie podejście ma jeden efekt uboczny. Nie zachowa podpisu funkcji!
źródło
Czy próbowałeś argumentów słów kluczowych z wartościami domyślnymi? Coś jak
def decorate_something(foo=bar, baz=quux): pass
źródło
Generalnie możesz podać domyślne argumenty w Pythonie ...
def redirect_output(fn, output = stderr): # whatever
Nie jestem jednak pewien, czy to działa również w przypadku dekoratorów. Nie znam żadnego powodu, dla którego by nie miał.
źródło
Opierając się na odpowiedzi firmy 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...
źródło
@redirect_output("somewhere.log") def foo()
przykładzie w pytaniu.