第10章 10.4 性能分析与优化

🎯 开场 3 分钟:为什么你的程序跑得比蜗牛还慢?

上一章我们学会了怎么把自己写的工具发布到 PyPI,让全世界的人都能用 pip install 安装你的作品。太酷了!

但问题来了——你写的代码发布出去后,用户跑起来发现:慢!卡!等半天没反应!

这就是今天要解决的问题。

举个例子:你写了一个自动处理 10 万条数据的脚本,本地测试几条数据飞快,结果一跑真实数据,等了 5 分钟还在转圈圈。这种事儿,十有八九的新手都遇到过。

学完这章,你就能:

  • 定位代码里到底哪一行在拖后腿
  • 量化到底慢了多少秒(而不是「感觉有点慢」)
  • 优化让程序快上 10 倍甚至 100 倍

🧱 基础 25 分钟:性能分析的核心概念

概念 1:timeit —— 你的随身秒表

是什么:Python 内置的计时工具,就像你手机上的秒表。

为什么要用:想知道某段代码到底跑了多久,精确到微秒。

怎么用

import timeit

# 测量执行 1000 次需要多久
结果 = timeit.timeit('sum(range(1000))', number=1000)
print(f"1000 次合计耗时: {结果:.4f} 秒")

# 单次执行时间
单次 = timeit.timeit('x = [i**2 for i in range(100)]', number=1)
print(f"单次耗时: {单次:.6f} 秒")

输出:

1000 次合计耗时: 0.0234 秒
单次耗时: 0.000089 秒

说白了:timeit 就是个高精度的秒表,专门用来测量代码跑一次/跑 N 次要多久。

配图1 - 配图1

概念 2:cProfile —— 代码的 X 光片

是什么:Python 内置的性能分析器,能告诉你「哪个函数被调用了多少次、花了多少时间」。

为什么要用:timeit 只能告诉你「总时间」,但 cProfile 能告诉你「具体哪一行/哪个函数最慢」。

怎么用

import cProfile
import pstats
import io

def慢函数():
total = 0
for i in range(10000):
    total += i ** 2
return total

def快函数():
return sum(i ** 2 for i in range(10000))

# 创建一个性能分析器
profiler = cProfile.Profile()
profiler.enable()

# 运行你要分析的代码
结果1 = 慢函数()
结果2 = 快函数()

profiler.disable()

# 把分析结果打印出来
stream = io.StringIO()
stats = pstats.Stats(profiler, stream=stream)
stats.print_stats()  # 打印所有统计信息
print(stream.getvalue())

输出:

     10004 function calls in 0.008 seconds

rdered by: cumulative time

calls  tottime  percall  cumtime  percall filename:lineno(function)
    1    0.000    0.000    0.008    0.008 10_4_性能分析.py:11(快函数)
    1    0.005    0.005    0.005    0.005 10_4_性能分析.py:7(慢函数)

说白了:cProfile 就是代码的体检报告,告诉你哪个器官(函数)有问题。

概念 3:line_profiler —— 逐行扫描的显微镜

是什么:第三方库,能逐行分析代码执行时间,精确到毫秒级。

为什么要用:cProfile 告诉你哪个函数慢,line_profiler 告诉你函数里哪一行最慢。

怎么用

先安装:

pip install line_profiler

然后用 @profile 装饰器标记要分析的函数:

# my_script.py
from line_profiler import profile

@profile
def分析你的代码():
数据 = list(range(10000))

# 第一步:过滤偶数
过滤后 = [x for x in 数据 if x % 2 == 0]

# 第二步:平方
平方后 = [x ** 2 for x in 过滤后]

# 第三步:求和
结果 = sum(平方后)

print(f"总和是: {结果}")

if __name__ == "__main__":
分析你的代码()

运行:

kernprof -l -v my_script.py

输出:

Timer unit: 1e-06 s

Line #    Hits         Time  Per Hit   % Time  Line Contents
==============================================================
 5                                           @profile
 6                                           def 分析你的代码():
 7         1            48      48.0      0.2  数据 = list(range(10000))
 8         1          1204    1204.0      5.1  过滤后 = [x for x in 数据 if x % 2 == 0]
 9         1         20987   20987.0     88.7  平方后 = [x ** 2 for x in 过滤后]
10         1          1340    1340.0      5.9  结果 = sum(平方后)

看!第 9 行(平方操作)吃了 88.7% 的时间,这就是你要优化的目标!

配图2 - 配图2

概念 4:内存分析 —— 看看你的代码吃了多少内存

是什么:分析你的程序用了多少内存,内存泄漏在哪里。

怎么用(用 memory_profiler):

pip install memory_profiler
from memory_profiler import profile

@profile
def吃内存的函数():
# 创建一个大列表
大数据 = [i ** 2 for i in range(1000000)]
return sum(大数据)

if __name__ == "__main__":
结果 = 吃内存的函数()
print(f"结果是: {结果}")

运行:

python -m memory_profiler 你的文件.py

概念 5:常见的性能瓶颈(记住这 4 个)

瓶颈类型 举例子 怎么改
循环里做计算 for i in data: result.append(i**2) 用列表推导式或 map
重复创建对象 for i in range(100): obj = MyClass() 移到循环外面
字符串拼接 s += "a" 循环 1 万次 ''.join(list)
不必要的复制 new_list = old_list[:] 直接用切片或引用

🔥 实战 35 分钟:3 个递进的小项目

项目 1(5 分钟):给任意函数装上计时器

场景:你写了好几个函数,想知道每个跑多久。

完整代码

import time
from functools import wraps

def计时器(函数):
"""装饰器:给函数加上执行时间统计"""
@wraps(函数)
def包装函数(*args, **kwargs):
    开始 = time.time()
    结果 = 函数(*args, **kwargs)
    结束 = time.time()
    print(f"【{函数.__name__}】耗时 {结束 - 开始:.4f} 秒")
    return 结果
return 包装函数

# 使用方式:加个 @计时器 就搞定
@计时器
def处理数据(数据量):
return sum(i ** 2 for i in range(数据量))

@计时器
def找最大值(数据量):
数据 = list(range(数据量))
return max(数据)

# 测试一下
print("测试 10000 条数据:")
处理数据(10000)
找最大值(10000)

print("\n测试 100000 条数据:")
处理数据(100000)
找最大值(100000)

预期输出

测试 10000 条数据:
【处理数据】耗时 0.000892 秒
【找最大值】耗时 0.001234 秒

测试 100000 条数据:
【处理数据】耗时 0.008901 秒
【找最大值】耗时 0.012345 秒

一句话解释:用装饰器把函数包一层,自动计时,打印出来。


项目 2(15 分钟):分析 CSV 数据处理哪里慢

场景:你写了个脚本从 CSV 读取 1 万行数据,做清洗和统计,但跑得很慢。

数据文件 sales.csv(自己创建一个):

日期,商品,销量,单价
2024-01-01,苹果,100,5.5
2024-01-01,香蕉,80,3.2
2024-01-01,橙子,120,4.8
...(自行生成更多数据)

完整代码

import csv
import cProfile
import pstats
import io
from collections import defaultdict

def读取数据(文件名):
"""从 CSV 读取数据"""
数据 = []
with open(文件名, 'r', encoding='utf-8') as f:
    reader = csv.DictReader(f)
    for row in reader:
        数据.append(row)
return 数据

def清洗数据(原始数据):
"""清洗数据:转成正确类型"""
清洗后 = []
for row in 原始数据:
    try:
        清洗后.append({
            '日期': row['日期'],
            '商品': row['商品'],
            '销量': int(row['销量']),
            '单价': float(row['单价'])
        })
    except (ValueError, KeyError):
        continue  # 跳过有问题的行
return 清洗后

def按商品统计(数据):
"""按商品汇总销售额"""
统计 = defaultdict(lambda: {'销量': 0, '销售额': 0})
for row in 数据:
    商品 = row['商品']
    统计[商品]['销量'] += row['销量']
    统计[商品]['销售额'] += row['销量'] * row['单价']
return dict(统计)

def性能分析():
"""用 cProfile 分析整个流程"""
profiler = cProfile.Profile()
profiler.enable()

# 跑一遍完整流程
原始 = 读取数据('sales.csv')
清洗后 = 清洗数据(原始)
统计结果 = 按商品统计(清洗后)

profiler.disable()

# 打印分析报告
stream = io.StringIO()
stats = pstats.Stats(profiler, stream=stream)
stats.print_stats(10)  # 只显示前 10 行
print("=== 性能分析报告 ===")
print(stream.getvalue())

return 统计结果

# 运行
if __name__ == "__main__":
结果 = 性能分析()
print("\n=== 统计结果 ===")
for 商品, 数据 in 结果.items():
    print(f"{商品}: 销量={数据['销量']}, 销售额={数据['销售额']:.2f}")

预期输出

=== 性能分析报告 ===
     10004 function calls in 0.452 seconds

rdered by: cumulative time

calls  tottime  percall  cumtime  percall filename:lineno(function)
    1    0.001    0.001    0.452    0.452 10_4_demo.py:48(性能分析)
    1    0.003    0.003    0.380    0.380 10_4_demo.py:8(读取数据)
    1    0.180    0.180    0.340    0.340 10_4_demo.py:17(清洗数据)
    1    0.050    0.050    0.065    0.065 10_4_demo.py:28(按商品统计)

=== 统计结果 ===
苹果: 销量=10000, 销售额=55000.00
香蕉: 销量=8000, 销售额=25600.00
...

一句话解释:用 cProfile 一跑就知道,清洗数据 这个函数最慢(吃了 0.38 秒),可能需要优化。


项目 3(15 分钟):做个「代码跑得慢?让我来诊断」小工具

场景:做一个交互式工具,用户粘贴代码,它告诉你哪里可能慢。

完整代码

import timeit
import cProfile
import pstats
import io
import sys

class性能诊断器:
def __init__(self):
    self.函数耗时 = {}

def诊断(self, 代码字符串, 运行环境=None):
    """诊断一段代码的性能问题"""
    print("=" * 50)
    print("🔍 性能诊断开始")
    print("=" * 50)

    # 1. 基本计时
    print("\n📊 【基本计时】")
    try:
        单次 = timeit.timeit(代码字符串, number=1, globals=运行环境)
        千次 = timeit.timeit(代码字符串, number=1000, globals=运行环境)
        print(f"  单次执行: {单次*1000:.2f} 毫秒")
        print(f"  1000次执行: {千次*1000:.2f} 毫秒")
        print(f"  平均每次: {千次:.4f} 秒")
    except Exception as e:
        print(f"  ❌ 计时失败: {e}")

    # 2. 函数级分析
    print("\n📊 【函数调用分析】")
    try:
        # 用 cProfile 分析
        profiler = cProfile.Profile()
        profiler.enable()

        # 执行代码
        exec(代码字符串, 运行环境 or {})

        profiler.disable()

        # 打印结果
        stream = io.StringIO()
        stats = pstats.Stats(profiler, stream=stream)
        stats.strip_dirs()
        stats.sort_stats('cumulative')
        stats.print_stats(8)  # 前 8 行
        print(stream.getvalue())
    except Exception as e:
        print(f"  ❌ cProfile 失败: {e}")

    print("=" * 50)
    print("💡 优化建议")
    print("  - 如果某函数调用次数很多,考虑减少调用")
    print("  - 如果某函数 cumtime 很高,考虑优化算法")
    print("  - 检查是否有循环内重复计算")
    print("=" * 50)

# 使用例子
if __name__ == "__main__":
诊断器 = 性能诊断器()

# 测试代码
测试代码 = """
总数 = 0
for i in range(10000):
总数 += i ** 2
"""

print("这段代码快不快?")
print(测试代码)
诊断器.诊断(测试代码)

预期输出

这段代码快不快?

总数 = 0
for i in range(10000):
总数 += i ** 2

==================================================
🔍 性能诊断开始
==================================================

📊 【基本计时】
单次执行: 0.89 毫秒
1000次执行: 892.34 毫秒
平均每次: 0.000892 秒

📊 【函数调用分析】
     10004 function calls in 0.001 seconds

rdered by: cumulative time

calls  tottime  percall  cumtime  percall filename:lineno(function)
    1    0.000    0.000    0.001    0.001 <string>:3(<module>)
==================================================
💡 优化建议
- 如果某函数调用次数很多,考虑减少调用
- 如果某函数 cumtime 很高,考虑优化算法
- 检查是否有循环内重复计算
==================================================

一句话解释:把 timeit + cProfile + 优化建议打包成一个小工具,输入代码字符串就能自动诊断。


💪 进阶 20 分钟:常见坑 + 性能小贴士

坑 1:循环里用 + 拼接字符串

# ❌ 错误示范:循环 1 万次拼接,超慢
结果 = ""
for i in range(10000):
结果 += str(i)

# ✅ 正确示范:用 join
结果 = "".join(str(i) for i in range(10000))

原因:每次 += 都会创建新字符串、复制旧内容,O(n²) 复杂度。join 一次搞定,O(n)。


坑 2:在循环里创建大对象

# ❌ 错误示范:循环 1000 次创建列表
for i in range(1000):
临时列表 = [j ** 2 for j in range(10000)]  # 每次都重新分配内存
总和 += sum(临时列表)

# ✅ 正确示范:只创建一次
大数据 = [j ** 2 for j in range(10000)]
for i in range(1000):
总和 += sum(大数据)

坑 3:以为「列表推导式一定比循环快」

# ❌ 错误示范:复杂逻辑硬塞进列表推导式,反而更慢
结果 = [math.sqrt(x) if x > 0 else 0 for x in 数据 if x % 2 == 0 and func(x)]

# ✅ 正确示范:简单逻辑用推导式,复杂逻辑用普通循环
结果 = []
for x in 数据:
if x % 2 == 0:
    if x > 0:
        结果.append(math.sqrt(x))
    else:
        结果.append(0)

坑 4:用 list(range(...)) 而不是 range(...) 直接用

# ❌ 错误示范:转成列表,白占内存
数据 = list(range(10000000))

# ✅ 正确示范:range 对象直接用,不占额外内存
数据 = range(10000000)

原因range(1000) 只是个懒对象,不占内存;但 list(range(1000)) 要真实分配内存。


坑 5:忘记 lru_cache 缓存

# ❌ 错误示范:递归计算斐波那契,每次都重算
def fib(n):
if n < 2:
    return n
return fib(n-1) + fib(n-2)

# ✅ 正确示范:加缓存,飞快
from functools import lru_cache

@lru_cache(maxsize=None)
def fib(n):
if n < 2:
    return n
return fib(n-1) + fib(n-2)

性能小贴士:用 __slots__ 减少内存占用

如果你要创建大量对象(比如 100 万个小对象),用 __slots__ 可以省很多内存:

# 普通类:每个实例有自己的 __dict__,占 ~100 字节
class Person:
def __init__(self, name, age):
    self.name = name
    self.age = age

# 用 __slots__:没有 __dict__,占 ~50 字节(省一半)
class Person:
__slots__ = ['name', 'age']
def __init__(self, name, age):
    self.name = name
    self.age = age

调试技巧:用 print 打日志定位问题

import time

def处理数据(数据列表):
总数 = len(数据列表)
print(f"[DEBUG] 开始处理,共 {总数} 条数据")

开始 = time.time()
结果 = []
for i, item in enumerate(数据列表):
    if i % 1000 == 0:
        print(f"[DEBUG] 已处理 {i}/{总数}")
    结果.append(item ** 2)

结束 = time.time()
print(f"[DEBUG] 处理完成,耗时 {结束-开始:.2f} 秒")
return 结果

关键:加几个 print 就能知道卡在哪一步,比 cProfile 还直观!


✏️ 练习题 + 作业题

练习题(5 道,10 分钟内完成)

练习 1(2 分钟):给函数加计时器
- 输入:有一个函数 def 求和(n): return sum(range(n))
- 预期输出:打印出函数耗时
- 提示:用项目 1 的 @计时器 装饰器


练习 2(2 分钟):找出最慢的那一行
- 输入:用 line_profiler 分析这段代码:

def test():
a = [i for i in range(1000)]
b = [i**2 for i in a]
c = sum(b)
return c
  • 预期输出:说出哪一行最慢
  • 提示:看 % Time

练习 3(2 分钟):修复字符串拼接
- 输入:for i in range(1000): s += str(i) 很慢
- 预期输出:写出优化后的版本
- 提示:用 join


练习 4(2 分钟):用 lru_cache 加速
- 输入:计算 fib(30) 的递归函数,超级慢
- 预期输出:加速后的代码(加 @lru_cache
- 提示:加一行装饰器


练习 5(2 分钟):读性能分析报告
- 输入:以下 cProfile 输出,说出哪个函数最慢:

ncalls  tottime  cumtime
00    0.50    1.20  func_a
10    0.30    0.80  func_b
 1    0.10    2.10  func_c
  • 预期输出:func_c 最慢,因为 cumtime 最大
  • 提示:看 cumtime 列,不是 tottime

作业题(30 分钟 - 2 小时)

作业:做一个「第 10 章 10.4 性能监控小助手」

需求描述:做一个命令行工具,能分析任意 Python 文件的性能。

功能点
1. 接收一个 .py 文件路径作为参数
2. 自动运行 cProfile 分析
3. 找出耗时最长的 3 个函数
4. 给出优化建议

加分项
1. 支持 --output 参数把报告保存到文件
2. 支持 --top N 参数指定显示前 N 个函数

验收标准
- 能跑起来:python profiler.py my_script.py
- 输出包含函数名、调用次数、耗时
- 代码有注释

提交方式:评论区贴代码或 GitHub 链接


📚 总结 + 资源

本章 3 个核心点

  1. timeit 给你精确到微秒的计时,cProfile 给你函数级别的分析,line_profiler 给你逐行的显微镜
  2. 性能瓶颈就那几个:循环里计算、重复创建对象、字符串拼接、不必要的复制
  3. 优化口诀:能用推导式就不用循环,能缓存就缓存,能一次搞定就别分多次

延伸学习资源

  1. Python 官方文档 - timeit —— 官方出品,必属精品
  2. Python 官方文档 - cProfile —— 详细到你会爱上它
  3. 《Python 性能编程》—— 书如其名,专门讲 Python 性能优化

互动钩子:你在写代码时遇到过「明明数据不多,但跑起来奇慢无比」的情况吗?最后怎么解决的?评论区聊聊,老粉优先回复!


📢 下章预告:学会了性能分析,下一章我们要做一个真正的命令行工具,把你写的程序打包成 pip install 就能用的那种。敬请期待!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。