Python装饰器是影响函数行为的常用工具,它可以在不修改原函数代码的前提下,为函数添加额外的功能。但装饰器的使用往往会改变被装饰函数的签名信息,导致通过inspect模块获取的参数、注解等内容和原函数不一致,影响代码的可用性和可维护性。

函数签名的基本概念
函数签名是描述函数特征的信息集合,主要包括函数的参数列表、参数默认值、参数注解、返回值注解等内容。在Python中,我们可以通过inspect模块的signature函数获取函数的签名信息,示例如下:
import inspect
def original_func(name: str, age: int = 18) -> str:
return f"name:{name}, age:{age}"
# 获取原函数签名
sig = inspect.signature(original_func)
print(sig) # 输出 (name: str, age: int = 18) -> str
print(sig.parameters) # 输出 OrderedDict([('name', <Parameter "name: str">), ('age', <Parameter "age: int = 18">)])
普通装饰器对函数签名的影响
最常见的装饰器写法是定义一个外层函数接收原函数,再定义内层包装函数执行额外逻辑后调用原函数,最后返回包装函数。这种写法会直接替换原函数的引用,导致签名信息变成包装函数的签名。
我们看一个基础装饰器的示例:
import inspect
def simple_decorator(func):
def wrapper(*args, **kwargs):
print("装饰器额外逻辑执行")
return func(*args, **kwargs)
return wrapper
@simple_decorator
def decorated_func(name: str, age: int = 18) -> str:
return f"name:{name}, age:{age}"
# 获取被装饰函数的签名
sig = inspect.signature(decorated_func)
print(sig) # 输出 (*args, **kwargs)
print(sig.parameters) # 输出 OrderedDict([('args', <Parameter "*args">), ('kwargs', <Parameter "**kwargs">)])
可以看到,被装饰后的decorated_func的签名已经变成了包装函数wrapper的签名,原函数的参数定义、注解等所有签名信息都丢失了。这是因为装饰器返回的是wrapper函数对象,decorated_func的引用已经指向了wrapper,自然获取到的就是wrapper的签名。
带参数的装饰器对函数签名的影响
带参数的装饰器会比普通装饰器多一层函数嵌套,用来接收装饰器参数,最终返回的还是包装函数,因此对函数签名的影响和普通装饰器一致,同样会丢失原函数的签名信息。
示例如下:
import inspect
def param_decorator(prefix=""):
def decorator(func):
def wrapper(*args, **kwargs):
print(f"前缀:{prefix}")
return func(*args, **kwargs)
return wrapper
return decorator
@param_decorator(prefix="测试")
def param_decorated_func(name: str) -> str:
return f"name:{name}"
sig = inspect.signature(param_decorated_func)
print(sig) # 输出 (*args, **kwargs)
如何保留原函数的函数签名
Python标准库的functools模块提供了wraps装饰器,它可以把原函数的签名、名称、文档字符串等属性复制到包装函数上,从而避免装饰器对函数签名的影响。
使用wraps的示例如下:
import inspect
from functools import wraps
def wraps_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print("装饰器额外逻辑执行")
return func(*args, **kwargs)
return wrapper
@wraps_decorator
def wraps_decorated_func(name: str, age: int = 18) -> str:
return f"name:{name}, age:{age}"
sig = inspect.signature(wraps_decorated_func)
print(sig) # 输出 (name: str, age: int = 18) -> str
print(wraps_decorated_func.__name__) # 输出 wraps_decorated_func,而不是wrapper
wraps的实现原理是读取原函数的__wrapped__属性,同时复制原函数的__name__、__doc__、__annotations__等属性到包装函数上,因此inspect.signature在获取签名时,会追溯到__wrapped__指向的原函数,从而返回正确的签名信息。
不同场景下的签名影响对比
我们可以通过表格对比不同装饰器写法对函数签名的影响:
| 装饰器类型 | 是否保留原函数签名 | 签名内容 |
|---|---|---|
| 无装饰器 | 是 | 原函数的参数、注解、返回值信息 |
| 普通装饰器(无wraps) | 否 | 包装函数的*args, **kwargs参数 |
| 带参数装饰器(无wraps) | 否 | 最内层包装函数的*args, **kwargs参数 |
| 使用functools.wraps的装饰器 | 是 | 原函数的参数、注解、返回值信息 |
注意事项
如果包装函数本身定义了明确的参数,那么即使不使用wraps,签名也会是包装函数的参数,但此时参数信息和原函数可能不一致,仍然不符合预期。因此只要编写装饰器,都建议加上@wraps(func)的写法,保证函数签名的正确性。
另外,如果装饰器需要修改函数的参数,比如新增参数或者删除参数,那么签名会自然发生变化,这种场景下属于合理的签名修改,不需要强制保留原签名,只需要保证新的签名符合装饰器的参数设计即可。