Python-1-基础
Python-1-基础
模块 1:Type Hints + Pydantic
知识点 0:from ... import ... 语法——怎么把工具搬进来
Python 的模块就是一个"工具箱",你写代码时需要从工具箱里拿工具出来。
| 写法 | 含义 | 例子 | 使用时 |
|---|---|---|---|
import 模块名 |
整个工具箱搬进来 | import typing |
typing.Optional |
from 模块 import 工具 |
只拿你要的那件 | from typing import Optional |
直接写 Optional |
from 模块 import A, B, C |
拿多件 | from typing import Optional, Union, Literal |
直接用每个名字 |
⚠️
from 模块 import *会搬全部,但不知道哪些名字被导入,容易冲突,面试不推荐。
实战场景:
# typing 是 Python 内置的"类型标注工具箱"
from typing import Optional, Union, Literal
# pydantic 是第三方库,需要 pip install pydantic
# 它是"数据校验工具箱"
from pydantic import BaseModel, Field
# 搬进来之后,Optional、Literal、BaseModel、Field 这些名字
# 就可以在你的代码里直接使用了
```text
### 知识点 1:**函数**定义语法 + 类型标注
普通函数你可能见过:
```python
def greet(name):
return "Hello " + name
```text
加上类型标注后,每个参数和返回值都贴了"标签":
```python
def create_prompt(
system: str, # 参数 system 是字符串
messages: list[dict[str, str]], # 参数 messages 是嵌套列表(下面详讲)
temperature: float = 0.7, # 参数 temperature 是浮点数,默认 0.7
model: Optional[str] = None, # 参数 model 可以是字符串或 None,默认 None
) -> dict: # 返回值是字典
...
```text
逐行解读:
| 语法 | 读法 | 含义 |
| -------------------------------- | ------------------------------- | ---------- |
| `system: str` | system 是字符串 | 入参必须是 str |
| `messages: list[dict[str, str]]` | messages 是列表,列表里是字典,字典的键和值都是字符串 | 嵌套类型(下面详讲) |
| `temperature: float = 0.7` | temperature 是浮点数,不传就默认 0.7 | 有默认值的参数 |
| `model: Optional[str] = None` | model 可以是字符串或 None,不传就默认 None | 可空参数 |
| `-> dict` | 返回值是字典 | 函数输出什么类型 |
> `Optional[str]` 等价于 `str | None`(Python 3.10+ 的新写法,下面详讲竖线 `|`)
### 知识点 2:数据类型标注——嵌套形式怎么读
你看到 `list[dict[str, str]]` 这种"套娃"写法可能会懵。**从外向里读**:
```text
list[dict[str, str]]
│ │ │ │
│ │ │ └── 值(value)是字符串
│ │ └────── 键(key)是字符串
│ └────── 每个列表元素是一个字典
└────── 最外层是列表
```text
**实际数据长这样**:
```python
messages = [
{"role": "system", "content": "你是一个AI助手"}, # 第1个字典
{"role": "user", "content": "什么是RAG?"}, # 第2个字典
{"role": "assistant", "content": "RAG是检索增强生成"}, # 第3个字典
]
# 3个字典排成队 → list
# 每个字典的key和value都是字符串 → dict[str, str]
```text
更多嵌套例子:
| 类型标注 | 含义 | 实际数据例子 |
|----------|------|-------------|
| `list[str]` | 字符串列表 | `["a", "b", "c"]` |
| `dict[str, int]` | 键是字符串、值是整数的字典 | `{"age": 25, "score": 100}` |
| `list[list[int]]` | 列表的列表(二维数组) | `[[1,2], [3,4], [5,6]]` |
| `dict[str, list[str]]` | 键是字符串、值是字符串列表 | `{"tags": ["python", "ai"]}` |
| `list[dict[str, str]]` | 字典排成队 | 见上面 messages |
### 知识点 3:`|` 竖线语法——"或者"的意思
你在代码里看到 `str | None`,这个竖线 `|` 是 Python 3.10+ 引入的新语法,就是"或者":
```python
# 新写法(Python 3.10+,推荐)
reasoning: str | None = None # reasoning 可以是字符串,也可以是 None
# 旧写法(Python 3.9 以前,也能用)
reasoning: Optional[str] = None # Optional[str] 等价于 str | None
reasoning: Union[str, None] = None # Union 也是"或者"的意思
```text
三者完全等价:
| 写法 | 版本要求 | 含义 |
|------|---------|------|
| `str | None` | Python 3.10+ | 可以是字符串,也可以是 None |
| `Optional[str]` | Python 3.5+ | 同上,专门给"可为空"用的简写 |
| `Union[str, None]` | Python 3.5+ | 同上,通用"或者"写法 |
更多 `|` 例子:
```python
# 变量可以是多种类型之一
result: str | int # 字符串或整数
data: list[str] | dict # 列表或字典
model: Literal["gpt-4", "deepseek", "claude"] | None # 限定值或空值
# 函数参数也可以用 |
def process(input: str | dict) -> str | None:
...
```text
> ⚠️ `|` 和 `Union` 只是**声明**——Python 运行时不会自动拦你。你传个 int 给声明了 `str` 的参数,代码照样跑。**真正拦你的是 Pydantic**(下面讲)。
### 知识点 4:Literal——"只能选这几个值"
`Literal` 是 typing 工具箱里的"限定器"——告诉 Python 这个变量只能是列出来的几个值之一:
```python
from typing import Literal
role: Literal["system", "user", "assistant"]
# role 只能是这三个字符串之一,传别的值会:
# - Type Hints 层面:IDE 会提示你错了
# - Pydantic 层面:运行时真的报 ValidationError
```python
实际场景:LLM 对话的 role 字段只能是 system、user、assistant 三个角色,用 Literal 就锁死了,不会出现 "admin"、"god" 这种奇怪值。
### 知识点 5:类定义语法——TypedDict vs Pydantic BaseModel
原文档里的 `class Message(TypedDict)` 和 `class LLMResponse(BaseModel)` **都是类**定义,但作用不同:
**TypedDict——只贴标签,不拦人**:
```python
from typing import TypedDict
class Message(TypedDict):
role: Literal["system", "user", "assistant"]
content: str
# TypedDict 只是告诉你"字典应该有这两个键"
# 运行时不会校验,传个 {"role": "hacker", "content": 123} 也不会报错
msg = Message(role="user", content="你好") # OK,但类型错误不会拦
```text
**Pydantic BaseModel——贴标签 + 真拦截**:
```python
from pydantic import BaseModel, Field
class LLMResponse(BaseModel):
content: str # 必须是字符串
reasoning: str | None = None # 可以是字符串或空,默认空
tool_calls: list[dict] = Field(default_factory=list) # 默认空列表
tokens_used: int = Field(ge=0) # 整数,且 >= 0
# Pydantic 运行时真查真拦
response = LLMResponse(content="回答", tokens_used=100) # ✅ OK
response = LLMResponse(content="回答", tokens_used=-5) # ❌ ValidationError!
response = LLMResponse(tokens_used=100) # ❌ ValidationError! content 缺失
```text
关键对比:
| 维度 | TypedDict | Pydantic BaseModel |
| -------- | --------- | ----------------------------- |
| 运行时校验 | ❌ 不校验 | ✅ 自动校验 |
| 字段缺失 | 不报错 | 报 ValidationError |
| 类型错误 | 不报错 | 报 ValidationError |
| 值范围限制 | 不支持 | `Field(ge=0)` 支持 |
| 默认值 | 不支持工厂 | `Field(default_factory=list)` |
| JSON 序列化 | 需要手动 | `.model_dump()` 一行搞定 |
| 使用场景 | 简单字典结构标注 | API 数据校验、LLM 输出校验 |
### 知识点 6:Field 的各种用法
`Field` 是 Pydantic 给字段加"约束"的工具:
```python
from pydantic import BaseModel, Field
class Example(BaseModel):
# 默认值
name: str = "unknown"
# 有约束的默认值
steps: int = Field(default=5, ge=1, le=20) # 默认5,且必须在 1~20 之间
# 默认值工厂(用于可变默认值,如列表、字典)
tags: list[str] = Field(default_factory=list) # 默认空列表,每次创建新实例
# 范围约束
age: int = Field(ge=0, le=150) # 0~150
score: float = Field(gt=0, lt=100) # 0 ⚠️ 为什么 `default_factory=list` 而不是 `default=[]`?因为 Python 的默认值是共享的——如果写 `default=[]`,所有实例共享同一个列表,改了一个就全改了。`default_factory=list` 每次创建时调用 `list()` 生成新列表,互不干扰。
### 知识点 7:Type Hints + Pydantic 的完整流程——为什么 LLM 输出必须校验
整体流程是这样的:
```text
你定义期望结构(Type Hints 声明 + Pydantic 写校验规则)
↓
LLM 返回 JSON 数据(可能不完整、可能类型错)
↓
Pydantic 校验(字段齐全?类型对?值合规?)
↓ ✅ 通过 ↓ ❌ 不通过
拿到干净数据 报 ValidationError → 重试或降级
```text
为什么必须校验?三个真实场景:
1. **LLM 输出不稳定**——同一个 prompt,可能这次返回 3 个字段,下次只返回 2 个
2. **LLM 会搞错类型**——你让它返回整数,它可能返回 `"100"`(字符串形式的整数)
3. **下游代码依赖结构**——如果你不校验就直接用 `response["content"]`,字段缺失就崩溃
```python
# 实战:用 Pydantic 校验 LLM 输出
from pydantic import BaseModel, ValidationError
class LLMResponse(BaseModel):
content: str
tokens_used: int = Field(ge=0)
# LLM 返回的原始 JSON
raw_json = {"content": "RAG是检索增强生成", "tokens_used": 150}
try:
response = LLMResponse(**raw_json) # ✅ 校验通过
print(response.content) # 安全使用
except ValidationError as e:
print(f"数据格式不对: {e}") # 拦住了脏数据
# 可以选择重试、降级、或报错
```text
### ⚡ 动手练习(10min)
用 Pydantic 定义一个 `AgentConfig` 模型,包含:agent_name(str)、max_steps(int, 默认 5)、allowed_tools(list[str])、model(str, 必须是 "gpt-4"/"deepseek"/"claude" 之一)。
> 💡 提示:
- `agent_name: str` 普通字符串字段
- `max_steps: int = Field(default=5, ge=1, le=20)` 有约束的默认值
- `allowed_tools: list[str] = Field(default_factory=list)` 默认空列表
- `model: Literal["gpt-4", "deepseek", "claude"]` 限定值
参考答案
```python
from pydantic import BaseModel, Field
from typing import Literal
class AgentConfig(BaseModel):
agent_name: str
max_steps: int = Field(default=5, ge=1, le=20)
allowed_tools: list[str] = Field(default_factory=list)
model: Literal["gpt-4", "deepseek", "claude"]
```text
验证一下:
```python
# 正常创建
config = AgentConfig(agent_name="my_agent", model="deepseek")
print(config.max_steps) # 5(默认值)
print(config.allowed_tools) # [](默认空列表)
# 类型错误 → ValidationError
bad_config = AgentConfig(agent_name="x", model="invalid_model")
# 值范围错误 → ValidationError
bad_config = AgentConfig(agent_name="x", model="deepseek", max_steps=100)
```python
## 最终检验测试
---
> 下面是 10 道题,**独立完成,不查资料**,能做出 7/10 即达标。
### Part A:代码题(每题 10 分)
**Q1**:用 Pydantic 定义一个 `ChatMessage` 模型,字段:role(必须是 system/user/assistant)、content(str)、metadata(dict,可为空,默认 {})。
**Q2**:写一个 `@cache_result(ttl_seconds)` 装饰器:首次调用缓存结果,ttl 秒内再次调用直接返回缓存。
**Q3**:写一个生成器 `chunk_text(text, chunk_size)`,每次 yield chunk_size 长度的一段文本,最后一段可能不足 chunk_size。
**Q4**:用 `@contextmanager` 写一个 `temp_dir()` 上下文管理器:进入时创建临时目录,退出时删除。
**Q5**:补全以下代码,实现 3 个异步任务并发执行,任何一个失败不影响其他,最终打印所有成功的结果。
```python
async def fetch_url(url: str) -> str:
await asyncio.sleep(random.uniform(0.5, 2.0))
if random.random() < 0.3:
raise Exception(f"{url} 请求失败")
return f"{url} 的内容"
async def fetch_all(urls: list[str]) -> list[str]:
# TODO: 补全这里
...
```python
**Q6**:写一个 `LLMCaller` 类,用 `async with` 使用,`__aenter__` 创建 httpx.AsyncClient,`__aexit__` 关闭。
### Part B:概念题(每题 5 分)
**Q7**:解释 `asyncio.gather(return_exceptions=True)` 和 `asyncio.gather(return_exceptions=False)` 的区别。
**Q8**:`@functools.wraps` 的作用是什么?不用它会有什么问题?
**Q9**:生成器(Generator)和列表(List)在处理大数据时的核心区别是什么?
**Q10**:Pydantic BaseModel vs Python dataclass 的区别?
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 茯茶养生人的博客!





