第 3 章:Python 工程化
本章将带你从"写脚本"进阶到"做工程"。就像前端项目需要 npm、webpack、ESLint 一样,Python 项目也需要虚拟环境、包管理、代码规范和测试体系。掌握这些工具链,是写出可维护、可协作 Python 代码的关键。
3.1 虚拟环境
在前端开发中,每个项目的 node_modules 都是独立的,A 项目用 React 18,B 项目用 React 17,互不干扰。Python 同样需要这种隔离机制——这就是虚拟环境。
为什么需要虚拟环境
Python 的包默认安装在全局环境中。如果项目 A 需要 requests 2.28,项目 B 需要 requests 2.31,全局安装必然冲突。虚拟环境为每个项目创建独立的 Python 运行时和包目录,彻底解决这个问题。
Python 的虚拟环境 ≈ 前端项目中的 node_modules + .nvmrc(指定 Node 版本)。venv 创建隔离环境,类似 npm 管理依赖。
venv:Python 内置方案
Python 3.3+ 内置了 venv 模块,无需额外安装。
# 创建虚拟环境(在项目根目录执行)
python -m venv .venv
# Windows 激活
.venv\Scripts\activate
# macOS/Linux 激活
source .venv/bin/activate
# 激活后,命令行提示符前会出现 (.venv)
# 退出虚拟环境
deactivate
conda:数据科学首选
如果你从事数据科学或 AI 方向,conda 是更好的选择。它不仅管理 Python 包,还能管理非 Python 依赖(如 CUDA、MKL)。
# 创建环境并指定 Python 版本
conda create -n myproject python=3.11
# 激活环境
conda activate myproject
# 安装包(同时安装 numpy、pandas 等科学计算库)
conda install numpy pandas matplotlib
# 导出环境配置
conda env export > environment.yml
# 从配置重建环境
conda env create -f environment.yml
# 退出环境
conda deactivate
uv:极速现代方案
uv 是用 Rust 编写的 Python 包管理器和环境管理器,速度比 pip 快 10-100 倍,是 2024 年以来最值得关注的新工具。
# 安装 uv(跨平台)
curl -LsSf https://astral.sh/uv/install.sh | sh
# 创建虚拟环境
uv venv .venv
# 激活(与 venv 相同)
source .venv/bin/activate # Linux/macOS
.venv\Scripts\activate # Windows
# 安装包(极速!)
uv pip install requests fastapi
# 从 requirements.txt 安装
uv pip install -r requirements.txt
将 .venv/ 目录加入 .gitignore,就像前端不把 node_modules 提交到 Git 一样。
3.2 包管理进阶
前端有 package.json 管理依赖,Python 也有多种依赖声明方式。从传统的 requirements.txt 到现代的 pyproject.toml,工具链正在快速进化。
requirements.txt:传统方式
这是最常见的依赖声明文件,格式简单直接,类似 npm 的 package-lock.json 的简化版。
# requirements.txt
requests>=2.28.0
fastapi==0.104.0
pydantic>=2.0.0,<3.0.0
pytest>=7.0.0; python_version >= "3.8"
# 开发依赖(通常放在 requirements-dev.txt)
black
ruff
mypy
pytest-cov
# 生成当前环境的依赖列表
pip freeze > requirements.txt
# 安装依赖
pip install -r requirements.txt
pyproject.toml:现代标准
PEP 518 和 PEP 621 确立了 pyproject.toml 作为 Python 项目的标准配置文件。它类似前端的 package.json + tsconfig.json + .eslintrc 的合体。
# pyproject.toml
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "my-awesome-project"
version = "0.1.0"
description = "一个示例项目"
readme = "README.md"
requires-python = ">=3.9"
license = { text = "MIT" }
authors = [
{ name = "你的名字", email = "you@example.com" }
]
keywords = ["web", "api", "fastapi"]
classifiers = [
"Development Status :: 4 - Beta",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
]
dependencies = [
"fastapi>=0.104.0",
"uvicorn[standard]>=0.24.0",
"pydantic>=2.0.0",
"sqlalchemy>=2.0.0",
]
[project.optional-dependencies]
dev = [
"pytest>=7.0.0",
"black>=23.0.0",
"ruff>=0.1.0",
"mypy>=1.0.0",
]
[project.scripts]
mycli = "my_project.cli:main"
[tool.black]
line-length = 88
target-version = ['py39']
[tool.ruff]
line-length = 88
select = ["E", "F", "I", "N", "W", "UP"]
[tool.mypy]
python_version = "3.9"
strict = true
warn_return_any = true
Poetry:类 npm 体验
Poetry 提供了最接近 npm/yarn/pnpm 的开发体验,包括依赖锁定、虚拟环境自动管理、发布打包等功能。
# 安装 Poetry
pip install poetry
# 创建新项目(类似 npm init)
poetry new my-project
cd my-project
# 添加依赖(类似 npm install)
poetry add fastapi uvicorn
# 添加开发依赖(类似 npm install -D)
poetry add --group dev pytest black ruff
# 安装所有依赖(类似 npm install)
poetry install
# 进入项目的虚拟环境(类似 npx)
poetry shell
# 直接运行命令(无需进入 shell)
poetry run python main.py
poetry run pytest
# 锁定依赖版本(类似 package-lock.json)
poetry lock
# 构建并发布到 PyPI(类似 npm publish)
poetry build
poetry publish
Poetry 的 poetry.lock 对应 npm 的 package-lock.json,pyproject.toml 对应 package.json。Poetry 还会自动创建和管理虚拟环境,省去了手动操作 venv 的步骤。
uv 的现代工作流
uv 不仅速度快,还提供了类似 Poetry 的完整工作流,且完全兼容标准 pyproject.toml。
# 初始化项目(创建 pyproject.toml)
uv init my-project
cd my-project
# 添加依赖
uv add fastapi uvicorn
# 添加开发依赖
uv add --dev pytest black ruff
# 运行脚本(自动使用项目虚拟环境)
uv run python main.py
# 运行测试
uv run pytest
# 同步依赖(根据 pyproject.toml 安装)
uv sync
# 生成锁定文件
uv lock
3.3 项目结构规范
良好的项目结构是代码可维护性的基础。就像前端项目有 src/、public/、components/ 等约定一样,Python 项目也有成熟的目录组织方式。
src layout vs flat layout
Python 项目主要有两种布局方式。前端开发者应该优先选择 src layout,因为它与前端项目的 src/ 目录习惯一致,且能避免很多导入问题。
# src layout(推荐)
my-project/
├── src/
│ └── my_project/ # 包目录(注意下划线命名)
│ ├── __init__.py # 包初始化文件
│ ├── models.py
│ ├── services/
│ │ ├── __init__.py
│ │ └── user_service.py
│ └── utils/
│ ├── __init__.py
│ └── helpers.py
├── tests/
│ ├── __init__.py
│ ├── test_models.py
│ └── test_services/
├── docs/
├── scripts/
├── pyproject.toml
├── README.md
├── .gitignore
└── .venv/
# flat layout(简单项目可用)
my-project/
├── my_project/ # 包直接在根目录
│ ├── __init__.py
│ └── ...
├── tests/
├── pyproject.toml
└── ...
__init__.py 的作用
__init__.py 是 Python 包的标志文件。它的存在告诉 Python:这个目录是一个包,可以导入。这与 JS 的 index.js 非常相似。
Python 的 __init__.py ≈ JS 的 index.js。两者都是包的入口点。不过 Python 3.3+ 支持隐式命名空间包(无 __init__.py 也能导入),但显式声明仍是最佳实践。
# src/my_project/__init__.py
"""My Project - 一个示例 Python 包.
类似 JS 的 index.js,控制包的公开 API。
"""
from .models import User, Post
from .services.user_service import UserService
# __all__ 定义 from my_project import * 时导出的内容
# 类似 JS 的 export { ... }
__all__ = ["User", "Post", "UserService"]
# 包级别的常量
VERSION = "0.1.0"
# src/my_project/services/__init__.py
"""服务层模块.
通过 __init__.py 重新导出,简化导入路径。
类似 JS 的 barrel export(桶式导出)。
"""
from .user_service import UserService
from .email_service import EmailService
__all__ = ["UserService", "EmailService"]
# 使用方式:
# from my_project.services import UserService
# 而不是:
# from my_project.services.user_service import UserService
Python .gitignore 模板
以下是 Python 项目的标准 .gitignore 配置,直接复制使用:
# Python
__pycache__/
*.py[cod]
*.so
.Python
*.egg-info/
dist/
build/
eggs/
*.egg
# 虚拟环境
.venv/
venv/
ENV/
env/
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# 环境变量
.env
.env.local
.env.*.local
# 测试和覆盖率
.pytest_cache/
.coverage
htmlcov/
.tox/
# 类型检查
.mypy_cache/
# 操作系统
.DS_Store
Thumbs.db
# Jupyter
.ipynb_checkpoints/
# 项目特定
logs/
*.log
data/
*.db
*.sqlite
你也可以使用 gitignore.io
在线生成,或者 VS Code 的 gitignore 扩展,一键创建 Python 项目模板。
3.4 代码质量工具
前端开发离不开 ESLint、Prettier、TypeScript,Python 生态也有对应的代码质量工具 trio:Black(格式化)、Ruff(Lint)、MyPy(类型检查)。
Black:毫不妥协的格式化工具
Black 被称为"毫不妥协的 Python 代码格式化工具",类似 Prettier。它几乎不提供配置选项,确保所有项目风格统一。
# 安装
pip install black
# 格式化单个文件
black main.py
# 格式化整个目录
black src/
# 检查格式(不修改,CI 中使用)
black --check src/
# 查看差异
black --diff src/
Black = Prettier。两者都主张"约定优于配置",几乎不可配置,确保团队协作时代码风格完全一致。
Ruff:极速 Lint 工具
Ruff 是用 Rust 编写的 Python Linter,速度比 flake8 + pylint 快数百倍,功能却更强大。它对应前端生态中的 ESLint。
# 安装
pip install ruff
# 检查代码(类似 eslint)
ruff check src/
# 自动修复可修复的问题(类似 eslint --fix)
ruff check --fix src/
# 检查并格式化(包含 black 功能)
ruff format src/
# 查看所有规则
ruff linter
# pyproject.toml 中的 Ruff 配置
[tool.ruff]
target-version = "py39" # 目标 Python 版本,类似 ESLint 的 env
line-length = 88
[tool.ruff.lint]
select = [
"E", # pycodestyle errors(类似 ESLint 的 error 规则)
"F", # Pyflakes(未使用变量等)
"I", # isort(导入排序,类似 ESLint 的 sort-imports)
"N", # pep8-naming(命名规范)
"W", # pycodestyle warnings
"UP", # pyupgrade(升级语法)
"B", # flake8-bugbear(潜在 Bug)
"C4", # flake8-comprehensions(推导式优化)
]
ignore = ["E501"] # 行过长由 Black 处理
[tool.ruff.lint.pydocstyle]
convention = "google" # 文档字符串风格
MyPy:静态类型检查
MyPy 是 Python 的静态类型检查器,类似 TypeScript 编译器的类型检查功能。它在运行前检查类型错误,大幅提升代码健壮性。
# 安装
pip install mypy
# 类型检查
mypy src/
# 严格模式(推荐)
mypy --strict src/
# 生成报告
mypy --html-report mypy-report src/
# pyproject.toml 中的 MyPy 配置
[tool.mypy]
python_version = "3.9"
strict = true # 严格模式,类似 TS 的 strict: true
warn_return_any = true # 返回 Any 时警告
warn_unused_ignores = true # 未使用的 type: ignore 警告
disallow_untyped_defs = true # 要求所有函数都有类型注解
ignore_missing_imports = true # 第三方库无类型存根时不报错
MyPy = TypeScript 编译器(tsc)。Python 的类型注解在运行时被忽略,MyPy 在开发/CI 阶段做静态检查。这类似于 TS 编译成 JS 时的类型擦除。
集成到工作流
将质量工具集成到 Git Hooks 和 CI 中,确保代码提交前自动检查。
# 安装 pre-commit
pip install pre-commit
# 创建 .pre-commit-config.yaml
cat > .pre-commit-config.yaml << 'EOF'
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.3.0
hooks:
- id: ruff
args: [--fix]
- id: ruff-format
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.9.0
hooks:
- id: mypy
EOF
# 安装钩子
pre-commit install
# 手动运行(测试配置)
pre-commit run --all-files
3.5 单元测试
前端有 Jest/Vitest,Python 有 pytest——功能强大、插件丰富、语法简洁的测试框架。
pytest 基础
pytest 的断言使用原生 Python assert,无需记忆各种 assertEqual、assertTrue 方法。
# tests/test_calculator.py
"""计算器模块的单元测试.
对比 JS:pytest ≈ Jest/Vitest,assert 比 expect().toBe() 更简洁。
"""
from src.my_project.calculator import add, divide
class TestCalculator:
"""测试类,类似 JS 的 describe() 分组。"""
def test_add_positive_numbers(self) -> None:
"""测试正数相加。类似 JS 的 it('should add positive numbers', ...)"""
result = add(2, 3)
assert result == 5 # 原生 assert,pytest 会自动提供详细错误信息
def test_add_negative_numbers(self) -> None:
"""测试负数相加。"""
assert add(-1, -2) == -3
def test_divide_by_zero(self) -> None:
"""测试除以零异常。类似 JS 的 expect(() => ...).toThrow()"""
with pytest.raises(ZeroDivisionError):
divide(10, 0)
def test_list_equality(self) -> None:
"""测试列表相等。"""
assert [1, 2, 3] == [1, 2, 3]
# 如果不相等,pytest 会显示详细差异,类似 Jest 的 diff
# 运行测试
pytest
# 显示详细输出
pytest -v
# 运行特定文件
pytest tests/test_calculator.py
# 运行特定测试类
pytest tests/test_calculator.py::TestCalculator
# 运行特定方法
pytest tests/test_calculator.py::TestCalculator::test_add
# 遇到第一个失败就停止(类似 npm test -- --bail)
pytest -x
# 失败时进入 PDB 调试
pytest --pdb
Fixture:测试依赖注入
pytest 的 fixture 机制类似 Jest 的 beforeEach + 依赖注入,用于准备测试数据和清理资源。
# tests/test_user_service.py
import pytest
from src.my_project.services.user_service import UserService
from src.my_project.models import User
@pytest.fixture
def user_service() -> UserService:
"""创建用户服务实例。
类似 Jest 的 setup() 或 Vitest 的 beforeEach(),
但 pytest 通过参数注入,更灵活。
"""
service = UserService()
yield service # yield 前是 setup,yield 后是 teardown
# 清理代码(类似 afterEach)
service.cleanup()
@pytest.fixture(scope="module")
def test_database() -> None:
"""模块级 fixture,整个测试模块只执行一次。
scope 选项:function(默认)、class、module、package、session
类似 Jest 的 beforeAll/afterAll。
"""
db = create_test_database()
yield db
db.drop_all()
class TestUserService:
def test_create_user(self, user_service: UserService) -> None:
"""fixture 通过参数名自动注入。"""
user = user_service.create_user(name="张三", email="zhang@example.com")
assert user.name == "张三"
assert user.id is not None
def test_get_user(self, user_service: UserService) -> None:
"""每个测试方法都会获得独立的 fixture 实例。"""
user = user_service.create_user(name="李四", email="li@example.com")
found = user_service.get_user(user.id)
assert found is not None
assert found.name == "李四"
参数化测试
用一组数据自动运行多次测试,类似 Jest 的 test.each。
# tests/test_parametrize.py
import pytest
from src.my_project.calculator import add
# 类似 Jest 的 test.each([
# [1, 1, 2],
# [2, 3, 5],
# ])('add(%i, %i) = %i', (a, b, expected) => { ... })
@pytest.mark.parametrize("a,b,expected", [
(1, 1, 2),
(2, 3, 5),
(-1, 1, 0),
(-1, -1, -2),
(0, 0, 0),
])
def test_add_parametrized(a: int, b: int, expected: int) -> None:
"""参数化测试:一组数据生成多个测试用例。"""
assert add(a, b) == expected
# 也可以参数化 fixture
@pytest.fixture(params=["sqlite", "postgresql", "mysql"])
def database(request: pytest.FixtureRequest) -> str:
"""为每种数据库类型运行一次测试。"""
return request.param
def test_database_connection(database: str) -> None:
"""这个测试会执行 3 次,分别对应 3 种数据库。"""
assert connect(database) is not None
测试覆盖率
使用 pytest-cov 生成覆盖率报告,类似前端生态中的 vitest --coverage。
# 安装
pip install pytest-cov
# 生成终端覆盖率报告
pytest --cov=src/my_project
# 生成 HTML 报告(在 htmlcov/ 目录查看)
pytest --cov=src/my_project --cov-report=html
# 生成 XML 报告(CI 工具使用)
pytest --cov=src/my_project --cov-report=xml
# 设置最低覆盖率门槛(低于则失败)
pytest --cov=src/my_project --cov-fail-under=80
3.6 类型注解
Python 3.5+ 引入了类型注解语法,3.10+ 大幅简化了语法。类型注解在运行时被忽略,但能让 IDE 提供更好的智能提示,并让 MyPy 进行静态检查。
基础类型注解
# src/my_project/typed_examples.py
"""Python 类型注解示例,附 TypeScript 对比。
Python 3.10+ 推荐使用 | 代替 Union,类似 TS 的联合类型。
"""
from typing import Optional, List, Dict, Callable, TypedDict
# ========== 基础类型 ==========
# Python
def greet(name: str) -> str:
return f"Hello, {name}"
# TypeScript 对比:
# function greet(name: string): string {
# return `Hello, ${name}`;
# }
# ========== 可选类型 ==========
# Python 3.10+ 推荐写法(| 语法)
def find_user(user_id: int) -> User | None:
...
# Python 3.9 及以前
# def find_user(user_id: int) -> Optional[User]:
# ...
# TypeScript 对比:
# function findUser(userId: number): User | null { ... }
# ========== 列表和字典 ==========
# Python 3.9+ 推荐写法
def process_items(items: list[str]) -> dict[str, int]:
return {item: len(item) for item in items}
# Python 3.8 及以前
# def process_items(items: List[str]) -> Dict[str, int]:
# ...
# TypeScript 对比:
# function processItems(items: string[]): Record { ... }
# ========== 函数类型 ==========
# Python
Handler = Callable[[int, str], bool]
def register_handler(handler: Handler) -> None:
...
# TypeScript 对比:
# type Handler = (a: number, b: string) => boolean;
# function registerHandler(handler: Handler): void { ... }
TypedDict:结构化字典
TypedDict 类似 TypeScript 的 interface,用于描述字典的结构。
from typing import TypedDict, NotRequired
# Python TypedDict
class UserConfig(TypedDict):
name: str
age: int
email: str
avatar: NotRequired[str] # 可选字段,类似 TS 的 avatar?: string
# TypeScript 对比:
# interface UserConfig {
# name: string;
# age: number;
# email: string;
# avatar?: string;
# }
def update_config(config: UserConfig) -> None:
print(config["name"]) # IDE 会提示可用键名
# config["nam"] # MyPy 报错:TypedDict "UserConfig" has no key 'nam'
# 使用
config: UserConfig = {
"name": "张三",
"age": 25,
"email": "zhang@example.com",
}
update_config(config)
Python 与 TypeScript 类型映射表
| TypeScript | Python 3.10+ | Python 3.9 |
|---|---|---|
string |
str |
str |
number |
int / float |
int / float |
boolean |
bool |
bool |
string[] |
list[str] |
List[str] |
Record<K, V> |
dict[K, V] |
Dict[K, V] |
T | null |
T | None |
Optional[T] |
(a: T) => U |
Callable[[T], U] |
Callable[[T], U] |
interface |
TypedDict / @dataclass |
TypedDict / @dataclass |
type A = B |
TypeAlias = B |
TypeAlias = B |
在 VS Code 中安装 Pylance 扩展,配合类型注解可获得媲美 TypeScript 的智能提示体验。设置 "python.analysis.typeCheckingMode": "basic" 即可开启实时类型检查。
Dataclass:带类型的数据类
Python 的 @dataclass 装饰器类似 TypeScript 的接口 + 自动构造函数,是定义数据模型的利器。
from dataclasses import dataclass
from datetime import datetime
# Python @dataclass
@dataclass
class User:
id: int
name: str
email: str
created_at: datetime = datetime.now()
is_active: bool = True
# TypeScript 对比(需要手动写构造函数):
# class User {
# constructor(
# public id: number,
# public name: string,
# public email: string,
# public createdAt: Date = new Date(),
# public isActive: boolean = true,
# ) {}
# }
# 使用
user = User(id=1, name="张三", email="zhang@example.com")
print(user.name) # 张三
# 自动获得 __repr__、__eq__ 等方法
print(user) # User(id=1, name='张三', ...)
Python 的类型注解在运行时完全不被解释器检查,它们只是给 IDE 和 MyPy 看的"提示"。这与 TypeScript 编译时类型检查类似,但 Python 不会在编译阶段报错——你需要主动运行 MyPy 来捕获类型错误。
3.7 GitHub Actions CI/CD 集成
将代码质量检查集成到 CI 流水线是工程化的最后一步。以下是一个完整的 GitHub Actions 配置, 包含 pytest 测试、Ruff 代码检查和 MyPy 类型检查:
# .github/workflows/ci.yml
name: Python CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- name: 设置 Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: 安装依赖
run: |
python -m pip install --upgrade pip
pip install ruff pytest pytest-cov mypy
pip install -e ".[dev]"
- name: Ruff 代码检查
run: ruff check src/ tests/
- name: MyPy 类型检查
run: mypy src/ --ignore-missing-imports
- name: pytest 测试 + 覆盖率
run: pytest --cov=src --cov-report=xml --cov-fail-under=80
- name: 上传覆盖率报告
uses: codecov/codecov-action@v4
with:
file: ./coverage.xml
这个 CI 配置相当于前端的:npm run lint && npm run type-check && npm test -- --coverage。
GitHub Actions 的 matrix strategy 等同于在多个 Node.js 版本下运行 CI。
3.8 EditorConfig 配置
.editorconfig 帮助不同编辑器(VS Code、PyCharm、Vim 等)保持一致的编码风格。
这是团队协作中一个简单但极其有效的工具:
# .editorconfig
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.py]
indent_style = space
indent_size = 4
[*.{html,css,js,json,yml,yaml,toml}]
indent_style = space
indent_size = 2
[Makefile]
indent_style = tab
[*.md]
trim_trailing_whitespace = false
VS Code 需要安装 EditorConfig for VS Code 扩展才能自动读取此配置。
大多数其他编辑器(PyCharm、Sublime、Vim)默认支持。