一个数据处理程序最初可能只有几十行:读取文件,计算几个指标,再把结果写回磁盘。随着需求增加,里面又加入进度显示、错误提示和网络请求。此时修改一个计算公式,也可能需要准备文件目录和网络环境才能验证。

遇到这种情况,可以先按“计算”和“外部操作”划分职责,不必立即引入复杂框架。

让计算函数只接收数据

例如,需要从一组数值中计算平均数。计算函数可以直接接收列表,返回结果:

def mean_value(values):
    if not values:
        raise ValueError("values must not be empty")
    return sum(values) / len(values)

这个函数不需要知道列表来自文本文件、数据库还是手工输入。对同样的有效输入,它会给出同样的结果;遇到空列表,也有明确的失败方式。

读取文件和保存结果则由外层代码负责:

from pathlib import Path

source = Path("example-input.txt")
values = [float(line) for line in source.read_text().splitlines()]
result = mean_value(values)
print(f"mean: {result:.2f}")

上面的例子只处理格式正确的数值行。实际读取模块还应根据需求处理空行、编码和错误行号,但这些细节不用进入平均数的计算过程。

给外部操作留出替换位置

文件系统、屏幕、网络和时钟都会影响程序运行。可以把这些依赖放在外层,必要时通过参数传入。例如,让生成报告的函数接收一个保存函数,而不是在内部固定写入某个目录。

不过,抽象也有成本。如果程序只有一种输出方式、十几行代码,拆成很多接口未必更清楚。先在确有测试或替换需求的位置划分边界,通常就够用了。

分别验证两类问题

计算部分可以使用小型合成输入,检查正常值、空输入和边界值。外部操作部分则单独验证文件不存在、目录不可写或数据格式错误等情况。

assert mean_value([2, 4, 6]) == 4
assert mean_value([5]) == 5

测试计算函数时无需创建文件,这让反馈更快。但单独的计算测试不能证明整个程序可用,还需要用一个小文件跑通读取、计算和输出的完整路径。

从一处最难测试的代码开始

整理旧程序时,可以先找出经常修改、却必须连接外部环境才能验证的那段逻辑。把它需要的数据列成参数,把计算结果作为返回值,暂时保留外层流程。

这种小范围调整的收益很具体:文件读取出错时看读取层,数值结果不对时看计算层,输出异常时看保存层。代码的组织开始对应问题的来源,后续维护也就有了明确入口。