类型系统与 typing
现代 Python 开发必须掌握类型注解。它让代码更可读、IDE 更智能、bug 更早暴露。
为什么需要类型注解
python
# 没有类型注解 — 参数含义不明
def process(data, config, flag):
...
# 有类型注解 — 一目了然
def process(
data: list[dict],
config: dict[str, str],
flag: bool = False
) -> list[str]:
...基础类型注解
python
# 变量注解
name: str = "Alice"
age: int = 30
score: float = 9.5
active: bool = True
# 函数注解
def greet(name: str, times: int = 1) -> str:
return (f"Hello, {name}! " * times).strip()
# 注解不影响运行时(Python 是动态语言)
greet(123, "abc") # 运行时不报错,但 mypy 会报错typing 模块核心类型
容器类型
python
from typing import List, Dict, Tuple, Set # Python 3.8 及以下
# Python 3.9+ 直接用内置类型
from __future__ import annotations # 3.7-3.9 启用新语法
def process_users(
names: list[str],
scores: dict[str, float],
coords: tuple[int, int],
tags: set[str],
) -> list[dict[str, float]]:
...Optional 和 Union
python
from typing import Optional, Union
# Optional[X] 等价于 Union[X, None]
def find_user(user_id: int) -> Optional[dict]:
... # 可能返回 None
# Union — 多种类型之一
def parse(value: Union[str, int, float]) -> str:
return str(value)
# Python 3.10+ 用 | 语法
def parse_new(value: str | int | float) -> str:
return str(value)Any — 逃生舱
python
from typing import Any
def legacy_function(data: Any) -> Any:
# 与旧代码交互时使用,尽量避免
return data泛型
python
from typing import TypeVar, Generic
T = TypeVar('T')
class Stack(Generic[T]):
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
return self._items.pop()
def peek(self) -> T:
return self._items[-1]
# 使用时类型安全
int_stack: Stack[int] = Stack()
int_stack.push(1)
int_stack.push(2)
val: int = int_stack.pop() # mypy 知道这是 intProtocol — 结构子类型
不需要继承,只需要有对应的方法:
python
from typing import Protocol
class Drawable(Protocol):
def draw(self) -> None: ...
def resize(self, factor: float) -> None: ...
class Circle:
def draw(self) -> None:
print("画圆")
def resize(self, factor: float) -> None:
self.radius *= factor
class Square:
def draw(self) -> None:
print("画方")
def resize(self, factor: float) -> None:
self.side *= factor
def render(shape: Drawable) -> None:
shape.draw()
# Circle 和 Square 都没有继承 Drawable,但满足协议
render(Circle()) # 类型检查通过
render(Square()) # 类型检查通过TypedDict — 字典类型
python
from typing import TypedDict, Required, NotRequired
class UserProfile(TypedDict):
name: str
age: int
email: str
# Python 3.11+ 支持 Required/NotRequired
class Config(TypedDict):
host: Required[str]
port: Required[int]
debug: NotRequired[bool] # 可选字段
def create_user(profile: UserProfile) -> None:
print(f"创建用户: {profile['name']}")
# mypy 会检查字段类型和必填项
create_user({"name": "Alice", "age": 30, "email": "alice@example.com"})Literal — 字面量类型
python
from typing import Literal
Mode = Literal['r', 'w', 'a', 'rb', 'wb']
def open_file(path: str, mode: Mode) -> None:
...
open_file("data.txt", "r") # OK
# open_file("data.txt", "x") # mypy 报错:不在 Literal 范围内dataclass + 类型注解
python
from dataclasses import dataclass, field
from typing import ClassVar
@dataclass
class Point:
x: float
y: float
label: str = ""
_cache: dict = field(default_factory=dict, repr=False)
# 类变量(不是实例字段)
dimensions: ClassVar[int] = 2
def distance_to(self, other: 'Point') -> float:
return ((self.x - other.x)**2 + (self.y - other.y)**2) ** 0.5
p1 = Point(0, 0)
p2 = Point(3, 4)
print(p1.distance_to(p2)) # 5.0ParamSpec 和 Concatenate — 装饰器类型
python
from typing import ParamSpec, Callable, TypeVar
P = ParamSpec('P')
R = TypeVar('R')
def logged(func: Callable[P, R]) -> Callable[P, R]:
import functools
@functools.wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
print(f"调用 {func.__name__}")
return func(*args, **kwargs)
return wrapper
@logged
def add(x: int, y: int) -> int:
return x + y
# mypy 知道 add 的参数类型仍然是 (int, int) -> int
result: int = add(1, 2)overload — 函数重载
python
from typing import overload
@overload
def process(x: int) -> int: ...
@overload
def process(x: str) -> str: ...
@overload
def process(x: list) -> list: ...
def process(x):
if isinstance(x, int):
return x * 2
elif isinstance(x, str):
return x.upper()
else:
return [i * 2 for i in x]
# mypy 根据输入类型推断返回类型
n: int = process(5)
s: str = process("hello")运行时类型检查
python
from typing import get_type_hints
import inspect
def validate_call(func):
hints = get_type_hints(func)
sig = inspect.signature(func)
import functools
@functools.wraps(func)
def wrapper(*args, **kwargs):
bound = sig.bind(*args, **kwargs)
for name, value in bound.arguments.items():
if name in hints and not isinstance(value, hints[name]):
raise TypeError(f"{name} 应为 {hints[name]}")
return func(*args, **kwargs)
return wrapper
# 或者直接用 pydantic 的 @validate_call(更强大)
from pydantic import validate_call
@validate_call
def create_user(name: str, age: int) -> dict:
return {"name": name, "age": age}
create_user("Alice", 30) # OK
# create_user("Alice", "30") # ValidationError(pydantic 会自动转换或报错)推荐工具
- mypy:最成熟的静态类型检查器
- pyright / pylance:微软出品,VS Code 内置,速度更快
- pydantic:运行时类型验证,FastAPI 的基础