【Python 进阶之路】第1讲 | 性能优化与调优实战:让你的代码飞起来

Python 版本Python 3.12+
开发工具PyCharm / VS Code
操作系统Windows / macOS / Linux (通用)


1. 说在前面:为什么你的代码总是"慢半拍"?

兄弟们,恭喜你完成了基础系列的学习!现在你已经能写出能跑的代码了。但是,能跑和跑得快,中间隔着一道鸿沟。

你有没有遇到过这些场景:

  • 你的 API 接口响应时间超过 3 秒,用户开始骂娘了。
  • 你的数据处理脚本跑了一晚上还没结束,内存却已经爆了。
  • 你的爬虫程序越跑越慢,最后直接卡死。

这些问题,不是你代码逻辑写错了,而是你不懂性能优化。今天这一讲,咱们就来聊聊怎么让你的代码从"老爷车"变成"法拉利"。

补充:Python 之父 Guido van Rossum 的设计哲学是"优雅、明确、简单",但这不意味着 Python 代码不能高效运行。

2. 性能分析工具:先诊断,再开药

很多新手优化代码的方式是"瞎猜":我觉得这里慢,就改改那里。这是大忌!性能优化讲究的是数据驱动,你得先知道哪里慢,才能对症下药。

2.1 cProfile:Python 自带的"体检中心"

cProfile 是 Python 标准库里的性能分析工具,它能告诉你每个函数被调用了多少次、花了多少时间。

import cProfile import time defslow_function(): time.sleep(0.1)return"我是个慢函数"deffast_function():return"我是个快函数"defmain():for _ inrange(10): slow_function()for _ inrange(100): fast_function()if __name__ =="__main__": cProfile.run("main()", sort="cumulative")

运行这段代码,你会看到类似这样的输出:

 206 function calls in 1.007 seconds Ordered by: cumulative time ncalls tottime percall cumtime percall filename:lineno(function) 1 0.000 0.000 1.007 1.007 <string>:1(<module>) 1 0.000 0.000 1.007 1.007 test.py:9(main) 10 0.000 0.000 1.006 0.101 test.py:4(slow_function) 10 1.006 0.101 1.006 0.101 {built-in method time.sleep} 100 0.000 0.000 0.000 0.000 test.py:7(fast_function) 

老司机解读

字段含义说明
ncalls调用次数函数被调用的次数
tottime自身耗时函数本身花费的时间(不包括调用的其他函数)
cumtime累计耗时函数累计花费的时间(包括调用的其他函数)
percall单次平均每次调用的平均时间

从这个结果可以看出,slow_function 是罪魁祸首,它花了 1.006 秒,占了总时间的 99% 以上。

2.2 line_profiler:逐行分析的神器

cProfile 只能告诉你哪个函数慢,但函数里面具体哪一行慢呢?这时候就需要 line_profiler 了。

首先安装它:

pip install line_profiler 

然后这样使用:

from line_profiler import LineProfiler import time defprocess_data(): data =[]for i inrange(10000): data.append(i *2) result =[]for item in data: result.append(item **2)return result lp = LineProfiler() lp_wrapper = lp(process_data) lp_wrapper() lp.print_stats()

输出结果会告诉你每一行代码的执行时间:

Timer unit: 1e-07 s Total time: 0.015 s File: test.py Function: process_data at line 4 Line # Hits Time Per Hit % Time Line Contents ============================================================== 4 def process_data(): 5 1 5.0 5.0 0.0 data = [] 6 10001 35000.0 3.5 23.3 for i in range(10000): 7 10000 25000.0 2.5 16.7 data.append(i * 2) 8 9 1 2.0 2.0 0.0 result = [] 10 10001 30000.0 3.0 20.0 for item in data: 11 10000 55000.0 5.5 36.7 result.append(item ** 2) 12 13 1 5.0 5.0 0.0 return result 

一眼看穿:第 11 行的 item ** 2 操作花了 36.7% 的时间,是性能瓶颈。

2.3 memory_profiler:内存泄漏的"照妖镜"

有时候你的程序慢不是因为 CPU,而是因为内存不够用,频繁触发垃圾回收。memory_profiler 可以帮你监控内存使用情况。

pip install memory_profiler 
from memory_profiler import profile @profiledefcreate_large_list(): large_list =[i for i inrange(1000000)]return large_list if __name__ =="__main__": create_large_list()

运行后会输出每一行的内存变化:

Line # Mem usage Increment Occurrences Line Contents ============================================================= 3 38.1 MiB 38.1 MiB 1 @profile 4 def create_large_list(): 5 76.5 MiB 38.4 MiB 1000003 large_list = [i for i in range(1000000)] 6 76.5 MiB 0.0 MiB 1 return large_list 

避坑小贴士

如果你发现某行代码的内存增量异常大,而且一直不释放,那就要警惕内存泄漏了。


3. 常见性能瓶颈与优化方案

知道了怎么分析性能,接下来咱们聊聊常见的性能瓶颈和对应的优化方案。

3.1 循环里的字符串拼接

错误示范

defbad_concat(items): result =""for item in items: result +=str(item)return result 

问题所在:字符串是不可变对象,每次 += 都会创建一个新的字符串对象,然后复制旧内容。如果你拼接 10000 次,就会创建 10000 个中间对象。

正确做法

defgood_concat(items):return"".join(str(item)for item in items)

性能对比

import time items =list(range(100000)) start = time.time() bad_concat(items)print(f"bad_concat: {time.time()- start:.3f}s") start = time.time() good_concat(items)print(f"good_concat: {time.time()- start:.3f}s")
方法执行时间性能提升
bad_concat2.156s基准
good_concat0.012s180x

快了将近 180 倍!这就是算法选择的重要性。

3.2 列表查找 vs 字典查找

错误示范

deffind_in_list(target, items):return target in items items =list(range(100000)) find_in_list(99999, items)

问题所在:列表的 in 操作是 O(n) 复杂度,需要从头遍历到尾。

正确做法

deffind_in_dict(target, items_dict):return target in items_dict items_dict ={i:Truefor i inrange(100000)} find_in_dict(99999, items_dict)

字典的 in 操作是 O(1) 复杂度,因为它基于哈希表实现。

性能对比

import time items_list =list(range(100000)) items_dict ={i:Truefor i inrange(100000)} start = time.time()for i inrange(1000): find_in_list(99999, items_list)print(f"列表查找 1000 次: {time.time()- start:.3f}s") start = time.time()for i inrange(1000): find_in_dict(99999, items_dict)print(f"字典查找 1000 次: {time.time()- start:.3f}s")
数据结构1000次查找时间时间复杂度
列表 (List)0.234sO(n)
字典 (Dict)0.000sO(1)

老司机建议:如果你需要频繁判断某个元素是否存在,用集合(set)或字典,别用列表。

3.3 全局变量 vs 局部变量

Python 访问局部变量比全局变量快得多,因为局部变量存储在固定大小的数组中,而全局变量存储在字典中。

错误示范

import math defcompute_with_global(n): result =0for i inrange(n): result += math.sqrt(i)return result 

正确做法

import math defcompute_with_local(n): sqrt = math.sqrt result =0for i inrange(n): result += sqrt(i)return result 

性能对比

变量类型执行时间 (100万次)性能提升
全局变量0.187s基准
局部变量0.156s1.2x

虽然差距不大,但在高频调用的场景下,积少成多,效果还是很明显的。


4. 时间复杂度与空间复杂度实战

性能优化的核心是理解算法复杂度。咱们用几个经典案例来讲解。

4.1 大 O 表示法速记

复杂度名称示例数据规模 10⁴ 时的操作次数
O(1)常数字典查找、数组索引1
O(log n)对数二分查找~14
O(n)线性遍历列表10,000
O(n log n)线性对数快速排序、归并排序~140,000
O(n²)平方双重循环、冒泡排序100,000,000
O(2ⁿ)指数递归斐波那契(无缓存)天文数字

4.2 实战案例:两数之和

问题:给定一个列表和目标值,找出列表中两个数,使它们的和等于目标值。

暴力解法(O(n²))

deftwo_sum_slow(nums, target): n =len(nums)for i inrange(n):for j inrange(i +1, n):if nums[i]+ nums[j]== target:return[i, j]return[]

优化解法(O(n))

deftwo_sum_fast(nums, target): seen ={}for i, num inenumerate(nums): complement = target - num if complement in seen:return[seen[complement], i] seen[num]= i return[]

性能对比

import time import random nums =[random.randint(1,10000)for _ inrange(10000)] target =15000 start = time.time() result = two_sum_slow(nums, target)print(f"暴力解法: {time.time()- start:.3f}s, 结果: {result}") start = time.time() result = two_sum_fast(nums, target)print(f"优化解法: {time.time()- start:.3f}s, 结果: {result}")
算法执行时间时间复杂度
暴力解法0.456sO(n²)
优化解法0.001sO(n)

一句话总结:用空间换时间,把 O(n²) 降到了 O(n)。


5. 内存泄漏排查与解决

内存泄漏是 Python 程序的隐形杀手。它不会让你的程序立刻崩溃,但会让你的程序越跑越慢,最终被系统杀掉。

5.1 常见的内存泄漏场景

场景一:全局列表无限增长

cache =[]defadd_to_cache(item): cache.append(item)

这个 cache 永远不会被清理,会一直增长直到内存爆满。

解决方案:使用 functools.lru_cache 或者手动限制大小。

from functools import lru_cache @lru_cache(maxsize=1000)defexpensive_function(n):return n **2

场景二:循环引用

classNode:def__init__(self): self.ref =None a = Node() b = Node() a.ref = b b.ref = a 

ab 互相引用,引用计数永远不会归零,导致内存无法释放。

解决方案:Python 的垃圾回收器(GC)会处理循环引用,但如果你手动关闭了 GC,就会出问题。

import gc gc.collect()

场景三:闭包捕获大对象

defcreate_closure(): large_data =[i for i inrange(1000000)]definner():returnlen(large_data)return inner func = create_closure()

inner 函数捕获了 large_data,即使 create_closure 执行完毕,large_data 也不会被释放。

解决方案:如果不需要 large_data,就在闭包外清理它。

5.2 使用 objgraph 定位内存泄漏

objgraph 是一个强大的工具,可以帮你可视化对象引用关系。

pip install objgraph 
import objgraph x =[1,2,3] y =[x, x, x] objgraph.show_refs([y], filename='refs.png')

这会生成一张图片,显示 y 引用了哪些对象。


6. 高级优化技巧

6.1 使用 slots 减少内存占用

默认情况下,Python 类的每个实例都有一个 __dict__ 来存储属性,这很耗内存。使用 __slots__ 可以显著减少内存占用。

# 普通类classRegularUser:def__init__(self, name, age, email): self.name = name self.age = age self.email = email # 使用 __slots__classSlotUser: __slots__ =['name','age','email']def__init__(self, name, age, email): self.name = name self.age = age self.email = email 

内存对比

import sys regular_users =[RegularUser(f"User{i}", i,f"user{i}@example.com")for i inrange(10000)] slot_users =[SlotUser(f"User{i}", i,f"user{i}@example.com")for i inrange(10000)]print(f"RegularUser 内存: {sys.getsizeof(regular_users[0])} bytes")print(f"SlotUser 内存: {sys.getsizeof(slot_users[0])} bytes")
类类型单个实例内存10000 实例总内存
RegularUser~152 bytes~1.5 MB
SlotUser~56 bytes~560 KB

内存对比详细数据

类类型单个实例内存1000 实例10000 实例100000 实例
RegularUser~152 bytes~148 KB~1.5 MB~14.5 MB
SlotUser~56 bytes~55 KB~560 KB~5.4 MB
内存节省63%63%63%63%

为什么 __slots__ 能节省内存?

普通类使用 __dict__ 存储属性,每个实例都有一个独立的字典对象,开销很大。而 __slots__ 预先声明了固定属性,使用固定大小的数组存储,避免了字典的开销。

完整的性能测试代码

import sys import time import tracemalloc # 普通类classRegularUser:def__init__(self, name, age, email): self.name = name self.age = age self.email = email # 使用 __slots__classSlotUser: __slots__ =['name','age','email']def__init__(self, name, age, email): self.name = name self.age = age self.email = email deftest_memory():"""内存占用测试"""print("=== 内存占用测试 ===")# 使用 tracemalloc 获取更准确的内存数据 tracemalloc.start() regular_users =[RegularUser(f"User{i}", i,f"user{i}@example.com")for i inrange(100000)] current1, peak1 = tracemalloc.get_traced_memory() tracemalloc.stop() tracemalloc.start() slot_users =[SlotUser(f"User{i}", i,f"user{i}@example.com")for i inrange(100000)] current2, peak2 = tracemalloc.get_traced_memory() tracemalloc.stop()print(f"RegularUser 单实例: {sys.getsizeof(regular_users[0])} bytes")print(f"SlotUser 单实例: {sys.getsizeof(slot_users[0])} bytes")print(f"普通类总内存: {current1 /1024/1024:.2f} MB")print(f"优化类总内存: {current2 /1024/1024:.2f} MB")print(f"内存节省: {(1- current2/current1)*100:.1f}%")deftest_access_speed():"""属性访问速度测试"""print("\n=== 属性访问速度测试 ===") regular_users =[RegularUser(f"User{i}", i,f"user{i}@example.com")for i inrange(10000)] slot_users =[SlotUser(f"User{i}", i,f"user{i}@example.com")for i inrange(10000)]# 测试读取速度 start = time.time()for user in regular_users: _ = user.name _ = user.age _ = user.email regular_time = time.time()- start start = time.time()for user in slot_users: _ = user.name _ = user.age _ = user.email slot_time = time.time()- start print(f"RegularUser 访问时间: {regular_time:.4f}s")print(f"SlotUser 访问时间: {slot_time:.4f}s")print(f"访问速度提升: {(regular_time/slot_time -1)*100:.1f}%")if __name__ =="__main__": test_memory() test_access_speed()

运行结果示例

=== 内存占用测试 === RegularUser 单实例: 152 bytes SlotUser 单实例: 56 bytes 普通类总内存: 14.52 MB 优化类总内存: 5.37 MB 内存节省: 63.0% === 属性访问速度测试 === RegularUser 访问时间: 0.0015s SlotUser 访问时间: 0.0012s 访问速度提升: 25.0% 

避坑小贴士

__slots__ 的适用场景

场景是否推荐使用说明
需要创建大量实例的类推荐内存节省效果显著
内存敏感的应用(如爬虫、数据处理)推荐降低内存占用
属性固定的数据类推荐类似 C 语言的结构体
需要动态添加属性的类不推荐会失去灵活性
多重继承场景谨慎使用需要处理属性冲突

__slots__ 的限制与注意事项

  1. 失去了 __dict__ 的灵活性
    • 无法使用 vars(obj) 获取所有属性
    • 无法序列化为 JSON(需要自定义方法)

多重继承时的属性冲突

classA: __slots__ =['a']classB: __slots__ =['b']classC(A, B):# 多重继承 __slots__ =['c']# 需要显式声明所有父类的 slots

不能动态添加新属性(除非在 __slots__ 中声明)

classUser: __slots__ =['name'] u = User() u.name ="张三"# 正常 u.age =20# AttributeError: 'User' object has no attribute 'age'

最佳实践

classDataRecord:""" 使用 __slots__ 的数据记录类 适用于:大量数据对象的存储 """ __slots__ =['id','name','value','timestamp','_initialized']def__init__(self,id, name, value, timestamp): self.id=id self.name = name self.value = value self.timestamp = timestamp self._initialized =Truedef__repr__(self):returnf"DataRecord(id={self.id}, name={self.name})"defto_dict(self):"""转换为字典,便于序列化"""return{'id': self.id,'name': self.name,'value': self.value,'timestamp': self.timestamp }

7. Python 3.12+ 性能改进新特性

Python 3.12 带来了许多性能改进,了解这些新特性可以帮助我们写出更高效的代码。

7.1 核心性能提升

特性Python 3.11Python 3.12+提升幅度
函数调用开销基准优化后约 15%
字符串处理基准优化后约 20%
字典操作基准优化后约 10%
列表推导式基准优化后约 10%
启动时间基准优化后约 10%

7.2 新特性对性能优化的影响

1. 改进的 f-string 解析

Python 3.12 重写了 f-string 的解析器,性能更好,错误提示更清晰:

# Python 3.12+ 中 f-string 性能更好 name ="Python" version =3.12# 这种写法在 3.12+ 中更高效 message =f"{name}{version} 性能更优"# 甚至可以嵌套更复杂的表达式 result =f"{(x :=10)+(y :=20)=}"# 输出: (x := 10) + (y := 20) = 30

2. 类型注解的性能改进

Python 3.12 优化了类型注解的处理,运行时开销更低:

from typing import List, Dict, Optional # Python 3.12+ 中类型注解不会显著影响性能classDataProcessor:defprocess(self, items: List[Dict[str, Optional[int]]])-> List[int]:return[item.get("value",0)for item in items if item]

3. 新的 typing 特性

from typing import override, TypedDict # Python 3.12+ 支持 override 装饰器classBase:defmethod(self)->int:return0classDerived(Base):@override# 明确标记重写,帮助 IDE 和类型检查器defmethod(self)->int:return1# TypedDict 的新语法(更简洁)classMovie(TypedDict): name:str year:int

7.3 利用 Python 3.12+ 特性优化代码

示例:使用新的异常处理语法

# Python 3.12+ 支持更清晰的异常组处理import asyncio asyncdefmain():try:asyncwith asyncio.TaskGroup()as tg: tg.create_task(task1()) tg.create_task(task2())except* ValueError as eg:# 处理一组 ValueErrorfor error in eg.exceptions:print(f"Value error: {error}")except* TypeError as eg:# 处理一组 TypeErrorfor error in eg.exceptions:print(f"Type error: {error}")

性能建议

  1. 升级到 Python 3.12+:享受原生性能提升
  2. 使用新的语法特性:f-string、类型注解等新语法更高效
  3. 关注标准库优化:许多内置模块在 3.12 中都有性能改进
  4. 使用 asyncio.TaskGroup:比手动管理任务更安全高效

8. 性能优化总结

经过这一讲的学习,我们掌握了以下性能优化技能:

优化技术适用场景性能提升
cProfile/line_profiler定位性能瓶颈-
字符串拼接优化大量字符串操作180x
字典查找替代列表频繁查找操作O(n) → O(1)
局部变量优化高频循环1.2x
算法优化两数之和等经典问题O(n²) → O(n)
__slots__大量实例创建节省 63% 内存
Python 3.12+ 新特性全场景10-20%

一句话总结:性能优化的核心是先测量,再优化,用数据驱动决策,而不是凭感觉。

经过这一讲的学习,我们掌握了以下性能优化技能:

优化技术适用场景性能提升
cProfile/line_profiler定位性能瓶颈-
字符串拼接优化大量字符串操作180x
字典查找替代列表频繁查找操作O(n) → O(1)
局部变量优化高频循环1.2x
算法优化两数之和等经典问题O(n²) → O(n)
__slots__大量实例创建节省 63% 内存

一句话总结:性能优化的核心是先测量,再优化,用数据驱动决策,而不是凭感觉。


8. 实战演练:巩固你的内功

题目 1:优化列表去重

需求
有一个包含100万个整数的列表,需要去除重复项。
比较 set()dict.fromkeys() 的性能差异。

点击查看参考答案

import time import random # 生成测试数据 data =[random.randint(1,100000)for _ inrange(1000000)]# 方法1:使用 set() start = time.time() result1 =list(set(data))print(f"set() 方法: {time.time()- start:.3f}s, 结果数量: {len(result1)}")# 方法2:使用 dict.fromkeys() start = time.time() result2 =list(dict.fromkeys(data))print(f"dict.fromkeys() 方法: {time.time()- start:.3f}s, 结果数量: {len(result2)}")# 方法3:保持顺序的优化方法defunique_ordered(items): seen =set() result =[]for item in items:if item notin seen: seen.add(item) result.append(item)return result start = time.time() result3 = unique_ordered(data)print(f"自定义方法: {time.time()- start:.3f}s, 结果数量: {len(result3)}")

题目 2:内存优化实战

需求
有一个日志处理系统,需要存储大量的日志记录。
每条日志包含:时间戳、级别、消息、来源IP。
使用 __slots__ 优化内存占用,并对比优化前后的内存使用情况。

点击查看参考答案

import sys import tracemalloc from datetime import datetime # 普通类classLogRecord:def__init__(self, timestamp, level, message, source_ip): self.timestamp = timestamp self.level = level self.message = message self.source_ip = source_ip # 使用 __slots__classLogRecordOptimized: __slots__ =['timestamp','level','message','source_ip']def__init__(self, timestamp, level, message, source_ip): self.timestamp = timestamp self.level = level self.message = message self.source_ip = source_ip deftest_memory_usage():# 测试普通类 tracemalloc.start() records1 =[LogRecord(datetime.now(),"INFO",f"Message {i}","192.168.1.1")for i inrange(100000)] current1, peak1 = tracemalloc.get_traced_memory() tracemalloc.stop()# 测试优化类 tracemalloc.start() records2 =[LogRecordOptimized(datetime.now(),"INFO",f"Message {i}","192.168.1.1")for i inrange(100000)] current2, peak2 = tracemalloc.get_traced_memory() tracemalloc.stop()print(f"普通类内存占用: {current1 /1024/1024:.2f} MB")print(f"优化类内存占用: {current2 /1024/1024:.2f} MB")print(f"内存节省: {(1- current2/current1)*100:.1f}%") test_memory_usage()

题目 3:算法复杂度分析

需求
分析以下代码的时间复杂度,并提出优化方案:

deffind_duplicates(nums): duplicates =[]for i inrange(len(nums)):for j inrange(i +1,len(nums)):if nums[i]== nums[j]and nums[i]notin duplicates: duplicates.append(nums[i])return duplicates 

点击查看参考答案

原算法分析

  • 时间复杂度:O(n³) - 双重循环 + in 操作
  • 空间复杂度:O(k) - k为重复元素数量

优化方案

deffind_duplicates_optimized(nums): seen =set() duplicates =set()for num in nums:if num in seen: duplicates.add(num)else: seen.add(num)returnlist(duplicates)# 测试import random import time data =[random.randint(1,10000)for _ inrange(10000)] start = time.time() result1 = find_duplicates(data)print(f"原算法: {time.time()- start:.3f}s") start = time.time() result2 = find_duplicates_optimized(data)print(f"优化后: {time.time()- start:.3f}s")

优化后复杂度

  • 时间复杂度:O(n)
  • 空间复杂度:O(n)

9. 本章小结

通过本讲的学习,我们系统地掌握了 Python 性能优化的核心技能:

核心知识点回顾

1. 性能分析工具

  • 使用 cProfile 进行函数级性能分析
  • 使用 line_profiler 进行行级性能分析
  • 使用 timeit 进行精确计时
  • 使用 tracemalloc 进行内存分析

2. 代码级优化技巧

  • 字符串拼接:使用 join() 替代 +
  • 查找操作:使用 set/dict 替代 list
  • 循环优化:使用局部变量、列表推导式
  • 算法优化:降低时间复杂度

3. 内存优化

  • 使用 __slots__ 减少实例内存占用
  • 使用生成器替代列表(惰性求值)
  • 及时释放不再使用的对象

4. Python 3.12+ 新特性

  • 原生性能提升 10-20%
  • 改进的 f-string 解析
  • 类型注解性能优化
  • 新的异常处理语法

下一步学习建议

立即实践

  1. 找一个你之前写过的项目,用 cProfile 分析性能瓶颈
  2. 尝试用 __slots__ 优化一个数据类,观察内存变化
  3. 重构一段使用字符串拼接的代码,对比性能差异

深入学习

推荐资源

  • Python 官方文档 - 性能优化章节
  • 《Python 高性能编程》
  • LeetCode 算法练习(关注复杂度分析)

10. 系列索引


写在最后
性能优化是一门艺术,也是一门科学。记住三个原则:先测量,再优化 - 不要凭感觉优化关注瓶颈 - 80%的性能问题来自20%的代码权衡取舍 - 优化往往伴随着复杂度增加,要适可而止

如果觉得有收获,点赞、收藏、关注! 咱们下一讲聊聊异步编程!
Could not load content