在最近的开发中,我越来越多地使用 Pydantic 来进行字段校验。而在使用 Pydantic 的过程中,类型注解(type hint)几乎是必不可少的。其中一个经常出现的类型工具就是 typing.Annotated
。
起初,我在阅读 官方文档 时对它的用途感到困惑,看了半天也没搞懂它到底是做什么用的。后来在网上查阅了一些资料,终于理解了它的基本用途和背后的设计理念。于是,我决定将自己的理解总结下来,记录在这篇博客中,方便自己回顾,也希望能帮到同样疑惑的你。
Annotated 是什么?
在 Python 中,Annotated
允许开发者为类型声明附加元数据。语法如下:
1 2
| var_name: Annotated[T, x]
|
来看一个简单的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| from typing import Annotated, TypedDict, get_type_hints
class A(TypedDict): name: Annotated[str, "first letter is capital"]
get_type_hints(A)
get_type_hints(A, include_extras=True)
get_type_hints(A, include_extras=True)['name'].__metadata__[0]
|
可以看到,Annotated
本身并不会影响类型本身的行为,它的作用只是附加一些元数据。这些元数据可以通过 .__metadata__
属性在运行时获取。但要真正“用起来”,还需要其他工具或框架来解析这些元数据并据此执行某些逻辑。
Annotated 在 Pydantic 和 FastAPI 中的应用
在使用 Pydantic 的框架中,Annotated
被广泛用于表达校验规则。例如在 FastAPI 中,你可以用它来传递额外的校验信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| from typing import Annotated from fastapi import Header, Query
def read_items(q: Annotated[str, Query(max_length=50)]): pass
def UserIdHeader(alias: str | None = None, **kwargs): if alias is None: alias = "X-Forwarded-User" return Header(alias=alias, **kwargs)
UserIdHeaderDep = Annotated[str | None, UserIdHeader()]
|
在 Pydantic 模型中定义校验规则
你还可以在 Pydantic 模型中使用 Annotated 来定义字段的校验规则:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| from typing import Annotated from pydantic import BaseModel, Field, field_validator
class Model(BaseModel): x: int
@field_validator("x") def between_2_and_20(cls, v: int) -> int: if 2 < v < 20: return v raise ValueError(f"{v} is not between 2 and 20.")
class Model2(BaseModel): x: Annotated[int, Field(lt=20, gt=2)]
|
这是因为 Pydantic 内部能够解析 Annotated
中的 Field
并据此自动生成验证器,简化了手动定义校验逻辑的过程。
更高级的用法:函数作为元数据
Annotated
不仅可以添加静态信息,甚至可以附加函数。比如在 langgraph
库中就使用这种方式来实现动态行为。下面是一个简化示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| from typing import Annotated, TypedDict, Callable, get_type_hints
def reducer(a: list, b: float) -> list: return a + [b]
class State(TypedDict): x: Annotated[list, reducer]
class Graph: def __init__(self, schema: type): self.metadata = { name: typ.__metadata__[0] for name, typ in get_type_hints(schema, include_extras=True).items() }
def add_node(self, func: Callable) -> None: self.func = func
def invoke(self, input_: dict[str, float]) -> dict[str, list[float]]: output = {k: [v] for k, v in input_.items()} for k, v in output.items(): processed = self.func(output) output[k] = self.metadata[k](output[k], processed[k]) return output
graph = Graph(State) graph.add_node(lambda x: {"x": x["x"][-1] + 1}) print(graph.invoke({"x": 0.5}))
|
这个例子展示了一个强大的用法:将函数作为元数据附加到类型上,在运行时动态处理字段的值。
总结
typing.Annotated
是 Python 类型系统中一个非常灵活的工具,它本身不会做任何“实事”,但提供了一种标准方式来附加元数据。只要配合能够解析它的框架或代码逻辑,就能实现非常强大的功能。在 Pydantic、FastAPI、LangGraph 等现代 Python 工具链中,它都发挥着关键作用。
希望这篇文章能帮你更好地理解并应用 Annotated
,让你的类型注解既“好看”又“有用”。