3、代码级优化(Python):数据结构选择、循环优化、减少函数调用开销
Python 的性能优化,说白了就是跟解释器斗智斗勇。很多人觉得 Python 慢,其实大部分时候是代码写得不够「Pythonic」。我见过太多人把 Python 当 C 语言写,结果性能惨不忍睹。今天咱们就聊聊三个最核心的优化点:数据结构怎么选、循环怎么写、函数调用怎么省。
3.1 数据结构选择:选对了,性能翻倍
数据结构的选择,直接影响代码的执行效率。我个人习惯是:能用内置的,就别自己造轮子。Python 的内置数据结构都是 C 语言实现的,性能远比你手写的循环快得多。
3.1.1 列表 vs 元组:可变性的代价
列表和元组看起来很像,但性能差距不小。元组是不可变的,Python 解释器可以做一些优化,比如缓存哈希值、减少内存分配。我在项目中遇到过,把一个频繁访问的列表改成元组后,内存占用降低了 15%。
核心原则:如果数据不需要修改,就用元组。不仅更安全,而且更快。
# 慢:频繁修改的列表
data = [1, 2, 3, 4, 5]
for i in range(1000000):
x = data[2] # 每次都要检查列表是否被修改
# 快:不可变的元组
data = (1, 2, 3, 4, 5)
for i in range(1000000):
x = data[2] # 解释器直接缓存,无需检查
3.1.2 字典 vs 集合:查找的艺术
字典和集合底层都是哈希表,查找时间复杂度是 O(1)。但很多人不知道,集合的查找比字典更快,因为集合只存储键,不存储值,内存布局更紧凑。
我曾经优化过一个日志去重程序,原来用列表去重,跑了 20 分钟。改成集合后,3 秒就搞定了。你想想看,这差距有多大?
# 慢:列表去重
unique_items = []
for item in large_list:
if item not in unique_items: # O(n) 查找
unique_items.append(item)
# 快:集合去重
unique_items = list(set(large_list)) # O(1) 查找
3.1.3 选择合适的数据结构
| 操作 | 推荐数据结构 | 时间复杂度 | 我的建议 |
|---|---|---|---|
| 频繁查找 | set / dict | O(1) | 别用 list,除非数据量很小 |
| 频繁插入/删除 | collections.deque | O(1) | list 的 insert(0) 是 O(n) |
| 有序数据 | list / tuple | O(1) 索引 | bisect 模块做二分查找 |
| 计数统计 | collections.Counter | O(n) | 比手写字典快 2-3 倍 |
3.2 循环优化:别让循环成为瓶颈
循环是 Python 性能的「重灾区」。Python 的循环很慢,因为每次迭代都要做类型检查、边界检查。我建议:能不用循环,就不用循环。
3.2.1 用内置函数替代循环
Python 的内置函数,比如 map()、filter()、sum(),都是用 C 实现的,比手写循环快得多。我记得有一次优化一个数据处理脚本,把 for 循环改成 map(),速度提升了 5 倍。
# 慢:手写循环
result = []
for i in range(1000000):
result.append(i * 2)
# 快:列表推导式
result = [i * 2 for i in range(1000000)]
# 更快:map 函数
result = list(map(lambda x: x * 2, range(1000000)))
3.2.2 减少循环内的函数调用
循环内的函数调用开销很大。每次调用都要创建栈帧、传递参数。我见过有人把 len() 放在循环条件里,每次迭代都调用一次,白白浪费性能。
避坑指南:我曾经优化过一个爬虫程序,发现循环里调用了 time.time() 来记录时间。每次迭代都调用系统调用,性能直接腰斩。改成在循环外获取时间,问题就解决了。
# 慢:循环内重复计算
for i in range(len(data)): # 每次迭代都调用 len()
process(data[i])
# 快:提前计算
n = len(data)
for i in range(n):
process(data[i])
# 更快:直接迭代
for item in data:
process(item)
3.2.3 局部变量加速
Python 访问局部变量比全局变量快。因为局部变量存在栈上,全局变量存在字典里,需要哈希查找。我建议:把频繁使用的全局变量赋值给局部变量。
# 慢:频繁访问全局变量
import math
def slow_func():
for i in range(1000000):
x = math.sqrt(i) # 每次都要查找 math 模块
# 快:局部变量缓存
def fast_func():
sqrt = math.sqrt # 赋值给局部变量
for i in range(1000000):
x = sqrt(i) # 直接访问局部变量
3.3 减少函数调用开销:能省则省
函数调用在 Python 中开销不小。每次调用都要创建栈帧、分配内存。我见过一些代码,把简单的逻辑拆成几十个小函数,结果性能惨不忍睹。
3.3.1 内联小函数
对于只有一两行代码的小函数,直接内联到调用处。别为了「代码整洁」牺牲性能。我个人习惯是:函数超过 10 行才考虑封装。
# 慢:过度封装
def add_one(x):
return x + 1
def process_data(data):
return [add_one(x) for x in data]
# 快:内联
def process_data(data):
return [x + 1 for x in data]
3.3.2 使用默认参数缓存
Python 的函数默认参数在定义时只计算一次。利用这个特性,可以缓存一些昂贵的计算结果。嗯,这里要注意:默认参数必须是不可变对象,否则会有坑。
# 慢:每次调用都计算
def get_config():
with open('config.json') as f:
return json.load(f)
# 快:默认参数缓存
def get_config(config=None):
if config is None:
with open('config.json') as f:
config = json.load(f)
return config
3.3.3 用装饰器做缓存
对于纯函数(同样的输入,同样的输出),可以用 functools.lru_cache 做缓存。我优化过一个递归计算斐波那契数列的程序,加上缓存后,从指数级复杂度降到了线性。
from functools import lru_cache
# 慢:重复计算
def fib(n):
if n < 2:
return n
return fib(n-1) + fib(n-2)
# 快:缓存结果
@lru_cache(maxsize=128)
def fib(n):
if n < 2:
return n
return fib(n-1) + fib(n-2)
我的经验:代码优化不是一蹴而就的。我建议先用 cProfile 做性能分析,找到真正的瓶颈,再针对性地优化。别一开始就想着「优化所有代码」,那样只会浪费时间。
好了,这一章的内容就到这里。记住:数据结构选对,循环少写,函数少调。下一章咱们聊聊内存管理和垃圾回收,那又是一个大坑。