透明的蟒蛇装饰

时间:2018-01-11 09:07:25

标签: python python-decorators

我需要一个装饰器来记录方法调用。我将使用这样的东西(打印将被替换为在某处发送数据):

def log(function):
    varnames = function.__code__.co_varnames[:function.func_code.co_argcount]

    defaults = {}
    varnames_list = list(varnames)
    for default in reversed(foo.func_defaults):
        defaults[varnames_list.pop()] = default

    @functools.wraps(function)
    def decorator(*arg, **kargs):
        result = function(*arg, **kargs)
        incoming_args = defaults
        incoming_args.update(kargs)
        for i, value in enumerate(arg):
            incoming_args[varnames[i]] = value
        obj_to_log = {'function':function.__name__, 'incoming_args':incoming_args, "result":result}
        print('log', obj_to_log)
        return result

    return decorator
    pass

它工作正常,但它会影响原始方法签名:

def print_function_args(function):
    argcount = function.func_code.co_argcount
    print(function.__name__,
        'argnames:', function.__code__.co_varnames[:argcount],
        'defaults:', function.func_defaults)


def foo(a, b='default', c=42):
    pass

print_function_args(foo)
# prints: foo argnames: ('a', 'b', 'c') defaults: ('default', 42)

@log
def bar(a, b='default', c=42):
    pass

print_function_args(bar)
# bar argnames: () defaults: None

我必须保持方法签名不变,所以

有没有办法用装饰器包装函数,保留它的签名和文档?

1 个答案:

答案 0 :(得分:1)

这里的主要问题是你直接从函数的代码对象中获取函数参数。这不是你想要的 - 你实际上对函数的代码并不感兴趣,你对函数本身感兴趣。装饰器有一个特殊的__wrapped__属性,指向原始函数;但代码对象没有。因此,您应该使用inspect module代替,这将为您处理大部分事情,并为您提供您期望的结果。

不幸的是,维护已修饰函数的签名必须通过将signature object分配给decorator.__signature__来手动完成。

以下是使用python 3的log模块的inspect装饰器的实现:

def log(function):
    signature = inspect.signature(function)

    @functools.wraps(function)
    def decorator(*args, **kwargs):
        result = function(*args, **kwargs)

        incoming_args = signature.bind(*args, **kwargs)
        incoming_args.apply_defaults()
        incoming_args = dict(incoming_args.arguments)
        obj_to_log = {'function': function.__name__,
                    'incoming_args': incoming_args,
                    "result": result}
        print('log', obj_to_log)

        return result

    # maintain the decorated function's signature
    decorator.__signature__ = signature
    return decorator

使用getfullargspec可以简化(并更正)print_function_args函数:

def print_function_args(function):
    argspec = inspect.getfullargspec(function)
    print(function.__name__,
        'argnames:', argspec.args,
        'defaults:', argspec.defaults,
        'docstring:', function.__doc__)

你会发现现在一切正常:

def foo(a, b='default', c=42):
    """sample docstring"""

@log
def bar(a, b='default', c=42):
    """sample docstring"""

print_function_args(foo)
print_function_args(bar)

bar(3)
foo argnames: ['a', 'b', 'c'] defaults: ('default', 42) docstring: sample docstring
bar argnames: ['a', 'b', 'c'] defaults: ('default', 42) docstring: sample docstring
log {'function': 'bar', 'incoming_args': {'a': 3, 'b': 'default', 'c': 42}, 'result': None}