函数签名与inspect模块

深入理解Python函数签名和参数自省

专题: Python进阶编程 -- 元编程与自省

核心主题: 函数签名(Signature)与 inspect 模块的深度应用

主要内容: Signature对象、Parameter对象、参数种类、注解获取、装饰器签名处理、依赖注入框架、__signature__自定义

关键词: Python, 签名, Signature, Parameter, inspect, 参数, 自省, 依赖注入

一、概述:什么是函数签名

在Python中,函数签名(Function Signature)描述了函数的输入输出协议——它定义了函数接受哪些参数、这些参数的类型约束以及函数返回值的类型。Python标准库中的 inspect 模块提供了强大的签名提取和分析工具,其核心是 Signature 对象和 Parameter 对象,二者共同构成了Python函数自省机制的基石。

函数签名在现代Python开发中扮演着至关重要的角色:它是装饰器编写的基础设施,是依赖注入框架的底层支撑,也是类型检查器文档生成器的数据来源。深入理解函数签名,能让你写出更加灵活、健壮的Python代码。

核心概念速览:

  • Signature 对象:代表函数的完整签名,包含参数列表和返回值注解
  • Parameter 对象:代表签名中的单个参数,包含名称、种类、默认值、注解等信息
  • 参数种类(Parameter.kind):区分参数是位置参数、关键字参数还是可变参数
  • inspect.signature():从函数对象中提取签名的标准接口

二、Signature 对象详解

Signature 对象是函数签名的容器,它聚合了函数所有的参数信息以及返回值注解。我们可以通过 inspect.signature() 函数从任意可调用对象中获取其签名。

2.1 获取函数签名

最基本的使用方式如下:

import inspect def greet(name: str, age: int = 18, *tags: str, verbose: bool = False) -> str: """向某人问好""" msg = f"Hello, {name}!" if verbose: msg += f" Age: {age}." return msg sig = inspect.signature(greet) print(sig) # (name: str, age: int = 18, *tags: str, verbose: bool = False) -> str print(type(sig)) # print(repr(sig)) # str>

2.2 Signature 的核心属性

parameters: OrderedDict[str, Parameter]

一个有序字典,按照参数定义顺序存储所有 Parameter 对象。键是参数名,值是 Parameter 实例。

sig = inspect.signature(greet) print(list(sig.parameters.keys())) # ['name', 'age', 'tags', 'verbose'] # 遍历所有参数 for name, param in sig.parameters.items(): print(f"{name}: {param.kind.name}") # name: POSITIONAL_OR_KEYWORD # age: POSITIONAL_OR_KEYWORD # tags: VAR_POSITIONAL # verbose: KEYWORD_ONLY

return_annotation: Any

函数的返回值注解。如果没有注解,返回 Signature.empty

print(sig.return_annotation) # print(sig.return_annotation is inspect.Signature.empty) # False # 无注解的情况 def plain_func(x): return x sig2 = inspect.signature(plain_func) print(sig2.return_annotation) # print(sig2.return_annotation is inspect.Signature.empty) # True

2.3 bind 与 bind_args:动态参数绑定

Signature 提供了两个强大的方法 —— bind()bind_args() —— 用于将实际参数动态绑定到签名的参数上。这是实现依赖注入和参数校验的核心工具。

sig = inspect.signature(greet) # bind(): 绑定参数,返回 BoundArguments 对象 # 如果参数不匹配,抛出 TypeError bound = sig.bind("Alice", 25, "student", "urgent", verbose=True) print(bound) # # 通过 .arguments 获取绑定后的参数字典 for name, value in bound.arguments.items(): print(f"{name} = {value!r}") # name = 'Alice' # age = 25 # tags = ('student', 'urgent') # verbose = True # bind_args(): 更宽松的版本,允许多余的关键字参数 # 注意:多余的参数会被收集到 **kwargs 中 try: sig.bind("Bob", unknown=42) except TypeError as e: print(e) # got an unexpected keyword argument 'unknown' # bind_args 允许额外关键字参数 bound2 = sig.bind_args("Bob", 30, unknown=42) print(bound2.arguments) # {'name': 'Bob', 'age': 30, 'unknown': 42}

bind 与 bind_args 的核心区别:

  • bind():严格匹配签名定义,未知关键字参数会抛出 TypeError,适合用于参数校验
  • bind_args():允许额外关键字参数存在,适合用于参数透传和容器适配
  • 二者都返回 BoundArguments 对象,通过 .arguments 属性获取参数字典

三、Parameter 对象详解

Parameter 对象描述函数签名中的一个具体参数。它包含五个核心属性:namekinddefaultannotationempty 哨兵值。

3.1 创建与访问 Parameter

from inspect import Parameter, Signature # 手动创建一个 Parameter param = Parameter( name='username', kind=Parameter.POSITIONAL_OR_KEYWORD, default=None, annotation=str ) print(param) # 'username: str = None'

3.2 Parameter 的五个核心属性

属性 类型 说明 示例值
name str 参数名称 'username'
kind ParameterKind 参数种类枚举 Parameter.KEYWORD_ONLY
default Any 参数的默认值 NoneParameter.empty
annotation Any 参数的类型注解 strParameter.empty
empty sentinel 表示"未设置"的哨兵值 Parameter.empty
sig = inspect.signature(greet) for name, param in sig.parameters.items(): print(f"╔══ {name} ══╗") print(f" name: {param.name}") print(f" kind: {param.kind.name} ({param.kind.value})") print(f" default: {param.default!r}") print(f" annotation: {param.annotation!r}") # 输出结果: # ╔══ name ══╗ # name: name # kind: POSITIONAL_OR_KEYWORD (1) # default: # annotation: # ╔══ age ══╗ # name: age # kind: POSITIONAL_OR_KEYWORD (1) # default: 18 # annotation: # ╔══ tags ══╗ # name: tags # kind: VAR_POSITIONAL (2) # default: # annotation: # ╔══ verbose ══╗ # name: verbose # kind: KEYWORD_ONLY (3) # default: False # annotation:

四、参数种类(Parameter.kind)的深入理解

Python 支持五种参数种类,Parameter.kind 属性用枚举值标识每个参数的类型。理解这五种参数种类是正确解析函数签名的前提。

4.1 五种参数种类对照表

枚举值 数值 含义 语法形式 示例
POSITIONAL_ONLY 0 仅位置参数 / 之前 def f(a, b, /):
POSITIONAL_OR_KEYWORD 1 位置或关键字参数 普通参数 def f(a, b):
VAR_POSITIONAL 2 可变位置参数 *args def f(*args):
KEYWORD_ONLY 3 仅关键字参数 **args 之后 def f(*, key):
VAR_KEYWORD 4 可变关键字参数 **kwargs def f(**kw):

4.2 完整示例:展示所有参数种类

def demo( a, # POSITIONAL_ONLY -> 在 / 之前 b, # POSITIONAL_ONLY /, c, # POSITIONAL_OR_KEYWORD d=10, # POSITIONAL_OR_KEYWORD *e, # VAR_POSITIONAL f, # KEYWORD_ONLY g="hello", # KEYWORD_ONLY **h # VAR_KEYWORD ): pass sig = inspect.signature(demo) for name, p in sig.parameters.items(): print(f"{name:6s} -> {p.kind.name:25s} (value={p.kind.value})") # 输出: # a -> POSITIONAL_ONLY (value=0) # b -> POSITIONAL_ONLY (value=0) # c -> POSITIONAL_OR_KEYWORD (value=1) # d -> POSITIONAL_OR_KEYWORD (value=1) # e -> VAR_POSITIONAL (value=2) # f -> KEYWORD_ONLY (value=3) # g -> KEYWORD_ONLY (value=3) # h -> VAR_KEYWORD (value=4)

参数种类的实用判断:

利用 Parameter.kind 的枚举比较,可以精确识别参数类型:

# 判断参数是否只能通过位置传入 def is_positional_only(param: Parameter) -> bool: return param.kind == Parameter.POSITIONAL_ONLY # 判断是否为可变参数 def is_variadic(param: Parameter) -> bool: return param.kind in (Parameter.VAR_POSITIONAL, Parameter.VAR_KEYWORD) # 判断是否需要默认值(没有默认值的参数是必须传的) def is_required(param: Parameter) -> bool: return param.default is Parameter.empty and not is_variadic(param)

五、利用签名处理装饰器中的函数参数

编写装饰器时,保留被装饰函数的签名是一个常见但容易被忽视的需求。使用 functools.wraps 只能保留基本的函数元信息(如 __name____doc__),但无法保留签名。下面通过几个典型案例展示如何用 inspect 模块优雅地处理装饰器签名问题。

5.1 基础案例:参数校验装饰器

import inspect from functools import wraps def validate_types(func): """装饰器:在函数调用前验证参数类型是否匹配注解""" sig = inspect.signature(func) @wraps(func) def wrapper(*args, **kwargs): # 将传入参数绑定到签名 bound = sig.bind(*args, **kwargs) bound.apply_defaults() # 应用默认值 # 遍历每个参数,检查类型 for name, value in bound.arguments.items(): param = sig.parameters[name] if param.annotation is not Parameter.empty: expected_type = param.annotation if not isinstance(value, expected_type): raise TypeError( f"参数 '{name}' 期望类型 {expected_type.__name__}, " f"实际得到 {type(value).__name__}" ) return func(*args, **kwargs) return wrapper @validate_types def add(x: int, y: int) -> int: return x + y print(add(3, 5)) # 8 print(add("3", 5)) # TypeError: 参数 'x' 期望类型 int, 实际得到 str

5.2 进阶案例:保留签名信息的日志装饰器

import inspect import logging from functools import wraps logger = logging.getLogger(__name__) def logged(func): """装饰器:记录函数调用的参数名称和值""" sig = inspect.signature(func) @wraps(func) def wrapper(*args, **kwargs): bound = sig.bind(*args, **kwargs) bound.apply_defaults() # 构建有意义的日志信息:显示参数名=值对 args_repr = ", ".join( f"{name}={value!r}" for name, value in bound.arguments.items() ) logger.debug(f"调用 {func.__name__}({args_repr})") return func(*args, **kwargs) return wrapper @logged def process_order( order_id: int, items: list, *, discount: float = 0.0, express: bool = False ) -> dict: return {"order_id": order_id, "total": len(items) * (1 - discount)} # 调用时,日志输出: # 调用 process_order(order_id=42, items=['a','b'], discount=0.1, express=False) result = process_order(42, ["a", "b"], discount=0.1)

5.3 高阶:运行时参数注入

import inspect from functools import wraps def inject_context(context_provider): """装饰器工厂:从函数签名中识别需要注入的参数,自动注入上下文""" def decorator(func): sig = inspect.signature(func) ctx_params = { name: param for name, param in sig.parameters.items() if param.annotation is not Parameter.empty and getattr(param.annotation, '__injectable__', False) } @wraps(func) def wrapper(*args, **kwargs): context = context_provider() # 只注入那些未由调用者显式提供的参数 for name in ctx_params: if name not in kwargs: kwargs[name] = getattr(context, name, None) return func(*args, **kwargs) return wrapper return decorator

六、签名在依赖注入框架中的应用

依赖注入(Dependency Injection, DI)是现代框架中管理组件依赖关系的核心技术。函数签名是DI框架的"眼睛"——框架通过签名来"看"清一个函数或类需要哪些依赖,以及这些依赖的类型是什么。

6.1 一个迷你依赖注入容器

import inspect from inspect import Parameter class Container: """一个基于函数签名的迷你依赖注入容器""" def __init__(self): self._providers = {} # type -> callable def register(self, abstract_type, provider): """注册类型及其提供者""" self._providers[abstract_type] = provider def resolve(self, func): """解析函数的依赖,返回注入后的可调用对象""" sig = inspect.signature(func) def wrapper(*args, **kwargs): # 绑定用户显式传入的参数 bound = sig.bind_partial(*args, **kwargs) # 检查缺失的参数,通过签名进行依赖注入 for name, param in sig.parameters.items(): if name not in bound.arguments: annotation = param.annotation if annotation is not Parameter.empty and annotation in self._providers: # 自动注入依赖 provider = self._providers[annotation] if callable(provider): # 提供者本身可能也需要依赖注入 kwargs[name] = self.resolve(provider)() else: kwargs[name] = provider return func(*args, **kwargs) return wrapper # 使用示例 class Database: def query(self, sql): return f"结果: {sql}" class UserService: def __init__(self, db: Database): self.db = db def get_user(self, user_id: int): return self.db.query(f"SELECT * FROM users WHERE id={user_id}") container = Container() container.register(Database, Database()) container.register(UserService, UserService) # 依赖自动注入 @container.resolve def handle_request(service: UserService, user_id: int): return service.get_user(user_id) print(handle_request(user_id=42)) # 结果: SELECT * FROM users WHERE id=42

这段代码展示了DI框架的核心模式:通过 inspect.signature(func) 获取函数签名,利用 bind_partial() 分离用户已传入的参数和缺失的参数,再根据 param.annotation 查找已注册的提供者,自动完成依赖注入。许多工业级框架(如 FastAPI、Flask-Injector、dependency-injector)的底层原理与此类似。

DI框架中的签名应用模式:

  • 类型匹配:通过 param.annotation 匹配注册的依赖类型
  • 默认值检测param.default is Parameter.empty 判断依赖是否可选
  • 参数名注入:某些框架(如 AngularJS)也通过参数名进行注入
  • 递归解析:依赖的提供者本身可能也有依赖,需要递归签名解析
  • 生命周期管理:通过参数注解区分单例、工厂、瞬态等作用域

七、函数的注解获取

Python 函数注解(Function Annotations)是附加在参数和返回值上的元数据。除了通过 inspect.signature 获取外,还可以通过 __annotations__ 属性直接访问。

7.1 __annotations__ 属性

def process( name: str, count: int = 1, tags: list[str] = None ) -> list[dict]: """处理数据并返回结果列表""" return [{"name": name, "count": count, "tags": tags}] # 方式1: 通过 __annotations__ 字典 print(process.__annotations__) # {'name': , 'count': , 'tags': list[str], 'return': list[dict]} # 方式2: 通过 inspect.signature (更推荐:包含更多上下文) sig = inspect.signature(process) for name, param in sig.parameters.items(): print(f"{name}: annotation={param.annotation!r}") print(f"return: annotation={sig.return_annotation!r}") # 注意区别: # 1. __annotations__ 返回原始字典,参数名是字符串键 # 2. inspect.signature 返回 Signature 对象,可以区分参数种类、默认值等 # 3. __annotations__ 不包含 "无注解" 信息,而 signature 用 Parameter.empty 标识 # 4. signature 會正确处理继承和装饰器链中的注解

7.2 获取嵌套注解(PEP 593 -- Annotated)

from typing import Annotated, get_type_hints class Validator: def __init__(self, min_value: int, max_value: int): self.min_value = min_value self.max_value = max_value def set_temperature( value: Annotated[float, Validator(-273.15, 10000)] ) -> None: pass # 使用 get_type_hints 获取完整注解信息 hints = get_type_hints(set_temperature, include_extras=True) print(hints) # {'value': typing.Annotated[float, Validator], 'return': None} # 提取 Annotated 中的元数据 annotated_type = hints['value'] print(getattr(annotated_type, '__metadata__', [])) # [Validator]

八、__signature__ 自定义属性

Python 允许通过设置 __signature__ 属性来显式覆盖一个可调用对象的签名。这是实现"伪函数"和"参数适配"的核心技术。

__signature__ 的作用机制:

inspect.signature() 检测到一个可调用对象时,它会优先检查该对象是否定义了 __signature__ 属性。如果有,则直接返回该属性值,而不进行自动推导。这让我们可以"伪造"或"覆盖"一个函数的签名信息。

8.1 基本用法

import inspect from inspect import Parameter, Signature # 创建一个"看起来"需要两个字符串参数的函数 def hidden_func(*args, **kwargs): """实际实现:处理任意参数""" return args, kwargs # 伪造签名:让外界认为它接受 (username: str, password: str) -> bool hidden_func.__signature__ = Signature( parameters=[ Parameter('username', Parameter.POSITIONAL_OR_KEYWORD, annotation=str), Parameter('password', Parameter.POSITIONAL_OR_KEYWORD, annotation=str), ], return_annotation=bool ) sig = inspect.signature(hidden_func) print(sig) # (username: str, password: str) -> bool # inspect 会被 __signature__ 欺骗 print(sig.parameters['username'].annotation) # print(sig.return_annotation) #

8.2 实际应用:参数适配器

import inspect from inspect import Parameter, Signature class FunctionAdapter: """将旧版函数接口适配为新版签名的适配器""" def __init__(self, func, param_mapping: dict): self._func = func self._mapping = param_mapping # 根据映射关系生成新的签名 original_sig = inspect.signature(func) new_params = [] for old_name, new_name in param_mapping.items(): original_param = original_sig.parameters[old_name] new_params.append( Parameter( name=new_name, kind=original_param.kind, default=original_param.default, annotation=original_param.annotation ) ) for name, param in original_sig.parameters.items(): if name not in param_mapping: new_params.append(param) self.__signature__ = Signature( parameters=new_params, return_annotation=original_sig.return_annotation ) def __call__(self, **kwargs): # 将新参数名映射回旧参数名 reverse_map = {v: k for k, v in self._mapping.items()} mapped = {} for name, value in kwargs.items(): old_name = reverse_map.get(name, name) mapped[old_name] = value return self._func(**mapped) # 使用示例:旧版 API 使用 create_user,新版改为 register_user def create_user(name, email, age): return f"创建用户: {name}, {email}, {age}" adapter = FunctionAdapter(create_user, param_mapping={"name": "username"}) print(inspect.signature(adapter)) # (username, email, age) result = adapter(username="Alice", email="alice@example.com", age=30) print(result) # 创建用户: Alice, alice@example.com, 30

__signature__ 的典型应用场景:

  • API 兼容层:重构函数签名后保持对调用方的透明兼容
  • 测试替身:mock/stub 对象暴露与被替身一致的签名
  • 框架集成:让自定义的可调用对象能被框架的签名解析器正确识别
  • 代码生成:动态生成的函数需要正确的签名信息
  • CLI 工具:根据自定义签名自动生成命令行参数解析器

九、inspect 模块的其他签名相关工具

9.1 getfullargspec:传统方式的参数检查

import inspect def sample(a, b=10, *args, c, **kwargs): pass # getfullargspec 返回一个 FullArgSpec 对象 spec = inspect.getfullargspec(sample) print(f"args: {spec.args}") # ['a', 'b'] print(f"varargs: {spec.varargs}") # 'args' print(f"varkw: {spec.varkw}") # 'kwargs' print(f"defaults: {spec.defaults}") # (10,) print(f"kwonlyargs: {spec.kwonlyargs}") # ['c'] print(f"annotations:{spec.annotations}")# {} # 注:getfullargspec 在新代码中已被 inspect.signature 取代 # Signature 对象提供了更统一、更强大的接口

9.2 getsource 与 getsourcelines

# 获取函数的源代码 print(inspect.getsource(greet)) # def greet(name: str, age: int = 18, *tags: str, verbose: bool = False) -> str: # """向某人问好""" # msg = f"Hello, {name}!" # ... # 获取源代码的行号和文件路径 lines, line_no = inspect.getsourcelines(greet) print(f"起始行: {line_no}") print(f"文件: {inspect.getfile(greet)}")

十、最佳实践与常见陷阱

最佳实践

  1. 优先使用 inspect.signature:相比 getfullargspec,Signature/Parameter API 更统一、类型信息更完整,且支持 __signature__ 自定义
  2. 总是调用 apply_defaults():在使用 bind() 后调用 apply_defaults() 可以填充默认值,避免后续判断
  3. 使用 bind_partial() 进行选择性绑定:在需要部分绑定参数时(如依赖注入),bind_partial 不会因缺少数值而报错
  4. 处理 C 扩展函数:部分用 C 实现的內建函数没有可提取的签名,需要 try/except 捕获 ValueError
  5. 利用 Parameter.empty 进行判断:判断是否有注解或默认值时,始终使用 is Parameter.empty,不要用 is None

常见陷阱

  • 装饰器不保留签名:仅用 @wraps 不够,如果是复杂的参数变换,需要用 __signature__ 显式更新或使用 makefundecorator 等第三方库
  • 类型注解只是元数据:Python 不会强制检查类型注解,即使注解声明为 int,传入字符串也不会报错
  • 泛型注解的提取:对于 list[int]dict[str, Any] 等泛型,需要额外调用 typing.get_origin()typing.get_args() 来分解
  • get_type_hints() 的字符串解析:对于包含前向引用(字符串注解)的函数,需要用 typing.get_type_hints() 替代直接访问 __annotations__
  • 性能考虑inspect.signature() 有缓存机制,但在热路径中频繁调用仍可能造成性能问题,建议在装饰器工厂中预先提取签名

十一、核心要点总结