Python:学到后面最容易出现哪些坏味道,应该怎么重构

Python 的优势之一是写得快。
但也正因为写得快,坏味道往往也长得很快。

最典型的过程通常是:

  • 一开始只是一个脚本
  • 后来补几个参数
  • 再加几个分支
  • 再加几个输出格式
  • 最后功能越来越多,代码却越来越难动

真正麻烦的地方不是“丑”,而是这些坏味道会直接带来工程问题:

  • 改一处坏三处
  • 测试难写
  • 日志难查
  • 状态难控
  • 新人难接手

这一篇不只列清单,而是围绕一个实际场景展开:把一组巡检脚本做一次有边界的重构

一、先看这组脚本是怎么慢慢变坏的

假设现在有一个巡检脚本 inspect.py,最初只做一件事:

  • 读接口状态
  • 打印失败项

后来需求不断加:

  • 增加环境切换
  • 增加告警通知
  • 增加输出文件
  • 增加重试
  • 增加跳过某些服务
  • 增加汇总报告

最后一个文件里堆了:

  • 配置
  • 请求
  • 解析
  • 统计
  • 通知
  • 文件输出

坏味道通常就是这样长出来的,不是一天突然出现的。

二、坏味道一:一个文件或一个函数承担太多责任

最常见的第一类坏味道就是:

  • 单文件越来越长
  • 一个函数从头跑到尾,什么都做

比如:

1
2
3
4
5
6
7
8
def run_inspection():
# 读配置
# 拉接口
# 解析结果
# 写报告
# 发通知
# 打日志
...

这种写法最大的问题不是长度,而是责任混在一起:

  • 任何一点改动都可能影响整条链
  • 很难单独测试某一个阶段
  • 排障时不知道问题落在哪一层

更稳的重构方向通常是先按阶段拆:

  • load_targets()
  • fetch_status()
  • analyze_results()
  • build_report()
  • send_alert()

三、坏味道二:字典到处飞,字段全靠记忆

Python 很容易把所有东西都先塞进字典里。

短期看起来很快,长期最容易出现这些问题:

  • 字段名改一处漏三处
  • 某些字段有时是 status,有时是 state
  • 调用方不知道哪些键是必有,哪些键是可选

例如:

1
item = {"svc": "order", "code": 500, "cost": 132}

写到后面经常很难清楚回答:

  • svcservice_name 是不是一回事
  • cost 单位是毫秒还是秒
  • 缺字段时应该怎么处理

更稳的重构方式通常是:

  • 先统一字段命名
  • 再抽成明确结构
  • 让输入输出契约稳定下来

四、坏味道三:隐藏全局状态越来越多

下面这种写法在 Python 脚本里非常常见:

1
2
3
ENV = "test"
REPORT_DIR = "/tmp/reports"
FAILED_ITEMS = []

一开始看起来很方便,后面很容易变成:

  • 函数不接参数也能跑
  • 但调用顺序一变就出问题
  • 测试之间互相污染

隐藏全局状态最容易制造的现象是:

  • 单独跑某个函数没问题
  • 整批跑时结果互相串

这类坏味道的重构重点通常是:

  • 把状态从全局挪到参数和返回值里
  • 必须共享的状态,显式放进对象或上下文结构

五、坏味道四:布尔参数和分支越来越多

另一类高频坏味道是:

1
2
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 和阈值。

这类代码真正的问题不是重复本身,而是:

  • 规则一改,要改很多处
  • 某一处漏改就会出现行为不一致

但这里也不要一看到重复就过度抽象。
更稳的顺序通常是:

  1. 先确认重复逻辑真的稳定
  2. 再抽公共流程
  3. 把变化点留成参数

七、坏味道六:导入模块就开始执行副作用

这类问题在脚本型 Python 项目里特别高频:

1
2
3
print("start init")
load_config()
run_job()

这些代码如果直接写在模块顶层,就会导致:

  • import 就执行
  • 测试导入时也触发真实动作
  • 调试时副作用很难控制

更稳的方式通常是:

  • 把执行入口收进 main()
  • 模块顶层只放定义
  • 真正执行放进 if __name__ == "__main__":

这不是风格问题,而是可测试性和可控性问题。

八、坏味道七:异常处理不是没写,而是写得像吞噬器

看起来很“稳”的写法其实常常是:

1
2
3
4
try:
do_work()
except Exception:
pass

这种代码最大的问题不是不优雅,而是:

  • 真实错误被吞掉
  • 排障线索断掉
  • 上层还以为执行成功了

更稳的重构方向通常是:

  • 只捕获明确异常
  • 打出关键上下文
  • 决定是继续抛出、转换还是局部降级

九、坏味道八:测试只能测整条流程,测不了核心逻辑

有些 Python 项目看起来有测试,但其实只有一类测试:

  • 跑整个脚本
  • 看最后有没有文件产出

这种测试方式一旦项目变大,很快会出现问题:

  • 失败时不知道坏在哪层
  • 某个小逻辑改动也要跑整条链
  • 本地调试成本越来越高

这通常说明代码结构本身已经不利于测试。

真正需要重构的,不只是测试文件,而是业务函数的边界和依赖注入方式。

十、重构不要一次性推倒,先按风险排序

一看到坏味道就想一次性重写,这通常不是最稳的方式。

更实用的顺序通常是:

  1. 先找最影响测试和排障的部分
  2. 再找重复最多、改动最频繁的部分
  3. 最后再处理更深层的结构优化

对大多数脚本项目来说,最先值得重构的通常是:

  • 输入输出边界
  • 大函数拆分
  • 隐藏全局状态
  • 结果结构统一

十一、一个完整示例:把发臭的巡检脚本整理成可维护版本

先看一段非常典型的坏味道版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
FAILED_ITEMS = []


def run_inspection(targets, send_alert=False, save_report=True):
for item in targets:
result = request_target(item)
if result["code"] != 0:
FAILED_ITEMS.append(item["name"])

if save_report:
with open("report.txt", "w") as f:
f.write(str(FAILED_ITEMS))

if send_alert and FAILED_ITEMS:
notify(FAILED_ITEMS)

这段代码的问题几乎一眼就能看到:

  • 全局状态污染
  • 流程职责全堆在一起
  • 输出结构不清
  • 测试很难只测某一层

更稳的整理方式通常是先拆成三层:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def collect_failures(targets):
failed = []
for item in targets:
result = request_target(item)
if result["code"] != 0:
failed.append(item["name"])
return failed


def build_report(failed):
return {"failed_count": len(failed), "failed_items": failed}


def run_inspection(targets, send_alert=False, save_report=True):
failed = collect_failures(targets)
report = build_report(failed)

if save_report:
write_report(report)

if send_alert and failed:
notify(failed)

return report

这种重构不炫,但能立刻带来三件事:

  • 全局状态消失
  • 核心逻辑开始可测
  • 输出结构开始稳定

十二、怎么测试重构没有把业务改坏

重构最怕的不是改慢,而是把原来能工作的行为悄悄改坏。

所以更稳的顺序通常是:

1. 先给核心行为补最小测试

1
2
3
4
def test_build_report_should_keep_failed_count_and_items():
report = build_report(["order", "payment"])
assert report["failed_count"] == 2
assert report["failed_items"] == ["order", "payment"]

2. 再给重构前后最关键的输出做对照

例如:

  • 失败数量有没有变
  • 报告字段有没有变
  • 通知触发条件有没有变

3. 最后再替换实现

真正有价值的不是“把代码改得更漂亮”,而是让行为保持稳定的同时,结构开始变清晰。

十三、一个更稳的重构骨架

如果当前项目已经出现上面这些坏味道,可以按下面这条线走:

1. 固定输入输出

先把入口参数和结果结构收住。

2. 拆阶段

把“读、算、写、发通知”拆成独立阶段。

3. 抽变化点

把 URL、阈值、环境参数从流程里剥出来。

4. 去全局状态

把隐藏在模块顶层的状态挪到上下文里。

5. 补最小测试

优先给最核心、最常改的函数补测试。

这样重构不会显得很猛,但更容易落地。

十四、一个实际排错案例

来看一个很典型的现象:

  • 巡检脚本在测试环境正常
  • 到生产环境后,有时会重复发告警,有时又完全不发

排查下来,根因常常不是通知接口不稳定,而是:

  • FAILED_ITEMS 放在全局列表里
  • 前一轮执行没清干净
  • 下一轮又继续复用

排查顺序通常可以这样走:

  1. 先看状态存在哪
  2. 再看状态是函数内局部,还是模块级共享
  3. 最后发现问题不是逻辑分支,而是状态泄漏

修复方式往往不复杂:

  • 去掉全局列表
  • 每次执行创建新的上下文对象
  • 把失败项作为返回值往下传

这类案例很有代表性,因为它说明:

  • 很多坏味道最后暴露出来的不是“不好看”
  • 而是线上行为不稳定

十五、什么时候该停下,不要把重构做成重写

重构里另一个常见陷阱是:

  • 发现坏味道很多
  • 一下子想把整个项目全部推倒

这通常不是最稳的做法。

更实用的判断通常是:

1. 先停在“结构已清楚、测试已能覆盖”的位置

如果职责边界已经收住、核心函数已经可测,未必还需要继续做更大的结构动作。

2. 没证据证明有收益的重构,先别做

例如只是因为“看起来还能更优雅”,但对测试、排障、交付没有直接收益,这种重构优先级通常没那么高。

3. 把重构做成一系列小步,而不是一次性重写

小步重构更容易:

  • 观察行为变化
  • 回滚
  • 控制风险

真正稳的重构不是“改得最多”,而是“每一步都能解释为什么值得改”。

十六、一个实际练习

练习目标:给一个已有 Python 脚本做最小重构。

要求:

  1. 找出 3 个最明显的坏味道
  2. 不改业务结果,只改结构
  3. 至少拆出 3 个职责函数
  4. 去掉 1 个隐藏全局状态
  5. 补 2 条最小测试
  6. 写下“这次没继续重构的部分为什么先不动”

如果这个练习能独立做完,说明已经开始有重构判断力,而不是只会“感觉乱”。

十七、这一篇学完以后,下一步应该补什么

如果这一篇已经真正吸收,后面最适合继续补的是:

  1. 项目结构设计
  2. pytest 夹具与测试分层
  3. Python 在不同工程场景里的角色边界

因为重构的本质,最终还是为了把结构、测试和职责边界重新收清楚。

十八、结语

Python 的坏味道之所以常见,不是因为这门语言差,而是因为:

  • 写得快
  • 试错成本低
  • 很容易从一个小脚本长成一组长期工具

真正要建立的不是“代码必须写得很优雅”,而是:

  • 什么时候已经开始失控
  • 先该重构哪一层
  • 怎么重构才不会越改越乱

能把这些判断建立起来,Python 项目才会从“越来越难碰”走向“还能继续演进”。