Python 2.7 向字典添加数据速度过慢,寻求优化方案
看到你说Python脚本处理订阅用户流量数据时,字典填充占了5秒,比PHP慢不少,我之前也遇到过类似的性能瓶颈,分享几个亲测有效的优化思路:
1. 减少嵌套字典的重复哈希查找
每次访问struct[subscriberId][key]都会做两次哈希查找(先找subscriberId对应的子字典,再找子字典里的key)。如果循环里多次操作同一个订阅用户的子字典,先把它存到局部变量里,能减少重复查找的开销:
# 优化前:每次都要两次哈希查找 for line in data_lines: sub_id, traffic = parse_line(line) struct[sub_id]['total_traffic'] += traffic struct[sub_id]['last_update'] = current_time # 优化后:只做一次哈希查找,后续操作局部变量 for line in data_lines: sub_id, traffic = parse_line(line) sub_dict = struct[sub_id] # 先获取子字典 sub_dict['total_traffic'] += traffic sub_dict['last_update'] = current_time
如果sub_id可能不存在,用struct.get(sub_id, lambda: {'total_traffic':0, 'last_update':None})先初始化,但注意避免可变对象的共享引用问题。
2. 用collections.defaultdict简化嵌套结构初始化
手动判断sub_id是否存在会增加分支逻辑的开销,defaultdict可以自动初始化缺失的子结构,代码更简洁的同时,内部实现也比手动判断更高效:
from collections import defaultdict # 定义默认的子结构,每次新增sub_id时自动创建 struct = defaultdict(lambda: {'total_traffic': 0, 'last_update': None}) # 直接操作即可,无需判断sub_id是否存在 for line in data_lines: sub_id, traffic = parse_line(line) struct[sub_id]['total_traffic'] += traffic
3. 先批量统计再填充嵌套结构
如果你的逻辑是逐行累加流量,可以先在单层字典里统计每个sub_id的总流量,再一次性填充到嵌套结构中。单层字典的操作比嵌套字典更轻量,能减少内存操作的次数:
# 先在单层字典统计 temp_traffic = defaultdict(int) for line in data_lines: sub_id, traffic = parse_line(line) temp_traffic[sub_id] += traffic # 再批量生成嵌套结构 struct = {} for sub_id, total in temp_traffic.items(): struct[sub_id] = { 'total_traffic': total, 'last_update': current_time, # 其他字段 }
4. 替换嵌套字典为更高效的数据结构
如果subscriberId是整数类型,且范围可控,用列表替代字典会大幅提升性能——列表的索引访问是O(1)的直接内存寻址,比字典的哈希查找快得多:
# 假设sub_id的最大值是已知的,或者可以提前统计 max_sub_id = get_max_subscriber_id() # 初始化每个位置的默认结构 struct = [{'total_traffic': 0, 'last_update': None} for _ in range(max_sub_id + 1)] # 直接通过索引访问,无需哈希查找 for line in data_lines: sub_id, traffic = parse_line(line) struct[sub_id]['total_traffic'] += traffic
如果sub_id是字符串,可以先建立一个sub_id -> 整数索引的映射表,再用列表存储,同样能获得性能提升。
5. 优化循环内的局部变量访问
Python中局部变量的查找速度远快于全局变量。如果你的处理逻辑在函数内,把常用的变量和函数都转为局部变量:
def process_traffic_data(file_path): # 把全局变量/函数转为局部变量 struct = defaultdict(lambda: {'total_traffic': 0}) parse_line = custom_parse_line # 自定义的行解析函数 current_time = get_current_time() with open(file_path, 'r', buffering=1024*1024) as f: for line in f: sub_id, traffic = parse_line(line) sub_dict = struct[sub_id] sub_dict['total_traffic'] += traffic sub_dict['last_update'] = current_time return struct
这样循环内的所有访问都是局部变量,能减少命名空间查找的开销。
6. 试试PyPy代替CPython
如果你的代码没有依赖CPython特有的C扩展,用PyPy运行脚本可能会带来数量级的性能提升——PyPy的JIT编译器会对循环和字典操作做优化,很多场景下性能能追上甚至超过PHP。
另外,你提到文件读取占了7秒,这部分也可以优化:比如设置更大的缓冲区(buffering=1024*1024),用mmap映射文件到内存,或者用csv模块(如果是CSV格式)替代手动拆分字符串,这些都能减少文件IO的耗时。
内容的提问来源于stack exchange,提问作者user9724397

