Python 函数被装饰后失去身份

1 投票
1 回答
623 浏览
提问于 2025-04-18 16:35

(Python 3)首先,我觉得我的标题可能不太合适,所以如果你能耐心看完这个问题并想出一个更好的标题,请随意修改。

我最近学习了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 个回答

3

虽然 wraps 可以保留函数的注解,但它并不能保留函数的签名。你可以通过打印 co_varnames 来看到这一点。因为 ensure_types 是通过比较参数的名字和注解字典中的名字来进行检查的,所以它无法匹配上,因为被包装的函数没有名为 xy 的参数(它只是接受通用的 *args**kwargs)。

你可以尝试使用 decorator 模块,它允许你编写像 functools.wrap 一样的装饰器,但同时也能保留函数的签名(包括注解)。

也许还有一种方法可以“手动”实现这个功能,但会有点麻烦。基本上,你需要让 wraps 存储原始函数的参数信息(参数的名字),然后让 ensure_dict 在检查类型时使用这个存储的参数信息,而不是包装函数的参数信息。简单来说,你的装饰器需要与被包装的函数一起传递参数信息。不过,使用 decorator 可能会更简单。

撰写回答