怎么通过__annotations__获取Python函数参数类型信息
详细讲解如何通过Python内置的__annotations__属性获取函数的参数类型信息,包含基础用法、过滤返回值、延迟求值处理及与标准库推荐方案的对比。
在 Python 3 中,函数参数类型注解(Type Hints)不仅能辅助编辑器做代码补全和静态类型检查,还会作为元数据直接绑定在函数对象上。最直接、轻量获取这些类型信息的方式,就是访问函数对象自带的内置字典属性 __annotations__。本文将从基本访问语法、提取纯参数信息、规避常见陷阱以及现代推荐方案四个方面,详细讲解如何准确获取函数参数的类型信息。
一、直接访问 __annotations__ 的基本方法
Python 函数在定义阶段如果包含类型注解,解释器就会自动创建一个名为 __annotations__ 的字典,并将参数名及其对应的类型对象保存在其中。如果函数声明了返回值类型,其类型信息会存放在固定的特殊键 'return' 中。
def calculate_fee(amount: float, rate: float = 0.05) -> float:
return amount * rate
# 直接访问函数的 __annotations__ 属性
annotations = calculate_fee.__annotations__
print(annotations)
# 示例输出:
# {'amount': <class 'float'>, 'rate': <class 'float'>, 'return': <class 'float'>}
从输出可以看出,__annotations__ 是一个标准的 Python 字典。字典的键是参数名称(字符串),值则是定义函数时指定的类型对象(例如 float、int 或自定义类)。如果某个参数没有声明类型注解,该参数将不会出现在该字典中。
二、提取纯参数类型信息的规范步骤
在实际开发(如参数校验、API 自动文档生成)中,我们通常只需要“参数”的类型,而需要剥离“返回值”的类型。标准提取流程包括:安全获取属性、剥离 'return' 键、以及遍历键值对。
步骤 1:安全检查属性是否存在
虽然大部分普通函数都有 __annotations__,但一些由 C 语言编写的内置函数、第三方扩展对象或通过特殊动态代理封装的函数可能未定义该属性。使用 getattr(func, '__annotations__', None) 可以避免触发 AttributeError。
步骤 2:过滤排除 'return' 键
'return' 是 Python 专门保留用于存储函数返回类型的字典键,它不是函数的形参。我们可以通过字典推导式快速将真正的参数类型筛选出来。
from typing import Callable, Any, Dict
def get_parameter_annotations(func: Callable[..., Any]) -> Dict[str, Any]:
"""安全提取函数的纯参数类型注解"""
raw_annotations = getattr(func, '__annotations__', {})
if not raw_annotations:
return {}
# 剥离 'return' 键,仅保留实际参数类型
return {
param_name: param_type
for param_name, param_type in raw_annotations.items()
if param_name != 'return'
}
# 验证测试
def register_user(username: str, age: int, is_active: bool = True) -> int:
return 1001
params = get_parameter_annotations(register_user)
print(params)
# 示例输出:
# {'username': <class 'str'>, 'age': <class 'int'>, 'is_active': <class 'bool'>}
三、容易混淆的边界情况与直接访问的局限
虽然直接读取 func.__annotations__ 语法简单直观,但在复杂或高版本 Python 环境下,往往会遇到以下三个容易混淆的问题:
1. 延迟求值导致的“字符串化”陷阱
在 Python 3.7+ 中,如果代码开头引入了 from __future__ import annotations(PEP 563),或者在将来某个版本的默认规则下,所有的类型注解都不会在定义时立即求值,而是被直接编译为纯字符串。此时直接访问 __annotations__ 得到的不是 <class 'int'>,而是 'int':
from __future__ import annotations
def calculate(x: int) -> int:
return x * 2
print(calculate.__annotations__)
# 输出为字符串形态:
# {'x': 'int', 'return': 'int'}
# 此时 isinstance(val, calculate.__annotations__['x']) 将直接抛出 TypeError!
2. 未添加类型注解的参数直接缺席
许多初学者误以为 __annotations__ 能列出函数的所有参数。但事实上,__annotations__ 仅仅记录“有注解”的项目。例如对于 def send(target, message: str): pass,字典中只有 'message',没有 'target'。如果你的逻辑依赖参数顺序或参数总数,单凭 __annotations__ 就会遗漏非注解参数。
3. 直接修改字典可能导致全局副作用
直接通过 func.__annotations__ 获取的是该函数的原生字典引用(或特定状态的直接映射)。如果在业务代码中执行 del annotations['return'],可能会直接修改函数本身的元数据,破坏其他第三方库或工具对该函数的后续内省。
四、更健壮的标准库替代方案
为了解决上述边界问题,Python 官方在标准库中提供了更加完备的内省工具。理解它们与 __annotations__ 的关系有助于在不同场景下做出合理选型。
1. 使用 inspect.get_annotations(Python 3.10+ 推荐)
从 Python 3.10 开始,PEP 563 正式推荐使用 inspect.get_annotations 替代直接访问 __annotations__。它不仅能够跨版本安全访问,还能通过 eval_str=True 参数自动将字符串格式的注解解析为真实类型:
import inspect
def process_order(order_id: int, tags: list[str]) -> bool:
return True
# 自动处理并安全获取注解
annotations = inspect.get_annotations(process_order, eval_str=True)
param_types = {k: v for k, v in annotations.items() if k != 'return'}
print(param_types)
# 示例输出: {'order_id': <class 'int'>, 'tags': list[str]}
2. 使用 typing.get_type_hints 进行深度类型还原
如果需要在低版本(如 Python 3.7 - 3.9)或复杂类型(包含前向引用、字符串类型声明)下精确求值类型对象,使用 typing.get_type_hints 是最经典的做法:
import typing
class CustomNode:
pass
def link_node(current: "CustomNode", next_id: int) -> None:
pass
# typing.get_type_hints 会自动将字符串 "CustomNode" 解析为真正的类对象
hints = typing.get_type_hints(link_node)
print(hints['current']) # 输出: <class '__main__.CustomNode'>
3. 使用 inspect.signature 获取参数全貌
如果业务不仅需要参数类型,还需要参数默认值、位置参数或关键字参数的顺序,建议使用 inspect.signature。inspect.Parameter.annotation 同样读取自该机制:
import inspect
def send_alert(message: str, level: int = 1, timeout=30):
pass
sig = inspect.signature(send_alert)
for name, param in sig.parameters.items():
ann = param.annotation if param.annotation is not inspect.Parameter.empty else "未注解"
default_val = param.default if param.default is not inspect.Parameter.empty else "无默认值"
print(f"参数: {name:<8} 类型: {str(ann):<15} 默认值: {default_val}")
# 示例输出:
# 参数: message 类型: <class 'str'> 默认值: 无默认值
# 参数: level 类型: <class 'int'> 默认值: 1
# 参数: timeout 类型: 未注解 默认值: 30
五、实战示例:基于参数注解的简易运行时校验器
以下示例展示了如何在实际工具中利用提取到的参数类型信息,实现一个轻量级的运行时类型检查装饰器:
import functools
import inspect
def validate_argument_types(func):
"""根据函数参数注解检查入参类型的装饰器"""
# 提取纯参数类型映射
annotations = inspect.get_annotations(func, eval_str=True)
param_types = {k: v for k, v in annotations.items() if k != 'return'}
@functools.wraps(func)
def wrapper(*args, **kwargs):
# 绑定传入的实参到形参名称
sig = inspect.signature(func)
bound = sig.bind(*args, **kwargs)
bound.apply_defaults()
for name, value in bound.arguments.items():
if name in param_types:
expected_type = param_types[name]
# 针对常规类型对象进行校验
if isinstance(expected_type, type) and not isinstance(value, expected_type):
raise TypeError(
f"参数 '{name}' 类型错误: 期望 {expected_type.__name__}, 实际传入 {type(value).__name__}"
)
return func(*args, **kwargs)
return wrapper
# 测试应用
@validate_argument_types
def set_port(port: int, protocol: str):
print(f"服务运行在端口 {port},协议为 {protocol}")
# 正确调用
set_port(8080, "HTTP")
# 异常调用触发校验(抛出 TypeError)
try:
set_port("8080", "HTTP") # 传入字符串而非整型
except TypeError as err:
print(f"捕获校验异常: {err}")
# 示例输出: 捕获校验异常: 参数 'port' 类型错误: 期望 int, 实际传入 str
六、核心要点总结
- 字典存储机制:
func.__annotations__是 Python 存放类型注解的原生字典,其中参数名为键,对应类型为值。 - 区分返回值:
'return'键专门用来记录返回值类型,获取纯参数信息时需要通过param_name != 'return'进行过滤。 - 非注解参数不会列出:没有显式写类型标注的参数不会记录在字典中;如需全量形参列表,应配合
inspect.signature。 - 延迟求值注意:当存在
from __future__ import annotations时,注解会存储为纯字符串。在 Python 3.10+ 中优先推荐使用inspect.get_annotations(func, eval_str=True),低版本中可使用typing.get_type_hints(func)。
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









