Python:性能优化入门,定位慢代码、减少无效对象和处理 I/O 瓶颈
Python 说到性能优化,最容易出现的误区不是“不会优化”,而是优化顺序一开始就错了。
最常见的情况通常是:
- 脚本一慢,就开始盲目换写法
- 一看到循环,就怀疑是不是
for太慢 - 一看到 Python,就默认问题一定在语言本身
但实际项目里,性能问题通常没那么抽象。更常见的是:
- 文件读写次数太多
- 同样的数据反复构造对象
- 不必要的字符串拼接太频繁
- 本该流式处理的数据一次性全读进内存
- 真正慢的是 I/O,不是那几行 Python 运算
这一篇不按“优化技巧大全”来写,而是直接围绕一个实际脚本展开:做一个日志汇总慢查询脚本。
这个脚本要完成这些事:
- 读取一批日志文件
- 提取每个接口的耗时
- 汇总慢请求数量
- 输出一份按接口分组的统计报告
用这个场景把性能优化入门最核心的三件事串起来:
- 先定位慢代码在哪里
- 再减少无效对象和重复工作
- 最后看 I/O 瓶颈怎么处理
一、这篇文章要解决什么问题
读完这一篇,应该能独立完成这些动作:
- 用最小手段定位一段 Python 脚本慢在哪
- 分清 CPU 计算慢和 I/O 读写慢
- 识别几类典型低效写法,例如重复解析、无意义中间列表、大量字符串累加
- 选择更合适的处理方式,例如流式读取、一次遍历、多用生成器
如果这些动作能独立做出来,性能优化就不会再是“看见慢就乱改”,而是开始有判断顺序。
二、先看这篇文章要完成的实际功能
假设现在有一批访问日志文件,每一行像这样:
1 | GET /orders 120 |
现在要做一个脚本,输出类似这样:
1 | 总日志数: 5 |
并且把慢请求结果写进 slow_report.txt。
这个需求不复杂,但已经足够把定位、对象分配、I/O 三块问题串起来。
三、先写一个最容易变慢的版本
很多脚本第一版通常会长这样:
1 | from pathlib import Path |
这个版本当然能跑,但已经有几个很明显的性能风险:
- 先把所有日志都堆进
all_lines - 每一行都做多次中间对象创建
- 输出时逐条写文件
这些问题在小数据下不明显,一旦日志量上来就会开始放大。
四、性能优化第一步不是改代码,而是先定位
1. 先用最小时间统计确认哪段慢
先别上复杂 profiler,先给主要步骤加最小计时。
1 | import time |
输出可能类似这样:
1 | 读取阶段耗时: 1.82 |
这一步的意义非常大,因为它先回答一个最基本的问题:
- 到底是读取慢
- 还是处理逻辑慢
2. 一个实际错误:没定位就先改循环写法
第一步很容易先改成:
- 列表推导式
mapjoin
但如果真正慢的是文件读写,这些改动通常没太大价值。
性能优化最怕的不是不会技巧,而是优化了错误的地方。
五、第二步看无效对象和重复工作
1. 先看看这段代码里有哪些重复对象创建
这段代码里至少有这些额外成本:
content.splitlines()会先把整个文件拆成列表all_lines.append()又把所有行再存一次line.split()每次循环都创建新列表parts[0] + " " + parts[1]每次都创建新字符串
单看一条都不大,但量大时就会变成明显成本。
2. 实际优化方向:一边读,一边处理
更直接的写法通常是:
1 | from pathlib import Path |
这次优化不是“技巧更高级”,而是减少了几件实际浪费:
- 不再存整批
all_lines - 不再多做一次完整遍历
- 不再提前把整个文件拆成列表
3. 一个实际错误:为了省内存把可复用结果也流掉了
有时候会把所有东西都写成生成器,结果后面还要重复消费,反而更乱。
所以优化不是“对象越少越好”,而是:
- 该流式处理的流式处理
- 该保留结果的保留结果
像 slow_lines 这种后面还要输出到文件的结果,保留列表就合理。
六、第三步再看 I/O 瓶颈
1. 逐文件整体读取和逐行读取,差别在哪
这两种写法看起来都能跑:
1 | content = file_path.read_text(encoding="utf-8") |
和:
1 | with file_path.open("r", encoding="utf-8") as f: |
第二种的优势在于:
- 不需要一次把整个文件都读进内存
- 更适合大文件
2. 输出也有 I/O 成本
错误写法:
1 | with open("slow_report.txt", "w", encoding="utf-8") as f: |
这当然可以,但如果数据很多,频繁写也会有额外成本。
更直接的写法通常是:
1 | with open("slow_report.txt", "w", encoding="utf-8") as f: |
3. 一个实际错误:把所有性能问题都归因到 Python 本身
如果日志文件本身就在机械盘、网络盘、远程挂载目录里,I/O 慢是很常见的。
这时候再怎么折腾 for 和 list,收益也可能有限。
所以性能优化里一个很基本的判断是:
- 问题在代码结构
- 还是在外部读写环境
七、把定位、对象优化和 I/O 优化真正串起来
现在把前面的内容整理成一个更稳的版本。
slow_report.py:
1 | import time |
执行:
1 | python slow_report.py |
输出可能类似这样:
1 | 总日志数: 5 |
八、再补一层:什么时候才值得上 profiler
如果最小时间统计已经告诉你:
- 某段逻辑明显慢
- 而且脚本也开始变大
这时候再考虑 cProfile 就更合适。
例如:
1 | python -m cProfile slow_report.py |
这个命令的价值不是让你一上来就读懂所有统计表,而是:
- 当简单计时已经不够细时
- 再进一步定位函数级别热点
一个实际错误
有时一上来就直接跑 profiler,但连主流程分几步都没拆开。
这样就算拿到数据,也很难解释。
所以更顺的顺序通常是:
- 先拆阶段
- 再做最小计时
- 最后才上 profiler
九、一个实际排错场景
这类脚本里一个非常常见的实际问题是:数据量一大,脚本不是单纯变慢,而是内存明显上涨。
这时排查顺序通常很直接:
- 先看是不是把整批文件内容都读进了列表
- 再看是不是构造了多层中间列表
- 再看结果里哪些数据其实不需要长期保留
如果最后发现类似:
1 | all_lines = [] |
那根因通常就不是某一行 if 慢,而是数据保留方式本身太重了。
十、一个实际练习
可以直接把这一篇变成一个完整练习。
练习目标:做一个“接口日志慢请求分析脚本”。
要求:
- 读取多个日志文件
- 统计总请求数和慢请求数
- 输出每个接口的请求次数
- 把慢请求写入报告文件
- 至少比较一次“整批读入”和“逐行读取”的差异
- 至少补一个实际错误场景,例如整批读入导致内存涨得明显
如果这个练习能独立做完,说明性能优化入门这一层已经开始真正掌握。
十一、这篇文章学完以后,下一步应该补什么
如果这一篇已经能跟着做完,下一步最适合继续补的是:
- 数据结构与算法入门:列表、链表、栈、队列、哈希到底怎么理解
- 常见算法题的工程价值:双指针、滑动窗口、递归、回溯、动态规划怎么入门
- 更系统的测试和性能基准怎么继续接进项目
因为到这一步,已经开始不只是写脚本,而是在判断“这段代码为什么慢,应该先改哪一层”。
十二、结语
Python 性能优化最值得先建立的,不是技巧清单,而是判断顺序。
只要先按这条线走:
- 先定位
- 再减少无效对象和重复工作
- 最后判断是不是 I/O 瓶颈
很多问题都会比盲目改写法更容易收敛。