Programing

장식 된 기능의 서명 보존

crosscheck 2020. 8. 21. 07:10
반응형

장식 된 기능의 서명 보존


내가 매우 일반적인 것을하는 데코레이터를 작성했다고 가정하자. 예를 들어 모든 인수를 특정 유형으로 변환하고, 로깅을 수행하고, 메모 화를 구현할 수 있습니다.

다음은 예입니다.

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

지금까지 모든 것이 잘되었습니다. 그러나 한 가지 문제가 있습니다. 데코 레이팅 된 함수는 원래 함수의 문서를 유지하지 않습니다.

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

g(*args, **kwargs)

다행히 해결 방법이 있습니다.

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

이번에는 함수 이름과 문서가 정확합니다.

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

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

그러나 여전히 문제가 있습니다. 함수 서명이 잘못되었습니다. 정보 "* args, ** kwargs"는 쓸모가 없습니다.

무엇을해야합니까? 간단하지만 결함이있는 두 가지 해결 방법을 생각할 수 있습니다.

1-독 스트링에 올바른 서명을 포함합니다.

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

중복으로 인해 좋지 않습니다. 서명은 여전히 ​​자동으로 생성 된 문서에 제대로 표시되지 않습니다. 함수를 업데이트하고 독 스트링 변경을 잊어 버리거나 오타를 만드는 것은 쉽습니다. [ 그리고 예, 독 스트링이 이미 함수 본문을 복제한다는 사실을 알고 있습니다. 이것을 무시하십시오; funny_function은 임의의 예입니다. ]

2-데코레이터를 사용하지 않거나 모든 특정 시그니처에 특수 목적의 데코레이터를 사용합니다.

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

이것은 동일한 서명을 가진 함수 세트에 대해 잘 작동하지만 일반적으로 쓸모가 없습니다. 처음에 말했듯이 데코레이터를 완전히 일반적으로 사용할 수 있기를 원합니다.

완전히 일반적이고 자동 인 솔루션을 찾고 있습니다.

그래서 질문은 : 데코 레이팅 된 함수 시그니처가 생성 된 후 편집 할 수있는 방법이 있습니까?

그렇지 않으면, 데코 레이팅 된 함수를 구성 할 때 함수 시그니처를 추출하고 "* kwargs, ** kwargs"대신 해당 정보를 사용하는 데코레이터를 작성할 수 있습니까? 그 정보를 어떻게 추출합니까? exec로 데코 레이팅 된 함수를 어떻게 구성해야합니까?

다른 방법은 없나요?


  1. 데코레이터 모듈 설치 :

    $ pip install decorator
    
  2. 정의를 조정하십시오 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는 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()최소한 Python 2.5부터 사용할 수 있지만 서명을 보존하지 않습니다.

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

고시 : *args, **kwargs대신 x, y, z=3.


This is solved with Python's standard library functools and specifically functools.wraps function, which is designed to "update a wrapper function to look like the wrapped function". It's behaviour depends on Python version, however, as shown below. Applied to the example from the question, the code would look like:

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

When executed in Python 3, this would produce the following:

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

Its only drawback is that in Python 2 however, it doesn't update function's argument list. When executed in Python 2, it will produce:

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

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

There is a decorator module with decorator decorator you can use:

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

Then the signature and help of the method is preserved:

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

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

EDIT: J. F. Sebastian pointed out that I didn't modify args_as_ints function -- it is fixed now.


Take a look at the decorator module - specifically the decorator decorator, which solves this problem.


Second option:

  1. Install wrapt module:

$ easy_install wrapt

wrapt have a bonus, preserve class signature.


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

As commented above in jfs's answer ; if you're concerned with signature in terms of appearance (help, and inspect.signature), then using functools.wraps is perfectly fine.

If you're concerned with signature in terms of behavior (in particular TypeError in case of arguments mismatch), functools.wraps does not preserve it. You should rather use decorator for that, or my generalization of its core engine, named 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)

See also this post about functools.wraps.

참고URL : https://stackoverflow.com/questions/147816/preserving-signatures-of-decorated-functions

반응형