上周三晚上,我跑一个联邦学习的实验代码,数据集不大,大概50万条。点了运行,去睡觉。
第二天早上起来看,还没跑完。进度条卡在37%,估算剩余时间:11小时。
同门的师兄看了一眼我的代码,改了3行,重新跑。5分钟出结果。
当时我觉得自己像个傻子。但后来发现,这不是我一个人的问题——大部分科研人写Python,从来不考虑性能。代码能跑就行,直到跑不动了才慌。
这篇文章把我踩过的所有Python性能坑总结出来。不需要你是计算机专业的,跟着改就行。
一、为什么你的Python这么慢?
先说结论:90%的慢,不是Python本身的问题,是写法的问题。
Python慢的根源通常就3个:
- 用了for循环处理大数据
- 频繁创建和销毁对象
- 没利用多核CPU
下面逐个解决。
二、第1个坑:for循环是性能杀手
错误写法(慢100倍)
假设你要对50万条数据做平方运算。大部分人的写法:
import numpy as np data = np.random.rand(500000) result = [] for x in data: result.append(x ** 2) result = np.array(result)
这个写法在我的机器上跑:3.2秒。看起来不慢?等数据量到500万就变成32秒,到5000万就要5分钟。
正确写法(向量化)
result = data ** 2 # 就这一行
同样的操作:0.008秒。快了400倍。
原理很简单:for循环是逐个元素处理,每次都要Python解释器介入。而NumPy的向量化操作底层是C语言实现的,一次性处理整个数组,跳过了Python解释器的开销。
记住一条铁律:在NumPy/Pandas里看到for循环,先想想能不能换成向量化操作。90%的情况下可以。
三、第2个坑:DataFrame的iterrows陷阱
处理表格数据时,很多人喜欢用Pandas的iterrows逐行处理:
# 慢写法:逐行处理 for index, row in df.iterrows(): if row['score'] > 80: df.loc[index, 'grade'] = 'A' else: df.loc[index, 'grade'] = 'B'
10万行数据,这个写法要跑15秒。
正确写法:用apply或np.where
# 快写法1:np.where import numpy as np df['grade'] = np.where(df['score'] > 80, 'A', 'B') # 快写法2:apply(复杂逻辑用这个) df['grade'] = df['score'].apply(lambda x: 'A' if x > 80 else 'B')
同样的10万行:0.03秒。快了500倍。
如果逻辑更复杂(需要多列参与计算),用np.select:
conditions = [ df['score'] > 90, df['score'] > 80, df['score'] > 60 ] choices = ['A', 'B', 'C'] df['grade'] = np.select(conditions, choices, default='F')
一行搞定多条件判断,比iterrows快几百倍。
四、第3个坑:重复计算
这个坑很隐蔽,但极其常见。看这段代码:
# 慢写法:每次循环都重新计算 for col in columns: normalized = (df[col] - df[col].mean()) / df[col].std() df[col + '_norm'] = normalized
看起来没毛病,但如果columns有100列,每次循环Pandas都要重新扫描整个列做统计。可以改成一次性计算:
# 快写法:向量化标准化 means = df[columns].mean() stds = df[columns].std() df_norm = (df[columns] - means) / stds # 一次性加后缀 for col in columns: df[col + '_norm'] = df_norm[col]
100列数据,从12秒降到0.2秒。
五、第4个坑:没用多进程
Python有GIL(全局解释器锁),默认只能用一个CPU核心。你的电脑8核16线程,Python只用了1核,其余15个在围观。
如果你要跑参数搜索(比如不同超参数组合训练模型),用multiprocessing:
from multiprocessing import Pool def train_with_params(params): # 你的训练逻辑 model = train_model(params) return model.score() if __name__ == '__main__': param_list = generate_param_grid() # 100组参数 with Pool(processes=8) as pool: # 用8个核 results = pool.map(train_with_params, param_list)
100组参数,单核跑10小时,8核并行1.5小时搞定。
注意:多进程适合CPU密集型任务(模型训练、数值计算)。如果是IO密集型(读文件、网络请求),用多线程或asyncio就行。
六、一张图总结:Python性能优化优先级
| | | |
| | | |
| iterrows → np.where/apply | | |
| | | |
| | | |
| | | |
前3个改完,90%的性能问题都解决了。Numba是进阶玩法,适合数值计算密集的代码——在函数上加一个@jit装饰器,Python直接编译成机器码执行,速度接近C语言。
七、常见坑补充
坑1:用list存数据而不是numpy array
Python list的每个元素都是独立对象,内存占用是numpy array的10-20倍,操作速度也慢得多。数值数据一律用numpy array。
坑2:Pandas的inplace=True不一定快
很多人觉得inplace=True更省内存,但实际上Pandas底层有时会先创建副本再修改。不要依赖inplace,改用链式赋值更清晰。
坑3:读取大文件用read_csv全量加载
文件超过1GB时,read_csv会卡死。解决方案:用chunksize参数分块读取,或换用Dask(类似Pandas但支持并行和懒加载)。
写在最后
科研代码不需要写得多优雅,但"能跑"和"能跑完"是两回事。我见过太多人因为代码太慢,实验跑了一周才出一个结果,而别人一下午跑了20组实验。
性能优化的投入产出比极高:花1小时改写法,省下来的可能是几百小时的等待时间。
关注后回复【Python性能】,领取:
- 本文所有代码的完整可运行版本(Jupyter Notebook)
- NumPy/Pandas向量化操作速查表(一张图覆盖90%常用场景)
- multiprocessing参数搜索模板代码(直接套用)
你跑Python最慢的一次等了多久?评论区吐槽,点赞最高的送一份完整性能优化手册。
这是「科研人效率工具系列」第2期,上期:导师催交论文,我用Zotero半天整理了200篇文献。下期预告:用AI 10分钟读完100篇文献(真的能做到)
— 关注「科研创新社」,每天一个科研效率提升技巧 —