第九章:函数接口设计:输入 / 输出 / 边界条件

第九章:函数接口设计:输入 / 输出 / 边界条件

    • [1. 先建立一个共识:接口就是"契约"](#1. 先建立一个共识:接口就是“契约”)
    • [2. 输入设计:比你想的更重要](#2. 输入设计:比你想的更重要)
      • [2.1 明确输入的"数据形状"](#2.1 明确输入的“数据形状”)
      • [2.2 输入要"可验证":尽早 fail fast](#2.2 输入要“可验证”:尽早 fail fast)
      • [2.3 输入不要过于"灵活"](#2.3 输入不要过于“灵活”)
    • [3. 输出设计:决定你的函数是否可组合](#3. 输出设计:决定你的函数是否可组合)
      • [3.1 返回值要"稳定且明确"](#3.1 返回值要“稳定且明确”)
      • [3.2 不要随意"打印",要么返回,要么记录日志](#3.2 不要随意“打印”,要么返回,要么记录日志)
      • [3.3 输出要可序列化(在数据/AI工程中很关键)](#3.3 输出要可序列化(在数据/AI工程中很关键))
    • [4. 边界条件:这才是数据工程的主战场](#4. 边界条件:这才是数据工程的主战场)
      • [4.1 空数据](#4.1 空数据)
      • [4.2 缺失值与 NaN](#4.2 缺失值与 NaN)
      • [4.3 类型漂移](#4.3 类型漂移)
      • [4.4 极端值](#4.4 极端值)
      • [4.5 重复与不唯一](#4.5 重复与不唯一)
    • [5. 三种边界处理策略:选一种并保持一致](#5. 三种边界处理策略:选一种并保持一致)
      • [策略 A:严格模式(工程默认推荐)](#策略 A:严格模式(工程默认推荐))
      • [策略 B:宽松模式(业务可用)](#策略 B:宽松模式(业务可用))
      • [策略 C:显式返回"状态"](#策略 C:显式返回“状态”)
    • [6. 接口设计的"工程模板":一眼看懂该怎么写](#6. 接口设计的“工程模板”:一眼看懂该怎么写)
    • [7. 两个实战案例:写对接口后,测试和维护都轻了](#7. 两个实战案例:写对接口后,测试和维护都轻了)
      • [7.1 指标函数:CTR](#7.1 指标函数:CTR)
      • [7.2 文本清洗:clean_text](#7.2 文本清洗:clean_text)
    • [8. 把接口设计和 pytest 串起来:你会更快进入"可维护状态"](#8. 把接口设计和 pytest 串起来:你会更快进入“可维护状态”)
    • [9. 本章小结:你应该带走的三条规则](#9. 本章小结:你应该带走的三条规则)
    • 课后练习(建议你真的做一下)
    • 下一章:

写 Python 写到后面,你会发现:

真正决定代码"能不能长期维护"的,不是你会不会写算法,而是你函数接口设计得好不好

尤其在数据分析与 AI 工程里,一个函数往往要面对:

  • 上游数据格式不稳定(字段缺失、类型漂移)
  • 下游复用场景越来越多(训练/推理/回测/批处理)
  • 边界条件极其常见(空数据、NaN、分母为 0、超长文本、极端值)

这章我们不讲"抽象的设计原则",只讲一件事:

如何把函数设计成"别人敢用、未来敢改、出了问题好定位"。


1. 先建立一个共识:接口就是"契约"

函数接口不是参数列表那么简单,它至少包含四部分契约:

  1. 输入:允许什么类型、什么形状、什么范围
  2. 输出:返回什么、是否稳定、是否可复现
  3. 边界条件:空值、异常、极端情况如何处理
  4. 副作用:是否读写文件、是否修改入参、是否依赖全局状态

你把这四件事写清楚,函数就能"长期存在";

写不清楚,函数就会变成"只敢自己用"的脚本片段。


2. 输入设计:比你想的更重要

2.1 明确输入的"数据形状"

数据类函数最常见的设计失败:

只写了类型,没写"形状"。

  • list[float] 到底允许空列表吗?
  • pd.DataFrame 需要哪些列?
  • np.ndarray 是一维还是二维?维度顺序是什么?

建议你在 docstring 里把 shape 写出来,尤其是 numpy:

python 复制代码
def cosine_sim(a: "ndarray[float] shape=(d,)", b: "ndarray[float] shape=(d,)") -> float:
    ...

或者在注释里写清楚:

  • X: (n_samples, n_features)
  • y: (n_samples,)

2.2 输入要"可验证":尽早 fail fast

接口设计里最实用的一条:
校验越靠近入口越好

python 复制代码
def build_features(df):
    required = {"user_id", "ts", "amount"}
    missing = required - set(df.columns)
    if missing:
        raise KeyError(f"missing columns: {sorted(missing)}")

Fail fast 的好处:

  • 错误离源头近
  • 日志更清晰
  • 不会在 pipeline 末端才爆炸

2.3 输入不要过于"灵活"

很多人为了"好用",允许函数同时接受:

  • list / numpy / pandas / 单值 / None

结果就是函数内部一堆 if/else,最后谁也不敢改。

更稳妥的做法:

  • 主入口只接受一种形态
  • 其他形态通过适配器函数转换

这在 AI 工程里尤其重要:训练与推理需要一致,接口也要一致。


3. 输出设计:决定你的函数是否可组合

3.1 返回值要"稳定且明确"

一个函数如果有多种返回结构,下游会痛苦:

  • 有时返回 float
  • 有时返回 dict
  • 有时返回 None

建议你做到两点:

  1. 返回结构固定
  2. 异常时明确抛错或返回默认值,不要"悄悄吞掉"

例如指标函数:

  • 业务允许空集:返回 0.0
  • 业务不允许空集:直接抛错

不要两种策略混用。

3.2 不要随意"打印",要么返回,要么记录日志

在工程化代码里,print 是一种副作用。

测试、部署、并发、异步环境下会变得不可控。

策略:

  • 调试阶段:用 logging(可控)
  • 业务输出:通过 return 返回
  • 错误信息:通过 exception 抛出

3.3 输出要可序列化(在数据/AI工程中很关键)

你会经常把输出写入:

  • parquet / json / sqlite / redis
  • 特征仓库
  • 模型输入输出缓存

所以建议:

  • 返回 dict / dataclass / pydantic model
  • 避免返回不可序列化对象(复杂迭代器、匿名函数、open file handle)

4. 边界条件:这才是数据工程的主战场

在算法题里边界条件是"加分项",

在数据工程里边界条件是"常态"。

下面是高频边界列表(建议直接贴到你的项目 Wiki):

4.1 空数据

  • 空 DataFrame / 空数组 / 空列表
  • 空字符串 / 全是空格

4.2 缺失值与 NaN

  • None
  • NaN
  • inf / -inf

4.3 类型漂移

  • 数字字段变成字符串 "123"
  • 时间字段变成 "2026/01/01"

4.4 极端值

  • 分母为 0
  • outlier
  • 超长文本(prompt、文档)

4.5 重复与不唯一

  • user_id 重复
  • 主键不唯一导致 merge 爆炸

5. 三种边界处理策略:选一种并保持一致

很多人代码混乱的根源:

同一类边界在不同函数里处理方式不一致。

建议你在项目里固定策略,常见三种:

策略 A:严格模式(工程默认推荐)

  • 边界条件直接抛错
  • 用于训练/离线计算/数据质量要求高的场景

策略 B:宽松模式(业务可用)

  • 返回默认值或空结构
  • 用于在线服务必须"不断"的场景

策略 C:显式返回"状态"

  • 返回 (result, warnings)Result[T]
  • 用于需要记录质量问题但不中断流程的场景

关键点:

不要在同一个函数里混着来。


6. 接口设计的"工程模板":一眼看懂该怎么写

这里给一个数据特征函数的推荐模板(你可以复用到整套专栏里):

python 复制代码
def make_user_features(
    df,
    *,
    user_col: str = "user_id",
    ts_col: str = "ts",
    strict: bool = True,
):
    """
    Build user-level features.

    Inputs:
      df: DataFrame, must contain [user_col, ts_col, ...]
    Outputs:
      DataFrame with columns: [user_col, feat_x, feat_y, ...]
    Edge cases:
      - empty df: raise ValueError if strict else return empty feature df
      - missing columns: raise KeyError
      - invalid ts: raise ValueError
    Side effects:
      none
    """
    ...

你会发现,这个模板强迫你把"契约"写出来。

写出来,你就更容易测试;也更容易复用。


7. 两个实战案例:写对接口后,测试和维护都轻了

7.1 指标函数:CTR

坏接口(常见):

  • views=0 时返回 None
  • clicks 不是 int 时默默转型
  • 同时打印日志

好接口:

  • views=0 返回 0.0(宽松策略)
  • 类型不对直接抛错(严格策略)

关键是:你必须提前决定"你的项目属于哪种策略"。

7.2 文本清洗:clean_text

坏接口:

  • 输入 None 直接报错
  • 输入数字自动 str()
  • 输出有时返回 list 有时返回 str

好接口:

  • 输入必须是 str(或显式允许 Optional[str])
  • 输出永远是 str
  • 空文本返回 ""(明确策略)

8. 把接口设计和 pytest 串起来:你会更快进入"可维护状态"

你会发现:

  • 接口契约清晰 → 测试用例自然 → 迭代风险变小

这一章和上一章(pytest)的关系非常紧:

  • 第 8 章:怎么写测试
  • 第 9 章:怎么设计一个"值得测试、好测试"的函数接口

工程里最怕的是:

接口混乱导致测试写不下去,最终没人写测试。


9. 本章小结:你应该带走的三条规则

  1. 接口先写契约:输入/输出/边界/副作用
  2. 边界处理选策略:严格、宽松、显式状态,选一种统一
  3. 越靠近入口越校验:fail fast,错误更可控

课后练习(建议你真的做一下)

从你现有项目里找一个"最容易出事"的函数,按本章重构:

  • 补齐 docstring 契约

  • 加输入校验

  • 明确边界策略

  • 给它写 3 个 pytest:

    1. 正常样例
    2. 空数据
    3. 缺列/非法值

你做完会发现:

代码没变复杂,反而更"敢改"。


如果你愿意,把你项目里一个函数(脱敏即可)贴出来,我可以按本章的"接口契约模板"帮你重写一版,并顺手给出对应的 pytest 用例框架。

下一章:

《面向复用的工具库:utils 的设计与拆分》

相关推荐
万行2 小时前
机器人系统ros2&期末速通&1
人工智能·python·机器学习·机器人
轻竹办公PPT2 小时前
AI 生成 2026 年工作计划 PPT,逻辑清晰度对比测试
人工智能·python·powerpoint
Direction_Wind2 小时前
抖音视频下载,直播间监控,直播间发言采集,最新加密算法
python·node.js
旦莫2 小时前
使用OCR加持的APP自动化测试
python·测试开发·自动化·ocr·pytest·ai测试
天若有情6732 小时前
用 Python 爬取电商商品数据:从入门到反爬破解
开发语言·python
七夜zippoe2 小时前
RabbitMQ与Celery深度集成:构建高性能Python异步任务系统
分布式·python·rabbitmq·celery·amqp
Hello阿尔法2 小时前
SCons 一款基于 Python 的自动化构建工具
python·跨平台·构建工具·scons
Pyeako2 小时前
Opencv计算机视觉--图像边缘检测
人工智能·python·opencv·计算机视觉·sobel·canny·图像边缘检测