Bagaimana cara menyampaikan argumen tambahan ke dekorator Python?

100

Saya memiliki dekorator seperti di bawah ini.

def myDecorator(test_func):
    return callSomeWrapper(test_func)
def callSomeWrapper(test_func):
    return test_func
@myDecorator
def someFunc():
    print 'hello'

Saya ingin meningkatkan dekorator ini untuk menerima argumen lain seperti di bawah ini

def myDecorator(test_func,logIt):
    if logIt:
        print "Calling Function: " + test_func.__name__
    return callSomeWrapper(test_func)
@myDecorator(False)
def someFunc():
    print 'Hello'

Tetapi kode ini memberikan kesalahan,

TypeError: myDecorator () mengambil tepat 2 argumen (1 diberikan)

Mengapa fungsinya tidak otomatis diteruskan? Bagaimana cara meneruskan fungsi secara eksplisit ke fungsi dekorator?

balki
sumber
3
balki: harap hindari menggunakan boolean sebagai argumen Anda, ini bukan pendekatan gd dan kurangi keterbacaan kode
Kit Ho
8
@KitHo - ini adalah bendera boolean, jadi menggunakan nilai boolean adalah pendekatan yang tepat.
AKX
2
@Hmps - apa itu "gd"? Apakah itu "bagus"?
Rob Bednark

Jawaban:

173

Karena Anda memanggil dekorator seperti sebuah fungsi, ia perlu mengembalikan fungsi lain yang merupakan dekorator sebenarnya:

def my_decorator(param):
    def actual_decorator(func):
        print("Decorating function {}, with parameter {}".format(func.__name__, param))
        return function_wrapper(func)  # assume we defined a wrapper somewhere
    return actual_decorator

Fungsi luar akan diberi argumen apa pun yang Anda teruskan secara eksplisit, dan harus mengembalikan fungsi dalam. Fungsi bagian dalam akan diberikan fungsi untuk menghias, dan mengembalikan fungsi yang dimodifikasi.

Biasanya Anda ingin dekorator mengubah perilaku fungsi dengan membungkusnya dalam fungsi pembungkus. Berikut adalah contoh yang secara opsional menambahkan logging saat fungsinya dipanggil:

def log_decorator(log_enabled):
    def actual_decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            if log_enabled:
                print("Calling Function: " + func.__name__)
            return func(*args, **kwargs)
        return wrapper
    return actual_decorator

Itu functools.wraps panggilan salinan hal-hal seperti nama dan docstring untuk fungsi wrapper, untuk membuatnya lebih mirip dengan fungsi asli.

Contoh penggunaan:

>>> @log_decorator(True)
... def f(x):
...     return x+1
...
>>> f(4)
Calling Function: f
5
interjay
sumber
11
Dan penggunaan functools.wrapsdisarankan - ini mempertahankan nama asli, docstring, dll. Dari fungsi yang dibungkus.
AKX
@ AKX: Terima kasih, saya menambahkan ini ke contoh kedua.
interjay
1
Jadi pada dasarnya dekorator selalu hanya membutuhkan satu argumen yang merupakan fungsinya. Tapi dekorator bisa menjadi nilai kembali dari suatu fungsi yang mungkin membutuhkan argumen. Apakah ini benar?
balki
2
@ Balki: Ya, itu benar. Yang membingungkan adalah banyak orang juga menyebut fungsi luar (di myDecoratorsini) sebagai dekorator. Ini nyaman bagi pengguna dekorator, tetapi bisa membingungkan saat Anda mencoba menulisnya.
interjay
1
Detail kecil yang membingungkan saya: jika Anda log_decoratormengambil argumen default, Anda tidak dapat menggunakannya @log_decorator, itu pasti@log_decorator()
Stardidi
46

Hanya untuk memberikan sudut pandang yang berbeda: sintaks

@expr
def func(...): #stuff

setara dengan

def func(...): #stuff
func = expr(func)

Secara khusus, exprbisa apa saja yang Anda suka, asalkan terevaluasi menjadi callable. Secara khusus , exprdapat menjadi pabrik dekorator: Anda memberikan beberapa parameter dan itu memberi Anda dekorator. Jadi mungkin cara yang lebih baik untuk memahami situasi Anda adalah sebagai

dec = decorator_factory(*args)
@dec
def func(...):

yang kemudian dapat disingkat menjadi

@decorator_factory(*args)
def func(...):

Tentu saja, karena terlihat seperti decorator_factorydekorator, orang cenderung menamakannya untuk mencerminkan hal itu. Yang bisa membingungkan saat Anda mencoba mengikuti tingkat tipuan.

Katriel
sumber
Terima kasih, ini benar-benar membantu saya memahami alasan di balik apa yang terjadi.
Aditya Sriram
26

Hanya ingin menambahkan beberapa trik berguna yang memungkinkan argumen dekorator menjadi opsional. Ini juga akan memungkinkan untuk menggunakan kembali dekorator dan mengurangi sarang

import functools

def myDecorator(test_func=None,logIt=None):
    if not test_func:
        return functools.partial(myDecorator, logIt=logIt)
    @functools.wraps(test_func)
    def f(*args, **kwargs):
        if logIt==1:
            print 'Logging level 1 for {}'.format(test_func.__name__)
        if logIt==2:
            print 'Logging level 2 for {}'.format(test_func.__name__)
        return test_func(*args, **kwargs)
    return f

#new decorator 
myDecorator_2 = myDecorator(logIt=2)

@myDecorator(logIt=2)
def pow2(i):
    return i**2

@myDecorator
def pow3(i):
    return i**3

@myDecorator_2
def pow4(i):
    return i**4

print pow2(2)
print pow3(2)
print pow4(2)
Alexey Smirnov
sumber
16

Hanya cara lain melakukan dekorator. Saya menemukan cara ini yang paling mudah untuk membungkus kepala saya.

class NiceDecorator:
    def __init__(self, param_foo='a', param_bar='b'):
        self.param_foo = param_foo
        self.param_bar = param_bar

    def __call__(self, func):
        def my_logic(*args, **kwargs):
            # whatever logic your decorator is supposed to implement goes in here
            print('pre action baz')
            print(self.param_bar)
            # including the call to the decorated function (if you want to do that)
            result = func(*args, **kwargs)
            print('post action beep')
            return result

        return my_logic

# usage example from here on
@NiceDecorator(param_bar='baaar')
def example():
    print('example yay')


example()
Robert Fey
sumber
Terima kasih! telah melihat beberapa "solusi" yang mencengangkan selama sekitar 30 menit dan ini adalah yang pertama yang benar-benar masuk akal.
canhazbits
0

Sekarang jika Anda ingin memanggil fungsi function1dengan dekorator decorator_with_argdan dalam hal ini baik fungsi dan dekorator mengambil argumen,

def function1(a, b):
    print (a, b)

decorator_with_arg(10)(function1)(1, 2)
SuperNova
sumber