一些项目开发工具与技巧
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/CD或CICD通常指的是持续集成(英语: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 .