
第十二章 类型标注与可读性:让协作与复用更容易
-
- [0. 本章目标与适用场景](#0. 本章目标与适用场景)
- [1. 为什么类型标注在数据/AI工程里更重要?](#1. 为什么类型标注在数据/AI工程里更重要?)
- [2. 最小起步:把"函数签名"写清楚](#2. 最小起步:把“函数签名”写清楚)
-
- [2.1 从一个数据清洗函数开始](#2.1 从一个数据清洗函数开始)
- [2.2 返回值不要"猜"](#2.2 返回值不要“猜”)
- [3. 半结构化数据:TypedDict 让"字典"可控](#3. 半结构化数据:TypedDict 让“字典”可控)
-
- [3.1 用 TypedDict 给字典"上合同"](#3.1 用 TypedDict 给字典“上合同”)
- [4. 结构化返回:dataclass 让接口更稳定](#4. 结构化返回:dataclass 让接口更稳定)
- [5. 让"可读性"真正落地:四条硬规则](#5. 让“可读性”真正落地:四条硬规则)
-
- [5.1 命名:别用 data、tmp、res 糊弄未来](#5.1 命名:别用 data、tmp、res 糊弄未来)
- [5.2 函数长度:超过 40 行就要考虑拆](#5.2 函数长度:超过 40 行就要考虑拆)
- [5.3 返回值结构统一](#5.3 返回值结构统一)
- [5.4 异常策略明确](#5.4 异常策略明确)
- [6. mypy/pyright:让类型标注变成"自动化守门员"](#6. mypy/pyright:让类型标注变成“自动化守门员”)
-
- [6.1 轻量路线:pyright(更顺滑)](#6.1 轻量路线:pyright(更顺滑))
- [6.2 严谨路线:mypy(更工程)](#6.2 严谨路线:mypy(更工程))
- [7. 数据科学常见类型坑:给你一份"工程白名单"](#7. 数据科学常见类型坑:给你一份“工程白名单”)
-
- [7.1 pandas / numpy 类型怎么写?](#7.1 pandas / numpy 类型怎么写?)
- [7.2 Any 不是罪,但要有边界](#7.2 Any 不是罪,但要有边界)
- [8. 一个可落地的"类型标注改造顺序"](#8. 一个可落地的“类型标注改造顺序”)
- [9. 小结](#9. 小结)
- 下一章:
你有没有遇到过这种场景:
- 函数参数叫
data,到底是DataFrame、list[dict]还是np.ndarray?只能靠猜。 - 同一个特征工程函数,A 同事传了
str,B 同事传了Path,线上才发现兼容性问题。 - 你接手一段"能跑但看不懂"的脚本:变量名短、返回值不清、异常不写,改一次要冒一次风险。
在数据分析 + AI 工程 里,代码的复杂度不只来自算法,更来自"数据形态"和"边界条件"。
类型标注(type hints)不是形式主义,它更像一套低成本的契约机制:提前把输入/输出说清楚,把误用变成可见问题,把协作成本压下去。
本章我们不空谈规范,直接回答三个工程问题:
- 为什么在数据/AI项目里类型标注更"值钱"?
- 该标哪里、不该标哪里?
- 怎么把类型标注和可读性一起做成"团队可用"的实践?
0. 本章目标与适用场景
学完你应该能做到:
- 给核心函数写出清晰的输入/输出类型(而不是全是
Any) - 用
TypedDict / dataclass / Protocol描述"半结构化数据" - 让 IDE 与静态检查(mypy/pyright)真正帮你挡住低级错误
- 通过命名、注释、docstring、返回值结构统一,显著提升可读性
- 在 utils、特征工程、评测脚本、RAG 工程里,建立可复用的类型体系
1. 为什么类型标注在数据/AI工程里更重要?
Web 项目里的 bug 多半是"逻辑错"。
数据/AI工程里,更多是"形态错":
- 某列从
int变str,你没发现,特征全变 NaN - 你以为传的是
list[str],结果是list[dict] - pipeline 某步返回
DataFrame,下游却当成np.ndarray用 - 模型推理接受
dict,你传了pydantic model,线上才爆
这些问题的特点是:
不一定当场报错,但会悄悄把结果做坏。
类型标注的价值就是:
把"靠经验猜"的隐性契约,变成"写在代码里的显性契约"。
2. 最小起步:把"函数签名"写清楚
2.1 从一个数据清洗函数开始
python
from __future__ import annotations
from typing import Iterable
def normalize_text(text: str) -> str:
return " ".join(text.strip().split()).lower()
def normalize_batch(texts: Iterable[str]) -> list[str]:
return [normalize_text(t) for t in texts]
对数据项目来说,Iterable[str] -> list[str] 这种签名非常关键:
它告诉读者"我接受可迭代字符串,不承诺输入可索引;输出一定是 list"。
2.2 返回值不要"猜"
坏例子(协作灾难):
python
def load_data(path):
...
return data
好例子(可复用):
python
from pathlib import Path
import pandas as pd
def load_csv(path: str | Path) -> pd.DataFrame:
return pd.read_csv(path)
在数据工程里,只要是核心 IO,就必须标注。
3. 半结构化数据:TypedDict 让"字典"可控
很多 pipeline 都是 dict 传来传去:
快是快,但协作非常痛苦。
3.1 用 TypedDict 给字典"上合同"
python
from typing import TypedDict
class Sample(TypedDict):
doc_id: str
title: str
abstract: str
lang: str
def build_prompt(sample: Sample) -> str:
return f"[{sample['lang']}] {sample['title']}\n{sample['abstract']}"
好处是:
- 读代码的人知道有哪些字段
- IDE 会提示键名
- 静态检查能发现拼写错误(
abstact这种低级坑)
在 RAG/CLIR/KG 这类工程里,TypedDict 的性价比极高。
4. 结构化返回:dataclass 让接口更稳定
当你的函数返回值不是一个简单数,而是多个字段组合,建议用 dataclass。
python
from dataclasses import dataclass
@dataclass(frozen=True)
class EvalResult:
recall_at_k: float
ndcg_at_k: float
n_queries: int
def evaluate(...) -> EvalResult:
...
return EvalResult(recall_at_k=0.42, ndcg_at_k=0.31, n_queries=100)
为什么这比 tuple 或 dict 好?
- 字段有名字,减少误用
- 调用方更稳定:新增字段不容易破坏老代码
- 更适合长期维护与版本迭代
5. 让"可读性"真正落地:四条硬规则
类型标注解决"你传什么、我返回什么"。
可读性解决"我为什么这么写"。
5.1 命名:别用 data、tmp、res 糊弄未来
坏命名:
data,d,tmp,res,x1
好命名:
raw_df,clean_df,query_text,doc_chunks,embedding_dim
经验法则:
变量名里尽量包含形态信息(df/list/ids/text)。
5.2 函数长度:超过 40 行就要考虑拆
数据脚本常见的问题是:
一个函数把 IO、清洗、特征、日志全包了。
拆分建议:
parse_*:解析validate_*:校验transform_*:变换compute_*:计算save_*:落盘
5.3 返回值结构统一
如果同一类函数,有的返回 df,有的返回 (df, meta),有的返回 dict,团队很快失控。
建议:
- 要么都返回
DataFrame - 要么都返回
dataclass - 要么都返回
(data, meta)并写清类型
5.4 异常策略明确
工具函数最怕"吞异常"。
python
def read_json(path: str) -> dict:
# 要么抛异常,要么返回 Optional,并明确约定
...
如果选择返回 None,请写成:
python
from typing import Optional
def try_read_json(path: str) -> Optional[dict]:
...
用命名告诉读者:这是"可能失败"的函数。
6. mypy/pyright:让类型标注变成"自动化守门员"
只写类型标注不检查,价值会打折。
推荐两种路线:
6.1 轻量路线:pyright(更顺滑)
适合数据团队快速起步,配合 VSCode 提示很强。
6.2 严谨路线:mypy(更工程)
适合工具库/公共模块/长期维护项目。
建议从"关键模块"开始启用:
utils/features/metrics/api schema / DTO
不要一上来全仓库严格检查,会被历史债拖死。
7. 数据科学常见类型坑:给你一份"工程白名单"
7.1 pandas / numpy 类型怎么写?
pd.DataFrame,pd.Seriesnp.ndarray
如果你想更精细,可以在后续引入更强的 typing 扩展,但起步阶段不建议复杂化。
7.2 Any 不是罪,但要有边界
这三处允许用 Any:
- 入口层(解析外部数据)
- 第三方库返回值不稳定的地方
- 快速原型脚本(但要标记 TODO)
核心逻辑层(特征/指标/工具库)尽量不要 Any 泛滥。
8. 一个可落地的"类型标注改造顺序"
如果你现在的项目完全没类型标注,可以按这个顺序做:
- 公共 utils:IO、路径、校验、重试
- 指标 metrics:输入输出明确,边界多,最需要契约
- 特征 features:数据形态复杂,最容易误用
- pipeline:先把每步输入输出写清,再谈整体编排
- 应用层(API/UI):用 DTO(TypedDict/dataclass/pydantic)做边界
每做完一层,跑一次静态检查与 pytest,你会明显感到"变稳"。
9. 小结
类型标注不是为了"好看",而是为了把工程从:
- "能跑就行"
- 变成
- "能跑、能协作、能复用、能长期维护"
在数据分析与 AI 工程里,真正拖慢团队的往往不是模型,而是:
- 数据形态混乱
- 接口不清
- 返回值不稳定
- 变更不敢做
类型标注 + 可读性实践,就是对这些问题的最低成本对冲。
你现在的项目里,最想从哪一类代码开始"类型标注改造"?
- utils 工具库(IO/路径/校验)
- 特征工程(输入输出总不一致)
- 指标评测(边界 bug 多)
- pipeline 编排(每步传什么没人说得清)
你可以贴一个你最常用的函数签名(脱敏即可),我可以按本章方式帮你补齐:类型标注、返回值结构、异常策略与最小测试用例。
下一章:
《第十三章 性能意识入门:你代码慢在哪(profiling 思路)》