Image

前言:别一上来就换语言、加机器

做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,简洁高效

Image


第二章:循环优化——最容易被忽略的性能杀手

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),数据量大的时候,差距是数量级的。

循环优化总结:

  1. 循环外的计算,别放到循环里

  2. 能用推导式,就别手动append

  3. 能用内置函数,就别自己写循环

  4. 频繁访问的变量,缓存成局部变量

  5. 尽量减少循环层数,降低时间复杂度


第三章:字符串与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,不用全排序

这些都是实战中非常常见的低效写法,遇到了直接改就行。


第七章:性能分析——找到瓶颈再优化

讲了这么多优化技巧,最重要的一条放在最后:不要瞎优化,先找到瓶颈。

很多人一上来就凭感觉优化,改了半天,结果发现优化的地方根本不是瓶颈,真正慢的地方一动没动。

正确的做法是:先测,找到瓶颈,再针对性优化。

Image

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 找到瓶颈。

步骤:

  1. 用cProfile跑一遍,找到最慢的函数

  2. 分析慢的原因:数据结构?循环?IO?算法?

  3. 针对性优化

  4. 优化后再测一遍,确认有提升

盲目优化 = 浪费时间。找到真正的瓶颈,才能事半功倍。

公式二:数据结构优先选对,比什么都重要

选对数据结构,性能提升一个数量级。

快速决策表:

  • 成员查找(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?

算法和架构层面的优化,带来的提升是数量级的;微优化的提升通常是百分之几十。

先搞定大的优化点,再去抠细节。


总结

好了,这篇文章到这里就差不多了。

最后再回顾一下核心观点:

  1. 大部分Python性能问题,零成本就能解决——不需要换语言、加机器,改改写法就行。

  2. 数据结构的选择最重要——选对了直接起飞,选错了怎么优化都没用。

  3. 先找瓶颈再优化——不要瞎优化,用profiling工具找到热点再动手。

  4. 六个万能公式直接套用——先测后优化、数据结构选对、优化循环、用内置标准库、批量IO、缓存重复计算。

  5. 避免常见误区——不要过早优化、不要迷信多线程、不要纠结微优化。

性能优化不是什么高深的学问,更多的是经验和意识。知道了这些常见的优化点,写代码的时候多留意一下,代码质量自然就上去了。

当然,也不要为了优化而优化。代码的正确性和可读性永远是第一位的。在这个前提下,能优化就优化,不能优化也没关系。

希望这篇文章能帮你写出更快、更好的Python代码。

如果你觉得有用,欢迎分享给更多的人。也欢迎留言说说你常用的Python优化技巧,大家一起交流学习。

Image

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐