장식 된 기능의 서명 보존


111

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

다음은 예입니다.

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
"오래된"이라고 말하지 마십시오. inspect.Signature장식 된 기능을 처리하는 데 무엇이 추가 되었는지 다소 궁금했습니다 .
NightShadeQueen 2015

답변:


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


귀하의 답변은 첫 번째 답변은 아니지만 지금까지 가장 포괄적 인 답변입니다. :-) 실제로 타사 모듈을 포함하지 않는 솔루션을 선호하지만 데코레이터 모듈의 소스를 살펴보면 충분히 간단합니다. 그냥 복사하세요.
Fredrik Johansson

1
@MarkLodato : functools.wraps()이미 Python 3.4 이상 에서 서명을 유지합니다 (답변에서 말했듯이). wrapper.__signature__이전 버전에서 설정 도움말 을 의미 합니까? (이 버전은 테스트가?)
JFS

1
@MarkLodato : help()Python 3.4에서 올바른 서명을 표시합니다. 왜 functools.wraps()IPython이 아니라 망가 졌다고 생각 하십니까?
jfs

1
@MarkLodato : 문제를 해결하기 위해 코드를 작성해야한다면 고장났습니다. help()올바른 결과 를 생성 한다는 점을 감안할 때 문제는 어떤 소프트웨어를 수정해야 functools.wraps()하는가입니다 . 또는 IPython? 어쨌든 수동 할당 __signature__은 기껏해야 해결 방법이며 장기적인 솔루션이 아닙니다.
jfs

1
외모가 좋아 inspect.getfullargspec()에 대한 적절한 서명을 반환하지 않습니다 아직 functools.wraps파이썬 3.4에서 당신이 사용해야하는 inspect.signature()대신.
Tuukka Mustonen 2011

16

이 문제는 Python의 표준 라이브러리 functools와 특히 functools.wraps" 래퍼 함수를 ​​래핑 된 함수처럼 보이도록 업데이트 "하도록 설계된 함수 로 해결됩니다 . 그러나 아래와 같이 동작은 Python 버전에 따라 다릅니다. 질문의 예제에 적용된 코드는 다음과 같습니다.

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

Python 3에서 실행하면 다음이 생성됩니다.

>>> 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 2에서는 함수의 인수 목록을 업데이트하지 않는다는 것입니다. Python 2에서 실행되면 다음이 생성됩니다.

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

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

스핑크스인지 확실하지 않지만 래핑 된 함수가 클래스의 메서드 인 경우 작동하지 않는 것 같습니다. Sphinx는 계속해서 데코레이터의 호출 서명을보고합니다.
alphabetasoup

9

사용할 수 있는 데코레이터 가있는 데코레이터 모듈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)

그런 다음 메서드의 서명과 도움말이 유지됩니다.

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

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

편집 : JF Sebastian은 내가 args_as_ints기능을 수정하지 않았다고 지적했습니다 -이제 수정되었습니다.



6

두 번째 옵션 :

  1. wrapt 모듈 설치 :

$ easy_install 랩트

wrapt에는 보너스가 있으며 클래스 서명을 보존하십시오.


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

위에서 jfs의 답변 에서 언급했듯이 ; 모양 ( help, 및 inspect.signature) 측면에서 서명에 관심이 있다면 사용하는 functools.wraps것이 좋습니다.

동작 측면에서 서명에 관심이 있다면 (특히 TypeError인수가 일치하지 않는 경우) 서명을 functools.wraps보존하지 마십시오. 오히려 decorator그것을 사용 하거나라는 핵심 엔진의 일반화를 사용해야 합니다 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)

대한이 게시물을functools.wraps 참조하십시오 .


1
또한 결과는 inspect.getfullargspec호출로 보관되지 않습니다.functools.wraps .
laike9m

유용한 추가 의견 @ laike9m에 감사드립니다!
smarie
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.