第 8 章 · 类型提示¶
作为资深 Java 工程师,你最怀念的可能是编译期的类型安全网。Python 是动态类型,但通过类型提示(type hints) + 静态检查工具(mypy/pyright),你能拿回接近 Java 的类型安全——而且是渐进式的:想加多少加多少,从关键模块开始。
8.1 类型提示:可选、不强制、给工具看¶
类型注解写在变量、参数、返回值上,运行时不强制(只是元数据):
真正干活的是静态检查器:mypy 或 pyright。它们在"运行前"分析代码,像 Java 编译器那样抓类型错误。
8.2 容器类型注解(泛型)¶
Python 3.9+ 直接用小写容器名做泛型(不用再 from typing import List):
-
Java
-
Python(3.9+)
现代写法
旧代码里你会看到 from typing import List, Dict,那是 3.8 及以前的写法。3.9+ 直接 list[int]、dict[str, int],更简洁。本教程基线 3.13,一律用新写法。
8.3 Optional / 联合类型 X | Y¶
-
Java
-
Python(3.10+)
X | None 是现代写法(等价于旧式 Optional[X])。X | Y | Z 表示"联合类型",比 Java 的类型系统更灵活——一个值可以合法地属于多种类型:
⚠️ Java 程序员的陷阱
Java 里 Integer 默认可空、int 不可空,混在一起很乱。Python 的 int 本身不含"可能为 None"的语义——要表达可空,必须显式写 int | None。这让"是否可能为空"成为签名的一部分,比 Java 清晰。
8.4 Callable:函数的类型¶
Java 用 Function<T,R>/Consumer<T>/Predicate<T>。Python 用 Callable:
from collections.abc import Callable
# 一个接受两个 int、返回 int 的函数
def apply(func: Callable[[int, int], int], a: int, b: int) -> int:
return func(a, b)
apply(lambda x, y: x + y, 1, 2)
不加参数的 Callable[..., int] 表示"任意参数、返回 int"。
8.5 泛型(TypeVar)¶
Java 的 <T>。Python 用 TypeVar:
-
Java
-
Python
有界泛型(bound,对应 Java <T extends Comparable>):
from typing import TypeVar
from numbers import Real
N = TypeVar("N", bound=Real) # N 必须是 Real 的子类
def add(a: N, b: N) -> N: ...
8.6 Protocol:结构化类型(重点)⭐¶
这是 Python 类型系统相对 Java 最有意思的地方。回顾第 4 章:Java 的 interface 是 nominal(必须显式 implements),Python 的 Protocol 是结构化的——有对应方法就算符合,不需要声明。
from typing import Protocol
class Quackable(Protocol):
def quack(self) -> str: ...
def make_sound(x: Quackable) -> str:
return x.quack()
class Duck: # 不写 (Quackable)
def quack(self) -> str: return "quack"
make_sound(Duck()) # ✅ mypy 认可:结构匹配
-
Java nominal
-
Python structural
核心认知
Protocol = "鸭子类型 + 静态检查"。你拿到 Java interface 的类型安全,却不用到处 implements,还能适配改不了的第三方类。这是 Python 类型系统比 Java 更灵活的体现。
运行时可检查的 Protocol(@runtime_checkable)支持 isinstance,但只检查方法是否存在、不检查签名。
8.7 其他常用类型工具¶
from typing import Any, Final, Literal, NoReturn
config: Any = ... # 关闭类型检查(逃生舱,慎用)
MAX_SIZE: Final[int] = 100 # 常量(类似 Java final)
def http_method() -> Literal["GET", "POST"]: ... # 字面值类型
def fail(msg: str) -> NoReturn: # 表示函数永不正常返回(如 sys.exit)
raise SystemExit(msg)
# 类型别名(3.12+ 用 type 语句)
type Vector = list[float]
def norm(v: Vector) -> float: ...
Literal 很有用:Literal["r", "w"] 限定文件打开模式,比 Java 的枚举更轻。
8.8 渐进式采用策略¶
类型提示是渐进式(gradual)的——你可以:
- 先给公共 API 加注解(函数签名),内部实现留
Any。 - 从关键/复杂模块开始,逐步铺开。
- 配合
mypy/pyright在 CI 里跑。
mypy 配置(写在 pyproject.toml):
[tool.mypy]
python_version = "3.13"
strict = true # 最严格(可选,按团队接受度调)
warn_return_any = true
disallow_untyped_defs = true
mypy vs pyright
- mypy:Python 官方生态、成熟、命令行友好。
- pyright(微软,PyCharm/VSCode 背后的引擎):更快、IDE 集成好、推断更智能。
- 日常开发靠 IDE 实时提示(PyCharm 内置、VSCode 装 Pylance),CI 里跑 mypy。
本章练习¶
练习 8.1
给下面函数加上精确的类型注解:接收一个字符串列表,返回一个"按首字母分组的字典"(dict[str, list[str]]),键是小写首字母。
参考答案
练习 8.2
用 Protocol 定义一个 Closeable(有 close(self) -> None),写一个泛型函数 with_resource[T](x: T) -> T(用 TypeVar)调用它的 close。思考:为什么这里用 Protocol 而不是 ABC?
参考答案
from typing import Protocol, TypeVar
class Closeable(Protocol):
def close(self) -> None: ...
T = TypeVar("T", bound=Closeable)
def with_resource(x: T) -> T:
x.close()
return x
close() 常见于各种第三方对象(文件、连接),它们没有共同基类。结构化类型让 with_resource 能接受任何有 close 的对象,无需它们继承某个 ABC。
练习 8.3
解释 age: int = 30 之后 age = "thirty" 为何运行时不报错,以及如何让它"被发现"。
参考答案
类型注解只是元数据,解释器运行时不校验,故赋字符串能跑。要让错误暴露,需用静态检查器:mypy yourfile.py 会报 error: Incompatible types in assignment (expression has type "str", variable has type "int")。IDE(Pylance/PyCharm)也会实时标红。
上一章:第 7 章 · Pythonic 惯用法 | 下一章:第 9 章 · 标准库对照速查。