系列文章
Python 从入门到精通
第 34 / 36 篇
从环境安装和基础语法出发,逐步学习工程实践、自动化、数据处理与 Web 开发。
-
01
Python 怎么装、怎么用 -
02
变量、数字和字符串 -
03
列表:把一组数据放在一起 -
04
元组、集合和字典 -
05
条件判断:让程序做选择 -
06
循环:重复的事交给程序 -
07
函数:把代码整理成可复用的块 -
08
模块与包:拆分你的程序 -
09
输入、输出与字符串格式化 -
10
文件读写:保存程序的数据 -
11
异常处理:程序出错时怎么办 -
12
基础阶段练习:命令行记账本 -
13
类和对象:面向对象入门 -
14
继承、组合与特殊方法 -
15
迭代器与生成器 -
16
列表推导式与生成器表达式 -
17
装饰器:给函数增加能力 -
18
上下文管理器与 with -
19
类型标注与 dataclass -
20
正则表达式:从文本中找规律 -
21
日期、时间与时区 -
22
日志与调试 -
23
虚拟环境与依赖管理 -
24
测试:让修改不再提心吊胆 -
25
网络请求:用 Python 调用 API -
26
网页解析与合规采集 -
27
操作 Excel、CSV 与批量文件 -
28
SQLite:给程序加一个数据库 -
29
数据分析入门:NumPy 与 Pandas -
30
画图:把数据变得直观 -
31
Flask 入门:做一个小网站 -
32
异步编程:同时处理多项任务 -
33
线程、进程与并发选择 -
34
性能分析与优化正在阅读 -
35
项目结构、配置与发布 -
36
综合项目:从需求到上线
程序变慢时,第一反应别是”优化”,而是先问一句:慢在哪?盲优化就像没诊断就开药。这一篇讲性能分析的正确姿势:先测量、再定位、最后优化,以及最常见的优化手段。
第一原则:先测量,再优化
优化的黄金流程:
- 测量:哪里慢?慢多少?(用工具,别用感觉)
- 定位:瓶颈在哪个函数、哪一行?
- 优化:针对瓶颈改,改完再测验证
反例:凭感觉觉得”列表慢”,改成复杂结构,结果瓶颈根本不在那——白忙一场还可能变慢。还有一句名言:“过早优化是万恶之源”——先让代码正确、可读,再谈性能。
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,大数据用生成器
- 一次性脚本、外部瓶颈、小数据量——不值得优化
练习
- 用 timeit 对比:列表推导式 vs for 循环 append,谁快?快多少?
- 用 cProfile 分析一个包含”字符串拼接循环”的程序,找到瓶颈并优化
- 思考:为什么”先优化再测量”是反模式?举个”优化了半天发现瓶颈在别处”的场景