古法编程·新解:Python 类型注解的"一箭三雕"之术

Python 的类型注解,比你想象的更强大。它不只是给 IDE 看的提示,更是一个可以在运行时被读取、被解释、被执行的元数据容器。本文从「一个注解,三种用途」切入,揭秘 Python 运行时、mypy 静态检查、FastAPI 动态验证各自如何消费注解;深入 Annotated 的底层实现,手写依赖注入装饰器,带你理解 FastAPI 推荐风格的核心原理。读完你会明白:为什么说 Python 的类型注解是「便利贴」------语言不管,但框架玩得飞起。

一个注解,三种用途

类型注解编译后保留在 __annotations__ 中,Python 运行时不管它,mypy 在运行前用它做静态检查,FastAPI/pydantic 在运行时主动读取它做动态验证。 同一个注解,三个东西各自用。

arduino 复制代码
源码:   def add(a: int, b: int) -> int: ...
              │
              ▼ 编译
字节码:  保留注解 → __annotations__ = {'a': int, 'b': int, 'return': int}
              │
    ┌─────────┼─────────┐
    ▼         ▼         ▼
 运行时    mypy      FastAPI / pydantic
 忽略     静态检查    动态读取注解做验证/转换
角色 怎么用注解
Python 运行时 完全不看,只存着
mypy / pyright 静态分析,不运行代码,检查类型是否一致
FastAPI / pydantic 运行时读取 __annotations__,用它做参数验证和转换
python 复制代码
def get_user(user_id: int) -> str:
    ...

# 1. 运行时:Python 不管
get_user("abc")  # 照跑,但逻辑可能崩

# 2. 静态检查:mypy 分析源码
# mypy 报错:Argument 1 to "get_user" has incompatible type "str"; expected "int"

# 3. 动态读取:FastAPI 运行时用 __annotations__ 做验证
# 自动把 "123" 转成 123,如果传 "abc" 则返回 422 错误

Java视角深入理解Python类型注解的设计哲学

Python 的类型注解是便利贴------贴在那,谁想读谁读,做什么随你。Python 选择了极度自由,代价是框架各自造轮子

Python 把"注解是什么意思"这个责任,从语言层面推给了库作者。 好处是极度灵活,代价是不看文档就不知道注解的实际行为

python 复制代码
def f(x: int):
    ...

# FastAPI 的理解:x 是查询参数,从 URL 解析并转成 int
# pydantic 的理解:x 必须是 int,否则验证失败并报错
# mypy 的理解:x 应该传 int,我来静态检查
# Python 的理解:x 随便是什么,关我屁事

Annotated

给类型贴标签的容器

元数据可以是任何对象 ------字符串、类、函数、Depends 实例,Python 不管,由框架自己解释。

python 复制代码
from typing import Annotated

# 语法:Annotated[真实类型, 元数据1, 元数据2, ...]
x: Annotated[int, "范围 0-100", "必填"]
#            │      │            │
#            │      └── 元数据(任何 Python 对象)
#            └── 真实类型

# 拆开看
x.__origin__     # <class 'int'>
x.__metadata__   # ('范围 0-100', '必填')

其中 __metadata__ 是其特有属性

python 复制代码
# 检查是否为 Annotated 类型
if hasattr(param_type, '__metadata__'):
    # 提取元数据
    metadata = param_type.__metadata__

一个简单案例,利用函数本身的元数据来给自己的函数参数赋值

主要是熟悉怎么获取函数的 Annotated 类型注解,以及获取它的元数据

python 复制代码
import functools
from typing import Annotated, get_type_hints

def happy_birthday(name: Annotated[str, ("在深圳", 18), "希望你天天开心"],
                   age,
                   address,
                   msg) -> str:
    """通过获取 Annotated 里面的元数据类进行赋值"""
    if age and address and msg:
        print(f"🎂 祝 {name} {age}岁生日快乐!你现在在{address},{msg}")
    else:
        print("happy_birthday", name, age, address, msg)
    print(f"祝福已发送给 {name}")


# {'name': typing.Annotated[str, ('在深圳', 18), '希望你天天开心'], 'return': <class 'str'>}
hints = get_type_hints(happy_birthday, include_extras=True)

for name, annotation in hints.items():
    # 获取 Annotated 类型注解
    if hasattr(annotation, "__metadata__"):
        (address, age), msg = getattr(annotation, "__metadata__")
        happy_birthday = functools.partial(happy_birthday, age=age, address=address, msg=msg)

happy_birthday("Pkmer")

"""
🎂 祝 Pkmer 18岁生日快乐!你现在在在深圳,希望你天天开心
祝福已发送给 Pkmer
"""

利用类型注解实现依赖注入

模仿 FastAPI 推荐的 Annotated,实现依赖注入 user: Annotated[dict, DependsOn(get_user)]

python 复制代码
import functools
import inspect
from collections.abc import Callable
from typing import ParamSpec, TypeVar, Annotated, get_type_hints

P = ParamSpec('P')
R = TypeVar('R')


class DependsOn:
    def __init__(self, func: Callable):
        self.func = func


def my_inject(func):
    """依赖注入装饰器"""
    sig = inspect.signature(func)

    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        """包装函数"""
        # 获取函数的参数类型
        parameters = sig.parameters
        # include_extras 对 Annotated 携带元数据的支持,获取所有参数类型
        hints = get_type_hints(func, include_extras=True)
        # 先绑定用户先传递的参数
        bound = sig.bind_partial(*args, **kwargs)
        # 遍历参数类型,处理 DependsOn
        for name, annotation in parameters.items():
            # annotation_type = annotation.annotation
            annotation_type = hints.get(name)
            # 找到 Annotated 类型注解
            if annotation_type and hasattr(annotation_type, "__metadata__"):
                for metadata in annotation_type.__metadata__:
                    if isinstance(metadata, DependsOn) and name not in bound.arguments:
                        bound.arguments[name] = metadata.func()
        print(bound.arguments)
        return func(*bound.args, **bound.kwargs)

    return wrapper


def get_user() -> dict:
    return {"name": "Pkmer"}


@my_inject
def greet(user: Annotated[dict, DependsOn(get_user)],
          book="Learn Python Programming 3rd Edition",
          *,
          msg: str):
    print(f"{user.get("name")} {msg} {book}")

greet(msg="在深圳,听歌")

参考链接

相关推荐
吃好睡好便好1 小时前
在Matlab中绘制三维等高线图
开发语言·python·学习·算法·matlab·信息可视化
keineahnung23451 小时前
PyTorch symbolic_shapes 模組的 is_contiguous 從哪來?── sizes_strides_user 安裝與實作解析
人工智能·pytorch·python·深度学习
C137的本贾尼1 小时前
别怕异步:`async` 和 `await` 的简单理解
开发语言·python
__log2 小时前
ComfyUI 集成技术方案分析报告
javascript·python·django
有味道的男人2 小时前
1688 商品价格 API:阶梯价、代发价、批发价实时查询
开发语言·windows·python
范范@2 小时前
python基础-for循环和列表
开发语言·python
Nightwatchman2 小时前
收藏了两年 AI 编程教程,最后能留在手里的几乎没什么
ai编程
小白学大数据2 小时前
Python 爬虫动态 JS 渲染与无头浏览器实战选型指南
开发语言·javascript·爬虫·python