Python性能优化指南--让你的Python代码快x3倍的秘诀

简介: Python最为人诟病的就是其执行速度。如何让Python程序跑得更快一直是Python核心团队和社区努力的方向。本文将带大家深入探讨Python程序性能优化方法。

Python性能优化指南

Python最为人诟病的就是其执行速度。如何让Python程序跑得更快一直是Python核心团队和社区努力的方向。作为Python开发者,我们同样可以采用某些原则和技巧,写出性能更好的Python代码。本文将带大家深入探讨Python程序性能优化方法。

Intermediate_Watermarked.png

优化原则

有些优化原则是所有编程语言都适用的,当然对Python也同样适用。这些原则作为程序优化的“心法”,我们每个程序员都要牢记于心,并在日常开发中加以贯彻。

1. 切忌边开发边优化

编写程序时不要去考虑可能的优化,而是集中精力确保代码干净、正确、可读、易懂。如果在写完后发现它太大或太慢,那时再考虑如何优化它。大神高德纳(Donald Knuth)说过一句至理名言:

“过早优化是一切罪恶的根源。(Premature optimization is the root of all evil.)”

这其实也是曾国藩的处事哲学:“物来顺应,未来不迎,当时不杂,既过不恋”

2. 牢记20/80法则

在许多领域,你都可以用20%的努力获得80%的结果(有时可能是10/90法则)。每当您要优化代码时,首先用分析工具找出80%的执行时间花在哪里,这样您就知道应该集中精力优化哪里了。

3. 一定要做优化前后的性能对比

如果不做优化前后的性能比较,我们无法知道优化是否产生了实际效果。如果优化后的代码只比优化前稍快一点,那么请撤消优化并返回优化前版本。因为用牺牲代码的清晰整洁、易读好懂为代价换来的一丁点性能提升不值得。

以上3条优化原则请大家牢记于心,无论今后大家使用何种语言,在做性能优化时都请遵守这3条法则。

优化工具

正如优化原则第二条所讲,我们需要将优化精力放在最耗时的地方。那么怎么找到程序中最耗时的地方呢?此时我们需要用优化工具收集程序运行中的数据,帮我们找到程序瓶颈所在。这个过程称为Profiling。Python中有多个Profiling工具,每个工具各自有其使用场景和重点,下面一一为大家介绍。

cProfile

Python中自带了Profiling工具,名叫cProfile。这也是我推荐大家使用的Profiling工具,因为它功能最强大。它可以注入程序中的每一个方法,收集丰富的数据,包括:

  • ncalls: 方法被调用的次数
  • tottime: 方法执行的总时间(不包含子函数的执行时间)
  • percall: 每次执行花费的平均时间,即tottime除以ncalls的商
  • cumtime: 方法执行的累计时间(包含子函数的执行时间),对递归同样准确有效
  • percall: cumtime除以原始调用次数(不含递归调用)的商
  • filename:lineno(function): 提供每个函数的相应数据

cProfile可以直接在命令行里使用。

$  python -m cProfile main.py

假设我们的main.py实现的是求1000000以内的素数和,代码如下:

import math


def is_prime(n: int) -> bool:
    for i in range(2, math.floor(math.sqrt(n)) + 1):
        if n % i == 0:
            return False
    return True


def main():
    s = 0
    for i in range(2, 1000000):
        if is_prime(i):
            s += i
    print(s)


if __name__ == "__main__": 
    main()

那么执行cProfile后,控制台会输出(部分)如下信息:

profile_p1.png

从输出看,整个程序运行花了3.091秒,共有3000064次函数调用,下面列表是每个函数的详细数据。cProfile默认用函数名排序,而我们更关注函数的执行时间,所以通常我们在使用cProfile时会带上-s time,让cProfile按执行时间来排序输出。

$ python -m cProfile -s time .\main.py

按时间排序输出后的信息如下:

profile_p2.png

从上面的输出信息可以看到,最耗时的是is_prime函数。如果要优化,is_prime将是我们的优化重点。

%%timeit 和 %timeit

上面介绍的cProfile主要在命令行中使用。但是在数据分析和机器学习中我们经常使用Jupyter作为交互式编程环境。在Jupyter或IPython等交互式编程环境下,cProfile就无法使用了,我们需要用%%timeit%timeit

%%timeit%timeit的区别在于,%%timeit作用于整个代码块,统计整个代码块的执行时间;%timeit作用于语句,统计该行语句的执行时间。还是求质数和的代码,我们看一下在Jupyter中如何获取其运行时间:

profile_p3.png

上面代码在代码块开头加入%%timeit, 它会统计整个代码的运行时间。%%timeit会多次运行,取平均运行时长。输出结果如下:

3.87 s ± 151 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

timeit()方法

有时我们可能仅仅是想知道代码中的某个函数或某行语句的执行情况,此时用cProfile显得太重了(cProfile会输出所有函数的执行情况),我们可以import timeit,用timeit()包裹住我们要profile的函数或语句。例如:

import timeit

timeit.timeit('list(itertools.repeat("a", 100))', 'import itertools', number=10000000)

上面的代码会测试list(itertools.repeat("a", 100)) 10000000次,计算平均运行时间。

10.997665435877963

timeit同样可以在命令行中使用。例如:

$ python -m timeit "'-'.join(str(n) for n in range(100))"
20000 loops, best of 5: 10.5 usec per loop

第三方工具line_profiler

上面的介绍的工具都是Python或IPython自带的,提供的功能个更多是函数的运行时间。当我们需要深入地了解程序的执行情况时,上面介绍的3个工具就不够用了。此时我们需要请出代码优化神器--line_profiler。ine_profiler是Python的一个第三方库,其功能时基于函数的逐行代码分析工具。通过该库,可以对目标函数(允许分析多个函数)进行时间消耗分析,便于代码调优。

由于是第三方工具,使用line_profiler前需要安装

$ pip install line_profiler

安装成功后,我们就可以用@profile注解和kernprof命令来收集代码运行情况。我们将上面求质数和的例子改造成@profile注解的形式。

import math
import profile


@profile
def is_prime(n: int) -> bool:
    for i in range(2, math.floor(math.sqrt(n)) + 1):
        if n % i == 0:
            return False
    return True


@profile
def main():
    s = 0
    for i in range(2, 1000000):
        if is_prime(i):
            s += i
    print(s)


if __name__ == "__main__": 
    main()

然后用运行kernprof命令

$ kernprof -lv main.py

其中,参数-l表示用line-by-line profiler代替cProfile。要想@profile注解生效,一定要加-l参数。分析结果会保存到.lprof文件里,加上-v参数可以将结果写入文件后展示在控制台。可以用line_profiler命令查看分析结果:

$ python -m line_profiler <.lprof文件>

解决瓶颈

选择合适的算法和数据结构

利用profiling工具我们可以轻松找到程序的瓶颈所在。接下来就是如何解决瓶颈。有统计,90%的程序性能问题与算法和数据结构有关。选择合适的算法对提升程序性能最关重要。比如,如果要对包含上千元素的列表进行排序,不要用时间复杂度为O(n2)的冒泡排序,用快速排序(时间复杂度为O(nlogn))速度会快很多。

上面的例子讲的是算法对性能的影响。选择恰当的数据结构对性能影响也很大。比如,对海量数据进行查找。如果我们用列表存储数据,那么查找指定元素的时间复杂度为O(n);但如果我们用二叉树来存储,那么查找速度将提升为O(logn);如果我们用哈希表来存储,那么查找速度将变为O(1)

在描述算法复杂度时,我们通常使用大O标注法。大O标注法定义了算法所需时间的上界。例如插入排序,在最佳情况下需要线性时间,在最坏情况下需要二次时间。于是我们插入排序的时间复杂度是 O(n2),这是算法所需时间的上界,任何情况下都不会超过这个时间。

为此我专门整理了Python中常见操作的时间复杂度,供大家参考。

除了选择合适的算法和数据结构外,Python开发过程中也有一些技巧可以提升程序执行速度。

多用列表推导式

能用列表推导式的地方尽量用列表推导式。比如找出10000以内3的倍数,我们可以这么写:

l = []
for i in range (1, 10000):
    if i%3 == 0:
        l.append(i)

用列表推导式来写会更好,不但代码简洁,性能也比上面的代码高,因为列表推导式比append性能高。

l = [i for i in range (1, 100000) if i%3 == 0]

两段代码运行时间(%%timeit)对比

profile_p4.png

可见,循环100次取平均时间,列表推导式比用append要快。

少用.操作

开发中尽量避免使用.操作,比如

import math
val = math.sqrt(60)

应该替换为

from math import sqrt
val = sqrt(60)

以为当我们用.调用方法时, 会先调用__getattribute()____getattr()__ ,这两个方法中都包含一些字典操作,这些操作是会耗时的。

profile_p5.png

从上面的测试看,不用.要快很多。所以多用 from module import function 直接将方法引入,避免用.调用方法。

善用多重赋值

如果遇到连续变量赋值,比如

a = 2
b = 3
c = 4
d = 5

建议写成

a, b, c, d = 2, 3, 4, 5

避免使用全局变量

Python有global关键字声明或关联全局变量。但是处理全局变量比局部变量要花费更多时间。因此如无必要,勿用全局变量。

尽量使用库方法

一个功能如果Python标准库或第三方库已经提供,就用库方法,不要自己去实现。库方法都是经过高度优化的,甚至很多底层是C语言实现的,我们自己写的方法大概率不会比库方法更高效,并且自己写也不符合DRY精神。

用join拼接字符串

很多语言都是用+拼接字符串,当然Python也支持用+拼接字符串,但我更推荐用join()方法来拼接字符串,因为join()拼接字符串比+要快。+会创建新字符串并将旧字符串的值复制过去,而join()不会。

善用生成器

当我们要处理包含大量数据的列表时,用生成器会更快。我专门写了一篇文章《深入理解Python生成器和yield》,详细为大家讲解Python的生成器和为什么用生成器处理大文件或大数据集速度会更快。

利用加速工具

有很多项目致力于通过提供更好的运行环境或运行时优化来提升Python的速度。其中成熟的有PyPyNumba

PyPy平均比CPython快4.5倍;关于如何利用PyPy加速Python运行请看这篇文章《用PyPy加速Python程序》

Numba是一个JIT编译器,与Numpy配合良好,能将Python函数编译成机器码,极大提升科学计算的速度。如何利用Numb提升Python运行速度请看这篇文章《用Numba:一行代码将Python程序运行速度提升100倍》

所以如果条件允许,可以使用上面2个工具来加速Python代码。

用C/C++/Rust实现核心功能

C/C++/Rust都比Python快很多。Python的强大之处是可以和其他语言绑定。所以当处理某些对性能敏感的功能时,我们可以考虑用C/C++/Rust实现核心功能,然后绑定到Python语言上。Python中很多库都是这么做的,比如Numpy, Scipy, Pandas, Polars等。

如何用C语言开发C扩展模块请参阅《让你的Python程序像C语言一样快》

使用最新版本的Python

Python的核心团队也在不懈地优化Python的性能。每一次新版本的发布都比上一版本更加优化,速度也更快。就在前不久,Python发布了最新的3.11.0,这个版本的性能得到极大提升,比3.10性能提升10%~60%,比Python 2.7还快5%。所以,在条件允许的情况下,尽可能用更新版本的Python或获得性能上的提升。

目录
相关文章
|
9天前
|
机器学习/深度学习 存储 设计模式
Python 高级编程与实战:深入理解性能优化与调试技巧
本文深入探讨了Python的性能优化与调试技巧,涵盖profiling、caching、Cython等优化工具,以及pdb、logging、assert等调试方法。通过实战项目,如优化斐波那契数列计算和调试Web应用,帮助读者掌握这些技术,提升编程效率。附有进一步学习资源,助力读者深入学习。
|
3月前
|
开发框架 数据建模 中间件
Python中的装饰器:简化代码,增强功能
在Python的世界里,装饰器是那些静悄悄的幕后英雄。它们不张扬,却能默默地为函数或类增添强大的功能。本文将带你了解装饰器的魅力所在,从基础概念到实际应用,我们一步步揭开装饰器的神秘面纱。准备好了吗?让我们开始这段简洁而富有启发性的旅程吧!
68 6
|
10天前
|
数据采集 搜索推荐 C语言
Python 高级编程与实战:深入理解性能优化与调试技巧
本文深入探讨了Python的性能优化和调试技巧,涵盖使用内置函数、列表推导式、生成器、`cProfile`、`numpy`等优化手段,以及`print`、`assert`、`pdb`和`logging`等调试方法。通过实战项目如优化排序算法和日志记录的Web爬虫,帮助你编写高效稳定的Python程序。
|
16天前
|
数据采集 供应链 API
实战指南:通过1688开放平台API获取商品详情数据(附Python代码及避坑指南)
1688作为国内最大的B2B供应链平台,其API为企业提供合法合规的JSON数据源,直接获取批发价、SKU库存等核心数据。相比爬虫方案,官方API避免了反爬严格、数据缺失和法律风险等问题。企业接入1688商品API需完成资质认证、创建应用、签名机制解析及调用接口四步。应用场景包括智能采购系统、供应商评估模型和跨境选品分析。提供高频问题解决方案及安全合规实践,确保数据安全与合法使用。立即访问1688开放平台,解锁B2B数据宝藏!
|
17天前
|
API 开发工具 Python
【Azure Developer】编写Python SDK代码实现从China Azure中VM Disk中创建磁盘快照Snapshot
本文介绍如何使用Python SDK为中国区微软云(China Azure)中的虚拟机磁盘创建快照。通过Azure Python SDK的Snapshot Class,指定`location`和`creation_data`参数,使用`Copy`选项从现有磁盘创建快照。代码示例展示了如何配置Default Azure Credential,并设置特定于中国区Azure的`base_url`和`credential_scopes`。参考资料包括官方文档和相关API说明。
|
2月前
|
存储 缓存 Java
Python高性能编程:五种核心优化技术的原理与Python代码
Python在高性能应用场景中常因执行速度不及C、C++等编译型语言而受质疑,但通过合理利用标准库的优化特性,如`__slots__`机制、列表推导式、`@lru_cache`装饰器和生成器等,可以显著提升代码效率。本文详细介绍了这些实用的性能优化技术,帮助开发者在不牺牲代码质量的前提下提高程序性能。实验数据表明,这些优化方法能在内存使用和计算效率方面带来显著改进,适用于大规模数据处理、递归计算等场景。
86 5
Python高性能编程:五种核心优化技术的原理与Python代码
|
3月前
|
Python
课程设计项目之基于Python实现围棋游戏代码
游戏进去默认为九路玩法,当然也可以选择十三路或是十九路玩法 使用pycharam打开项目,pip安装模块并引用,然后运行即可, 代码每行都有详细的注释,可以做课程设计或者毕业设计项目参考
89 33
|
3月前
|
JavaScript API C#
【Azure Developer】Python代码调用Graph API将外部用户添加到组,结果无效,也无错误信息
根据Graph API文档,在单个请求中将多个成员添加到组时,Python代码示例中的`members@odata.bind`被错误写为`members@odata_bind`,导致用户未成功添加。
61 10
|
3月前
|
数据可视化 Python
以下是一些常用的图表类型及其Python代码示例,使用Matplotlib和Seaborn库。
通过这些思维导图和分析说明表,您可以更直观地理解和选择适合的数据可视化图表类型,帮助更有效地展示和分析数据。
127 8
|
3月前
|
Python
探索Python中的装饰器:简化代码,增强功能
在Python的世界里,装饰器就像是给函数穿上了一件神奇的外套,让它们拥有了超能力。本文将通过浅显易懂的语言和生动的比喻,带你了解装饰器的基本概念、使用方法以及它们如何让你的代码变得更加简洁高效。让我们一起揭开装饰器的神秘面纱,看看它是如何在不改变函数核心逻辑的情况下,为函数增添新功能的吧!

热门文章

最新文章