D4 学习笔记:装饰器、生成器、async——FastAPI 三大底层技能

系列:海口三港 AI 全栈实战 · 从参赛大屏到 AI 平台
仓库:https://github.com/2003Tim/haikou-ai-port


前言:D4 学什么?

D3 我学了 Python 基础 —— 类型注解、Optional、@dataclass。这些是 FastAPI 的"语法基础"。

但 D3 我看 FastAPI 文档时还是一脸懵,直到 D4 学了这三个东西 —— 装饰器、生成器、async —— 才恍然大悟:

  • @app.get("/") 不再神秘,因为理解了装饰器
  • LLM 流式输出不再抽象,因为理解了生成器
  • FastAPI 为什么快不再神奇,因为理解了 async

今天这篇博客记下这三个概念,以及它们如何对应到 FastAPI 的设计


一、装饰器:函数包装函数

概念

装饰器是接收函数、返回新函数的可调用对象。@decorator 是语法糖:

# 下面这两段代码完全等价

# 写法 1:@ 语法糖
@timer
def slow_function():
    ...

# 写法 2:手动调用装饰器
def slow_function():
    ...
slow_function = timer(slow_function)

我写的练习:计时装饰器

def timer(func):
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"[{func.__name__}] 耗时 {elapsed:.4f}s")
        return result
    return wrapper

@timer
def slow_function():
    time.sleep(0.3)

为什么 FastAPI 必须用装饰器

FastAPI 路由就是装饰器:

@app.get("/ports")           # ← 装饰器!
def get_ports():
    return [...]

@app.get("/ports") 做了三件事:

  1. get_ports 函数注册到 FastAPI 的路由表
  2. 把路径 /ports 和 GET 方法关联到这个函数
  3. 读 type hints 自动生成 Swagger 文档

💡 装饰器的真正威力是横向切面:鉴权(@require_login)、日志(@log_calls)、缓存(@cached)都可以用一个装饰器加上去,不用改业务代码。

坑:装饰器会"偷走"函数名

我的 timer 装饰器打印的是 [wrapper] 而不是 [hello],因为:

def timer(func):
    def wrapper(*args, **kwargs):  # ← 新函数叫 wrapper,不是 hello
        print(f"[{func.__name__}]")  # func 是 wrapper,不是原函数!

修复用 functools.wraps:

from functools import wraps

def timer(func):
    @wraps(func)   # ← 把原函数的 __name__、__doc__ 复制过来
    def wrapper(*args, **kwargs):
        ...

这个细节我第一次写装饰器就踩了,记下来提醒自己。


二、生成器:用 yield 产出多个值

概念

普通函数 return 一次就结束,生成器函数用 yield 可以产出多个值,每次调用 next() 才执行到下一个 yield:

def fibonacci(n):
    a, b = 0, 1
    for _ in range(n):
        yield a      # ← 不是 return,是 yield
        a, b = b, a + b

fib = fibonacci(10)
print(next(fib))   # 0
print(next(fib))   # 1
print(next(fib))   # 1

我写的练习:模拟 LLM 流式输出

def mock_llm_stream(prompt):
    response = f"AI 助手回答关于 {prompt} 的问题:..."
    for word in response.split():
        yield word + " "
        time.sleep(0.05)   # 模拟一个 token 一个 token 生成

跑出来的效果是一个字一个字蹦出来,和真实 LLM 的"打字机效果"一模一样。

为什么 LLM 必须用流式

LLM 不是一次生成一篇文章,是一个 token 一个 token 生成(每个几毫秒)。如果 API 等所有 token 生成完再返回:

  • 用户要等 5-10 秒才看到第一个字
  • 体验极差,以为"卡住了"

流式输出让用户边生成边显示,ChatGPT 的"打字机效果"就是这个。

FastAPI 里用 StreamingResponse 把生成器包装成 HTTP 流式响应:

from fastapi.responses import StreamingResponse

@app.post("/chat")
def chat(prompt: str):
    def generate():
        for token in llm.stream(prompt):  # 调真实 LLM API
            yield token
    return StreamingResponse(generate(), media_type="text/plain")

💡 v3.0 阶段做 RAG 智能问答时,前端会看到"AI 助手正在输入..."的效果 —— 就是这个 yield 的功劳。


三、async/await:为什么 FastAPI 这么快

震撼的对比

我的练习里:

方式 10 个 I/O 请求 耗时
顺序 await(同步思维) 一个一个等 1.007s
asyncio.gather(并发) 一起等 0.103s
加速比 9.8x

10 倍加速!这还是纯本地 asyncio.sleep,真实网络请求差距更大。

为什么

Web 服务器大部分时间都在等 I/O —— 数据库查询、调用其他 API、读文件。

  • 同步代码:等 I/O 时线程闲着(浪费资源)
  • 异步代码:等 I/O 时让出控制权,服务器去处理其他请求

FastAPI 用 async 是因为:一个进程能处理上千并发,同步 Flask 几百就卡。

FastAPI 里的 async

@app.get("/ports")
async def get_ports():
    # await 数据库(让出控制权,服务器去处理其他请求)
    ports = await db.fetch_all("SELECT * FROM ports")
    return ports

async def 告诉 FastAPI:这个函数会 await,不要阻塞线程

关键认知

  • FastAPI 同时支持 def(同步)和 async def(异步)路由
  • 异步路由里不能调用阻塞的同步代码,否则就白异步了
  • 异步代码里只能 await 其他异步函数

⚠️ 实际项目中有个经典坑:在 async 函数里调 requests.get()(同步),会阻塞整个事件循环。这种情况要用 httpx.AsyncClient 替代。


我踩过的坑

  1. 装饰器链的返回值忘了 print
    我写了 @timer @repeat(times=2) def hello(),但调用时只写了 hello("海口三港") 没接收返回值。结果 list 被丢了,只看到 timer 打印的那一行。修复:加 result = hello(...) + print(result)

  2. timer 显示 [wrapper] 而不是 [hello]
    装饰器"偷走"了原函数的 __name__。修复用 @functools.wraps(func) 保留元数据。

  3. async 函数里调用同步代码会阻塞事件循环
    这条我现在还没踩,但提前记下:在 async def 里不要用 requests/time.sleep/同步数据库驱动,要用对应的 async 版本(httpx、asyncpg、aiomysql 等)。


完整代码

👉 learn_d4.py

git clone https://github.com/2003Tim/haikou-ai-port.git
cd haikou-ai-port/backend
uv venv
.\.venv\Scripts\Activate.ps1
python learn_d4.py

下一步:D5 学什么?

D5 我会装 Postman(或 Apifox) + 学 HTTP 基础 —— GET/POST、状态码、JSON、请求头。这些是后端工程师和 AI 工程师的"通用语言",面试必问。

D6-D7 就要迎来 第一个 FastAPI Hello World —— 所有前面学的语法都会在这里汇合。


参考资料

posted @ 2026-08-21 18:07  吴逸清  阅读(1)  评论(0)    收藏  举报