Lưu giữ chữ ký của các chức năng được trang trí


111

Giả sử tôi đã viết một trình trang trí làm một cái gì đó rất chung chung. Ví dụ: nó có thể chuyển đổi tất cả các đối số thành một kiểu cụ thể, thực hiện ghi nhật ký, triển khai ghi nhớ, v.v.

Đây là một ví dụ:

def args_as_ints(f):
    def g(*args, **kwargs):
        args = [int(x) for x in args]
        kwargs = dict((k, int(v)) for k, v in kwargs.items())
        return f(*args, **kwargs)
    return g

@args_as_ints
def funny_function(x, y, z=3):
    """Computes x*y + 2*z"""
    return x*y + 2*z

>>> funny_function("3", 4.0, z="5")
22

Mọi thứ tốt cho đến nay. Tuy nhiên, có một vấn đề. Chức năng được trang trí không giữ lại tài liệu về chức năng ban đầu:

>>> help(funny_function)
Help on function g in module __main__:

g(*args, **kwargs)

May mắn thay, có một cách giải quyết:

def args_as_ints(f):
    def g(*args, **kwargs):
        args = [int(x) for x in args]
        kwargs = dict((k, int(v)) for k, v in kwargs.items())
        return f(*args, **kwargs)
    g.__name__ = f.__name__
    g.__doc__ = f.__doc__
    return g

@args_as_ints
def funny_function(x, y, z=3):
    """Computes x*y + 2*z"""
    return x*y + 2*z

Lần này, tên hàm và tài liệu chính xác:

>>> help(funny_function)
Help on function funny_function in module __main__:

funny_function(*args, **kwargs)
    Computes x*y + 2*z

Nhưng vẫn còn một vấn đề: chữ ký hàm bị sai. Thông tin "* args, ** kwargs" là vô ích.

Để làm gì? Tôi có thể nghĩ ra hai cách giải quyết đơn giản nhưng thiếu sót:

1 - Bao gồm chữ ký chính xác trong chuỗi tài liệu:

def funny_function(x, y, z=3):
    """funny_function(x, y, z=3) -- computes x*y + 2*z"""
    return x*y + 2*z

Điều này thật tệ vì sự trùng lặp. Chữ ký sẽ vẫn không được hiển thị chính xác trong tài liệu được tạo tự động. Thật dễ dàng để cập nhật hàm và quên việc thay đổi chuỗi tài liệu hoặc mắc lỗi đánh máy. [ Và vâng, tôi biết thực tế là docstring đã sao chép nội dung hàm. Hãy bỏ qua điều này; fun_ Chức năng chỉ là một ví dụ ngẫu nhiên.]

2 - Không sử dụng trình trang trí hoặc sử dụng trình trang trí có mục đích đặc biệt cho mọi chữ ký cụ thể:

def funny_functions_decorator(f):
    def g(x, y, z=3):
        return f(int(x), int(y), z=int(z))
    g.__name__ = f.__name__
    g.__doc__ = f.__doc__
    return g

Điều này hoạt động tốt đối với một tập hợp các hàm có chữ ký giống hệt nhau, nhưng nói chung nó vô dụng. Như tôi đã nói ở phần đầu, tôi muốn có thể sử dụng các bộ trang trí hoàn toàn chung chung.

Tôi đang tìm kiếm một giải pháp hoàn toàn chung chung và tự động.

Vậy câu hỏi đặt ra là: có cách nào để chỉnh sửa chữ ký hàm được trang trí sau khi nó đã được tạo không?

Nếu không, tôi có thể viết trình trang trí trích xuất chữ ký hàm và sử dụng thông tin đó thay vì "* kwargs, ** kwargs" khi xây dựng hàm được trang trí không? Làm cách nào để trích xuất thông tin đó? Tôi nên xây dựng hàm được trang trí như thế nào - với hàm executive?

Bất kỳ cách tiếp cận nào khác?


1
Không bao giờ nói "hết thời". Tôi đã ít nhiều tự hỏi những gì inspect.Signatuređược thêm vào việc xử lý các chức năng được trang trí.
NightShadeQueen

Câu trả lời:


79
  1. Cài đặt mô-đun decorator :

    $ pip install decorator
  2. Điều chỉnh định nghĩa của args_as_ints():

    import decorator
    
    @decorator.decorator
    def args_as_ints(f, *args, **kwargs):
        args = [int(x) for x in args]
        kwargs = dict((k, int(v)) for k, v in kwargs.items())
        return f(*args, **kwargs)
    
    @args_as_ints
    def funny_function(x, y, z=3):
        """Computes x*y + 2*z"""
        return x*y + 2*z
    
    print funny_function("3", 4.0, z="5")
    # 22
    help(funny_function)
    # Help on function funny_function in module __main__:
    # 
    # funny_function(x, y, z=3)
    #     Computes x*y + 2*z

Python 3.4+

functools.wraps()from stdlib giữ nguyên chữ ký kể từ Python 3.4:

import functools


def args_as_ints(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        args = [int(x) for x in args]
        kwargs = dict((k, int(v)) for k, v in kwargs.items())
        return func(*args, **kwargs)
    return wrapper


@args_as_ints
def funny_function(x, y, z=3):
    """Computes x*y + 2*z"""
    return x*y + 2*z


print(funny_function("3", 4.0, z="5"))
# 22
help(funny_function)
# Help on function funny_function in module __main__:
#
# funny_function(x, y, z=3)
#     Computes x*y + 2*z

functools.wraps()có sẵn ít nhất kể từ Python 2.5 nhưng nó không bảo tồn chữ ký ở đó:

help(funny_function)
# Help on function funny_function in module __main__:
#
# funny_function(*args, **kwargs)
#    Computes x*y + 2*z

Chú ý: *args, **kwargsthay vì x, y, z=3.


Của bạn không phải là câu trả lời đầu tiên, nhưng là câu trả lời toàn diện nhất cho đến nay :-) Tôi thực sự muốn giải pháp không liên quan đến mô-đun của bên thứ ba, nhưng nhìn vào nguồn cho mô-đun trang trí, nó đủ đơn giản để tôi có thể chỉ cần sao chép nó.
Fredrik Johansson

1
@MarkLodato: functools.wraps()đã lưu giữ chữ ký trong Python 3.4+ (như đã nói trong câu trả lời). Bạn có nghĩa là cài đặt wrapper.__signature__giúp ích cho các phiên bản trước? (bạn đã thử nghiệm phiên bản nào?)
jfs

1
@MarkLodato: help()hiển thị chữ ký chính xác trên Python 3.4. Bạn nghĩ tại sao lại functools.wraps()bị hỏng mà không phải là IPython?
jfs

1
@MarkLodato: nó bị hỏng nếu chúng ta phải viết mã để sửa nó. Cho rằng điều đó help()tạo ra kết quả chính xác, câu hỏi đặt ra là phần mềm nào nên được sửa: functools.wraps()hay IPython? Trong mọi trường hợp, chỉ định thủ công __signature__là một giải pháp tốt nhất - nó không phải là một giải pháp lâu dài.
jfs

1
Có vẻ như inspect.getfullargspec()vẫn không trả lại chữ ký thích hợp cho functools.wrapstrong python 3.4 và bạn phải sử dụng inspect.signature()thay thế.
Tuukka Mustonen

16

Điều này được giải quyết bằng thư viện chuẩn của Python functoolsfunctools.wrapshàm cụ thể , được thiết kế để " cập nhật một hàm bao bọc để trông giống như hàm được bao bọc ". Tuy nhiên, hành vi của nó phụ thuộc vào phiên bản Python, như được hiển thị bên dưới. Được áp dụng cho ví dụ từ câu hỏi, mã sẽ giống như sau:

from functools import wraps

def args_as_ints(f):
    @wraps(f) 
    def g(*args, **kwargs):
        args = [int(x) for x in args]
        kwargs = dict((k, int(v)) for k, v in kwargs.items())
        return f(*args, **kwargs)
    return g


@args_as_ints
def funny_function(x, y, z=3):
    """Computes x*y + 2*z"""
    return x*y + 2*z

Khi được thực thi trong Python 3, điều này sẽ tạo ra như sau:

>>> funny_function("3", 4.0, z="5")
22
>>> help(funny_function)
Help on function funny_function in module __main__:

funny_function(x, y, z=3)
    Computes x*y + 2*z

Tuy nhiên, hạn chế duy nhất của nó là trong Python 2, nó không cập nhật danh sách đối số của hàm. Khi được thực thi trong Python 2, nó sẽ tạo ra:

>>> help(funny_function)
Help on function funny_function in module __main__:

funny_function(*args, **kwargs)
    Computes x*y + 2*z

Không chắc đó có phải là Sphinx hay không, nhưng điều này dường như không hoạt động khi hàm được bọc là một phương thức của một lớp. Sphinx tiếp tục báo cáo chữ ký cuộc gọi của người trang trí.
alphabetasoup

9

Có một mô-đundecorator decorator với decorator mà bạn có thể sử dụng:

@decorator
def args_as_ints(f, *args, **kwargs):
    args = [int(x) for x in args]
    kwargs = dict((k, int(v)) for k, v in kwargs.items())
    return f(*args, **kwargs)

Sau đó, chữ ký và trợ giúp của phương thức được giữ nguyên:

>>> help(funny_function)
Help on function funny_function in module __main__:

funny_function(x, y, z=3)
    Computes x*y + 2*z

CHỈNH SỬA: JF Sebastian đã chỉ ra rằng tôi đã không sửa đổi args_as_intschức năng - nó đã được sửa ngay bây giờ.



6

Sự lựa chọn thứ hai:

  1. Cài đặt mô-đun wrapt:

$ easy_install wrapt

wrapt có tiền thưởng, giữ gìn chữ ký của lớp.


import wrapt
import inspect

@wrapt.decorator def args_as_ints(wrapped, instance, args, kwargs): if instance is None: if inspect.isclass(wrapped): # Decorator was applied to a class. return wrapped(*args, **kwargs) else: # Decorator was applied to a function or staticmethod. return wrapped(*args, **kwargs) else: if inspect.isclass(instance): # Decorator was applied to a classmethod. return wrapped(*args, **kwargs) else: # Decorator was applied to an instancemethod. return wrapped(*args, **kwargs) @args_as_ints def funny_function(x, y, z=3): """Computes x*y + 2*z""" return x * y + 2 * z >>> funny_function(3, 4, z=5)) # 22 >>> help(funny_function) Help on function funny_function in module __main__: funny_function(x, y, z=3) Computes x*y + 2*z

2

Như đã nhận xét ở trên trong câu trả lời của jfs ; nếu bạn quan tâm đến chữ ký về hình thức ( helpinspect.signature), thì hãy sử dụngfunctools.wraps là hoàn toàn ổn.

Nếu bạn lo lắng về chữ ký về mặt hành vi (đặc biệt là TypeErrortrong trường hợp các đối số không khớp), functools.wrapsđừng lưu giữ nó. Bạn nên sử dụng decoratorcho điều đó, hoặc khái quát của tôi về động cơ cốt lõi của nó, được đặt tên makefun.

from makefun import wraps

def args_as_ints(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("wrapper executes")
        args = [int(x) for x in args]
        kwargs = dict((k, int(v)) for k, v in kwargs.items())
        return func(*args, **kwargs)
    return wrapper


@args_as_ints
def funny_function(x, y, z=3):
    """Computes x*y + 2*z"""
    return x*y + 2*z


print(funny_function("3", 4.0, z="5"))
# wrapper executes
# 22

help(funny_function)
# Help on function funny_function in module __main__:
#
# funny_function(x, y, z=3)
#     Computes x*y + 2*z

funny_function(0)  
# observe: no "wrapper executes" is printed! (with functools it would)
# TypeError: funny_function() takes at least 2 arguments (1 given)

Xem thêm bài đăng này vềfunctools.wraps .


1
Ngoài ra, kết quả của inspect.getfullargspeckhông được lưu giữ bằng cách gọi functools.wraps.
laike9m

Cảm ơn vì nhận xét bổ sung hữu ích @ laike9m!
smarie
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.