第8章: 类型系统 --- Python的类型注解革命
Java/Kotlin 开发者习惯了编译器在类型上兜底,初看 Python 会觉得"这语言没类型"。错。Python 3.10+ 的类型注解体系已经非常成熟,PEP 484/526/612/695/696 一路演进,加上 mypy/pyright 等静态检查器,你可以在 Python 中获得接近 Java 的类型安全体验------但本质不同:Python 的类型注解是可选的、运行时默认忽略的、由外部工具检查的 。理解这个本质差异,是本章的核心。
8.1 类型注解基础
Java/Kotlin 对比
java
复制代码
// Java: 类型是语言的一部分,编译器强制执行
public class User {
private String name; // 必须声明类型
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String greet() { // 返回类型必须声明
return "Hi, " + this.name;
}
}
// 类型错误 → 编译失败,一行代码都跑不了
kotlin
复制代码
// Kotlin: 类型推断减少样板,但仍然是编译期强制
class User(val name: String, val age: Int) {
fun greet(): String = "Hi, $name" // 返回类型可推断但推荐声明
}
// val x: Int = "hello" // 编译错误!
Python 实现
python
复制代码
# Python: 类型注解是"提示",运行时默认不检查
def greet(name: str, age: int) -> str:
return f"Hi, {name}, age {age}"
# 运行时完全正常------Python 不在乎你传了什么
result = greet("Alice", 30) # 正常
result2 = greet("Bob", "thirty") # 也正常!运行时不会报错
result3 = greet(42, None) # 照样跑!f-string 不会抛异常
# 但类型检查器会报警
# mypy 会报: Argument 2 to "greet" has incompatible type "str"; expected "int"
# === 变量注解 ===
name: str = "Alice"
age: int = 30
scores: list[float] = [98.5, 87.3, 95.0]
# 注解但不赋值------运行时不会创建变量
address: str # 只是告诉类型检查器"这个变量将来会是 str"
# print(address) # NameError! 变量根本不存在
# === 类注解 ===
class User:
name: str # 类属性注解(不创建实例属性)
age: int
def __init__(self, name: str, age: int) -> None:
self.name = name # 实例属性赋值
self.age = age
def greet(self) -> str:
return f"Hi, {self.name}"
# === typing.get_type_hints(): 运行时获取注解 ===
from typing import get_type_hints
class Config:
host: str
port: int
debug: bool
def __init__(self, host: str, port: int = 8080) -> None:
self.host = host
self.port = port
# 获取函数的类型注解
hints = get_type_hints(Config.__init__)
print(hints)
# {'host': <class 'str'>, 'port': <class 'int'>, 'return': <class 'None'>}
# 获取类的类型注解
class_hints = get_type_hints(Config)
print(class_hints)
# {'host': <class 'str'>, 'port': <class 'int'>, 'debug': <class 'bool'>}
# 直接访问 __annotations__ 也可以(但 get_type_hints 会处理前向引用)
print(Config.__annotations__)
# {'host': <class 'str'>, 'port': <class 'int'>, 'debug': <class 'bool'>}
核心差异
维度
Java/Kotlin
Python
类型检查时机
编译期,强制
运行时默认忽略,mypy/pyright 外部检查
类型错误后果
编译失败,无法运行
正常运行,类型检查器报警
注解本质
语言语法的一部分
特殊属性 __annotations__,本质是字典
性能影响
零(编译期处理)
微小(注解存储在字典中)
前向引用
编译器自动处理
需要字符串引号或 from __future__ import annotations
常见陷阱
python
复制代码
# 陷阱1: 注解不是类型约束!
x: int = "hello" # 运行时完全正常,mypy 才会报错
# 陷阱2: 类属性注解 vs 实例属性
class Bad:
name: str # 这是类属性注解,不是实例属性!
b = Bad()
# print(b.name) # AttributeError! 没有通过 __init__ 赋值
# 陷阱3: 前向引用需要引号(Python 3.10)
class Node:
def __init__(self, value: int, next: "Node | None" = None) -> None:
self.value = value
self.next = next
# Python 3.11+ 可以用 from __future__ import annotations 避免引号
# 陷阱4: __annotations__ 不包含函数参数默认值的类型推导
def foo(x=42):
pass
print(foo.__annotations__) # {} --- 没有注解就没有
何时使用
必须用 : 公共 API、团队协作项目、数据处理管道
推荐用 : 任何超过 200 行的文件
可以不用 : 一次性脚本、Jupyter notebook 探索性分析
8.2 typing 核心: Optional, Union, Literal, Any, NoReturn
Java/Kotlin 对比
java
复制代码
// Java: Optional<T> 是一个包装容器,防止 null
import java.util.Optional;
public class UserService {
// 返回 Optional 表示可能找不到
public Optional<User> findUser(String name) {
return Optional.ofNullable(db.get(name));
}
// 参数用 @Nullable 注解(非标准,需 lombok/checker framework)
public void updateName(@Nullable String newName) {
if (newName != null) {
// ...
}
}
}
// Java 的 null 是一等公民,Optional 只是包装
kotlin
复制代码
// Kotlin: ? 是语言内置的可空类型,编译器强制 null 检查
fun findUser(name: String): User? { // ? 表示可为 null
return db[name]
}
val user = findUser("Alice")
println(user?.name) // 安全调用
println(user?.name ?: "N/A") // Elvis 运算符
// println(user.name) // 编译错误!必须先处理 null
Python 实现
python
复制代码
from typing import Optional, Union, Literal, Any, NoReturn
# === Optional[X] 等价于 Union[X, None] ===
def find_user(name: str) -> Optional[str]:
"""找不到返回 None"""
users = {"Alice": "alice@example.com", "Bob": "bob@example.com"}
return users.get(name) # dict.get 找不到返回 None
email = find_user("Alice") # str | None
email2 = find_user("Eve") # None
# Python 不会强制你处理 None!
# print(email2.upper()) # 运行时 AttributeError!mypy 会提前警告
# === Union: 多种类型之一 ===
def process(value: Union[int, str, float]) -> str:
"""接受 int、str 或 float"""
if isinstance(value, int):
return f"integer: {value}"
elif isinstance(value, str):
return f"string: {value}"
else:
return f"float: {value}"
# Python 3.10+ 可以用 | 语法代替 Union
def process_modern(value: int | str | float) -> str:
if isinstance(value, int):
return f"integer: {value}"
elif isinstance(value, str):
return f"string: {value}"
else:
return f"float: {value}"
# Optional 也可以用 | 语法
def find_user_modern(name: str) -> str | None:
return {"Alice": "alice@example.com"}.get(name)
# === Literal: 限定为特定字面值 ===
def set_mode(mode: Literal["debug", "info", "error"]) -> None:
print(f"Mode set to: {mode}")
set_mode("debug") # OK
set_mode("info") # OK
# set_mode("warn") # mypy 报错!不在 Literal 列表中
# set_mode(42) # mypy 报错!
# 组合 Literal 和 Union
def configure(key: Literal["host", "port"], value: str | int) -> None:
print(f"Setting {key} = {value}")
# === Any: 放弃类型检查 ===
from typing import Any
def process_any(data: Any) -> Any:
"""Any 是顶级类型,任何值都能赋给它,它也能赋给任何类型"""
return data
x: int = process_any("hello") # mypy 不报错!Any 会"污染"类型
# 对比 Java: Object 也有类似问题,但 Kotlin 的 Any 不会隐式赋值给 String
# === NoReturn: 标记永远不会正常返回的函数 ===
import sys
def fail(message: str) -> NoReturn:
"""抛异常或退出进程,永远不会 return"""
print(f"FATAL: {message}")
sys.exit(1)
def assert_positive(value: int) -> None:
if value < 0:
fail(f"Expected positive, got {value}")
# mypy 知道 fail() 不会返回,所以这里不需要 else 分支
print(f"OK: {value}")
# === 实战: 用 Union + Literal 建模配置 ===
from typing import TypedDict
class ServerConfig(TypedDict):
host: str
port: Literal[80, 443, 8080, 8443]
ssl: bool
db: Union[str, None] # 数据库连接字符串,可选
config: ServerConfig = {
"host": "localhost",
"port": 8080,
"ssl": False,
"db": None,
}
核心差异
概念
Java/Kotlin
Python
可空
Kotlin T? / Java @Nullable
Optional[T] 或 `T
多类型
Java 无原生支持
Union[A, B] 或 `A
字面值约束
无原生支持
Literal["a", "b"]
顶级类型
Object / Any
Any(会污染类型)
不返回
无对应概念
NoReturn
常见陷阱
python
复制代码
# 陷阱1: Optional 不会帮你处理 None
def get_length(s: Optional[str]) -> int:
return len(s) # mypy 报错!s 可能是 None
# 正确: return len(s) if s else 0
# 陷阱2: Any 会绕过所有类型检查------慎用
def bad(x: Any) -> int:
return x # mypy 不报错,但运行时可能返回任何东西
# 陷阱3: Union 的顺序不影响语义
Union[int, str] == Union[str, int] # True --- 不是"优先匹配"
# 陷阱4: isinstance 检查后 mypy 能自动收窄类型(type narrowing)
def handle(value: int | str) -> int:
if isinstance(value, str):
return len(value) # mypy 知道这里 value 是 str
return value # mypy 知道这里 value 是 int
何时使用
Optional / T | None: 函数可能不返回有效值时
Union: API 需要兼容多种输入时
Literal: 限定枚举值、状态机、配置项
Any: 遗留代码迁移、动态数据(JSON)的临时方案------尽快消除
NoReturn: 异常抛出函数、进程退出函数
8.3 泛型: TypeVar, Generic, Protocol
Java/Kotlin 对比
java
复制代码
// Java: 泛型在编译期检查,运行时擦除(type erasure)
public class Box<T> {
private T value;
public Box(T value) { this.value = value; }
public T get() { return value; }
public void set(T value) { this.value = value; }
}
// 运行时 Box<String> 和 Box<Integer> 都是 Box
Box<String> box = new Box<>("hello");
// box.getClass() == Box.class --- 看不到 String
// 泛型边界
public class NumberBox<T extends Number> {
private T value;
public double doubleValue() { return value.doubleValue(); }
}
kotlin
复制代码
// Kotlin: 泛型声明处型变(out/in)+ 类型投影
class Box<out T>(val value: T) // 协变:只能读不能写
class MutableBox<T>(var value: T) // 不变:可读可写
fun <T : Comparable<T>> maxOf(a: T, b: T): T =
if (a >= b) a else b
// 星投影:不知道具体类型参数时
fun process(box: Box<*>) {
val value: Any? = box.value // 只能当 Any 用
}
Python 实现
python
复制代码
from typing import TypeVar, Generic, Protocol, TypeGuard
from collections.abc import Iterable
# === TypeVar: 定义类型变量 ===
T = TypeVar("T") # 无约束,可以是任何类型
N = TypeVar("N", int, float) # 约束为 int 或 float(值类型约束)
C = TypeVar("C", bound="Comparable") # 上界约束(类似 Java extends)
# === 泛型函数 ===
def first(lst: list[T]) -> T:
"""返回列表第一个元素"""
return lst[0]
# mypy 推断: first([1, 2, 3]) -> int, first(["a", "b"]) -> str
x: int = first([1, 2, 3])
s: str = first(["hello", "world"])
def add(a: N, b: N) -> N:
"""两个数字相加"""
return a + b # type: ignore # int + float 返回 float,mypy 可能报错
result: int = add(1, 2)
result2: float = add(1.5, 2.5)
# result3: str = add("a", "b") # mypy 报错!N 只允许 int 或 float
# === 泛型类 ===
class Box(Generic[T]):
"""泛型容器"""
def __init__(self, value: T) -> None:
self._value = value
def get(self) -> T:
return self._value
def set(self, value: T) -> None:
self._value = value
int_box: Box[int] = Box(42)
str_box: Box[str] = Box("hello")
# mypy 检查类型一致性
int_box.set(100)
# int_box.set("hello") # mypy 报错!
val: int = int_box.get()
# === 多类型参数 ===
K = TypeVar("K")
V = TypeVar("V")
class Pair(Generic[K, V]):
def __init__(self, key: K, value: V) -> None:
self.key = key
self.value = value
pair: Pair[str, int] = Pair("age", 30)
# === Protocol: 结构化子类型(鸭子类型的静态版本)===
class Drawable(Protocol):
"""任何有 draw() 方法的类都自动满足此协议"""
def draw(self) -> str: ...
class Circle:
def draw(self) -> str:
return "○"
class Rectangle:
def draw(self) -> str:
return "□"
class NotDrawable:
def render(self) -> str:
return "???"
def render_all(items: list[Drawable]) -> None:
for item in items:
print(item.draw())
# Circle 和 Rectangle 自动满足 Drawable,无需继承!
render_all([Circle(), Rectangle()])
# render_all([NotDrawable()]) # mypy 报错!没有 draw() 方法
# 这就是 Python 的"静态鸭子类型"------vs Java 必须显式 implements
# === Protocol 带属性 ===
class Named(Protocol):
name: str # 协议可以要求属性
class Person:
def __init__(self, name: str) -> None:
self.name = name
class Company:
def __init__(self, name: str) -> None:
self.name = name
def get_name(entity: Named) -> str:
return entity.name
# Person 和 Company 都自动满足 Named
print(get_name(Person("Alice"))) # Alice
print(get_name(Company("Google"))) # Google
# === TypeGuard: 自定义类型收窄 ===
from typing import TypeGuard
def is_int_list(val: list) -> TypeGuard[list[int]]:
"""告诉 mypy: 如果返回 True,val 就是 list[int]"""
return all(isinstance(x, int) for x in val)
def process_numbers(items: list[int | str]) -> None:
if is_int_list(items):
# mypy 知道这里 items 是 list[int]
total = sum(items) # OK
print(f"Sum: {total}")
核心差异
维度
Java/Kotlin
Python
泛型实现
编译期检查,运行时擦除
纯运行时属性,mypy 检查
类型约束
extends / :
bound= 参数
协变/逆变
out T / in T(声明处)
covariant=True / contravariant=True
结构化类型
无(必须显式 implements)
Protocol(鸭子类型的静态版)
泛型实例
运行时不可见
运行时可通过 __orig_class__ 查看
常见陷阱
python
复制代码
# 陷阱1: TypeVar 名字和变量名必须相同
T = TypeVar("T") # 正确
# X = TypeVar("T") # 能用但极度混乱,mypy 警告
# 陷阱2: Generic 必须显式继承
class BadBox: # 不是泛型类!
def __init__(self, value: T) -> None: # T 在这里只是个全局变量
self._value = value
class GoodBox(Generic[T]): # 正确
def __init__(self, value: T) -> None:
self._value = value
# 陷阱3: Protocol 不能用于 isinstance 检查(运行时)
# isinstance(Circle(), Drawable) # TypeError!
# 用 @runtime_checkable 装饰器可以启用,但有性能开销
from typing import runtime_checkable
@runtime_checkable
class CheckableDrawable(Protocol):
def draw(self) -> str: ...
print(isinstance(Circle(), CheckableDrawable)) # True
何时使用
TypeVar: 函数/类需要保持输入输出类型一致时
Generic: 需要类型参数化的容器类
Protocol: 定义接口但不强制继承关系时------Python 的杀手特性
TypeGuard: 自定义类型收窄逻辑时
8.4 PEP 695 (3.12+): 新泛型语法
Java/Kotlin 对比
java
复制代码
// Java: 泛型参数在方法名前声明
public <T> T first(List<T> list) {
return list.get(0);
}
// 泛型参数在类名后声明
public class Box<T> {
private T value;
public Box(T value) { this.value = value; }
public T get() { return value; }
}
kotlin
复制代码
// Kotlin: 泛型参数在 fun/class 关键字后声明
fun <T> first(lst: List<T>): T = lst.first()
class Box<T>(val value: T) {
fun get(): T = value
}
// 泛型默认值(Kotlin 不支持)
// Java 也不支持泛型参数默认值
Python 实现
python
复制代码
# === PEP 695: 全新泛型语法(Python 3.12+)===
# 不再需要 from typing import TypeVar, Generic
# === 泛型函数: 类型参数在方括号中声明 ===
def first[T](lst: list[T]) -> T:
"""返回列表第一个元素"""
return lst[0]
# mypy 自动推断类型参数
x: int = first([1, 2, 3])
s: str = first(["hello"])
# === 泛型类: 类型参数在类名后声明 ===
class Box[T]:
def __init__(self, value: T) -> None:
self._value = value
def get(self) -> T:
return self._value
def set(self, value: T) -> None:
self._value = value
int_box: Box[int] = Box(42)
str_box: Box[str] = Box("hello")
# === 多类型参数 ===
class Pair[K, V]:
def __init__(self, key: K, value: V) -> None:
self.key = key
self.value = value
pair: Pair[str, int] = Pair("age", 30)
# === 类型参数约束 ===
def add[T: (int, float)](a: T, b: T) -> T:
"""T 必须是 int 或 float"""
return a + b # type: ignore
# 对比旧语法: T = TypeVar("T", int, float)
# 新语法用冒号+元组,更简洁
# === 类型参数上界 ===
class Container[T: object]:
"""T 必须是 object 的子类型(几乎所有类型都满足)"""
def __init__(self, value: T) -> None:
self._value = value
# === 类型参数默认值(3.13+)===
# Python 3.13 起支持泛型参数默认值
class Result[T, E: Exception = Exception]:
"""类似 Rust 的 Result<T, E>,E 默认为 Exception"""
def __init__(self, value: T | None = None, error: E | None = None) -> None:
self._value = value
self._error = error
def is_ok(self) -> bool:
return self._error is None
def unwrap(self) -> T:
if self._error is not None:
raise self._error
return self._value # type: ignore
ok: Result[int] = Result(value=42) # E 默认为 Exception
err: Result[int, ValueError] = Result(error=ValueError("bad"))
# === 新旧语法对比 ===
# 旧(3.11 及之前):
from typing import TypeVar, Generic
T_old = TypeVar("T_old")
def first_old(lst: list[T_old]) -> T_old:
return lst[0]
class Box_old(Generic[T_old]):
def __init__(self, value: T_old) -> None:
self._value = value
# 新(3.12+):
def first_new[T](lst: list[T]) -> T:
return lst[0]
class Box_new[T]:
def __init__(self, value: T) -> None:
self._value = value
# 新语法的优势:
# 1. 不需要 import TypeVar/Generic
# 2. 类型参数作用域限定在函数/类内(不会污染全局)
# 3. 语法更接近 Java/Kotlin,JVM 开发者更直观
核心差异
维度
Java/Kotlin
Python 3.12+
泛型函数声明
<T> def first(...) / fun <T> first(...)
def first[T](...)
泛型类声明
class Box<T> / class Box<T>
class Box[T]
约束语法
T extends Number / T : Number
T: (int, float) 或 T: bound
默认类型参数
不支持
3.13+ 支持
作用域
类/方法内
类/函数内(新语法),全局(旧语法)
常见陷阱
python
复制代码
# 陷阱1: 新语法需要 Python 3.12+,3.10/3.11 只能用旧语法
# def first[T](lst: list[T]) -> T: # 3.11 及之前 SyntaxError!
# 陷阱2: 新旧语法不能混用
T = TypeVar("T")
# def foo[T, U](x: T, y: U) -> T: # SyntaxError! 不能同时用两种语法
# 陷阱3: 类型参数默认值需要 3.13+
# class Box[T = int]: # 3.12 会 SyntaxError
何时使用
Python 3.12+: 优先使用新语法,更简洁、作用域更清晰
Python 3.10/3.11: 只能用旧语法(TypeVar + Generic)
跨版本兼容: 用旧语法,或 typing_extensions 库
8.5 type 语句 (3.12+): 类型别名
Java/Kotlin 对比
java
复制代码
// Java: 没有类型别名(直到最近才有 preview)
// 变通方案: 继承或包装
public class UserID extends String { // 不合法!Java 不支持
}
// Java 21 preview: 类型别名(尚未稳定)
// typealias UserID = String;
kotlin
复制代码
// Kotlin: typealias 创建类型别名
typealias UserID = String
typealias Point = Pair<Double, Double>
typealias Handler = (Event) -> Unit
fun lookup(id: UserID): User? { ... }
// UserID 在编译后就是 String,但提供了语义信息
Python 实现
python
复制代码
# === type 语句: 创建类型别名(Python 3.12+)===
# 基本类型别名
type UserID = str
type Point = tuple[float, float]
type Score = int
def lookup(user_id: UserID) -> Point:
"""UserID 和 Point 都是类型别名,运行时就是 str 和 tuple"""
return (0.0, 0.0)
uid: UserID = "alice-001"
pos: Point = (1.0, 2.0)
score: Score = 95
# mypy 会用别名名显示类型,提高可读性
# === 泛型类型别名 ===
type Matrix[T] = list[list[T]]
type Mapping[K, V] = dict[K, V]
type Result[T] = tuple[bool, T | None]
int_matrix: Matrix[int] = [[1, 2], [3, 4]]
str_matrix: Matrix[str] = [["a", "b"], ["c", "d"]]
name_map: Mapping[str, int] = {"Alice": 30, "Bob": 25}
result: Result[str] = (True, "success")
# === 复杂类型的可读别名 ===
type JSON = dict[str, "JSON"] | list["JSON"] | str | int | float | bool | None
type Headers = dict[str, str]
type Handler = Callable[[str], bool]
# 对比不用别名的函数签名:
# def process(data: dict[str, dict[str, "JSON"] | list["JSON"] | str | int | float | bool | None], headers: dict[str, str]) -> None:
# ...
def process(data: JSON, headers: Headers) -> None:
"""类型别名让签名清晰可读"""
pass
# === 旧语法: TypeAlias ===
# Python 3.10/3.11 需要用赋值或 TypeAlias
from typing import TypeAlias
PointOld: TypeAlias = tuple[float, float]
UserIDOld: TypeAlias = str
# 或者直接赋值(不推荐,可读性差)
PointOld2 = tuple[float, float] # mypy 能识别,但不够明确
# === 递归类型别名 ===
# JSON 是递归定义的:对象值可以是 JSON 本身
type JSONObject = dict[str, "JSONValue"]
type JSONValue = str | int | float | bool | None | JSONObject | list["JSONValue"]
# 注意: 递归别名需要用前向引用(引号包裹)
# === type 语句 vs 赋值 ===
# type 语句是显式的类型别名声明
type X = int | str # 显式,mypy/pyright 明确知道这是别名
Y = int | str # 隐式,也能工作但语义不够清晰
# type 语句的优势:
# 1. 意图明确------一眼就知道这是类型别名
# 2. 支持泛型参数: type Matrix[T] = ...
# 3. 不能用作普通变量: type X = int; X = 42 # 赋值后 X 不再是类型别名
核心差异
维度
Java/Kotlin
Python 3.12+
语法
typealias X = Y
type X = Y
泛型别名
typealias Matrix<T> = List<List<T>>
type Matrix[T] = list[list[T]]
递归别名
支持
支持(需前向引用)
运行时行为
编译后完全擦除
运行时是原类型的别名
旧版兼容
N/A
TypeAlias 注解或直接赋值
常见陷阱
python
复制代码
# 陷阱1: type 语句创建的别名在运行时就是原类型
type UserID = str
uid: UserID = "alice"
print(type(uid)) # <class 'str'> --- 不是 UserID
print(isinstance(uid, str)) # True
# 陷阱2: type 语句需要 3.12+
# type Point = tuple[float, float] # 3.11 及之前 SyntaxError
# 陷阱3: 别名不提供运行时类型安全
type UserID = str
uid: UserID = "alice"
uid2: UserID = 123 # mypy 报错,但运行时不检查
何时使用
复杂联合类型需要命名以提高可读性
领域概念需要类型级别的语义(如 UserID vs str)
泛型容器类型需要参数化别名(如 Matrix[int])
8.6 ParamSpec 与 TypeVarTuple: 高级泛型
Java/Kotlin 对比
java
复制代码
// Java: 泛型通配符处理未知类型参数
public interface Function<T, R> {
R apply(T t);
}
// 通配符: ? extends T (上界), ? super T (下界)
public double sum(List<? extends Number> numbers) {
double total = 0;
for (Number n : numbers) {
total += n.doubleValue();
}
return total;
}
// Java 无法捕获整个函数签名(参数个数+类型)
kotlin
复制代码
// Kotlin: 星投影 * 表示未知类型参数
fun process(list: List<*>) {
// 只能当 Any? 读取
}
// reified 类型参数(内联函数特化)
inline fun <reified T> typeName(): String = T::class.simpleName ?: "Unknown"
// Java 的泛型在运行时擦除,Kotlin 的 reified 通过内联绕过
Python 实现
python
复制代码
from typing import (
ParamSpec, TypeVarTuple, Concatenate,
Callable, Any
)
from collections.abc import Callable as AbcCallable
# === ParamSpec: 捕获函数的完整参数签名 ===
P = ParamSpec("P")
R = TypeVar("R")
def log_calls(func: Callable[P, R]) -> Callable[P, R]:
"""装饰器: 记录函数调用,ParamSpec 保留原始参数签名"""
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
print(f"Calling {func.__name__} with args={args}, kwargs={kwargs}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned {result}")
return result
return wrapper
@log_calls
def add(a: int, b: int) -> int:
return a + b
@log_calls
def greet(name: str, greeting: str = "Hello") -> str:
return f"{greeting}, {name}!"
# mypy 知道 add 仍然是 (a: int, b: int) -> int
# greet 仍然是 (name: str, greeting: str = "Hello") -> str
result: int = add(1, 2)
# 输出:
# Calling add with args=(1, 2), kwargs={}
# add returned 3
# === Concatenate: 在参数签名前添加参数 ===
def with_retry(
func: Callable[P, R],
max_retries: int = 3
) -> Callable[Concatenate[int, P], R]:
"""
装饰器工厂: 在原函数参数前添加一个 timeout 参数
Concatenate[int, P] 表示 (int, *原始参数)
"""
def wrapper(timeout: int, *args: P.args, **kwargs: P.kwargs) -> R:
last_error: Exception | None = None
for attempt in range(max_retries):
try:
return func(*args, **kwargs)
except Exception as e:
last_error = e
print(f"Attempt {attempt + 1} failed: {e}")
raise last_error # type: ignore
return wrapper
@with_retry(max_retries=5)
def fetch_data(url: str) -> str:
"""被装饰后签名变成 (timeout: int, url: str) -> str"""
return f"Data from {url}"
# mypy 知道 fetch_data 现在需要 timeout 参数
data: str = fetch_data(timeout=10, url="https://example.com")
# === TypeVarTuple: 可变长度泛型 ===
Ts = TypeVarTuple("Ts")
def flatten(*arrays: *Ts) -> list:
"""将多个列表展平为一个列表"""
result: list = []
for arr in arrays:
result.extend(arr)
return result
result = flatten([1, 2], [3, 4], [5, 6])
# result: [1, 2, 3, 4, 5, 6]
# === 实战: 类型安全的矩阵运算 ===
import numpy as np
from typing import TypeVarTuple
T = TypeVar("T")
Ts = TypeVarTuple("Ts")
# TypeVarTuple 可以捕获任意长度的类型参数
# 例如: tuple[*Ts] 可以匹配 tuple[int], tuple[int, str], tuple[int, str, float] 等
# === 实战: 通用日志装饰器 ===
def create_logger(prefix: str) -> Callable[[Callable[P, R]], Callable[P, R]]:
"""工厂函数返回装饰器,ParamSpec 保留签名"""
def decorator(func: Callable[P, R]) -> Callable[P, R]:
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
print(f"[{prefix}] {func.__name__} called")
return func(*args, **kwargs)
return wrapper
return decorator
@create_logger("API")
def get_user(user_id: int, detailed: bool = False) -> dict[str, str]:
return {"id": str(user_id), "detail": "full" if detailed else "basic"}
# mypy 知道 get_user 仍然是 (user_id: int, detailed: bool) -> dict
info: dict[str, str] = get_user(42, detailed=True)
ParamSpec 实战: 装饰器保留函数签名
python
复制代码
from typing import ParamSpec, TypeVar, Callable
from functools import wraps
P = ParamSpec('P') # 捕获函数的参数签名
R = TypeVar('R') # 捕获返回类型
def log_calls(func: Callable[P, R]) -> Callable[P, R]:
"""装饰器: 记录函数调用,完整保留原函数的签名"""
@wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
print(f"调用 {func.__name__}({args}, {kwargs})")
result = func(*args, **kwargs)
print(f"返回: {result}")
return result
return wrapper
# 使用: 类型检查器知道 greet 的签名是 (name: str, times: int) -> str
@log_calls
def greet(name: str, times: int = 1) -> str:
return f"Hello, {name}! " * times
# mypy/pyright 能正确推断:
result: str = greet("Alice", times=3)
# greet(123) # mypy 报错: Expected str, got int
# === Concatenate: 在参数前添加参数 ===
from typing import Concatenate
# 添加一个 db 参数到所有方法
def with_db(func: Callable[Concatenate[dict, P], R]) -> Callable[P, R]:
@wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
db = {"connection": "active"} # 模拟数据库连接
return func(db, *args, **kwargs)
return wrapper
@with_db
def get_user(db: dict, user_id: int) -> str:
return f"User {user_id} from {db['connection']}"
# 类型检查器知道 get_user 的签名是 (user_id: int) -> str
# db 参数被 with_db 自动注入
result = get_user(42) # mypy 正确!
print(result) # User 42 from active
TypeVarTuple 实战: 可变长度泛型
python
复制代码
from typing import TypeVarTuple
Ts = TypeVarTuple('Ts')
# 定义一个可以接受任意数量类型的数组
class Array:
def __init__(self, *items):
self.items = items
def __repr__(self):
return f"Array({self.items})"
# TypeVarTuple 用于矩阵等数学类型
def flatten(arrays: list[*Ts]) -> list[*Ts]:
"""展平嵌套列表 --- TypeVarTuple 保留元素类型"""
result = []
for arr in arrays:
result.extend(arr)
return result
# 实际应用: batched 函数 (3.12+ 内置)
# Python 3.12 之前需要手动实现
from itertools import islice
def batched(iterable, n):
"""将可迭代对象按 n 个一组分割"""
it = iter(iterable)
while batch := tuple(islice(it, n)):
yield batch
data = list(range(10))
for batch in batched(data, 3):
print(batch)
# (0, 1, 2)
# (3, 4, 5)
# (6, 7, 8)
# (9,)
核心差异
维度
Java/Kotlin
Python
捕获参数签名
不支持
ParamSpec
添加参数到签名
不支持
Concatenate
可变长度泛型
不支持
TypeVarTuple
通配符
? extends T / *
ParamSpec 更强大
reified
Kotlin inline reified
Python 天然 reified(运行时有类型)
常见陷阱
python
复制代码
# 陷阱1: ParamSpec 只能用于 Callable 的参数部分
P = ParamSpec("P")
# def bad(x: P) -> None: # TypeError! P 只能和 Callable 一起用
# 陷阱2: Concatenate 的第一个参数必须是具体类型
from typing import Concatenate
# def bad(func: Callable[Concatenate[P, int], R]): # 错误!第一个必须是具体类型
# 陷阱3: TypeVarTuple 用 * 解包
Ts = TypeVarTuple("Ts")
# def foo(args: Ts): ... # 错误!需要 * 解包
def foo(*args: *Ts): ... # 正确
何时使用
ParamSpec: 编写保留原函数签名的装饰器时------非常实用
Concatenate: 装饰器需要添加额外参数时
TypeVarTuple: 处理可变长度类型参数(如矩阵、张量)时
8.7 TypedDict, NamedTuple: 结构化类型
Java/Kotlin 对比
java
复制代码
// Java Record (Java 16+): 不可变数据载体
public record Point(double x, double y) {}
// 自动生成: constructor, getX(), getY(), equals(), hashCode(), toString()
// Java 没有直接的字典结构类型------通常用 Map<String, Object>
Map<String, Object> config = Map.of(
"host", "localhost",
"port", 8080
);
// 没有类型安全------值都是 Object
kotlin
复制代码
// Kotlin data class: 不可变数据载体
data class Point(val x: Double, val y: Double)
// 自动生成: constructor, component1(), component2(), copy(), equals(), hashCode(), toString()
// Map 没有结构约束
val config = mapOf(
"host" to "localhost",
"port" to 8080
)
// 类型: Map<String, Any> --- 值类型丢失
Python 实现
python
复制代码
from typing import TypedDict, NamedTuple, NotRequired, Required
# === TypedDict: 给字典加上结构约束 ===
class User(TypedDict):
name: str
age: int
email: str
# 创建 TypedDict 实例------本质还是 dict
user: User = {
"name": "Alice",
"age": 30,
"email": "alice@example.com",
}
# mypy 会检查键和值的类型
print(user["name"]) # mypy 知道返回 str
# user["phone"] = "123" # mypy 报错!User 没有 phone 键
# user["age"] = "thirty" # mypy 报错!age 必须是 int
# TypedDict 就是 dict------所有 dict 操作都能用
for key, value in user.items():
print(f"{key}: {value}")
# === NotRequired / Required: 可选键 ===
class ServerConfig(TypedDict):
host: str
port: int
debug: NotRequired[bool] # 可选键
ssl: Required[bool] # 显式必需(在 total=False 时有用)
config: ServerConfig = {
"host": "localhost",
"port": 8080,
"ssl": True,
# debug 可以省略
}
# === total=False: 所有键默认可选 ===
class PartialUser(TypedDict, total=False):
name: str
age: int
email: str
# 用于 PATCH 请求------只传要更新的字段
update: PartialUser = {"age": 31} # 只更新 age
# === 嵌套 TypedDict ===
class Address(TypedDict):
city: str
zip_code: str
class Employee(TypedDict):
name: str
address: Address
emp: Employee = {
"name": "Bob",
"address": {"city": "Beijing", "zip_code": "100000"},
}
# mypy 知道 emp["address"]["city"] 是 str
# === NamedTuple: 不可变的元组 + 字段名 ===
class Point(NamedTuple):
x: float
y: float
p = Point(1.0, 2.0)
print(p.x, p.y) # 1.0 2.0 --- 像类一样访问
print(p[0], p[1]) # 1.0 2.0 --- 也像元组一样索引
# p.x = 3.0 # AttributeError! 不可变
# 自动生成 __repr__, ==, hash
print(p) # Point(x=1.0, y=2.0)
print(Point(1.0, 2.0) == Point(1.0, 2.0)) # True
# 支持解构
x, y = p
print(x, y) # 1.0 2.0
# === NamedTuple 带默认值和方法 ===
class UserInfo(NamedTuple):
name: str
age: int
email: str = "N/A" # 默认值
def is_adult(self) -> bool:
return self.age >= 18
info = UserInfo("Alice", 30)
print(info.is_adult()) # True
print(info.email) # N/A
# === NamedTuple vs TypedDict 选择 ===
# NamedTuple: 不可变、可哈希(可做 dict key)、位置访问
# TypedDict: 可变、键值访问、兼容 JSON 序列化
# === 实战: API 响应建模 ===
class APIError(TypedDict):
code: int
message: str
details: NotRequired[str]
class APIResponse(TypedDict):
success: bool
data: NotRequired[dict[str, object]]
error: NotRequired[APIError]
# 成功响应
ok_response: APIResponse = {
"success": True,
"data": {"id": 1, "name": "Alice"},
}
# 错误响应
err_response: APIResponse = {
"success": False,
"error": {"code": 404, "message": "Not found"},
}
# === 实战: 类型安全的配置 ===
from dataclasses import dataclass
# dataclass 是另一种选择------可变、有默认值、更灵活
@dataclass
class Config:
host: str = "localhost"
port: int = 8080
debug: bool = False
cfg = Config(host="0.0.0.0", port=3000)
cfg.debug = True # 可变
核心差异
维度
Java Record
Kotlin data class
Python NamedTuple
Python TypedDict
可变性
不可变
可变(var)/不可变(val)
不可变
可变
位置访问
componentN()
componentN()
索引
不支持
命名访问
getX()
.x
.x
["x"]
可哈希
是
是(全 val 时)
是
否
JSON 兼容
需要库
需要库
需要转换
原生兼容
默认值
支持
支持
支持
不支持(用 NotRequired)
常见陷阱
python
复制代码
# 陷阱1: TypedDict 运行时不会检查类型
user: User = {"name": 42, "age": "thirty", "email": []} # 运行时正常!
# 只有 mypy 会报错
# 陷阱2: TypedDict 不能用 kwargs 构造
# user = User(name="Alice", age=30) # TypeError! 必须用字典字面量
user = {"name": "Alice", "age": 30, "email": "a@b.com"} # 正确
# 陷阱3: NamedTuple 继承顺序
class Bad(NamedTuple, dict): # 不要这样做
x: int
# 陷阱4: TypedDict 的 NotRequired 和 total=False 的区别
class A(TypedDict):
x: NotRequired[int] # x 可以不存在
class B(TypedDict, total=False):
x: int # 所有键都可以不存在
class C(TypedDict):
x: Required[int] # x 必须存在(默认行为)
何时使用
TypedDict: JSON API 响应、配置字典、需要类型约束的字典
NamedTuple: 不可变数据记录、需要哈希的键、函数返回多值
dataclass: 需要可变性+默认值+方法时(第4章已详述)
8.8 typing.override (3.12+) 与其他装饰器
Java/Kotlin 对比
java
复制代码
// Java: @Override 注解------编译器检查是否真的覆盖了父类方法
public class Dog extends Animal {
@Override
public void speak() {
System.out.println("Woof!");
}
// @Override
// public void bark() {} // 编译错误!Animal 没有 bark() 方法
}
// @Override 不是必须的,但强烈推荐------防止签名拼写错误
kotlin
复制代码
// Kotlin: override 是关键字------必须显式声明
open class Animal {
open fun speak() { println("...") }
}
class Dog : Animal() {
override fun speak() { // 必须写 override!
println("Woof!")
}
}
// Kotlin 比 Java 更严格:不加 override 直接报错
Python 实现
python
复制代码
import typing
import warnings
# === typing.override (3.12+): 标记方法覆盖 ===
class Animal:
def speak(self) -> str:
return "..."
class Dog(Animal):
@typing.override
def speak(self) -> str:
return "Woof!"
class Cat(Animal):
@typing.override
def speak(self) -> str:
return "Meow!"
# mypy 检查: 如果父类没有 speak(),@typing.override 会报错
class Bad(Animal):
@typing.override
def bark(self) -> str: # mypy 报错!Animal 没有 bark()
return "Woof!"
# === typing.final: 防止覆盖和继承 ===
class BaseService:
@typing.final
def authenticate(self, token: str) -> bool:
"""子类不能覆盖此方法"""
return token == "secret"
class UserService(BaseService):
# @typing.override
# def authenticate(self, token: str) -> bool: # mypy 报错!final 方法不能覆盖
# return True
def get_user(self, user_id: int) -> dict[str, str]:
return {"id": str(user_id)}
@typing.final
class Singleton:
"""此类不能被继承"""
pass
# class Sub(Singleton): # mypy 报错!Singleton 是 final 的
# pass
# === typing.deprecated (3.13+): 标记弃用 ===
# Python 3.13 起支持
# 3.10-3.12 可以用 warnings.warn() 手动处理
class OldAPI:
@typing.deprecated("Use new_method() instead")
def old_method(self) -> str:
"""弃用的方法------mypy 会报警"""
warnings.warn(
"old_method is deprecated, use new_method instead",
DeprecationWarning,
stacklevel=2,
)
return "old result"
def new_method(self) -> str:
return "new result"
api = OldAPI()
api.old_method() # mypy 报弃用警告 + 运行时 DeprecationWarning
# === 实战: 框架基类设计 ===
class Controller:
"""Web 控制器基类"""
@typing.final
def handle_request(self, path: str) -> str:
"""模板方法------子类不能覆盖"""
self.before_request()
result = self.dispatch(path)
self.after_request()
return result
def before_request(self) -> None:
"""钩子方法------子类可以覆盖"""
pass
@typing.override
def dispatch(self, path: str) -> str:
"""子类必须覆盖"""
raise NotImplementedError
def after_request(self) -> None:
"""钩子方法------子类可以覆盖"""
pass
class UserController(Controller):
@typing.override
def dispatch(self, path: str) -> str:
if path == "/":
return "User list"
return "User detail"
@typing.override
def before_request(self) -> None:
print("Authenticating...")
# === 3.10/3.11 兼容方案 ===
# 没有 typing.override,用 typing_extensions
# pip install typing_extensions
from typing_extensions import override, final
class AnimalCompat:
def speak(self) -> str:
return "..."
class DogCompat(AnimalCompat):
@override
def speak(self) -> str:
return "Woof!"
核心差异
维度
Java @Override
Kotlin override
Python @typing.override
性质
注解(可选)
关键字(必须)
装饰器(可选)
检查时机
编译期
编译期
mypy/pyright
运行时效果
无
无
无(纯类型提示)
未覆盖父类方法
编译错误
编译错误
mypy 报错
常见陷阱
python
复制代码
# 陷阱1: @typing.override 运行时什么都不做
class Parent:
pass
class Child(Parent):
@typing.override
def foo(self) -> None: # 运行时正常!只有 mypy 报错
pass
# 陷阱2: @typing.final 运行时也不阻止覆盖
class Parent:
@typing.final
def foo(self) -> None:
pass
class Child(Parent):
def foo(self) -> None: # 运行时正常!只有 mypy 报错
pass
# 陷阱3: 3.10/3.11 需要从 typing_extensions 导入
# from typing import override # 3.11 及之前 ImportError
from typing_extensions import override # 正确
何时使用
@override: 所有覆盖父类方法的地方------防止签名错误,提高可读性
@final: 框架/库中不允许子类修改的关键方法
@deprecated: API 迁移期标记旧接口
8.9 mypy/pyright 严格模式配置
Java/Kotlin 对比
java
复制代码
// Java: javac 默认就做类型检查
// 没有开关可以关闭------类型错误就是编译错误
// 可以用 -Xlint 增加额外警告
// javac -Xlint:all Main.java
kotlin
复制代码
// Kotlin: kotlinc 默认做类型检查
// 编译器选项控制严格程度
// kotlinc -Werror Main.kt // 警告也当错误
// kotlinc -Xstrict Main.kt // 严格模式
Python 实现
python
复制代码
# === mypy: 最流行的 Python 类型检查器 ===
# 安装: pip install mypy
# 基本用法
# mypy script.py # 检查单个文件
# mypy src/ # 检查目录
# mypy --strict script.py # 严格模式
# === mypy --strict 包含什么 ===
# --strict 等价于同时开启以下选项:
#
# --disallow-untyped-defs 函数必须有类型注解
# --disallow-any-generics 泛型不能用 Any
# --disallow-untyped-calls 不能调用无注解的函数
# --disallow-subclassing-any 不能继承 Any
# --warn-return-any 返回 Any 时警告
# --no-implicit-optional None 不会自动成为默认值
# --strict-optional Optional 严格检查
# --warn-redundant-casts 多余的类型转换警告
# --no-warn-no-return 无返回的函数必须标注 NoReturn
# --warn-unused-ignores 无用的 # type: ignore 警告
# --no-implicit-reexport __init__.py 不隐式导出
# --strict-equality == / != 类型检查
# --extra-checks 额外检查
# === pyright: 微软出品的类型检查器(更快)===
# 安装: npm install -g pyright
# 或: pip install pyright
# 基本用法
# pyright script.py
# pyright --level standard src/
# === pyproject.toml 配置示例 ===
toml
复制代码
# pyproject.toml --- 推荐的项目级配置
[tool.mypy]
# 基本设置
python_version = "3.10"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
disallow_any_generics = true
# 严格模式(渐进式开启)
strict_optional = true
strict_equality = true
check_untyped_defs = true
# 第三方库类型存根
# mypy 默认不检查无存根的第三方库
[[tool.mypy.overrides]]
module = [
"requests.*",
"numpy.*",
"pandas.*",
]
ignore_missing_imports = true
# 测试代码可以放宽要求
[[tool.mypy.overrides]]
module = "tests.*"
disallow_untyped_defs = false
warn_return_any = false
[tool.pyright]
# pyright 配置
pythonVersion = "3.10"
typeCheckingMode = "standard"
# typeCheckingMode 可选: "off", "basic", "standard", "strict"
# 排除目录
exclude = [
"venv",
".venv",
"build",
"dist",
"node_modules",
]
# 第三方库
venvPath = "."
venv = ".venv"
python
复制代码
# === 渐进式类型化策略 ===
# 不要一上来就开 strict------按阶段推进
# 阶段1: 基础注解(第1周)
# 只给公共 API 加注解
def public_api(user_id: int) -> dict[str, str]:
"""公共函数加注解"""
return {"id": str(user_id)}
# 内部函数暂时不管
def _internal_helper(data): # 暂时无注解
return process(data)
# 阶段2: 启用 mypy 基础检查(第2-3周)
# mypy --disallow-untyped-defs src/
# 修复所有 "Missing type annotation" 错误
# 阶段3: 消除 Any(第4周)
# mypy --disallow-any-generics src/
# 把 List[Any] 改为 list[str] 等
# 阶段4: 严格模式(第5周+)
# mypy --strict src/
# 修复剩余的类型问题
# === # type: ignore 和 @overload ===
# 遗留代码或第三方库没有类型时,临时绕过
def legacy_function(x, y):
# type: ignore # mypy 跳过此函数的类型检查
return x + y
# 更好的做法: 用 @overload 提供类型签名
from typing import overload
@overload
def process(value: int) -> int: ...
@overload
def process(value: str) -> str: ...
def process(value: int | str) -> int | str:
"""mypy 根据参数类型选择正确的重载签名"""
if isinstance(value, int):
return value * 2
return value.upper()
# === 存根文件 (.pyi) ===
# 第三方库没有类型时,可以写 .pyi 存根
# mypy_stub_example.pyi
# def parse_json(text: str) -> dict[str, Any]: ...
# def format_date(dt: datetime) -> str: ...
# 存根文件和 .py 同目录,mypy 自动发现
# === pre-commit 集成 ===
# .pre-commit-config.yaml
# repos:
# - repo: https://github.com/pre-commit/mirrors-mypy
# rev: v1.8.0
# hooks:
# - id: mypy
# args: [--strict]
# additional_dependencies: [types-requests]
核心差异
维度
javac/kotlinc
mypy/pyright
检查时机
编译期(必须通过)
开发期(可选工具)
配置方式
编译器参数
pyproject.toml / mypy.ini
渐进式采用
不支持
支持(逐文件/逐模块)
第三方库
有源码就有类型
需要存根 (.pyi) 或 types-xxx 包
运行时影响
零
零(纯静态分析)
常见陷阱
python
复制代码
# 陷阱1: mypy 默认不检查无存根的第三方库
import requests # mypy 报: Library stubs not installed
# 解决: pip install types-requests
# 或在 pyproject.toml 中 ignore_missing_imports = true
# 陷阱2: --strict 对测试代码太严格
# 解决: 用 [[tool.mypy.overrides]] 给 tests/ 放宽
# 陷阱3: __init__.py 的隐式导出
# from package.module import func # mypy 可能找不到
# 解决: 在 __init__.py 中显式导入,或设置 no_implicit_reexport = false
# 陷阱4: pyright 和 mypy 行为不完全一致
# 同一份代码可能一个报错一个不报------选择一个作为标准
# 推荐: 团队统一用 mypy(生态更成熟)或 pyright(VS Code 集成更好)
# 陷阱5: 动态特性无法类型化
values = [1, "hello", 3.14] # 类型: list[int | str | float]
for v in values:
# mypy 知道 v 是 int | str | float
# 但无法知道循环第几次是什么类型
pass
何时使用
新项目 : 从第一天就加类型注解,用 --strict
现有项目 : 渐进式类型化,先公共 API 后内部代码
库/框架 : 必须有完整类型注解 + .pyi 存根
脚本/工具 : 至少给函数签名加注解
CI/CD : 集成 mypy 到 pre-commit 或 CI pipeline
总结: Python 类型系统的心智模型
markdown
复制代码
Java/Kotlin: 编译器强制类型 → 类型错误 = 编译失败
Python: 类型注解是提示 → mypy/pyright 检查 → 运行时不强制
关键原则:
1. 类型注解写给工具看,不是写给解释器看
2. 渐进式采用------从公共 API 开始
3. Protocol 是 Python 的杀手特性------鸭子类型 + 静态检查
4. 3.12+ 新语法(type 语句、泛型语法)大幅降低样板代码
5. 选择 mypy 或 pyright,不要两个混用