
写Python代码的人常碰到一个情况。程序跑得慢。自己觉得逻辑没问题。电脑风扇转得厉害。这时候需要看看代码哪里出了问题。优化不是把代码改短。也不是用上各种花哨写法。优化是让程序干活更少。让CPU做真正有用的事。
先看一个例子。假设要处理一千万个数字。找出其中所有偶数。很多人会写一个循环。往空列表里不停添加。代码长这样。
result = [] for i in range(10000000): if i % 2 == 0: result.append(i)
这段代码能跑。速度不快。原因是每次循环都调用append方法。append本身是C实现的。调用次数太多。开销就上来了。更好的写法用列表推导式。
result = [i for i in range(10000000) if i % 2 == 0]
列表推导式在解释器层做了优化。中间没有Python函数调用的开销。速度能快两倍左右。这个差距在数据量大的时候非常明显。
再看一个常见操作。字符串拼接。初学者会这样做。
s = '' for word in words: s += word
字符串在Python里不可变。每次加号都创建一个新字符串。旧字符串被扔掉。一千万次操作就创建一千万个临时对象。内存分配和回收吃掉大量时间。换成join方法。
s = ''.join(words)
join只分配一次内存。把所有片段一次性拷贝进去。速度快几十倍。这个写法的另一个好处是代码更短。可读性反而更好。
循环内部还有优化空间。看下面这段代码。
for i in range(len(data)): process(data[i])
每次循环都要算len(data)。len在Python里很快。毕竟它是内置函数。更好的做法是用enumerate或者直接迭代。
for item in data: process(item)
直接迭代省掉了索引操作。也省掉了len调用。代码更干净。有人担心拿不到索引。enumerate就是为这个场景设计的。
for index, item in enumerate(data): process(index, item)
字典的遍历也有讲究。不需要键的时候用values。不需要值的时候用keys。两个都需要用items。有人习惯用keys再取value。那样多做一次哈希查找。
for key in my_dict: value = my_dict[key]
改成下面这样少一次查找。
for key, value in my_dict.items(): pass
函数调用也有成本。Python的函数调用比C慢很多。如果一个函数在循环里被调用几百万次。开销就不可忽视。有时候可以把函数体直接展开到循环里。代价是代码变长。需要看是否值得。
局部变量比全局变量快。Python访问局部变量用的是数组索引。访问全局变量用的是字典查找。规则是LEGB。局部、闭包、全局、内置。写循环的时候把全局变量赋给一个局部名字。
def compute(): local_data = global_data for i in range(1000000): local_data[i] = i * 2
这样每次循环省掉一次全局查找。积少成多。效果在热循环里很明显。
生成器适合处理大数据。列表会把所有结果放进内存。生成器一次只产出一个。处理日志文件或者数据库查询结果时。生成器能省下大量内存。
def read_lines(file_path): with open(file_path) as f: for line in f: yield line.strip()
用这个函数的时候不会一次性读入整个文件。每读一行处理一行。内存占用恒定。适合处理几十G的日志。
内置函数和标准库是用C写的。速度比纯Python快一到两个数量级。能用内置就用内置。比如求和一个列表。自己写循环累加。不如直接用sum。
total = sum(numbers) 类似的还有min、max、any、all。这些函数不仅快。还更不容易写错。
选择合适的数据结构也能提速。判断一个元素是否在集合里。用set比用list快。list是线性查找。set是哈希查找。当元素数量超过几千。差距非常明显。
members = set(ids) if target in members: pass
操作字符串的时候注意编码。频繁做encode和decode会消耗CPU。如果整个流程都是字节操作。那就一直用bytes。不要来回转换。
多线程在Python里有GIL的限制。CPU密集的任务用多线程不会变快。这时候用多进程。multiprocessing模块能利用多核。IO密集的任务多线程还是有效的。因为线程在等待IO时会释放GIL。
异步IO适合网络请求。aiohttp加asyncio能处理成千上万的并发连接。每个连接不会独占一个线程。资源消耗比多线程小很多。写异步代码要注意不要在协程里做同步阻塞操作。一个阻塞调用会卡住整个事件循环。
代码性能分析不能靠猜。用cProfile找出真正的瓶颈。
import cProfile cProfile.run('my_function()')
它会输出每个函数的调用次数和耗时。看到输出结果往往跟直觉不一样。花时间优化一个只占1%时间的函数没有意义。把精力放在占80%时间的那部分。
line_profiler能分析到每一行。安装之后用kernprof运行。看哪一行最慢。然后针对那一行改。
算法上的改进永远比代码微调重要。一个O(n²)的算法换成O(n log n)。比任何语法优化都管用。写代码之前先想清楚数据规模和操作复杂度。数据小的时候随便写。数据大了必须选对算法。
用PyPy解释器可以不用改代码就提速。PyPy带JIT编译器。对长期运行的程序效果很好。对短脚本效果不明显。因为JIT需要时间预热。CPython也在改进。3.11版本比3.10快了不少。升级解释器版本是最省力的优化方式。
用type hints不会让代码变快。它只是给人和工具看的。不过配合mypy能提前发现类型错误。减少运行时的意外。mypy检查不花运行时间。值得加上。
最后提一点。优化之前先保证代码正确。没有测试的优化是危险的。改完跑一遍测试用例。确认结果跟原来一样。性能测试要跑多次取平均值。单次测量波动太大。用timeit模块做基准测试比较可靠。
优化是一个权衡。代码可读性、开发时间、运行速度三者要平衡。不要为了快把代码写成天书。三个月后自己都看不懂。先写清楚。再测性能。最后只改真正慢的地方。
以上就是“Python性能优化三招:代码提速几十倍,但90%的人都用错了方法!”的详细内容,想要了解更多Python教程欢迎持续关注编程学习网。
扫码二维码 获取免费视频学习资料

- 本文固定链接: http://www.phpxs.com/post/14576/
- 转载请注明:转载必须在正文中标注并保留原文链接
- 扫码: 扫上方二维码获取免费视频资料