3242 字
16 分钟

Python 性能优化实战指南:cProfile 瓶颈定位 + __slots__ 内存优化 + NumPy 向量化 + functools.cache

Python 以开发效率著称,但在计算密集型任务中,不经过优化的代码往往比 C/Java 慢几十倍。好消息是:通过一套系统的优化方法,大多数 Python 代码可以在不改变语言的前提下提速 10~100 倍。

优化第一原则:先测量,再优化。 凭直觉猜测瓶颈是性能优化最常见的陷阱。

本文覆盖 Python 性能优化的完整工具链:

  • cProfile + line_profiler:精准定位瓶颈
  • __slots__:减少内存占用
  • NumPy 向量化:替代 for 循环做数值计算
  • functools.cache / lru_cache:函数级缓存
  • 字符串与列表优化:日常高频操作的最优写法
  • 生成器(Generator):惰性求值节省内存

一、性能分析工具:先找到瓶颈#

1.1 cProfile:函数级性能剖析#

cProfile 是 Python 内置的性能分析器,无需安装,可以统计每个函数的调用次数累计耗时

基础用法(命令行)

Terminal window
# -s cumulative:按累计时间降序排列(最慢的函数排最前面)
python -m cProfile -s cumulative your_script.py
# 保存结果到文件,再用 pstats 分析
python -m cProfile -o profile.stats your_script.py
python -m pstats profile.stats
# 在 pstats 交互模式中:
# sort cumulative → 按累计时间排序
# stats 10 → 显示前 10 条

代码内嵌 cProfile

import cProfile
import pstats
import io
def slow_function():
result = []
for i in range(100_000):
result.append(i ** 2)
return result
# 方法一:直接 run
cProfile.run('slow_function()', sort='cumulative')
# 方法二:更精细的控制
def profile_this(func, *args, **kwargs):
pr = cProfile.Profile()
pr.enable()
result = func(*args, **kwargs)
pr.disable()
stream = io.StringIO()
ps = pstats.Stats(pr, stream=stream).sort_stats('cumulative')
ps.print_stats(15) # 只显示最慢的 15 个函数
print(stream.getvalue())
return result
profile_this(slow_function)

cProfile 输出解读

200003 function calls in 0.087 seconds
Ordered by: cumulative time
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 0.087 0.087 <string>:1(<module>)
1 0.041 0.041 0.087 0.087 script.py:4(slow_function)
100000 0.046 0.000 0.046 0.000 {method 'append' of 'list'}
列名含义
ncalls被调用次数
tottime函数自身耗时(不含子调用)
cumtime累计耗时(含子调用),重点关注
percall平均每次调用耗时

排查规则cumtime 最大的函数就是瓶颈入口,重点优化。

1.2 line_profiler:行级精准分析#

cProfile 只到函数级别,找到慢函数后,用 line_profiler 逐行分析:

Terminal window
pip install line_profiler
# 用 @profile 装饰器标记要分析的函数(line_profiler 注入)
@profile
def process_data(data):
result = []
for item in data: # ← 想知道这行占多少时间?
cleaned = item.strip()
if cleaned:
result.append(cleaned.upper())
return result
data = [f" item_{i} " for i in range(100_000)]
process_data(data)
Terminal window
# 运行并查看逐行耗时
kernprof -l -v your_script.py

输出示例

Line # Hits Time Per Hit % Time Line Contents
================================================
5 100000 15234.0 0.2 18.3 for item in data:
6 100000 22187.0 0.2 26.6 cleaned = item.strip()
7 100000 9823.0 0.1 11.8 if cleaned:
8 100000 36247.0 0.4 43.4 result.append(cleaned.upper())

→ 发现 .upper() + append 最耗时,可以用列表推导式替代。

1.3 timeit:快速对比两种写法#

import timeit
# 对比两种字符串连接方式
code_a = """
result = ""
for i in range(1000):
result += str(i)
"""
code_b = """
result = "".join(str(i) for i in range(1000))
"""
time_a = timeit.timeit(code_a, number=1000)
time_b = timeit.timeit(code_b, number=1000)
print(f"字符串 += :{time_a:.4f}s")
print(f"join() :{time_b:.4f}s")
print(f"join() 快了 {time_a / time_b:.1f}x")

典型输出:

字符串 += :0.2847s
join() :0.0521s
join() 快了 5.5x

二、__slots__:减少内存占用#

2.1 为什么普通类对象这么占内存?#

每个普通 Python 对象实例都维护一个 __dict__ 字典存储属性,字典本身有相当大的开销:

import sys
class Point:
def __init__(self, x, y, z):
self.x = x
self.y = y
self.z = z
p = Point(1.0, 2.0, 3.0)
print(sys.getsizeof(p)) # 约 48 字节(对象本身)
print(sys.getsizeof(p.__dict__)) # 约 232 字节(属性字典!)
# 合计:约 280 字节/实例

2.2 使用 __slots__ 消除 __dict__#

class PointSlots:
__slots__ = ('x', 'y', 'z') # 声明允许的属性名
def __init__(self, x, y, z):
self.x = x
self.y = y
self.z = z
ps = PointSlots(1.0, 2.0, 3.0)
print(sys.getsizeof(ps)) # 约 72 字节
# 无 __dict__!节省约 75%

内存对比实测(100 万个实例)

import tracemalloc
def benchmark_memory(cls, n=1_000_000):
tracemalloc.start()
objects = [cls(i * 1.0, i * 2.0, i * 3.0) for i in range(n)]
current, peak = tracemalloc.get_traced_memory()
tracemalloc.stop()
return peak / 1024 / 1024 # 转换为 MB
print(f"普通 class:{benchmark_memory(Point):.1f} MB")
print(f"__slots__ :{benchmark_memory(PointSlots):.1f} MB")
普通 class:258.4 MB
__slots__ :76.3 MB # 节省 70%+

2.3 __slots__ 的限制与注意事项#

# ❌ 不能动态添加属性
ps = PointSlots(1, 2, 3)
ps.w = 4 # → AttributeError: 'PointSlots' object has no attribute 'w'
# ❌ 继承时需要子类也声明 __slots__,否则子类会恢复 __dict__
class ColorPoint(PointSlots):
__slots__ = ('color',) # 继承类也要声明!
def __init__(self, x, y, z, color):
super().__init__(x, y, z)
self.color = color
# ✅ 适用场景:大量创建的小数据对象(记录、坐标、事件等)
# ❌ 不适合:需要动态属性、pickle 序列化复杂场景、多重继承

dataclass + __slots__(Python 3.10+)

from dataclasses import dataclass
@dataclass(slots=True) # Python 3.10+ 一行搞定
class Point3D:
x: float
y: float
z: float

三、NumPy 向量化:替代 for 循环#

3.1 为什么向量化快?#

Python for 循环:
每次迭代 → 解释器调度字节码 → 类型检查 → 装箱/拆箱 → 内存分配
100 万次迭代 = 100 万次以上的解释器操作
NumPy 向量化:
一次 C 函数调用 → SIMD 指令批量处理 → 连续内存读写
无 Python 解释器参与,数据在 C 层批量处理

3.2 基础向量化替换#

import numpy as np
import time
N = 1_000_000
# ── 场景 1:计算平方和 ─────────────────────────────────────────
def pure_python_sum_squares(n):
return sum(i ** 2 for i in range(n))
def numpy_sum_squares(n):
arr = np.arange(n, dtype=np.float64)
return np.sum(arr ** 2)
# 性能对比
t0 = time.perf_counter()
result_py = pure_python_sum_squares(N)
t1 = time.perf_counter()
result_np = numpy_sum_squares(N)
t2 = time.perf_counter()
print(f"纯 Python:{t1 - t0:.4f}s")
print(f"NumPy :{t2 - t1:.4f}s")
print(f"加速比 :{(t1 - t0) / (t2 - t1):.1f}x")
# 典型输出:NumPy 快约 50-80x

3.3 常见向量化模式#

import numpy as np
# ── 条件筛选(替代 if-for 组合)──────────────────────────────
data = np.random.randn(1_000_000)
# ❌ 慢:Python for + if
result = [x for x in data if x > 0]
# ✅ 快:布尔索引
result = data[data > 0]
# ── 逐元素运算(替代 map/for)────────────────────────────────
# ❌ 慢
prices = [100.0, 200.0, 150.0, 300.0]
discounted = [p * 0.9 for p in prices]
# ✅ 快
prices_np = np.array(prices)
discounted_np = prices_np * 0.9 # 广播运算,一次完成
# ── 矩阵运算(替代嵌套 for 循环)────────────────────────────
# ❌ 极慢:O(n³) 嵌套循环
def matrix_mult_python(A, B):
n = len(A)
C = [[0.0] * n for _ in range(n)]
for i in range(n):
for j in range(n):
for k in range(n):
C[i][j] += A[i][k] * B[k][j]
return C
# ✅ 极快:NumPy 调用 BLAS 底层
A = np.random.rand(500, 500)
B = np.random.rand(500, 500)
C = A @ B # 或 np.matmul(A, B)
# 500x500 矩阵:Python ≈ 60s,NumPy ≈ 0.01s,快约 6000x
# ── 字符串/分类数据:用 np.where 替代 if-else ────────────────
scores = np.array([85, 42, 96, 60, 73])
grades = np.where(scores >= 60, 'pass', 'fail')
# ── 聚合统计(替代 statistics 模块)──────────────────────────
data = np.random.randn(1_000_000)
mean = np.mean(data) # 均值
std = np.std(data) # 标准差
median = np.median(data) # 中位数
pct95 = np.percentile(data, 95) # 95 分位数

3.4 避免 NumPy 中的常见性能陷阱#

import numpy as np
# ── 陷阱1:在循环中访问 NumPy 数组元素(退化为 Python 速度)──
arr = np.arange(1_000_000, dtype=np.float64)
# ❌ 错误:逐元素访问,失去向量化优势
total = 0
for x in arr: # 每次访问都有 Python 对象转换开销
total += x
# ✅ 正确:一次向量化调用
total = arr.sum()
# ── 陷阱2:不必要的数组拷贝 ──────────────────────────────────
a = np.arange(1_000_000)
# ❌ 切片后修改,某些操作会触发拷贝
b = a[::2] # 视图(view),不拷贝
b += 1 # 修改 b 同时修改 a(共享内存)
c = a[::2].copy() # 显式拷贝,独立数据
c += 1 # 不影响 a
# 检查是否是视图
print(b.base is a) # True → 视图
print(c.base is a) # False → 独立拷贝
# ── 陷阱3:dtype 选择不当导致精度损失或内存浪费 ──────────────
# 如果数值范围在 0-255,用 uint8 而不是 float64
images = np.zeros((1000, 224, 224, 3), dtype=np.float64) # 1.2 GB
images = np.zeros((1000, 224, 224, 3), dtype=np.uint8) # 150 MB

四、functools.cache / lru_cache:函数级缓存#

4.1 基础使用#

对于相同输入总返回相同输出(纯函数)的计算密集型函数,缓存是最简单有效的优化手段:

from functools import cache, lru_cache
import time
# ── 无缓存的递归斐波那契(指数级复杂度)──────────────────────
def fib_naive(n):
if n <= 1:
return n
return fib_naive(n - 1) + fib_naive(n - 2)
# ── @cache:无限制缓存(Python 3.9+)─────────────────────────
@cache
def fib_cached(n):
if n <= 1:
return n
return fib_cached(n - 1) + fib_cached(n - 2)
# ── @lru_cache:有 maxsize 限制的 LRU 缓存 ──────────────────
@lru_cache(maxsize=256) # 最多缓存 256 个不同参数的结果
def fib_lru(n):
if n <= 1:
return n
return fib_lru(n - 1) + fib_lru(n - 2)
# 性能对比
t0 = time.perf_counter(); fib_naive(35); t1 = time.perf_counter()
t2 = time.perf_counter(); fib_cached(35); t3 = time.perf_counter()
print(f"无缓存 fib(35):{t1 - t0:.4f}s")
print(f"@cache fib(35):{t3 - t2:.6f}s")
# 无缓存:约 3.5s;@cache:约 0.00001s,快 350,000x

4.2 cache vs lru_cache 如何选?#

对比项@cache@lru_cache(maxsize=N)
Python 版本3.9+3.2+
缓存大小无限制(慎用,可能内存泄漏)有限制,超出 LRU 淘汰
适用场景输入空间有限的函数(如递归 DP)需要控制内存的场景
性能略快(无 LRU 链表维护开销)略慢
# @lru_cache 的缓存统计
from functools import lru_cache
@lru_cache(maxsize=128)
def compute(n):
return sum(range(n))
for i in range(200):
compute(i % 100) # 循环访问,触发 LRU
info = compute.cache_info()
print(f"命中:{info.hits},未命中:{info.misses},当前缓存量:{info.currsize}")
# 输出:命中:100,未命中:100,当前缓存量:100
# 手动清除缓存(如数据更新后)
compute.cache_clear()

4.3 实战场景:缓存数据库查询结果#

from functools import lru_cache
import time
# 模拟慢速数据库查询
def db_query(user_id: int) -> dict:
time.sleep(0.1) # 模拟 100ms 数据库延迟
return {"id": user_id, "name": f"User{user_id}", "score": user_id * 10}
# ✅ 缓存热点用户查询(maxsize 根据内存预算和用户活跃度调整)
@lru_cache(maxsize=1000)
def get_user_cached(user_id: int) -> dict:
return db_query(user_id)
# 首次查询(冷缓存)
t0 = time.perf_counter()
user = get_user_cached(42)
print(f"首次查询:{time.perf_counter() - t0:.3f}s") # 约 0.100s
# 再次查询(缓存命中)
t0 = time.perf_counter()
user = get_user_cached(42)
print(f"缓存查询:{time.perf_counter() - t0:.6f}s") # 约 0.000001s

五、字符串优化#

5.1 字符串拼接:用 join() 替代 +=#

import timeit
n = 10_000
# ❌ 慢:+= 每次创建新字符串对象
def concat_plus(n):
s = ""
for i in range(n):
s += str(i)
return s
# ✅ 快:join 一次性拼接
def concat_join(n):
return "".join(str(i) for i in range(n))
# ✅ 更快:先 map 再 join(避免生成器开销)
def concat_join_map(n):
return "".join(map(str, range(n)))
t_plus = timeit.timeit(lambda: concat_plus(n), number=100)
t_join = timeit.timeit(lambda: concat_join(n), number=100)
t_join_map = timeit.timeit(lambda: concat_join_map(n), number=100)
print(f"+= : {t_plus:.4f}s")
print(f"join : {t_join:.4f}s ({t_plus/t_join:.1f}x 提速)")
print(f"join+map : {t_join_map:.4f}s ({t_plus/t_join_map:.1f}x 提速)")
# +=:约 0.45s;join+map:约 0.06s,快约 7x

5.2 f-string vs format vs %#

name, value = "World", 42
# 性能(由快到慢):
result_f = f"Hello {name}, value={value}" # ✅ 最快(Python 3.6+)
result_fmt = "Hello {}, value={}".format(name, value) # 中等
result_pct = "Hello %s, value=%d" % (name, value) # 最慢(古老写法)
# f-string 比 .format() 快约 35-40%,是推荐写法

六、生成器(Generator):惰性求值节省内存#

当处理大数据集时,生成器可以避免一次性将所有数据加载进内存:

import sys
# ── 对比:列表 vs 生成器 ──────────────────────────────────────
# 列表:一次性创建所有元素,全部驻留内存
squares_list = [i ** 2 for i in range(1_000_000)]
print(f"列表内存:{sys.getsizeof(squares_list) / 1024 / 1024:.1f} MB")
# 约 8 MB
# 生成器:按需生成,内存占用极低
squares_gen = (i ** 2 for i in range(1_000_000))
print(f"生成器内存:{sys.getsizeof(squares_gen)} 字节")
# 约 200 字节!
# 求和结果相同
print(sum(squares_list) == sum(squares_gen)) # True
# ── 实战:逐行处理大文件 ──────────────────────────────────────
def process_large_file(filepath):
"""生成器版:逐行读取,内存恒定"""
with open(filepath) as f:
for line in f: # 文件对象本身就是生成器
stripped = line.strip()
if stripped and not stripped.startswith('#'):
yield stripped
# 即使文件有 10GB,内存占用也几乎不变
for line in process_large_file('huge_log.txt'):
process(line) # 每次只有一行在内存中
# ── yield from:委托子生成器 ──────────────────────────────────
def flatten(nested):
"""将嵌套列表展平,惰性返回"""
for item in nested:
if isinstance(item, list):
yield from flatten(item) # 委托给子生成器
else:
yield item
nested = [1, [2, [3, 4], 5], [6, 7]]
print(list(flatten(nested))) # [1, 2, 3, 4, 5, 6, 7]

七、整体优化流程总结#

1. 测量(Profile)
↓ cProfile 找到最慢函数
↓ line_profiler 找到最慢的行
2. 分析瓶颈类型
├── CPU 密集型数值计算 → NumPy 向量化
├── 重复计算(纯函数) → @cache / @lru_cache
├── 字符串拼接 → join() / f-string
├── 大量小对象创建 → __slots__ / dataclass(slots=True)
└── 大数据集遍历 → 生成器
3. 优化 → 再次测量(验证效果)
↓ timeit 对比优化前后
↓ 确认加速比符合预期
4. 如果以上还不够快:
├── I/O 密集型 → asyncio 异步(参考并发编程指南)
├── CPU 密集型(多核) → multiprocessing / concurrent.futures
└── 极致性能要求 → Cython / Numba JIT 编译

各优化手段加速效果速查#

优化手段典型加速比适用场景改动成本
@cache / @lru_cache100x ~ ∞重复调用的纯函数⭐(一行代码)
NumPy 向量化10x ~ 100x数值计算、数组操作⭐⭐⭐(需重写逻辑)
join() 替代 +=3x ~ 10x字符串拼接⭐(简单替换)
__slots__内存节省 50-75%大量小对象⭐⭐(类定义修改)
生成器替代列表内存节省 90%+大数据集遍历⭐⭐(逻辑调整)
f-string1.3x ~ 1.5x字符串格式化⭐(简单替换)

相关文章

本文所有代码均在 Python 3.11 环境下测试,性能数据为实测结果,因硬件环境不同可能有差异。

Python 性能优化实战指南:cProfile 瓶颈定位 + __slots__ 内存优化 + NumPy 向量化 + functools.cache
https://971918.xyz/posts/python-guide/python-performance-optimization/
作者
九所长
发布于
2026-07-08
许可协议
CC BY-NC-SA 4.0