九、工程化与代码质量
阶段定位:写代码容易,写好代码难。工程化是将个人编码习惯升级为团队协作规范的过程。本阶段从编码规范出发,覆盖测试策略、性能优化、CI/CD 和项目发布,帮你建立”可维护、可协作、可交付”的工程思维。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| ┌──────────────────────────────────────────────────────────────────┐ │ Python 工程化知识图谱 │ ├──────────────────────────────────────────────────────────────────┤ │ │ │ 编码规范 │ │ ├── PEP 8 风格指南 │ │ ├── 命名规范(模块/类/函数/常量) │ │ └── 文档字符串(Google/Numpy 风格) │ │ │ │ 代码质量工具 │ │ ├── black(格式化) │ │ ├── isort(导入排序) │ │ ├── flake8 / pylint(代码检查) │ │ ├── mypy(类型检查) │ │ └── pre-commit(Git 钩子) │ │ │ │ 测试策略 │ │ ├── 单元测试(pytest) │ │ ├── 集成测试 │ │ ├── Mock 与依赖隔离 │ │ └── 测试覆盖率(pytest-cov) │ │ │ │ 性能优化 │ │ ├── cProfile(性能分析) │ │ ├── 优化技巧(推导式/生成器/内置函数) │ │ └── Numba / Cython(JIT 编译) │ │ │ │ CI/CD │ │ ├── GitHub Actions │ │ ├── tox(多环境测试) │ │ └── 自动化流水线 │ │ │ │ 打包发布 │ │ ├── pyproject.toml │ │ ├── 构建与发布(build / twine) │ │ └── Docker 容器化 │ │ │ └──────────────────────────────────────────────────────────────────┘
|
1. 编码规范与文档
1.1 PEP 8 核心规则
PEP 8 是 Python 的官方编码风格指南,遵循它不是束缚,而是让代码”像 Python”。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| module_name.py package_name ClassName function_name CONSTANT_NAME _private __mangled
x = 1 + 2
def func(a, b=1): pass
items[1:3]
items = [1, 2, 3]
result = some_function( arg1, arg2, arg3, arg4, arg5, )
import os import sys
import requests import numpy as np
from myproject import utils from myproject.core import models
|
PEP 8 命名规范速查:
| 类型 |
命名风格 |
示例 |
| 模块 |
小写 + 下划线 |
my_module.py |
| 包 |
小写,简短 |
mypackage |
| 类 |
大驼峰 |
MyClass |
| 函数 |
小写 + 下划线 |
my_function |
| 变量 |
小写 + 下划线 |
my_variable |
| 常量 |
大写 + 下划线 |
MY_CONSTANT |
| 私有属性 |
单下划线前缀 |
_private_attr |
| 名称改写 |
双下划线前缀 |
__mangled |
1.2 代码质量工具链
现代 Python 项目使用自动化工具保证代码质量:
| 工具 |
用途 |
命令 |
black |
代码格式化(opinionated) |
black src/ |
isort |
导入排序 |
isort src/ |
flake8 |
风格检查 |
flake8 src/ |
pylint |
深度代码分析 |
pylint src/ |
mypy |
静态类型检查 |
mypy src/ |
bandit |
安全检查 |
bandit -r src/ |
pydocstyle |
文档字符串规范 |
pydocstyle src/ |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| [tool.black] line-length = 88 target-version = ['py39']
[tool.isort] profile = "black" line_length = 88
[tool.mypy] python_version = "3.9" warn_return_any = true warn_unused_configs = true disallow_untyped_defs = true
[tool.flake8] max-line-length = 88 extend-ignore = ["E203", "W503"]
|
工具链工作流程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 代码提交 │ ▼ pre-commit 钩子 │ ├── black(格式化)─────────────┐ │ │ ├── isort(导入排序)───────────┤ │ │ ├── flake8(风格检查)──────────┤──→ 通过 ✓ │ │ └── mypy(类型检查)────────────┘ │ ▼ 提交成功
|
1.3 pre-commit 钩子
pre-commit 在每次 git commit 前自动运行代码检查,确保代码质量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.5.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: check-yaml - id: check-added-large-files
- repo: https://github.com/psf/black rev: 23.12.1 hooks: - id: black
- repo: https://github.com/PyCQA/isort rev: 5.13.2 hooks: - id: isort
- repo: https://github.com/PyCQA/flake8 rev: 7.0.0 hooks: - id: flake8
- repo: https://github.com/pre-commit/mirrors-mypy rev: v1.8.0 hooks: - id: mypy
|
1 2 3 4
| pip install pre-commit pre-commit install pre-commit run --all-files
|
1.4 文档规范
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| """ 模块级文档字符串。
描述模块的职责、主要类和函数的概要。 可以包含使用示例。
Example: >>> from myproject.utils import calculate >>> calculate(1, 2) 3 """
class Calculator: """计算器类。
提供基础的数学运算功能。
Attributes: history: 操作历史记录。
Example: >>> calc = Calculator() >>> calc.add(1, 2) 3 """
def add(self, a: int, b: int) -> int: """返回两数之和。
Args: a: 第一个加数。 b: 第二个加数。
Returns: 两数之和。
Raises: TypeError: 如果参数不是数字。 """ if not isinstance(a, (int, float)) or not isinstance(b, (int, float)): raise TypeError("参数必须是数字") return a + b
|
文档生成工具:
| 工具 |
特点 |
适用场景 |
Sphinx |
功能强大,支持多种输出格式 |
大型项目文档 |
MkDocs |
基于 Markdown,配置简单 |
项目文档站点 |
pdoc |
零配置,自动生成 API 文档 |
快速文档生成 |
2. 测试策略
2.1 测试金字塔
1 2 3 4 5 6 7 8 9 10 11 12
| /\ / \ E2E 测试(少而精) /----\ - 模拟真实用户操作 / \ - 执行慢,维护成本高 /--------\ / \ 集成测试(中等数量) /------------\ - 测试模块间交互 / \ - 需要外部依赖 /----------------\ 单元测试(大量) - 测试最小单元 - 快速、独立、可重复
|
比例建议:单元测试 70% / 集成测试 20% / E2E 测试 10%
测试类型对比:
| 类型 |
速度 |
维护成本 |
覆盖范围 |
数量建议 |
| 单元测试 |
快 |
低 |
单个函数/方法 |
大量 |
| 集成测试 |
中等 |
中等 |
模块间交互 |
中等 |
| E2E 测试 |
慢 |
高 |
完整业务流程 |
少量 |
2.2 pytest 框架
pytest 是 Python 最流行的测试框架,语法简洁,功能强大。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| import pytest from myproject.calculator import Calculator
@pytest.fixture def calc(): """每个测试方法前创建 Calculator 实例。""" return Calculator()
@pytest.fixture(scope="module") def database(): """模块级只创建一次。""" db = create_test_database() yield db db.cleanup()
@pytest.mark.parametrize("a,b,expected", [ (1, 2, 3), (-1, 1, 0), (0, 0, 0), (100, 200, 300), ]) def test_add(calc, a, b, expected): assert calc.add(a, b) == expected
def test_add_invalid_type(calc): with pytest.raises(TypeError, match="必须是数字"): calc.add("1", 2)
@pytest.mark.skip(reason="功能尚未实现") def test_future_feature(): pass
@pytest.mark.skipif(sys.version_info < (3, 10), reason="需要 Python 3.10+") def test_match_case(): pass
@pytest.mark.slow @pytest.mark.integration def test_database_integration(): pass
|
pytest 常用命令:
1 2 3 4 5 6 7
| pytest -v pytest -x pytest -k "test_add" pytest -m "not slow" pytest --cov=myproject pytest --cov=myproject --cov-report=html pytest -n 4
|
fixture 作用域:
| 作用域 |
说明 |
适用场景 |
function |
每个测试函数调用一次(默认) |
独立测试数据 |
class |
每个测试类调用一次 |
类级别共享资源 |
module |
每个模块调用一次 |
数据库连接、配置 |
package |
每个包调用一次 |
包级别资源 |
session |
整个测试会话调用一次 |
全局资源 |
2.3 Mock 与依赖隔离
Mock 用于隔离外部依赖,让测试更快、更稳定。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| from unittest.mock import Mock, patch, MagicMock, call
def test_fetch_user(): with patch("myproject.api.requests.get") as mock_get: mock_get.return_value.json.return_value = {"id": 1, "name": "Alice"} mock_get.return_value.status_code = 200
user = fetch_user(1) assert user["name"] == "Alice" mock_get.assert_called_once_with("https://api.example.com/users/1")
@patch("myproject.api.requests.get") def test_fetch_user_decorator(mock_get): mock_get.return_value.json.return_value = {"id": 1} user = fetch_user(1) assert user["id"] == 1
mock = Mock() mock.some_method.return_value = 42 mock.other.side_effect = [1, 2, 3]
print(mock.some_method()) print(mock.other()) print(mock.other())
mock.func(1, 2, key="value") mock.func.assert_called_with(1, 2, key="value") mock.func.assert_called_once() print(mock.func.call_args) print(mock.func.call_count)
mock_dict = MagicMock() mock_dict.__getitem__.return_value = "value" print(mock_dict["key"])
|
Mock 使用场景:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| ┌─────────────────────────────────────────────────────────────┐ │ 测试代码 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ def test_fetch_user(): │ │ with patch("requests.get") as mock: │ │ # Mock 替代真实的 HTTP 请求 │ │ mock.return_value.json.return_value = {...} │ │ result = fetch_user(1) │ │ assert result["name"] == "Alice" │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ 替代 ▼ ┌─────────────────────────────────────────────────────────────┐ │ 外部依赖(被 Mock) │ ├─────────────────────────────────────────────────────────────┤ │ │ │ - HTTP 请求(requests) │ │ - 数据库查询 │ │ - 文件系统 │ │ - 第三方 API │ │ │ └─────────────────────────────────────────────────────────────┘
|
2.4 测试覆盖率
测试覆盖率衡量测试代码覆盖了多少生产代码。
1 2 3 4 5 6 7
| pip install pytest-cov
pytest --cov=myproject --cov-report=term-missing pytest --cov=myproject --cov-report=html pytest --cov=myproject --cov-report=xml
|
1 2 3 4 5 6 7 8 9 10 11
| [run] source = src/myproject omit = */tests/*, */migrations/*
[report] fail_under = 80 exclude_lines = pragma: no cover if TYPE_CHECKING: raise NotImplementedError
|
覆盖率目标:
| 项目类型 |
目标覆盖率 |
| 个人项目 |
60-70% |
| 团队项目 |
70-80% |
| 开源库 |
80-90% |
| 金融/医疗 |
90%+ |
3. 性能优化与 Profiling
3.1 性能分析工具
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import cProfile import pstats
profiler = cProfile.Profile() profiler.enable()
result = heavy_computation() profiler.disable()
stats = pstats.Stats(profiler) stats.sort_stats("cumulative") stats.print_stats(20)
stats.dump_stats("profile.stats")
|
cProfile 输出示例:
1 2 3 4 5 6 7 8 9
| 1000004 function calls in 0.357 seconds
Ordered by: cumulative time
ncalls tottime percall cumtime percall filename:lineno(function) 1 0.000 0.000 0.357 0.357 script.py:1(<module>) 1 0.200 0.200 0.350 0.350 script.py:5(heavy_computation) 1000000 0.150 0.000 0.150 0.000 {built-in method math.sqrt} 1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler'}
|
3.2 常见性能优化技巧
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
|
squares = [] for x in range(1000): squares.append(x ** 2)
squares = [x ** 2 for x in range(1000)]
total = sum(x ** 2 for x in range(1000000))
def process_local(data): append = result.append for item in data: append(item * 2)
result = [] for x in items: if x > 0: result.append(x)
result = list(filter(None, items))
result = "" for s in strings: result += s
result = "".join(strings)
if item in large_list: if item in large_set:
class Point: __slots__ = ["x", "y"] def __init__(self, x, y): self.x = x self.y = y
for user_id in user_ids: db.query(f"SELECT * FROM users WHERE id = {user_id}")
placeholders = ",".join(["?"] * len(user_ids)) db.query(f"SELECT * FROM users WHERE id IN ({placeholders})", user_ids)
|
性能优化速查表:
| 优化点 |
慢代码 |
快代码 |
提升 |
| 循环 |
for + append |
列表推导式 |
~20% |
| 大数据 |
列表 |
生成器 |
内存节省 90% |
| 字符串拼接 |
+= |
join() |
~10x |
| 查找 |
list |
set/dict |
O(n)→O(1) |
| 函数调用 |
全局函数 |
局部变量绑定 |
~15% |
| 内存 |
普通 class |
__slots__ |
节省 30-50% |
3.3 Numba JIT 编译
对于数值计算密集型任务,Numba 可以实现接近 C 的性能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
|
from numba import jit import numpy as np
@jit(nopython=True) def fast_sum(arr): """使用 Numba 加速的求和函数。""" total = 0.0 for i in range(arr.shape[0]): total += arr[i] return total
arr = np.random.randn(1000000)
def slow_sum(arr): """纯 Python 实现。""" total = 0.0 for i in range(arr.shape[0]): total += arr[i] return total
|
4. CI/CD 与自动化
4.1 GitHub Actions 示例
GitHub Actions 是 GitHub 提供的 CI/CD 服务,与代码仓库深度集成。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| name: CI
on: push: branches: [main, develop] pull_request: branches: [main]
jobs: test: runs-on: ubuntu-latest strategy: matrix: python-version: ["3.9", "3.10", "3.11", "3.12"]
steps: - uses: actions/checkout@v4
- name: Set up Python uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }}
- name: Install dependencies run: | python -m pip install --upgrade pip pip install -e ".[dev]"
- name: Lint with ruff run: ruff check .
- name: Format check with black run: black --check .
- name: Type check with mypy run: mypy src/
- name: Test with pytest run: pytest --cov=src --cov-report=xml
- name: Upload coverage uses: codecov/codecov-action@v3 with: file: ./coverage.xml
|
CI/CD 流水线:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| 代码推送 │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ GitHub Actions │ ├─────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │ 安装依赖 │──→│ 代码检查 │──→│ 类型检查 │──→│ 运行测试 │ │ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │ │ │ │ │ │ │ └─────────────┴─────────────┴─────────────┘ │ │ │ │ │ ▼ │ │ ┌───────────┐ │ │ │ 上传覆盖率 │ │ │ └───────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌───────────┐ │ 自动部署 │ └───────────┘
|
4.2 tox 多环境测试
tox 可以在多个 Python 版本下运行测试,确保代码兼容性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| [tox] envlist = py39, py310, py311, py312, lint, type
[testenv] deps = pytest pytest-cov commands = pytest {posargs}
[testenv:lint] deps = flake8 black commands = black --check . flake8 src/
[testenv:type] deps = mypy commands = mypy src/
|
1 2 3
| tox tox -e py311 tox -e lint
|
5. 打包与发布
5.1 现代打包流程
Python 现在使用 pyproject.toml 作为项目配置文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| [build-system] requires = ["setuptools>=61.0", "wheel"] build-backend = "setuptools.build_meta"
[project] name = "myproject" version = "1.0.0" description = "项目描述" readme = "README.md" license = {text = "MIT"} requires-python = ">=3.9" classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", ] dependencies = [ "requests>=2.28.0", "pydantic>=2.0", ]
[project.optional-dependencies] dev = [ "pytest>=7.0", "pytest-cov", "black", "flake8", "mypy", "pre-commit", ]
[project.scripts] myproject = "myproject.cli:main"
[project.urls] Homepage = "https://github.com/username/myproject" Repository = "https://github.com/username/myproject" Documentation = "https://myproject.readthedocs.io"
[tool.setuptools.packages.find] where = ["src"]
|
项目结构:
1 2 3 4 5 6 7 8 9 10 11
| myproject/ ├── src/ │ └── myproject/ │ ├── __init__.py │ └── core.py ├── tests/ │ ├── __init__.py │ └── test_core.py ├── pyproject.toml ├── README.md └── LICENSE
|
构建与发布命令:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| pip install build twine
python -m build
python -m twine upload dist/*
python -m twine upload --repository testpypi dist/*
pip install -e ".[dev]"
|
5.2 Docker 化
Docker 确保应用在任何环境下一致运行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt
COPY src/ ./src/
ENV PYTHONPATH=/app/src EXPOSE 8000
CMD ["python", "-m", "myproject"]
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| version: "3.8" services: app: build: . ports: - "8000:8000" environment: - DATABASE_URL=postgresql://user:pass@db:5432/mydb depends_on: - db
db: image: postgres:15 environment: POSTGRES_USER: user POSTGRES_PASSWORD: pass POSTGRES_DB: mydb volumes: - postgres_data:/var/lib/postgresql/data
volumes: postgres_data:
|
Docker 常用命令:
1 2 3 4 5 6 7 8 9 10
| docker build -t myproject:1.0 .
docker run -p 8000:8000 myproject:1.0
docker-compose up -d docker-compose logs -f docker-compose down
|
附录:第九阶段自检清单
在继续学习第十阶段之前,请确保你能:
6. 工作实战场景
6.1 场景一:为遗留项目添加测试
问题:接手一个没有测试的老项目,如何快速建立测试体系?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
|
def calculate_discount(price, discount_rate, max_discount=100): """计算折扣价格。""" if price <= 0: raise ValueError("价格必须大于 0") if discount_rate < 0 or discount_rate > 1: raise ValueError("折扣率必须在 0-1 之间")
discount = price * discount_rate actual_discount = min(discount, max_discount) return price - actual_discount
import pytest from myproject.utils import calculate_discount
def test_calculate_discount_normal(): assert calculate_discount(100, 0.1) == 90
def test_calculate_discount_max_cap(): assert calculate_discount(1000, 0.5, max_discount=200) == 800
def test_calculate_discount_invalid_price(): with pytest.raises(ValueError, match="价格必须大于 0"): calculate_discount(0, 0.1)
|
步骤总结:
- 优先级排序:核心业务 > 常用功能 > 边缘功能
- 集成测试先行:对于难以单元测试的遗留代码,先写集成测试
- 重构时添加单元测试:每次修改遗留代码时,先写测试
- 设定合理目标:遗留项目 60-70% 覆盖率即可,新代码 80%+
6.2 场景二:CI/CD 流水线搭建
目标:为团队项目搭建完整的 CI/CD 流水线。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135
| name: CI/CD Pipeline
on: push: branches: [main, develop] pull_request: branches: [main]
jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.11"
- name: Install linters run: pip install black isort flake8 mypy ruff
- name: Run Black run: black --check .
- name: Run isort run: isort --check-only --diff .
- name: Run flake8 run: flake8 src/ --max-line-length=88
- name: Run mypy run: mypy src/ --ignore-missing-imports
test: runs-on: ubuntu-latest needs: lint strategy: matrix: python-version: ["3.9", "3.10", "3.11", "3.12"] os: [ubuntu-latest, windows-latest, macos-latest] fail-fast: false
steps: - uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }}
- name: Install dependencies run: | python -m pip install --upgrade pip pip install -e ".[dev]"
- name: Run tests run: pytest --cov=src --cov-report=xml --cov-report=html
- name: Upload coverage to Codecov uses: codecov/codecov-action@v3 with: file: ./coverage.xml flags: unittests fail_ci_if_error: true
security: runs-on: ubuntu-latest needs: lint steps: - uses: actions/checkout@v4
- name: Run Bandit security check uses: tj-actions/bandit@v5 with: targets: src/
- name: Run Safety check for dependencies run: | pip install safety safety check --json
build: runs-on: ubuntu-latest needs: [test, security] if: github.ref == 'refs/heads/main'
steps: - uses: actions/checkout@v4
- name: Set up Python uses: actions/setup-python@v5 with: python-version: "3.11"
- name: Install build tools run: pip install build twine
- name: Build package run: python -m build
- name: Publish to PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: password: ${{ secrets.PYPI_API_TOKEN }}
docker: runs-on: ubuntu-latest needs: [test, security] if: github.ref == 'refs/heads/main'
steps: - uses: actions/checkout@v4
- name: Set up Docker Buildx uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push uses: docker/build-push-action@v5 with: context: . push: true tags: | myproject:latest myproject:${{ github.sha }}
|
流水线架构图:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| ┌─────────────────────────────────────────────────────────────┐ │ CI/CD 流水线 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ 代码推送 │ │ │ │ │ ▼ │ │ ┌──────────────┐ │ │ │ lint 检查 │ black + isort + flake8 + mypy │ │ └──────────────┘ │ │ │ │ │ ├──────────────────────┐ │ │ ▼ ▼ │ │ ┌──────────────┐ ┌──────────────┐ │ │ │ test 测试 │ │ security 检查 │ │ │ │ (多版本多系统) │ │ bandit+safety │ │ │ └──────────────┘ └──────────────┘ │ │ │ │ │ │ └──────────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────┐ │ │ │ 构建发布 │ PyPI + Docker Hub │ │ └─────────────┘ │ │ │ │ 耗时估计: │ │ lint: ~1min │ │ test: ~5min (并行) │ │ build: ~2min │ │ 总计: ~8-10min │ │ │ └─────────────────────────────────────────────────────────────┘
|
6.3 场景三:性能优化实战
问题:API 接口响应慢,如何定位和优化?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87
| import cProfile import pstats from io import StringIO
def profile_api(): profiler = cProfile.Profile() profiler.enable()
result = slow_api_endpoint()
profiler.disable()
s = StringIO() stats = pstats.Stats(profiler, stream=s).sort_stats('cumulative') stats.print_stats(20) print(s.getvalue())
def get_users_slow(): users = db.query("SELECT * FROM users") result = [] for user in users: result.append({ "id": user.id, "name": user.name, "email": user.email, }) return json.dumps(result)
def get_users_fast(): users = db.query("SELECT id, name, email FROM users")
result = [ {"id": u.id, "name": u.name, "email": u.email} for u in users ]
import orjson return orjson.dumps(result)
from numba import jit import numpy as np
@jit(nopython=True) def calculate_score_batch(user_scores: np.ndarray): """批量计算用户评分(比纯 Python 快 50x)。""" result = np.zeros_like(user_scores) for i in range(len(user_scores)): result[i] = np.log(user_scores[i] + 1) * 100 return result
from functools import lru_cache import redis
@lru_cache(maxsize=1000) def get_user_config(user_id: int): return db.query("SELECT config FROM user_settings WHERE user_id = ?", user_id)
redis_client = redis.Redis(host='localhost', port=6379, db=0)
def get_user_stats_cached(user_id: int): cache_key = f"user_stats:{user_id}" cached = redis_client.get(cache_key)
if cached: return json.loads(cached)
stats = db.query("SELECT * FROM user_stats WHERE user_id = ?", user_id) redis_client.setex(cache_key, 300, json.dumps(stats)) return stats
|
性能优化检查清单:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| □ 数据库查询 □ 是否只查询必要的字段? □ 是否使用了索引? □ 是否避免了 N+1 查询? □ 是否使用了批量操作?
□ 数据处理 □ 是否使用了列表推导式代替循环? □ 是否使用了生成器处理大数据? □ 是否使用了内置函数(map/filter/sorted)?
□ I/O 操作 □ 是否使用了缓存(lru_cache / Redis)? □ 是否减少了文件读写次数? □ 是否使用了异步 I/O(aiohttp/asyncio)?
□ 序列化 □ 是否使用了高速 JSON 库(orjson/ujson)? □ 是否避免了大对象的深拷贝?
□ 计算密集型 □ 是否可以考虑 Numba/Cython 加速? □ 是否可以使用多进程(multiprocessing)?
|
7. 常见面试题汇总
7.1 测试相关
Q1: 什么是测试金字塔?如何平衡不同类型的测试?
答案要点:
- 测试金字塔:单元测试(底)> 集成测试(中)> E2E 测试(顶)
- 比例建议:70% / 20% / 10%
- 单元测试:快速、独立、维护成本低
- 集成测试:测试模块间交互,需要外部依赖
- E2E 测试:模拟真实用户操作,执行慢、维护成本高
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| def test_calculate_discount(): assert calculate_discount(100, 0.1) == 90
def test_order_flow(): order = create_order(user_id=1, items=[...]) payment = process_payment(order.id) assert payment.status == "success"
def test_user_checkout_flow(): browser.get("/products/1") browser.find_element("#add-to-cart").click() browser.find_element("#checkout").click() assert "订单成功" in browser.page_source
|
Q2: 如何使用 Mock 测试依赖外部 API 的函数?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| from unittest.mock import patch, Mock import pytest from myproject.api import fetch_user_data
@patch("myproject.api.requests.get") def test_fetch_user_data(mock_get): """测试依赖 HTTP 请求的函数。""" mock_response = Mock() mock_response.json.return_value = {"id": 1, "name": "Alice"} mock_response.status_code = 200 mock_get.return_value = mock_response
result = fetch_user_data(1)
assert result["name"] == "Alice"
mock_get.assert_called_once_with("https://api.example.com/users/1")
@patch("myproject.api.requests.get") def test_fetch_user_data_retry(mock_get): """测试重试逻辑。""" mock_get.side_effect = [ ConnectionError("网络错误"), Mock(json=lambda: {"id": 1}, status_code=200) ]
result = fetch_user_data_with_retry(1) assert result["id"] == 1 assert mock_get.call_count == 2
|
Q3: 测试覆盖率多少合适?100% 覆盖率是必要的吗?
答案要点:
- 个人项目:60-70%
- 团队项目:70-80%
- 开源库:80-90%
- 金融/医疗:90%+
100% 覆盖率不是目标:
- 有些代码难以测试(如异常处理分支)
- 过度追求覆盖率会导致测试质量下降
- 关键业务逻辑必须有测试,边缘功能可以适当放宽
1 2 3 4 5 6 7 8
| def debug_log(message): if os.getenv("DEBUG"): print(message)
if TYPE_CHECKING: from typing import List
|
7.2 CI/CD 相关
Q4: 解释 CI/CD 的核心概念和流程。
CI(持续集成):
- 开发者频繁提交代码到主分支
- 每次提交自动运行测试、代码检查
- 快速发现问题,避免集成地狱
CD(持续交付/部署):
- 持续交付:代码随时可以部署到生产环境
- 持续部署:自动部署到生产环境(无需人工干预)
典型流程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 开发者提交代码 ↓ Git Push ↓ CI 服务器触发(GitHub Actions/Jenkins) ↓ 代码检查(black/flake8/mypy) ↓ 运行测试(pytest) ↓ 构建产物(Docker/Wheel) ↓ 部署到测试环境 ↓ 人工审批(可选) ↓ 部署到生产环境
|
Q5: 如何确保 CI/CD 流水线的安全性?
答案要点:
密钥管理:
- 不要在代码中硬编码密钥
- 使用 CI 平台的 Secrets 功能
- 定期轮换密钥
权限控制:
- 限制谁能修改 CI 配置
- 分支保护:关键分支必须通过 CI 才能 merge
依赖安全:
- 锁定依赖版本(requirements.lock)
- 使用 Dependabot 自动更新依赖
- 运行安全扫描(Safety/Bandit)
1 2 3 4 5 6 7 8 9 10
| env: DATABASE_URL: ${{ secrets.DATABASE_URL }} API_KEY: ${{ secrets.API_KEY }}
- name: Run Safety run: | pip install safety safety check --json
|
7.3 性能优化相关
Q6: 如何定位 Python 程序的性能瓶颈?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| import cProfile import pstats
profiler = cProfile.Profile() profiler.enable()
result = slow_function() profiler.disable()
stats = pstats.Stats(profiler) stats.sort_stats('cumulative') stats.print_stats(20)
from line_profiler import LineProfiler
lp = LineProfiler() lp.add_function(slow_function) lp_wrapper = lp(slow_function)
result = lp_wrapper() lp.print_stats()
from memory_profiler import profile
@profile def memory_intensive_function(): large_list = [i for i in range(1000000)] return sum(large_list)
|
Q7: 列举 5 种常见的 Python 性能优化技巧。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
|
squares = [] for x in range(1000): squares.append(x ** 2)
squares = [x ** 2 for x in range(1000)]
data = [process(item) for item in huge_list]
data = (process(item) for item in huge_list)
result = "" for s in strings: result += s
result = "".join(strings)
if item in large_list: pass
if item in large_set: pass
import math def calculate_fast(): sqrt = math.sqrt return [sqrt(x) for x in range(1000)]
|
7.4 打包发布相关
Q8: 解释 pyproject.toml 的作用和结构。
答案要点:
- 现代 Python 项目的配置文件(替代 setup.py)
- 声明式配置,更易维护
- 包含构建系统、项目元数据、依赖、工具配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| [build-system] requires = ["setuptools>=61.0"] build-backend = "setuptools.build_meta"
[project] name = "myproject" version = "1.0.0" description = "项目描述" requires-python = ">=3.9" dependencies = [ "requests>=2.28.0", "pydantic>=2.0", ]
[project.optional-dependencies] dev = [ "pytest>=7.0", "black", "mypy", ]
[project.scripts] myproject = "myproject.cli:main"
[tool.black] line-length = 88
[tool.mypy] python_version = "3.11" strict = true
|
Q9: 如何发布 Python 包到 PyPI?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| pip install build twine
python -m build
twine check dist/*
twine upload --repository testpypi dist/*
twine upload dist/*
|
7.5 Docker 相关
Q10: 编写一个生产级 Dockerfile 的最佳实践。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| FROM python:3.11-slim as builder
WORKDIR /app
COPY requirements.txt . RUN pip install --no-cache-dir --user -r requirements.txt
FROM python:3.11-slim
WORKDIR /app
COPY --from=builder /root/.local /root/.local ENV PATH=/root/.local/bin:$PATH
COPY src/ ./src/
RUN useradd -m appuser USER appuser
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \ CMD curl -f http://localhost:8000/health || exit 1
ENV PYTHONUNBUFFERED=1 ENV PYTHONDONTWRITEBYTECODE=1
EXPOSE 8000
CMD ["python", "-m", "myproject"]
|
最佳实践要点:
- 使用
.dockerignore 排除无关文件
- 多阶段构建减小镜像体积
- 非特权用户运行(安全)
- 健康检查(HEALTHCHECK)
- 固定基础镜像版本(
python:3.11-slim 而非 python:latest)
- 利用 Docker 缓存层(依赖先安装,代码后复制)
8. 实战项目:完整的 CI/CD 流水线搭建
8.1 项目概述
目标:为 Flask API 项目搭建完整的 CI/CD 流水线,包括代码检查、测试、安全扫描、自动部署。
技术栈:
- Flask:Web 框架
- pytest:测试框架
- GitHub Actions:CI/CD 平台
- Docker:容器化
- PyPI:包发布
8.2 项目结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| myflaskapp/ ├── src/ │ └── myflaskapp/ │ ├── __init__.py │ ├── app.py │ ├── models.py │ └── utils.py ├── tests/ │ ├── __init__.py │ ├── conftest.py │ ├── test_app.py │ └── test_utils.py ├── .github/ │ └── workflows/ │ └── ci.yml ├── Dockerfile ├── docker-compose.yml ├── pyproject.toml ├── requirements.txt ├── .pre-commit-config.yaml ├── .dockerignore └── .gitignore
|
8.3 核心文件
pyproject.toml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| [build-system] requires = ["setuptools>=61.0"] build-backend = "setuptools.build_meta"
[project] name = "myflaskapp" version = "1.0.0" description = "Flask API with CI/CD" requires-python = ">=3.9" dependencies = [ "flask>=3.0", "flask-sqlalchemy>=3.0", "pydantic>=2.0", ]
[project.optional-dependencies] dev = [ "pytest>=7.0", "pytest-cov", "pytest-flask", "black", "isort", "flake8", "mypy", "pre-commit", ]
[tool.black] line-length = 88 target-version = ['py39']
[tool.isort] profile = "black"
[tool.mypy] python_version = "3.11" warn_return_any = true disallow_untyped_defs = true
[tool.pytest.ini_options] testpaths = ["tests"] python_files = "test_*.py" python_classes = "Test*" python_functions = "test_*" addopts = "-v --cov=src --cov-report=term-missing"
|
Flask 应用代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| from flask import Flask, jsonify, request from pydantic import BaseModel, Field from typing import Optional
app = Flask(__name__)
class UserCreate(BaseModel): """用户创建模型。""" name: str = Field(..., min_length=1, max_length=50) email: str = Field(..., pattern=r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$") age: Optional[int] = Field(None, ge=0, le=150)
users_db = {}
@app.route("/health", methods=["GET"]) def health(): """健康检查端点。""" return jsonify({"status": "ok"})
@app.route("/users", methods=["POST"]) def create_user(): """创建用户。""" try: user_data = UserCreate(**request.json) except Exception as e: return jsonify({"error": str(e)}), 400
user_id = len(users_db) + 1 users_db[user_id] = user_data.dict()
return jsonify({ "id": user_id, "user": user_data.dict() }), 201
@app.route("/users/<int:user_id>", methods=["GET"]) def get_user(user_id: int): """获取用户。""" if user_id not in users_db: return jsonify({"error": "User not found"}), 404
return jsonify({ "id": user_id, "user": users_db[user_id] })
if __name__ == "__main__": app.run(debug=True)
|
测试代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| import pytest from myflaskapp.app import app
@pytest.fixture def client(): """创建测试客户端。""" app.config["TESTING"] = True with app.test_client() as client: yield client
import pytest from myflaskapp.app import app
def test_health(client): """测试健康检查。""" response = client.get("/health") assert response.status_code == 200 assert response.json["status"] == "ok"
def test_create_user(client): """测试创建用户。""" response = client.post("/users", json={ "name": "Alice", "email": "alice@example.com", "age": 30 }) assert response.status_code == 201 assert response.json["user"]["name"] == "Alice"
def test_create_user_invalid_email(client): """测试无效邮箱。""" response = client.post("/users", json={ "name": "Bob", "email": "invalid-email" }) assert response.status_code == 400
def test_get_user_not_found(client): """测试获取不存在的用户。""" response = client.get("/users/999") assert response.status_code == 404
|
GitHub Actions CI/CD:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
| name: CI/CD Pipeline
on: push: branches: [main, develop] pull_request: branches: [main]
jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4
- uses: actions/setup-python@v5 with: python-version: "3.11"
- name: Install linters run: pip install black isort flake8 mypy
- name: Run Black run: black --check .
- name: Run isort run: isort --check-only --diff .
- name: Run flake8 run: flake8 src/ --max-line-length=88
- name: Run mypy run: mypy src/
test: runs-on: ubuntu-latest needs: lint strategy: matrix: python-version: ["3.9", "3.10", "3.11", "3.12"]
steps: - uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }}
- name: Install dependencies run: | python -m pip install --upgrade pip pip install -e ".[dev]"
- name: Run tests run: pytest --cov=src --cov-report=xml
- name: Upload coverage uses: codecov/codecov-action@v3 with: file: ./coverage.xml
build: runs-on: ubuntu-latest needs: [test] if: github.ref == 'refs/heads/main'
steps: - uses: actions/checkout@v4
- name: Build Docker image run: docker build -t myflaskapp:${{ github.sha }} .
- name: Push to registry run: | echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin docker push myflaskapp:${{ github.sha }}
|
Dockerfile:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt
COPY src/ ./src/
ENV FLASK_APP=myflaskapp.app ENV FLASK_ENV=production
EXPOSE 5000
HEALTHCHECK --interval=30s --timeout=3s \ CMD curl -f http://localhost:5000/health || exit 1
CMD ["python", "-m", "flask", "run", "--host=0.0.0.0"]
|
8.4 项目亮点
完整的 CI/CD 流程:
- 代码检查(lint)
- 多版本测试(test)
- Docker 构建(build)
- 自动部署
测试覆盖:
- 单元测试(pytest)
- 覆盖率报告(pytest-cov)
- 异常处理测试
代码质量:
- pre-commit 钩子
- 类型检查(mypy)
- 格式化(black)
容器化:
工程师寄语:代码质量不是”写完后再检查”,而是”写的时候就有意识”。自动化工具(black、pytest、CI)是质量的护城河,但真正的质量来自于工程师对”简洁、清晰、可测试”的追求。记住:写代码是给人看的,顺便让机器执行。
工程化建议:
- 从项目第一天就配置好自动化工具(pre-commit、CI)
- 测试覆盖率是手段,不是目的——关键路径必须有测试
- 性能优化前先 profiling,避免过早优化
- CI/CD 流水线越早建立,后期越轻松