第 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 运行时和包目录,彻底解决这个问题。

ℹ️与 JS 的对比

Python 的虚拟环境 ≈ 前端项目中的 node_modules + .nvmrc(指定 Node 版本)。venv 创建隔离环境,类似 npm 管理依赖。

venv:Python 内置方案

Python 3.3+ 内置了 venv 模块,无需额外安装。

bash
# 创建虚拟环境(在项目根目录执行)
python -m venv .venv

# Windows 激活
.venv\Scripts\activate

# macOS/Linux 激活
source .venv/bin/activate

# 激活后,命令行提示符前会出现 (.venv)
# 退出虚拟环境
deactivate

conda:数据科学首选

如果你从事数据科学或 AI 方向,conda 是更好的选择。它不仅管理 Python 包,还能管理非 Python 依赖(如 CUDA、MKL)。

bash
# 创建环境并指定 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 年以来最值得关注的新工具。

bash
# 安装 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 的简化版。

text
# 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
bash
# 生成当前环境的依赖列表
pip freeze > requirements.txt

# 安装依赖
pip install -r requirements.txt

pyproject.toml:现代标准

PEP 518 和 PEP 621 确立了 pyproject.toml 作为 Python 项目的标准配置文件。它类似前端的 package.json + tsconfig.json + .eslintrc 的合体。

toml
# 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 的开发体验,包括依赖锁定、虚拟环境自动管理、发布打包等功能。

bash
# 安装 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
ℹ️与 JS 的对比

Poetry 的 poetry.lock 对应 npm 的 package-lock.jsonpyproject.toml 对应 package.json。Poetry 还会自动创建和管理虚拟环境,省去了手动操作 venv 的步骤。

uv 的现代工作流

uv 不仅速度快,还提供了类似 Poetry 的完整工作流,且完全兼容标准 pyproject.toml

bash
# 初始化项目(创建 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/ 目录习惯一致,且能避免很多导入问题。

text
# 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 非常相似。

ℹ️与 JS 的对比

Python 的 __init__.py ≈ JS 的 index.js。两者都是包的入口点。不过 Python 3.3+ 支持隐式命名空间包(无 __init__.py 也能导入),但显式声明仍是最佳实践。

python
# 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"
python
# 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 配置,直接复制使用:

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。它几乎不提供配置选项,确保所有项目风格统一。

bash
# 安装
pip install black

# 格式化单个文件
black main.py

# 格式化整个目录
black src/

# 检查格式(不修改,CI 中使用)
black --check src/

# 查看差异
black --diff src/
ℹ️与 JS 的对比

Black = Prettier。两者都主张"约定优于配置",几乎不可配置,确保团队协作时代码风格完全一致。

Ruff:极速 Lint 工具

Ruff 是用 Rust 编写的 Python Linter,速度比 flake8 + pylint 快数百倍,功能却更强大。它对应前端生态中的 ESLint。

bash
# 安装
pip install ruff

# 检查代码(类似 eslint)
ruff check src/

# 自动修复可修复的问题(类似 eslint --fix)
ruff check --fix src/

# 检查并格式化(包含 black 功能)
ruff format src/

# 查看所有规则
ruff linter
toml
# 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 编译器的类型检查功能。它在运行前检查类型错误,大幅提升代码健壮性。

bash
# 安装
pip install mypy

# 类型检查
mypy src/

# 严格模式(推荐)
mypy --strict src/

# 生成报告
mypy --html-report mypy-report src/
toml
# 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    # 第三方库无类型存根时不报错
ℹ️与 JS 的对比

MyPy = TypeScript 编译器(tsc)。Python 的类型注解在运行时被忽略,MyPy 在开发/CI 阶段做静态检查。这类似于 TS 编译成 JS 时的类型擦除。

集成到工作流

将质量工具集成到 Git Hooks 和 CI 中,确保代码提交前自动检查。

bash
# 安装 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,无需记忆各种 assertEqualassertTrue 方法。

python
# 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
bash
# 运行测试
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 + 依赖注入,用于准备测试数据和清理资源。

python
# 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

python
# 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

bash
# 安装
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 进行静态检查。

基础类型注解

python
# 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,用于描述字典的结构。

python
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 的接口 + 自动构造函数,是定义数据模型的利器。

python
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 类型检查:

yaml
# .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 等)保持一致的编码风格。 这是团队协作中一个简单但极其有效的工具:

ini
# .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)默认支持。