「学习笔记」Python 进阶(三):工程实战 — 从脚本到项目

文章目录

此篇为 Python 进阶的第三篇——工程实战篇,聚焦如何用 Python 构建生产级 Web 应用。涵盖异步并发、ORM、Web 框架、数据验证、测试框架等工程化技能。

前置:语言特质篇 → [抽象设计篇]语言特质篇

第一章:异步编程 async/await — 并发的新范式

Python 的 async/await 基于协程,类似 Go 的 goroutine 但需显式调度。适合 IO 密集型任务,不适合 CPU 密集型。

  • async:用于定义一个异步函数(协程)。调用该函数时,它不会立即执行,而是返回一个协程对象(Coroutine Object)。
  • await:只能在 async 函数内部使用。它用于暂停当前协程,并等待另一个异步操作(如协程、Task、Future)完成后再继续执行。

实际场景:Web 爬虫并发抓取、LLM API 并发调用、文件批量上传——所有 IO 密集型任务都可以从多线程切换到协程,单机轻松承载上千并发。

1.1 协程基础

import asyncio

async def fetch_data(url):
    print(f"开始请求: {url}")
    await asyncio.sleep(2)  # 模拟耗时 I/O,期间控制权交还给事件循环
    print(f"请求完成: {url}")
    return f"Data from {url}"

async def main():
    # 必须使用 await 获取结果,否则只会得到协程对象
    result = await fetch_data("https://api.example.com")
    print(result)

# Python 3.7+ 启动事件循环的标准方式
asyncio.run(main())

1.2 并发执行:asyncio.gather

asyncio.gather()批量并发与结果聚合。它会并发执行传入的协程,并按输入顺序返回结果。

import asyncio, time

async def task(name, delay):
    await asyncio.sleep(delay)
    return f"{name} 完成"

async def main():
    start = time.time()
    results = await asyncio.gather(
        task("任务A", 3),
        task("任务B", 1),
        task("任务C", 2)
    )
    print(results)  # 输出: ['任务A 完成', '任务B 完成', '任务C 完成']
    print(f"并发执行,总耗时: {time.time() - start:.1f}s")  # ~3s(取最大值(最慢的任务)),不是 6s

asyncio.run(main())

1.3 并发控制:Semaphore

import asyncio

# 限制同时最多 3 个并发请求
semaphore = asyncio.Semaphore(3)

async def rate_limited_request(url):
    async with semaphore:              # 获取信号量(最多 3 个并发)
        return await fetch_data(url)

async def main():
    urls = [f"url_{i}" for i in range(20)]
    results = await asyncio.gather(*[
        rate_limited_request(u) for u in urls
    ])

asyncio.run(main())

1.4 TaskGroup(Python 3.11+,更安全)

async def main():
    async with asyncio.TaskGroup() as tg:
        t1 = tg.create_task(task("A", 2))
        t2 = tg.create_task(task("B", 1))
        t3 = tg.create_task(task("C", 3))
    # 离开 async with 时自动等待所有任务
    print(t1.result(), t2.result(), t3.result())

1.5 在同步代码中调用异步函数

# 推荐:asyncio.run()(Python 3.7+)
result = asyncio.run(async_fetch("http://example.com"))

# 备选:手动管理事件循环
loop = asyncio.new_event_loop()
try:
    result = loop.run_until_complete(async_fetch("http://example.com"))
finally:
    loop.close()

1.6 trio 库入门(替代 asyncio 的另一种范式)

trio 是一个设计更严格的异步库。相比标准库 asyncio,它的核心优势是 Nursery 模型——所有子任务在 with 块结束时自动等待,不会出现"忘了 await"导致的悬挂任务:

import trio

async def main():
    # Nursery 模型:所有子任务在 with 块结束时自动等待
    async with trio.open_nursery() as nursery:
        nursery.start_soon(child_task, "A")
        nursery.start_soon(child_task, "B")
        nursery.start_soon(child_task, "C")
    # 离开 with 块时,自动等待所有子任务完成——不会遗漏

# trio 核心工具:
# trio.Semaphore(n)           — 并发数限制
# trio.CapacityLimiter(n)     — 容量限制
# trio.fail_after(timeout)    — 超时控制
# trio.to_thread.run_sync     — 在线程池中运行同步函数

async def with_timeout():
    with trio.fail_after(10):        # 10 秒超时
        result = await trio.to_thread.run_sync(cpu_intensive_task)

trio vs asyncio 选择

  • asyncio:Python 标准库,生态成熟(aiohttp、FastAPI),大多数项目用这个
  • trio:设计更严格(Nursery 模型避免悬挂任务),适合对并发正确性要求极高的场景

第二章:Peewee ORM — 轻量级数据库操作

Peewee 是一个轻量级 Python ORM,类似 Java 的 MyBatis 或 Go 的 GORM。适合中小型项目或对 ORM 框架侵入性有要求的场景。

实际场景:所有需要持久化存储的 Web 应用——用户管理、订单系统、配置存储——都通过 ORM 完成数据库操作。

2.1 模型定义

from peewee import (
    Model, MySQLDatabase, CharField, TextField,
    IntegerField, BigIntegerField, DateTimeField,
    AutoField, fn
)

# 数据库连接
db = MySQLDatabase("ragflow", host="localhost", user="root", password="123456", port=3306)

# 基类模型
class BaseModel(Model):
    create_time = BigIntegerField(null=True, index=True)
    create_date = DateTimeField(null=True, index=True)

    class Meta:
        database = db

# 具体模型
class User(BaseModel):
    id = AutoField()
    name = CharField(max_length=100)
    email = CharField(max_length=255, unique=True)
    status = CharField(max_length=10, default="active")

class Knowledgebase(BaseModel):
    id = AutoField()
    name = CharField(max_length=100)
    description = TextField(null=True)
    tenant_id = IntegerField()     # 手动管理外键

    class Meta:
        table_name = "knowledgebase"

2.2 CRUD 操作

# Create
user = User.create(name="Tom", email="tom@test.com")
# 或:user = User(name="Tom", email="tom@test.com"); user.save()

# Read — 单条
user = User.get(User.email == "tom@test.com")

# Read — 列表 + 条件 + 排序 + 分页
users = (User.select()
    .where((User.status == "active") & (User.name.contains("Tom")))
    .order_by(User.create_time.desc())
    .limit(10))

# 聚合
count = User.select().where(User.status == "active").count()
max_id = User.select(fn.MAX(User.id)).scalar()

# Update
(User.update(status="inactive")
     .where(User.last_login < "2024-01-01")
     .execute())

# Delete
User.delete().where(User.status == "inactive").execute()

2.3 自定义 JSONField(ORM 扩展实战)

import json
from peewee import TextField

class JSONField(TextField):
    """Python 对象 ↔ JSON 字符串自动转换"""
    default_value = {}

    def db_value(self, value):
        """存储时:Python dict → JSON 字符串"""
        if value is None:
            value = self.default_value
        return json.dumps(value, ensure_ascii=False)

    def python_value(self, value):
        """读取时:JSON 字符串 → Python dict"""
        if not value:
            return self.default_value
        return json.loads(value)

# 使用:像操作 dict 一样操作数据库 JSON 字段
class Task(BaseModel):
    id = AutoField()
    config = JSONField()
    tags = JSONField(default_value=[])

task = Task.create(config={"model": "gpt-4"}, tags=["urgent"])
print(task.config)  # {"model": "gpt-4"} — 自动反序列化

2.4 事务与连接池

# 上下文管理器事务(推荐)
with db.atomic():
    user = User.create(name="Tom", email="tom@test.com")
    kb = Knowledgebase.create(name="KB1", tenant_id=user.id)
    # 任何一句失败,全部回滚

# 装饰器事务
@db.atomic()
def create_user_with_kb(name, email, kb_name):
    user = User.create(name=name, email=email)
    kb = Knowledgebase.create(name=kb_name, tenant_id=user.id)
    return user, kb

# 连接池(生产环境推荐)
from playhouse.pool import PooledMySQLDatabase

db = PooledMySQLDatabase(
    "ragflow",
    max_connections=20,
    stale_timeout=300,
    host="localhost", user="root", password="123456",
)

第三章:Flask Web 框架 — RESTful API 构建

Flask 是 Python 最流行的轻量级 Web 框架,类似 Spring Boot 但更简洁。

Flask是一个基于Werkzeug和Jinja2的微型Web框架:

  • Werkzeug:WSGI工具库,处理HTTP请求和响应
  • Jinja2:模板引擎,用于渲染HTML页面

3.1 Hello World

from flask import Flask, request, jsonify

app = Flask(__name__) # 创建Flask应用实例,__name__表示当前模块名,Flask用它来确定资源位置

@app.route("/api/hello", methods=["GET"]) # 路由装饰器,将URL映射到函数
def hello():                              # 视图函数,处理请求并返回响应
    name = request.args.get("name", "World")
    return jsonify({"message": f"Hello, {name}!"})

@app.route("/api/user", methods=["POST"])
def create_user():
    data = request.json                 # JSON body
    # data = request.form.to_dict()     # 表单数据
    # file = request.files.get("file")  # 上传文件
    return jsonify({"id": 1, "name": data["name"]}), 201
    
@app.route('/post/<int:post_id>') # 动态路由(string(默认)/int/float/path)
def show_post(post_id):
    return f'文章ID: {post_id}'

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=9380, debug=True) # 启动开发服务器

3.2 Jinja2 — 模板与静态文件

3.2.1 使用Jinja2模板

创建templates目录,新建index.html

<!DOCTYPE html>
<html>
<head>
    <title>{{ title }}</title>
</head>
<body>
    <h1>欢迎来到 {{ title }}</h1>
    {% if user %}
        <p>用户: {{ user }}</p>
    {% else %}
        <p>请登录</p>
    {% endif %}
</body>
</html>

在视图函数中渲染模板:

from flask import render_template

@app.route('/')
def index():
    return render_template('index.html', title='首页', user='admin')

3.2.2 模板继承

创建基础模板base.html

<!DOCTYPE html>
<html>
<head>
    <title>{% block title %}{% endblock %}</title>
</head>
<body>
    <div class="content">
        {% block content %}{% endblock %}
    </div>
</body>
</html>

继承基础模板:

{% extends "base.html" %}

{% block title %}
首页
{% endblock %}

{% block content %}
    <h1>欢迎来到首页</h1>
{% endblock %}

3.2.3 静态文件

创建static目录存放CSS、JS、图片等静态资源:

<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<script src="{{ url_for('static', filename='script.js') }}"></script>
<img src="{{ url_for('static', filename='logo.png') }}" alt="Logo">

3.3 Blueprint — 模块化路由

from flask import Blueprint

# 创建蓝图(类似 Spring 的 @RestController 分包)
manager = Blueprint("conversation", __name__)

@manager.route("/set", methods=["POST"])
def set_conversation():
    return jsonify({"success": True})

@manager.route("/get/<int:conv_id>", methods=["GET"])
def get_conversation(conv_id):
    return jsonify({"id": conv_id})

# 注册蓝图到 app
app = Flask(__name__)
app.register_blueprint(manager, url_prefix="/api/v1/conversation")
# 访问: POST /api/v1/conversation/set
#      GET  /api/v1/conversation/get/123

3.4 请求钩子与鉴权中间件

from flask import g, request
from functools import wraps

# 请求前钩子
@app.before_request
def before_request():
    g.start_time = time.time()             # g 是请求级上下文

# 请求后钩子
@app.after_request
def after_request(response):
    elapsed = time.time() - g.start_time
    response.headers["X-Response-Time"] = f"{elapsed:.3f}s"
    return response

# 自定义鉴权装饰器
def login_required(func):
    @wraps(func)
    def decorated_function(*args, **kwargs):
        token = request.headers.get("Authorization")
        if not token:
            return jsonify({"error": "Unauthorized"}), 401
        kwargs["tenant_id"] = get_tenant_from_token(token)
        return func(*args, **kwargs)
    return decorated_function

3.5 SSE 流式响应

from flask import Response

@manager.route("/chat", methods=["POST"])
@login_required
def chat(tenant_id):
    """SSE 流式聊天"""
    def generate():
        for chunk in llm_stream(query):
            yield f"data: {json.dumps(chunk)}\n\n"
    return Response(generate(), mimetype="text/event-stream")

# 客户端(JavaScript): new EventSource("/api/chat")
# 客户端(Python): requests.get(url, stream=True).iter_lines()

3.6 实战:动态蓝图注册

这是 Flask 项目中最值得学习的架构模式——通过文件扫描 + 动态导入实现零配置的路由注册:

from pathlib import Path
from importlib.util import module_from_spec, spec_from_file_location
from flask import Blueprint

def register_page(app, page_path):
    """动态注册一个路由模块 — Flask 项目的核心架构模式"""
    page_name = page_path.stem.removesuffix("_app")
    module_name = f"api.apps.{page_name}"

    # 动态导入模块
    spec = spec_from_file_location(module_name, page_path)
    page = module_from_spec(spec)
    page.app = app
    page.manager = Blueprint(page_name, module_name)
    sys.modules[module_name] = page
    spec.loader.exec_module(page)         # 执行模块,注册路由

    app.register_blueprint(page.manager, url_prefix=f"/api/v1/{page_name}")
    return f"/api/v1/{page_name}"

# 批量注册所有 *_app.py
pages_dir = Path(__file__).parent / "apps"
for path in pages_dir.glob("*_app.py"):
    if not path.name.startswith("."):
        register_page(app, path)

设计精华:新增 API 模块只需在 apps/ 下新建 xxx_app.py,自动注册路由——零配置。

第四章:Pydantic — 数据验证与序列化

Pydantic 基于类型注解自动验证数据,类似 Java 的 Bean Validation。通过 Python 的类型注解(Type Hints)来定义数据模型,从而确保数据在输入和输出时的一致性与有效性。

实际场景:API 请求验证、配置文件解析、数据序列化——任何"外部数据进入系统"的关口都适合用 Pydantic 做一层类型守卫,避免脏数据流入业务逻辑。

4.1 基本模型

使用 Pydantic 只需继承 BaseModel 类并定义带有类型注解的字段:

from pydantic import BaseModel, ValidationError

class User(BaseModel):
    id: int
    name: str
    age: int

# 1. 自动类型转换(宽松模式)
user = User(id="1", name="Alice", age="30") 
print(user.id, type(user.id))  # 输出: 1 <class 'int'>

# 2. 捕获验证错误
try:
    User(id="invalid", name="Bob", age="thirty")
except ValidationError as e:
    print(e)

4.2 Field — 字段约束

通过 Field 函数,可以为字段设置默认值、长度限制或数值范围:

from pydantic import BaseModel, Field

class Product(BaseModel):
    name: str = Field(..., min_length=2, max_length=50)
    price: float = Field(..., gt=0, le=9999.99)
    stock: int = Field(default=0, ge=0)

4.3 自定义验证器

当内置类型检查无法满足业务需求时,可以使用装饰器自定义校验逻辑。

from pydantic import BaseModel, field_validator

class Account(BaseModel):
    username: str = Field(..., min_length=2, max_length=50)

    @field_validator('username')
    @classmethod
    def check_username_length(cls, v: str) -> str:
        not_allowed = {"python", "javascript", "go", "java"}
        if v in not_allowed:
            raise ValueError(f"Must be not in {not_allowed}")
        return v

第五章:pytest — 专业测试框架

pytest 是 Python 最流行的测试框架,比 unittest 更简洁。

5.1 基本测试

# test_example.py
def test_add():
    assert add(1, 2) == 3
    assert add(-1, 1) == 0

def test_divide_by_zero():
    import pytest
    with pytest.raises(ZeroDivisionError):
        1 / 0

5.2 fixture — 测试前置/后置

import pytest
from peewee import SqliteDatabase

@pytest.fixture
def db():
    """创建内存数据库,测试结束后自动销毁"""
    database = SqliteDatabase(":memory:")
    database.connect()
    database.create_tables([User, Knowledgebase])
    yield database       # yield 之前 = setup
    database.close()     # yield 之后 = teardown

def test_create_user(db):
    user = User.create(name="Tom", email="tom@test.com")
    assert user.id is not None
    assert User.select().count() == 1

5.3 参数化测试

import pytest

@pytest.mark.parametrize("input,expected", [
    ("hello", "HELLO"),
    ("World", "WORLD"),
    ("", ""),
])
def test_upper(input, expected):
    assert input.upper() == expected

@pytest.mark.parametrize("a,b,expected", [
    (1, 2, 3), (-1, 1, 0), (100, 200, 300),
])
def test_add(a, b, expected):
    assert add(a, b) == expected

5.4 常用运行命令

pytest                       # 运行所有测试
pytest -v                    # 详细输出
pytest -s                    # 显示 print 输出
pytest test_file.py::test_fn # 运行指定测试
pytest --cov=rag             # 生成覆盖率报告

第六章:多线程与并发

基础篇 已覆盖 threading.Thread 的创建、启动、join 等待。这里补充进阶内容。

6.1 concurrent.futures — 线程池/进程池

from concurrent.futures import ThreadPoolExecutor, as_completed

def download(url):
    return f"Downloaded {url}"

urls = [f"url_{i}" for i in range(10)]

# 方式 1:批量提交
with ThreadPoolExecutor(max_workers=5) as executor:
    futures = {executor.submit(download, url): url for url in urls}
    for future in as_completed(futures):
        result = future.result()

# 方式 2:map(保持顺序)
with ThreadPoolExecutor(max_workers=5) as executor:
    results = list(executor.map(download, urls))

6.2 线程安全

import threading

# Lock — 互斥锁
lock = threading.Lock()
with lock:
    shared_resource.update()

# RLock — 可重入锁(同一线程可多次获取)
rlock = threading.RLock()

# Queue — 线程安全队列
from queue import Queue
q = Queue()
q.put(item)
item = q.get()

6.3 线程 vs 协程选择

场景 推荐 原因
IO 密集型(网络请求、文件读写) asyncio 协程切换开销极小,单线程即可高并发
CPU 密集型(计算、图像处理) ThreadPoolExecutor / ProcessPoolExecutor 需要利用多核
既有同步库 ThreadPoolExecutor 不需要改写为 async
需要精确并发控制 trio Nursery 模型避免悬挂任务

第七章:网络通信进阶

基础篇用 urllib.request 发送请求,工程中推荐用 httpxrequests

# httpx(推荐,支持 async)
import httpx

# 同步
response = httpx.get("http://localhost:8080/api/data")
data = response.json()

# 异步
async with httpx.AsyncClient() as client:
    responses = await asyncio.gather(*[
        client.get(f"http://localhost:8080/api/item/{i}")
        for i in range(10)
    ])

# requests(老牌)
import requests
resp = requests.get("http://localhost:8080/api/data")
print(resp.json())
print(resp.status_code)

第八章:工程化工具链

8.1 虚拟环境与包管理

  • venv(Python 3.3+ 内置):
python -m venv .venv
source .venv/bin/activate   # Linux/Mac
.venv\Scripts\activate      # Windows PowerShell
pip install flask peewee
  • uv(现代替代方案,更快):
uv venv                              # 创建虚拟环境
uv pip install flask peewee          # 安装包(比 pip 快 10-100 倍)
uv pip install -e ".[full]"          # 安装项目 + 可选依赖

8.2 pyproject.toml — 项目配置

[project]
name = "ragflow"
version = "0.16.0"
requires-python = ">=3.10"
dependencies = [
    "flask>=3.0.0",
    "peewee>=3.17.0",
    "elasticsearch>=8.0.0",
]

[project.optional-dependencies]
full = [
    "torch>=2.0.0",
    "transformers>=4.40.0",
    "paddleocr>=2.7.0",
]

[tool.ruff]                  # 代码检查(替代 flake8)
line-length = 120

[tool.pytest.ini_options]    # pytest 配置
testpaths = ["test"]

8.3 logging — 日志系统

import logging

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
)

logger = logging.getLogger(__name__)

# 使用(5 个级别)
logger.debug("调试信息")       # 通常不显示
logger.info("普通信息")        # 记录关键流程
logger.warning("警告")         # 可能的问题
logger.error("错误")           # 需要关注的错误
logger.critical("严重错误")

# 最佳实践:用 % 格式化延迟求值(不要用 f-string!)
logger.info("Processing doc %s, chunks: %d", doc_name, len(chunks))  # ✅
# logger.info(f"Processing doc {doc_name}")  # ❌ 即使不输出也会先格式化

# 记录异常 + 完整堆栈
try:
    1 / 0
except Exception:
    logger.exception("计算失败")  # 自动包含 traceback

本篇自检清单

学完本篇后确认以下问题能答上来:

  • 异步编程:为什么协程适合 IO 密集不适合 CPU 密集?asyncio.gather vs trio.open_nursery 区别?
  • PeeweeJSONFielddb_valuepython_value 分别在何时调用?如何开启事务?
  • Flask:Blueprint 的 url_prefix 和路由路径如何拼接?如何实现 SSE 流式响应?
  • Pydantic@field_validator@model_validator 的区别?如何实现自定义验证?
  • pytestfixtureyield 前后分别代表什么?@parametrize 如何驱动多组测试?
  • 工程化logger.info("msg %s", var) vs logger.info(f"msg {var}") 性能差异在哪?

回到:语言特质篇 | 抽象设计篇

END .

相关系列文章

×