翻遍80个Python性能调优实战案例,我总结出了普通人能直接套用的零成本优化万能公式

前言:别一上来就换语言、加机器
做Python开发的同学,应该都有过这样的经历:
写了个脚本,测试环境跑得好好的,一到生产环境跑大数据量,就慢得像蜗牛。
然后怎么办?很多人的第一反应是:
-
“Python太慢了,换C++/Go重写吧!”
-
“加机器啊,分布式搞起来!”
-
“上多线程、多进程!”
结果呢?折腾了一大圈,花了好几周时间,性能是提升了,但成本也上去了,代码复杂度也高了,维护起来苦不堪言。
我以前也是这样。直到后来我翻了80多个Python性能调优的实战案例,又在自己的项目里踩了无数坑,才发现一个真相:
80%的Python性能问题,根本不需要换语言、加机器、搞分布式。只要改改写法,零成本就能获得几倍甚至几十倍的性能提升。
什么叫零成本?就是不需要改架构、不需要加服务器、不需要换技术栈,只需要调整一下代码写法,用更高效的方式实现同样的功能。
举个最简单的例子:你用列表存了10万个元素,然后频繁判断某个元素在不在里面。只要把列表换成集合,查找速度就能提升几百倍。代码改动量?一行而已。
再比如:你用for循环+append构建列表,改成列表推导式,速度能快30%-50%。
这些优化,不需要你学什么高深的算法,也不需要你懂什么底层原理,知道了就能直接用,用了就有效果。
这篇文章,我把这80多个案例中最实用、最容易上手的零成本优化技巧整理出来,最后提炼成6个万能公式,普通人看完就能直接套用。
如果你也经常被Python的性能问题困扰,或者想写出更高效的代码,这篇文章你一定要看完。
第一章:数据结构选对了,性能直接翻倍
性能优化的第一原则:数据结构的选择,比代码写法的优化重要得多。
很多人写代码,不管什么场景都用列表。但其实不同的数据结构,在不同操作下的性能差异可能是几百倍、几千倍。
选对了数据结构,性能直接上一个台阶。
1.1 成员查找:列表 vs 集合
这是最常见、也是收益最大的一个优化点。
很多人喜欢用列表存数据,然后频繁判断某个元素在不在列表里:
# 低效写法
data = [1, 2, 3, ..., 100000] # 10万元素的列表
if x in data:
do_something()
列表的in操作是线性查找,时间复杂度是O(n)。也就是说,列表里有多少元素,最坏情况下就要比较多少次。
10万元素的列表,平均要查5万次才能找到。如果在循环里频繁做这个操作,那速度可想而知。
优化方法:把列表换成集合(set)
# 高效写法
data_set = set(data) # 转成集合
if x in data_set:
do_something()
集合的底层是哈希表,查找的时间复杂度是O(1)。不管集合里有多少元素,查找都是一瞬间的事。
我们来实测一下:
import time
# 准备数据
data_list = list(range(100_000))
data_set = set(data_list)
# 测试列表查找
start = time.time()
count = 0
for i in range(50_000):
if i in data_list:
count += 1
print(f"列表查找耗时: {time.time() - start:.4f}秒")
# 测试集合查找
start = time.time()
count = 0
for i in range(50_000):
if i in data_set:
count += 1
print(f"集合查找耗时: {time.time() - start:.4f}秒")
运行结果(大概):
列表查找耗时: 28.5632秒
集合查找耗时: 0.0031秒
差了多少?将近1万倍!
这就是数据结构的力量。你不需要改逻辑,不需要加机器,就改个数据类型,性能直接起飞。
什么时候用?
-
需要频繁判断元素是否存在 → 用集合
-
需要去重 → 用集合
-
只关心有没有,不关心顺序和重复 → 用集合
1.2 键值查找:字典的威力
除了成员查找,另一个常见场景是按键查找值。
比如,你有一个用户列表,每个用户有id和name,现在要根据user_id找用户名:
# 低效写法:用列表存,遍历查找
users = [
{"id": 1, "name": "张三"},
{"id": 2, "name": "李四"},
{"id": 3, "name": "王五"},
# ... 1万个用户
]
def get_user_name(user_id):
for user in users:
if user["id"] == user_id:
return user["name"]
return None
每次查找都要遍历列表,O(n)的时间复杂度。数据量大了之后,慢得离谱。
优化方法:预处理成字典,用id做key
# 高效写法:转成字典
user_map = {user["id"]: user["name"] for user in users}
def get_user_name(user_id):
return user_map.get(user_id)
字典的查找也是O(1),瞬间就能找到。
这是典型的空间换时间:用一点额外的内存存字典,换来查找速度的巨大提升。大多数情况下,这都是非常划算的。
1.3 队列操作:deque vs list
如果你需要频繁地在列表头部添加或删除元素,千万别用list。
因为list的底层是数组,在头部插入/删除元素,需要把后面所有元素都移动,时间复杂度O(n)。
# 低效写法:用list当队列
queue = []
# 从头部弹出(很慢)
queue.pop(0)
# 从头部插入(很慢)
queue.insert(0, x)
优化方法:用collections.deque
from collections import deque
# 高效写法:用deque
queue = deque()
# 从尾部添加
queue.append(x)
# 从头部弹出(O(1),很快)
queue.popleft()
# 从头部添加(O(1),很快)
queue.appendleft(x)
deque的底层是双向链表,头尾的添加/删除操作都是O(1),非常快。
如果你的代码里有大量的队列操作,换成deque,性能提升会很明显。
1.4 大数据处理:生成器代替列表
这个我们上一篇文章详细讲过,这里再简单提一下。
如果数据量很大,不需要随机访问,只需要遍历一次,那用生成器代替列表能节省大量内存。
# 低效:所有数据都装内存里
def get_data():
result = []
for item in big_dataset:
result.append(process(item))
return result
# 高效:用一个处理一个
def get_data():
for item in big_dataset:
yield process(item)
内存节省了,程序就不会因为内存不够而频繁交换(swap),整体速度自然就快了。
1.5 collections模块的宝藏数据结构
Python的collections模块里有很多好用的数据结构,很多人都不知道。
defaultdict:带默认值的字典
普通字典,访问不存在的key会报错。defaultdict可以指定默认值:
from collections import defaultdict
# 普通写法
counts = {}
for word in words:
if word not in counts:
counts[word] = 0
counts[word] += 1
# defaultdict写法
counts = defaultdict(int)
for word in words:
counts[word] += 1
代码更简洁,而且比自己判断要快一点。
Counter:专门用来计数
统计元素出现次数,用Counter最方便:
from collections import Counter
counts = Counter(words)
# 直接就能得到每个词的出现次数
print(counts.most_common(10)) # 出现最多的前10个
Counter内部是优化过的C实现,比自己手写循环快很多。
OrderedDict:有序字典
Python 3.7之后普通字典也是有序的了,但OrderedDict还有一些特殊方法,比如move_to_end、popitem等,在某些场景下很有用。
总结一下:
-
成员查找 → 用set,O(1) vs O(n)
-
键值查找 → 用dict,O(1) vs O(n)
-
队列操作 → 用deque,头尾O(1)
-
大数据遍历 → 用生成器,省内存
-
计数统计 → 用Counter,简洁高效

第二章:循环优化——最容易被忽略的性能杀手
Python的循环,相对来说是比较慢的。所以循环的优化,收益通常都很明显。
2.1 减少循环内的重复计算
这是最简单也最容易被忽略的优化。
看这段代码:
# 低效写法
result = []
for item in data:
# 每次循环都计算一次,但这个值其实是不变的
tax_rate = get_tax_rate()
result.append(item * tax_rate)
get_tax_rate()的返回值是固定的,但每次循环都调用一次。如果循环10万次,就多调用了99999次。
优化方法:把不变的计算移到循环外面
# 高效写法
tax_rate = get_tax_rate() # 移到外面,只算一次
result = []
for item in data:
result.append(item * tax_rate)
就这么简单,把一行代码移出去,性能就提升了。
类似的还有:
-
循环内的属性访问(比如obj.attr,如果不变就提出来)
-
循环内的字典查找
-
循环内的函数调用(结果不变的话)
原则就是:循环内只放每次都变的东西,不变的全移出去。
2.2 用推导式代替for循环+append
很多人喜欢这么写:
# 普通写法
result = []
for x in data:
if condition(x):
result.append(process(x))
改成列表推导式:
# 列表推导式
result = [process(x) for x in data if condition(x)]
列表推导式比手动for循环+append快多少呢?大概30%-50%。
为什么?因为列表推导式是Python底层优化过的,比纯Python层面的循环要快。
同理,还有字典推导式、集合推导式、生成器表达式:
# 字典推导式
result = {k: process(v) for k, v in data.items()}
# 集合推导式
result = {process(x) for x in data}
# 生成器表达式(大数据量用这个,省内存)
result = (process(x) for x in data)
能用推导式的地方,尽量用推导式,既简洁又快。
2.3 用内置函数代替手动循环
Python有很多内置函数,底层是C实现的,比你自己写Python循环快得多。
比如求和:
# 慢:手动循环
total = 0
for x in data:
total += x
# 快:内置sum
total = sum(data)
求最大值、最小值:
# 慢:手动循环
max_val = data[0]
for x in data:
if x > max_val:
max_val = x
# 快:内置max
max_val = max(data)
判断是否存在满足条件的元素:
# 慢:手动循环
has_error = False
for log in logs:
if log.level == "ERROR":
has_error = True
break
# 快:内置any
has_error = any(log.level == "ERROR" for log in logs)
判断是否全部满足:
# 慢:手动循环
all_valid = True
for item in items:
if not is_valid(item):
all_valid = False
break
# 快:内置all
all_valid = all(is_valid(item) for item in items)
还有map、filter这些,虽然现在用得少了,但在某些场景下也比手动循环快。
原则:能用内置函数解决的,就别自己写循环。
2.4 局部变量缓存,减少全局查找
Python中,局部变量的访问速度比全局变量快。
为什么?因为局部变量存在栈帧里,查找很快;全局变量要去全局字典里找,相对慢一些。
看个例子:
# 慢:循环内访问全局变量
import math
def calc_sqrt_list(data):
result = []
for x in data:
result.append(math.sqrt(x)) # 每次都要找math模块,再找sqrt
return result
优化一下,把math.sqrt缓存成局部变量:
# 快:缓存成局部变量
def calc_sqrt_list(data):
sqrt = math.sqrt # 提出来,变成局部变量
append = list.append
result = []
for x in data:
append(result, sqrt(x))
return result
虽然看起来有点奇怪,但确实会快一些。特别是循环次数很多的时候,积少成多。
同样的道理,属性访问、字典查找,如果在循环里频繁访问同一个,都可以缓存成局部变量。
不过这个优化属于微优化,收益没有前面几个大。在循环次数特别多的时候可以考虑,一般情况下不用太纠结,代码可读性更重要。
2.5 用enumerate代替range(len())
很多人写循环喜欢这么写:
# 不推荐
for i in range(len(items)):
item = items[i]
do_something(i, item)
改成用enumerate:
# 推荐
for i, item in enumerate(items):
do_something(i, item)
不仅代码更简洁,速度也稍微快一点,因为少了一次下标访问。
2.6 减少循环层数
这个是算法层面的了。
如果你写了两层甚至三层嵌套循环,先想想能不能优化。
比如,判断两个列表有没有共同元素:
# O(n*m),慢
has_common = False
for a in list_a:
for b in list_b:
if a == b:
has_common = True
break
if has_common:
break
改成用集合,一层循环就够了:
# O(n + m),快
set_b = set(list_b)
has_common = any(a in set_b for a in list_a)
从O(n*m)降到O(n+m),数据量大的时候,差距是数量级的。
循环优化总结:
-
循环外的计算,别放到循环里
-
能用推导式,就别手动append
-
能用内置函数,就别自己写循环
-
频繁访问的变量,缓存成局部变量
-
尽量减少循环层数,降低时间复杂度
第三章:字符串与IO优化
字符串操作和文件IO,也是常见的性能瓶颈。
3.1 字符串拼接:join vs +
这是经典老题了,但还是有很多人踩坑。
用+拼接字符串:
# 低效写法
result = ""
for s in strings:
result += s
为什么慢?因为Python的字符串是不可变的,每次+都会创建一个新的字符串,把旧的复制过去。循环N次,就有N次内存分配和拷贝,时间复杂度O(n²)。
正确做法:用列表收集,最后join
# 高效写法
parts = []
for s in strings:
parts.append(s)
result = "".join(parts)
join会先算好总长度,一次性分配内存,然后把所有字符串复制过去,时间复杂度O(n)。
字符串越多,差距越大。如果有上万个字符串要拼接,用join比用+快几十上百倍。
3.2 格式化字符串怎么选?
Python有三种字符串格式化方式:%、format、f-string。
它们的性能排序是怎样的?
import timeit
# % 格式化
def test_percent():
name = "张三"
age = 25
return "我叫%s,今年%d岁" % (name, age)
# format格式化
def test_format():
name = "张三"
age = 25
return "我叫{},今年{}岁".format(name, age)
# f-string
def test_fstring():
name = "张三"
age = 25
return f"我叫{name},今年{age}岁"
print("%:", timeit.timeit(test_percent, number=1000000))
print("format:", timeit.timeit(test_format, number=1000000))
print("f-string:", timeit.timeit(test_fstring, number=1000000))
大概结果:
%: 0.23秒
format: 0.37秒
f-string: 0.16秒
f-string最快,其次是%,最慢的是format。
所以,Python 3.6+,优先用f-string,又快又好看。
3.3 文件读取:怎么读最快?
读取文件,有几种方式:
# 方式一:一次性全读
with open("file.txt") as f:
content = f.read()
lines = content.splitlines()
# 方式二:readlines
with open("file.txt") as f:
lines = f.readlines()
# 方式三:逐行迭代
with open("file.txt") as f:
for line in f:
process(line)
怎么选?
-
文件不大,内存装得下 → 一次性读最快(read或readlines)
-
文件很大,内存装不下 → 逐行迭代,用生成器处理
不要自己去readline然后while循环,直接for line in f就行,这是最地道也最高效的写法。
3.4 批量写入代替逐行写入
写文件也是一样,尽量批量写,别一行一行写。
# 慢:逐行写
with open("output.txt", "w") as f:
for line in lines:
f.write(line + "\n")
# 快:批量写
with open("output.txt", "w") as f:
f.write("\n".join(lines))
减少IO次数,性能就上去了。
当然,如果数据量特别大,没办法一次性拼好,那也只能逐行写。但能批量的尽量批量。
3.5 用上下文管理器(with)
这个更多是正确性的问题,但也和性能有点关系。
用with语句打开文件,会自动关闭,不需要你手动f.close()。而且文件句柄是稀缺资源,及时释放也能避免资源泄漏。
# 推荐
with open("file.txt") as f:
data = f.read()
# 自动关闭
第四章:函数与类的优化技巧
4.1 减少不必要的函数调用
函数调用是有开销的。虽然Python的函数调用开销不算特别大,但如果在紧循环里频繁调用小函数,积少成多也很可观。
# 慢:循环里频繁调用小函数
def add_one(x):
return x + 1
result = []
for x in data:
result.append(add_one(x))
如果函数逻辑很简单,而且只在一个地方用,可以考虑直接内联:
# 快:直接内联
result = [x + 1 for x in data]
当然,这不是说让你不要写函数。函数的主要作用是代码复用和可读性,为了一点点性能把代码搞得一团糟,得不偿失。
原则:外层的大函数该拆就拆,紧循环里的小函数可以考虑内联。
4.2 生成器函数代替返回列表的函数
如果函数返回一个列表,但调用者只需要遍历一次,那改成生成器函数,既省内存又可能更快。
# 返回列表
def get_even_numbers(n):
result = []
for i in range(n):
if i % 2 == 0:
result.append(i)
return result
# 生成器函数
def get_even_numbers(n):
for i in range(n):
if i % 2 == 0:
yield i
调用方式差不多,但内存占用天差地别。
4.3 __slots__减少类的内存占用
如果你要创建大量的对象(几十上百万个),普通类的内存开销会很大。因为每个对象都有一个__dict__字典,用来存属性,字典的开销不小。
用__slots__可以取消__dict__,显著减少内存占用:
# 普通类
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
# 用__slots__
class Point:
__slots__ = ("x", "y")
def __init__(self, x, y):
self.x = x
self.y = y
用了__slots__之后,每个对象的内存能减少30%-50%,属性访问也会快一点。
代价是不能动态添加属性了,只能用__slots__里声明的那些。但对于数据类来说,这通常不是问题。
如果你的程序里有大量小对象,__slots__是个性价比很高的优化。
4.4 局部变量比属性访问快
在循环里,访问self.xxx比访问局部变量慢。
# 慢:每次都访问self
def process(self, items):
result = []
for item in items:
result.append(self.calculate(item))
return result
# 快:缓存成局部变量
def process(self, items):
calculate = self.calculate
result = []
for item in items:
result.append(calculate(item))
return result
同样,这属于微优化,循环次数多的时候才有明显效果。
4.5 装饰器的开销
装饰器很好用,但它也是有开销的。特别是叠了好几层装饰器的函数,调用开销会比普通函数大。
如果是在紧循环里频繁调用的函数,注意一下装饰器的开销。必要的话,可以把装饰器去掉,或者把逻辑直接写进去。
不过一般情况下,装饰器带来的便利远大于那点开销,不用太纠结。
第五章:标准库的正确打开方式
Python的标准库里藏着很多宝藏,用好了既能少写代码,又能提升性能。
5.1 functools.lru_cache:一行代码实现缓存
如果有一个函数,会被反复调用相同的参数,那加上lru_cache装饰器,性能直接起飞。
比如斐波那契数列:
# 慢:朴素递归,大量重复计算
def fib(n):
if n <= 1:
return n
return fib(n-1) + fib(n-2)
# 快:加上缓存
from functools import lru_cache
@lru_cache(maxsize=None)
def fib(n):
if n <= 1:
return n
return fib(n-1) + fib(n-2)
加上这一行装饰器,递归的时间复杂度直接从O(2^n)降到O(n),天壤之别。
只要是有重复调用、相同输入返回相同输出的函数(纯函数),都可以加lru_cache。比如数据库查询、计算密集型函数等。
注意:
-
函数参数必须是可哈希的(数字、字符串、元组等)
-
缓存会占内存,注意maxsize的设置
-
函数不能有副作用(比如改全局变量、写文件等)
5.2 itertools:高效迭代神器
itertools模块里有很多高效的迭代器工具,都是C实现的,比自己写Python循环快。
举几个常用的:
chain:拼接多个可迭代对象
from itertools import chain
# 手动拼接
def combine(list1, list2, list3):
for item in list1:
yield item
for item in list2:
yield item
for item in list3:
yield item
# 用chain
def combine(list1, list2, list3):
return chain(list1, list2, list3)
product:笛卡尔积
from itertools import product
# 代替多层嵌套循环
for a, b in product(list_a, list_b):
do_something(a, b)
islice:切片迭代器
from itertools import islice
# 取生成器的前N个元素
first_10 = islice(generator, 10)
groupby:分组
from itertools import groupby
# 按key分组(注意:数据要先排序)
for key, group in groupby(data, key=lambda x: x.category):
process_group(key, group)
itertools里还有很多好用的工具,建议大家去翻一翻文档,能省不少代码。
5.3 bisect:二分查找
如果列表是有序的,查找元素别用in,用bisect做二分查找,O(log n) vs O(n)。
import bisect
sorted_list = [1, 3, 5, 7, 9]
# 查找元素是否存在
index = bisect.bisect_left(sorted_list, 5)
exists = index < len(sorted_list) and sorted_list[index] == 5
数据量大的时候,二分查找比线性查找快得多。
当然,如果查找非常频繁,还是转成集合更快。但如果数据本身就是有序列表,不想转来转去,用bisect也不错。
5.4 heapq:堆队列
需要优先队列的时候,用heapq。
import heapq
heap = []
heapq.heappush(heap, (priority, item)) # 入堆
priority, item = heapq.heappop(heap) # 出堆(最小的)
堆的插入和弹出都是O(log n),用来实现Top K问题、任务调度等场景非常合适。
5.5 operator:代替lambda
在sorted、map等函数里,用operator模块的函数比lambda快一点。
from operator import itemgetter, attrgetter
# lambda写法
sorted_data = sorted(items, key=lambda x: x["age"])
# itemgetter,更快
sorted_data = sorted(items, key=itemgetter("age"))
# 同理,对象用attrgetter
sorted_data = sorted(users, key=attrgetter("age"))
因为operator是C实现的,比Python的lambda函数调用开销小。
5.6 内置函数优先,不要自己造轮子
最后强调一遍:Python的内置函数、标准库,大部分都是C优化过的,比你自己用Python写的快得多。
不要重复造轮子。写之前先想想,标准库里有没有现成的?
第六章:算法与逻辑层面的优化
前面讲的都是写法层面的优化。但真正决定性能上限的,是算法和逻辑。
6.1 时间复杂度意识
写代码的时候,心里要有个时间复杂度的概念。
-
O(1):哈希查找、数组下标访问 → 很快
-
O(log n):二分查找 → 很快
-
O(n):单次遍历 → 数据量大了也还行
-
O(n log n):排序 → 通常可以接受
-
O(n²):两层循环 → 数据量大了就很慢
-
O(2ⁿ):指数级 → 基本不能用
看到两层嵌套循环,就要警觉一下:能不能优化?能不能用哈希表降一层?
举个例子:两数之和问题。
# O(n²),暴力解法
def two_sum(nums, target):
for i in range(len(nums)):
for j in range(i+1, len(nums)):
if nums[i] + nums[j] == target:
return [i, j]
用哈希表优化到O(n):
# O(n),哈希表解法
def two_sum(nums, target):
seen = {}
for i, num in enumerate(nums):
complement = target - num
if complement in seen:
return [seen[complement], i]
seen[num] = i
return []
从O(n²)降到O(n),数据量大的时候,性能提升是数量级的。
6.2 空间换时间,用预处理代替重复计算
很多时候,用一点额外的空间存中间结果,可以节省大量的计算时间。
比如前面说的:
-
列表转集合/字典,加快查找
-
lru_cache缓存函数结果
-
预处理计算好中间值,避免重复计算
这是最常用的优化思路之一。大多数情况下,内存是廉价的,时间是宝贵的。
6.3 提前退出,别做无用功
找到答案了就及时返回,不要继续循环。
# 慢:遍历完所有元素
def has_error(logs):
has = False
for log in logs:
if log.level == "ERROR":
has = True
return has
# 快:找到就返回
def has_error(logs):
for log in logs:
if log.level == "ERROR":
return True
return False
看起来是很简单的道理,但很多人写代码的时候就是会忘记。特别是复杂的循环里,多检查一下能不能提前break或者return。
6.4 减少循环内的工作量
循环里每多做一件事,乘以循环次数,总时间就增加很多。
所以:
-
能在循环外算的,别放循环里
-
能在循环前预处理的,提前预处理好
-
循环里只做必须做的事
6.5 常见的低效写法
用列表模拟集合,频繁in操作 → 换成set
频繁字符串拼接用+ → 用join
多层嵌套循环查找 → 用字典/集合降维
递归没有缓存 → 加lru_cache,或者改成迭代
大数据全量加载到内存 → 流式处理,用生成器
排序后只取前N个 → 用heapq的nsmallest/nlargest,不用全排序
这些都是实战中非常常见的低效写法,遇到了直接改就行。
第七章:性能分析——找到瓶颈再优化
讲了这么多优化技巧,最重要的一条放在最后:不要瞎优化,先找到瓶颈。
很多人一上来就凭感觉优化,改了半天,结果发现优化的地方根本不是瓶颈,真正慢的地方一动没动。
正确的做法是:先测,找到瓶颈,再针对性优化。

7.1 cProfile:函数级性能分析
Python自带的cProfile,可以分析每个函数的调用次数和耗时。
用法很简单:
import cProfile
def your_function():
# 你的代码
pass
cProfile.run("your_function()", sort="cumulative")
或者命令行运行:
python -m cProfile -s cumulative your_script.py
输出会按累计耗时排序,你一眼就能看到哪个函数最慢。
关键指标:
-
ncalls:调用次数
-
tottime:函数本身耗时(不包括子函数)
-
cumtime:累计耗时(包括子函数)
重点看cumtime高的函数,那就是瓶颈所在。
7.2 timeit:小段代码测速
如果想比较两种写法哪个快,用timeit。
import timeit
def way1():
# 写法一
pass
def way2():
# 写法二
pass
print("写法一:", timeit.timeit(way1, number=10000))
print("写法二:", timeit.timeit(way2, number=10000))
跑个几万次,看谁用时少,谁就快。
7.3 memory_profiler:内存分析
如果是内存问题,用memory_profiler。
from memory_profiler import profile
@profile
def your_function():
# 你的代码
pass
your_function()
运行后会输出每行代码的内存占用,帮你找到内存泄漏或者内存占用高的地方。
7.4 优化验证:优化前后对比
优化完了,一定要跑一下对比测试,确认确实变快了。
不要想当然地觉得"这样写肯定更快"。有时候你以为的优化,反而可能更慢。
用数据说话,优化前后测一下,有提升再提交。
7.5 二八定律
性能优化遵循二八定律:80%的时间花在20%的代码上。
所以,集中精力优化那20%的热点代码就够了。剩下的80%,再怎么优化也没什么效果,反而可能把代码搞复杂。
先找到热点,再针对性优化,事半功倍。
第八章:零成本优化万能公式
讲了这么多,最后来总结一下。
我把这些优化技巧提炼成6个万能公式,普通人直接套用就行。
公式一:先测后优化,找到瓶颈再动手
不要凭感觉优化,先 profiling 找到瓶颈。
步骤:
-
用cProfile跑一遍,找到最慢的函数
-
分析慢的原因:数据结构?循环?IO?算法?
-
针对性优化
-
优化后再测一遍,确认有提升
盲目优化 = 浪费时间。找到真正的瓶颈,才能事半功倍。
公式二:数据结构优先选对,比什么都重要
选对数据结构,性能提升一个数量级。
快速决策表:
-
成员查找(x in xxx)→ 用set,O(1)
-
键值查找(根据key找value)→ 用dict,O(1)
-
头尾增删(队列/栈)→ 用deque,O(1)
-
大数据遍历一次 → 用生成器,省内存
-
计数统计 → 用Counter,简洁高效
-
大量小对象 → 加__slots__,省内存
数据结构选错了,再怎么优化代码写法都是杯水车薪。
公式三:循环能少则少,循环内能移则移
循环是性能热点,重点优化循环。
优化清单:
-
循环外的计算,全部移出去(不变的东西别每次都算)
-
能用推导式,就别for+append(列表/字典/集合推导式)
-
能用内置函数,就别自己写循环(sum、max、min、any、all等)
-
减少循环层数,能用哈希降维就降维(O(n²) → O(n))
-
找到结果就提前退出,别继续跑
循环优化的收益通常都很直接。
公式四:优先用内置,标准库是宝藏
能用内置和标准库的,别自己造轮子。
常用宝藏:
-
内置函数:sum、max、min、any、all、sorted、enumerate等
-
functools.lru_cache:函数结果缓存
-
itertools:各种高效迭代器
-
collections:deque、defaultdict、Counter等
-
bisect:二分查找
-
heapq:堆/优先队列
标准库大多是C实现的,比纯Python代码快得多,还不用自己写。
公式五:字符串和IO,批量处理更高效
字符串拼接用join,文件读写尽量批量。
-
大量字符串拼接 → 列表收集 + join,别用+循环拼
-
格式化字符串 → 优先f-string
-
文件读取 → 小文件一次性读,大文件逐行迭代
-
文件写入 → 能批量写就别逐行写,减少IO次数
IO操作通常比CPU计算慢得多,减少IO次数收益很大。
公式六:缓存重复计算,避免重复劳动
同样的结果,别算第二遍。
常用缓存手段:
-
函数结果缓存 → @lru_cache
-
查找加速 → 列表转set/dict预处理
-
中间结果保存 → 算过的值存起来,别重复算
空间换时间,大多数情况下都是划算的。
第九章:性能优化的5个常见误区
最后,讲几个常见的误区,避免走弯路。
误区一:过早优化是万恶之源
Donald Knuth的名言:“过早优化是万恶之源。”
不是说优化不好,而是说在功能还没做完、代码还不稳定的时候,就开始纠结各种微优化,把代码搞得复杂难懂,得不偿失。
正确的顺序是:先让代码跑起来,正确,可读,然后再去优化瓶颈部分。
大部分代码,一辈子都不会成为性能瓶颈,写得简单清晰最重要。
误区二:多线程一定更快
很多人一看到慢,就说"加多线程啊!"
但Python有GIL(全局解释器锁),CPU密集型的任务,多线程反而可能更慢(因为线程切换有开销)。
-
CPU密集型(计算多)→ 用多进程,或者用C扩展
-
IO密集型(网络、文件多)→ 用多线程/协程,确实能提速
不要上来就加线程,先搞清楚是CPU瓶颈还是IO瓶颈。
误区三:代码越短越快
代码行数和性能没有必然联系。
有时候一行看起来很简洁的代码,底层可能做了很多事情,反而更慢。
比如:
# 看起来短,但可能慢(创建了中间列表)
result = sum([x * 2 for x in data])
# 多了几个字,但更快(生成器,不创建中间列表)
result = sum(x * 2 for x in data)
不要以代码长短论英雄,要用测试数据说话。
误区四:换语言就能解决一切问题
“Python太慢了,换Go/换Rust!”
确实,编译型语言比Python快很多。但问题是:你的瓶颈真的是语言本身吗?
很多时候,Python写得慢,是因为写法不对、数据结构选错了、算法太差。把这些问题改了,Python也能很快。
换语言的成本是很高的:学习成本、重写成本、维护成本。在换语言之前,先把Python本身的优化做到位。
80%的场景,把Python写好就足够快了。
误区五:微优化比架构优化重要
很多人纠结于"用for还是用while"、"用format还是用%"这种微优化,却忽略了更大的问题:算法是不是O(n²)的?是不是有重复计算?是不是有没必要的IO?
算法和架构层面的优化,带来的提升是数量级的;微优化的提升通常是百分之几十。
先搞定大的优化点,再去抠细节。
总结
好了,这篇文章到这里就差不多了。
最后再回顾一下核心观点:
-
大部分Python性能问题,零成本就能解决——不需要换语言、加机器,改改写法就行。
-
数据结构的选择最重要——选对了直接起飞,选错了怎么优化都没用。
-
先找瓶颈再优化——不要瞎优化,用profiling工具找到热点再动手。
-
六个万能公式直接套用——先测后优化、数据结构选对、优化循环、用内置标准库、批量IO、缓存重复计算。
-
避免常见误区——不要过早优化、不要迷信多线程、不要纠结微优化。
性能优化不是什么高深的学问,更多的是经验和意识。知道了这些常见的优化点,写代码的时候多留意一下,代码质量自然就上去了。
当然,也不要为了优化而优化。代码的正确性和可读性永远是第一位的。在这个前提下,能优化就优化,不能优化也没关系。
希望这篇文章能帮你写出更快、更好的Python代码。
如果你觉得有用,欢迎分享给更多的人。也欢迎留言说说你常用的Python优化技巧,大家一起交流学习。

更多推荐



所有评论(0)