跳转至

第 10 章 · 工程化基础

Java 工程师习惯了 Maven/Gradle 的"一站式"体验:一个 pom.xml 声明依赖、构建工具管 classpath、IDE 集成一切。Python 的工程化历史上很碎片化,但已收敛到 pyproject.toml + 虚拟环境的现代标准。本章两条路并行:路线一(主推)uv——最接近 Maven/Gradle 的现代统一工具;路线二 pip + venv——最通用、企业环境最常见。


10.1 为什么要虚拟环境

Java 每个 Maven 项目靠依赖坐标 + 本地仓库 ~/.m2 隔离,全局一个 JDK。Python 不同:第三方包装进解释器的 site-packages,默认全局共享。如果所有项目共用一个全局环境,A 项目要 django==3、B 项目要 django==5 就冲突了。

虚拟环境(virtual environment) = 一个项目专属的、独立的 Python 环境(独立 site-packages)。每个项目一个,互不干扰。

  • Java 心智

    全局 JDK
      └─ Maven 本地仓库 ~/.m2(依赖按坐标隔离)
    
  • Python 心智

    全局解释器(干净,不装业务包)
      ├─ 项目A/.venv(独立 site-packages)
      └─ 项目B/.venv(独立 site-packages)
    

10.2 路线一:uv(主推,现代统一工具)

uv 是 Rust 写的、极快的现代工具链,把 venv + pip + pip-tools + 甚至 poetry 的依赖管理统一到一个命令——最接近你熟悉的 Maven/Gradle。

# 安装(若尚未安装)
pip install uv            # 或用官方安装脚本

# 新项目
uv init myproject         # 生成 pyproject.toml 等
cd myproject

# 管理依赖
uv add requests           # 加依赖(自动写进 pyproject.toml、装进 .venv、更新 uv.lock)
uv add --dev pytest       # 开发依赖
uv remove requests        # 移除

# 同步环境(按 pyproject.toml + uv.lock 复现环境)
uv sync                   # 别人 clone 后第一条命令

# 运行
uv run python main.py     # 在项目环境里运行
uv run pytest             # 在项目环境里跑测试

uv.lock 相当于 Maven 的锁定版本——保证团队/CI 装出完全一致的依赖版本。

为什么主推 uv

一个工具搞定"环境创建 + 依赖声明 + 版本锁定 + 运行",速度快(比 pip 快 10-100 倍),心智模型最接近 Maven/Gradle。2026 年已是社区主流推荐。


10.3 路线二:pip + venv(传统,最通用)

没有 uv 的环境(企业受限网络、CI 基础镜像)用标准库自带的 venv + pip

# 1. 创建虚拟环境
python -m venv .venv

# 2. 激活(Windows PowerShell / bash)
.venv\Scripts\activate           # Windows CMD
.venv/Scripts/activate           # Windows Git Bash / source 形式
source .venv/bin/activate        # macOS/Linux

# 3. 装依赖
pip install requests pytest
pip install -r requirements.txt

# 4. 冻结当前版本(生成 requirements.txt)
pip freeze > requirements.txt

requirements.txt 是传统的依赖清单(每行一个 包==版本)。激活后命令行提示符会出现 (.venv) 前缀,表示当前在虚拟环境里。

⚠️ Java 程序员的陷阱

  • 别用 pip install 装到全局解释器——养成"先建/激活 .venv 再装"的习惯。
  • requirements.txt扁平列表,不像 pom.xml 那样有依赖树/坐标体系;要可复现的锁定,pip 生态有 pip-tools 生成 requirements.lock。这也是 uv 的优势之一。

10.4 pyproject.toml:Python 的 pom.xml

pyproject.toml(PEP 621)是现代 Python 项目的唯一标准配置文件,取代老的 setup.py

[project]
name = "myapp"
version = "0.1.0"
description = "My app"
requires-python = ">=3.13"
dependencies = [
    "requests>=2.32",
    "pydantic>=2",
]

[project.optional-dependencies]
dev = ["pytest>=8", "mypy>=1.10", "ruff>=0.5"]

[project.scripts]
myapp = "myapp.cli:main"        # 安装后提供 `myapp` 命令(贯穿项目会用)

# 各工具的配置也放这里:
[tool.mypy]
python_version = "3.13"
strict = true

[tool.pytest.ini_options]
testpaths = ["tests"]

[tool.ruff]
line-length = 100

对照:

Java Python
项目元数据 + 依赖 pom.xml / build.gradle pyproject.toml
锁定版本 Gradle lock / Maven enforcer uv.lock
可执行入口 main-class / application 插件 [project.scripts]

10.5 项目结构:src 布局

推荐 src layout(避免测试意外导入到本地源码、更接近安装后的结构):

myapp/
├── pyproject.toml
├── uv.lock
├── src/
│   └── myapp/
│       ├── __init__.py
│       ├── cli.py
│       └── core.py
└── tests/
    ├── __init__.py
    └── test_core.py

10.6 测试:pytest(vs JUnit)

pytest 是 Python 事实标准的测试框架,比 unittest(标准库,类 JUnit 风格)更简洁:

  • Java JUnit

    @Test
    void addWorks() {
        assertEquals(3, add(1, 2));
    }
    
  • Python pytest

    def test_add():
        assert add(1, 2) == 3   # 直接 assert!
    

三个关键差异

  1. 用裸 assert,不用 assertEquals/assertThat——失败时 pytest 自动给出详细对比。
  2. 测试是普通函数,不用类(也可用类组织)。
  3. 文件名 test_*.py、函数名 test_* 即被自动发现。

fixture(vs @Before

import pytest

@pytest.fixture
def sample_user():
    return User("Alice")

def test_greet(sample_user):          # 参数名匹配 fixture,自动注入
    assert sample_user.name == "Alice"

参数化(vs @ParameterizedTest

@pytest.mark.parametrize("a, b, expected", [
    (1, 2, 3),
    (10, 5, 15),
    (-1, 1, 0),
])
def test_add(a, b, expected):
    assert add(a, b) == expected

运行:

uv run pytest                  # 跑全部
uv run pytest tests/test_core.py -v       # 指定文件、详细
uv run pytest -k "add"         # 按名字过滤

Pythonic 写法

  • 测试函数依赖注入靠 fixture(conftest.py 里放共享 fixture),比 JUnit 的 @Before 灵活。
  • 断言失败信息由 pytest 自动美化,不需要手写消息。

10.7 调试与格式化

调试:breakpoint()

def buggy(data):
    total = sum(data)
    breakpoint()        # 3.7+,运行到这里进入交互式调试器(pdb)
    return total / len(data)

进入后可用 n(下一步)、s(步入)、p var(打印)、c(继续)、l(看代码)。比 Java 的 System.out.println 调试强大,但 IDE(PyCharm/VSCode)的图形调试器更友好。

Lint + 格式化:ruff

ruff(Rust 写,极快)一个工具取代 flake8 + black + isort,相当于 Java 的 Checkstyle + Spotless:

uv run ruff check .          # 检查代码规范
uv run ruff format .         # 格式化(类似 black)
uv run ruff check --fix .    # 自动修复

本章练习

练习 10.1

说明"在全局 Python 里 pip install 业务依赖"为什么不推荐,以及虚拟环境如何解决。

参考答案

全局 site-packages 被所有项目共享,会导致:不同项目依赖同一库的不同版本时冲突、升级一个库破坏另一个项目、难以复现环境。虚拟环境给每个项目一份独立的 site-packages,依赖彼此隔离,且可通过 requirements.txt/uv.lock 精确复现。

练习 10.2

用 uv 从零建一个项目 calc:加 pytest 为开发依赖,写一个 add 函数和它的测试,运行测试通过。

参考答案

uv init calc && cd calc
uv add --dev pytest
# src/calc/core.py
def add(a: int, b: int) -> int: return a + b
# tests/test_core.py
from calc.core import add
def test_add(): assert add(2, 3) == 5
uv run pytest
(注:src 布局下 pytest 需能导入包,uv run 会以可编辑模式装好本项目。)

练习 10.3

把下面 JUnit 风格测试改写成 pytest 风格(裸 assert + 参数化):

@ParameterizedTest
@CsvSource({"1,2,3", "10,5,15"})
void addWorks(int a, int b, int expected) {
    assertEquals(expected, add(a, b));
}

参考答案
import pytest

@pytest.mark.parametrize("a, b, expected", [
    (1, 2, 3),
    (10, 5, 15),
])
def test_add(a, b, expected):
    assert add(a, b) == expected

上一章:第 9 章 · 标准库对照速查 | 下一章:第 11 章 · 并发模型