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 做性能分析,找到真正的瓶颈,再针对性地优化。别一开始就想着「优化所有代码」,那样只会浪费时间。

好了,这一章的内容就到这里。记住:数据结构选对,循环少写,函数少调。下一章咱们聊聊内存管理和垃圾回收,那又是一个大坑。