Python:学到后面最容易出现哪些坏味道,应该怎么重构
Python 的优势之一是写得快。
但也正因为写得快,坏味道往往也长得很快。
最典型的过程通常是:
- 一开始只是一个脚本
- 后来补几个参数
- 再加几个分支
- 再加几个输出格式
- 最后功能越来越多,代码却越来越难动
真正麻烦的地方不是“丑”,而是这些坏味道会直接带来工程问题:
- 改一处坏三处
- 测试难写
- 日志难查
- 状态难控
- 新人难接手
这一篇不只列清单,而是围绕一个实际场景展开:把一组巡检脚本做一次有边界的重构。
一、先看这组脚本是怎么慢慢变坏的
假设现在有一个巡检脚本 inspect.py,最初只做一件事:
- 读接口状态
- 打印失败项
后来需求不断加:
- 增加环境切换
- 增加告警通知
- 增加输出文件
- 增加重试
- 增加跳过某些服务
- 增加汇总报告
最后一个文件里堆了:
- 配置
- 请求
- 解析
- 统计
- 通知
- 文件输出
坏味道通常就是这样长出来的,不是一天突然出现的。
二、坏味道一:一个文件或一个函数承担太多责任
最常见的第一类坏味道就是:
- 单文件越来越长
- 一个函数从头跑到尾,什么都做
比如:
1 | def run_inspection(): |
这种写法最大的问题不是长度,而是责任混在一起:
- 任何一点改动都可能影响整条链
- 很难单独测试某一个阶段
- 排障时不知道问题落在哪一层
更稳的重构方向通常是先按阶段拆:
load_targets()fetch_status()analyze_results()build_report()send_alert()
三、坏味道二:字典到处飞,字段全靠记忆
Python 很容易把所有东西都先塞进字典里。
短期看起来很快,长期最容易出现这些问题:
- 字段名改一处漏三处
- 某些字段有时是
status,有时是state - 调用方不知道哪些键是必有,哪些键是可选
例如:
1 | item = {"svc": "order", "code": 500, "cost": 132} |
写到后面经常很难清楚回答:
svc和service_name是不是一回事cost单位是毫秒还是秒- 缺字段时应该怎么处理
更稳的重构方式通常是:
- 先统一字段命名
- 再抽成明确结构
- 让输入输出契约稳定下来
四、坏味道三:隐藏全局状态越来越多
下面这种写法在 Python 脚本里非常常见:
1 | ENV = "test" |
一开始看起来很方便,后面很容易变成:
- 函数不接参数也能跑
- 但调用顺序一变就出问题
- 测试之间互相污染
隐藏全局状态最容易制造的现象是:
- 单独跑某个函数没问题
- 整批跑时结果互相串
这类坏味道的重构重点通常是:
- 把状态从全局挪到参数和返回值里
- 必须共享的状态,显式放进对象或上下文结构
五、坏味道四:布尔参数和分支越来越多
另一类高频坏味道是:
1 | def run_job(use_cache=False, send_alert=False, save_report=True, with_retry=True): |
表面上只是参数多一点,实际问题是:
- 行为组合越来越不可控
- 分支路径爆炸
- 调用方很难知道不同组合的语义
如果一个函数开始长成这样,通常说明:
- 它承担了过多模式
- 这些模式可能应该拆成不同策略或不同流程
更稳的处理方式往往是:
- 拆职责
- 收敛模式
- 用更明确的配置结构替代布尔开关乱飞
六、坏味道五:复制粘贴逻辑越来越多
Python 写得快,复制也快。
所以很容易出现:
check_order_service()check_user_service()check_payment_service()
三段代码长得几乎一样,只差 URL 和阈值。
这类代码真正的问题不是重复本身,而是:
- 规则一改,要改很多处
- 某一处漏改就会出现行为不一致
但这里也不要一看到重复就过度抽象。
更稳的顺序通常是:
- 先确认重复逻辑真的稳定
- 再抽公共流程
- 把变化点留成参数
七、坏味道六:导入模块就开始执行副作用
这类问题在脚本型 Python 项目里特别高频:
1 | print("start init") |
这些代码如果直接写在模块顶层,就会导致:
- 一
import就执行 - 测试导入时也触发真实动作
- 调试时副作用很难控制
更稳的方式通常是:
- 把执行入口收进
main() - 模块顶层只放定义
- 真正执行放进
if __name__ == "__main__":
这不是风格问题,而是可测试性和可控性问题。
八、坏味道七:异常处理不是没写,而是写得像吞噬器
看起来很“稳”的写法其实常常是:
1 | try: |
这种代码最大的问题不是不优雅,而是:
- 真实错误被吞掉
- 排障线索断掉
- 上层还以为执行成功了
更稳的重构方向通常是:
- 只捕获明确异常
- 打出关键上下文
- 决定是继续抛出、转换还是局部降级
九、坏味道八:测试只能测整条流程,测不了核心逻辑
有些 Python 项目看起来有测试,但其实只有一类测试:
- 跑整个脚本
- 看最后有没有文件产出
这种测试方式一旦项目变大,很快会出现问题:
- 失败时不知道坏在哪层
- 某个小逻辑改动也要跑整条链
- 本地调试成本越来越高
这通常说明代码结构本身已经不利于测试。
真正需要重构的,不只是测试文件,而是业务函数的边界和依赖注入方式。
十、重构不要一次性推倒,先按风险排序
一看到坏味道就想一次性重写,这通常不是最稳的方式。
更实用的顺序通常是:
- 先找最影响测试和排障的部分
- 再找重复最多、改动最频繁的部分
- 最后再处理更深层的结构优化
对大多数脚本项目来说,最先值得重构的通常是:
- 输入输出边界
- 大函数拆分
- 隐藏全局状态
- 结果结构统一
十一、一个完整示例:把发臭的巡检脚本整理成可维护版本
先看一段非常典型的坏味道版本:
1 | FAILED_ITEMS = [] |
这段代码的问题几乎一眼就能看到:
- 全局状态污染
- 流程职责全堆在一起
- 输出结构不清
- 测试很难只测某一层
更稳的整理方式通常是先拆成三层:
1 | def collect_failures(targets): |
这种重构不炫,但能立刻带来三件事:
- 全局状态消失
- 核心逻辑开始可测
- 输出结构开始稳定
十二、怎么测试重构没有把业务改坏
重构最怕的不是改慢,而是把原来能工作的行为悄悄改坏。
所以更稳的顺序通常是:
1. 先给核心行为补最小测试
1 | def test_build_report_should_keep_failed_count_and_items(): |
2. 再给重构前后最关键的输出做对照
例如:
- 失败数量有没有变
- 报告字段有没有变
- 通知触发条件有没有变
3. 最后再替换实现
真正有价值的不是“把代码改得更漂亮”,而是让行为保持稳定的同时,结构开始变清晰。
十三、一个更稳的重构骨架
如果当前项目已经出现上面这些坏味道,可以按下面这条线走:
1. 固定输入输出
先把入口参数和结果结构收住。
2. 拆阶段
把“读、算、写、发通知”拆成独立阶段。
3. 抽变化点
把 URL、阈值、环境参数从流程里剥出来。
4. 去全局状态
把隐藏在模块顶层的状态挪到上下文里。
5. 补最小测试
优先给最核心、最常改的函数补测试。
这样重构不会显得很猛,但更容易落地。
十四、一个实际排错案例
来看一个很典型的现象:
- 巡检脚本在测试环境正常
- 到生产环境后,有时会重复发告警,有时又完全不发
排查下来,根因常常不是通知接口不稳定,而是:
FAILED_ITEMS放在全局列表里- 前一轮执行没清干净
- 下一轮又继续复用
排查顺序通常可以这样走:
- 先看状态存在哪
- 再看状态是函数内局部,还是模块级共享
- 最后发现问题不是逻辑分支,而是状态泄漏
修复方式往往不复杂:
- 去掉全局列表
- 每次执行创建新的上下文对象
- 把失败项作为返回值往下传
这类案例很有代表性,因为它说明:
- 很多坏味道最后暴露出来的不是“不好看”
- 而是线上行为不稳定
十五、什么时候该停下,不要把重构做成重写
重构里另一个常见陷阱是:
- 发现坏味道很多
- 一下子想把整个项目全部推倒
这通常不是最稳的做法。
更实用的判断通常是:
1. 先停在“结构已清楚、测试已能覆盖”的位置
如果职责边界已经收住、核心函数已经可测,未必还需要继续做更大的结构动作。
2. 没证据证明有收益的重构,先别做
例如只是因为“看起来还能更优雅”,但对测试、排障、交付没有直接收益,这种重构优先级通常没那么高。
3. 把重构做成一系列小步,而不是一次性重写
小步重构更容易:
- 观察行为变化
- 回滚
- 控制风险
真正稳的重构不是“改得最多”,而是“每一步都能解释为什么值得改”。
十六、一个实际练习
练习目标:给一个已有 Python 脚本做最小重构。
要求:
- 找出 3 个最明显的坏味道
- 不改业务结果,只改结构
- 至少拆出 3 个职责函数
- 去掉 1 个隐藏全局状态
- 补 2 条最小测试
- 写下“这次没继续重构的部分为什么先不动”
如果这个练习能独立做完,说明已经开始有重构判断力,而不是只会“感觉乱”。
十七、这一篇学完以后,下一步应该补什么
如果这一篇已经真正吸收,后面最适合继续补的是:
- 项目结构设计
- pytest 夹具与测试分层
- Python 在不同工程场景里的角色边界
因为重构的本质,最终还是为了把结构、测试和职责边界重新收清楚。
十八、结语
Python 的坏味道之所以常见,不是因为这门语言差,而是因为:
- 写得快
- 试错成本低
- 很容易从一个小脚本长成一组长期工具
真正要建立的不是“代码必须写得很优雅”,而是:
- 什么时候已经开始失控
- 先该重构哪一层
- 怎么重构才不会越改越乱
能把这些判断建立起来,Python 项目才会从“越来越难碰”走向“还能继续演进”。