[collect]Python代码的性能优化技巧


之前在看《Python高手之路》那本书的第10章(性能与优化)时,做了一些读书笔记(其实就是摘抄),后来又看到了一篇文章:Python 代码性能优化技巧,两者有异曲同工之妙,再加上自己之前写Python脚本时的一些经验小结,将内容整理如下:

10.1 数据结构

如果使用正确的数据结构,大多数计算机问题都能以一种优雅而简单的方式解决,而Python就恰恰提供了很多可供选择的数据结构。

通常,有一种诱惑是实现自定义的数据结构,但这必然是徒劳无功、注定失败的想法。因为Python总是能够提供更好的数据结构和代码,要学会使用它们。

熟悉基本的Python数据结构和它所提供的方法,你可以凭此解决大多数问题

dict/set/collections

10.2 性能分析

  • cProfile 性能分析
  • dis 模块是Python字节码的反编译器,用起来很简单:dis.dis()函数会反编译作为参数传入的函数,并打印出这个函数运行的字节码指令的清单。

10.3 有序列表和二分查找

使用内置的bisect模块对有序列表进行二分查找

10.4 namedtupleslots

对有时创建只拥有固定属性的简单对象是非常有用的。

10.5 memoization

  • Memoization是指通过缓存纯函数返回结果来加速函数调用的一种技术。但当且仅当函数是纯函数时结果才可以被缓存。
  • 从Python3.3开始,functools模块提供了一个LRU缓存装饰器,该模块提供了对缓存命中、缺失等的统计。

10.6 PyPy

PyPy是符合标准的一个Python的高效实现。

10.7 通过缓冲区协议实现零复制

  • 对字符串进行大量的复制、分片和修改等操作是非常低效的。
  • 推荐使用memory_profiler模块逐行查看内存的使用情况。
  • memoryview对象。

10.8 Victor Stinner访谈

  • 优化Python代码的一个初步策略是什么?

针对Python的策略其实和在其他的语言中一样。首先需要定义良好的用例,以得到一个稳定可重现的基准。在没有可靠基准的情况下尝试不同的优化方法很可能导致时间的浪费和不成熟的优化。无用的优化可能使代码更糟,更不易懂,甚至更慢。有用的优化必须至少让程序加速5%。

  • 关于Python代码的性能分析和优化有许多不同的工具,你最喜欢的是哪个?
  1. Python 3.3 提供了一个新的 time.perf_counter() 函数,用来为基准测试衡量已耗用时间。
  2. 对于微基准测试,使用 timeit 模块是个不错的选择。
  3. 优化是非常花时间的,所以最好能专注于那些耗费最多CPU的函数,可以使用 cProfile 模块来进行分析。
  4. 测试运行应该不止一次,最少3次,一般5次就够了。
  • 能够改进性能的最有意思的Python技巧是什么?
  1. 尽可能重用标准库
  2. 应该有一种——最好只有一种——显而易见的方式去实现一个功能
  • 在哪些领域中Python的性能很差?哪些领域中应该小心使用?
  1. 首先需要确定瓶颈在哪,不是所有的问题都由Python引起
  2. multiprocessing
  3. 编写异步代码
  • 你见过最多的导致性能差的“错误”是什么?
  1. 在不需要复制的时候错误的使用了copy.deepcopy()
  2. 错误的使用不适合的数据结构(应该了解每个操作(add/get/delete)在不同的数据结构上的复杂性和影响

==

代码优化能够让程序运行更快,它是在不改变程序运行结果的情况下使得程序的运行效率更高,根据 80/20 原则,实现程序的重构、优化、扩展以及文档相关的事情通常需要消耗 80% 的工作量。优化通常包含两方面的内容:减小代码的体积,提高代码的运行效率。

改进算法,选择合适的数据结构

一个良好的算法能够对性能起到关键作用,因此性能改进的首要点是对算法的改进。在算法的时间复杂度排序上依次是:

O(1) -> O(lg n) -> O(n lg n) -> O(n^2) -> O(n^3) -> O(n^k) -> O(k^n) -> O(n!)

因此如果能够在时间复杂度上对算法进行一定的改进,对性能的提高不言而喻。但对具体算法的改进不属于本文讨论的范围,读者可以自行参考这方面资料。下面的内容将集中讨论数据结构的选择。

字典 (dictionary) 与列表 (list)

Python 字典中使用了 hash table,因此查找操作的复杂度为 O(1),而 list 实际是个数组,在 list 中,查找需要遍历整个 list,其复杂度为 O(n),因此对成员的查找访问等操作字典要比 list 更快。因此在需要多数据成员进行频繁的查找或者访问的时候,使用 dict 而不是 list 是一个较好的选择

集合 (set) 与列表 (list)

set 的 union, intersection,difference 操作要比 list 的迭代要快。因此如果涉及到求 list 交集,并集或者差的问题可以转换为 set 来操作

对循环的优化

对循环的优化所遵循的原则是尽量减少循环过程中的计算量,有多重循环的尽量将内层的计算提到上一层。

充分利用 Lazy if-evaluation 的特性

python 中条件表达式是 lazy evaluation 的,也就是说如果存在条件表达式 if x and y,在 x 为 false 的情况下 y 表达式的值将不再计算。因此可以利用该特性在一定程度上提高程序效率。

字符串的优化

python 中的字符串对象是不可改变的,因此对任何字符串的操作如拼接,修改等都将产生一个新的字符串对象,而不是基于原字符串,因此这种持续的 copy 会在一定程度上影响 python 的性能。对字符串的优化也是改善性能的一个重要的方面,特别是在处理文本较多的情况下。字符串的优化主要集中在以下几个方面:

  1. 在字符串连接的使用尽量使用 join() 而不是 + 。因此在字符的操作上 join 比 + 要快,因此要尽量使用 join 而不是 + 。
  2. 当对字符串可以使用正则表达式或者内置函数来处理的时候,选择内置函数。如 str.isalpha(),str.isdigit(),str.startswith((‘x’, ‘yz’)),str.endswith((‘x’, ‘yz’))
  3. 对字符进行格式化比直接串联读取要快,因此要使用:
out = "<html>%s%s%s%s</html>" % (head, prologue, query, tail)

而避免:

out = "<html>" + head + prologue + query + tail + "</html>"

使用列表解析(list comprehension)和生成器表达式(generator expression

列表解析要比在循环中重新构建一个新的 list 更为高效,因此我们可以利用这一特性来提高运行的效率。

其他优化技巧

  • 如果需要交换两个变量的值使用 a,b=b,a 而不是借助中间变量 t=a;a=b;b=t;
  • 在循环的时候使用 xrange 而不是 range;使用 xrange 可以节省大量的系统内存,因为 xrange() 在序列中每次调用只产生一个整数元素。而 range() 将直接返回完整的元素列表,用于循环时会有不必要的开销。在 python3 中 xrange 不再存在,里面 range 提供一个可以遍历任意长度的范围的 iterator。
  • 使用局部变量,避免”global”关键字。python 访问局部变量会比全局变量要快得多,因此可以利用这一特性提升性能。
  • if done is not None 比语句 if done != None 更快,读者可以自行验证;
  • 在耗时较多的循环中,可以把函数的调用改为内联的方式;
  • 使用级联比较 “x < y < z” 而不是 “x < y and y < z”;
  • while 1 要比 while True 更快(当然后者的可读性更好);
  • build in 函数通常较快,add(a,b) 要优于 a+b。

定位程序性能瓶颈

对代码优化的前提是需要了解性能瓶颈在什么地方,程序运行的主要时间是消耗在哪里,对于比较复杂的代码可以借助一些工具来定位,python 内置了丰富的性能分析工具,如 profile,cProfile 与 hotshot 等。其中 Profiler 是 python 自带的一组程序,能够描述程序运行时候的性能,并提供各种统计帮助用户定位程序的性能瓶颈。Python 标准模块提供三种 profilers:cProfile, profile 以及 hotshot。

对于大型应用程序,如果能够将性能分析的结果以图形的方式呈现,将会非常实用和直观,常见的可视化工具有 Gprof2Dot,visualpytune,KCacheGrind 等,读者可以自行查阅相关官网,本文不做详细讨论。

参考链接:

=EOF=


《“[collect]Python代码的性能优化技巧”》 有 9 条评论

  1. Python性能优化的方法路径
    http://blog.soliloquize.org/2018/05/26/Python%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96%E7%9A%84%E6%96%B9%E6%B3%95%E8%B7%AF%E5%BE%84/
    `
    决定是否优化:
    性能分析
    优化检查
    持续监控

    Python代码优化的常见方向:
    减少不必要的函数调用
    让运行时的动态计算静态化
    适当应用缓存
    减少gc
    修改明显错误的代码实现
    高频调用中改进一些基础用法
    优化算法
    将部分逻辑改为原生代码实现
    `

  2. 提升Python程序性能的7个习惯
    https://zhuanlan.zhihu.com/p/38160586
    `
    Python不以性能见长,但掌握一些技巧,也可尽量提高程序性能,避免不必要的资源浪费。

    1、使用局部变量
    尽量使用局部变量代替全局变量:便于维护,提高性能并节省内存。

    2、减少函数调用次数
    对象类型判断时,采用isinstance()最优,采用对象类型身份(id())次之,采用对象值(type())比较最次。

    3、采用映射替代条件查找
    映射(比如dict等)的搜索速度远快于条件语句(如if等)。Python中也没有select-case语句。

    4、直接迭代序列元素
    对序列(str、list、tuple等),直接迭代序列元素,比迭代元素的索引速度要更快。

    5、采用生成器表达式替代列表解析
    列表解析(list comprehension),会产生整个列表,对大量数据的迭代会产生负面效应。

    6、先编译后调用
    使用eval()、exec()函数执行代码时,最好调用代码对象(提前通过compile()函数编译成字节码),而不是直接调用str,可以避免多次执行重复编译过程,提高程序性能。

    7、模块编程习惯
    模块中的最高级别Python语句(没有缩进的代码)会在模块导入(import)时执行(不论其是否真的必要执行)。因此,应尽量将模块所有的功能代码放到函数中,包括主程序相关的功能代码也可放到main()函数中,主程序本身调用main()函数。
    可以在模块的main()函数中书写测试代码。在主程序中,检测__name__的值,如果为’__main__’(表示模块是被直接执行),则调用main()函数,进行测试;如果为模块名字(表示模块是被调用),则不进行测试。
    `

  3. 头条面试题:如何实现 LRU 原理?
    https://mp.weixin.qq.com/s/HE22NfxaCP6p5t39izvWdg
    `
    LRU 算法的设计原则是:如果一个数据在最近一段时间没有被访问到,那么在将来它被访问的可能性也很小。也就是说,当限定的空间已存满数据时,应当把最久没有被访问到的数据淘汰。

    这个算法的出发点在于:如果某个页面被访问了,则它可能马上还要访问。反之,如果很长时间未被访问,则它在最近一段时间也不会被访问。

    该算法的性能接近于最佳算法,但实现起来较困难。因为要找出最近最久未使用的页面,必须为每一页设置相关记录项,用于记录页面的访问情况,并且每访问一次页面都须更新该信息。这将使系统的开销加大,所以在实际系统中往往使用该算法的近似算法。
    `
    1.LRU原理和Redis实现——一个今日头条的面试题
    https://zhuanlan.zhihu.com/p/34133067

    2.LeetCode 算法题解——LRU 缓存机制
    https://zhuanlan.zhihu.com/p/57733537

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注