
引言
Python是最适合初学者的编程语言之一。但如果你使用它有一段时间,可能会遇到运行几分钟才能完成的循环、占用全部内存的数据处理任务等等。
Airia 企业级AI
你无需成为性能优化专家,就能实现显著的改进。大多数缓慢的Python代码,都是由几个常见问题导致的——一旦你知道要关注哪些点,这些问题就能轻松解决。
在本文中,你将学习5种实用技巧来加速缓慢的Python代码,并通过“优化前vs优化后”的示例,直观看到效果差异。
你可以在GitHub上找到本文的相关代码。
前置条件
开始之前,请确保你已具备以下条件:
安装Python 3.10及以上版本
熟悉函数、循环和列表的使用
对标准库中的time模块有一定了解
部分示例还需要你安装以下库:
pip install pandas numpy
1. 优化前先进行测量
在修改任何一行代码之前,你需要明确代码的缓慢之处到底在哪里。优化错误的代码部分不仅浪费时间,甚至可能让情况变得更糟。
Python标准库提供了一种简单的方法来计时任何代码块:time模块。如需更详细的性能分析,cProfile可以精确显示哪些函数耗时最长。
假设你有一个处理销售记录列表的脚本,以下是找到缓慢部分的方法:
import time
def load_records():
# 模拟加载100,000条记录
return list(range(100_000))
def filter_records(records):
return [r for r in records if r % 2 == 0]
def generate_report(records):
return sum(records)
# 计时每一步操作
start = time.perf_counter()
records = load_records()
print(f"加载数据 : {time.perf_counter() - start:.4f}秒")
start = time.perf_counter()
filtered = filter_records(records)
print(f"筛选数据 : {time.perf_counter() - start:.4f}秒")
start = time.perf_counter()
report = generate_report(filtered)
print(f"生成报告 : {time.perf_counter() - start:.4f}秒")
输出结果:
加载数据 : 0.0034秒
筛选数据 : 0.0060秒
生成报告 : 0.0012秒
现在你知道该重点优化哪里了:filter_records()是最耗时的步骤,其次是load_records()。因此,优化这两个部分才能获得最大收益。如果不进行测量,你可能会浪费时间去优化本就很快的generate_report()。
对于短时间测量,time.perf_counter()比time.time()更精确。只要你需要计时代码性能,就使用它。
经验法则:永远不要猜测性能瓶颈在哪里。先测量,再优化。
2. 使用内置函数和标准库工具
Python的内置函数——sum()、map()、filter()、sorted()、min()、max()——底层是用C语言实现的。它们比用纯Python循环编写等效逻辑要快得多。
我们来对比一下“手动求和列表”和“使用内置sum()函数求和”的效率:
import time
numbers = list(range(1_000_000))
# 手动循环求和
start = time.perf_counter()
total = 0
for n in numbers:
total += n
print(f"手动循环 : {time.perf_counter() - start:.4f}秒 → {total}")
# 内置sum()函数求和
start = time.perf_counter()
total = sum(numbers)
print(f"内置函数 : {time.perf_counter() - start:.4f}秒 → {total}")
输出结果:
手动循环 : 0.1177秒 → 499999500000
内置函数 : 0.0103秒 → 499999500000
正如你所见,使用内置函数的速度几乎是手动循环的6倍。
同样的原理也适用于排序。如果你需要根据键对字典列表进行排序,Python的sorted()函数(搭配key参数)比手动排序更快、更简洁。以下是另一个示例:
orders = [
{"id": "ORD-003", "amount": 250.0},
{"id": "ORD-001", "amount": 89.99},
{"id": "ORD-002", "amount": 430.0},
]
# 缓慢:手动比较逻辑
def manual_sort(orders):
for i in range(len(orders)):
for j in range(i + 1, len(orders)):
if orders[i]["amount"] > orders[j]["amount"]:
orders[i], orders[j] = orders[j], orders[i]
return orders
# 快速:内置sorted()函数
sorted_orders = sorted(orders, key=lambda o: o["amount"])
print(sorted_orders)
输出结果:
[{'id': 'ORD-001', 'amount': 89.99}, {'id': 'ORD-003', 'amount': 250.0}, {'id': 'ORD-002', 'amount': 430.0}]
作为练习,你可以尝试为上述两种方法计时。
经验法则:当你需要编写循环来完成常见操作(求和、排序、找最大值等)时,先检查Python是否已有对应的内置函数。几乎总能找到,而且速度几乎总是更快。
3. 避免在循环内重复执行耗时操作
最常见的性能错误之一,是在循环内执行本可以在循环外一次性完成的耗时操作。每次循环都会付出相应代价,即使操作结果从未改变。
以下示例:根据批准列表验证一系列产品代码。
import time
approved = ["SKU-001", "SKU-002", "SKU-003", "SKU-004", "SKU-005"] * 1000
incoming = [f"SKU-{str(i).zfill(3)}" for i in range(5000)]
# 缓慢:每次循环都执行列表长度计算和成员检查
start = time.perf_counter()
valid = []
for code in incoming:
if code in approved: # 列表查找时间复杂度为O(n)——缓慢
valid.append(code)
print(f"列表检查 : {time.perf_counter() - start:.4f}秒 → {len(valid)}个有效代码")
# 快速:循环前将批准列表转换为集合(仅执行一次)
start = time.perf_counter()
approved_set = set(approved) # 集合查找时间复杂度为O(1)——快速
valid = []
for code in incoming:
if code in approved_set:
valid.append(code)
print(f"集合检查 : {time.perf_counter() - start:.4f}秒 → {len(valid)}个有效代码")
输出结果:
列表检查 : 0.3769秒 → 5个有效代码
集合检查 : 0.0014秒 → 5个有效代码
第二种方法快得多,而改进仅仅是将一次类型转换移到了循环外面。
同样的模式适用于所有“循环中结果不变”的耗时操作,比如读取配置文件、编译正则表达式、打开数据库连接等。这些操作应在循环前执行一次,而不是每次循环都执行。
import re
# 缓慢:每次调用都重新编译正则表达式
def extract_slow(text):
return re.findall(r'\d+', text)
# 快速:编译一次,重复使用
DIGIT_PATTERN = re.compile(r'\d+')
def extract_fast(text):
return DIGIT_PATTERN.findall(text)
经验法则:如果循环内某一行代码的结果在每次循环中都相同,就把它移到循环外面。
4. 选择合适的数据结构
Python提供了多种内置数据结构——列表(list)、集合(set)、字典(dict)、元组(tuple)——为任务选择错误的数据结构,会让你的代码比必要情况慢得多。
最关键的区别在于,使用in运算符进行成员检查时,列表和集合的效率差异:
检查一个元素是否在列表中,耗时会随着列表长度增加而变长,因为需要逐个扫描列表
集合使用哈希表实现,无论集合大小如何,都能在固定时间内完成成员检查
以下示例:从大型数据集中找出已经下过单的客户ID。
import time
import random
all_customers = [f"CUST-{i}" for i in range(100_000)]
ordered = [f"CUST-{i}" for i in random.sample(range(100_000), 10_000)]
# 缓慢:ordered是列表
start = time.perf_counter()
repeat_customers = [c for c in all_customers if c in ordered]
print(f"列表查找 : {time.perf_counter() - start:.4f}秒 → 找到{len(repeat_customers)}个重复客户")
# 快速:ordered转换为集合
ordered_set = set(ordered)
start = time.perf_counter()
repeat_customers = [c for c in all_customers if c in ordered_set]
print(f"集合查找 : {time.perf_counter() - start:.4f}秒 → 找到{len(repeat_customers)}个重复客户")
输出结果:
列表查找 : 16.7478秒 → 找到10000个重复客户
集合查找 : 0.0095秒 → 找到10000个重复客户
同样的逻辑也适用于字典(需要快速键查找时),以及collections模块的deque(需要频繁从序列两端添加或删除元素时——这是列表的薄弱环节)。
以下是快速参考指南,帮助你选择合适的数据结构:
| 需求 | 推荐使用的数据结构 |
|---|---|
| 有序序列、索引访问 | list(列表) |
| 快速成员检查 | set(集合) |
| 键值对查找 | dict(字典) |
| 统计元素出现次数 | collections.Counter |
| 队列或双端队列操作 | collections.deque |
经验法则:如果在循环中使用“x in 某对象”进行成员检查,且该对象包含超过几百个元素,那么它应该是一个集合(set)。
5. 对数值数据使用向量化操作
如果你的代码需要处理数值——比如跨数据行计算、统计操作、数据转换——编写Python循环几乎总是最慢的方式。NumPy和pandas等库正是为这类场景设计的:它们能在优化后的C代码中一次性对整个数组执行操作,完全无需Python循环。
这就是所谓的“向量化”。你无需告诉Python逐个处理每个元素,只需将整个数组交给函数,函数会在内部以C语言的速度完成所有操作。
import time
import numpy as np
import pandas as pd
prices = [round(10 + i * 0.05, 2) for i in range(500_000)]
discount_rate = 0.15
# 缓慢:Python循环
start = time.perf_counter()
discounted = []
for price in prices:
discounted.append(round(price * (1 - discount_rate), 2))
print(f"Python循环 : {time.perf_counter() - start:.4f}秒")
# 快速:NumPy向量化
prices_array = np.array(prices)
start = time.perf_counter()
discounted = np.round(prices_array * (1 - discount_rate), 2)
print(f"NumPy : {time.perf_counter() - start:.4f}秒")
# 快速:pandas向量化
prices_series = pd.Series(prices)
start = time.perf_counter()
discounted = (prices_series * (1 - discount_rate)).round(2)
print(f"pandas : {time.perf_counter() - start:.4f}秒")
输出结果:
Python循环 : 1.0025秒
NumPy : 0.0122秒
pandas : 0.0032秒
对于这项操作,NumPy的速度几乎是Python循环的100倍。代码也更简洁:没有循环,没有append(),只有一行表达式。
如果你已经在使用pandas的Datafr ame,同样的原理也适用于列操作。永远优先选择列级操作,而不是用iterrows()逐行循环:
df = pd.Datafr ame({"price": prices})
# 缓慢:用iterrows()逐行处理
start = time.perf_counter()
for idx, row in df.iterrows():
df.at[idx, "discounted"] = round(row["price"] * 0.85, 2)
print(f"iterrows方法 : {time.perf_counter() - start:.4f}秒")
# 快速:向量化列操作
start = time.perf_counter()
df["discounted"] = (df["price"] * 0.85).round(2)
print(f"向量化操作 : {time.perf_counter() - start:.4f}秒")
输出结果:
iterrows方法 : 34.5615秒
向量化操作 : 0.0051秒
iterrows()是pandas中最常见的性能陷阱之一。如果你的代码中使用了它,且处理的数据超过几千行,将其替换为列操作几乎总是值得的。
经验法则:如果你的代码在循环处理数值或Datafr ame行,不妨问问自己——NumPy或pandas是否能通过向量化操作完成同样的事情。
结论
缓慢的Python代码通常是“模式问题”。优化前先测量、依赖内置函数、避免循环内重复工作、选择合适的数据结构、对数值数据使用向量化操作——这5个技巧,能解决初学者遇到的绝大多数性能问题。
每次优化都从第一步开始:测量。找到真正的性能瓶颈,修复它,然后再次测量。你会惊讶地发现,在需要更高级的优化方法之前,代码还有很大的性能提升空间。
本文介绍的5种技巧,覆盖了Python代码缓慢的最常见原因。但有时你可能需要更进一步:
多进程(Multiprocessing)——如果你的任务是CPU密集型,且使用的是多核机器,Python的multiprocessing模块可以将任务分配到多个核心上
异步I/O(Async I/O)——如果你的代码大部分时间都在等待网络请求或文件读取,asyncio可以同时处理多个任务
Dask或Polars——对于无法放入内存的大型数据集,这些库的扩展性远超pandas
当你应用完基础技巧后,若仍需要更高的性能,再去探索这些进阶方法也不迟。编程愉快!
推荐学习书籍 《CDA一级教材》适合CDA一级考生备考,也适合业务及数据分析岗位的从业者提升自我。完整电子版已上线CDA网校,累计已有10万+在读~ !



雷达卡





京公网安备 11010802022788号







