Python 工程化详解:项目结构、依赖管理、代码质量、测试与部署

文章目录

    • 一、前言
    • [二、什么是 Python 工程化](#二、什么是 Python 工程化)
    • 三、从脚本到工程
      • [3.1 单文件脚本](#3.1 单文件脚本)
      • [3.2 多模块项目](#3.2 多模块项目)
      • [3.3 不要过度设计](#3.3 不要过度设计)
    • 四、推荐项目目录
      • [4.1 `src` 目录](#4.1 src 目录)
      • [4.2 `tests` 目录](#4.2 tests 目录)
      • [4.3 不要滥用 `utils.py`](#4.3 不要滥用 utils.py)
    • 五、模块分层
      • [5.1 API 层](#5.1 API 层)
      • [5.2 Application 层](#5.2 Application 层)
      • [5.3 Domain 层](#5.3 Domain 层)
      • [5.4 Infrastructure 层](#5.4 Infrastructure 层)
    • [六、使用 `pyproject.toml` 统一配置](#六、使用 pyproject.toml 统一配置)
      • [6.1 完整配置示例](#6.1 完整配置示例)
      • [6.2 `requires-python`](#6.2 requires-python)
      • [6.3 命令行入口](#6.3 命令行入口)
    • 七、虚拟环境
      • [7.1 使用标准库 `venv`](#7.1 使用标准库 venv)
      • [7.2 不提交虚拟环境](#7.2 不提交虚拟环境)
    • [八、使用 uv 管理依赖](#八、使用 uv 管理依赖)
      • [8.1 初始化项目](#8.1 初始化项目)
      • [8.2 添加运行依赖](#8.2 添加运行依赖)
      • [8.3 添加开发依赖](#8.3 添加开发依赖)
      • [8.4 同步环境](#8.4 同步环境)
      • [8.5 执行命令](#8.5 执行命令)
      • [8.6 锁文件](#8.6 锁文件)
      • [8.7 不要随意执行全量升级](#8.7 不要随意执行全量升级)
    • 九、配置管理
      • [9.1 配置与代码分离](#9.1 配置与代码分离)
      • [9.2 Pydantic Settings 示例](#9.2 Pydantic Settings 示例)
      • [9.3 `.env.example`](#9.3 .env.example)
      • [9.4 启动时验证配置](#9.4 启动时验证配置)
    • 十、领域模型
      • [10.1 金额使用 `Decimal`](#10.1 金额使用 Decimal)
      • [10.2 业务规则放入模型](#10.2 业务规则放入模型)
    • [十一、依赖反转与 Repository](#十一、依赖反转与 Repository)
    • 十二、异常设计
      • [12.1 不要吞掉异常](#12.1 不要吞掉异常)
      • [12.2 保留异常链](#12.2 保留异常链)
    • 十三、日志工程化
      • [13.1 日志配置](#13.1 日志配置)
      • [13.2 日志等级](#13.2 日志等级)
      • [13.3 日志字段](#13.3 日志字段)
      • [13.4 使用结构化日志](#13.4 使用结构化日志)
    • 十四、代码格式化与静态检查
      • [14.1 Ruff](#14.1 Ruff)
      • [14.2 不要一开始启用全部规则](#14.2 不要一开始启用全部规则)
    • 十五、类型检查
      • [15.1 类型注解的价值](#15.1 类型注解的价值)
      • [15.2 使用 mypy](#15.2 使用 mypy)
      • [15.3 类型检查不是运行时校验](#15.3 类型检查不是运行时校验)
      • [15.4 逐步引入类型](#15.4 逐步引入类型)
    • 十六、自动化测试
      • [16.1 测试金字塔](#16.1 测试金字塔)
      • [16.2 单元测试](#16.2 单元测试)
      • [16.3 Fixture](#16.3 Fixture)
      • [16.4 集成测试](#16.4 集成测试)
      • [16.5 测试覆盖率](#16.5 测试覆盖率)
    • 十七、外部服务调用
      • [17.1 必须设置超时](#17.1 必须设置超时)
      • [17.2 重试不是越多越好](#17.2 重试不是越多越好)
      • [17.3 幂等](#17.3 幂等)
    • 十八、同步与异步
      • [18.1 适合异步](#18.1 适合异步)
      • [18.2 不适合直接异步](#18.2 不适合直接异步)
      • [18.3 不要为了异步而异步](#18.3 不要为了异步而异步)
    • 十九、数据库工程化
      • [19.1 事务边界](#19.1 事务边界)
      • [19.2 数据库迁移](#19.2 数据库迁移)
      • [19.3 避免 N+1 查询](#19.3 避免 N+1 查询)
      • [19.4 不要拼接 SQL](#19.4 不要拼接 SQL)
    • 二十、应用启动入口
    • 二十一、资源生命周期
    • [二十二、构建 Python 包](#二十二、构建 Python 包)
    • [二十三、Docker 部署](#二十三、Docker 部署)
    • [二十四、CI 持续集成](#二十四、CI 持续集成)
      • [24.1 GitHub Actions 示例](#24.1 GitHub Actions 示例)
      • [24.2 CI 不应自动修改代码](#24.2 CI 不应自动修改代码)
    • 二十五、可观测性
      • [25.1 日志指标](#25.1 日志指标)
      • [25.2 服务指标](#25.2 服务指标)
      • [25.3 业务指标](#25.3 业务指标)
      • [25.4 健康检查](#25.4 健康检查)
    • 二十六、安全工程化
      • [26.1 Secret 管理](#26.1 Secret 管理)
      • [26.2 依赖安全](#26.2 依赖安全)
      • [26.3 输入校验](#26.3 输入校验)
      • [26.4 最小权限](#26.4 最小权限)
    • 二十七、版本与发布管理
    • 二十八、常见工程化误区
      • [28.1 所有代码都放在 `main.py`](#28.1 所有代码都放在 main.py)
      • [28.2 使用相对路径假设当前目录](#28.2 使用相对路径假设当前目录)
      • [28.3 修改 `sys.path`](#28.3 修改 sys.path)
      • [28.4 直接提交 `.env`](#28.4 直接提交 .env)
      • [28.5 捕获所有异常后继续](#28.5 捕获所有异常后继续)
      • [28.6 过度 Mock](#28.6 过度 Mock)
      • [28.7 只追求覆盖率](#28.7 只追求覆盖率)
      • [28.8 本地和生产使用不同命令](#28.8 本地和生产使用不同命令)
      • [28.9 为了分层而分层](#28.9 为了分层而分层)
    • 二十九、落地实施顺序
    • 三十、上线检查清单
    • 三十一、总结

一、前言

很多 Python 项目最初都从一个脚本开始:

text 复制代码
main.py

随着业务不断增加,项目逐渐变成:

text 复制代码
main.py
utils.py
utils_new.py
utils_final.py
config.py
test.py

此时通常会出现以下问题:

  • 代码全部堆在少数文件中;
  • 模块之间循环依赖;
  • 开发环境和生产环境依赖版本不同;
  • 配置、密码和业务代码混在一起;
  • 没有统一的格式化和代码检查;
  • 修改代码后不知道是否影响其他功能;
  • 测试依赖真实数据库和外部接口;
  • 日志只有 print(),无法定位线上问题;
  • 本地可以运行,部署后却无法启动;
  • 不同开发人员使用不同命令和目录结构。

所谓 Python 工程化,就是通过项目结构、开发规范、自动化工具和交付流程,让 Python 项目具备:

  • 可维护性;
  • 可测试性;
  • 可复现性;
  • 可观测性;
  • 可部署性;
  • 可扩展性;
  • 团队协作能力。

本文将以一个订单服务为例,介绍现代 Python 项目的目录结构、pyproject.toml、依赖锁定、配置管理、日志、异常、类型检查、自动化测试、Docker 和 CI/CD。


二、什么是 Python 工程化

Python 工程化并不等于"安装很多工具"。

它真正要解决的是:

text 复制代码
代码如何组织
依赖如何复现
配置如何隔离
质量如何检查
功能如何测试
问题如何定位
应用如何交付
变更如何自动验证

一个完整的工程流程可以表示为:

text 复制代码
需求设计
   │
   ▼
模块划分
   │
   ▼
编码与类型检查
   │
   ▼
格式化与静态检查
   │
   ▼
单元测试与集成测试
   │
   ▼
构建制品
   │
   ▼
自动部署
   │
   ▼
日志、指标与告警

工程化的目标不是让项目结构越复杂越好,而是让复杂度有明确的存放位置。


三、从脚本到工程

3.1 单文件脚本

对于一次性任务,单文件完全合理:

text 复制代码
cleanup.py

适合:

  • 临时数据处理;
  • 本地自动化;
  • 一次性迁移;
  • 简单运维脚本。

3.2 多模块项目

当项目开始出现以下情况时,应考虑工程化:

  • 多个业务模块;
  • 多个开发人员;
  • 需要长期维护;
  • 需要连接数据库;
  • 需要对外提供 API;
  • 需要自动化测试;
  • 需要部署到生产环境;
  • 需要发布为 Python 包。

3.3 不要过度设计

一个只有几百行代码的小工具,不一定需要复杂的领域驱动架构。

项目结构应与业务复杂度匹配:

项目类型 推荐结构
一次性脚本 单文件或少量模块
CLI 工具 Package + Commands
Web API API、Service、Repository
数据处理 Pipeline、Task、Adapter
复杂业务系统 Domain、Application、Infrastructure
公共库 清晰 API、测试、构建和版本管理

四、推荐项目目录

下面是一套适合中小型服务的目录结构:

text 复制代码
order-service/
├── pyproject.toml
├── uv.lock
├── README.md
├── .python-version
├── .env.example
├── .gitignore
├── Dockerfile
│
├── src/
│   └── order_service/
│       ├── __init__.py
│       ├── __main__.py
│       ├── config.py
│       ├── logging_config.py
│       ├── exceptions.py
│       │
│       ├── domain/
│       │   ├── __init__.py
│       │   └── order.py
│       │
│       ├── application/
│       │   ├── __init__.py
│       │   └── order_service.py
│       │
│       ├── infrastructure/
│       │   ├── __init__.py
│       │   └── order_repository.py
│       │
│       └── api/
│           ├── __init__.py
│           └── main.py
│
├── tests/
│   ├── unit/
│   │   └── test_order_service.py
│   ├── integration/
│   │   └── test_order_repository.py
│   └── conftest.py
│
├── migrations/
├── scripts/
└── .github/
    └── workflows/
        └── ci.yml

4.1 src 目录

业务代码放在:

text 复制代码
src/order_service/

而不是直接放在项目根目录。

Python Packaging User Guide 指出,src 布局能够降低开发时意外导入项目根目录源码的风险,使测试更接近真实安装后的运行方式。src Layout vs Flat Layout

4.2 tests 目录

测试代码与生产代码分离:

text 复制代码
tests/unit
tests/integration

这样可以明确区分:

  • 单元测试;
  • 数据库测试;
  • 外部接口测试;
  • 端到端测试。

pytest 的官方实践也推荐新项目结合 src 布局,并让测试针对已经安装的项目执行。pytest Good Integration Practices

4.3 不要滥用 utils.py

很多项目最后会出现一个几千行的:

text 复制代码
utils.py

它通常混合了:

  • 日期处理;
  • HTTP 请求;
  • 数据库函数;
  • 加密函数;
  • 业务计算;
  • 文件处理。

更好的方式是按照能力拆分:

text 复制代码
time_utils.py
http_client.py
money.py
security.py

如果函数只服务于订单业务,就应放在订单模块,而不是放入全局工具类。


五、模块分层

可以将项目划分为四个主要层次。

text 复制代码
API 层
接收请求和返回响应
        │
        ▼
Application 层
编排业务流程
        │
        ▼
Domain 层
表达核心业务规则
        │
        ▼
Infrastructure 层
数据库、缓存、消息和外部系统

5.1 API 层

负责:

  • 参数解析;
  • 身份认证;
  • 输入校验;
  • 调用应用服务;
  • 转换响应;
  • 映射错误码。

API 层不应包含大量业务规则。

5.2 Application 层

负责:

  • 编排业务步骤;
  • 控制事务边界;
  • 调用领域对象;
  • 调用 Repository;
  • 发送领域事件。

5.3 Domain 层

负责:

  • 核心业务模型;
  • 状态转换;
  • 业务约束;
  • 金额和时间规则;
  • 与框架无关的业务逻辑。

5.4 Infrastructure 层

负责:

  • 数据库实现;
  • Redis;
  • 消息队列;
  • HTTP 客户端;
  • 对象存储;
  • 第三方 SDK。

依赖方向应尽量保持:

text 复制代码
外层依赖内层
内层不依赖外层框架

六、使用 pyproject.toml 统一配置

pyproject.toml 是现代 Python 项目的核心配置文件,可以用于:

  • 项目元数据;
  • Python 版本;
  • 运行依赖;
  • 开发依赖;
  • 构建系统;
  • 命令行入口;
  • pytest 配置;
  • Ruff 配置;
  • mypy 配置;
  • Coverage 配置。

Python Packaging User Guide 将其划分为:

  • [build-system];
  • [project];
  • 各种 [tool.*] 工具配置。

详细规范可以参考 Writing pyproject.toml。

6.1 完整配置示例

toml 复制代码
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "order-service"
version = "0.1.0"
description = "A production-ready Python order service"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
    "pydantic-settings>=2,<3",
]

[project.scripts]
order-service = "order_service.__main__:main"

[dependency-groups]
dev = [
    "mypy",
    "pytest",
    "pytest-cov",
    "ruff",
]

[tool.hatch.build.targets.wheel]
packages = ["src/order_service"]

[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = [
    "--import-mode=importlib",
    "--strict-config",
    "--strict-markers",
    "-ra",
]
markers = [
    "integration: tests requiring infrastructure",
]

[tool.coverage.run]
branch = true
source = ["order_service"]

[tool.coverage.report]
show_missing = true
skip_covered = true
fail_under = 80

[tool.ruff]
line-length = 100
target-version = "py312"
src = ["src", "tests"]

[tool.ruff.lint]
select = [
    "E",
    "F",
    "I",
    "B",
    "UP",
    "SIM",
    "RUF",
]
ignore = []

[tool.ruff.format]
quote-style = "double"
indent-style = "space"

[tool.mypy]
python_version = "3.12"
strict = true
packages = ["order_service"]
warn_unused_configs = true

6.2 requires-python

toml 复制代码
requires-python = ">=3.12"

它表示项目支持的 Python 版本范围。

不要仅仅在 README 中写版本要求,因为工具无法自动验证普通文档中的说明。

6.3 命令行入口

toml 复制代码
[project.scripts]
order-service = "order_service.__main__:main"

安装项目后,可以直接运行:

bash 复制代码
order-service

而不需要:

bash 复制代码
python src/order_service/main.py

七、虚拟环境

不同项目可能依赖不同版本的第三方库。

如果全部安装到系统 Python:

text 复制代码
项目 A 需要 package==1.x
项目 B 需要 package==2.x

就可能产生依赖冲突。

虚拟环境可以为项目提供隔离的 Python 环境。

7.1 使用标准库 venv

创建环境:

bash 复制代码
python -m venv .venv

激活环境:

Linux 或 macOS:

bash 复制代码
source .venv/bin/activate

Windows:

powershell 复制代码
.venv\Scripts\activate

安装项目:

bash 复制代码
python -m pip install -e .

安装开发依赖的具体方式取决于项目采用的依赖管理方案。

7.2 不提交虚拟环境

.gitignore 中应包含:

gitignore 复制代码
.venv/
__pycache__/
*.py[cod]
.pytest_cache/
.mypy_cache/
.ruff_cache/
.coverage
htmlcov/
dist/
build/
.env

虚拟环境应该在每台开发机和 CI 环境中根据依赖配置重新创建。


八、使用 uv 管理依赖

Python 项目可以使用:

  • pip;
  • uv;
  • Poetry;
  • PDM;
  • Hatch;
  • Conda。

本文以 uv 为例,但工程原则并不依赖某一个工具。

8.1 初始化项目

bash 复制代码
uv init --package order-service
cd order-service

8.2 添加运行依赖

bash 复制代码
uv add pydantic-settings

8.3 添加开发依赖

bash 复制代码
uv add --dev pytest pytest-cov mypy ruff

8.4 同步环境

bash 复制代码
uv sync

8.5 执行命令

bash 复制代码
uv run pytest
uv run ruff check .
uv run mypy src

8.6 锁文件

uv 使用:

text 复制代码
uv.lock

保存解析后的精确依赖版本。

text 复制代码
pyproject.toml
描述允许使用哪些版本
        │
        ▼
依赖解析
        │
        ▼
uv.lock
记录实际安装版本

uv.lock 应提交到版本控制,使本地、CI 和部署环境尽量使用一致版本。uv 官方文档将锁定定义为把依赖解析为 Lockfile,将同步定义为从 Lockfile 安装依赖。uv Locking and Syncing

生产和 CI 中可以执行:

bash 复制代码
uv sync --locked

如果 pyproject.toml 与锁文件不一致,命令会失败,而不是静默修改依赖。

8.7 不要随意执行全量升级

不建议在上线构建中自动执行:

bash 复制代码
uv lock --upgrade

更合理的方式是:

text 复制代码
创建依赖升级分支
       │
       ▼
更新指定依赖
       │
       ▼
执行测试和安全扫描
       │
       ▼
人工审核变更
       │
       ▼
合并并发布

九、配置管理

配置通常包括:

  • 数据库地址;
  • Redis 地址;
  • 服务端口;
  • 日志级别;
  • 超时时间;
  • 功能开关;
  • 第三方 API 地址;
  • 密钥引用。

9.1 配置与代码分离

不要这样写:

python 复制代码
DATABASE_URL = "postgresql://admin:password@prod-db/orders"

更合理的方式是通过:

  • 环境变量;
  • 配置中心;
  • Secret Manager;
  • Kubernetes Secret;
  • 密钥管理服务;

注入运行环境。

9.2 Pydantic Settings 示例

python 复制代码
# src/order_service/config.py

from functools import lru_cache
from typing import Literal

from pydantic import SecretStr
from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    model_config = SettingsConfigDict(
        env_file=".env",
        env_prefix="APP_",
        extra="ignore",
    )

    environment: Literal[
        "local",
        "test",
        "staging",
        "production",
    ] = "local"

    database_url: SecretStr
    log_level: str = "INFO"
    request_timeout_seconds: float = 5.0


@lru_cache
def get_settings() -> Settings:
    return Settings()

环境变量:

bash 复制代码
export APP_DATABASE_URL="postgresql://..."
export APP_ENVIRONMENT="production"
export APP_LOG_LEVEL="INFO"

使用配置:

python 复制代码
settings = get_settings()

database_url = settings.database_url.get_secret_value()

9.3 .env.example

仓库中可以提交:

dotenv 复制代码
APP_ENVIRONMENT=local
APP_DATABASE_URL=postgresql://user:password@localhost:5432/orders
APP_LOG_LEVEL=DEBUG
APP_REQUEST_TIMEOUT_SECONDS=5

但不要提交真实的:

text 复制代码
.env

9.4 启动时验证配置

配置缺失时,应该在应用启动阶段立即失败:

text 复制代码
启动失败
缺少 APP_DATABASE_URL

而不是等到收到第一笔请求后才报错。


十、领域模型

领域模型用于表达业务,而不是数据库表的简单复制。

python 复制代码
# src/order_service/domain/order.py

from dataclasses import dataclass
from decimal import Decimal
from enum import StrEnum


class OrderStatus(StrEnum):
    CREATED = "CREATED"
    PAID = "PAID"
    CANCELLED = "CANCELLED"


@dataclass(slots=True)
class Order:
    id: str
    user_id: str
    amount: Decimal
    status: OrderStatus = OrderStatus.CREATED

    def mark_paid(self) -> None:
        if self.status != OrderStatus.CREATED:
            raise ValueError(
                f"Order {self.id} cannot be paid "
                f"from status {self.status}"
            )

        self.status = OrderStatus.PAID

    def cancel(self) -> None:
        if self.status != OrderStatus.CREATED:
            raise ValueError(
                f"Order {self.id} cannot be cancelled "
                f"from status {self.status}"
            )

        self.status = OrderStatus.CANCELLED

10.1 金额使用 Decimal

错误方式:

python 复制代码
amount = 0.1 + 0.2

金融金额应使用:

python 复制代码
from decimal import Decimal

amount = Decimal("0.10") + Decimal("0.20")

避免直接使用二进制浮点数进行金额计算。

10.2 业务规则放入模型

错误方式是在 Controller 中判断:

python 复制代码
if order.status == "CREATED":
    order.status = "PAID"

更好的方式:

python 复制代码
order.mark_paid()

这样状态转换规则不会散落在多个接口和任务中。


十一、依赖反转与 Repository

业务服务不应该直接依赖某一个数据库驱动。

可以通过 Protocol 定义接口:

python 复制代码
# src/order_service/application/order_service.py

from typing import Protocol

from order_service.domain.order import Order


class OrderRepository(Protocol):
    def get_by_id(self, order_id: str) -> Order | None:
        ...

    def save(self, order: Order) -> None:
        ...


class OrderNotFoundError(Exception):
    pass


class OrderApplicationService:
    def __init__(
        self,
        repository: OrderRepository,
    ) -> None:
        self._repository = repository

    def pay(self, order_id: str) -> Order:
        order = self._repository.get_by_id(order_id)

        if order is None:
            raise OrderNotFoundError(order_id)

        order.mark_paid()
        self._repository.save(order)

        return order

生产环境可以提供:

text 复制代码
PostgreSQLOrderRepository

单元测试可以提供:

text 复制代码
InMemoryOrderRepository

应用服务不需要知道底层使用 PostgreSQL、MySQL 还是内存。


十二、异常设计

不要在所有地方直接抛出:

python 复制代码
Exception("error")

可以建立异常层次:

python 复制代码
# src/order_service/exceptions.py


class ApplicationError(Exception):
    """Application-level base exception."""


class ValidationError(ApplicationError):
    pass


class ResourceNotFoundError(ApplicationError):
    pass


class ConflictError(ApplicationError):
    pass


class ExternalServiceError(ApplicationError):
    pass

API 层负责映射:

业务异常 HTTP 状态
ValidationError 400
ResourceNotFoundError 404
ConflictError 409
未知异常 500

12.1 不要吞掉异常

错误方式:

python 复制代码
try:
    process()
except Exception:
    pass

这会让系统表面上运行正常,但业务数据可能已经没有被处理。

更合理的方式:

python 复制代码
try:
    process()
except ExpectedBusinessError:
    logger.warning(
        "business operation rejected",
        exc_info=True,
    )
    raise
except Exception:
    logger.exception(
        "unexpected processing failure"
    )
    raise

12.2 保留异常链

python 复制代码
try:
    response = client.send()
except TimeoutError as error:
    raise ExternalServiceError(
        "payment service timeout"
    ) from error

from error 能够保留原始异常原因,方便排查问题。


十三、日志工程化

线上服务不应该依赖:

python 复制代码
print("error")

应使用标准日志系统:

python 复制代码
import logging

logger = logging.getLogger(__name__)

13.1 日志配置

python 复制代码
# src/order_service/logging_config.py

from logging.config import dictConfig


def configure_logging(level: str = "INFO") -> None:
    dictConfig(
        {
            "version": 1,
            "disable_existing_loggers": False,
            "formatters": {
                "default": {
                    "format": (
                        "%(asctime)s "
                        "%(levelname)s "
                        "%(name)s "
                        "%(message)s"
                    ),
                },
            },
            "handlers": {
                "console": {
                    "class": "logging.StreamHandler",
                    "formatter": "default",
                    "level": level,
                },
            },
            "root": {
                "handlers": ["console"],
                "level": level,
            },
        }
    )

Python 标准库支持通过 dictConfig() 使用字典集中配置 Logger、Handler 和 Formatter。Python Logging HOWTO

13.2 日志等级

等级 使用场景
DEBUG 调试信息
INFO 正常业务节点
WARNING 可恢复的异常情况
ERROR 当前操作失败
CRITICAL 系统无法继续运行

13.3 日志字段

生产日志建议包含:

text 复制代码
timestamp
level
service
environment
request_id
trace_id
user_id
operation
duration_ms
error_type

不要记录:

  • 密码;
  • Token;
  • 私钥;
  • 完整银行卡号;
  • 完整身份证件;
  • 数据库连接密码;
  • 第三方 API Secret。

13.4 使用结构化日志

在容器环境中,推荐把日志写到标准输出,并采用 JSON 等结构化格式。

json 复制代码
{
  "level": "INFO",
  "message": "order paid",
  "order_id": "ORDER-10001",
  "request_id": "req-123",
  "duration_ms": 42
}

这样更便于日志平台进行检索、聚合和告警。


十四、代码格式化与静态检查

代码规范不应依赖人工记忆,应交给自动化工具。

14.1 Ruff

Ruff 可以用于:

  • 代码格式化;
  • 未使用导入检查;
  • 导入顺序检查;
  • 常见错误检查;
  • 代码现代化;
  • 部分复杂度和安全规则检查。

配置:

toml 复制代码
[tool.ruff]
line-length = 100
target-version = "py312"

[tool.ruff.lint]
select = [
    "E",
    "F",
    "I",
    "B",
    "UP",
    "SIM",
    "RUF",
]

检查:

bash 复制代码
uv run ruff check .

自动修复安全范围内的问题:

bash 复制代码
uv run ruff check . --fix

格式化:

bash 复制代码
uv run ruff format .

验证格式但不修改:

bash 复制代码
uv run ruff format . --check

Ruff 可以通过 pyproject.toml、ruff.toml 或 .ruff.toml 配置。Ruff Configuration

14.2 不要一开始启用全部规则

直接启用所有规则可能产生:

  • 大量历史问题;
  • 互相冲突的风格要求;
  • 团队无法一次完成整改;
  • 开发人员绕过检查。

更合理的方式:

text 复制代码
从基础规则开始
      │
      ▼
清理现有问题
      │
      ▼
CI 阻止新增问题
      │
      ▼
逐步增加更严格规则

十五、类型检查

Python 是动态语言,但可以使用类型注解提高可维护性。

python 复制代码
def calculate_total(
    prices: list[Decimal],
) -> Decimal:
    return sum(prices, start=Decimal("0"))

15.1 类型注解的价值

类型注解可以帮助发现:

  • 参数类型错误;
  • 返回值错误;
  • None 未处理;
  • 不兼容接口实现;
  • 重构遗漏;
  • 容器元素类型错误。

15.2 使用 mypy

执行:

bash 复制代码
uv run mypy src

严格配置:

toml 复制代码
[tool.mypy]
python_version = "3.12"
strict = true
packages = ["order_service"]

mypy 支持在 pyproject.toml 的 [tool.mypy] 中进行配置。mypy Documentation

15.3 类型检查不是运行时校验

下面的注解:

python 复制代码
def create_order(amount: Decimal) -> None:
    ...

不会自动阻止运行时传入字符串。

类型检查主要发生在开发和 CI 阶段。外部输入仍需要使用:

  • Pydantic;
  • Dataclass 转换;
  • 显式校验;
  • Web 框架参数模型;

进行运行时验证。

15.4 逐步引入类型

遗留项目不需要第一天就开启全局严格模式。

可以按照模块逐步执行:

text 复制代码
新模块严格检查
核心业务优先改造
基础设施逐步补齐
最终提高全局标准

十六、自动化测试

测试不是为了追求百分之百覆盖率,而是为了降低变更风险。

16.1 测试金字塔

text 复制代码
          端到端测试
        数量少、成本高
              ▲
              │
          集成测试
      数据库、缓存、外部接口
              ▲
              │
          单元测试
      数量多、执行快、定位准

16.2 单元测试

使用内存 Repository 测试业务逻辑:

python 复制代码
# tests/unit/test_order_service.py

from decimal import Decimal

from order_service.application.order_service import (
    OrderApplicationService,
)
from order_service.domain.order import (
    Order,
    OrderStatus,
)


class InMemoryOrderRepository:
    def __init__(self) -> None:
        self.orders: dict[str, Order] = {}

    def get_by_id(
        self,
        order_id: str,
    ) -> Order | None:
        return self.orders.get(order_id)

    def save(self, order: Order) -> None:
        self.orders[order.id] = order


def test_pay_order() -> None:
    repository = InMemoryOrderRepository()

    repository.save(
        Order(
            id="ORDER-10001",
            user_id="USER-1",
            amount=Decimal("99.00"),
        )
    )

    service = OrderApplicationService(repository)

    result = service.pay("ORDER-10001")

    assert result.status == OrderStatus.PAID
    assert (
        repository.orders["ORDER-10001"].status
        == OrderStatus.PAID
    )

执行:

bash 复制代码
uv run pytest

16.3 Fixture

公共测试对象可以放入:

python 复制代码
# tests/conftest.py

from decimal import Decimal

import pytest

from order_service.domain.order import Order


@pytest.fixture
def created_order() -> Order:
    return Order(
        id="ORDER-10001",
        user_id="USER-1",
        amount=Decimal("99.00"),
    )

测试中使用:

python 复制代码
def test_order_default_status(
    created_order: Order,
) -> None:
    assert created_order.status.value == "CREATED"

16.4 集成测试

集成测试应验证真实边界,例如:

  • SQL 是否正确;
  • ORM 映射是否正确;
  • 数据库事务是否生效;
  • Redis 序列化是否正确;
  • HTTP 请求和响应是否匹配;
  • 消息队列是否能够正确消费。

不要把所有外部依赖都 Mock 掉后,就把测试称为集成测试。

16.5 测试覆盖率

执行:

bash 复制代码
uv run pytest \
  --cov=order_service \
  --cov-report=term-missing

覆盖率可以帮助发现没有执行过的代码,但高覆盖率并不等于高质量测试。

需要重点覆盖:

  • 核心业务规则;
  • 边界条件;
  • 异常分支;
  • 状态转换;
  • 金额计算;
  • 权限控制;
  • 幂等逻辑;
  • 事务失败。

十七、外部服务调用

调用第三方服务时,需要统一处理:

  • 超时;
  • 重试;
  • 限流;
  • 熔断;
  • 状态码;
  • 幂等;
  • 日志;
  • 指标。

17.1 必须设置超时

错误方式:

python 复制代码
response = http_client.get(url)

正确方式:

python 复制代码
response = http_client.get(
    url,
    timeout=5.0,
)

没有超时的请求可能长时间占用:

  • Worker;
  • 线程;
  • 数据库连接;
  • HTTP 连接池;
  • 整条业务链路。

17.2 重试不是越多越好

适合重试:

  • 临时网络错误;
  • 连接重置;
  • 部分限流响应;
  • 明确可恢复的服务异常。

不适合直接重试:

  • 参数错误;
  • 权限错误;
  • 业务校验失败;
  • 非幂等写操作;
  • 已经确定的永久失败。

推荐使用:

text 复制代码
有限次数
+
指数退避
+
随机抖动
+
最大等待时间

17.3 幂等

如果创建订单请求因为超时而重试,服务端必须判断:

text 复制代码
第一次请求是否已经成功

可以使用:

text 复制代码
Idempotency-Key
业务唯一号
数据库唯一约束
请求状态表

十八、同步与异步

Python 提供 asyncio,但异步并不适合所有任务。

18.1 适合异步

  • HTTP 请求;
  • 网络 I/O;
  • 异步数据库驱动;
  • WebSocket;
  • 高并发 I/O 服务。

18.2 不适合直接异步

  • CPU 密集计算;
  • 图像处理;
  • 大规模压缩;
  • 机器学习推理;
  • 使用阻塞式 SDK 的代码。

如果在异步函数中调用阻塞函数:

python 复制代码
async def handle() -> None:
    blocking_client.request()

可能阻塞整个事件循环。

18.3 不要为了异步而异步

异步会增加:

  • 调试复杂度;
  • 调用链复杂度;
  • 资源生命周期管理难度;
  • 测试复杂度;
  • 错误传播复杂度。

应根据实际并发模型选择同步、线程、进程或异步。


十九、数据库工程化

数据库访问需要统一处理:

  • 连接池;
  • 事务;
  • 超时;
  • Migration;
  • Repository;
  • 慢查询;
  • 幂等;
  • 索引。

19.1 事务边界

事务通常由 Application 层控制:

text 复制代码
开始事务
   │
   ├── 查询订单
   ├── 更新状态
   ├── 写入流水
   └── 保存 Outbox 事件
   │
   ▼
提交事务

不要在一个业务流程中随意多次提交,导致部分成功、部分失败。

19.2 数据库迁移

数据库结构变更应通过 Migration 管理:

text 复制代码
001_create_order.sql
002_add_order_status.sql
003_create_payment_record.sql

不要在生产环境中手工修改表结构后不留下记录。

Migration 需要:

  • 进入版本控制;
  • 在测试环境验证;
  • 支持重复执行检查;
  • 考虑大表锁表;
  • 准备回滚或前向修复方案。

19.3 避免 N+1 查询

错误流程:

text 复制代码
查询 100 个订单
      │
      ▼
循环查询 100 次用户

应考虑:

  • Join;
  • 批量查询;
  • 预加载;
  • DataLoader;
  • 缓存。

19.4 不要拼接 SQL

错误方式:

python 复制代码
sql = (
    "SELECT * FROM users "
    f"WHERE name = '{user_input}'"
)

应使用参数化查询:

python 复制代码
cursor.execute(
    "SELECT * FROM users WHERE name = %s",
    (user_input,),
)

二十、应用启动入口

__main__.py 可以作为统一启动入口:

python 复制代码
# src/order_service/__main__.py

import logging

from order_service.config import get_settings
from order_service.logging_config import (
    configure_logging,
)


def main() -> int:
    settings = get_settings()

    configure_logging(settings.log_level)

    logger = logging.getLogger(__name__)

    logger.info(
        "service starting",
        extra={
            "environment": settings.environment,
        },
    )

    # 初始化数据库、HTTP 客户端或 Web 服务
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

运行方式:

bash 复制代码
uv run order-service

也可以:

bash 复制代码
uv run python -m order_service

统一入口可以集中完成:

  • 配置加载;
  • 日志初始化;
  • 依赖创建;
  • 资源注册;
  • 优雅关闭。

二十一、资源生命周期

数据库连接池、HTTP Client 和消息客户端都需要正确关闭。

可以使用上下文管理器:

python 复制代码
from contextlib import contextmanager
from collections.abc import Iterator


@contextmanager
def managed_resource() -> Iterator[Resource]:
    resource = Resource()

    try:
        yield resource
    finally:
        resource.close()

使用:

python 复制代码
with managed_resource() as resource:
    resource.process()

对于异步资源:

python 复制代码
from contextlib import asynccontextmanager
from collections.abc import AsyncIterator


@asynccontextmanager
async def managed_client() -> AsyncIterator[Client]:
    client = Client()

    try:
        yield client
    finally:
        await client.close()

不要只考虑正常流程,还要考虑:

  • 初始化失败;
  • 运行时异常;
  • 容器终止;
  • 优雅停机;
  • 部分资源创建成功。

二十二、构建 Python 包

构建项目:

bash 复制代码
uv build

通常会生成:

text 复制代码
dist/
├── order_service-0.1.0-py3-none-any.whl
└── order_service-0.1.0.tar.gz

其中:

  • Wheel 用于快速安装;
  • Source Distribution 包含源代码发行内容。

安装构建结果:

bash 复制代码
python -m pip install \
  dist/order_service-0.1.0-py3-none-any.whl

建议在 CI 中额外验证:

  • Wheel 能否安装;
  • 安装后能否导入;
  • 命令行入口能否运行;
  • 构建制品是否包含必要文件;
  • 构建制品是否意外包含密码和测试数据。

二十三、Docker 部署

下面使用 uv 官方基础镜像构建应用:

dockerfile 复制代码
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim

WORKDIR /app

ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV UV_COMPILE_BYTECODE=1
ENV UV_LINK_MODE=copy

COPY pyproject.toml uv.lock README.md ./

RUN uv sync \
    --locked \
    --no-dev \
    --no-install-project

COPY src ./src

RUN uv sync \
    --locked \
    --no-dev \
    --no-editable

ENV PATH="/app/.venv/bin:${PATH}"

RUN useradd \
    --create-home \
    --uid 10001 \
    appuser

USER appuser

CMD ["order-service"]

生产环境还应注意:

  • 固定基础镜像版本或摘要;
  • 使用非 Root 用户;
  • 不把 .env 复制进镜像;
  • 不在镜像中保存密钥;
  • 减少不必要的软件包;
  • 设置健康检查;
  • 限制 CPU 和内存;
  • 扫描镜像漏洞;
  • 记录镜像和代码版本。

二十四、CI 持续集成

每次提交代码后,CI 应自动执行:

text 复制代码
安装锁定依赖
      │
      ▼
格式检查
      │
      ▼
静态检查
      │
      ▼
类型检查
      │
      ▼
自动化测试
      │
      ▼
构建制品

24.1 GitHub Actions 示例

yaml 复制代码
name: CI

on:
  push:
  pull_request:

jobs:
  quality:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v7

      - name: Install uv
        uses: astral-sh/setup-uv@v9
        with:
          enable-cache: true

      - name: Install Python
        run: uv python install

      - name: Sync dependencies
        run: uv sync --locked --dev

      - name: Lint
        run: uv run ruff check .

      - name: Check formatting
        run: uv run ruff format . --check

      - name: Type check
        run: uv run mypy src

      - name: Run tests
        run: >
          uv run pytest
          --cov=order_service
          --cov-report=term-missing

      - name: Build
        run: uv build

实际项目应将第三方 Action 固定到经过审核的 Commit SHA,降低供应链风险。uv 官方也推荐使用 astral-sh/setup-uv,并在 CI 中通过锁文件同步项目依赖。Using uv in GitHub Actions

24.2 CI 不应自动修改代码

CI 中应执行:

bash 复制代码
ruff format . --check
ruff check .

而不是自动格式化后直接通过。

代码修改应由开发环境或单独的自动修复流程完成。


二十五、可观测性

成熟的 Python 服务需要同时具备:

text 复制代码
Logs
发生了什么

Metrics
发生了多少次、速度如何

Traces
一条请求经过了哪些服务

25.1 日志指标

建议监控:

  • ERROR 数量;
  • 异常类型;
  • 关键业务失败;
  • 第三方服务错误;
  • 慢请求;
  • 重试次数。

25.2 服务指标

建议监控:

  • 请求量;
  • 成功率;
  • P95 和 P99 延迟;
  • CPU;
  • 内存;
  • 线程或任务数量;
  • 数据库连接池;
  • 消息积压。

25.3 业务指标

例如订单服务需要监控:

  • 订单创建数量;
  • 支付成功率;
  • 支付失败原因;
  • 重复请求数量;
  • 长时间未完成订单;
  • 补偿任务数量。

只监控 CPU 和内存,无法判断业务是否正常。

25.4 健康检查

健康检查可以分为:

text 复制代码
Liveness
进程是否还活着

Readiness
当前是否能够接收流量

Startup
应用是否完成初始化

不要让普通健康检查执行复杂的大表 SQL 或调用所有下游系统,否则健康检查本身可能制造额外故障。


二十六、安全工程化

26.1 Secret 管理

不要把以下内容提交到仓库:

  • 数据库密码;
  • JWT 密钥;
  • 私钥;
  • API Secret;
  • 云平台凭证;
  • 生产环境 .env。

应使用:

  • Secret Manager;
  • Vault;
  • Kubernetes Secret;
  • CI Secret;
  • KMS;
  • HSM。

26.2 依赖安全

第三方依赖可能存在:

  • 已知漏洞;
  • 恶意包;
  • 拼写相似包;
  • 供应链攻击;
  • 维护停止;
  • 不兼容升级。

建议:

  • 锁定依赖;
  • 定期扫描漏洞;
  • 审核新增依赖;
  • 使用可信镜像源;
  • 记录软件物料清单;
  • 分批升级和回归测试。

26.3 输入校验

所有外部输入都不可信,包括:

  • HTTP 参数;
  • 消息队列数据;
  • CSV 文件;
  • 环境变量;
  • 数据库历史数据;
  • 第三方 API 响应。

需要校验:

  • 类型;
  • 长度;
  • 范围;
  • 格式;
  • 枚举值;
  • 业务关系。

26.4 最小权限

应用使用的数据库账号不应拥有:

  • 创建管理员;
  • 删除整个数据库;
  • 修改系统配置;
  • 访问无关业务库;

等不必要权限。


二十七、版本与发布管理

推荐使用语义化版本:

text 复制代码
MAJOR.MINOR.PATCH

例如:

text 复制代码
1.4.2

可以理解为:

  • MAJOR:不兼容变更;
  • MINOR:向后兼容的新功能;
  • PATCH:向后兼容的问题修复。

一次发布应能够关联:

text 复制代码
Git Commit
Git Tag
构建制品
Docker 镜像
依赖锁文件
数据库 Migration
发布记录

这样线上出现问题时,才能准确回答:

text 复制代码
部署了哪一版代码?
使用了哪些依赖?
执行了哪些数据库变更?
能否回滚?

二十八、常见工程化误区

28.1 所有代码都放在 main.py

问题:

  • 模块职责不清;
  • 难以测试;
  • 冲突频繁;
  • 重构困难。

应按业务和职责拆分模块。

28.2 使用相对路径假设当前目录

错误方式:

python 复制代码
open("./config.json")

从不同目录启动时可能失败。

应通过:

  • Package Resource;
  • 明确配置路径;
  • pathlib.Path;
  • 环境变量;

解析文件位置。

28.3 修改 sys.path

错误方式:

python 复制代码
import sys

sys.path.append("../")

这通常说明项目没有被正确安装或目录结构存在问题。

应使用:

bash 复制代码
uv sync

或者:

bash 复制代码
pip install -e .

让项目以 Package 形式进入环境。

28.4 直接提交 .env

.env 通常包含敏感配置,不应进入 Git。

只提交:

text 复制代码
.env.example

28.5 捕获所有异常后继续

python 复制代码
try:
    process()
except Exception:
    return None

这会隐藏真实故障,并可能造成错误数据继续流转。

28.6 过度 Mock

如果测试把数据库、序列化、SQL 和 HTTP 全部 Mock 掉,只能证明 Mock 按照预期工作。

核心基础设施仍需要集成测试。

28.7 只追求覆盖率

为了覆盖率编写没有断言意义的测试,无法真正降低风险。

应该优先验证业务行为和边界条件。

28.8 本地和生产使用不同命令

如果本地运行:

bash 复制代码
python app.py

CI 运行:

bash 复制代码
pytest

生产运行另一套自定义脚本,容易出现环境差异。

应尽量统一为:

bash 复制代码
uv run order-service

或者安装后的标准入口。

28.9 为了分层而分层

如果每一个简单函数都需要跨越五个抽象层,项目会变得难以理解。

抽象应服务于:

  • 业务隔离;
  • 可测试性;
  • 可替换性;
  • 变化控制。

没有明确变化点时,不要创建无意义接口。


二十九、落地实施顺序

遗留 Python 项目不适合一次完成所有改造。

可以按照以下顺序实施。

第一阶段:统一运行环境

完成:

  • 固定 Python 版本;
  • 建立虚拟环境;
  • 整理依赖;
  • 引入锁文件;
  • 编写 README。

第二阶段:统一代码质量

完成:

  • Ruff;
  • 基础类型注解;
  • 提交前检查;
  • CI 质量门禁。

第三阶段:补充测试

优先覆盖:

  • 金额计算;
  • 状态转换;
  • 权限控制;
  • 核心业务流程;
  • 历史故障场景。

第四阶段:拆分结构

逐步拆分:

  • API;
  • Application;
  • Domain;
  • Infrastructure。

不要在没有测试保护的情况下进行大规模重构。

第五阶段:完善交付

完成:

  • 构建 Wheel;
  • Docker 镜像;
  • CI/CD;
  • 配置中心;
  • Secret 管理。

第六阶段:完善可观测性

增加:

  • 结构化日志;
  • 指标;
  • Trace;
  • 健康检查;
  • 业务告警。

三十、上线检查清单

项目结构

  • 使用清晰的 Package 名称;
  • 生产代码与测试代码分离;
  • 模块职责明确;
  • 没有超大型 utils.py;
  • 没有随意修改 sys.path;
  • 具有统一启动入口。

依赖管理

  • 声明最低 Python 版本;
  • 使用独立虚拟环境;
  • 运行依赖和开发依赖分离;
  • 锁文件进入版本控制;
  • CI 使用锁文件安装;
  • 依赖升级经过测试;
  • 定期执行安全扫描。

配置与安全

  • 配置与代码分离;
  • Secret 没有提交到 Git;
  • 提供 .env.example;
  • 启动时验证必要配置;
  • 日志不会输出敏感信息;
  • 生产账号遵循最小权限。

代码质量

  • Ruff 检查通过;
  • 格式检查通过;
  • 类型检查通过;
  • 核心代码具有类型注解;
  • 未使用导入和死代码得到清理;
  • CI 能阻止不合格代码合并。

测试

  • 核心业务具有单元测试;
  • 数据库操作具有集成测试;
  • 异常分支得到覆盖;
  • 金额和边界条件得到覆盖;
  • 测试可以重复运行;
  • 测试不依赖执行顺序;
  • 覆盖率门槛符合项目实际情况。

部署运维

  • 可以构建 Wheel;
  • Docker 使用非 Root 用户;
  • 镜像中不包含 Secret;
  • 支持优雅关闭;
  • 具有健康检查;
  • 日志输出到标准输出;
  • 监控请求、错误和业务指标;
  • 发布版本可以追溯。

三十一、总结

Python 工程化不是把简单代码变复杂,而是通过标准化结构和自动化流程控制项目不断增长的复杂度。

一套完整的 Python 工程体系可以概括为:

text 复制代码
src 项目结构
      │
      ▼
pyproject.toml 统一配置
      │
      ▼
虚拟环境与依赖锁定
      │
      ▼
配置、日志与异常规范
      │
      ▼
Ruff 与类型检查
      │
      ▼
单元测试和集成测试
      │
      ▼
Wheel 与 Docker 构建
      │
      ▼
CI/CD 自动验证
      │
      ▼
日志、指标与链路追踪

小型项目可以从最基础的内容开始:

text 复制代码
pyproject.toml
src 目录
虚拟环境
Ruff
pytest
README

随着业务增长,再逐步加入:

text 复制代码
类型检查
依赖锁定
数据库 Migration
Docker
CI/CD
结构化日志
指标与 Trace

工程化的价值最终体现在:

  • 新成员能够快速启动项目;
  • 相同代码在不同环境中行为一致;
  • 修改代码后能够及时发现问题;
  • 线上故障能够被定位和追溯;
  • 项目可以持续发布而不是依靠人工操作。

最重要的原则可以概括为:

Python 的灵活性适合快速开始,而工程化的意义,是让这种快速开发能力在项目变大、团队变多和运行时间变长以后依然可控。

相关推荐
打工仔折腾 AI3 小时前
鸿蒙移动端直连 Windows 远程桌面配置与性能调优实践
人工智能·windows·后端·python·华为·性能优化·harmonyos
YIN_尹3 小时前
【git原理与使用】git基本操作及底层原理(一)
java·git·elasticsearch
乐橙开放平台3 小时前
跨地域监控难管理?通过 GB28181 把各地海康大华乐橙摄像头注册到云端平台
开发语言·笔记·物联网·自动化·php·音视频·智能家居
广东融科数据服务有限公司3 小时前
网络监控经常卡顿、掉线、画面模糊?一线排查问答实录
开发语言·网络·php
程序员飞哥3 小时前
AI 编程额度怎么用才不浪费?
java·前端·后端
yh弓长3 小时前
Spring MVC 的请求链路
java·spring·mvc
拆房老料3 小时前
【第四部分:Agentic 智能体设计模式】17. Agent 路由模式详解:从意图识别到动态模型选择
java·设计模式·ai编程·策略模式
Sarvartha3 小时前
Iterator 迭代器、ListIterator & foreach 循环
开发语言
樱花落木兰3 小时前
Redis 消息队列的可靠性演进:从 List、Pub/Sub 到 Stream 消费者组,以及 8.x 版本改变了什么
java·开发语言·经验分享·redis·分布式·python·软件工程