解决装饰器对文档字符串的破坏

习惯上我们会将函数的第一条语句写成文档字符串,用于描述函数的用途,例如:

def factorial(n):
    """Computes n factorial. For example:
        
        >>>factorial(6)
        120
        >>>
    """
    if n <= 1: return 1
    else: return n * factorial(n-1)

文档字符串保存在函数的 __doc__ 属性中,IDE 通常使用该函数提供交互式帮助:

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

factorial(n)
    Computes n factorial. For example:

    >>>factorial(6)
    120
    >>>

如果需要使用装饰器,要注意使用装饰器包装函数可能会破坏与文档字符串相关的帮助功能。例如,考虑以下代码:

def wrap(func):
    def call(*args, **kwargs):
        return func(*args, **kwargs)
    return call
	
@wrap
def factorial(n):
    """Computes n factorial."""
    ...

如果用户请求这个版本的 factorial() 函数的帮助,将会看到一种相当诡异的解释:

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

call(*args, **kwargs)

这个问题的解决办法是编写可以传递函数名称和文档字符串的装饰器函数,例如:

def wrap(func):
    def call(*args, **kwargs):
        return func(*args, **kwargs)
    call.__doc__ = func.__doc__
    call.__name__ = func.__name__
    return call

因为这是一个常见问题,所以 functools 模块提供了函数 wraps,用于自动复制这些属性。显而易见,它也是一个装饰器:

from functools import wraps

def wrap(func):
    @wraps(func)
    def call(*args, **kwargs):
        return func(*args, **kwargs)
    return call

functools 模块中定义的 @wraps(func) 装饰器可以将属性从 func 传递给要定义的包装器函数。
因此 wraps 接收一个函数作为参数,返回一个能包装函数的装饰器,自己编写其代码的结构如下:

def wraps(func):
    def callfunc(call):
        def f(*args, **kwargs):
            return call(*args, **kwargs)
        f.__doc__ = func.__doc__
        f.__name__ = func.__name__
        return f
    return callfunc