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 的区别?