写Python的人常听到一句话:"Python太慢了"。确实,和C/C++、Java相比,Python的执行速度是慢一些。但在实际项目中,90%的性能问题不是Python本身慢,而是代码写得不够好。这篇文章从内存管理和性能优化两个角度,聊聊如何让Python代码跑得更快。
一、Python的内存管理机制
了解Python的内存管理,是写出高性能代码的第一步。
1. 引用计数与垃圾回收
Python的内存管理主要靠引用计数。每个对象都有一个计数器,记录有多少个引用指向它。当引用计数归零时,对象的内存会被立即释放。
python
import sys a = [1, 2, 3] print(sys.getrefcount(a)) # 2(a本身 + getrefcount的临时引用) b = a # 引用计数+1 print(sys.getrefcount(a)) # 3 del b # 引用计数-1 print(sys.getrefcount(a)) # 2
引用计数有一个问题:循环引用。两个对象互相引用,导致引用计数永远不为零。
python
class Node: def __init__(self): self.next = None a = Node() b = Node() a.next = b b.next = a # 循环引用 del a del b # 这两个对象的内存不会被释放,因为引用计数不为0
为了解决循环引用,Python还引入了分代垃圾回收器(GC),专门检测和回收循环引用的对象。GC是周期性的,有性能开销。在性能敏感的场景下,可以临时关闭GC:
python
import gc gc.disable() # 关闭GC # 执行性能敏感的操作 gc.enable() # 重新开启
2. 对象池与内存复用
Python对常用对象做了缓存,称为对象池,避免频繁创建和销毁对象。
整数池:-5到256的整数是预先创建好的,所有引用都指向同一个对象。
python
a = 100 b = 100 print(a is b) # True,指向同一个对象 c = 1000 d = 1000 print(c is d) # False,超出缓存范围,每次都新建
字符串池:长度为1的字符和部分短字符串也会被缓存。
小对象池:Python使用内存池来管理小对象(小于512字节),避免频繁向操作系统申请内存。
了解这些可以解释一些看似奇怪的现象:
python
# 列表的扩容策略 import sys lst = [] print(sys.getsizeof(lst)) # 56字节(空列表) lst.append(1) print(sys.getsizeof(lst)) # 88字节(扩容了) # 内存不是用完一点加一点,而是提前分配预留空间
二、内存优化的实战技巧
1. 使用生成器替代列表
这是最常见的优化手段。处理大量数据时,生成器比列表节省内存。
python
# ❌ 一次性加载所有数据 def read_all_lines(filename): with open(filename) as f: return f.readlines() # 大文件会爆内存 lines = read_all_lines('huge.log') for line in lines: process(line) # ✅ 逐行读取,内存友好 def read_lines_generator(filename): with open(filename) as f: for line in f: yield line for line in read_lines_generator('huge.log'): process(line)2. 使用__slots__减少对象内存
前面文章提过__slots__,这里补充一个实际数据:
python
class User: __slots__ = ('name', 'age', 'email') def __init__(self, name, age, email): self.name = name self.age = age self.email = email # 普通类:每个实例约占用 80-100 字节 # __slots__类:每个实例约占用 30-40 字节 # 创建100万个实例,节省约50MB内存3. 使用array模块存储数值
如果你的数据全是数字,用array模块比列表更省内存。
python
import array # ❌ Python列表:每个元素是独立的Python对象 numbers = [1, 2, 3, 4, 5] * 100000 # 内存约 30MB # ✅ array:连续存储C类型数值 from array import array numbers = array('i', [1, 2, 3, 4, 5] * 100000) # 内存约 2MB4. 使用struct处理二进制数据
处理网络协议或二进制文件时,struct模块比手动解析更高效。
python
import struct # 将4个整数打包成二进制 packed = struct.pack('iiii', 1, 2, 3, 4) unpacked = struct.unpack('iiii', packed)5. 使用memoryview避免拷贝
memoryview可以在不拷贝数据的情况下操作大块内存。
python
import memoryview data = bytearray(b'hello world') mv = memoryview(data) # 切片不拷贝数据,直接引用原内存 sub = mv[0:5] sub[0] = ord('H') # 修改会影响原数据 print(data) # b'Hello world'三、性能分析的常用工具
优化之前,先搞清楚瓶颈在哪里。不要凭感觉优化,要用数据说话。
1. timeit — 微基准测试
测试小段代码的执行时间。
python
import timeit # 方式1 result = timeit.timeit('"-".join(str(n) for n in range(100))', number=10000) # 方式2(在交互环境中) %timeit [-x for x in range(1000)] # Jupyter/IPython专用 # 方式3(命令行) # python -m timeit '"-".join(str(n) for n in range(100))'2. cProfile — 性能分析器
找出程序中哪个函数最耗时。
python
import cProfile import pstats def slow_function(): total = 0 for i in range(1000000): total += i * i return total def fast_function(): return sum(i * i for i in range(1000000)) def main(): slow_function() fast_function() # 性能分析 cProfile.run('main()', 'profile_output') # 查看结果 p = pstats.Stats('profile_output') p.sort_stats('cumtime').print_stats(10) # 按累计时间排序,显示前10行3. line_profiler — 逐行分析
找出函数中每行的耗时,精确到行级别。
bash
# 安装 pip install line_profiler # 在函数上加装饰器 @profile def my_function(): # ... # 运行 kernprof -l -v script.py
4. memory_profiler — 内存分析
监控程序的内存使用。
bash
pip install memory_profiler # 在函数上加装饰器 @profile def my_function(): # ... # 运行 python -m memory_profiler script.py
四、常见的性能优化技巧
1. 局部变量比全局变量快
Python访问局部变量的速度比全局变量快很多。
python
# ❌ 慢 def slow(): total = 0 for i in range(1000000): total += i * math.sqrt(i) # math是全局变量 return total # ✅ 快 def fast(): import math sqrt = math.sqrt # 局部变量引用 total = 0 for i in range(1000000): total += i * sqrt(i) return total
2. 字符串拼接用join
python
# ❌ 慢(每次拼接都创建新字符串) result = '' for s in strings: result += s # ✅ 快(一次性创建) result = ''.join(strings)
3. 列表推导比循环快
python
# ❌ 慢 result = [] for i in range(1000000): if i % 2 == 0: result.append(i * i) # ✅ 快 result = [i * i for i in range(1000000) if i % 2 == 0]
4. 使用内置函数
Python的内置函数都是用C实现的,速度远快于Python代码。
python
# ❌ 慢 max_value = 0 for x in data: if x > max_value: max_value = x # ✅ 快(内置max用C实现) max_value = max(data)
5. 选择合适的容器
不同容器的操作复杂度不同,选择合适的数据结构能大幅提升性能。
| 操作 | list | dict | set |
|---|---|---|---|
| 索引 | O(1) | — | — |
| 查找 | O(n) | O(1) | O(1) |
| 插入 | O(n) | O(1) | O(1) |
| 删除 | O(n) | O(1) | O(1) |
python
# 需要快速查找 → 用set/dict user_ids = set([1, 2, 3, 4, 5]) if user_id in user_ids: # O(1) pass # 不要用list做查找 user_ids = [1, 2, 3, 4, 5] if user_id in user_ids: # O(n) pass
6. 使用functools.lru_cache缓存计算结果
python
from functools import lru_cache # 计算结果被缓存,相同参数直接返回 @lru_cache(maxsize=1024) def expensive_function(x, y): # 复杂的计算... return result # 适用于:递归、重复计算的场景
7. 使用NumPy处理数值计算
如果涉及大量数值运算,NumPy是必须的。
python
import numpy as np # Python列表运算 data = list(range(1000000)) result = [x * 2 + 1 for x in data] # NumPy运算(快100倍以上) data_np = np.arange(1000000) result_np = data_np * 2 + 1
五、实战案例:优化一个慢函数
假设有一个函数,处理100万条用户数据:
python
# 原始版本(慢) def process_users_raw(users): result = [] for user in users: if user['age'] > 18: user['is_adult'] = True user['score'] = user['score'] * 1.1 result.append(user) return result # 优化版本 def process_users_optimized(users): # 1. 使用局部变量引用 result_append = result.append for user in users: # 2. 减少字典查找 age = user.get('age', 0) if age > 18: user['is_adult'] = True # 3. 直接修改字典 user['score'] = user.get('score', 0) * 1.1 result_append(user) return result # 进一步优化:使用pandas处理大数据 import pandas as pd def process_users_pandas(users_df): users_df['is_adult'] = users_df['age'] > 18 users_df['score'] = users_df['score'] * 1.1 return users_df写在最后
性能优化的核心原则:
先测试再优化:用性能分析工具找瓶颈,别靠猜
优化瓶颈代码:80%的时间花在20%的代码上
选择合适的算法和数据结构:这是最大的优化
利用缓存:用空间换时间
考虑使用C扩展:极端情况下用Cython或C扩展
记住:过早优化是万恶之源。在代码能跑、逻辑正确的前提下,再考虑性能优化。写清晰可读的代码永远比追求微小的性能提升更重要。
但如果是处理大规模数据或高并发场景,这些优化技巧就是必不可少的了。希望这篇文章对你有帮助。
(全文约2200字)
本回答由 AI 生成,内容仅供参考,请仔细甄别