2. Python后端项目结构规范化

很多Python后端项目最初都从一个 app.py 或 main.py 开始:路由、数据库连接、配置、业务逻辑全部堆在一个文件里。当代码量超过几百行、需要多人协作、需要写单元测试、需要区分开发与生产环境时,这种“脚本式”结构就会迅速失控。本节的目标,是把一个单文件脚本重构为可测试、可部署、配置分层的标准工程结构,并理解每一步背后的工程动机。

一、脚本式结构为什么会失效

假设有一个最简的 Flask 脚本:

# app.py(反例)
from flask import Flask
import pymysql

app = Flask(__name__)
DB = pymysql.connect(host="localhost", user="root", password="123456", db="demo")

@app.route("/users")
def users():
    cur = DB.cursor()
    cur.execute("SELECT id, name FROM users")
    return {"data": cur.fetchall()}

if __name__ == "__main__":
    app.run(debug=True)

这段代码能跑,但存在四个硬伤:配置硬编码(数据库密码写死在源码里,无法区分环境);副作用发生在导入时(import app 就会尝试连数据库,测试时无法隔离);没有依赖声明(换一台机器不知道要装 flask 和 pymysql 的哪个版本);无法测试(路由函数与数据库强耦合,没有可替换的边界)。

二、规范化后的目录结构

一个可测试、可部署的 Python 后端工程,通常采用如下布局(以 Flask 为例,FastAPI/Django 思路一致):

backend-demo/
├── pyproject.toml          # 项目元数据 + 依赖声明
├── requirements.txt        # 锁定版本(可选,用于部署)
├── .env.example            # 环境变量样例(不含真实密钥)
├── .gitignore
├── src/
│   └── backend_demo/
│       ├── __init__.py     # 应用工厂 create_app()
│       ├── config.py       # 配置分层
│       ├── api/
│       │   ├── __init__.py
│       │   └── users.py    # 路由蓝图
│       ├── services/
│       │   └── user_service.py   # 业务逻辑
│       ├── repositories/
│       │   └── user_repo.py      # 数据访问
│       └── models/
│           └── user.py
└── tests/
    ├── conftest.py
    └── test_users.py

这个结构的关键不是“目录多”,而是每一层都有明确职责和可替换边界:api 只负责解析请求和返回响应,services 承载业务规则,repositories 封装数据库访问,config 集中管理配置。测试时可以只替换 repository,而不必启动真实数据库。

从脚本到工程:分层结构与依赖方向 脚本式(反例) 路由 + 业务 + SQL 全部混在 app.py 配置硬编码 password="123456" 导入即连库 无法隔离测试 无依赖声明 环境不可复现 测试无从下手 无替换边界 重构 规范化工程(分层 + 依赖倒置) config.py — 配置分层(Base / Dev / Prod / Test) 从环境变量读取,不写死密钥 api/users.py — 路由蓝图(只做请求/响应) 调用 service,不直接写 SQL services/user_service.py — 业务逻辑 依赖 repository 接口,可注入 mock repositories/user_repo.py — 数据访问 封装 SQL / ORM,可替换为内存实现 tests/ — 单元 + 集成测试 conftest 注入测试配置 pyproject.toml 依赖声明 + 版本约束 调用 调用

三、配置分层:让同一份代码跑在不同环境

配置分层的核心思想是:代码不变,环境变量变。常见做法是定义一个基类配置,再按环境派生:

# src/backend_demo/config.py
import os

class BaseConfig:
    SECRET_KEY = os.environ.get("SECRET_KEY", "dev-secret")
    DB_HOST = os.environ.get("DB_HOST", "localhost")
    DB_PORT = int(os.environ.get("DB_PORT", 3306))
    DB_NAME = os.environ.get("DB_NAME", "demo")
    DB_USER = os.environ.get("DB_USER", "root")
    DB_PASSWORD = os.environ.get("DB_PASSWORD", "")

class DevConfig(BaseConfig):
    DEBUG = True

class TestConfig(BaseConfig):
    TESTING = True
    DB_NAME = "demo_test"          # 测试库独立
    DB_PASSWORD = "test"

class ProdConfig(BaseConfig):
    DEBUG = False

CONFIG_MAP = {"dev": DevConfig, "test": TestConfig, "prod": ProdConfig}

def get_config(env: str = None):
    env = env or os.environ.get("APP_ENV", "dev")
    return CONFIG_MAP[env]

这样,本地开发用 APP_ENV=dev,CI 跑测试用 APP_ENV=test,生产部署用 APP_ENV=prod,同一份代码通过环境变量切换行为。真实密钥通过 .env 文件或部署平台的 Secret 注入,.env.example 只保留字段名和占位值,提交到仓库供他人参考。

四、应用工厂:消除导入副作用

把 app = Flask(__name__) 从模块顶层移入工厂函数,是让项目可测试的关键一步:

# src/backend_demo/__init__.py
from flask import Flask
from .config import get_config
from .api.users import users_bp

def create_app(config=None):
    app = Flask(__name__)
    app.config.from_object(config or get_config())
    app.register_blueprint(users_bp, url_prefix="/api")
    return app

此时 import backend_demo 不会连接数据库,也不会启动服务。测试代码可以传入 TestConfig 创建独立实例:

# tests/conftest.py
import pytest
from backend_demo import create_app
from backend_demo.config import TestConfig

@pytest.fixture
def client():
    app = create_app(TestConfig)
    with app.test_client() as c:
        yield c

五、依赖声明与环境隔离

依赖声明解决“换台机器跑不起来”的问题。推荐在 pyproject.toml 中声明直接依赖及其版本范围:

[project]
name = "backend-demo"
version = "0.1.0"
requires-python = ">=3.10"
dependencies = [
    "flask>=3.0,<4.0",
    "pymysql>=1.1,<2.0",
]

[project.optional-dependencies]
dev = ["pytest>=8.0", "pytest-cov>=5.0", "ruff>=0.5"]

环境隔离则通过虚拟环境实现。标准流程是:

python -m venv .venv
source .venv/bin/activate        # Windows: .venv\Scripts\activate
pip install -e ".[dev]"          # 安装项目本身 + 开发依赖
pip freeze > requirements.txt    # 锁定精确版本,供部署使用

注意区分两类文件:pyproject.toml 声明“需要什么”,requirements.txt 锁定“实际装了什么版本”。前者用于开发,后者用于部署复现。

判断结构是否合格的一个简单标准:能否在不启动数据库、不设置真实密钥的情况下,用 pytest 跑通至少一个针对路由的测试?如果能,说明配置分层、应用工厂和依赖边界都已到位。

六、从结构到可部署

规范化结构最终服务于部署。部署时通常只需要三样东西:代码(src/)、依赖锁定(requirements.txt)、环境变量(APP_ENV、DB_*、SECRET_KEY)。启动命令也从 python app.py 变为通过 WSGI 服务器加载工厂:

gunicorn "backend_demo:create_app()" -b 0.0.0.0:8000 -w 4