Làm cách nào để chuyển các đối số bổ sung cho trình trang trí Python?


99

Tôi có một người trang trí như dưới đây.

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

Tôi muốn nâng cao trình trang trí này để chấp nhận một lập luận khác như bên dưới

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

Nhưng mã này gây ra lỗi,

TypeError: myDecorator () nhận đúng 2 đối số (1 đối số đã cho)

Tại sao hàm không được tự động chuyển qua? Làm cách nào để chuyển một cách rõ ràng hàm cho hàm decorator?


3
Balki: hãy tránh sử dụng boolean như là đối số của bạn, nó không phải là một cách tiếp cận gd và giảm readliability mã của
Kit Hồ

8
@KitHo - đó là cờ boolean, vì vậy sử dụng giá trị boolean là cách tiếp cận phù hợp.
AKX

2
@KitHo - "gd" là gì? Liệu nó có tốt không"?
Rob Bednark

Câu trả lời:


172

Vì bạn đang gọi trình trang trí giống như một hàm, nó cần trả về một hàm khác là trình trang trí thực tế:

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

Hàm bên ngoài sẽ được cung cấp bất kỳ đối số nào bạn truyền một cách rõ ràng và sẽ trả về hàm bên trong. Hàm bên trong sẽ được chuyển sang hàm để trang trí, và trả về hàm đã sửa đổi.

Thông thường, bạn muốn trình trang trí thay đổi hành vi của hàm bằng cách gói nó trong một hàm trình bao bọc. Đây là một ví dụ tùy chọn thêm ghi nhật ký khi hàm được gọi:

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

Lệnh functools.wrapsgọi sao chép những thứ như tên và docstring vào hàm wrapper, để làm cho nó giống với hàm gốc hơn.

Ví dụ sử dụng:

>>> @log_decorator(True)
... def f(x):
...     return x+1
...
>>> f(4)
Calling Function: f
5

11
Và việc sử dụng functools.wrapsđược khuyến khích - nó giữ lại tên ban đầu, docstring, v.v. của hàm được bọc.
AKX

@AKX: Cảm ơn, tôi đã thêm điều này vào ví dụ thứ hai.
interjay

1
Vì vậy, về cơ bản decorator luôn chỉ nhận một đối số là hàm. Nhưng decorator có thể là giá trị trả về của một hàm có thể nhận đối số. Điều này có chính xác?
balki

2
@balki: Vâng, chính xác. Điều gây nhầm lẫn là nhiều người cũng sẽ gọi chức năng bên ngoài ( myDecoratorở đây) là trình trang trí. Điều này thuận tiện cho người sử dụng trình trang trí, nhưng có thể gây nhầm lẫn khi bạn đang cố gắng viết một trang.
giữa các

1
Chi tiết nhỏ khiến tôi bối rối: nếu bạn log_decoratornhận một đối số mặc định, bạn không thể sử dụng @log_decorator, nó phải là@log_decorator()
Stardidi

46

Chỉ để cung cấp một quan điểm khác: cú pháp

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

tương đương với

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

Cụ thể, exprcó thể là bất cứ thứ gì bạn thích, miễn là nó được đánh giá là có thể gọi được. Trong đó đặc biệt, exprcó thể là một nhà máy trang trí: bạn cung cấp cho nó một số thông số và nó mang lại cho bạn một trang trí. Vì vậy, có thể cách tốt hơn để hiểu tình huống của bạn là

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

sau đó có thể được rút ngắn thành

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

Tất nhiên, vì nó trông giống như decorator_factorymột người trang trí, mọi người có xu hướng đặt tên nó để phản ánh điều đó. Điều này có thể gây nhầm lẫn khi bạn cố gắng làm theo các cấp độ chuyển hướng.


Cảm ơn, điều này thực sự đã giúp tôi hiểu được lý do đằng sau những gì đang xảy ra.
Aditya Sriram

25

Chỉ muốn thêm một số thủ thuật hữu ích sẽ cho phép làm cho các đối số trang trí là tùy chọn. Nó cũng sẽ cho phép sử dụng lại đồ trang trí và giảm sự lồng vào nhau

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)

16

Chỉ là một cách khác để làm trang trí. Tôi thấy cách này dễ nhất để quấn quanh đầu.

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

cảm ơn bạn! đã xem xét một số "giải pháp" uốn nắn tâm trí trong khoảng 30 phút và đây là giải pháp đầu tiên thực sự có ý nghĩa.
canhazbits

0

Bây giờ nếu bạn muốn gọi một hàm function1với trình trang trí decorator_with_argvà trong trường hợp này, cả hàm và trình trang trí đều nhận đối số,

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

decorator_with_arg(10)(function1)(1, 2)
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.