RSS
菜单
全部文章快讯开发科技深度热点

类型注解与 mypy(Python 从精通到入门 · 10)

内容摘要

类型注解不是给解释器看的,是给「下一个读代码的人」和「静态检查工具」看的——它能在一个 Bug 跑到生产环境之前就把它拦下来。

类型注解不是给解释器看的,是给「下一个读代码的人」和「静态检查工具」看的——它能在一个 Bug 跑到生产环境之前就把它拦下来。

你将学到

  • 为什么动态类型语言也要写类型注解,以及它到底解决什么问题
  • 变量、函数、容器的基本标注语法(含 3.10+ 的 | 联合类型)
  • typing 里的高频成员:Optional / Union / Callable / Literal / Protocol / TypeVar
  • from __future__ import annotations 到底在做什么
  • 用 mypy / pyright 做静态检查,以及 dataclasses、pydantic 与类型的关系与边界

前置知识

建议先读完 上一篇:性能剖析与优化,对 Python 对象模型和函数调用有基本认识。

一、为什么要在动态语言里写类型

Python 是动态类型:变量没有类型,值才有类型。这带来灵活性,也带来一个问题——只有运行到那一行,才知道参数类型对不对。

def add_days(days, user): ...   # 3 个月后有人调用 add_days("3", user),没人报错,
                                # 直到线上数据算错,你半夜爬起来查日志

类型注解的核心价值:

  1. 文档化:函数签名自己说明白要什么、返回什么,不用翻注释。
  2. 静态检查:mypy 在运行前就能发现「你传了 str,这里要 int」。
  3. IDE 提示:补全、跳转、重构都依赖类型信息才精准。
  4. 重构安全:改一个函数签名,所有调用点的错误会一次性暴露出来。

注意:注解默认不产生运行时行为。下面这段标注离谱的代码照样能跑——解释器根本不看注解,检查是 mypy 这类工具的事,这是理解后面一切的前提。

def f(x: int) -> str:
    return x        # 类型标错了,Python 照样执行
print(f(1))         # 输出: 1

二、基础标注

变量与常量

name: str = "Ada"
age: int = 36
scores: list[float] = [90.5, 88.0]   # 3.9+ 直接用小写 list
config: dict[str, int] = {"retries": 3}

# 只声明不赋值也合法(mypy 会追踪后续赋值)
result: str
result = "ok"

函数

def greet(name: str, excited: bool = False) -> str:
    suffix = "!" if excited else "."
    return f"Hello, {name}{suffix}"

# 没有返回值的函数标注 -> None
def log(msg: str) -> None:
    print(msg)

print(greet("Ada", True))  # 输出: Hello, Ada!

容器

from collections.abc import Sequence, Mapping

# 精确到元素类型
names: list[str] = ["a", "b"]
ages: tuple[int, str] = (1, "one")        # 定长、类型逐个对应
pair: tuple[int, ...] = (1, 2, 3)         # 变长、同类型

# 抽象类型优先:接收方只关心"能按顺序取"
def first(items: Sequence[str]) -> str:
    return items[0] if items else ""

原则:参数用抽象类型(Sequence / Iterable / Mapping),返回值用具体类型(list / dict)。这样调用方传 list 或 tuple 都行。

三、typing 常用成员

Optional / Union / |

from typing import Optional

def find(name: str) -> Optional[str]:   # 返回 str 或 None
    return None

# 3.10+ 用 | 更简洁,等价写法
def parse(text: str) -> int | None:
    return int(text) if text.isdigit() else None

# 多类型:int | float 对应旧的 Union[int, float]
Num = int | float | str

X | None 就是 Optional[X]。谨慎用 None 表示"找不到"——要让所有调用方都记得判空,漏一个就是 AttributeError。

Callable

from typing import Callable

def apply(fn: Callable[[int, int], int], a: int, b: int) -> int:
    return fn(a, b)

print(apply(lambda x, y: x + y, 2, 3))  # 输出: 5

Callable[[参数类型...], 返回类型],参数为空写 Callable[[], int]。

Literal

限制只能是某几个字面值,比 str 严格得多。

from typing import Literal

def set_level(level: Literal["debug", "info", "error"]) -> None:
    ...

set_level("info")    # ✅
# set_level("warn")  # mypy 报错:不在允许的字面值里

TypeVar 与泛型

想表达「传进来什么类型,就返回什么类型」,用 TypeVar。

from typing import TypeVar

T = TypeVar("T")

def first_or(items: list[T], default: T) -> T:
    return items[0] if items else default

reveal_type(first_or([1, 2], 0))     # mypy 推断为 int
reveal_type(first_or(["a"], "z"))    # 推断为 str

3.12+ 可以直接在函数名后声明类型参数:def first_or[T](items: list[T], default: T) -> T: ...。

Protocol:结构化子类型

不用继承,只要「长得像」就算数。这叫鸭子类型的类型化表达。

from typing import Protocol

class Sized(Protocol):
    def __len__(self) -> int: ...

def show_length(obj: Sized) -> int:
    return len(obj)

show_length([1, 2, 3])   # ✅ list 有 __len__,天然满足
show_length("abc")       # ✅ str 也满足

Protocol 是解耦利器:你的函数依赖的是「能力」而不是「某个基类」。标准库里 collections.abc 的 Iterable、Sequence 本质上就是这么定义的。

四、from __future__ import annotations

在模块顶部写这一行,所有注解都会变成字符串,不再在运行时求值。

from __future__ import annotations

class Node:
    def __init__(self, child: Node | None = None):  # 3.9 上也能跑
        self.child = child

它解决两件事:

  1. 前向引用:类里引用还没定义完的类,不用再加引号 "Node"。
  2. 新语法降级:3.9 也能写 int | None(在注解位置)。

代价:运行时拿注解要调 typing.get_type_hints() 解析字符串,直接读 __annotations__ 得到的是字符串。

五、静态检查:mypy / pyright

装好直接跑,最基础的用法就是「指向包或文件」。

pip install mypy
mypy src/                # 检查整个目录
mypy --strict src/       # 严格模式,推荐新项目开启

配置写进 pyproject.toml:

[tool.mypy]
python_version = "3.11"
strict = true
warn_unused_ignores = true
exclude = ["tests/"]

[[tool.mypy.overrides]]
module = "some_untyped_lib.*"      # 没有类型标注的第三方库
ignore_missing_imports = true

pyright 是微软出的另一个检查器(VSCode 内置 Pylance 的底座),速度更快、对推断更强:

pip install pyright
pyright src/

两者选一个统一在团队里用,别混着跑,否则规则打架。mypy 生态老、插件多;pyright 快、开箱体验好。调试推断结果时可以用 reveal_type(x),mypy 会打印它推断出的类型。

六、dataclasses 与 pydantic 的边界

@dataclass 用注解生成 __init__ / __repr__ / __eq__,mypy 完全能理解它:

from dataclasses import dataclass

@dataclass
class Point:
    x: float
    y: float

p = Point(1.0, 2.0)
# Point("1", "2")  # 运行时不报错,但 mypy 会拦下

关键点:dataclass 的注解是给静态检查器用的,运行时不会做任何校验。传错类型照样进对象。

而 pydantic 在 __init__ 时真正校验、转换数据:

from pydantic import BaseModel, ValidationError

class User(BaseModel):
    name: str
    age: int

u = User(name="Ada", age="36")   # 注意:字符串会被强制转成 int
print(u.age, type(u.age))        # 输出: 36 <class 'int'>

try:
    User(name="Ada", age="abc")
except ValidationError as e:
    print("校验失败")            # 输出: 校验失败

边界怎么划:

  • 内部的数据结构、函数签名 → 类型注解 + mypy,够用且零运行开销。
  • 系统边界(读配置、接 API 请求、解析外部 JSON)→ pydantic,必须做运行时校验。
  • dataclass 想加校验可以配 pydantic.dataclasses,但别自己手写一堆 __post_init__ 打补丁。

常见坑

坑 1:把注解当成运行时校验

# ❌ 以为运行时会检查
def double(x: int) -> int:
    return x * 2

double("ab")   # 输出: abab,没人拦你
# ✅ 需要运行时校验就用 pydantic,或手动 assert
def double(x: int) -> int:
    if not isinstance(x, int):
        raise TypeError(f"expected int, got {type(x).__name__}")
    return x * 2

坑 2:可变默认参数(和类型无关,但常和 dataclass 一起踩)

# ❌ 所有实例共享同一个 list
@dataclass
class Bag:
    items: list = []
# ✅ 用 field(default_factory=...)
from dataclasses import field

@dataclass
class Bag:
    items: list[str] = field(default_factory=list)

小结

  • 类型注解默认无运行时行为,它的价值在静态检查、IDE 和文档化。
  • 基础标注覆盖变量/函数/容器;参数偏好抽象类型,返回值用具体类型。
  • Optional = X | None;Callable 描述函数;Literal 限制字面值;TypeVar 表达泛型;Protocol 表达结构化鸭子类型。
  • from __future__ import annotations 让注解惰性化,解决前向引用并支持新语法降级。
  • mypy / pyright 二选一,新项目开 strict;用 reveal_type 排查推断。
  • dataclass 的注解不校验运行时;边界数据用 pydantic 做真校验。

延伸阅读

  • PEP 484 / 526 / 604 / 612 —— 类型注解的官方定义演进
  • mypy 官方文档的 --strict 检查项清单
  • 《Fluent Python》第二版第 8 章:类型提示

上一篇:性能剖析与优化 · 下一篇:包管理与虚拟环境

— 全文完 —回到顶部 ↑
下载推广海报

文章推广海报

《类型注解与 mypy(Python 从精通到入门 · 10)》完整推广海报
DISCUSSION

文章回复

0 条公开回复
未登录回复需要审核后公开
还没有回复,欢迎参与讨论。