2. 代码层面优化:Python代码执行效率分析、使用内置函数与库、避免循环中的重复计算、合理使用数据结构

好,咱们直接进入正题。代码层面的优化,说白了就是让你写的 Python 跑得更快、更稳。很多同学写脚本,功能是实现了,但一跑就是半小时,CPU 还飙到 100%。这其实不是 Python 慢,是你没用好它。

我个人习惯,拿到一个脚本,先不急着改逻辑。我会先问自己三个问题:这段代码哪里最慢?有没有更快的写法?数据结构选对了吗?今天咱们就围绕这几个点,把 Python 代码的执行效率掰开揉碎了讲清楚。

2.1 Python 代码执行效率分析:先找到瓶颈再动手

优化之前,你得先知道慢在哪。凭感觉猜?不靠谱。我见过太多人上来就重构整个函数,结果发现慢的其实是另一段代码。

这里我推荐两个工具:timeitcProfile

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 倍以上
核心原则: 能用内置函数解决的问题,绝对不要自己写。你写的代码再快,也快不过 C 语言实现的底层库。

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 实现的,速度更快。

我整理了一个数据结构选择指南,你写代码时可以参考:

操作场景 推荐数据结构 原因
频繁查找元素 setdict O(1) 查找,比列表快得多
频繁插入/删除 collections.deque 两端操作 O(1),列表是 O(n)
需要保持顺序 listcollections.OrderedDict 列表适合索引访问,OrderedDict 适合有序字典
需要去重 set 天然去重,O(1) 操作
需要计数 collections.Counter 专为计数设计,性能极佳
需要优先级队列 heapq 堆操作 O(log n),比排序快
我的经验: 写代码前先想清楚数据的特点。如果是读多写少,用列表加索引;如果是写多读多,用字典或集合。别等到代码跑不动了才回头改数据结构,那代价太大了。

好了,代码层面的优化,核心就是这四点:先分析瓶颈、多用内置函数、避免循环重复计算、选对数据结构。你把这四点记牢了,写出来的脚本至少能快 50%。下一章咱们聊聊内存管理和 I/O 优化,那又是另一个世界了。