【Python AI教程】(四)类型提示:让AI代码更安全
Python 一直被称为”动态类型语言”,但随着 AI 应用的复杂度爆炸式增长,类型提示已经从”可选”变成了”必须”。本文将系统讲解类型提示的所有核心技能,让你的 AI 代码更安全、更易维护。
为什么 AI 代码特别需要类型提示?
AI 应用有几个独特特点,让类型变得尤为重要:
- 多源数据:API 响应、文件、数据库、LLM 输出——来源多,格式杂
- 嵌套结构:Agent 消息、Tool Schema、State 往往是多层嵌套的字典/对象
- 团队协作:AI 应用通常涉及多个模型/Agent/工具,接口协议必须清晰
- 重构频繁:Prompt 变了、数据结构变了——类型检查能第一时间发现问题
基础类型提示
1 2 3 4 5 6 7 8 9 10 11 12 13
| def greet(name: str, age: int = 0) -> str: return f"Hello {name}, age {age}"
from typing import List, Dict, Set, Tuple
def process_data( users: List[Dict[str, str]], scores: Set[int], metadata: Tuple[str, int, bool] ) -> Dict[str, any]: pass
|
注意:Python 3.9+ 可以直接用 list[str]、dict[str, int] 等内置类型,无需 typing 模块。
Optional 与 Union:处理”可能没有”的值
这是 AI 代码中使用频率最高的类型提示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| from typing import Optional, Union
def find_user(user_id: int) -> Optional[dict]: """找不到返回 None""" if user_id > 0: return {"id": user_id, "name": "Alice"} return None
def process_value(val: Union[str, int, float]) -> str: """三种输入都是合法的""" return str(val)
def process_value_v310(val: str | int | float) -> str: return str(val)
|
AI 应用场景
1 2 3 4 5 6 7 8 9 10 11
| @dataclass class LLMResponse: content: Optional[str] = None error: Optional[str] = None tokens_used: Optional[int] = None model: str = "gpt-4"
def __post_init__(self): if self.content is None and self.error is None: raise ValueError("Response must have either content or error")
|
Literal:枚举值与字符串字面量
当一个参数只能取特定的值时,Literal 是最佳选择:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| from typing import Literal
def http_method(method: Literal["GET", "POST", "PUT", "DELETE"]) -> None: print(f"HTTP {method}")
http_method("GET") http_method("PATCH")
AgentStatus = Literal["idle", "thinking", "acting", "waiting", "done"]
class Agent: def set_status(self, status: AgentStatus) -> None: self.status = status
MessageRole = Literal["user", "assistant", "system", "tool", "developer"]
|
Callable:函数作为参数和返回值
AI 代码中大量使用”函数作为参数”——这就是 Callable 的用武之地:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| from typing import Callable
def apply_transform( data: list[int], transform: Callable[[int], int] ) -> list[int]: return [transform(x) for x in data]
result = apply_transform([1, 2, 3], lambda x: x * 2) print(result)
def apply_two( fn: Callable[[int, int], int], a: int, b: int ) -> int: return fn(a, b)
print(apply_two(lambda x, y: x + y, 3, 4))
|
AI 应用:策略模式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| from dataclasses import dataclass from typing import Callable
LLMFactory = Callable[[str], str]
@dataclass class Agent: name: str model: str llm_call: LLMFactory
def think(self, prompt: str) -> str: return self.llm_call(prompt)
def openai_call(prompt: str) -> str: return f"[OpenAI] {prompt}"
def anthropic_call(prompt: str) -> str: return f"[Claude] {prompt}"
agent1 = Agent(name="GPT-Agent", model="gpt-4", llm_call=openai_call) agent2 = Agent(name="Claude-Agent", model="claude-3", llm_call=anthropic_call)
print(agent1.think("Explain AI")) print(agent2.think("Explain AI"))
|
TypeVar 与泛型:让类型更通用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| from typing import TypeVar, Generic
T = TypeVar("T") K = TypeVar("K") V = TypeVar("V")
def first(seq: list[T]) -> T: """取列表第一个元素,类型保持一致""" return seq[0]
def first_of_two(a: T, b: T) -> T: """两个同类型参数,返回同类型""" return a if True else b
print(first([1, 2, 3])) print(first(["a", "b"])) print(first_of_two(1, 2)) print(first_of_two("a", "b"))
|
泛型字典
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| from typing import TypeVar, Generic
K = TypeVar("K") V = TypeVar("V")
class Cache(Generic[K, V]): def __init__(self): self._store: dict[K, V] = {} def get(self, key: K) -> V | None: return self._store.get(key) def set(self, key: K, value: V) -> None: self._store[key] = value
string_cache: Cache[str, str] = Cache() string_cache.set("gpt-4", "Response from gpt-4") print(string_cache.get("gpt-4"))
|
Protocol:结构化子类型(最强大的技能)
这是 Python 3.8+ 引入的最强大特性——只要你有某个方法,就是某种类型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| from typing import Protocol, runtime_checkable
@runtime_checkable class LLMClient(Protocol): """只要你有 complete 方法,你就是 LLMClient""" def complete(self, prompt: str) -> str: ...
class RealOpenAI: def complete(self, prompt: str) -> str: return f"OpenAI: {prompt}"
class FakeLLM: """Mock 实现""" def complete(self, prompt: str) -> str: return f"Fake: {prompt}"
class NotAnLLM: """没有 complete 方法""" def query(self, prompt: str) -> str: return "I'm not an LLM"
real = RealOpenAI() fake = FakeLLM() not_llm = NotAnLLM()
print(f"RealOpenAI is LLM: {isinstance(real, LLMClient)}") print(f"FakeLLM is LLM: {isinstance(fake, LLMClient)}") print(f"NotAnLLM is LLM: {isinstance(not_llm, LLMClient)}")
|
Protocol vs ABC
| 特性 | Protocol | ABC |
|---|
| 继承方式 | 无需继承,结构匹配即可 | 需要显式继承 |
| 运行时检查 | @runtime_checkable 支持 isinstance | 始终支持 |
| 多继承冲突 | 无 | 可能有菱形继承问题 |
| 适用场景 | 插件系统、接口定义(AI Agent 场景) | 强制实现某些方法 |
AI 应用:统一 Agent 接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| from typing import Protocol, runtime_checkable, Any
@runtime_checkable class Tool(Protocol): """所有工具的统一接口""" @property def name(self) -> str: ... def execute(self, args: dict[str, Any]) -> str: ...
@runtime_checkable class Memory(Protocol): """所有记忆系统的统一接口""" def add(self, text: str) -> None: ... def search(self, query: str) -> list[str]: ...
class CalculatorTool: name = "calculator" def execute(self, args: dict) -> str: return str(eval(args["expr"], {"__builtins__": {}}))
class VectorStore: def add(self, text: str) -> None: print(f"Stored: {text[:20]}...") def search(self, query: str) -> list[str]: return [f"Result for {query}"]
def run_with_tools(tools: list[Tool]) -> None: for tool in tools: print(f"Running: {tool.name}")
calc = CalculatorTool() store = VectorStore() run_with_tools([calc])
|
typing vs Pydantic vs Dataclass
| 特性 | typing | Pydantic | @dataclass |
|---|
| 运行时验证 | ❌ | ✅✅ 自动 | ⚠️ 需 __post_init__ |
| JSON Schema 生成 | ❌ | ✅✅ | ❌ |
| 默认值 | ❌ | ✅ | ✅ |
| 嵌套模型 | ❌ | ✅ | ⚠️ 需配合 |
| 序列化 | ❌ | ✅✅ | ⚠️ 需配合 |
| 最佳场景 | 类型标注(不验证) | API 请求/响应 | 内部数据结构 |
AI 应用推荐:typing 做标注 + Pydantic 做 API 接口 + @dataclass 做内部状态
AI 应用实战:完整的消息与工具 Schema
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
| from dataclasses import dataclass, field from typing import Optional, Literal, Any from typing import Protocol
@dataclass class Message: role: Literal["user", "assistant", "system", "tool", "developer"] content: str name: Optional[str] = None tool_call_id: Optional[str] = None
@dataclass class ToolCall: id: str name: str args: dict[str, Any]
@dataclass class ToolResult: tool_call_id: str content: str is_error: bool = False
@dataclass class AgentState: messages: list[Message] = field(default_factory=list) tool_calls: list[ToolCall] = field(default_factory=list) tool_results: list[ToolResult] = field(default_factory=list) current_step: int = 0
def add_message(self, role: Literal["user", "assistant", "system", "tool"], content: str, **kwargs) -> None: self.messages.append(Message(role=role, content=content, **kwargs)) def add_tool_result(self, tool_call_id: str, content: str, is_error: bool = False) -> None: self.tool_results.append(ToolResult(tool_call_id=tool_call_id, content=content, is_error=is_error))
class Tool(Protocol): @property def name(self) -> str: ... @property def description(self) -> str: ... def execute(self, args: dict[str, Any]) -> str: ... def validate_args(self, args: dict[str, Any]) -> bool: ...
def validate_agent_state(state: AgentState) -> bool: """运行时验证状态一致性""" result_ids = {r.tool_call_id for r in state.tool_results} call_ids = {c.id for c in state.tool_calls} return result_ids.issubset(call_ids)
state = AgentState() state.add_message("user", "What's 2+2?") state.add_message("assistant", "Let me calculate...", tool_call_id="call_1") state.add_tool_result("call_1", "4")
print(f"State valid: {validate_agent_state(state)}") print(f"Messages: {len(state.messages)}")
|
类型提示检查工具
光写类型还不够,需要工具来检查:
1 2 3 4 5 6 7 8
| pip install mypy pyright
mypy your_agent_code.py --strict
pyright your_agent_code.py
|
总结
| 技能 | 何时用 | AI 应用场景 |
|---|
Optional[T] | 值可能不存在 | LLM 响应、文件读取 |
Union[T, U] | 多种可能类型 | 工具参数、API 响应 |
Literal["a", "b"] | 固定枚举值 | 消息角色、Agent 状态 |
Callable[[T], U] | 函数作为参数/返回值 | LLM 工厂、转换函数 |
TypeVar | 通用类型 | Cache、Container |
Protocol | 统一接口(最常用) | Tool、Memory、Agent 接口 |
@dataclass | 内部数据结构 | Message、AgentState |
下一章:【Python AI教程】(六)async/await:异步编程入门到精通——让你的 AI 应用并发处理多个 API 调用,不再排队等待。
graph LR
A[Python 源码]:::input --> B[PEP 484 类型注解]:::process
B --> C[mypy / pyright]:::process
C --> D{类型检查}:::decision
D -->|通过| E[IDE 智能提示]:::output
D -->|失败| F[类型错误报告]:::warn
classDef input fill:#FFE5E5,stroke:#FF9AA2,color:#333
classDef process fill:#E5F3FF,stroke:#A0C4FF,color:#333
classDef decision fill:#FFF4E5,stroke:#FFD6A0,color:#333
classDef output fill:#E5FFE5,stroke:#B5EAD7,color:#333
classDef warn fill:#FFD6A0,stroke:#FF9AA2,color:#333
📚 Python AI教程 系列导航
本文是《Python AI教程》系列第 4/14 篇。
📖 全部 14 篇目录(点击展开)
- (一)闭包与装饰器
- (二)上下文管理器
- (三)生成器与迭代器
- (四)类型提示 ← 当前
- (五)Dataclass 与 attrs
- (六)async/await
- (七)Threading 与 Multiprocessing
- (八)函数式编程
- (九)描述符协议
- (十)元类
- (十一)Protocol与结构化类型
- (十二)异常链与日志
- (十三)缓存艺术
- (十四)组合模式实战
对比分析
本章核心是 Python 的类型提示(typing 模块 / PEP 484+)。
维度一:类型提示 vs 旧写法(注释 + docstring)
| 方案 | 工具支持 | 运行时影响 | 文档能力 |
|---|
| 类型提示(PEP 484+) | mypy / pyright / IDE 自动补全 | 运行时默认不强制(Pydantic/Beartype 除外) | 类型即文档 |
| docstring + Sphinx | 仅文档生成 | 无 | 自由但易过时 |
| 手写注释 | 无工具 | 无 | 不可机器解析 |
# type: x 注释 | mypy 旧式支持 | 无 | 易读性差 |
维度二:与其他语言的静态类型
| 语言 | 类型系统 | 与 Python 类型提示对比 |
|---|
| Java | 强类型、编译期检查、泛型擦除 | 类型即强制约束;缺点是不支持结构化子类型 |
| C++ | 模板 + Concepts(C++20) | 编译期模板元编程、零运行时开销;学习曲线陡 |
| Go | 静态类型 + interface{} + generics(1.18+) | 显式声明、语法简单;缺点是泛型能力有限 |
| Rust | 静态类型 + Trait + 泛型 | 表达力最强(生命周期、Trait bound);缺点是编译慢 |
| TypeScript | 结构化类型(与 Python Protocol 思路一致) | 最像 Python 的静态类型;可在 JS 引擎直接擦除运行 |
| C# | 强类型 + var + generics | 与 Java 类似,但 nullable reference types(C# 8+)处理”可空”更优雅 |
维度三:typing vs 数据类库
| 方案 | 主要用途 | 校验 | 序列化 | 适用场景 |
|---|
| typing | 类型注解 | ❌(仅类型检查) | ❌ | 函数签名、IDE 提示 |
| dataclass | 简化类定义 | ❌ | ❌ | DTO、配置对象 |
| attrs | 同 dataclass,功能更多 | ✅(可选 validators) | ❌ | 复杂数据类 |
| Pydantic | 数据验证 + 序列化 | ✅ 强大 | ✅ JSON / dict | API 边界、配置加载 |
| TypedDict | 字典的键值类型 | ❌ | ❌ | JSON 风格数据结构 |
优缺点小结
- Python typing:渐进式、可选、零运行时开销;缺点是类型擦除后运行时无法校验
- Java / C#:强制类型、编译期保证;缺点是改类型要改大量代码
- TypeScript:结构化子类型最成熟;缺点是大型项目编译慢
- Rust:类型即正确性证明;缺点是开发速度最慢
何时选
- 选 typing:库作者公开 API、IDE 提示、文档
- 选 dataclass:纯数据容器,不需要运行时校验
- 选 Pydantic:API 边界、外部输入(HTTP body / 配置文件)
- 选 TypedDict:动态字典、JSON 风格结构
- 选 Protocol:需要”鸭子类型”的形式化(见第 11 章)
- 运行时校验需求强:再叠一层 Beartype 或 Pydantic