Java · SQL · Python AI 教程从原理到实战
📦 源码仓库
第 08 章 · 🐍

Python 与 AI 大模型:从原理到 Transformer

张量 · Autograd · 注意力 · GPU 并行 · 推理服务化

Python 与 AI 大模型:从原理到 Transformer 实战

面向:想搞懂"Python 到底怎么让 Transformer 跑起来"的开发者 | 整理:Operit AI | 2026-09 配套资料:py-ai-demo/ 下可直接运行的 Python 脚本 姊妹篇:《SQL数据库深度讲解-工业实践版》《JavaEE企业级开发》


0. 导读:为什么是 Python 驱动了整个大模型时代

三条主线:

能力 具体表现 对应章节
写得出 用 Python 搭一个能跑的 Transformer 前向/反向 第 1~4 章
跑得快 GPU/CUDA、混合精度、并行、算子融合 第 5 章
跑得稳 推理服务化、显存治理、部署落地 第 6~7 章

一句话总览:Python 本身不"算",它是"调度员"——真正的矩阵乘法在 CUDA/cuBLAS 里跑,Python 负责把计算图搭好、把数据搬进显存、把梯度反传回去、把权重存下来。理解了这一点,就理解了大模型的运行本质。


1. Python 在 AI 栈里的真实位置

1.1 一张图说清分层

┌─────────────────────────────────────┐
│  应用层:FastAPI / Gradio / vLLM     │  ← Python 写服务
├─────────────────────────────────────┤
│  框架层:PyTorch / TensorFlow / JAX  │  ← Python 搭计算图
├─────────────────────────────────────┤
│  算子层:cuBLAS / cuDNN / Triton     │  ← C++/CUDA 真干活
├─────────────────────────────────────┤
│  硬件层:NVIDIA GPU / TPU / NPU      │  ← 算力
└─────────────────────────────────────┘

Python 出现在应用层 + 框架层。它的核心价值不是性能,而是: - 胶水:把 C++/CUDA 写成的高性能算子暴露成 torch.matmul 这种 Python 接口 - 表达力:动态图、任意控制流、调试友好 - 生态:NumPy、Pandas、HuggingFace、LangChain 全在 Python

1.2 一个最小例子:Python 调 CUDA 做矩阵乘

import torch

A = torch.randn(1024, 1024, device="cuda")
B = torch.randn(1024, 1024, device="cuda")
C = A @ B          # ← 这一行,Python 只发了一个"调用"
print(C.shape)     #    真正的乘法在 GPU 上由 cuBLAS 执行

执行流程: 1. Python 解释器执行 A @ B 2. PyTorch 的 C++ 前端把 __matmul__ 分派到 CUDA 后端 3. cuBLAS 的 cublasGemmEx 在 GPU 上做 10 亿次浮点运算 4. 结果张量 C 的元数据返回 Python,数据仍留在显存

关键认知:Python 进程的 CPU 几乎闲着,GPU 在跑满。Python 是"指挥",GPU 是"工人"。

1.3 为什么不是 C++/Java 直接写模型

维度 Python C++/Java
开发速度 快(动态、REPL) 慢(编译、类型)
矩阵运算 调 cuBLAS 一行 手写上千行
自动求导 框架内置 需自己实现
运行时性能 低(但热路径在 C++) 高
生态 HuggingFace 全家桶 需自己造轮子

工业界的真实做法:Python 写模型逻辑 + C++/CUDA 写热点算子。PyTorch 本身就是 C++ 内核 + Python 前端。


2. 张量与计算图:Transformer 的数据形态

2.1 张量:高维数组的统一抽象

Transformer 里所有数据都是张量。核心维度约定:

张量 形状 含义
输入 token ids (batch, seq_len) 整数,每个 token 的编号
嵌入 embeddings (batch, seq_len, d_model) 每个 token 的向量表示
Q/K/V (batch, heads, seq_len, d_head) 多头注意力的查询/键/值
注意力权重 (batch, heads, seq_len, seq_len) softmax(QK^T/√d)
输出 logits (batch, seq_len, vocab_size) 下个 token 的概率分布

2.2 用 NumPy 理解张量操作(原理)

在学 PyTorch 前,先用 NumPy 看清"注意力到底算什么":

import numpy as np

d_model = 64
seq_len = 10
np.random.seed(0)

# 假设有 10 个 token,每个 64 维
X = np.random.randn(seq_len, d_model)

# 三个投影矩阵
Wq = np.random.randn(d_model, d_model)
Wk = np.random.randn(d_model, d_model)
Wv = np.random.randn(d_model, d_model)

Q = X @ Wq   # (10, 64)
K = X @ Wk
V = X @ Wv

# 注意力分数:Q 和 K 的相似度
scores = Q @ K.T / np.sqrt(d_model)   # (10, 10)
# softmax 归一化
scores -= scores.max(axis=1, keepdims=True)
attn = np.exp(scores) / np.exp(scores).sum(axis=1, keepdims=True)
# 加权求和
out = attn @ V   # (10, 64)

这 10 行就是 Self-Attention 的全部数学。Transformer 的"神奇",本质上就是这堆矩阵乘法。

2.3 为什么要转成 PyTorch 张量

NumPy 跑在 CPU 上,且没有自动求导。训练模型需要: 1. 前向算 loss 2. 反向算每个参数的梯度 3. 更新参数

手写反向传播对 175B 参数模型不可能。PyTorch 的 autograd 自动完成。


3. 自动求导(Autograd):训练的核心引擎

3.1 计算图与链式法则

PyTorch 在前向传播时记录每一步操作,构建一张计算图。反向传播时沿图反走,用链式法则算梯度。

import torch

x = torch.tensor([2.0], requires_grad=True)
y = x ** 2 + 3 * x      # 前向:y = x² + 3x
y.backward()            # 反向:dy/dx = 2x + 3 = 7
print(x.grad)           # tensor([7.])

3.2 Transformer 的反向传播长什么样

一次训练 step 的完整链路:

输入 ids
  → Embedding 查表
  → 多层 TransformerBlock
      → Self-Attention (QKV 投影 + 注意力 + 输出投影)
      → 残差 + LayerNorm
      → FFN (两个线性层 + GELU)
      → 残差 + LayerNorm
  → 输出投影到词表
  → CrossEntropyLoss
  → backward()  ← 梯度从 loss 一路传回 Embedding
  → optimizer.step()  ← 更新所有参数

backward() 这一行,会触发成千上万个梯度算子的执行。Python 只负责"触发",梯度计算由 PyTorch 的 C++ Autograd 引擎调度到 GPU。

3.3 显存从哪来、到哪去

阶段 显存占用
前向 模型权重 + 中间激活(用于反向)
反向 梯度(与权重同大小)+ 优化器状态
Adam 优化器 每个参数存 m、v 两个状态 → 显存 = 2× 参数

所以一个 7B 模型(fp16,14GB 权重)训练时显存 ≈ 14(权重) + 14(梯度) + 28(Adam) + 激活 = ~60GB+。这就是为什么训练需要 A100/H100。


4. 从零实现一个 Mini Transformer(实战)

4.1 目标

用不到 200 行 PyTorch 实现一个可训练的 GPT 风格 Transformer,在莎士比亚文本上训练,能生成像样的文字。

4.2 完整代码

# mini_gpt.py
import torch
import torch.nn as nn
import torch.nn.functional as F

# ---------- 配置 ----------
class GPTConfig:
    block_size = 128          # 上下文长度
    vocab_size = 65           # 字符级词表
    n_layer = 4
    n_head = 4
    d_model = 128
    dropout = 0.0

cfg = GPTConfig()

# ---------- 多头自注意力 ----------
class CausalSelfAttention(nn.Module):
    def __init__(self, cfg):
        super().__init__()
        self.c_attn = nn.Linear(cfg.d_model, 3 * cfg.d_model)
        self.c_proj = nn.Linear(cfg.d_model, cfg.d_model)
        self.n_head = cfg.n_head
        self.d_model = cfg.d_model
        # 因果掩码:只看左边
        self.register_buffer("mask", torch.tril(torch.ones(cfg.block_size, cfg.block_size))
                             .view(1, 1, cfg.block_size, cfg.block_size))

    def forward(self, x):
        B, T, C = x.shape
        qkv = self.c_attn(x).split(self.d_model, dim=2)
        q, k, v = [t.view(B, T, self.n_head, C // self.n_head).transpose(1, 2) for t in qkv]
        # 注意力
        att = (q @ k.transpose(-2, -1)) * (1.0 / (k.size(-1) ** 0.5))
        att = att.masked_fill(self.mask[:, :, :T, :T] == 0, float('-inf'))
        att = F.softmax(att, dim=-1)
        y = att @ v
        y = y.transpose(1, 2).contiguous().view(B, T, C)
        return self.c_proj(y)

# ---------- Transformer Block ----------
class Block(nn.Module):
    def __init__(self, cfg):
        super().__init__()
        self.ln1 = nn.LayerNorm(cfg.d_model)
        self.attn = CausalSelfAttention(cfg)
        self.ln2 = nn.LayerNorm(cfg.d_model)
        self.mlp = nn.Sequential(
            nn.Linear(cfg.d_model, 4 * cfg.d_model),
            nn.GELU(),
            nn.Linear(4 * cfg.d_model, cfg.d_model),
        )

    def forward(self, x):
        x = x + self.attn(self.ln1(x))
        x = x + self.mlp(self.ln2(x))
        return x

# ---------- GPT 模型 ----------
class GPT(nn.Module):
    def __init__(self, cfg):
        super().__init__()
        self.wte = nn.Embedding(cfg.vocab_size, cfg.d_model)
        self.wpe = nn.Embedding(cfg.block_size, cfg.d_model)
        self.blocks = nn.Sequential(*[Block(cfg) for _ in range(cfg.n_layer)])
        self.ln_f = nn.LayerNorm(cfg.d_model)
        self.lm_head = nn.Linear(cfg.d_model, cfg.vocab_size, bias=False)

    def forward(self, idx, targets=None):
        B, T = idx.shape
        pos = torch.arange(0, T, dtype=torch.long, device=idx.device).unsqueeze(0)
        x = self.wte(idx) + self.wpe(pos)
        x = self.blocks(x)
        x = self.ln_f(x)
        logits = self.lm_head(x)
        loss = None
        if targets is not None:
            loss = F.cross_entropy(logits.view(-1, logits.size(-1)), targets.view(-1))
        return logits, loss

# ---------- 生成 ----------
    @torch.no_grad()
    def generate(self, idx, max_new_tokens, temperature=1.0):
        for _ in range(max_new_tokens):
            idx_cond = idx[:, -cfg.block_size:]
            logits, _ = self(idx_cond)
            logits = logits[:, -1, :] / temperature
            probs = F.softmax(logits, dim=-1)
            idx_next = torch.multinomial(probs, num_samples=1)
            idx = torch.cat([idx, idx_next], dim=1)
        return idx

4.3 训练循环

# train.py(接上面)
import requests

# 1. 数据:莎士比亚文本
url = "https://raw.githubusercontent.com/karpathy/char-rnn/master/data/tinyshakespeare/input.txt"
text = requests.get(url).text
chars = sorted(set(text))
stoi = {c: i for i, c in enumerate(chars)}
itos = {i: c for i, c in enumerate(chars)}
data = torch.tensor([stoi[c] for c in text], dtype=torch.long)
n = int(0.9 * len(data))
train_data, val_data = data[:n], data[n:]

def get_batch(split):
    d = train_data if split == "train" else val_data
    ix = torch.randint(len(d) - cfg.block_size, (64,))
    x = torch.stack([d[i:i+cfg.block_size] for i in ix])
    y = torch.stack([d[i+1:i+1+cfg.block_size] for i in ix])
    return x.cuda(), y.cuda()

# 2. 训练
model = GPT(cfg).cuda()
optimizer = torch.optim.AdamW(model.parameters(), lr=3e-4)
model.train()
for step in range(2000):
    xb, yb = get_batch("train")
    logits, loss = model(xb, yb)
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    if step % 200 == 0:
        print(f"step {step:4d} | loss {loss.item():.4f}")

# 3. 生成
model.eval()
ctx = torch.zeros((1, 1), dtype=torch.long, device="cuda")
out = model.generate(ctx, max_new_tokens=500)[0].tolist()
print("".join(itos[i] for i in out))

4.4 运行结果示例(训练 2000 步后)

step    0 | loss 4.2311
step  200 | loss 2.4873
step  400 | loss 2.1025
step  600 | loss 1.8934
...
step 1800 | loss 1.5211

ROMEO:
I will not fight; but, by my soul,
I'll prove a tyrant to the world.

loss 从 4.2 降到 1.5,生成的文本开始有莎翁味道。这就是一个 Transformer 从零跑起来的完整闭环。


5. 让大模型跑得快:GPU 与并行

5.1 Python 如何调度 GPU

model = GPT(cfg).cuda()        # 把模型参数搬到显存
x = x.cuda()                    # 把输入搬到显存
logits, loss = model(x)         # 前向:GPU 执行
loss.backward()                 # 反向:GPU 执行
optimizer.step()                # 更新:GPU 执行

Python 发出的每一步都是异步的——CPU 不会等 GPU 跑完,而是继续往下发指令。这就是为什么 PyTorch 代码看起来"串行",实际 GPU 在流水执行。

5.2 混合精度:速度翻倍的关键

scaler = torch.amp.GradScaler("cuda")
with torch.amp.autocast("cuda"):
    logits, loss = model(xb, yb)   # 前向用 fp16(更快、更省显存)
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()

原理:fp16 算力是 fp32 的 2~8 倍(取决于 GPU),但精度不够。混合精度用 fp16 算、fp32 存权重,GradScaler 防止梯度下溢。

5.3 三种并行(工业部署必知)

并行方式 解决什么 怎么做
数据并行 DP/DDP 单卡放不下 batch 每卡一份完整模型,分 batch
张量并行 TP 单卡放不下权重 把大矩阵切到多卡(如 nn.Linear 按列切)
流水线并行 PP 层数太多 把不同层放不同卡,流水调度

一个 70B 模型:单机 8 卡通常 TP=8;多机则 TP+PP+DP 组合。这些并行策略都由 Python 框架(Megatron-LM / DeepSpeed)编排。

5.4 推理加速:vLLM 的 PagedAttention

训练完要上线。Python 写的原生推理很慢(一次只处理一个请求)。vLLM 用: - PagedAttention:像操作系统分页一样管理 KV Cache 显存 - 连续批处理:动态把新请求拼进当前 batch - CUDA graph:减少 Python ↔ GPU 的调度开销

同样一个模型,vLLM 吞吐量可比原生 PyTorch 高 10~50 倍。


6. 推理服务化:把模型变成 API

6.1 最小服务(FastAPI)

# server.py
from fastapi import FastAPI
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch, uvicorn

app = FastAPI()
tok = AutoTokenizer.from_pretrained("gpt2")
model = AutoModelForCausalLM.from_pretrained("gpt2").cuda().eval()

@app.post("/chat")
def chat(prompt: str, max_tokens: int = 128):
    ids = tok(prompt, return_tensors="pt").input_ids.cuda()
    out = model.generate(ids, max_new_tokens=max_tokens)
    return {"reply": tok.decode(out[0][ids.shape[1]:], skip_special_tokens=True)}

uvicorn.run(app, host="0.0.0.0", port=8000)

6.2 工业级:vLLM OpenAI 兼容服务

# 一行启动,自动加载模型、批处理、显存管理
vllm serve meta-llama/Llama-3.1-8B --host 0.0.0.0 --port 8000

调用:

curl http://localhost:8000/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{"model":"meta-llama/Llama-3.1-8B","messages":[{"role":"user","content":"你好"}]}'

6.3 Python 在服务化中的角色


7. 全景总结:一次完整的"Python 跑 Transformer"流程

[数据] Python 读文本 → tokenizer 编码 → 张量
  ↓
[模型] Python 定义 nn.Module → 加载预训练权重
  ↓
[前向] Python 调 forward → GPU 跑矩阵乘/注意力
  ↓
[损失] Python 算 CrossEntropy → loss 标量
  ↓
[反向] Python 调 loss.backward() → Autograd 算所有梯度
  ↓
[更新] Python 调 optimizer.step() → GPU 更新权重
  ↓
[保存] Python 调 torch.save() → 权重存盘
  ↓
[服务] Python(FastAPI/vLLM) 接收请求 → 生成 → 返回

Python 不直接做计算,但它是整个流程的"神经中枢":定义结构、调度算力、管理状态、对外服务。理解了这张图,就理解了大模型为什么必须用 Python、又是怎么真正跑起来的。


8. Python 现代化语法:写得更少,表达更强(AI 时代的工程底座)

前 7 章讲的是"Python 如何驱动 Transformer"。但要让代码在团队里跑得久、改得动、读得清,靠的是另一层功夫——现代化语法。这一章把 AI 工程里最常用、最能省事、最能少踩坑的现代 Python 特性集中讲透。版本基准:Python 3.10 ~ 3.12。

一句话原则:能用语法糖表达清楚的,绝不写样板代码;能在"类型层"挡住 bug 的,绝不留到运行时。

8.1 类型提示(Type Hints):让 Python 拥有静态语言的可维护性

类型提示不改变运行时行为(CPython 不强制检查),但它带来三个真实收益:

# 老写法:签名是黑盒
def train(model, data, epochs):
    ...

# 现代写法:意图一目了然
from torch import Tensor
from torch.nn import Module
from torch.utils.data import DataLoader

def train(
    model: Module,
    data: DataLoader,
    epochs: int,
    *,
    lr: float = 1e-3,        # ← 关键字参数强制写名,调用更安全
    device: str = "cuda",
) -> dict[str, float]:      # ← 返回类型也标注
    ...
    return {"final_loss": 0.23, "best_acc": 0.91}

几个易错的点:

8.2 dataclass 与 Pydantic:数据建模的两种范式

AI 工程里几乎每一段代码都在处理"结构化数据":模型配置、训练超参、推理请求、评估结果。两种主流方案:

# 方案 A:dataclass —— 轻量、纯 Python、无依赖
from dataclasses import dataclass, field

@dataclass(slots=True)            # 3.10+ slots 省内存,AI 场景里很关键
class GPTConfig:
    vocab_size: int = 50257
    n_layer: int = 12
    n_head: int = 12
    d_model: int = 768
    dropout: float = 0.0
    block_size: int = 1024

cfg = GPTConfig(d_model=1024)
print(cfg)                        # 自动生成 __repr__:GPTConfig(vocab_size=50257, ...)


# 方案 B:Pydantic —— 带"运行时校验 + 序列化",FastAPI 默认引擎
from pydantic import BaseModel, Field, field_validator

class ChatRequest(BaseModel):
    model: str = "gpt-2"
    prompt: str = Field(..., min_length=1, max_length=8192)
    temperature: float = Field(0.8, ge=0.0, le=2.0)   # 范围约束
    max_tokens: int = 256

    @field_validator("prompt")
    @classmethod
    def no_ctrl_chars(cls, v: str) -> str:
        if any(ord(c) < 0x20 and c not in "\n\t" for c in v):
            raise ValueError("控制字符不允许")
        return v

req = ChatRequest(prompt="你好", temperature=3.0)
# → ValidationError: temperature must be ≤ 2.0   ← 在边界处就拦住

选择标准:

8.3 结构化模式匹配(match/case):3.10 的革命性语法

PEP 634 引入的 match/case 不只是 switch,它支持结构解构,是处理嵌套数据的利器。AI 工程里处理消息路由、token 分流、异常分类时极好用。

# 旧写法:if-elif 链
def handle(msg):
    if msg["type"] == "text":
        return process_text(msg["content"])
    elif msg["type"] == "image":
        return process_image(msg["url"], msg.get("caption", ""))
    elif msg["type"] == "tool_call":
        if msg["tool"] == "calculator":
            return run_calc(msg["args"])
        elif msg["tool"] == "search":
            return run_search(msg["args"])
    else:
        return None

# 现代写法:match/case 结构化解构
def handle(msg: dict) -> object:
    match msg:
        case {"type": "text", "content": c}:
            return process_text(c)
        case {"type": "image", "url": u, "caption": caption}:
            return process_image(u, caption)
        case {"type": "tool_call", "tool": "calculator", "args": a}:
            return run_calc(a)
        case {"type": "tool_call", "tool": "search", "args": a}:
            return run_search(a)
        case {"type": "error", "code": code, **rest}:
            log_error(code, rest)
            return None
        case _:
            return None   # ← 通配,等价于 default

# 还能匹配类结构
match response:
    case OpenAIResponse(choices=[Choice(text=t, finish_reason="stop")]):
        return t
    case OpenAIResponse(choices=[Choice(delta=Delta(content=c))]):
        yield c        # 流式 token

几个关键点:

8.4 海象运算符(:=):赋值即表达式

PEP 572 的 :=(walrus operator)把"赋值"变成"表达式",最常见于两种场景:把表达式结果复用、在条件里同时绑定变量。

# 场景 1:避免重复调用昂贵的函数
# 老写法:调两次
if len(prompt_tokens) > 8192:
    print(f"超出 {len(prompt_tokens) - 8192} tokens")

# 海象:只算一次
if (n := len(prompt_tokens)) > 8192:
    print(f"超出 {n - 8192} tokens")


# 场景 2:在 while 条件里捕获值
# 老写法
line = f.readline()
while line:
    process(line)
    line = f.readline()

# 海象:经典的"读一行"
while (line := f.readline()):
    process(line)


# 场景 3:列表推导里复用计算
# 老写法:算两遍
results = [compute_loss(x) for x in batch if compute_loss(x) < 0.5]

# 海象:算一次
results = [loss for x in batch if (loss := compute_loss(x)) < 0.5]


# 场景 4:用 in 表达式同时绑定(替代 dict.get + if None 检查)
if (cap := msg.get("max_tokens")) is not None:
    use(cap)

使用边界:别在简单赋值里用 (x := 5)——那是炫技。它在"需要把中间值带进条件/推导/循环"时才发光。

8.5 async/await 与 asyncio:高并发推理服务的基石

vLLM、FastAPI、TGI 这些推理服务全是 async。原因:GPU 推理是 IO + 计算混合,async 能在一个事件循环里同时挂起几千个请求,等 GPU 时让出 CPU 给别的请求处理。

import asyncio
from fastapi import FastAPI

app = FastAPI()

# 协程:用 async def 定义,await 挂起等待
async def generate_stream(prompt: str) -> str:
    # await 一个耗 IO 的协程(不阻塞整个事件循环)
    tokens = await model.generate_async(prompt)
    return "".join(tokens)

@app.post("/chat")
async def chat(req: ChatRequest):
    # 关键:把阻塞调用丢给线程池,别用 await 包装同步函数
    text = await asyncio.to_thread(sync_inference, req.prompt)
    return {"reply": text}

# 并发跑多个任务:asyncio.gather
async def batch_inference(prompts: list[str]) -> list[str]:
    tasks = [generate_stream(p) for p in prompts]
    return await asyncio.gather(*tasks)      # 并发,不串行

# 限流:Semaphore 控制并发数,保护 GPU 不被打爆
sem = asyncio.Semaphore(8)

async def safe_generate(prompt: str) -> str:
    async with sem:        # ← 同时最多 8 个进入
        return await generate_stream(prompt)

关键避坑:

8.6 f-string 与 pathlib:现代化的字符串与路径

# f-string(3.6+):比 % 和 .format 都快、都直观
name, n = "transformer", 12
print(f"模型 {name} 有 {n} 层,参数量 {n * 768 * 768 / 1e6:.1f}M")
# 模型 transformer 有 12 层,参数量 7.1M

# 3.8+ 调试语法:在表达式后加 = 直接打印
x = compute_loss(batch)
print(f"{x=}")        # → x=0.234

# 多行 f-string + 表达式
template = f"""
[Prompt]
{prompt}

[Config]
temperature={temp:.2f}
max_tokens={max_tok}
"""


# pathlib(取代 os.path):面向对象、跨平台、可链式
from pathlib import Path

# 老写法
import os
ckpt = os.path.join(os.path.dirname(__file__), "checkpoints", "gpt.pt")

# 现代写法:/ 运算符重载,语义清晰
ckpt = Path(__file__).parent / "checkpoints" / "gpt.pt"

# 常用操作一气呵成
for f in Path("data/").rglob("*.jsonl"):
    if f.stat().st_size > 0:
        process(f.read_text(encoding="utf-8"))

# 写文件:一行
Path("output.txt").write_text("done", encoding="utf-8")

# 创建目录(已存在不报错)
Path("runs/exp1").mkdir(parents=True, exist_ok=True)

8.7 推导式、生成器与 Protocol:函数式与鸭子类型的现代写法

# 推导式:列表 / 字典 / 集合,一行表达映射+过滤
tokens = [tok for line in corpus for tok in line.split() if tok.isalpha()]

vocab = {tok: i for i, tok in enumerate(sorted(set(tokens)))}

stop = {"the", "a", "an"}
filtered = {w for w in vocab if w not in stop and len(w) > 2}


# 生成器表达式:惰性求值,省内存,适合大数据流
# 用 () 而非 [],不一次实例化全部
total = sum(len(t) for t in token_stream)   # 不会先建 list


# yield:把函数变成迭代器,流式产出
def stream_tokens(path: Path):
    for line in path.open(encoding="utf-8"):
        for tok in line.split():
            yield tok

# yield from:委托给子生成器
def stream_all(paths: list[Path]):
    for p in paths:
        yield from stream_tokens(p)


# Protocol(PEP 544):结构化子类型,比 ABC 更轻
from typing import Protocol

class Tokenizer(Protocol):
    def encode(self, text: str) -> list[int]: ...
    def decode(self, ids: list[int]) -> str: ...

# 任何有这两个方法的对象都自动满足 Tokenizer,无需继承
class MyTok:
    def encode(self, text): return [ord(c) for c in text]
    def decode(self, ids):  return "".join(chr(i) for i in ids)

def train(tok: Tokenizer, data):   # ← 鸭子类型 + 静态检查
    ...

train(MyTok(), ...)   # ✅ 不报错

8.8 typing 泛型与 PEP 695:3.12 的类型系统飞跃

3.12 引入 PEP 695,泛型写法大幅简化,不再需要 TypeVar + Generic 的样板代码。

# 老写法(3.11 及之前):样板代码多
from typing import TypeVar, Generic, TypeAlias

T = TypeVar("T")

class Stack(Generic[T]):
    def __init__(self) -> None:
        self._items: list[T] = []
    def push(self, x: T) -> None:
        self._items.append(x)
    def pop(self) -> T:
        return self._items.pop()

Vector: TypeAlias = list[float]


# 新写法(3.12+,PEP 695):语法糖,干净
class Stack[T]:
    def __init__(self) -> None:
        self._items: list[T] = []
    def push(self, x: T) -> None:
        self._items.append(x)
    def pop(self) -> T:
        return self._items.pop()

type Vector = list[float]      # ← type 语句,3.12 新

def first(xs: list[T]) -> T:   # 函数级泛型,自动推断
    return xs[0]


# 更实战的例子:一个泛型缓存
class LRUCache[K, V]:
    def __init__(self, cap: int): ...
    def get(self, k: K) -> V | None: ...
    def put(self, k: K, v: V) -> None: ...

cache: LRUCache[str, Tensor] = LRUCache(1024)

对 AI 工程的实际意义:自定义 Dataset / Sampler / Collator 这些泛型容器,3.12 写法可读性接近 TypeScript / Rust,对团队协作和静态检查(pyright)友好得多。

8.9 在 AI 工程里:这些语法如何组合实战

把本章特性拼起来,看一个真实的"流式推理 endpoint"长什么样:

from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from typing import Protocol
import asyncio, json
from fastapi import FastAPI
from pydantic import BaseModel, Field

# 1. 用 Protocol 定义抽象,不绑死实现
class Tokenizer(Protocol):
    def encode(self, text: str) -> list[int]: ...
    def decode(self, ids: list[int]) -> str: ...

# 2. dataclass 做配置,slots 省 GPU 服务器内存
@dataclass(slots=True)
class GenConfig:
    max_tokens: int = 256
    temperature: float = 0.8
    top_p: float = 0.95

# 3. Pydantic 做请求 DTO,边界校验
class ChatReq(BaseModel):
    prompt: str = Field(..., min_length=1, max_length=8192)
    stream: bool = False

app = FastAPI()
sem = asyncio.Semaphore(4)        # 4. 并发限流,保护 GPU

# 5. async + 生成器:流式产出 token
async def stream_tokens(tok: Tokenizer, ids: list[int], cfg: GenConfig):
    async with sem:
        for i in range(cfg.max_tokens):
            nxt = await model.next_token(ids)   # 假设的异步推理
            if nxt == EOS:
                break
            ids.append(nxt)
            yield tok.decode([nxt])             # 流式吐出

# 6. match/case 分流:流式 vs 一次性
@app.post("/chat")
async def chat(req: ChatReq):
    ids = tokenizer.encode(req.prompt)
    cfg = GenConfig()

    match req:
        case ChatReq(stream=True):
            async def gen():
                async for piece in stream_tokens(tokenizer, ids, cfg):
                    yield f"data: {json.dumps({'text': piece})}\n\n"
            return StreamingResponse(gen(), media_type="text/event-stream")

        case _:
            pieces = []
            async for p in stream_tokens(tokenizer, ids, cfg):
                pieces.append(p)
            return {"reply": "".join(pieces)}

# 7. 海象 + pathlib:启动时加载权重
if (ckpt := Path("models/gpt2.pt")).exists():
    model.load(ckpt.read_bytes())

这段 50 行代码里用到了:Protocol、dataclass(slots)、Pydantic、async/await、Semaphore、生成器 yield、match/case、海象运算符、pathlib。这正是现代 AI 服务端代码的真实形态——没有一样是炫技,每一样都在"少写 + 少错 + 好维护"上立竿见影。

本章小结:现代化语法不是"为了新而新",而是为了让代码在百万行级 AI 项目里仍然可读、可改、可静态检查。掌握这 9 个特性,你写出来的 Python 已经是 2026 年工程团队期望的样子,而不是 2015 年的脚本风格。

附录:学习路线与资源

  1. 基础:Python → NumPy → 线性代数(矩阵乘法、特征值)
  2. 框架:PyTorch 官方 60 分钟教程 → 实现一个 MLP/CNN
  3. Transformer:读《Attention Is All You Need》→ 手写自注意力
  4. 实战:用 HuggingFace Transformers 微调一个小模型
  5. 进阶:DeepSpeed / Megatron-LM 并行 → vLLM 推理优化
  6. 部署:FastAPI → Docker → K8s 弹性伸缩

核心心法:先跑通最小闭环,再逐层加复杂度。 不要一上来就啃 175B 模型——能从零训出能生成文字的 MiniGPT,就已经掌握了 Transformer 80% 的本质。