Parametreli veya parametreler olmadan kullanılabilen bir Python dekoratörü nasıl oluşturulur?


90

Parametreler ile kullanılabilecek bir Python dekoratörü oluşturmak istiyorum:

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

veya bunlar olmadan (örneğin çıktıyı varsayılan olarak stderr'e yönlendirmek için):

@redirect_output
def foo():
    ....

Bu mümkün mü?

Çıktıyı yeniden yönlendirme sorununa farklı bir çözüm aramadığımı unutmayın, bu sadece elde etmek istediğim sözdiziminin bir örneğidir.


Varsayılan görünüm @redirect_outputdikkat çekici derecede bilgisizdir. Bunun kötü bir fikir olduğunu söyleyebilirim. İlk formu kullanın ve hayatınızı çok kolaylaştırın.
S.Lott

Yine de ilginç bir soru - onu görene ve belgelere bakana kadar, @f'nin @f () ile aynı olduğunu varsaymıştım ve yine de dürüst olmak gerekirse öyle olması gerektiğini düşünüyorum (sağlanan herhangi bir argüman sadece takip edilecek fonksiyon bağımsız değişkenine)
rog

Yanıtlar:


68

Bu sorunun eski olduğunu biliyorum, ancak yorumların bazıları yeni ve uygulanabilir çözümlerin tümü temelde aynı olsa da, çoğu çok temiz veya okunması kolay değil.

Thobe'nin cevabının dediği gibi, her iki durumu da ele almanın tek yolu, her iki senaryoyu da kontrol etmektir. En kolay yol, tek bir argüman olup olmadığını ve bunun callabe olup olmadığını kontrol etmektir (NOT: dekoratörünüz yalnızca 1 argüman alırsa ve çağrılabilir bir nesne olursa ekstra kontroller gerekli olacaktır):

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)

İlk durumda, normal bir dekoratörün yaptığını yaparsınız, aktarılan işlevin değiştirilmiş veya sarılmış bir sürümünü döndürürsünüz.

İkinci durumda, * args, ** kwargs ile iletilen bilgileri bir şekilde kullanan 'yeni' bir dekoratör döndürürsünüz.

Bu iyi ve hepsi, ancak yaptığınız her dekoratör için yazmak zorunda kalmak oldukça can sıkıcı olabilir ve o kadar temiz olmayabilir. Bunun yerine, dekoratörlerimizi yeniden yazmak zorunda kalmadan otomatik olarak değiştirebilmek güzel olurdu ... ama dekoratörler bunun için var!

Aşağıdaki dekoratör dekoratörünü kullanarak, dekoratörlerimizi bağımsız değişkenlerle veya bağımsız olarak kullanılabilecekleri şekilde çözebiliriz:

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

Şimdi, dekoratörlerimizi @doublewrap ile süsleyebiliriz ve onlar argümanlarla ve argümansız çalışacaklar, tek bir uyarı ile:

Yukarıda not ettim ama burada tekrar etmeliyim, bu dekoratördeki kontrol, bir dekoratörün alabileceği argümanlar hakkında bir varsayımda bulunur (yani tek, çağrılabilir bir argüman alamayacağı). Şu anda herhangi bir jeneratöre uygulanabilir hale getirdiğimiz için, akılda tutulmalı veya çelişecekse değiştirilmelidir.

Aşağıdakiler kullanımını göstermektedir:

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

31

Varsayılan değerlerle anahtar kelime argümanları kullanmak (kquinn tarafından önerildiği gibi) iyi bir fikirdir, ancak parantez eklemenizi gerektirir:

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

Dekoratörde parantez olmadan çalışan bir versiyon istiyorsanız, dekoratör kodunuzdaki her iki senaryoyu da hesaba katmanız gerekecektir.

Python 3.0 kullanıyorsanız, bunun için yalnızca anahtar kelime argümanları kullanabilirsiniz:

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)

Python 2.x'te bu, varargs hileleriyle taklit edilebilir:

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

Bu sürümlerden herhangi biri şu şekilde kod yazmanıza izin verir:

@redirected_output
def foo():
    ...

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

1
Ne koyarsın your code here? Süslenen işlevi nasıl adlandırırsınız? fn(*args, **kwargs)çalışmıyor.
lum

bence çok daha basit bir cevap var, dekoratörün seçmeli argümanlarla yapacağı bir sınıf oluşturun. varsayılanlarla aynı bağımsız değişkenlere sahip başka bir işlev oluşturun ve dekoratör sınıflarının yeni bir örneğini döndürür. şuna benzemeli: def f(a = 5): return MyDecorator( a = a) ve class MyDecorator( object ): def __init__( self, a = 5 ): .... bir yorumda yazdığı için özür dilerim ama umarım bunu anlayacak kadar basittir
Omer Ben Haim

17

Bunun eski bir soru olduğunu biliyorum, ancak önerilen tekniklerin hiçbirini gerçekten sevmiyorum, bu yüzden başka bir yöntem eklemek istedim. Django'nun login_requireddekoratöründedjango.contrib.auth.decorators gerçekten temiz bir yöntem kullandığını gördüm . Eğer görebileceğiniz gibi dekoratör'ın docs , o kadar yalnız kullanılabilir @login_requiredveya argümanlarla @login_required(redirect_field_name='my_redirect_field').

Bunu yapma biçimleri oldukça basit. Dekoratör argümanlarından önce bir kwarg( function=None) eklerler . Dekoratör tek başına kullanılırsa, süslediği functionasıl işlev olacaktır, oysa bağımsız değişkenlerle çağrılırsa, functionolacaktır None.

Misal:

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

Django'nun kullandığı bu yaklaşımı, burada önerilen diğer tekniklerden daha zarif ve anlaşılması daha kolay buluyorum.


Evet, bu yöntemi beğendim. Eğer o notu Do sahip aksi ilk pozisyonel arg atanan dekoratör çağrılırken kwargs kullanmak functionşeyler kırmak ve daha sonra sizin dekore işlev sanki dekoratör çalışır ilk pozisyonel arg aramak için.
Dustin Wyatt

12

Her iki durumu da tespit etmeniz gerekir, örneğin ilk argümanın türünü kullanarak ve buna göre sarmalayıcı (parametresiz kullanıldığında) veya bir dekoratör (argümanlarla kullanıldığında).

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

@redirect_output("output.log")Sözdizimini kullanırken ,redirect_output tek bir bağımsız değişkenle çağrılır "output.log"ve işlevin bağımsız değişken olarak dekore edilmesini kabul eden bir dekoratör döndürmesi gerekir. Olarak kullanıldığında @redirect_output, doğrudan bir argüman olarak dekore edilecek fonksiyonla çağrılır.

Ya da başka bir deyişle: @sözdiziminin ardından, sonucu bir işlevi tek argüman olarak dekore edilecek bir işlevi kabul eden ve dekore edilmiş işlevi döndüren bir ifade gelmelidir. İfadenin kendisi bir işlev çağrısı olabilir, bu durumda @redirect_output("output.log"). Katkılı, ancak doğru :-)


9

Buradaki birkaç cevap zaten sorununuzu güzel bir şekilde ele alıyor. Tarza gelince functools.partial, bu dekoratör çıkmazını David Beazley'in Python Cookbook 3'te önerildiği gibi çözmeyi tercih ederim :

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

Evet iken, sadece yapabilirsin

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

acayip geçici çözümler olmadan, tuhaf görünüyor ve basitçe dekorasyon seçeneğine sahip olmaktan hoşlanıyorum @decorator.

İkincil görev hedefine gelince, bir işlevin çıktısının yeniden yönlendirilmesi bu Yığın Taşması yazısında ele alınmaktadır .


Daha derine dalmak istiyorsanız, çevrimiçi olarak ücretsiz olarak okunabilen Python Yemek Kitabı 3'teki Bölüm 9'a (Metaprogramlama) bakın .

Bu materyalin bir kısmı Beazley'in harika YouTube videosu Python 3 Metaprogramming'de canlı olarak (artı daha fazlası!) .

Mutlu kodlamalar :)


8

Bir python dekoratörü, ona argüman verip vermemenize bağlı olarak temelde farklı bir şekilde çağrılır. Dekorasyon aslında sadece (sözdizimsel olarak sınırlı) bir ifadedir.

İlk örneğinizde:

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

fonksiyon redirect_outputkendisi ile çağrılan bir dekoratör fonksiyonunu dönmesi bekleniyor verilen argüman, ile çağrılır foo(nihayet!) nihai dekore işlevi dönmek için beklendiği bir argüman olarak.

Eşdeğer kod şuna benzer:

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

İkinci örneğinizin eşdeğer kodu şöyle görünür:

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

Böylece istediğinizi yapabilirsiniz , ancak tamamen kusursuz bir şekilde değil:

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)

Bir işlevi dekoratörünüze argüman olarak kullanmak istemediğiniz sürece bu tamam olmalıdır, bu durumda dekoratör yanlış bir şekilde argümanı olmadığını varsayacaktır. Bu dekorasyon, bir işlev türü döndürmeyen başka bir dekorasyona uygulanırsa da başarısız olur.

Alternatif bir yöntem, bağımsız değişken olmasa bile, dekoratör işlevinin her zaman çağrılmasını gerektirmektir. Bu durumda, ikinci örneğiniz şöyle görünecektir:

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

Dekoratör işlev kodu şöyle görünür:

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)

2

Aslında, @ bj0 çözümündeki uyarı durumu kolayca kontrol edilebilir:

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

İşte bu hata korumalı sürümü için birkaç test durumu 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

1
Teşekkürler. Bu yardımcı olur!
Pragy Agarwal

0

Yukarıdakilerden daha eksiksiz bir cevap vermek için:

"Hem argümanlarla hem de argümansız olarak kullanılabilecek bir dekoratör oluşturmanın bir yolu var mı?

Hayır , genel bir yol yoktur çünkü şu anda python dilinde iki farklı kullanım durumunu tespit etmek için eksik bir şeyler vardır.

ancak evet olarak zaten gibi diğer cevaplar tarafından işaret bj0s , bir orada aksak geçici çözüm (başka argümanlar varsayılan olmayan bir değere sahip olmadığını ve kontrol etmek için) alınan ilk pozisyonel argüman değerinin ve türünün kontrol etmektir. Kullanıcıların dekoratörünüzün ilk argümanı olarak hiçbir zaman çağrılabilir bir çağrıyı geçemeyeceğinden eminseniz, bu geçici çözümü kullanabilirsiniz. Bunun sınıf dekoratörleri için de aynı olduğuna dikkat edin (önceki cümlede çağrılabilenleri sınıfla değiştirin).

Yukarıdakilerden emin olmak için, orada epeyce araştırma yaptım ve hatta decopatchyukarıda belirtilen tüm stratejilerin bir kombinasyonunu kullanan bir kitaplık bile uyguladım (ve iç gözlem dahil olmak üzere çok daha fazlasını) "en akıllı çözüm neyse" ihtiyacın üzerine.

Ama açıkçası en iyisi, burada herhangi bir kitaplığa ihtiyaç duymamak ve bu özelliği doğrudan python dilinden almaktır. Eğer benim gibi, python dilinin bugünkü gibi bu soruya düzgün bir cevap verememesinin üzücü olduğunu düşünüyorsanız, düşünüyorsanız bu fikri python bugtracker : https: //bugs.python'da desteklemekten çekinmeyin. .org / issue36553 !

Python'u daha iyi bir dil yapma yardımınız için çok teşekkürler :)


0

Bu işi zahmetsizce yapar:

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

0

Bundan hiç kimse bahsetmediği için, özellikle dekoratörün karmaşık olduğu ve birden çok yönteme (işlevlere) bölmek isteyebileceği durumlarda daha zarif bulduğum, çağrılabilir sınıf kullanan bir çözüm de var. Bu çözüm __new__, esasen başkalarının işaret ettiği şeyi yapmak için sihirli yöntemi kullanır . İlk olarak, dönüşü uygun şekilde ayarlamak yerine dekoratörün nasıl kullanıldığını tespit edin.

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)

Dekoratör bağımsız değişkenlerle kullanılıyorsa, bu şuna eşittir:

result = decorator_with_arguments(arg1=5)(sayHello)(a1, a2, a3, a4)

Argümanların arg1 kurucuya doğru bir şekilde iletildiği ve dekore edilmiş işlevin__call__

Ancak dekoratör argümansız kullanılırsa, bu şuna eşittir:

result = decorator_with_arguments(sayHello)(a1, a2, a3, a4)

Bu durumda, dekore edilmiş işlevin doğrudan kurucuya iletildiğini ve çağrının __call__tamamen atlandığını görüyorsunuz . Bu nedenle, bu davayla ilgilenmek için mantık kullanmamız gerekiyor.__new__ sihirli yöntemde .

Neden __init__yerine kullanamıyoruz __new__? Nedeni basit: python, Hiçbiri dışında herhangi bir değer döndürmeyi yasaklar__init__

UYARI

Bu yaklaşımın bir yan etkisi vardır. İşlev imzasını korumaz!


-1

Varsayılan değerlere sahip anahtar kelime bağımsız değişkenlerini denediniz mi? Gibi bir şey

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

-2

Genel olarak Python'da varsayılan argümanlar verebilirsiniz ...

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

Bunun dekoratörlerle de işe yarayıp yaramadığından emin değilim. Olmaması için herhangi bir sebep bilmiyorum.


2
@Dec (abc) derseniz, işlev doğrudan dec'e aktarılmaz. dec (abc) bir şey döndürür ve bu dönüş değeri dekoratör olarak kullanılır. Dolayısıyla, dec (abc) bir işlev döndürmek zorundadır, bu da daha sonra dekore edilmiş işlevi bir parametre olarak geçirir. (Ayrıca thobes kodu bakınız)
STH

-2

Vartec'in cevabına dayanarak:

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

bu @redirect_output("somewhere.log") def foo(), sorudaki örnekteki gibi bir dekoratör olarak kullanılamaz .
ehabkost
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.