函数该拆,当它同时处理不同抽象层的逻辑,如查库与渲染、校验与发请求;参数超3个未聚合、命名含“and/or/then”、返回类型混乱、测试需多mock,均是拆分信号。

函数是不是该拆?看它有没有「两件不相干的事」
一个函数干了多件事,不是代码多、行数长才叫“职责不清”,而是它在同时处理不同维度的逻辑。比如既查数据库又渲染 HTML,或者既校验输入又发 HTTP 请求——这两类动作天然属于不同抽象层。
实操建议:
- 问自己:如果我要给这个函数写单元测试,是否得 mock 数据库、网络、时间等至少两个外部依赖?如果是,大概率该拆
- 检查函数内部是否有明显分界:比如前半段处理
user_input,后半段拼接sql_query并执行,中间没共享状态,就是拆分信号 - 留意命名:如果函数名里带 “and”、“or”、“then”,比如
validate_and_save_user,基本是职责混合的铁证
参数超过 3 个时,先怀疑是不是封装错了
不是参数多就一定错,但 Python 的 def 函数一旦接受超过 3 个独立参数(不含 *args 或 **kwargs),往往意味着它在协调多个上下文,而没把关联数据聚合成一个概念。
常见错误现象:create_order(user_id, product_id, quantity, currency, discount_code, notify_email) —— 这些参数其实属于两个对象:User 和 OrderRequest,硬塞进函数里会让调用方反复构造零散值。
立即学习“Python免费学习笔记(深入)”;
实操建议:
- 把语义上成组的参数打包成 dataclass 或
NamedTuple,比如OrderRequest(product_id, quantity, discount_code) - 避免用字典传参代替结构化类型,
create_order(**order_dict)看似简洁,实则隐藏了契约,IDE 和类型检查器都帮不上忙 - 如果函数必须支持多种组合(如部分字段可选),优先用
dataclasses.field(default_factory=...)而非一堆None默认值
函数返回值类型混乱,说明它在承担「决策」和「执行」双重角色
比如一个叫 process_payment 的函数,有时返回 dict(成功),有时抛 ValueError(校验失败),有时又返回 None(余额不足但静默跳过)——这不是健壮,是职责模糊。
使用场景:支付、权限校验、配置加载这类有明确成败路径的操作,最容易在这里失控。
实操建议:
- 让函数只做一件事:要么纯计算(无副作用、确定性返回),要么纯执行(有副作用、统一返回
bool或Result类型) - 拒绝“成功返回结果,失败返回
None”模式;Python 有异常机制,就别用None当控制流 - 如果真需要多态返回(如
Union[str, int, None]),说明它本该是多个小函数,或者用Result模式(如result = process_payment(...); if result.is_ok(): ...)
测试难写、难读、难改,根本原因常是函数边界没划清
当你发现要为一个函数写 5 个以上测试用例,且每个都要 patch 三四个地方(patch('requests.post')、patch('datetime.now')、patch('os.getenv')),那不是测试写得不够好,是函数本身越界了。
性能影响:过度耦合的函数没法单独压测 IO 密集或 CPU 密集部分,也难以用 functools.lru_cache 缓存中间结果。
实操建议:
- 每个测试文件只验证一种行为:比如
test_payment_validation.py只管校验逻辑,不碰数据库;test_payment_gateway.py只管对接第三方,不碰业务规则 - 如果某个函数的测试需要启动真实数据库或网络,立刻停手——它已经不属于 unit test 范畴,该被隔离出去
- 用
typing.Protocol定义依赖接口(如PaymentGateway),而不是直接 import 具体实现,能倒逼你把职责切开
真正难的不是判断“要不要拆”,而是识别哪些逻辑看似相关、实则不该绑在一起——比如“生成订单号”和“保存订单”,前者是纯函数,后者是副作用,它们共享的只有“订单”这个名词,没有共享的执行上下文。









