Làm thế nào để tạo trình trang trí Python có thể được sử dụng có hoặc không có tham số?


89

Tôi muốn tạo một trình trang trí Python có thể được sử dụng với các tham số:

@redirect_output("somewhere.log")
def foo():
    ....

hoặc không có chúng (ví dụ: chuyển hướng đầu ra sang stderr theo mặc định):

@redirect_output
def foo():
    ....

Điều đó là có thể?

Lưu ý rằng tôi không tìm kiếm một giải pháp khác cho vấn đề chuyển hướng đầu ra, đó chỉ là một ví dụ về cú pháp mà tôi muốn đạt được.


Giao diện mặc định @redirect_outputlà không nhiều thông tin. Tôi cho rằng đó là một ý tưởng tồi. Sử dụng mẫu đầu tiên và đơn giản hóa cuộc sống của bạn rất nhiều.
S.Lott

mặc dù vậy, câu hỏi thú vị - cho đến khi tôi nhìn thấy nó và xem qua tài liệu, tôi đã cho rằng @f giống với @f () và tôi vẫn nghĩ nó phải như vậy, thành thật mà nói (mọi đối số được cung cấp sẽ chỉ được giải quyết vào đối số hàm)
rog

Câu trả lời:


64

Tôi biết câu hỏi này đã cũ, nhưng một số nhận xét là mới, và trong khi tất cả các giải pháp khả thi về cơ bản đều giống nhau, hầu hết chúng đều không rõ ràng hoặc dễ đọc.

Giống như câu trả lời của thobe đã nói, cách duy nhất để xử lý cả hai trường hợp là kiểm tra cả hai tình huống. Cách đơn giản nhất là kiểm tra xem có một đối số duy nhất và nó có phải là callabe hay không (LƯU Ý: sẽ cần kiểm tra thêm nếu trình trang trí của bạn chỉ nhận 1 đối số và nó là một đối tượng có thể gọi):

def decorator(*args, **kwargs):
    if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):
        # called as @decorator
    else:
        # called as @decorator(*args, **kwargs)

Trong trường hợp đầu tiên, bạn làm những gì mà bất kỳ trình trang trí bình thường nào làm, trả về phiên bản đã sửa đổi hoặc gói của hàm được truyền vào.

Trong trường hợp thứ hai, bạn trả về một trình trang trí 'mới' bằng cách nào đó sử dụng thông tin được chuyển vào bằng * args, ** kwargs.

Điều này là tốt và tất cả, nhưng phải viết nó ra cho mọi trang trí bạn thực hiện có thể khá khó chịu và không sạch sẽ. Thay vào đó, sẽ rất tuyệt nếu có thể tự động sửa đổi trình trang trí của chúng tôi mà không cần phải viết lại chúng ... nhưng đó là những gì người trang trí dành cho!

Sử dụng trình trang trí decorator sau đây, chúng ta có thể phân bổ trình trang trí của mình để chúng có thể được sử dụng có hoặc không có đối số:

def doublewrap(f):
    '''
    a decorator decorator, allowing the decorator to be used as:
    @decorator(with, arguments, and=kwargs)
    or
    @decorator
    '''
    @wraps(f)
    def new_dec(*args, **kwargs):
        if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):
            # actual decorated function
            return f(args[0])
        else:
            # decorator arguments
            return lambda realf: f(realf, *args, **kwargs)

    return new_dec

Bây giờ, chúng ta có thể trang trí các trình trang trí của mình bằng @doublewrap và chúng sẽ hoạt động có và không có đối số, với một lưu ý:

Tôi đã lưu ý ở trên nhưng nên lặp lại ở đây, việc kiểm tra trong trình trang trí này tạo ra một giả định về các đối số mà trình trang trí có thể nhận (cụ thể là nó không thể nhận một đối số duy nhất, có thể gọi). Vì chúng tôi hiện đang làm cho nó có thể áp dụng cho bất kỳ máy phát điện nào, nó cần được ghi nhớ hoặc sửa đổi nếu nó có mâu thuẫn.

Những điều sau đây thể hiện công dụng của nó:

def test_doublewrap():
    from util import doublewrap
    from functools import wraps    

    @doublewrap
    def mult(f, factor=2):
        '''multiply a function's return value'''
        @wraps(f)
        def wrap(*args, **kwargs):
            return factor*f(*args,**kwargs)
        return wrap

    # try normal
    @mult
    def f(x, y):
        return x + y

    # try args
    @mult(3)
    def f2(x, y):
        return x*y

    # try kwargs
    @mult(factor=5)
    def f3(x, y):
        return x - y

    assert f(2,3) == 10
    assert f2(2,5) == 30
    assert f3(8,1) == 5*7

31

Sử dụng các đối số từ khóa với các giá trị mặc định (theo đề xuất của kquinn) là một ý tưởng hay, nhưng sẽ yêu cầu bạn bao gồm dấu ngoặc đơn:

@redirect_output()
def foo():
    ...

Nếu bạn muốn một phiên bản hoạt động mà không có dấu ngoặc đơn trên trình trang trí, bạn sẽ phải tính cả hai trường hợp trong mã trình trang trí của mình.

Nếu bạn đang sử dụng Python 3.0, bạn có thể sử dụng các đối số chỉ từ khóa cho điều này:

def redirect_output(fn=None,*,destination=None):
  destination = sys.stderr if destination is None else destination
  def wrapper(*args, **kwargs):
    ... # your code here
  if fn is None:
    def decorator(fn):
      return functools.update_wrapper(wrapper, fn)
    return decorator
  else:
    return functools.update_wrapper(wrapper, fn)

Trong Python 2.x, điều này có thể được mô phỏng bằng các thủ thuật varargs:

def redirected_output(*fn,**options):
  destination = options.pop('destination', sys.stderr)
  if options:
    raise TypeError("unsupported keyword arguments: %s" % 
                    ",".join(options.keys()))
  def wrapper(*args, **kwargs):
    ... # your code here
  if fn:
    return functools.update_wrapper(wrapper, fn[0])
  else:
    def decorator(fn):
      return functools.update_wrapper(wrapper, fn)
    return decorator

Bất kỳ phiên bản nào trong số này đều cho phép bạn viết mã như sau:

@redirected_output
def foo():
    ...

@redirected_output(destination="somewhere.log")
def bar():
    ...

1
Bạn đặt your code herecái gì vào ? Làm thế nào để bạn gọi chức năng được trang trí? fn(*args, **kwargs)không hoạt động.
lum

Tôi nghĩ rằng có một câu trả lời đơn giản hơn nhiều, hãy tạo một lớp sẽ là người trang trí với các đối số tùy chọn. tạo một hàm khác có cùng các đối số với giá trị mặc định và trả về một phiên bản mới của các lớp decorator. nên trông giống như sau: def f(a = 5): return MyDecorator( a = a) class MyDecorator( object ): def __init__( self, a = 5 ): .... xin lỗi vì nó khó viết nó trong một bình luận nhưng tôi hy vọng điều này đủ đơn giản để hiểu
Omer Ben Haim

17

Tôi biết đây là một câu hỏi cũ, nhưng tôi thực sự không thích bất kỳ kỹ thuật nào được đề xuất vì vậy tôi muốn thêm một phương pháp khác. Tôi thấy rằng django sử dụng một phương pháp thực sự sạch sẽ trong login_requiredtrang trídjango.contrib.auth.decorators của họ trong . Như bạn có thể thấy trong tài liệu của người trang trí , nó có thể được sử dụng một mình @login_requiredhoặc với các đối số @login_required(redirect_field_name='my_redirect_field'),.

Cách họ làm cũng khá đơn giản. Họ thêm một kwarg( function=None) trước các đối số trang trí của họ. Nếu trình trang trí được sử dụng một mình, functionsẽ là chức năng thực sự mà nó đang trang trí, trong khi nếu nó được gọi với các đối số, functionsẽ là None.

Thí dụ:

from functools import wraps

def custom_decorator(function=None, some_arg=None, some_other_arg=None):
    def actual_decorator(f):
        @wraps(f)
        def wrapper(*args, **kwargs):
            # Do stuff with args here...
            if some_arg:
                print(some_arg)
            if some_other_arg:
                print(some_other_arg)
            return f(*args, **kwargs)
        return wrapper
    if function:
        return actual_decorator(function)
    return actual_decorator

@custom_decorator
def test1():
    print('test1')

>>> test1()
test1

@custom_decorator(some_arg='hello')
def test2():
    print('test2')

>>> test2()
hello
test2

@custom_decorator(some_arg='hello', some_other_arg='world')
def test3():
    print('test3')

>>> test3()
hello
world
test3

Tôi thấy cách tiếp cận mà django sử dụng này thanh lịch hơn và dễ hiểu hơn bất kỳ kỹ thuật nào khác được đề xuất ở đây.


Vâng, tôi thích phương pháp này. Xin lưu ý rằng bạn phải sử dụng kwargs khi gọi trình trang trí nếu không, đối số vị trí đầu tiên được gán cho functionvà sau đó mọi thứ sẽ phá vỡ vì trình trang trí cố gắng gọi đối số vị trí đầu tiên đó như thể nó là hàm được trang trí của bạn.
Dustin Wyatt

12

Bạn cần phát hiện cả hai trường hợp, ví dụ như sử dụng kiểu của đối số đầu tiên và theo đó trả về trình bao bọc (khi được sử dụng không có tham số) hoặc trình trang trí (khi được sử dụng với đối số).

from functools import wraps
import inspect

def redirect_output(fn_or_output):
    def decorator(fn):
        @wraps(fn)
        def wrapper(*args, **args):
            # Redirect output
            try:
                return fn(*args, **args)
            finally:
                # Restore output
        return wrapper

    if inspect.isfunction(fn_or_output):
        # Called with no parameter
        return decorator(fn_or_output)
    else:
        # Called with a parameter
        return decorator

Khi sử dụng @redirect_output("output.log")cú pháp, redirect_outputđược gọi với một đối số duy nhất "output.log"và nó phải trả về trình trang trí chấp nhận hàm được trang trí như một đối số. Khi được sử dụng như @redirect_output, nó được gọi trực tiếp với hàm được trang trí như một đối số.

Hay nói cách khác: @cú pháp phải được theo sau bởi một biểu thức có kết quả là một hàm chấp nhận một hàm được trang trí làm đối số duy nhất của nó và trả về hàm được trang trí. Bản thân biểu thức có thể là một lời gọi hàm, trường hợp này xảy ra với @redirect_output("output.log"). Biến đổi, nhưng đúng :-)


9

Một số câu trả lời ở đây đã giải quyết vấn đề của bạn một cách tốt đẹp. Tuy nhiên, liên quan đến phong cách, tôi thích giải quyết tình trạng khó khăn về trang trí này bằng cách sử dụng functools.partial, như được đề xuất trong Sách dạy nấu ăn Python 3 của David Beazley :

from functools import partial, wraps

def decorator(func=None, foo='spam'):
    if func is None:
         return partial(decorator, foo=foo)

    @wraps(func)
    def wrapper(*args, **kwargs):
        # do something with `func` and `foo`, if you're so inclined
        pass

    return wrapper

Trong khi có, bạn chỉ có thể làm

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

mà không có cách giải quyết thú vị, tôi thấy nó trông lạ và tôi thích có tùy chọn trang trí đơn giản với @decorator.

Đối với mục tiêu nhiệm vụ phụ, chuyển hướng đầu ra của một hàm được đề cập trong bài đăng Stack Overflow này .


Nếu bạn muốn tìm hiểu sâu hơn, hãy xem Chương 9 (Siêu lập trình) trong Python Cookbook 3 , được cung cấp miễn phí trên mạng .

Một số tài liệu đó được demo trực tiếp (cộng với nhiều hơn nữa!) Trong video tuyệt vời trên YouTube Python 3 Metaprogramming của Beazley .

Chúc bạn viết mã vui vẻ :)


8

Trình trang trí python được gọi theo một cách cơ bản khác tùy thuộc vào việc bạn có đưa ra đối số cho nó hay không. Trang trí thực sự chỉ là một biểu thức (bị hạn chế về mặt cú pháp).

Trong ví dụ đầu tiên của bạn:

@redirect_output("somewhere.log")
def foo():
    ....

hàm redirect_outputđược gọi với đối số đã cho, được mong đợi trả về một hàm trang trí, chính nó được gọi với foonhư một đối số, (cuối cùng!) được mong đợi trả về hàm được trang trí cuối cùng.

Mã tương đương trông như thế này:

def foo():
    ....
d = redirect_output("somewhere.log")
foo = d(foo)

Mã tương đương cho ví dụ thứ hai của bạn trông giống như sau:

def foo():
    ....
d = redirect_output
foo = d(foo)

Vì vậy, bạn có thể làm những gì bạn muốn nhưng không theo cách hoàn toàn liền mạch:

import types
def redirect_output(arg):
    def decorator(file, f):
        def df(*args, **kwargs):
            print 'redirecting to ', file
            return f(*args, **kwargs)
        return df
    if type(arg) is types.FunctionType:
        return decorator(sys.stderr, arg)
    return lambda f: decorator(arg, f)

Điều này sẽ ổn trừ khi bạn muốn sử dụng một hàm làm đối số cho trình trang trí của mình, trong trường hợp đó trình trang trí sẽ cho rằng nó không có đối số. Nó cũng sẽ không thành công nếu trang trí này được áp dụng cho một trang trí khác không trả về kiểu chức năng.

Một phương pháp thay thế chỉ là yêu cầu hàm decorator luôn được gọi, ngay cả khi nó không có đối số. Trong trường hợp này, ví dụ thứ hai của bạn sẽ giống như sau:

@redirect_output()
def foo():
    ....

Mã chức năng trang trí sẽ giống như sau:

def redirect_output(file = sys.stderr):
    def decorator(file, f):
        def df(*args, **kwargs):
            print 'redirecting to ', file
            return f(*args, **kwargs)
        return df
    return lambda f: decorator(file, f)

2

Trên thực tế, trường hợp báo trước trong giải pháp của @ bj0 có thể được kiểm tra dễ dàng:

def meta_wrap(decor):
    @functools.wraps(decor)
    def new_decor(*args, **kwargs):
        if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):
            # this is the double-decorated f. 
            # Its first argument should not be a callable
            doubled_f = decor(args[0])
            @functools.wraps(doubled_f)
            def checked_doubled_f(*f_args, **f_kwargs):
                if callable(f_args[0]):
                    raise ValueError('meta_wrap failure: '
                                'first positional argument cannot be callable.')
                return doubled_f(*f_args, **f_kwargs)
            return checked_doubled_f 
        else:
            # decorator arguments
            return lambda real_f: decor(real_f, *args, **kwargs)

    return new_decor

Dưới đây là một vài trường hợp thử nghiệm cho phiên bản an toàn không thành công này của meta_wrap.

    @meta_wrap
    def baddecor(f, caller=lambda x: -1*x):
        @functools.wraps(f)
        def _f(*args, **kwargs):
            return caller(f(args[0]))
        return _f

    @baddecor  # used without arg: no problem
    def f_call1(x):
        return x + 1
    assert f_call1(5) == -6

    @baddecor(lambda x : 2*x) # bad case
    def f_call2(x):
        return x + 1
    f_call2(5)  # raises ValueError

    # explicit keyword: no problem
    @baddecor(caller=lambda x : 100*x)
    def f_call3(x):
        return x + 1
    assert f_call3(5) == 600

1
Cảm ơn. Điều này là hữu ích!
Pragy Agarwal

0

Để đưa ra câu trả lời đầy đủ hơn những điều trên:

"Có cách nào để xây dựng trình trang trí có thể được sử dụng cả khi có và không có đối số không?"

Không có cách nào chung vì hiện tại ngôn ngữ python còn thiếu một thứ gì đó để phát hiện hai trường hợp sử dụng khác nhau.

Tuy nhiên như đã được chỉ ra bởi các câu trả lời khác như bj0s , có một cách giải quyết khó hiểu là kiểm tra kiểu và giá trị của đối số vị trí đầu tiên nhận được (và kiểm tra xem không có đối số nào khác có giá trị không mặc định). Nếu bạn được đảm bảo rằng người dùng sẽ không bao giờ chuyển đối số có thể gọi làm đối số đầu tiên của trình trang trí, thì bạn có thể sử dụng giải pháp thay thế này. Lưu ý rằng điều này cũng tương tự đối với trình trang trí lớp (thay thế có thể gọi bằng lớp trong câu trước).

Để chắc chắn về điều trên, tôi đã thực hiện khá nhiều nghiên cứu ở đó và thậm chí đã triển khai một thư viện có tên decopatchsử dụng kết hợp tất cả các chiến lược được trích dẫn ở trên (và nhiều chiến lược khác, bao gồm cả nội quan) để thực hiện "bất kỳ cách giải quyết nào là thông minh nhất" tùy thuộc theo nhu cầu của bạn.

Nhưng thành thật mà nói thì tốt nhất là không cần bất kỳ thư viện nào ở đây và lấy tính năng đó ngay từ ngôn ngữ python. Nếu, giống như bản thân tôi, bạn nghĩ rằng thật đáng tiếc khi ngôn ngữ python ngày nay không có khả năng cung cấp câu trả lời gọn gàng cho câu hỏi này, đừng ngần ngại hỗ trợ ý tưởng này trong trình duyệt lỗi python : https://bugs.python .org / issue36553 !

Cảm ơn bạn rất nhiều vì đã giúp làm cho python trở thành một ngôn ngữ tốt hơn :)


0

Điều này thực hiện công việc mà không có phiền phức:

from functools import wraps

def memoize(fn=None, hours=48.0):
  def deco(fn):
    @wraps(fn)
    def wrapper(*args, **kwargs):
      return fn(*args, **kwargs)
    return wrapper

  if callable(fn): return deco(fn)
  return deco

0

Vì không ai đề cập đến điều này, nên cũng có một giải pháp sử dụng lớp có thể gọi mà tôi thấy thanh lịch hơn, đặc biệt là trong trường hợp trình trang trí phức tạp và người ta có thể muốn chia nó thành nhiều phương thức (chức năng). Giải pháp này sử dụng __new__phương pháp ma thuật để làm về cơ bản những gì người khác đã chỉ ra. Trước tiên, hãy phát hiện cách trang trí đã được sử dụng hơn là điều chỉnh lợi nhuận một cách thích hợp.

class decorator_with_arguments(object):

    def __new__(cls, decorated_function=None, **kwargs):

        self = super().__new__(cls)
        self._init(**kwargs)

        if not decorated_function:
            return self
        else:
            return self.__call__(decorated_function)

    def _init(self, arg1="default", arg2="default", arg3="default"):
        self.arg1 = arg1
        self.arg2 = arg2
        self.arg3 = arg3

    def __call__(self, decorated_function):

        def wrapped_f(*args):
            print("Decorator arguments:", self.arg1, self.arg2, self.arg3)
            print("decorated_function arguments:", *args)
            decorated_function(*args)

        return wrapped_f

@decorator_with_arguments(arg1=5)
def sayHello(a1, a2, a3, a4):
    print('sayHello arguments:', a1, a2, a3, a4)

@decorator_with_arguments()
def sayHello(a1, a2, a3, a4):
    print('sayHello arguments:', a1, a2, a3, a4)

@decorator_with_arguments
def sayHello(a1, a2, a3, a4):
    print('sayHello arguments:', a1, a2, a3, a4)

Nếu trình trang trí được sử dụng với các đối số, thì giá trị này bằng:

result = decorator_with_arguments(arg1=5)(sayHello)(a1, a2, a3, a4)

Người ta có thể thấy rằng các đối số arg1được chuyển một cách chính xác đến hàm tạo và hàm được trang trí được chuyển đến__call__

Nhưng nếu trình trang trí được sử dụng mà không có đối số, thì giá trị này bằng:

result = decorator_with_arguments(sayHello)(a1, a2, a3, a4)

Bạn thấy rằng trong trường hợp này, hàm được trang trí được chuyển trực tiếp đến hàm tạo và lệnh gọi tới __call__hoàn toàn bị bỏ qua. Đó là lý do tại sao chúng ta cần sử dụng logic để giải quyết trường hợp này trong __new__phương pháp ma thuật.

Tại sao chúng ta không thể sử dụng __init__thay thế __new__? Lý do rất đơn giản: python cấm trả về bất kỳ giá trị nào khác ngoài Không có từ__init__

CẢNH BÁO

Sự chấp thuận này có một tác dụng phụ. Nó sẽ không bảo tồn chữ ký chức năng!


-1

Bạn đã thử đối số từ khóa với giá trị mặc định chưa? Cái gì đó như

def decorate_something(foo=bar, baz=quux):
    pass

-2

Nói chung, bạn có thể đưa ra các đối số mặc định trong Python ...

def redirect_output(fn, output = stderr):
    # whatever

Tuy nhiên, không chắc liệu điều đó có hoạt động với người trang trí hay không. Tôi không biết lý do tại sao nó không.


2
Nếu bạn nói @dec (abc) thì hàm không được chuyển trực tiếp đến dec. dec (abc) trả về một thứ gì đó và giá trị trả về này được sử dụng làm trình trang trí. Vì vậy, dec (abc) phải trả về một hàm, sau đó hàm được trang trí được truyền dưới dạng tham số. (Xem thêm mã thobes)
sth

-2

Dựa trên câu trả lời của vartec:

imports sys

def redirect_output(func, output=None):
    if output is None:
        output = sys.stderr
    if isinstance(output, basestring):
        output = open(output, 'w') # etc...
    # everything else...

điều này không thể được sử dụng như một trình trang trí như trong @redirect_output("somewhere.log") def foo()ví dụ trong câu hỏi.
ehabkost
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.