「学习笔记」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 发送请求,工程中推荐用 httpx 或 requests。
# 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.gathervstrio.open_nursery区别? - Peewee:
JSONField的db_value和python_value分别在何时调用?如何开启事务? - Flask:Blueprint 的
url_prefix和路由路径如何拼接?如何实现 SSE 流式响应? - Pydantic:
@field_validator和@model_validator的区别?如何实现自定义验证? - pytest:
fixture的yield前后分别代表什么?@parametrize如何驱动多组测试? - 工程化:
logger.info("msg %s", var)vslogger.info(f"msg {var}")性能差异在哪?
相关系列文章
- 「学习笔记」Python 进阶(三):工程实战 — 从脚本到项目
- 「学习笔记」Python 进阶(二):抽象设计 — 构建优雅系统
- 「学习笔记」Python 进阶(一):语言特质 — 写出地道 Python
- 「学习笔记」Python基础入门