Parametreleri olan dekoratörler?


401

Dekoratör tarafından değişken 'insurance_mode' aktarımı ile ilgili bir sorun var. Aşağıdaki dekoratör deyimiyle bunu yaparım:

 @execute_complete_reservation(True)
 def test_booking_gta_object(self):
     self.test_select_gta_object()

ama ne yazık ki, bu ifade işe yaramıyor. Belki de bu sorunu çözmenin daha iyi bir yolu vardır.

def execute_complete_reservation(test_case,insurance_mode):
    def inner_function(self,*args,**kwargs):
        self.test_create_qsf_query()
        test_case(self,*args,**kwargs)
        self.test_select_room_option()
        if insurance_mode:
            self.test_accept_insurance_crosseling()
        else:
            self.test_decline_insurance_crosseling()
        self.test_configure_pax_details()
        self.test_configure_payer_details

    return inner_function

3
Örneğiniz sözdizimsel olarak geçerli değil. execute_complete_reservationiki parametre alır, ancak bir parametre geçirirsiniz. Dekoratörler, diğer fonksiyonların içine sarma fonksiyonları için sadece sözdizimsel şekerdir. Eksiksiz belgeler için docs.python.org/reference/compound_stmts.html#function sayfasına bakın .
Brian Clapper

Yanıtlar:


687

Bağımsız değişkenli dekoratörlerin sözdizimi biraz farklıdır - bağımsız değişkenlere sahip dekoratör, bir işlevi alacak ve başka bir işlevi döndürecek bir işlev döndürmelidir. Bu yüzden gerçekten normal bir dekoratör dönmelidir. Biraz kafa karıştırıcı, değil mi? Demek istediğim ... dir:

def decorator_factory(argument):
    def decorator(function):
        def wrapper(*args, **kwargs):
            funny_stuff()
            something_with_argument(argument)
            result = function(*args, **kwargs)
            more_funny_stuff()
            return result
        return wrapper
    return decorator

Burada konu hakkında daha fazla bilgi edinebilirsiniz - bunu çağrılabilir nesneler kullanarak uygulamak da mümkündür ve bu da orada açıklanmaktadır.


56
GVR'nin neden parametreleri 'işlev'den sonra sonraki dekoratör argümanları olarak ileterek uygulamadığını merak ediyorum. 'Seni ahbap seni kapatmalar gibi duydum ...' vb.
Michel Müller

3
> İşlev ilk argüman mı yoksa sonuncusu mu? Açıkçası, çünkü parametreler değişken uzunluklu bir parametre listesidir. > İşlevi tanımdakinden farklı bir imzayla "çağırmanız" da gariptir. İşaret ettiğiniz gibi, aslında oldukça iyi uyuyordu - bir sınıf yönteminin nasıl adlandırıldığına oldukça benzer. Daha açık hale getirmek için, dekoratör (self_func, param1, ...) konvansiyonu gibi bir şey olabilir. Ancak not: Burada herhangi bir değişiklik yapılmasını savunmuyorum, Python bunun için çok uzakta ve değişikliklerin nasıl sonuç verdiğini görebiliriz ..
Michel Müller

21
Eğer sarmalayıcı dekorasyon için çok yararlı functools.wraps unuttun :)
socketpair

10
İşlevi çağırırken dönüşü unuttun, yani return function(*args, **kwargs)
formiaczek

36
Belki açık, ama her ihtimale karşı: sadece isteğe bağlı argümanlarınız olsa bile, bu dekoratörü @decorator()sadece olarak değil de kullanmanız gerekir @decorator.
Patrick Mevzek

325

Düzenleme : Dekoratörlerin zihinsel modelinin derinlemesine anlaşılması için bu harika Pycon Talk'a bir göz atın . iyi 30 dakika.

Tartışmalı dekoratörler hakkında düşünmenin bir yolu

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

Çevirir

foo = decorator(foo)

Dekoratörün argümanları olsaydı,

@decorator_with_args(arg)
def foo(*args, **kwargs):
    pass

Çevirir

foo = decorator_with_args(arg)(foo)

decorator_with_args özel bir bağımsız değişkeni kabul eden ve gerçek dekoratörü (süslü işleve uygulanacak) döndüren bir işlevdir.

Dekoratörlerimi kolaylaştırmak için kısmi basit bir numara kullanıyorum

from functools import partial

def _pseudo_decor(fun, argument):
    def ret_fun(*args, **kwargs):
        #do stuff here, for eg.
        print ("decorator arg is %s" % str(argument))
        return fun(*args, **kwargs)
    return ret_fun

real_decorator = partial(_pseudo_decor, argument=arg)

@real_decorator
def foo(*args, **kwargs):
    pass

Güncelleme:

Yukarıda, fooolurreal_decorator(foo)

Bir işlevi süslemenin bir etkisi, foodekoratör bildirimi üzerine adın geçersiz kılınmasıdır. footarafından döndürülen her şey tarafından "geçersiz kılınır" real_decorator. Bu durumda, yeni bir işlev nesnesi.

Tüm foometa verileri geçersiz kılınır, özellikle doktora ve işlev adı.

>>> print(foo)
<function _pseudo_decor.<locals>.ret_fun at 0x10666a2f0>

functools.wraps bize docstring ve ismi döndürülen fonksiyona "kaldırmak" için uygun bir yöntem sunar.

from functools import partial, wraps

def _pseudo_decor(fun, argument):
    # magic sauce to lift the name and doc of the function
    @wraps(fun)
    def ret_fun(*args, **kwargs):
        #do stuff here, for eg.
        print ("decorator arg is %s" % str(argument))
        return fun(*args, **kwargs)
    return ret_fun

real_decorator = partial(_pseudo_decor, argument=arg)

@real_decorator
def bar(*args, **kwargs):
    pass

>>> print(bar)
<function __main__.bar(*args, **kwargs)>

4
Cevabınız dekoratörün içsel
dikliğini

Eklemek misiniz @functools.wraps?
Mr_and_Mrs_D

1
@Mr_and_Mrs_D, yayını bir örnekle güncelledim functool.wraps. Örneğe eklenmesi okuyucuları daha fazla karıştırabilir.
srj

7
argBurada ne var?
görünen

1
Bu argümana aktarılan argümanı nasıl barileteceksiniz real_decorator?
Chang Zhao

85

IMHO'nun oldukça zarif bir fikir göstermek istiyorum. T.dubrownik tarafından önerilen çözüm her zaman aynı olan bir desen gösterir: dekoratörün ne yaptığından bağımsız olarak üç katmanlı sargıya ihtiyacınız vardır.

Bu yüzden bunun bir meta-dekoratör, yani dekoratörler için bir dekoratör olduğunu düşündüm. Bir dekoratör bir işlev olduğundan, aslında argümanlarla düzenli bir dekoratör olarak çalışır:

def parametrized(dec):
    def layer(*args, **kwargs):
        def repl(f):
            return dec(f, *args, **kwargs)
        return repl
    return layer

Bu, parametre eklemek için normal bir dekoratöre uygulanabilir. Diyelim ki, bir fonksiyonun sonucunu iki katına çıkaran dekoratöre sahibiz:

def double(f):
    def aux(*xs, **kws):
        return 2 * f(*xs, **kws)
    return aux

@double
def function(a):
    return 10 + a

print function(3)    # Prints 26, namely 2 * (10 + 3)

İle @parametrizedbir jenerik inşa edebilirsiniz @multiplybir parametre olan dekoratör

@parametrized
def multiply(f, n):
    def aux(*xs, **kws):
        return n * f(*xs, **kws)
    return aux

@multiply(2)
def function(a):
    return 10 + a

print function(3)    # Prints 26

@multiply(3)
def function_again(a):
    return 10 + a

print function(3)          # Keeps printing 26
print function_again(3)    # Prints 39, namely 3 * (10 + 3)

Bir geleneksel olarak birinci parametre parametrize kalan argümanlar parametrized dekoratör parametreye tekabül ederken dekoratör fonksiyonudur.

İlginç bir kullanım örneği, tür güvenli bir iddialı dekoratör olabilir:

import itertools as it

@parametrized
def types(f, *types):
    def rep(*args):
        for a, t, n in zip(args, types, it.count()):
            if type(a) is not t:
                raise TypeError('Value %d has not type %s. %s instead' %
                    (n, t, type(a))
                )
        return f(*args)
    return rep

@types(str, int)  # arg1 is str, arg2 is int
def string_multiply(text, times):
    return text * times

print(string_multiply('hello', 3))    # Prints hellohellohello
print(string_multiply(3, 3))          # Fails miserably with TypeError

Son bir not: burada functools.wrapssarıcı fonksiyonları için kullanmıyorum , ama her zaman kullanmanızı tavsiye ederim.


3
Bunu tam olarak kullanmadı, ama kavramın etrafında başımı almama yardımcı oldu :) Teşekkürler!
mouckatron

Bunu denedim ve bazı sorunlar yaşadım .
Jeff

@ Jeff, yaşadığınız sorunları bizimle paylaşır mısınız?
Dacav

Sorumla bağlantılandırdım ve çözdüm ... @wrapsÖzel durumum için benimkini aramam gerekiyordu .
Jeff

4
Ah oğlum, bu gün bütün bir gün kaybettim. Neyse ki, (bu arada tüm internette oluşturulan en iyi cevap olabilir) bu cevap geldi . Onlar da @parametrizedhilenizi kullanıyor. Sahip olduğum sorun, @sözdiziminin gerçek çağrılara eşit olduğunu unuttum (bir şekilde bunu biliyordum ve aynı zamanda sorumdan toplayabileceğiniz gibi bilmiyordum). Bu nedenle, nasıl çalıştığını kontrol etmek için @sözdizimini sıradan aramalara çevirmek istiyorsanız , önce geçici olarak yorumlamanız daha iyi olur ya da iki kez aramak ve mumbojumbo sonuçları almak
z33k 13:18 '

79

İşte t.dubrownik'in cevabının biraz değiştirilmiş bir versiyonu . Neden?

  1. Genel bir şablon olarak, dönüş değerini orijinal işlevden döndürmelisiniz.
  2. Bu, diğer dekoratörleri / kodu etkileyebilecek işlevin adını değiştirir.

Bu yüzden kullanın @functools.wraps():

from functools import wraps

def decorator(argument):
    def real_decorator(function):
        @wraps(function)
        def wrapper(*args, **kwargs):
            funny_stuff()
            something_with_argument(argument)
            retval = function(*args, **kwargs)
            more_funny_stuff()
            return retval
        return wrapper
    return real_decorator

37

Sorununuzun dekoratörünüze argümanlar aktardığını düşünüyorum. Bu biraz zor ve basit değil.

İşte bunun nasıl yapılacağına dair bir örnek:

class MyDec(object):
    def __init__(self,flag):
        self.flag = flag
    def __call__(self, original_func):
        decorator_self = self
        def wrappee( *args, **kwargs):
            print 'in decorator before wrapee with flag ',decorator_self.flag
            original_func(*args,**kwargs)
            print 'in decorator after wrapee with flag ',decorator_self.flag
        return wrappee

@MyDec('foo de fa fa')
def bar(a,b,c):
    print 'in bar',a,b,c

bar('x','y','z')

Baskılar:

in decorator before wrapee with flag  foo de fa fa
in bar x y z
in decorator after wrapee with flag  foo de fa fa

Daha fazla bilgi için Bruce Eckel'in makalesine bakın.


20
Dekoratör sınıflarına dikkat edin. Eğer instancemethod tanımlayıcılarının mantığını yeniden icat etmedikçe yöntemler üzerinde çalışmazlar.

9
delnan, özen göstermek ister misin? Bu modeli sadece bir kez kullanmak zorunda kaldım, bu yüzden henüz hiçbir tuzağa düşmedim.
Ross Rogers

2
@RossRogers Sanırım @delnan, __name__dekoratör sınıfının bir örneğinin sahip olmayacağı şeylere atıfta bulunuyor mu?
jamesc

9
@jamesc Bunu da çözmek kolay olsa da. Bahsettiğim özel durum, bağlı bir yöntem olmayacağı ve otomatik olarak geçmeyeceği için class Foo: @MyDec(...) def method(self, ...): blahişe yaramadı . Bu da bir tanımlayıcı yaparak ve bağlı yöntemler oluşturarak düzeltilebilir, ancak daha ilgili ve daha az açıktır. Sonunda, dekoratör sınıfları göründüğü kadar uygun değildir. Foo().methodselfMyDec__get__

2
@delnan Bu uyarının daha belirgin olduğunu görmek istiyorum. Ben vurmak ve işe yarayan bir çözüm görmek ilgimi çekiyor (daha az açık olsa da daha az dahil).
HaPsantran

12
def decorator(argument):
    def real_decorator(function):
        def wrapper(*args):
            for arg in args:
                assert type(arg)==int,f'{arg} is not an interger'
            result = function(*args)
            result = result*argument
            return result
        return wrapper
    return real_decorator

Dekoratörün kullanımı

@decorator(2)
def adder(*args):
    sum=0
    for i in args:
        sum+=i
    return sum

Sonra

adder(2,3)

üretir

10

fakat

adder('hi',3)

üretir

---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-143-242a8feb1cc4> in <module>
----> 1 adder('hi',3)

<ipython-input-140-d3420c248ebd> in wrapper(*args)
      3         def wrapper(*args):
      4             for arg in args:
----> 5                 assert type(arg)==int,f'{arg} is not an interger'
      6             result = function(*args)
      7             result = result*argument

AssertionError: hi is not an interger

8

Bu, ()herhangi bir parametre verilmemesi gerekmeyen bir işlev dekoratörü için bir şablondur :

import functools


def decorator(x_or_func=None, *decorator_args, **decorator_kws):
    def _decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kws):
            if 'x_or_func' not in locals() \
                    or callable(x_or_func) \
                    or x_or_func is None:
                x = ...  # <-- default `x` value
            else:
                x = x_or_func
            return func(*args, **kws)

        return wrapper

    return _decorator(x_or_func) if callable(x_or_func) else _decorator

bunun bir örneği aşağıda verilmiştir:

def multiplying(factor_or_func=None):
    def _decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            if 'factor_or_func' not in locals() \
                    or callable(factor_or_func) \
                    or factor_or_func is None:
                factor = 1
            else:
                factor = factor_or_func
            return factor * func(*args, **kwargs)
        return wrapper
    return _decorator(factor_or_func) if callable(factor_or_func) else _decorator


@multiplying
def summing(x): return sum(x)

print(summing(range(10)))
# 45


@multiplying()
def summing(x): return sum(x)

print(summing(range(10)))
# 45


@multiplying(10)
def summing(x): return sum(x)

print(summing(range(10)))
# 450

Da unutmayın factor_or_func(veya başka bir parametresi) yeniden alır asla içinde wrapper().
norok2

Neden check-in yapmanız gerekiyor locals()?
Shital Shah

@ShitalShah, dekoratörün kullanılmadığı durumu kapsar ().
norok2

4

Benim durumumda, bunu yeni bir dekoratör işlevi oluşturmak için tek satırlık bir lambda ile çözmeye karar verdim:

def finished_message(function, message="Finished!"):

    def wrapper(*args, **kwargs):
        output = function(*args,**kwargs)
        print(message)
        return output

    return wrapper

@finished_message
def func():
    pass

my_finished_message = lambda f: finished_message(f, "All Done!")

@my_finished_message
def my_func():
    pass

if __name__ == '__main__':
    func()
    my_func()

Yürütüldüğünde, bu yazdırılır:

Finished!
All Done!

Belki de diğer çözümler kadar genişletilemez, ama benim için çalıştı.


Bu çalışıyor. Evet olmasına rağmen, bu değeri dekoratöre ayarlamak zorlaşır.
Arindam Roychowdhury

3

Parametreli ve parametresiz çalışan bir dekoratör yazmak zordur, çünkü Python bu iki durumda tamamen farklı bir davranış bekler! Birçok cevap bu sorunu çözmek için çalıştı ve aşağıda @ norok2 tarafından cevap bir gelişme. Özellikle, bu varyasyon,locals() .

@ Norok2 tarafından verilen örnekle aynı:

import functools

def multiplying(f_py=None, factor=1):
    assert callable(f_py) or f_py is None
    def _decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            return factor * func(*args, **kwargs)
        return wrapper
    return _decorator(f_py) if callable(f_py) else _decorator


@multiplying
def summing(x): return sum(x)

print(summing(range(10)))
# 45


@multiplying()
def summing(x): return sum(x)

print(summing(range(10)))
# 45


@multiplying(factor=10)
def summing(x): return sum(x)

print(summing(range(10)))
# 450

Bu kodla oyna .

Yakalama, kullanıcının konum parametreleri yerine anahtar, değer parametre çiftleri sağlaması ve ilk parametrenin ayrılması gerektiğidir.


2

Aşağıdaki iki kod parçasının neredeyse eşdeğer olduğu iyi bilinmektedir:

@dec
def foo():
    pass    foo = dec(foo)

############################################
foo = dec(foo)

Yaygın bir hata, @sadece en soldaki argümanı gizlediğini düşünmektir .

@dec(1, 2, 3)
def foo():
    pass    
###########################################
foo = dec(foo, 1, 2, 3)

Yukarıda nasıl @çalışılırsa dekoratörler yazmak çok daha kolay olurdu . Ne yazık ki, işler böyle yapılmıyor.


WaitProgram yürütmesini birkaç saniyeliğine engelleyen bir dekoratörü düşünün . Bir Bekleme süresini geçmezseniz, varsayılan değer 1 saniyedir. Kullanım durumları aşağıda gösterilmiştir.

##################################################
@Wait
def print_something(something):
    print(something)

##################################################
@Wait(3)
def print_something_else(something_else):
    print(something_else)

##################################################
@Wait(delay=3)
def print_something_else(something_else):
    print(something_else)

Böyle Waitbir argüman olduğunda , @Wait(3)çağrı başka bir şey Wait(3) gerçekleşmeden önce yürütülür .

Yani, aşağıdaki iki kod eşdeğeri

@Wait(3)
def print_something_else(something_else):
    print(something_else)

###############################################
return_value = Wait(3)
@return_value
def print_something_else(something_else):
    print(something_else)

Bu bir problem.

if `Wait` has no arguments:
    `Wait` is the decorator.
else: # `Wait` receives arguments
    `Wait` is not the decorator itself.
    Instead, `Wait` ***returns*** the decorator

Bir çözüm aşağıda gösterilmiştir:

Aşağıdaki sınıfı oluşturarak başlayalım DelayedDecorator:

class DelayedDecorator:
    def __init__(i, cls, *args, **kwargs):
        print("Delayed Decorator __init__", cls, args, kwargs)
        i._cls = cls
        i._args = args
        i._kwargs = kwargs
    def __call__(i, func):
        print("Delayed Decorator __call__", func)
        if not (callable(func)):
            import io
            with io.StringIO() as ss:
                print(
                    "If only one input, input must be callable",
                    "Instead, received:",
                    repr(func),
                    sep="\n",
                    file=ss
                )
                msg = ss.getvalue()
            raise TypeError(msg)
        return i._cls(func, *i._args, **i._kwargs)

Şimdi şöyle yazabiliriz:

 dec = DelayedDecorator(Wait, delay=4)
 @dec
 def delayed_print(something):
    print(something)

Bunu not et:

  • dec birden fazla argümanı kabul etmiyor.
  • dec yalnızca kaydırılacak işlevi kabul eder.

    import inspect class PolyArgDecoratorMeta (type): def call (Bekle, * args, ** kwargs): dene: arg_count = len (args) eğer (arg_count == 1): eğer çağrılabilir ise (args [0]): SuperClass = incele. getmro (PolyArgDecoratorMeta) [1] r = Süper Sınıf. aramak (Wait, args [0]) else: r = DelayedDecorator (Wait, * args, ** kwargs) else: r = DelayedDecorator (Wait, * args, ** kwargs) nihayet: dönüş dönüşü r

    içe aktarma zaman sınıfı Bekleyin (metaclass = PolyArgDecoratorMeta): def init (i, func, delay = 2): i._func = func i._delay = delay

    def __call__(i, *args, **kwargs):
        time.sleep(i._delay)
        r = i._func(*args, **kwargs)
        return r 

Aşağıdaki iki kod parçası eşdeğerdir:

@Wait
def print_something(something):
     print (something)

##################################################

def print_something(something):
    print(something)
print_something = Wait(print_something)

"something"Konsola çok yavaş yazdırabiliriz :

print_something("something")

#################################################
@Wait(delay=1)
def print_something_else(something_else):
    print(something_else)

##################################################
def print_something_else(something_else):
    print(something_else)

dd = DelayedDecorator(Wait, delay=1)
print_something_else = dd(print_something_else)

##################################################

print_something_else("something")

Son Notlar

Çok fazla kod gibi görünebilir, ancak sınıfları DelayedDecoratorve PolyArgDecoratorMetaher seferinde yazmak zorunda değilsiniz . Kişisel olarak aşağıdaki gibi bir şey yazmanız gereken tek kod, oldukça kısa:

from PolyArgDecoratorMeta import PolyArgDecoratorMeta
import time
class Wait(metaclass=PolyArgDecoratorMeta):
 def __init__(i, func, delay = 2):
     i._func = func
     i._delay = delay

 def __call__(i, *args, **kwargs):
     time.sleep(i._delay)
     r = i._func(*args, **kwargs)
     return r

1

özelleştirilmiş dekoratör işlevi oluşturmak için bu "dekoratör işlevi" tanımlayın:

def decoratorize(FUN, **kw):
    def foo(*args, **kws):
        return FUN(*args, **kws, **kw)
    return foo

şu şekilde kullanın:

    @decoratorize(FUN, arg1 = , arg2 = , ...)
    def bar(...):
        ...

1

Yukarıdaki harika yanıtlar. Bu @wraps, doc dizesini ve işlev adını orijinal işlevden alan ve yeni kaydırılan sürüme uygulayan ayrıca şunu gösterir:

from functools import wraps

def decorator_func_with_args(arg1, arg2):
    def decorator(f):
        @wraps(f)
        def wrapper(*args, **kwargs):
            print("Before orginal function with decorator args:", arg1, arg2)
            result = f(*args, **kwargs)
            print("Ran after the orginal function")
            return result
        return wrapper
    return decorator

@decorator_func_with_args("foo", "bar")
def hello(name):
    """A function which prints a greeting to the name provided.
    """
    print('hello ', name)
    return 42

print("Starting script..")
x = hello('Bob')
print("The value of x is:", x)
print("The wrapped functions docstring is:", hello.__doc__)
print("The wrapped functions name is:", hello.__name__)

Baskılar:

Starting script..
Before orginal function with decorator args: foo bar
hello  Bob
Ran after the orginal function
The value of x is: 42
The wrapped functions docstring is: A function which prints a greeting to the name provided.
The wrapped functions name is: hello

0

Hem işlevin hem de dekoratörün argümanlar alması durumunda aşağıdaki yaklaşımı takip edebilirsiniz.

Örneğin, decorator1argüman alan bir dekoratör var

@decorator1(5)
def func1(arg1, arg2):
    print (arg1, arg2)

func1(1, 2)

Şimdi, decorator1bağımsız değişken dinamik olmalı veya işlevi çağırırken iletilmişse,

def func1(arg1, arg2):
    print (arg1, arg2)


a = 1
b = 2
seconds = 10

decorator1(seconds)(func1)(a, b)

Yukarıdaki kodda

  • seconds tartışması decorator1
  • a, b argümanları func1
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.