Trang trí với thông số?


401

Tôi gặp vấn đề với việc chuyển biến 'bảo hiểm_mode' của người trang trí. Tôi sẽ làm điều đó bằng tuyên bố trang trí sau đây:

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

nhưng thật không may, tuyên bố này không hoạt động. Có lẽ có cách nào tốt hơn để giải quyết vấn đề này.

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
Ví dụ của bạn không hợp lệ về mặt cú pháp. execute_complete_reservationmất hai tham số, nhưng bạn truyền nó một. Trang trí chỉ là đường cú pháp cho các chức năng gói bên trong các chức năng khác. Xem docs.python.org/reference/compound_stmts.html#feft để biết tài liệu đầy đủ.
Brian Clapper

Câu trả lời:


687

Cú pháp cho các trình trang trí với các đối số là một chút khác nhau - trình trang trí với các đối số sẽ trả về một hàm sẽ lấy một hàm và trả về một hàm khác. Vì vậy, nó thực sự nên trả lại một trang trí bình thường. Một chút khó hiểu, phải không? Ý của tôi là:

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

Ở đây bạn có thể đọc thêm về chủ đề - cũng có thể thực hiện điều này bằng cách sử dụng các đối tượng có thể gọi được và điều đó cũng được giải thích ở đó.


56
Tôi tự hỏi tại sao GVR không thực hiện nó bằng cách chuyển các tham số làm đối số trang trí tiếp theo sau 'hàm'. 'Yo dawg Tôi nghe nói bạn thích đóng cửa ...' vân vân.
Michel Müller

3
> Chức năng sẽ là đối số đầu tiên hay cuối cùng? Rõ ràng đầu tiên, vì các tham số là một danh sách tham số có độ dài thay đổi. > Thật kỳ lạ khi bạn "gọi" hàm với một chữ ký khác với chữ trong định nghĩa. Như bạn chỉ ra, nó thực sự sẽ phù hợp khá tốt - nó khá giống với cách gọi một phương thức lớp. Để làm cho nó rõ ràng hơn, bạn có thể có một cái gì đó như quy ước trang trí (self_func, param1, ...). Nhưng lưu ý: Tôi không ủng hộ bất kỳ thay đổi nào ở đây, Python ở quá xa cho điều đó và chúng ta có thể thấy các thay đổi đã được giải quyết như thế nào ..
Michel Müller

21
bạn đã quên RẤT
NHIỀU funcools.wraps

10
Bạn đã quên trả về khi gọi hàm, tức là return function(*args, **kwargs)
formiaczek

36
Có thể rõ ràng, nhưng chỉ trong trường hợp: bạn cần sử dụng trang trí này như @decorator()và không chỉ @decorator, ngay cả khi bạn chỉ có các đối số tùy chọn.
Patrick Mevzek

326

Chỉnh sửa : để hiểu sâu hơn về mô hình tinh thần của các nhà trang trí, hãy xem Pycon Talk tuyệt vời này . cũng đáng trong 30 phút.

Một cách nghĩ về trang trí với các đối số là

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

Dịch sang

foo = decorator(foo)

Vì vậy, nếu người trang trí có tranh luận,

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

Dịch sang

foo = decorator_with_args(arg)(foo)

decorator_with_args là một hàm chấp nhận một đối số tùy chỉnh và trả về trình trang trí thực tế (sẽ được áp dụng cho hàm được trang trí).

Tôi sử dụng một mẹo đơn giản với các hạt để làm cho trang trí của tôi dễ dàng

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

Cập nhật:

Ở trên, footrở thànhreal_decorator(foo)

Một hiệu ứng của việc trang trí một chức năng là tên foođược ghi đè khi khai báo trang trí. foobị "ghi đè" bởi bất cứ điều gì được trả về real_decorator. Trong trường hợp này, một đối tượng chức năng mới.

Tất cả foosiêu dữ liệu bị ghi đè, đáng chú ý là chuỗi hàm và tên hàm.

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

funcools.wraps cung cấp cho chúng ta một phương thức thuận tiện để "nâng" chuỗi doc và tên lên hàm trả về.

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
Câu trả lời của bạn đã giải thích hoàn hảo tính trực giao vốn có của người trang trí, cảm ơn bạn
zsf222

Bạn có thể thêm @functools.wraps?
Mr_and_Mrs_D

1
@Mr_and_Mrs_D, tôi đã cập nhật bài đăng với một ví dụ với functool.wraps. Thêm nó trong ví dụ có thể gây nhầm lẫn cho độc giả hơn nữa.
srj

7
Cái gì argđây!?
hiển thị

1
Làm thế nào bạn sẽ vượt qua đối số được truyền cho barđối số real_decorator?
Chang Zhao

85

Tôi muốn thể hiện một ý tưởng IMHO khá thanh lịch. Giải pháp được đề xuất bởi t.dubrownik cho thấy một mô hình luôn giống nhau: bạn cần trình bao bọc ba lớp bất kể người trang trí làm gì.

Vì vậy, tôi nghĩ rằng đây là một công việc cho một nhà trang trí meta, nghĩa là một người trang trí cho các nhà trang trí. Là một trình trang trí là một chức năng, nó thực sự hoạt động như một trình trang trí thông thường với các đối số:

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

Điều này có thể được áp dụng cho một trang trí thông thường để thêm các tham số. Ví dụ, giả sử chúng ta có trình trang trí nhân đôi kết quả của hàm:

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)

Với @parametrizedchúng ta có thể xây dựng một @multiplytrang trí chung có một tham số

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

Thông thường, tham số đầu tiên của trình trang trí tham số là hàm, trong khi các đối số còn lại sẽ tương ứng với tham số của trình trang trí tham số.

Một ví dụ sử dụng thú vị có thể là một trang trí quyết đoán loại an toàn:

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

Lưu ý cuối cùng: ở đây tôi không sử dụng functools.wrapscho các hàm bao bọc, nhưng tôi sẽ khuyên bạn nên sử dụng nó mọi lúc.


3
Không sử dụng chính xác điều này, nhưng đã giúp tôi hiểu được khái niệm này :) Cảm ơn!
mouckatron

Tôi đã thử điều này và có một số vấn đề .
Jeff

@Jeff bạn có thể chia sẻ với chúng tôi loại vấn đề bạn gặp phải không?
Dacav

Tôi đã có nó liên kết với câu hỏi của tôi, và tôi đã tìm ra nó ... Tôi cần phải gọi @wrapscho tôi cho trường hợp cụ thể của tôi.
Jeff

4
Oh boy, tôi đã mất cả một ngày về điều này. Rất may, tôi đã tìm ra câu trả lời này (tình cờ có thể là câu trả lời hay nhất từng được tạo ra trên toàn bộ internet). Họ cũng sử dụng @parametrizedmánh khóe của bạn . Vấn đề tôi gặp phải là tôi đã quên @cú pháp bằng với các cuộc gọi thực tế (bằng cách nào đó tôi biết điều đó và không biết điều đó cùng lúc với bạn có thể thu thập từ câu hỏi của tôi). Vì vậy, nếu bạn muốn dịch @cú pháp thành các cuộc gọi trần tục để kiểm tra cách thức hoạt động của nó, trước tiên bạn nên nhận xét nó hoặc cuối cùng bạn gọi nó hai lần và nhận kết quả
mumbojumbo

79

Đây là một phiên bản sửa đổi một chút của câu trả lời của t.dubrownik . Tại sao?

  1. Là một mẫu chung, bạn nên trả về giá trị trả về từ hàm ban đầu.
  2. Điều này thay đổi tên của hàm, có thể ảnh hưởng đến các mã trang trí / mã khác.

Vì vậy, sử dụng @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

Tôi đoán vấn đề của bạn là truyền các đối số cho trang trí của bạn. Đây là một chút khó khăn và không đơn giản.

Đây là một ví dụ về cách làm điều này:

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

Bản in:

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

Xem bài viết của Bruce Eckel để biết thêm chi tiết.


20
Coi chừng các lớp trang trí. Chúng không hoạt động trên các phương thức trừ khi bạn phát minh lại logic của các mô tả instancemethod.

9
del Nam, quan tâm đến công phu? Tôi chỉ phải sử dụng mô hình này một lần, vì vậy tôi chưa gặp phải bất kỳ cạm bẫy nào.
Ross Rogers

2
@RossRogers Tôi đoán là @delnan đang đề cập đến những thứ như __name__thể hiện của lớp trang trí sẽ không có?
jamesc

9
@jamesc Điều đó cũng vậy, mặc dù điều đó tương đối dễ giải quyết. Trường hợp cụ thể mà tôi đã đề cập là class Foo: @MyDec(...) def method(self, ...): blahkhông hoạt động vì Foo().methodsẽ không phải là một phương pháp ràng buộc và sẽ không selftự động vượt qua . Điều này cũng có thể được sửa chữa, bằng cách tạo MyDecmột mô tả và tạo các phương thức ràng buộc __get__, nhưng nó liên quan nhiều hơn và ít rõ ràng hơn. Cuối cùng, các lớp trang trí không thuận tiện như chúng có vẻ.

2
@delnan Tôi muốn thấy cảnh báo này nổi bật hơn. Tôi đang nhấn nó và quan tâm đến việc xem một giải pháp mà DOES hoạt động (liên quan nhiều hơn đến một điều ít rõ ràng hơn mặc dù nó có thể).
HaPsantran 13/03/2016

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

Cách sử dụng của trang trí

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

Sau đó

adder(2,3)

sản xuất

10

nhưng

adder('hi',3)

sản xuất

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

Đây là một mẫu cho một trình trang trí chức năng không yêu cầu ()nếu không có tham số nào được đưa ra:

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

một ví dụ về điều này được đưa ra dưới đây:

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

Cũng lưu ý rằng factor_or_func(hoặc bất kỳ thông số khác) không bao giờ nên được bố tríwrapper().
norok2

Tại sao bạn cần kiểm tra locals()?
Shital Shah

@ShitalShah bao gồm trường hợp sử dụng trang trí mà không có ().
norok2

4

Trong trường hợp của tôi, tôi quyết định giải quyết vấn đề này thông qua lambda một dòng để tạo chức năng trang trí mới:

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

Khi được thực thi, bản in này:

Finished!
All Done!

Có lẽ không thể mở rộng như các giải pháp khác, nhưng làm việc cho tôi.


Những công việc này. Mặc dù có, điều này làm cho việc đặt giá trị cho trang trí trở nên khó khăn.
Arindam Roychowdhury

3

Viết một trình trang trí hoạt động có và không có tham số là một thách thức vì Python mong đợi hành vi hoàn toàn khác nhau trong hai trường hợp này! Nhiều câu trả lời đã cố gắng giải quyết vấn đề này và dưới đây là sự cải thiện câu trả lời của @ norok2. Cụ thể, biến thể này giúp loại bỏ việc sử dụng locals().

Theo ví dụ tương tự như được đưa ra bởi @ 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

Chơi với mã này .

Điều hấp dẫn là người dùng phải cung cấp khóa, cặp giá trị của tham số thay vì tham số vị trí và tham số đầu tiên được bảo lưu.


2

Người ta biết rằng hai đoạn mã sau gần như tương đương nhau:

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

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

Một sai lầm phổ biến là nghĩ rằng @chỉ cần che giấu đối số ngoài cùng bên trái.

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

Sẽ dễ dàng hơn nhiều để viết trang trí nếu ở trên là cách @làm việc. Thật không may, đó không phải là cách mọi thứ được thực hiện.


Hãy xem xét một trình trang trí Waitgiúp thực hiện chương trình trong vài giây. Nếu bạn không vượt qua trong thời gian chờ thì giá trị mặc định là 1 giây. Các trường hợp sử dụng được hiển thị dưới đây.

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

Khi Waitcó một đối số, chẳng hạn như @Wait(3), thì cuộc gọi Wait(3) được thực hiện trước khi bất kỳ điều gì khác xảy ra.

Đó là, hai đoạn mã sau là tương đương

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

Đây là một vấn đề.

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

Một giải pháp được hiển thị dưới đây:

Hãy bắt đầu bằng cách tạo lớp sau 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)

Bây giờ chúng ta có thể viết những thứ như:

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

Lưu ý rằng:

  • dec không chấp nhận nhiều đối số.
  • dec chỉ chấp nhận chức năng được bọc.

    nhập lớp kiểm tra PolyArgDecoratorMeta (loại): def call (Wait, * args, ** kwargs): try: arg_count = len (args) if (arg_count == 1): if callable (args [0]): SuperClass = tests. getmro (PolyArgDecoratorMeta) [1] r = SuperClass. gọi (Wait, args [0]) other: r = DelayedDecorator (Wait, * args, ** kwargs) other: r = DelayedDecorator (Wait, * args, ** kwargs) cuối cùng: pass return r

    lớp thời gian nhập Chờ (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 

Hai đoạn mã sau là tương đương:

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

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

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

Chúng ta có thể in "something"ra bàn điều khiển rất chậm, như sau:

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

Ghi chú cuối cùng

Nó có thể trông giống như rất nhiều mã, nhưng bạn không phải viết các lớp DelayedDecoratorPolyArgDecoratorMetamọi lúc. Mã duy nhất bạn phải đích thân viết một cái gì đó như sau, khá ngắn:

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

định nghĩa "chức năng trang trí" này để tạo chức năng trang trí tùy chỉnh:

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

sử dụng nó theo cách này:

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

1

Câu trả lời tuyệt vời ở trên. Cái này cũng minh họa @wraps, lấy chuỗi doc và tên hàm từ hàm ban đầu và áp dụng nó cho phiên bản được gói mới:

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

Bản in:

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

Trong trường hợp cả hàm và trình trang trí phải lấy các đối số, bạn có thể làm theo cách tiếp cận dưới đây.

Ví dụ, có một trang trí có tên decorator1lấy một đối số

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

func1(1, 2)

Bây giờ nếu decorator1 đối số phải là động hoặc được truyền trong khi gọi hàm,

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


a = 1
b = 2
seconds = 10

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

Trong đoạn mã trên

  • seconds là đối số cho decorator1
  • a, b là những lập luận của func1
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.