Python函数在装饰后失去了身份

时间:2014-08-08 18:47:01

标签: python python-decorators decorator-chaining

(Python 3) 首先,我觉得我的头衔并不是应该是什么,所以如果你坚持这个问题并想出一个更好的头衔,请随时编辑它。

我最近学习了Python装饰器和Python注释,因此我编写了两个小函数来测试我最近学到的东西。 其中一个名为wraps,应该模仿functools wraps的行为,而另一个名为ensure_types,应该检查给定的函数,并通过其注释,如果传递给某个函数的参数是正确的。 这是我对这些功能的代码:

def wraps(original_func):
    """Update the decorated function with some important attributes from the
    one that was decorated so as not to lose good information"""
    def update_attrs(new_func):
        # Update the __annotations__
        for key, value in original_func.__annotations__.items():
            new_func.__annotations__[key] = value
        # Update the __dict__
        for key, value in original_func.__dict__.items():
            new_func.__dict__[key] = value
        # Copy the __name__
        new_func.__name__ = original_func.__name__
        # Copy the docstring (__doc__)
        new_func.__doc__ = original_func.__doc__
        return new_func
    return update_attrs # return the decorator

def ensure_types(f):
    """Uses f.__annotations__ to check the expected types for the function's
    arguments. Raises a TypeError if there is no match.
    If an argument has no annotation, object is returned and so, regardless of
    the argument passed, isinstance(arg, object) evaluates to True"""
    @wraps(f) # say that test_types is wrapping f
    def test_types(*args, **kwargs):
        # Loop through the positional args, get their name and check the type
        for i in range(len(args)):
            # function.__code__.co_varnames is a tuple with the names of the
            ##arguments in the order they are in the function def statement
            var_name = f.__code__.co_varnames[i]
            if not(isinstance(args[i], f.__annotations__.get(var_name, object))):
                raise TypeError("Bad type for function argument named '{}'".format(var_name))
        # Loop through the named args, get their value and check the type
        for key in kwargs.keys():
            if not(isinstance(kwargs[key], f.__annotations__.get(key, object))):
                raise TypeError("Bad type for function argument named '{}'".format(key))
        return f(*args, **kwargs)
    return test_types

据说,直到现在一切都还好。 wrapsensure_types都应该用作装饰器。当我定义第三个装饰器debug_dec时,问题就来了,它应该在调用函数及其参数时打印到控制台。功能:

def debug_dec(f):
    """Does some annoying printing for debugging purposes"""
    @wraps(f)
    def profiler(*args, **kwargs):
        print("{} function called:".format(f.__name__))
        print("\tArgs: {}".format(args))
        print("\tKwargs: {}".format(kwargs))
        return f(*args, **kwargs)
    return profiler

这也很有用。当我尝试同时使用debug_decensure_types时会遇到问题。

@ensure_types
@debug_dec
def testing(x: str, y: str = "lol"):
    print(x)
    print(y)

testing("hahaha", 3) # raises no TypeError as expected

但是如果我改变调用装饰器的顺序,它就可以正常工作。 有人可以帮我理解出了什么问题,除了交换这两行之外,还有什么方法可以解决问题吗?

修改 如果我添加行:

print(testing.__annotations__)
print(testing.__code__.co_varnames)

输出如下:

#{'y': <class 'str'>, 'x': <class 'str'>}
#('args', 'kwargs', 'i', 'var_name', 'key')

1 个答案:

答案 0 :(得分:3)

虽然wraps维护注释,但它不维护函数签名。打印出co_varnames时会看到这一点。由于ensure_types通过将参数的 names 与注释dict中的名称进行比较来进行检查,因此无法匹配它们,因为包装函数没有名为{{1}的参数}和x(它只接受通用y*args)。

您可以尝试使用decorator模块,该模块允许您编写类似**kwargs的装饰器,但也保留功能签名(包括注释)。

可能还有一种方法可以让它“手动”工作,但这会有点痛苦。基本上你需要做的是让functools.wrap存储原始函数argspec(其参数的名称),然后让wraps使用这个存储的argspec而不是包装器的argspec来检查类型。基本上你的装饰器会与包装函数并行传递argspec。但是,使用ensure_dict可能更容易。