系列文章

Python 从入门到精通

第 34 / 36 篇

从环境安装和基础语法出发,逐步学习工程实践、自动化、数据处理与 Web 开发。

  1. 01
    Python 怎么装、怎么用
  2. 02
    变量、数字和字符串
  3. 03
    列表:把一组数据放在一起
  4. 04
    元组、集合和字典
  5. 05
    条件判断:让程序做选择
  6. 06
    循环:重复的事交给程序
  7. 07
    函数:把代码整理成可复用的块
  8. 08
    模块与包:拆分你的程序
  9. 09
    输入、输出与字符串格式化
  10. 10
    文件读写:保存程序的数据
  11. 11
    异常处理:程序出错时怎么办
  12. 12
    基础阶段练习:命令行记账本
  13. 13
    类和对象:面向对象入门
  14. 14
    继承、组合与特殊方法
  15. 15
    迭代器与生成器
  16. 16
    列表推导式与生成器表达式
  17. 17
    装饰器:给函数增加能力
  18. 18
    上下文管理器与 with
  19. 19
    类型标注与 dataclass
  20. 20
    正则表达式:从文本中找规律
  21. 21
    日期、时间与时区
  22. 22
    日志与调试
  23. 23
    虚拟环境与依赖管理
  24. 24
    测试:让修改不再提心吊胆
  25. 25
    网络请求:用 Python 调用 API
  26. 26
    网页解析与合规采集
  27. 27
    操作 Excel、CSV 与批量文件
  28. 28
    SQLite:给程序加一个数据库
  29. 29
    数据分析入门:NumPy 与 Pandas
  30. 30
    画图:把数据变得直观
  31. 31
    Flask 入门:做一个小网站
  32. 32
    异步编程:同时处理多项任务
  33. 33
    线程、进程与并发选择
  34. 34
    性能分析与优化正在阅读
  35. 35
    项目结构、配置与发布
  36. 36
    综合项目:从需求到上线

查看整个系列 →

程序变慢时,第一反应别是”优化”,而是先问一句:慢在哪?盲优化就像没诊断就开药。这一篇讲性能分析的正确姿势:先测量、再定位、最后优化,以及最常见的优化手段。

第一原则:先测量,再优化

优化的黄金流程:

  1. 测量:哪里慢?慢多少?(用工具,别用感觉)
  2. 定位:瓶颈在哪个函数、哪一行?
  3. 优化:针对瓶颈改,改完再测验证

反例:凭感觉觉得”列表慢”,改成复杂结构,结果瓶颈根本不在那——白忙一场还可能变慢。还有一句名言:“过早优化是万恶之源”——先让代码正确、可读,再谈性能。

timeit:精确测小段代码

import timeit

# 测一段代码执行时间(自动跑多次取平均,很准)
t = timeit.timeit("[x * x for x in range(1000)]", number=10000)
print(f"平均每次:{t / 10000 * 1000:.3f} 毫秒")

# 测函数
def compute():
    return sum(x * x for x in range(1000))

t = timeit.timeit(compute, number=10000)
print(f"compute 平均:{t / 10000 * 1000:.3f} 毫秒")

命令行版更快:

python -m timeit "[x*x for x in range(1000)]"

timeit 适合对比两种写法谁快(比如列表推导式 vs 循环)。

cProfile:找出瓶颈函数

程序整体慢,不知道哪个函数拖后腿,用 cProfile 做性能剖析:

import cProfile
import pstats

def process_data():
    data = [i * 2 for i in range(100_000)]
    total = 0
    for d in data:
        if d % 3 == 0:
            total += d
    return total

def main():
    for _ in range(10):
        process_data()

# 剖析并输出统计
profiler = cProfile.Profile()
profiler.enable()
main()
profiler.disable()
stats = pstats.Stats(profiler)
stats.sort_stats("cumulative").print_stats(10)   # 按累计耗时排序,看前 10

输出关键列:ncalls 调用次数、tottime 自身耗时(不含子调用)、cumtime 累计耗时(含子调用)。看 cumtime 最大的,那就是瓶颈

更简单:命令行直接跑:

python -m cProfile -s cumulative my_program.py

常见优化手段(按性价比排序)

1. 选对数据结构(性价比最高)

# 慢:列表里查"在不在"(O(n),1 万个元素要查 1 万次)
names = ["小明", "小红", "小刚", ...]
if "小刚" in names: ...

# 快:集合查"在不在"(O(1),不管多大都是瞬间)
name_set = {"小明", "小红", "小刚", ...}
if "小刚" in name_set: ...

# 字典按键查找同样是 O(1),比列表遍历快得多

2. 用推导式/生成器代替手写循环(快 1.5-2 倍,还更简洁)

# 慢
result = []
for x in data:
    result.append(x * 2)
# 快
result = [x * 2 for x in data]

3. 局部变量缓存全局查找

# 慢:循环里每次都要查全局
import math
for x in range(100_000):
    y = math.sqrt(x)
# 快:把函数提到局部
sqrt = math.sqrt
for x in range(100_000):
    y = sqrt(x)

4. 避免重复计算

# 慢:循环里重复调用 len
for i in range(len(data)): ...
# 快
n = len(data)
for i in range(n): ...

5. 字符串拼接用 join 而不是 +

# 慢:循环里字符串 +,每次生成新字符串(O(n²))
s = ""
for word in words:
    s += word
# 快:join 一次拼完(O(n))
s = "".join(words)

大数据的优化:NumPy 和生成器

# 数值计算:NumPy 比纯 Python 循环快几十倍
import numpy as np

# 慢:纯 Python 循环算 100 万个数的平方和
data = list(range(1_000_000))
total = sum(x * x for x in data)

# 快:NumPy 向量化
arr = np.arange(1_000_000)
total = (arr * arr).sum()

# 内存优化:大数据用生成器,别一次全装
def read_lines(path):
    with open(path, "r", encoding="utf-8") as f:
        for line in f:
            yield line.strip()     # 逐行产出,内存恒定

什么时候不值得优化

这些情况别折腾:

  • 只跑一次的程序(脚本):慢 0.5 秒无所谓,优化花半小时不划算
  • 瓶颈在外部(网络、数据库):优化代码没用,改架构或加缓存
  • 数据量小:1 万个元素,列表和集合差距微秒级,别在意
  • 可读性被破坏:为了 10% 提速把代码写成人看不懂的谜语,亏

优化的正确心态:先让它对,再让它快;快不了就别硬快

新手坑

坑 1:凭感觉优化。不测量就改,改完可能更慢。先 timeit/cProfile。

坑 2:优化了不验证。改完必须重测对比,确认真的变快了(且结果没变)。

坑 3:微优化上瘾。i += 1 改成 i = i + 1 这种,浪费时间还伤可读性。优化大头的 O(n²) → O(n),别抠常数。

小结

  • 流程:测量 → 定位 → 优化 → 复测;先正确后可读,最后才性能
  • timeit 测小段代码,cProfile 找瓶颈函数(看 cumtime)
  • 高性价比优化:选对数据结构(集合/字典 O(1))、推导式、局部变量、join 拼串
  • 数值计算用 NumPy,大数据用生成器
  • 一次性脚本、外部瓶颈、小数据量——不值得优化

练习

  1. 用 timeit 对比:列表推导式 vs for 循环 append,谁快?快多少?
  2. 用 cProfile 分析一个包含”字符串拼接循环”的程序,找到瓶颈并优化
  3. 思考:为什么”先优化再测量”是反模式?举个”优化了半天发现瓶颈在别处”的场景