2. 代码层面优化:Python代码执行效率分析、使用内置函数与库、避免循环中的重复计算、合理使用数据结构
好,咱们直接进入正题。代码层面的优化,说白了就是让你写的 Python 跑得更快、更稳。很多同学写脚本,功能是实现了,但一跑就是半小时,CPU 还飙到 100%。这其实不是 Python 慢,是你没用好它。
我个人习惯,拿到一个脚本,先不急着改逻辑。我会先问自己三个问题:这段代码哪里最慢?有没有更快的写法?数据结构选对了吗?今天咱们就围绕这几个点,把 Python 代码的执行效率掰开揉碎了讲清楚。
2.1 Python 代码执行效率分析:先找到瓶颈再动手
优化之前,你得先知道慢在哪。凭感觉猜?不靠谱。我见过太多人上来就重构整个函数,结果发现慢的其实是另一段代码。
这里我推荐两个工具:timeit 和 cProfile。
timeit 适合测小段代码。比如你想比较列表推导式和 for 循环谁快,直接跑一下就知道。
import timeit
# 测一下列表推导式
list_comp_time = timeit.timeit('[i**2 for i in range(1000)]', number=10000)
print(f'列表推导式耗时: {list_comp_time:.4f} 秒')
# 测一下普通 for 循环
for_loop_time = timeit.timeit('''
s = []
for i in range(1000):
s.append(i**2)
''', number=10000)
print(f'普通 for 循环耗时: {for_loop_time:.4f} 秒')
你看,结果一目了然。列表推导式通常比 for 循环快 20%-30%。为什么会这样?因为列表推导式在底层是用 C 语言实现的,省去了 Python 解释器反复执行字节码的开销。
如果是整个脚本的性能分析,那就用 cProfile。它能告诉你每个函数调用了多少次、花了多少时间。
import cProfile
def my_slow_function():
total = 0
for i in range(1000000):
total += i ** 2
return total
cProfile.run('my_slow_function()')
输出结果里,重点关注 cumtime(累计时间)和 ncalls(调用次数)。如果某个函数调用次数不多但累计时间很长,那它就是你要优化的目标。
profile 模块的 enable() 和 disable()),只分析关键路径上的代码,避免影响整体性能。
2.2 使用内置函数与库:别重复造轮子
Python 的内置函数和标准库,都是用 C 写的。你想想看,你用 Python 写个循环,每执行一次都要经过解释器;而内置函数直接调用 C 代码,速度能差一个数量级。
举个例子,你要对一个列表求和。新手可能会这么写:
# 慢
total = 0
for num in my_list:
total += num
而老手会直接用 sum():
# 快
total = sum(my_list)
就这么简单的一改,速度能快 3-5 倍。我在项目中遇到过类似的情况,一个数据处理脚本跑 20 分钟,我把所有手写的累加、查找、排序都换成了内置函数,最后跑下来不到 3 分钟。
常用的内置函数和库,我列个表给你参考:
| 场景 | 不推荐写法 | 推荐写法 | 速度提升 |
|---|---|---|---|
| 列表求和 | for 循环累加 | sum() |
3-5 倍 |
| 查找元素 | 手写二分查找 | bisect 模块 |
2-3 倍 |
| 字符串拼接 | + 运算符 |
''.join() |
10 倍以上 |
| 排序 | 手写冒泡排序 | sorted() 或 list.sort() |
百倍以上 |
| 数学运算 | 手写平方根 | math.sqrt() |
10 倍以上 |
2.3 避免循环中的重复计算:把不变的东西提出来
这个坑,我踩过不止一次。循环里如果有重复计算,每次迭代都做一遍,那性能损耗是成倍增加的。
看个例子:
# 慢:每次循环都计算 len()
for i in range(len(my_list)):
print(my_list[i])
# 快:只计算一次
n = len(my_list)
for i in range(n):
print(my_list[i])
你可能觉得,len() 很快啊,多算几次无所谓。但如果是更复杂的计算呢?比如在循环里调用一个耗时的函数:
# 慢
for item in data:
result = expensive_function(item) * some_constant_value
process(result)
# 快
constant = some_constant_value # 提到循环外面
for item in data:
result = expensive_function(item) * constant
process(result)
我曾经在一个日志分析脚本里,循环中反复计算了一个正则表达式的编译结果。每次迭代都调用 re.compile(),导致脚本跑了 40 分钟。后来我把 re.compile() 提到循环外面,时间直接降到 5 分钟。
if 'error' in log_message.lower(),每次迭代都调用 lower()。后来改成 if 'error' in log_message 并提前把日志转小写,性能提升明显。记住,字符串操作、正则编译、文件打开这些操作,能提出来就提出来。
2.4 合理使用数据结构:选对了就赢了一半
数据结构选得好,代码跑得快还省内存。选得不好,那就是灾难。
我见过最典型的错误,就是用列表来频繁查找元素。列表的查找是 O(n) 的,数据量一大就完蛋。
# 慢:列表查找
user_list = ['user1', 'user2', 'user3', ...] # 假设有 10 万条
if 'target_user' in user_list: # O(n) 查找
print('找到了')
# 快:集合查找
user_set = {'user1', 'user2', 'user3', ...} # 同样 10 万条
if 'target_user' in user_set: # O(1) 查找
print('找到了')
集合和字典的查找是 O(1) 的,列表是 O(n) 的。数据量越大,差距越明显。10 万条数据,列表查找可能要几十毫秒,集合查找就是微秒级。
再比如,你要统计每个元素出现的次数。新手可能会用列表加循环:
# 慢
data = ['a', 'b', 'a', 'c', 'b', 'a']
counts = {}
for item in data:
if item in counts:
counts[item] += 1
else:
counts[item] = 1
而老手会用 collections.Counter:
# 快
from collections import Counter
data = ['a', 'b', 'a', 'c', 'b', 'a']
counts = Counter(data)
一行搞定,而且底层是用 C 实现的,速度更快。
我整理了一个数据结构选择指南,你写代码时可以参考:
| 操作场景 | 推荐数据结构 | 原因 |
|---|---|---|
| 频繁查找元素 | set 或 dict |
O(1) 查找,比列表快得多 |
| 频繁插入/删除 | collections.deque |
两端操作 O(1),列表是 O(n) |
| 需要保持顺序 | list 或 collections.OrderedDict |
列表适合索引访问,OrderedDict 适合有序字典 |
| 需要去重 | set |
天然去重,O(1) 操作 |
| 需要计数 | collections.Counter |
专为计数设计,性能极佳 |
| 需要优先级队列 | heapq |
堆操作 O(log n),比排序快 |
好了,代码层面的优化,核心就是这四点:先分析瓶颈、多用内置函数、避免循环重复计算、选对数据结构。你把这四点记牢了,写出来的脚本至少能快 50%。下一章咱们聊聊内存管理和 I/O 优化,那又是另一个世界了。