摘要:Python 3.5引入类型提示以来,类型系统已经非常成熟。本文从基础语法到高级用法,再到工程实践中的mypy配置和常见模式,帮你写出更健壮的Python代码。
为什么要用类型提示?
# 没有类型提示:猜参数类型全靠文档和运气
def process_data(data, config, callback):
...
# 有类型提示:一目了然
def process_data(
data: list[dict[str, Any]],
config: ProcessConfig,
callback: Callable[[str], None] | None = None,
) -> ProcessResult:
...
好处:IDE自动补全更准、重构更安全、文档自带、bug更早发现。
基础类型
# Python 3.10+ 可以直接用内置类型
name: str = '张三'
age: int = 25
score: float = 95.5
active: bool = True
# 容器类型
names: list[str] = ['张三', '李四']
scores: dict[str, int] = {'张三': 95, '李四': 88}
unique_ids: set[int] = {1, 2, 3}
point: tuple[float, float] = (1.0, 2.0)
# 可变长度元组
values: tuple[int, ...] = (1, 2, 3, 4, 5)
# None类型
result: str | None = None # Python 3.10+
# 等价于:Optional[str] = None
函数签名
def greet(name: str, times: int = 1) -> str:
return f'Hello {name}! ' * times
# 无返回值
def log(message: str) -> None:
print(message)
# *args 和 **kwargs
def flexible(*args: int, **kwargs: str) -> None:
...
# 默认值
def connect(
host: str = 'localhost',
port: int = 3306,
timeout: float | None = None,
) -> Connection:
...
高级类型
TypeAlias(类型别名)
from typing import TypeAlias
# 简单别名
UserId: TypeAlias = int
JsonDict: TypeAlias = dict[str, Any]
# 复杂类型简化
Handler: TypeAlias = Callable[[Request], Awaitable[Response]]
Middleware: TypeAlias = Callable[[Handler], Handler]
def add_middleware(app: list[Middleware], mw: Middleware) -> None:
app.append(mw)
Literal(字面量类型)
from typing import Literal
def set_log_level(level: Literal['DEBUG', 'INFO', 'WARNING', 'ERROR']) -> None:
...
set_log_level('INFO') # ✅
set_log_level('VERBOSE') # ❌ mypy报错
# 实用场景:限制参数取值
def sort_users(
by: Literal['name', 'age', 'created_at'] = 'created_at',
order: Literal['asc', 'desc'] = 'desc',
) -> list[User]:
...
TypedDict(字典结构约束)
from typing import TypedDict, NotRequired
class UserDict(TypedDict):
id: int
name: str
email: str
age: NotRequired[int] # 可选字段
def create_user(data: UserDict) -> None:
print(data['name']) # ✅ IDE知道有name字段
print(data['phone']) # ❌ mypy报错:没有phone字段
user: UserDict = {'id': 1, 'name': '张三', 'email': 'z@test.com'}
Protocol(结构化子类型)
from typing import Protocol, runtime_checkable
@runtime_checkable
class Drawable(Protocol):
def draw(self, x: int, y: int) -> None: ...
class Circle:
def draw(self, x: int, y: int) -> None:
print(f'Drawing circle at ({x}, {y})')
class Square:
def draw(self, x: int, y: int) -> None:
print(f'Drawing square at ({x}, {y})')
# Circle和Square都满足Drawable协议,不需要显式继承
def render(shape: Drawable) -> None:
shape.draw(0, 0)
render(Circle()) # ✅
render(Square()) # ✅
# runtime_checkable允许isinstance检查
isinstance(Circle(), Drawable) # True
Generic(泛型)
from typing import Generic, TypeVar
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('abc') # ❌ mypy报错
str_stack: Stack[str] = Stack()
str_stack.push('hello') # ✅
Overload(函数重载)
from typing import overload
@overload
def parse(data: str) -> dict: ...
@overload
def parse(data: bytes) -> dict: ...
@overload
def parse(data: str, as_list: Literal[True]) -> list: ...
def parse(data: str | bytes, as_list: bool = False) -> dict | list:
if isinstance(data, bytes):
data = data.decode()
result = json.loads(data)
if as_list and isinstance(result, dict):
return list(result.items())
return result
# mypy能根据参数类型推断返回类型
d = parse('{"a": 1}') # 推断为dict
l = parse('{"a": 1}', True) # 推断为list
实战模式
配置类
from dataclasses import dataclass, field
@dataclass
class DatabaseConfig:
host: str = 'localhost'
port: int = 3306
user: str = 'root'
password: str = ''
database: str = 'mydb'
pool_size: int = 5
@property
def url(self) -> str:
return f'mysql://{self.user}:{self.password}@{self.host}:{self.port}/{self.database}'
@dataclass
class AppConfig:
debug: bool = False
db: DatabaseConfig = field(default_factory=DatabaseConfig)
allowed_origins: list[str] = field(default_factory=lambda: ['*'])
回调和事件系统
from typing import Callable, TypeVar, ParamSpec
P = ParamSpec('P')
R = TypeVar('R')
class EventEmitter:
def __init__(self) -> None:
self._handlers: dict[str, list[Callable[..., None]]] = {}
def on(self, event: str, handler: Callable[..., None]) -> None:
self._handlers.setdefault(event, []).append(handler)
def emit(self, event: str, *args: Any, **kwargs: Any) -> None:
for handler in self._handlers.get(event, []):
handler(*args, **kwargs)
# 类型安全的事件定义
class UserEvents(EventEmitter):
def on_login(self, handler: Callable[[int, str], None]) -> None:
self.on('login', handler)
def emit_login(self, user_id: int, ip: str) -> None:
self.emit('login', user_id, ip)
mypy配置
pyproject.toml:
[tool.mypy]
python_version = "3.12"
strict = true
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
# 第三方库忽略
[[tool.mypy.overrides]]
module = ["requests.*", "bs4.*", "lxml.*"]
ignore_missing_imports = true
常用命令:
# 检查整个项目
mypy src/
# 检查单个文件
mypy app/main.py
# 生成报告
mypy src/ --html-report reports/mypy
渐进式迁移策略
老项目不需要一步到位:
# 第一步:函数签名加类型(收益最大)
def get_user(user_id: int) -> dict[str, Any]:
...
# 第二步:关键数据结构加类型
class User(TypedDict):
id: int
name: str
# 第三步:逐步开启strict模式
# mypy.ini中按模块配置严格程度
总结
类型提示的投入产出比非常高:
- 写的时候多花10%时间
- 省下50%的调试时间和90%的"这个参数是什么类型"的困惑
建议:新代码全部加类型提示,老代码在修改时逐步补上。配合mypy做CI检查,类型错误在提交前就能发现。