文章列表
5 分钟阅读

Python Web 项目结构怎么设计


系列:Python Web 框架

第 11 / 11 篇

  1. FastAPI 安装与使用指南
  2. FastAPI 进阶使用指南
  3. Django 安装与使用指南
  4. Django 进阶使用指南
  5. Django 与 FastAPI 对比:如何选择合适的 Python Web 框架
  6. Python Web 框架有哪些,怎么选
  7. Flask 安装与使用指南
  8. Django REST Framework 入门:把 Django 做成 API
  9. FastAPI 项目实战:用户登录与 JWT 鉴权
  10. Python Web 项目部署指南:Gunicorn、Uvicorn、Nginx 和 Docker
  11. Python Web 项目结构怎么设计

项目结构这件事很容易走两个极端。

一种是所有代码都塞在 main.pyviews.pyapp.py 里,写到几千行才发现不好改。另一种是一上来就搞 domain/application/infrastructure/,业务还没开始,目录先把人劝退。

我更喜欢按项目阶段拆。小项目就小结构,变复杂了再加层。结构的目的不是显得高级,而是让代码变化时少牵连。

这篇会横跨 Django、FastAPI、Flask。具体框架入门可以先看 Django 安装与使用指南FastAPI 安装与使用指南Flask 安装与使用指南

先别急着拆

一个只有 5 个接口的小项目,用这种结构完全够:

app/
├── main.py
├── config.py
└── tests/
└── test_main.py

问题不在“文件少”,而在“职责混在一起”。如果 main.py 里同时有路由、SQL、第三方 API、密码加密、日志配置,那就该拆。

我通常用三个信号判断是否需要拆:

单个文件超过 300 行,且还在增长
测试某个函数需要启动整个 Web 应用
一个需求改动要同时碰很多无关代码

满足其中两个,就可以动手。

通用分层:HTTP 层不要写业务

无论框架怎么选,一个原则都通用:

路由层处理 HTTP
服务层处理业务
数据层处理存取
配置层处理环境差异

比如 FastAPI 里不要这样写:

@app.post("/orders")
def create_order(data: OrderCreate, session: Session = Depends(get_session)):
user = session.get(User, data.user_id)
if not user:
raise HTTPException(status_code=404)
order = Order(user_id=user.id, amount=data.amount)
session.add(order)
session.commit()
send_message(user.email, "order created")
return order

这段代码能跑,但测试和复用都难。

更好的方向:

@router.post("/orders")
def create_order(data: OrderCreate, session: Session = Depends(get_session)):
return order_service.create_order(session, data)

业务放到 order_service.py

def create_order(session, data):
user = get_user_or_raise(session, data.user_id)
order = save_order(session, user.id, data.amount)
notify_order_created(user.email)
return order

路由层薄一点,后面换 CLI、定时任务、测试入口都更轻松。

FastAPI 推荐结构

中小型 FastAPI 项目可以这样:

app/
├── main.py
├── config.py
├── database.py
├── dependencies.py
├── routers/
│ ├── users.py
│ └── orders.py
├── schemas/
│ ├── users.py
│ └── orders.py
├── services/
│ ├── users.py
│ └── orders.py
└── repositories/
├── users.py
└── orders.py
tests/
├── test_users.py
└── test_orders.py

不是每个目录都必须一开始就有。我的顺序通常是:

先拆 routers
再拆 schemas
业务变复杂后拆 services
数据库查询重复后拆 repositories

main.py 应该只做组装:

from fastapi import FastAPI
from app.routers import orders, users
app = FastAPI()
app.include_router(users.router, prefix="/users", tags=["users"])
app.include_router(orders.router, prefix="/orders", tags=["orders"])

结构再漂亮,也还是要靠测试和运行检查兜底。目录不能替你保证质量。

我故意放这个小错误,是想强调一点:结构再漂亮,还是要靠测试和运行检查兜底。目录不能替你保证质量。

Django 推荐结构

Django 不太适合按 services/repositories/ 从根目录硬拆。它本来就是 app 组织方式。

一个常见结构:

mysite/
├── settings.py
├── urls.py
└── asgi.py
accounts/
├── models.py
├── views.py
├── urls.py
├── forms.py
├── selectors.py
├── services.py
└── tests.py
orders/
├── models.py
├── views.py
├── urls.py
├── selectors.py
├── services.py
└── tests.py

这里两个名字很好用:

selectors.py:只读查询
services.py:会修改状态的业务动作

比如:

orders/selectors.py
def get_user_orders(user):
return Order.objects.filter(user=user).order_by("-created_at")
orders/services.py
def cancel_order(order, user):
if order.user_id != user.id:
raise PermissionError("cannot cancel this order")
order.status = Order.Status.CANCELED
order.save(update_fields=["status"])
return order

Django 的 ORM 已经很强,没必要为了“分层”再套一层无意义 repository。除非你真的要隔离存储实现。

Flask 推荐结构

Flask 小项目可以从 app.py 开始,但一旦有多个模块,建议改成应用工厂:

app/
├── __init__.py
├── config.py
├── extensions.py
├── blueprints/
│ ├── users.py
│ └── orders.py
├── services/
│ └── orders.py
└── templates/
wsgi.py
tests/

app/__init__.py

from flask import Flask
from .blueprints.users import bp as users_bp
from .blueprints.orders import bp as orders_bp
def create_app():
app = Flask(__name__)
app.config.from_object("app.config.Config")
app.register_blueprint(users_bp, url_prefix="/users")
app.register_blueprint(orders_bp, url_prefix="/orders")
return app

wsgi.py

from app import create_app
app = create_app()

Flask 的结构没有 Django 那么强约束,所以更要克制。不要每写一个函数就新建一层目录。

配置怎么拆

配置不要散落在代码里。

比较通用的方式:

from dataclasses import dataclass
import os
@dataclass(frozen=True)
class Settings:
database_url: str = os.environ["DATABASE_URL"]
secret_key: str = os.environ["SECRET_KEY"]
debug: bool = os.environ.get("DEBUG") == "1"
settings = Settings()

如果是 Django,可以拆多个 settings 文件:

mysite/settings/
├── base.py
├── local.py
└── production.py

但也不要过度拆。很多小项目一个 settings.py 加环境变量已经够了。

核心原则:

密钥不进 Git
默认值只给非敏感配置
生产配置显式传入
测试环境能单独覆盖配置

测试目录放哪

两种方式都可以。

放在 app 内:

orders/
└── tests/
├── test_models.py
└── test_services.py

放在根目录:

tests/
├── test_orders.py
└── test_users.py

我更喜欢根目录 tests/,因为很多测试会跨模块。Django 大项目也可以按 app 放,配合 pytest-django 一样好用。

测试重点不要只测路由。服务层函数更值得测:

def test_cancel_order_changes_status(order, user):
result = cancel_order(order, user)
assert result.status == Order.Status.CANCELED

路由测试负责确认 HTTP 状态码和响应结构,业务规则尽量在普通函数测试里覆盖。

常见问题

要不要上 DDD

大多数 Python Web 项目不需要一开始就 DDD。

如果业务非常复杂,领域对象生命周期长,团队也理解 DDD,可以引入一部分概念。但为了一个 CRUD 后台强行建 domain/use_cases/adapters/,通常会让新人更难改代码。

先把 HTTP、业务、数据访问分开,就已经够用了。

service 和 repository 怎么分

简单说:

service:业务动作
repository:数据存取

但 Django 项目里 repository 经常没必要,因为 ORM 查询表达力足够强。FastAPI + SQLAlchemy 项目里,如果查询重复很多,repository 会更有价值。

不要为了“每张表一个 repository”而拆。等重复出现再拆。

配置类要不要用 Pydantic

FastAPI 项目可以用 Pydantic Settings,类型清晰,也适合从环境变量读取。

Django/Flask 项目用普通 Python 配置也没问题。工具不是重点,重点是配置来源清楚、敏感信息不进仓库。

什么时候拆微服务

比你想象得晚。

先把单体内部边界拆清楚。如果一个模块已经有独立数据、独立发布节奏、独立团队维护,再考虑拆服务。否则微服务只会把函数调用变成网络调用,把本地事务变成分布式问题。

总结

项目结构要服务于变化,而不是服务于审美。

我自己的默认路径是:

小项目:少文件,先跑通
路由变多:拆 routers / blueprints / app urls
业务变复杂:拆 services
查询重复:拆 selectors 或 repositories
配置混乱:集中配置和环境变量
测试困难:把业务从 HTTP 层拿出来

Django、FastAPI、Flask 的目录长得不一样,但底层原则一样:HTTP 层薄一点,业务逻辑集中一点,配置和密钥清楚一点,测试能直接打到核心函数。

后续如果要上线,再接着看 Python Web 项目部署指南:Gunicorn、Uvicorn、Nginx 和 Docker。项目结构和部署结构是连在一起的,目录清楚,部署脚本也会简单很多。