点我展开:基本信息

提升class的可读性、可维护性、可测试性、可扩展性,贴近大厂工程标准。

这是一个非常好的问题,而且你现在这个阶段(研究生做实验 + pipeline 项目)问它,说明你已经开始从“写代码”走向“做研究型工程”了 👍
下面我给你一个通用、可复用、适合科研实验的完整 pipeline 项目代码结构,并解释每一层的设计思想,不是只给目录树。


一、先说结论:一个「合格的科研 pipeline 项目」要解决什么?

一个完整的实验 pipeline,通常要解决这 5 件事:

  1. 数据可复现:原始数据、处理流程、特征工程清晰
  2. 实验可对比:不同模型 / 参数 / 方法可以系统对比
  3. 代码可维护:不是一次性脚本,而是模块化
  4. 结果可追溯:日志、配置、结果一一对应
  5. 论文友好:能快速复现实验、画图、导出结果

代码结构的设计,本质就是围绕这 5 点。


二、推荐的「标准科研 pipeline 项目结构」

下面是一个你可以直接照着用的结构(偏 ML / 算法实验,深度学习也适用):

project_name/
│
├── README.md                 # 项目说明(实验目的、方法、运行方式)
├── requirements.txt          # Python 依赖(或 environment.yml)
├── setup.py                  # 可选:做成可安装包
│
├── configs/                  # ⭐ 实验配置(非常重要)
│   ├── default.yaml
│   ├── model/
│   │   ├── model_a.yaml
│   │   └── model_b.yaml
│   ├── dataset/
│   │   └── dataset1.yaml
│   └── experiment/
│       └── exp1.yaml
│
├── data/                     # ⭐ 数据相关
│   ├── raw/                  # 原始数据(只读)
│   ├── processed/            # 预处理后的数据
│   └── splits/               # train/val/test 划分
│
├── src/                      # ⭐ 核心代码
│   ├── __init__.py
│   │
│   ├── datasets/             # 数据加载 & 预处理
│   │   ├── __init__.py
│   │   ├── dataset1.py
│   │   └── transforms.py
│   │
│   ├── models/               # 模型定义
│   │   ├── __init__.py
│   │   ├── model_a.py
│   │   └── model_b.py
│   │
│   ├── training/             # 训练逻辑
│   │   ├── __init__.py
│   │   ├── trainer.py
│   │   └── losses.py
│   │
│   ├── evaluation/           # 评估指标
│   │   ├── __init__.py
│   │   └── metrics.py
│   │
│   ├── utils/                # 工具函数
│   │   ├── logger.py
│   │   ├── seed.py
│   │   └── config.py
│   │
│   └── pipelines/            # ⭐ 串起整个流程
│       ├── train_pipeline.py
│       └── eval_pipeline.py
│
├── scripts/                  # ⭐ 实验入口(命令行)
│   ├── train.py
│   ├── evaluate.py
│   └── preprocess.py
│
├── experiments/              # ⭐ 实验输出(可追溯)
│   ├── exp_001/
│   │   ├── config.yaml
│   │   ├── logs/
│   │   ├── checkpoints/
│   │   └── results.json
│   └── exp_002/
│
├── notebooks/                # Jupyter(分析 / 画图)
│   └── analysis.ipynb
│
├── results/                  # ⭐ 汇总结果(画论文图)
│   ├── tables/
│   └── figures/
│
└── tests/                    # 单元测试(加分项)
    └── test_datasets.py

三、逐层解释(这是重点)

1️⃣ configs/ —— 科研项目的灵魂

原则:代码不写死参数

# configs/experiment/exp1.yaml
dataset: dataset1
model: model_a
epochs: 100
lr: 1e-3
seed: 42

好处:

  • 不同实验 = 不同 config
  • 论文复现实验:只要 config
  • 方便 grid search / ablation

👉 强烈建议你从第一天就用 config


2️⃣ data/ —— 保证“原始数据不可污染”

data/raw/        # 永远不改
data/processed/  # 由脚本生成

这是科研中最容易犯错的地方。
审稿人常问的:

“你的数据预处理是否影响了测试集?”

这个结构可以直接回答。


3️⃣ src/ —— 真正的 pipeline 核心

datasets/

只负责:

  • 读数据
  • 返回标准格式(X, y)

❌ 不训练
❌ 不评估


models/

只做一件事:

  • 定义模型结构
class ModelA(nn.Module):
    ...

training/

  • optimizer
  • loss
  • 训练循环

不要把训练逻辑写进 model


evaluation/

  • 指标:accuracy、F1、RMSE……
  • 不依赖训练细节

pipelines/(很多学生没有,但这是进阶)

# train_pipeline.py
load_config()
set_seed()
load_data()
build_model()
train()
evaluate()
save_results()

👉 pipeline = 方法论本身


4️⃣ scripts/ —— 实验入口(像开关)

python scripts/train.py --config configs/experiment/exp1.yaml
  • scripts 很薄
  • 真正逻辑在 src/

这是工业 & 顶会代码的共同点


5️⃣ experiments/ —— 为“可追溯性”而生

experiments/exp_001/
├── config.yaml
├── logs/
├── checkpoints/
└── results.json

任何结果都能回答:

“这是哪次实验?用的什么参数?”


6️⃣ notebooks/ —— 只做分析,不做 pipeline

原则:

  • notebook = 分析 / 可视化
  • 不作为唯一运行方式

四、不同研究方向的小调整

🔹 偏算法 / 理论实验

  • models/ 可以换成 algorithms/
  • pipeline 强调不同算法对比

🔹 深度学习

  • 增加 callbacks/
  • 增加 checkpoints/ 管理

🔹 多实验对比(消融)

  • 增加 sweeps/ablation/

五、导师和审稿人眼中:什么是“好结构”?

✅ 参数可配置
✅ 实验可复现
✅ 逻辑模块清晰
❌ 一堆 train_v3_final_final.py

你这个问题,本身就已经在往高质量科研靠拢了


六、我可以继续帮你的地方

如果你愿意,我可以:

  1. 🔧 根据你的具体方向(如 ML / CV / NLP / 算法)定制结构
  2. 📦 给你一个 可直接用的模板仓库结构
  3. 🧪 帮你把你现在“零散的实验代码”重构成 pipeline
  4. ✍️ 从代码结构角度 反推论文实验部分怎么写

你可以直接告诉我:
👉 你的研究方向 + 使用语言(Python/C++)+ 是否深度学习

代码对比

下面有两份代码,修改前与修改后,通过对比来理解相关修改处的作用。

下面为一个class中的方法,修改前:

    def identify_specific_tables(
        self, 
        documentation_file_path: str,
    ) -> str:
        logger.info("Extract the Word standard table extraction")

        doc = Document(documentation_file_path)
        tables = []
        for table in doc.tables:
            data = []
            for row in table.rows:
                row_data = [cell.text.strip() for cell in row.cells]
                data.append(row_data)
            df = pd.DataFrame(data)
            tables.append(df)

        tables_str = [df.to_string() for df in tables]
        prompt = f"""
        以下用三个反引号分隔的是从“云南电网有限责任公司安全文明施工费使用管理标准(试行).docx”Word文档中提取的所有表格,\
        Word文档提取表格集合:'''{tables_str}'''\
        各个表格以DataFrame格式存储在一个列表中,遍历所有所有表格,提取出特定表格“电网工程建设安全文明施工费使用指导模板”,并输出提取出的表格,
        该表格的特征如下:

        1. 该表格包含['费用类型及取费建议', '措施类别及使用范围', '措施类别编码', '是否可摊销', '变电', '线路', '配网'],7列指标;
        2. 其中'费用类型及取费建议'包含['安全生产费', '文明施工费', '环境保护费']三类费用信息;

        注意:
        - 直接返回表格信息,不要输出实现该功能的python代码;

        返回说明:返回特定表格的信息,不要求文字说明。
        """

        messages = [
            {"role": "user", "content": prompt}
        ]
        response = llm_client.chat(
            model=LLM_MODEL,
            messages=messages,
            stream=False,  # 不启用流式输出
        )
        return response.choices[0].message.content.strip()

将其单列出来作为一个class进行修改,修改后:

import logging
from typing import List

import pandas as pd
from docx import Document
from openai import OpenAI

from core.config import LLM_MODEL
from utils.logger import logger

# 如果 llm_client 已经在别处初始化,则直接导入;否则在此处初始化
# from clients.llm_client import llm_client


class DocumentationExtractor:
    """
    从 Word 文档中提取并识别特定表格的通用工具类。
    """

    @staticmethod
    def _extract_all_tables(file_path: str) -> List[pd.DataFrame]:
        """
        读取 Word 文档中的所有表格,并统一转换为 pandas.DataFrame 列表。

        Args:
            file_path (str): Word 文档绝对路径。

        Returns:
            List[pd.DataFrame]: 文档中所有表格对应的 DataFrame 列表。
        """
        doc = Document(file_path)
        tables: List[pd.DataFrame] = []

        for table in doc.tables:
            rows = [
                [cell.text.strip() for cell in row.cells]
                for row in table.rows
            ]
            tables.append(pd.DataFrame(rows))

        return tables

    @classmethod
    def identify_specific_table(
        cls,
        file_path: str,
        *,
        expected_columns: List[str],
        expected_fee_types: List[str],
    ) -> str:
        """
        识别并返回指定特征的安全文明施工费使用指导模板表格。

        说明:
        - 该方法为大模型调用入口,所有业务特征通过参数传入,保持通用性。
        - 仅返回表格纯文本,不包含任何代码或额外说明。

        Args:
            file_path (str): Word 文档路径,必需为绝对路径。
            expected_columns (List[str]): 目标表必须包含的列名列表。
            expected_fee_types (List[str]): “费用类型及取费建议”列必须包含的费用类别列表。

        Returns:
            str: 符合特征的表格文本;若未匹配则返回空字符串。
        """
        logger.info(
            "Start extracting specific table from Word document",
            extra={"file_path": file_path},
        )

        # 1. 提取所有表格
        tables = cls._extract_all_tables(file_path)
        tables_str = "\n\n".join(df.to_string(index=False) for df in tables)

        # 2. 构造大模型 prompt
        prompt = f"""
以下是从 Word 文档中提取的所有表格内容,已按 DataFrame 格式拼接:

{tables_str}

请依据以下规则,在所有表格中精确匹配并返回“电网工程建设安全文明施工费使用指导模板”:

规则:
1. 表格必须包含以下列(顺序无关):
   {", ".join(expected_columns)}
2. “费用类型及取费建议”列必须包含以下值(顺序无关):
   {", ".join(expected_fee_types)}
3. 仅返回匹配到的完整表格文本,不要附加任何解释或代码。
4. 若未匹配到,请返回空字符串。
""".strip()

        messages = [{"role": "user", "content": prompt}]

        # 3. 调用大模型
        response = llm_client.chat.completions.create(
            model=LLM_MODEL,
            messages=messages,
            temperature=0.0,
            stream=False,
        )

        result = response.choices[0].message.content.strip()
        logger.info("Successfully identified specific table", extra={"length": len(result)})
        return result


# 使用示例
if __name__ == "__main__":
    content = DocumentationExtractor.identify_specific_table(
        file_path="/abs/path/云南电网有限责任公司安全文明施工费使用管理标准(试行).docx",
        expected_columns=[
            "费用类型及取费建议",
            "措施类别及使用范围",
            "措施类别编码",
            "是否可摊销",
            "变电",
            "线路",
            "配网",
        ],
        expected_fee_types=["安全生产费", "文明施工费", "环境保护费"],
    )
    print(content)

修改分析

主要分为模块导入,...,对demo_class.py编写进行规范。

导入模块

核心要求是:标准库 → 第三方库 → 本地模块,各分组之间空一行,每组内部按字母序升序排列,绝对禁止 import *,class与导入模块相隔2行。

import logging
from typing import List

import pandas as pd
from docx import Document
from openai import OpenAI

from core.config import LLM_MODEL
from utils.logger import logger

注:不能一行导入多个包。

命名与可见性

主要对成员实例变量、方法、常量进行规范。前者为共有,后者为内部/私有。

  • 实例变量:user_name,_user_name
  • 方法:compute_total(),_compute_total()
  • 常量:MAX_RETRY,_MAX_RETRY

类方法

类方法分为@staticmethod与@classmethod,前者可以理解为只是一个普通函数“借住”在类里,跟类没有任何绑定,无法访问类级别的任何信息。后者为类方法,可以访问类级别的任一信息,两者在使用时均可以用cls.method(),类名称直接点方法调用,实例方法与@classmethod属性类似,均可以访问类级别的任一信息,但是实例方法必须实例化,obj.method(),实例对象点方法调用。还有一个普通类属性,直接cls.arg调用,四者相互配合,下面以一个实际例子来展现它们之间的关系。

from __future__ import annotations
from dataclasses import dataclass, asdict
from decimal import Decimal
import datetime

@dataclass
class Order:
    user_id: int
    amount_cents: int          # 以分为单位
    status: str = "PENDING"

    # ---------- 1. 实例方法:操作“这张订单” ----------
    def pay(self) -> None:
        if self.status != "PENDING":
            raise ValueError("Only PENDING order can pay")
        self.status = "PAID"
        # 更新类级统计
        Order._today_turnover_cents += self.amount_cents

    def cancel(self) -> None:
        self.status = "CANCELLED"

    # ---------- 2. 类方法:工厂 + 统计 ----------
    _today_turnover_cents: int = 0          # 类级属性:今日已付款总额

    @classmethod
    def from_json(cls, data: dict) -> Order:
        """反序列化 JSON -> Order 对象"""
        return cls(
            user_id=int(data["user_id"]),
            amount_cents=int(data["amount_cents"]),
            status=data.get("status", "PENDING")
        )

    @classmethod
    def today_turnover_yuan(cls) -> Decimal:
        """取今日已付款总额(单位:元)"""
        return Decimal(cls._today_turnover_cents) / 100

    # ---------- 3. 静态方法:纯工具 ----------
    @staticmethod
    def cents_to_yuan(cents: int) -> Decimal:
        """分 → 元,与任何订单实例无关"""
        return Decimal(cents) / 100

    # ---------- 4. 普通类属性:回调 / 钩子 ----------
    # 把“序列化方式”挂到类上,外部可替换(策略模式)
    dump_fn = asdict
    
Order.dump_fn = o1

Q:什么时候选择实例方法,什么时候选择类方法?
A:当逻辑需要类级共享数据,例如,订单总数,选择类方法,否则均选择实例方法,主要有如下原因:面向对象的核心是“对象”可扩展性更好线程/并发更安全(类属性是全局共享的;实例属性隔离在线程各自的实例里,天然避免竞态条件。)
Q:普通类属性的作用?
A:可以对类级信息进行修改,以此通过外部替换策略模式。

cls注释规范

在class对应位置添加注释可使得cls有更好的可读性。

class Order:
    """单笔订单的业务对象.

    负责状态机、金额计算与持久化。

    Attributes:
        order_id: 全局唯一订单号.
        amount_cents: 订单金额(单位:分).
        status: 订单状态,取值见 OrderStatus 枚举.
    """
    
    def __init__(
        self,
        order_id: str,
        items: List[str],
        coupon: Optional[str] = None,
    ) -> None:
        ...
    

class DocumentationExtractor:
    """
    从 Word 文档中提取并识别特定表格的通用工具类。
    """

    @staticmethod
    def _extract_all_tables(file_path: str) -> List[pd.DataFrame]:
        """
        读取 Word 文档中的所有表格,并统一转换为 pandas.DataFrame 列表。

        Args:
            file_path (str): Word 文档绝对路径。

        Returns:
            List[pd.DataFrame]: 文档中所有表格对应的 DataFrame 列表。
        """
  1. 对于类,在正式编写之前要加入类的功能说明及公共属性说明;
  2. 对于类中的函数,参数要指明输入的类型,后面同时要指出输出的类型,整体格式为竖列,在正式编写之前要加入相关说明,说明主要包含三个部分,函数功能说明、参数说明、返回值说明。

提供__main__示例

自包含可运行的示例,Python 解释器启动后,会给每个模块(.py)自动设置一个全局变量__name__,如果该文件为主程序值为__main__,若该文件是被import,则值变为模块名。当文件为主程序时,运行函数下的代码,若不为主程序则忽略;

# 使用示例
if __name__ == "__main__":
    content = DocumentationExtractor.identify_specific_table(
        file_path="/abs/path/云南电网有限责任公司安全文明施工费使用管理标准(试行).docx",
        expected_columns=[
            "费用类型及取费建议",
            "措施类别及使用范围",
            "措施类别编码",
            "是否可摊销",
            "变电",
            "线路",
            "配网",
        ],
        expected_fee_types=["安全生产费", "文明施工费", "环境保护费"],
    )
    print(content)

SZUer继续加油!