Python代码慢?这5个性能优化技巧让速度提升100倍
你一定有过这种感觉:
代码逻辑写对了,结果也对,但跑起来就是慢。一个循环要跑几分钟,一个数据处理脚本要等半小时,你盯着屏幕发呆,怀疑是不是电脑该换了。
其实大概率不是电脑的问题,而是你的代码"姿势"不对。Python本身确实不是最快的语言,但大多数人的Python代码连它应有速度的1%都没发挥出来。
下面这5个优化技巧,每一个都能带来数量级的性能提升。它们不是什么高深理论,而是实打实能让你今天就用上的方法。
技巧一:把for循环换成向量化(提升10-100倍)
这是最大的一颗低垂果实。绝大多数Python慢代码,罪魁祸首都是for循环。
慢代码长这样:
ini
# 计算100万个数的平方
import time
nums = list(range(1_000_000))
start = time.time()
result = []
for n in nums:
result.append(n ** 2)
end = time.time()
print(f"耗时:{end - start:.4f} 秒") # 约 0.15 秒
看着还行?那是因为数据量还不够大。当数据量到1000万时,这个循环要跑将近2秒。而且这只是最简单的计算,如果循环里涉及条件判断、字典查找、字符串操作,速度会进一步恶化。
优化后:
ini
import numpy as np
import time
nums = np.arange(1_000_000)
start = time.time()
result = nums ** 2 # 向量化运算,一行搞定
end = time.time()
print(f"耗时:{end - start:.6f} 秒") # 约 0.002 秒
75倍的差距。 而且代码还更短更清晰了。
为什么差距这么大?
Python的for循环,每一次迭代都要做类型检查、函数调用、内存分配。而NumPy的向量化运算,底层是C语言写的,直接在连续内存块上做批量计算,跳过了Python的所有解释器开销。
实战场景:数据处理
假设你有一个10万行的销售数据,要计算每条记录的"折扣后价格":
bash
# ❌ 慢:for循环逐行计算
df['final_price'] = None
for i in range(len(df)):
df.loc[i, 'final_price'] = df.loc[i, 'price'] * (1 - df.loc[i, 'discount'])
# 10万行,约 20-30 秒
# ✅ 快:向量化
df['final_price'] = df['price'] * (1 - df['discount'])
# 10万行,约 0.01 秒
2000倍以上的差距。 这就是很多人觉得pandas"快得离谱"的原因------它底层全是向量化运算。
适用场景
| 操作类型 | 用for循环 | 用向量化 |
|---|---|---|
| 数值计算 | ❌ | ✅ NumPy |
| DataFrame列运算 | ❌ | ✅ pandas直接运算 |
| 字符串处理 | ❌ | ✅ pandas.str 或 regex |
| 条件筛选 | ❌ | ✅ 布尔索引 |
| 逐行依赖前一行结果 | ✅ 有时不得不用 | ❌ 试试 cumsum/shift |
💡 核心原则:只要你的操作能写成"对整个数组/列做同一个运算",就不要用for循环。
技巧二:用生成器替代列表(内存降90%,速度提升2-5倍)
当你处理大文件或大数据集时,内存往往是瓶颈。内存不够,系统开始用磁盘交换(swap),速度瞬间掉到地板上。
慢代码:
python
# 读取一个大文件,把所有行加载到内存
def read_lines(filename):
lines = []
with open(filename, 'r') as f:
for line in f:
lines.append(line.strip())
return lines
# 如果文件有100万行,lines列表占用几百MB内存
优化后:
python
# 用生成器,一次只加载一行到内存
def read_lines(filename):
with open(filename, 'r') as f:
for line in f:
yield line.strip()
# 使用
for line in read_lines('huge_file.txt'):
process(line) # 逐行处理,内存占用恒定在几KB
更Pythonic的写法:
ini
# 直接用生成器表达式
squares = (x ** 2 for x in range(10_000_000)) # 几乎不占内存
# 对比列表推导式
squares = [x ** 2 for x in range(10_000_000)] # 立即占用几百MB
实战场景:处理大日志文件
python
# ❌ 慢:一次性读入内存
with open('server.log', 'r') as f:
lines = f.readlines() # 2GB日志文件?内存直接爆
errors = [line for line in lines if 'ERROR' in line]
# ✅ 快:流式处理
errors = []
with open('server.log', 'r') as f:
for line in f: # 一次只读一行
if 'ERROR' in line:
errors.append(line)
如果只需要统计错误数量,甚至不需要存下来:
arduino
error_count = sum(1 for line in open('server.log') if 'ERROR' in line)
内存占用从GB级别降到KB级别,速度自然也快了。
技巧三:用集合和字典替代列表查找(提升100-1000倍)
在列表里查找元素,Python要做线性扫描------从头到尾一个一个比对。列表有100万个元素,最坏情况要比较100万次。
慢代码:
ini
# 检查10万个ID是否在白名单中
whitelist = list(range(1_000_000)) # 100万个ID的列表
user_ids = list(range(0, 2_000_000, 20)) # 10万个待检查的ID
start = time.time()
valid_users = []
for uid in user_ids:
if uid in whitelist: # 每次都是O(n)的线性查找
valid_users.append(uid)
end = time.time()
print(f"耗时:{end - start:.2f} 秒") # 约 50-80 秒
优化后:
ini
# 把白名单转成集合
whitelist_set = set(whitelist)
start = time.time()
valid_users = [uid for uid in user_ids if uid in whitelist_set] # O(1)查找
end = time.time()
print(f"耗时:{end - start:.4f} 秒") # 约 0.02 秒
3000倍以上的差距。
为什么集合这么快?
列表查找是线性扫描(O(n)),集合和字典的查找基于哈希表(O(1))。原理类似字典查字:列表是从头翻到尾,集合是直接跳到那一页。
实战场景:数据去重和关联
ini
# ❌ 慢:用列表做去重
unique_items = []
for item in huge_list:
if item not in unique_items: # O(n)查找
unique_items.append(item)
# ✅ 快:用集合做去重
unique_items = list(set(huge_list)) # O(1)查找,一行搞定
技巧四:用内置函数和库函数(提升5-50倍)
Python的内置函数和标注库,底层都是C语言实现的。你自己写的Python代码再怎么优化,也跑不过C。
慢代码:
ini
# 自己写求和
total = 0
for num in numbers:
total += num
# 自己写排序
def bubble_sort(arr):
for i in range(len(arr)):
for j in range(len(arr)-1-i):
if arr[j] > arr[j+1]:
arr[j], arr[j+1] = arr[j+1], arr[j]
优化后:
ini
# 用内置函数
total = sum(numbers) # C语言实现,快5-10倍
sorted_arr = sorted(arr) # Timsort算法,快20-50倍
max_val = max(numbers) # 内置,快很多
实战场景:字符串拼接
ini
# ❌ 慢:字符串累加
result = ''
for word in words:
result += word + ' ' # 每次都创建新字符串对象
# ✅ 快:join方法
result = ' '.join(words) # 一次分配内存,快10-100倍
实战场景:计数
ini
# ❌ 慢:手动计数
counts = {}
for item in items:
if item in counts:
counts[item] += 1
else:
counts[item] = 1
# ✅ 快:用Counter
from collections import Counter
counts = Counter(items) # 快3-5倍,代码还短
常用高性能内置替代方案
| 你的代码 | 用这个替代 | 提速倍数 |
|---|---|---|
| 手动for循环求和 | sum() |
5-10x |
| 手动排序 | sorted() |
20-50x |
字符串拼接 += |
'sep'.join() |
10-100x |
| 手动计数 | Counter |
3-5x |
| 手动查找最大/最小 | max()/min() |
5-10x |
| 手动反转列表 | reversed() |
10-20x |
| 手动判断是否存在 | any()/all() |
5-10x |
技巧五:用多进程突破GIL限制(提升4-8倍)
Python有个著名的"缺陷"叫GIL(全局解释器锁),它让同一时刻只有一个线程能执行Python字节码。所以多线程对CPU密集型任务几乎没用。
但多进程可以绕过GIL,每个进程有独立的Python解释器和内存空间,可以真正利用多核CPU。
慢代码:
python
import time
def heavy_task(n):
"""模拟一个耗时计算"""
result = 0
for i in range(n):
result += i ** 0.5
return result
tasks = [10_000_000] * 8 # 8个耗时任务
start = time.time()
results = []
for task in tasks:
results.append(heavy_task(task))
end = time.time()
print(f"单进程耗时:{end - start:.2f} 秒") # 约 16-20 秒
优化后:
python
import time
from multiprocessing import Pool
start = time.time()
with Pool(processes=8) as pool: # 8个进程,对应8个CPU核心
results = pool.map(heavy_task, tasks)
end = time.time()
print(f"多进程耗时:{end - start:.2f} 秒") # 约 3-5 秒
4-6倍的提升。 如果你有8核CPU,理论上限就是8倍(实际因为进程创建和通信开销,到不了8倍,但4-6倍是很常见的)。
实战场景:批量图片处理
python
from multiprocessing import Pool
from PIL import Image
import os
def resize_image(filename):
"""调整单张图片尺寸"""
img_path = f"inputs/{filename}"
img = Image.open(img_path)
img = img.resize((224, 224))
img.save(f"outputs/{filename}")
return filename
image_files = os.listdir("inputs")
# 用多进程并行处理
with Pool(processes=os.cpu_count()) as pool:
pool.map(resize_image, image_files)
⚠️ 注意事项
多进程不是银弹,有几个坑要注意:
- 进程启动有开销:任务太小(比如只算几毫秒),多进程的创建开销反而比直接跑还大。一般任务耗时在0.1秒以上才值得用多进程。
- 内存占用翻倍:每个进程都有独立的内存空间。如果主进程占2GB内存,开8个进程可能占到16GB+。确保内存够用。
- Windows上注意
__main__保护 :Windows的进程创建方式不同,必须把主代码放在if __name__ == '__main__':下面,否则会无限递归创建进程。
ini
# Windows上正确的写法
def main():
# 你的多进程代码
pass
if __name__ == '__main__':
main()
综合实战:优化一个真实场景
把5个技巧结合起来,看一个真实的优化案例。
场景:分析1000万条用户行为日志
需求:从1000万条日志中,找出每个用户最后10次行为中,有多少次是"购买"行为,并且只统计购买次数≥3的用户。
原始代码(慢):
ini
# 读取日志
logs = []
with open('user_logs.txt', 'r') as f:
for line in f:
user_id, action, timestamp = line.strip().split(',')
logs.append((int(user_id), action, int(timestamp)))
# 按用户分组
user_logs = {}
for user_id, action, timestamp in logs:
if user_id not in user_logs:
user_logs[user_id] = []
user_logs[user_id].append((action, timestamp))
# 分析每个用户
result = {}
for user_id, actions in user_logs.items():
# 按时间排序
actions.sort(key=lambda x: x[1])
# 取最后10次
last_10 = actions[-10:]
# 统计购买次数
purchase_count = 0
for action, _ in last_10:
if action == 'purchase':
purchase_count += 1
if purchase_count >= 3:
result[user_id] = purchase_count
print(len(result))
运行时间:约 120-180 秒。
优化后代码:
ini
import pandas as pd
from collections import defaultdict
# 技巧二:用pandas高效读取(底层C实现)
df = pd.read_csv('user_logs.txt', names=['user_id', 'action', 'timestamp'])
# 技巧一:向量化过滤
purchases = df[df['action'] == 'purchase']
# 技巧三:用groupby(底层用哈希表,比手动字典快)
user_purchase_counts = (
purchases.groupby('user_id')
.size()
.reset_index(name='purchase_count')
)
# 过滤购买次数≥3的用户
result = user_purchase_counts[user_purchase_counts['purchase_count'] >= 3]
print(len(result))
运行时间:约 3-5 秒。
提速30-50倍。 而且代码从30行变成了10行,可读性反而更好了。
如果数据量更大(上亿行),还可以进一步:
ini
# 技巧四+五:分块读取 + 多进程
chunk_size = 1_000_000
results = []
for chunk in pd.read_csv('user_logs.txt', names=['user_id', 'action', 'timestamp'], chunksize=chunk_size):
purchases = chunk[chunk['action'] == 'purchase']
counts = purchases.groupby('user_id').size()
results.append(counts)
# 合并结果
final_counts = pd.concat(results).groupby(level=0).sum()
result = final_counts[final_counts >= 3]
性能优化速查表
把5个技巧总结成一张速查表,方便你写代码时对照:
| 技巧 | 核心思想 | 适用场景 | 预期提速 |
|---|---|---|---|
| 向量化 | 用NumPy/pandas替代for循环 | 数值计算、数据处理 | 10-100x |
| 生成器 | 用yield或()替代\[\] | 大文件读取、大数据流 | 内存降90%+ |
| 集合/字典 | 用set/dict替代list查找 | 成员判断、去重、关联 | 100-1000x |
| 内置函数 | 用C实现的内置函数 | 求和、排序、计数等 | 5-50x |
| 多进程 | 绕过GIL用多核CPU | CPU密集型批量任务 | 4-8x |
写在最后
优化Python代码,最重要的不是记住这些技巧,而是养成一个习惯:写完代码后,问自己一句"这里有没有更Pythonic的写法?"
大多数情况下:
- 你在写for循环 → 想想能不能向量化
- 你在列表里做查找 → 想想能不能用集合
- 你在手动实现常见功能 → 想想有没有内置函数
- 你在处理大数据 → 想想能不能用生成器
- 你在跑CPU密集型任务 → 想想能不能用多进程
不需要一开始就追求极致性能。先让代码跑起来,再找到瓶颈,针对性优化。用 time 模块或 cProfile 测一下,哪里慢优化哪里,比盲目优化有效得多。
python
# 快速定位瓶颈
import cProfile
cProfile.run('your_function()')
这行代码会告诉你每个函数花了多少时间,哪里是真正的瓶颈一目了然。