Dekorator dengan parameter?


401

Saya memiliki masalah dengan transfer variabel 'insurance_mode' oleh dekorator. Saya akan melakukannya dengan pernyataan dekorator berikut:

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

tapi sayangnya, pernyataan ini tidak berfungsi. Mungkin mungkin ada cara yang lebih baik untuk menyelesaikan masalah ini.

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
Contoh Anda tidak valid secara sintaksis. execute_complete_reservationmembutuhkan dua parameter, tetapi Anda melewatkannya satu. Dekorator hanyalah gula sintaksis untuk fungsi pembungkus di dalam fungsi lainnya. Lihat docs.python.org/reference/compound_stmts.html#fungsi untuk dokumentasi lengkap.
Brian Clapper

Jawaban:


687

Sintaksis untuk dekorator dengan argumen sedikit berbeda - dekorator dengan argumen harus mengembalikan fungsi yang akan mengambil fungsi dan mengembalikan fungsi lainnya. Jadi harus benar-benar mengembalikan dekorator yang normal. Agak membingungkan, bukan? Yang saya maksud:

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

Di sini Anda dapat membaca lebih lanjut tentang subjek - juga dimungkinkan untuk menerapkan ini menggunakan objek yang dapat dipanggil dan itu juga dijelaskan di sana.


56
Saya bertanya-tanya mengapa GVR tidak mengimplementasikannya dengan mengirimkan parameter sebagai argumen dekorator berikutnya setelah 'fungsi'. 'Yo dawg aku mendengarmu suka penutupan ...' dan sebagainya.
Michel Müller

3
> Apakah fungsi akan menjadi argumen pertama atau terakhir? Jelas pertama, karena parameter adalah daftar parameter dengan panjang variabel. > Ini juga aneh bahwa Anda akan "memanggil" fungsi dengan tanda tangan yang berbeda dari yang ada di definisi. Seperti yang Anda tunjukkan, itu akan sangat cocok sebenarnya - ini cukup analog dengan bagaimana metode kelas dipanggil. Untuk membuatnya lebih jelas, Anda dapat memiliki sesuatu seperti dekorator (self_func, param1, ...). Tetapi perhatikan: Saya tidak menganjurkan untuk perubahan apa pun di sini, Python terlalu jauh untuk itu dan kita bisa melihat bagaimana pemecahan perubahan berhasil ..
Michel Müller

21
Anda lupa functools.wraps SANGAT BERMANFAAT untuk menghias bungkus :)
socketpair

10
Anda lupa tentang kembali saat memanggil fungsi, yaitu return function(*args, **kwargs)
formiaczek

36
Mungkin jelas, tetapi untuk berjaga-jaga: Anda harus menggunakan dekorator ini sebagai @decorator()dan bukan hanya @decorator, bahkan jika Anda hanya memiliki argumen opsional.
Patrick Mevzek

327

Sunting : untuk pemahaman mendalam tentang model mental dekorator, lihat Pycon Talk yang mengagumkan ini . layak 30 menit.

Salah satu cara berpikir tentang dekorator dengan argumen adalah

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

diterjemahkan menjadi

foo = decorator(foo)

Jadi jika dekorator memiliki argumen,

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

diterjemahkan menjadi

foo = decorator_with_args(arg)(foo)

decorator_with_args adalah fungsi yang menerima argumen khusus dan yang mengembalikan dekorator yang sebenarnya (yang akan diterapkan ke fungsi yang didekorasi).

Saya menggunakan trik sederhana dengan parsial untuk membuat dekorator saya mudah

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

Memperbarui:

Di atas, foomenjadireal_decorator(foo)

Salah satu efek dari mendekorasi suatu fungsi adalah bahwa namanya fooditimpa pada deklarasi dekorator. foo"diganti" oleh apa pun yang dikembalikan oleh real_decorator. Dalam hal ini, objek fungsi baru.

Semua foometadata diganti, terutama nama fungsi dan docstring.

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

functools.wraps memberi kita metode yang mudah untuk "mengangkat" docstring dan nama ke fungsi yang dikembalikan.

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
Jawaban Anda dengan sempurna menjelaskan ortogonalitas yang melekat dari dekorator, terima kasih
zsf222

Bisakah Anda menambahkan @functools.wraps?
Mr_and_Mrs_D

1
@ Mr_and_Mrs_D, saya telah memperbarui posting dengan contoh dengan functool.wraps. Menambahkannya dalam contoh dapat membingungkan pembaca lebih lanjut.
srj

7
Ada apa di argsini !?
displayname

1
Bagaimana Anda meneruskan argumen yang diteruskan ke barargumen real_decorator?
Chang Zhao

85

Saya ingin menunjukkan ide yang IMHO cukup elegan. Solusi yang diajukan oleh t.dubrownik menunjukkan pola yang selalu sama: Anda membutuhkan pembungkus tiga lapis terlepas dari apa yang dilakukan dekorator.

Jadi saya pikir ini adalah pekerjaan untuk dekorator meta, yaitu dekorator untuk dekorator. Karena dekorator adalah fungsi, sebenarnya dekorator berfungsi sebagai dekorator biasa dengan argumen:

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

Ini dapat diterapkan pada dekorator biasa untuk menambah parameter. Jadi misalnya, misalkan kita memiliki dekorator yang menggandakan hasil fungsi:

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)

Dengan @parametrizedkita dapat membangun @multiplydekorator generik yang memiliki parameter

@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)

Secara konvensional parameter pertama dari dekorator parametrized adalah fungsinya, sedangkan argumen yang tersisa akan sesuai dengan parameter dekorator parametrized.

Contoh penggunaan yang menarik bisa menjadi dekorator asertif tipe-aman:

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

Catatan terakhir: di sini saya tidak menggunakan functools.wrapsuntuk fungsi wrapper, tapi saya akan merekomendasikan menggunakannya setiap saat.


3
Tidak menggunakan ini persis, tetapi membantu saya memahami konsep :) Terima kasih!
mouckatron

Saya mencoba ini dan memiliki beberapa masalah .
Jeff

@ Jeff dapatkah Anda berbagi dengan kami jenis masalah yang Anda miliki?
Dacav

Saya mengaitkannya dengan pertanyaan saya, dan saya mengetahuinya ... Saya perlu menghubungi @wrapssaya untuk kasus khusus saya.
Jeff

4
Oh nak, aku kehilangan satu hari penuh karena ini. Untungnya, saya menemukan jawaban ini (yang notabene bisa menjadi jawaban terbaik yang pernah dibuat di seluruh internet). Mereka juga menggunakan @parametrizedtrik Anda . Masalah yang saya miliki adalah saya lupa @sintaksnya sama dengan panggilan sebenarnya (entah bagaimana saya tahu itu dan tidak tahu bahwa pada saat yang sama Anda dapat mengumpulkan dari pertanyaan saya). Jadi, jika Anda ingin menerjemahkan @sintaks menjadi panggilan biasa untuk memeriksa cara kerjanya, Anda lebih baik mengomentariya sementara atau Anda akhirnya akan memanggilnya dua kali dan mendapatkan hasil
mumbojumbo

79

Ini adalah versi yang sedikit dimodifikasi dari jawaban t.dubrownik . Mengapa?

  1. Sebagai templat umum, Anda harus mengembalikan nilai pengembalian dari fungsi aslinya.
  2. Ini mengubah nama fungsi, yang dapat mempengaruhi dekorator / kode lainnya.

Jadi gunakan @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

Saya kira masalah Anda adalah memberikan argumen kepada dekorator Anda. Ini sedikit rumit dan tidak langsung.

Berikut ini contoh cara melakukannya:

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')

Cetakan:

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

Lihat artikel Bruce Eckel untuk lebih jelasnya.


20
Waspadalah terhadap kelas dekorator. Mereka tidak bekerja pada metode kecuali Anda secara manual menemukan kembali logika deskriptor instancemethod.

9
delnan, peduli untuk menjelaskan? Saya hanya perlu menggunakan pola ini satu kali, jadi saya belum menemukan salah satu dari perangkap itu.
Ross Rogers

2
@RossRogers Dugaan saya adalah bahwa @delnan mengacu pada hal-hal seperti __name__yang tidak dimiliki instance dari kelas dekorator?
jamesc

9
@ Jamesames Itu juga, meskipun itu relatif mudah dipecahkan. Kasus spesifik yang saya maksudkan adalah class Foo: @MyDec(...) def method(self, ...): blahyang tidak berfungsi karena Foo().methodtidak akan menjadi metode terikat dan tidak akan lulus selfsecara otomatis. Ini juga dapat diperbaiki, dengan membuat MyDecdeskriptor dan membuat metode terikat __get__, tetapi lebih terlibat dan jauh lebih jelas. Pada akhirnya, kelas dekorator tidak senyaman yang terlihat.

2
@delnan Saya ingin melihat peringatan ini tampil lebih menonjol. Saya memukulnya dan tertarik melihat solusi yang TIDAK BERLAKU (lebih banyak melibatkan yang kurang jelas meskipun mungkin).
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

Penggunaan dekorator

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

Lalu

adder(2,3)

menghasilkan

10

tapi

adder('hi',3)

menghasilkan

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

Ini adalah templat untuk dekorator fungsi yang tidak memerlukan ()jika tidak ada parameter yang diberikan:

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

contoh dari ini diberikan di bawah ini:

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

Perhatikan juga bahwa factor_or_func(atau parameter lainnya) tidak boleh dipindahkan ke wrapper().
norok2

1
Mengapa Anda harus check-in locals()?
Shital Shah

@ ShitalShah yang mencakup kasing di mana dekorator digunakan tanpa ().
norok2

4

Dalam contoh saya, saya memutuskan untuk menyelesaikan ini melalui lambda satu baris untuk membuat fungsi dekorator baru:

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()

Ketika dieksekusi, ini mencetak:

Finished!
All Done!

Mungkin tidak bisa diperluas seperti solusi lain, tetapi berhasil untuk saya.


Ini bekerja. Meskipun ya, ini membuatnya sulit untuk mengatur nilai ke dekorator.
Arindam Roychowdhury

3

Menulis dekorator yang berfungsi dengan dan tanpa parameter adalah tantangan karena Python mengharapkan perilaku yang sama sekali berbeda dalam dua kasus ini! Banyak jawaban telah mencoba untuk mengatasi ini dan di bawah ini merupakan peningkatan dari jawaban oleh @ norok2. Secara khusus, variasi ini menghilangkan penggunaan locals().

Berikut contoh yang sama seperti yang diberikan oleh @ norok2:

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

Mainkan dengan kode ini .

Tangkapannya adalah bahwa pengguna harus menyediakan kunci, nilai pasangan parameter, bukan parameter posisi dan parameter pertama dicadangkan.


2

Sudah diketahui umum bahwa dua bagian kode berikut ini hampir setara:

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

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

Kesalahan umum adalah berpikir bahwa @hanya menyembunyikan argumen paling kiri.

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

Akan jauh lebih mudah untuk menulis dekorator jika di atas adalah cara @kerjanya. Sayangnya, bukan itu yang dilakukan.


Pertimbangkan dekorator Waityang menghambat eksekusi program selama beberapa detik. Jika Anda tidak melewatkan waktu tunggu maka nilai default adalah 1 detik. Kasus penggunaan ditunjukkan di bawah ini.

##################################################
@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)

Ketika Waitmemiliki argumen, seperti @Wait(3), maka panggilan Wait(3) dieksekusi sebelum hal lain terjadi.

Artinya, dua potong kode berikut ini setara

@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)

Ini adalah sebuah masalah.

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

Satu solusi ditunjukkan di bawah ini:

Mari kita mulai dengan membuat kelas berikut, 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)

Sekarang kita dapat menulis hal-hal seperti:

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

Perhatikan bahwa:

  • dec tidak menerima banyak argumen.
  • dec hanya menerima fungsi yang akan dibungkus.

    import inspect class PolyArgDecoratorMeta (type): def call (Tunggu, * args, ** kwargs): coba: arg_count = len (args) if (arg_count == 1): jika dapat dipanggil (args [0]): SuperClass = inspect. getmro (PolyArgDecoratorMeta) [1] r = SuperClass. panggil (Tunggu, args [0]) lain: r = DelayedDecorator (Tunggu, * args, ** kwargs) else: r = DelayedDecorator (Tunggu, * args, ** kwargs) akhirnya: pass return r

    import time class Tunggu (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 

Dua potong kode berikut ini setara:

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

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

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

Kami dapat mencetak "something"ke konsol dengan sangat lambat, sebagai berikut:

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")

Catatan Akhir

Ini mungkin terlihat seperti banyak kode, tetapi Anda tidak harus menulis kelas DelayedDecoratordan PolyArgDecoratorMetasetiap waktu. Satu-satunya kode yang harus Anda tulis sendiri adalah sebagai berikut, yang cukup singkat:

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

tentukan "fungsi penghias ini" untuk menghasilkan fungsi penghias yang disesuaikan:

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

gunakan seperti ini:

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

1

Jawaban bagus di atas. Yang ini juga menggambarkan @wraps, yang mengambil string doc dan nama fungsi dari fungsi asli dan menerapkannya ke versi terbungkus baru:

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__)

Cetakan:

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

Jika fungsi dan dekorator harus mengambil argumen, Anda dapat mengikuti pendekatan di bawah ini.

Misalnya ada dekorator bernama decorator1yang mengambil argumen

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

func1(1, 2)

Sekarang jika decorator1argumen harus dinamis, atau diteruskan saat memanggil fungsi,

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


a = 1
b = 2
seconds = 10

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

Dalam kode di atas

  • seconds adalah argumen untuk decorator1
  • a, b adalah argumen dari func1
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.