一些项目开发工具与技巧

By ycshi & Gemini 2.5 Pro

一、规范化的项目结构与依赖管理

1.1 目录结构

以一个假想的药物属性预测项目`MolPropPred` 为例,建立如下结构:
  • MolPropPred/
  • ├── .github/                  # GitHub Actions CI/CD 配置文件
  • │   └── workflows/
  • │       └── ci.yml
  • ├── configs/                  # Hydra 配置文件
  • │   ├── config.yaml           # 主配置文件
  • │   ├── model/                # 模型相关配置
  • │   │   └── gnn.yaml
  • │   └── trainer/              # 训练器相关配置
  • │       └── default.yaml
  • ├── data/                     # 原始数据和处理后的数据
  • │   ├── raw/
  • │   └── processed/
  • ├── docs/                     # 项目文档
  • ├── src/                      # 项目核心源代码
  • │   └── molprop_pred/         # 包名
  • │       ├── __init__.py
  • │       ├── data_utils.py     # 数据处理模块
  • │       ├── models.py         # 模型定义模块
  • │       ├── train.py          # 训练脚本
  • │       └── utils.py          # 通用工具函数
  • ├── tests/                    # 测试代码
  • │   ├── test_data_utils.py
  • │   └── test_utils.py
  • ├── .gitignore                # Git忽略文件配置
  • ├── pyproject.toml            # 项目元数据和依赖管理
  • └── README.md                 # 项目说明书
将所有源代码放在一个`src`目录下的包内是一种现代Python项目的最佳实践,可以避免很多导入问题。

1.2 依赖管理:`pyproject.toml`

相比于`requirements.txt`,现代Python项目倾向于使用`pyproject.toml`来统一管理项目信息。
e.g.:
  • # pyproject.toml

  • [project]
  • name = "molprop_pred"
  • version = "0.1.0"
  • description = "A toy project for predicting molecular properties."
  • requires-python = ">=3.10"
  • dependencies = [
  •     "torch==2.1.0",
  •     "pandas",
  •     "pyyaml",
  •     "rdkit-pypi", 
  •     "hydra-core==1.3.2",
  •     "pytest",
  •     "pytest-cov",
  • ]

  • [project.optional-dependencies]
  • dev = [
  •     "black",
  •     "ruff",
  • ]
其优势在于可以在项目根目录运行 `pip install -e .`直接本地安装到环境中。

二、`pytest`&`coverage`

2.1 使用`pytest`进行测试

假设项目的`src/molprop_pred/utils.py`中有一个函数,其功能是简单地标准化SMILES字符串:
  • # src/molprop_pred/utils.py
  • from rdkit import Chem

  • def canonicalize_smiles(smiles: str) -> str:
  •     """Converts a SMILES string to its canonical form."""
  •     mol = Chem.MolFromSmiles(smiles)
  •     if mol is None:
  •         raise ValueError(f"Invalid SMILES string: {smiles}")
  •     return Chem.MolToSmiles(mol, canonical=True)
可为它编写一个测试脚本`tests/test_utils.py`
  • # tests/test_utils.py
  • import pytest
  • from molprop_pred.utils import canonicalize_smiles

  • def test_canonicalize_smiles_valid():
  •     """Tests that a valid, non-canonical SMILES is canonicalized."""
  •     # O=C(C)N 应该被标准化为 CC(=O)N
  •     assert canonicalize_smiles("O=C(C)N") == "CC(=O)N"

  • def test_canonicalize_smiles_already_canonical():
  •     """Tests that an already canonical SMILES remains unchanged."""
  •     assert canonicalize_smiles("CC(=O)N") == "CC(=O)N"

  • def test_canonicalize_smiles_invalid():
  •     """Tests that an invalid SMILES raises a ValueError."""
  •     with pytest.raises(ValueError):
  •         canonicalize_smiles("This is not a valid smiles")
然后在项目根目录运行
  • pytest
即可。`pytest`会自动发现并运行 `tests/` 目录下所有 `test_*.py` 文件中的 `test_*` 函数。

2.2 使用 `coverage` 衡量测试覆盖率

  • # 1. 通过 coverage 运行 pytest
  • coverage run -m pytest
  • # 2. 查看命令行报告
  • coverage report -m
输出信息形如:
  • Name                           Stmts   Miss  Cover   Missing
  • ------------------------------------------------------------
  • src/molprop_pred/__init__.py       1      0   100%
  • src/molprop_pred/utils.py          5      0   100%
  • ------------------------------------------------------------
  • TOTAL                              6      0   100%

三、使用`Hydra`进行配置管理

一个典型的train.py:
  • # train_before_hydra.py

  • # --- 超参数硬编码 ---
  • LEARNING_RATE = 0.001
  • BATCH_SIZE = 32
  • MODEL_NAME = "GNN"
  • NUM_LAYERS = 4
  • DATA_PATH = "/path/to/my/data.csv"

  • def main():
  •     print(f"Starting training with LR={LEARNING_RATE}...")
  •     # ... 训练逻辑 ...

  • if __name__ == "__main__":
  •     main()
当需要修改超参数时,就需要手动修改代码。
使用`Hydra`可以较为方便地管理配置。在`configs/`目录下创建`config.yaml`
  • # 主配置文件,定义默认值和结构
  • defaults:
  •   - model: gnn
  •   - trainer: default

  • data_path: "/path/to/default/data.csv"
创建`configs/model/gnn.yaml`
  • # gnn 模型的配置
  • name: GNN
  • num_layers: 4
  • emb_dim: 128
创建`configs/trainer/default.yaml`
  • # 训练器的配置
  • learning_rate: 0.001
  • batch_size: 32
  • epochs: 100
接着,在训练脚本中使用`@hydra.main`装饰器:
  • import hydra
  • from omegaconf import DictConfig

  • @hydra.main(config_path="../../configs", config_name="config", version_base=None)
  • def main(cfg: DictConfig):
  •     # cfg 对象包含了所有配置信息
  •     print(f"Model Name: {cfg.model.name}")
  •     print(f"Learning Rate: {cfg.trainer.learning_rate}")
  •     print(f"Data Path: {cfg.data_path}")
  •     
  •     # ... 训练逻辑 ...

  • if __name__ == "__main__":
  •     main()
这样即可从命令行快速切换配置:
  • # 使用默认配置运行
  • python train.py

  • # 覆盖学习率和批大小
  • python train.py trainer.learning_rate=0.01 trainer.batch_size=64

  • # 切换模型配置(假设有 model/transformer.yaml)
  • # python train.py model=transformer

  • # 覆盖数据路径
  • python train.py data_path="/path/to/another/dataset.csv"
`Hydra`还会自动创建带时间戳的输出目录,并将本次运行的配置快照保存下来,解决了实验可复现性问题。

四、使用`Github Actions`服务进行CI/CD

来自Wikipedia的定义:
  • 在软件工程中,CI/CDCICD通常指的是持续集成(英语:continuous integration)和持续交付(英语:continuous delivery)或持续部署(英语:continuous deployment)的组合实践。
GitHub Actions 是 GitHub 提供的一项免费(对公共repo)服务,帮助开发者在 GitHub 上实现 CI/CD。
首先在`.github/workflows/ci.yml`中定义workflow:
  • # 一个示例 ci.yml 文档
  • name: MolPropPred CI

  • on:
  •   push:
  •     branches: [ main ]
  •   pull_request:
  •     branches: [ main ]

  • jobs:
  •   test:
  •     runs-on: ubuntu-latest
  •     strategy:
  •       matrix:
  •         python-version: ["3.9", "3.10"]

  •     steps:
  •       - name: 1. Checkout repository
  •         uses: actions/checkout@v4

  •       - name: 2. Set up Python ${{ matrix.python-version }}
  •         uses: actions/setup-python@v5
  •         with:
  •           python-version: ${{ matrix.python-version }}

  •       - name: 3. Install Conda and RDKit
  •         uses: conda-incubator/setup-miniconda@v3
  •         with:
  •           auto-update-conda: true
  •           python-version: ${{ matrix.python-version }}
  •           channels: conda-forge
  •           channel-priority: strict
  •           activate-environment: test-env
  •           dependencies: >-
  •             rdkit

  •       - name: 4. Configure pip cache
  •         uses: actions/cache@v4
  •         with:
  •           path: ~/.cache/pip
  •           key: ${{ runner.os }}-pip-${{ hashFiles('**/pyproject.toml') }}
  •           restore-keys: |
  •             ${{ runner.os }}-pip-

  •       - name: 5. Install Python dependencies
  •         shell: bash -l {0}
  •         run: |
  •           pip install -e '.[dev]'

  •       - name: 6. Run tests with coverage
  •         shell: bash -l {0}
  •         run: |
  •           coverage run -m pytest
  •           coverage report -m --fail-under=70 # 设置覆盖率阈值,低于70%则CI失败
`main` 分支有 `push``pull_request` 时会触发这个CI workflow,在一个全新的环境中安装依赖并进行测试。
当把新改动`push`到Github后,`Actions`标签页下就会出现自动进行的测试。
在多人开发一个项目的情形下,当CI显示✅通过后,就可以请协作者review一下代码,然后进行`merge`

五、其他

使用`black`统一代码风格

  • # 安装
  • pip install black

  • # 格式化整个项目
  • black .