找回密码
 立即注册
首页 业界区 业界 让 Python 代码飙升330倍:从入门到精通的四种性能优化 ...

让 Python 代码飙升330倍:从入门到精通的四种性能优化实践

贺蛟亡 2025-7-10 00:26:50
花下猫语:性能优化是每个程序员的必修课,但你是否想过,除了更换算法,还有哪些“大招”?这篇文章堪称典范,它将一个普通的函数,通过四套组合拳,硬生生把性能提升了 330 倍!作者不仅展示了“术”,更传授了“道”。让我们一起跟随作者的思路,体验一次酣畅淋漓的优化之旅。
PS.本文选自最新一期Python 潮流周刊,如果你对优质文章感兴趣,诚心推荐你订阅我们的专栏。
作者:Itamar Turner-Trauring
译者:豌豆花下猫@Python猫
英文:330× faster: Four different ways to speed up your code
声明:本翻译是出于交流学习的目的,为便于阅读,部分内容略有改动。转载请保留作者信息。
温馨提示: 本文原始版本与当前略有不同,比如曾经提到过500倍加速;本文已根据实际情况重新梳理,使论证更清晰。
当你的 Python 代码慢如蜗牛,而你渴望它快如闪电时,其实有很多种提速方式,从并行化到编译扩展应有尽有。如果只盯着一种方法,往往会错失良机,最终的代码也难以达到极致性能。
为了不错过任何潜在的提速机会,我们可以从“实践”的角度来思考。每种实践:

  • 以独特方式加速你的代码
  • 涉及不同的技能和知识
  • 可以单独应用
  • 也可以组合应用,获得更大提升
为了让这一点更具体,本文将通过一个案例演示多种实践的应用,具体包括:

  • 效率(Efficiency): 消除浪费或重复的计算。
  • 编译(Compilation): 利用编译型语言,并巧妙绕开编译器限制。
  • 并行化(Parallelism): 充分发挥多核CPU的威力。
  • 流程(Process): 采用能产出更快代码的开发流程。
我们将看到:

  • 仅用效率实践,就能带来近 2倍 提速。
  • 仅用编译实践,可实现 10倍 提速。
  • 两者结合,速度更上一层楼。
  • 最后加上并行化实践,最终实现 330倍 惊人加速。
我们的例子:统计字母频率

我们有一本英文书,简·奥斯汀的《诺桑觉寺》:
  1. with open("northanger_abbey.txt") as f:
  2.     TEXT = f.read()
复制代码
我们的目标是分析书中字母的相对频率。元音比辅音更常见吗?哪个元音最常见?
下面是最初的实现:
  1. from collections import defaultdict
  2. def frequency_1(text):
  3.     # 一个当键不存在时默认值为0的字典
  4.     counts = defaultdict(lambda: 0)
  5.     for character in text:
  6.         if character.isalpha():
  7.             counts[character.lower()] += 1
  8.     return counts
复制代码
运行结果如下:
  1. sorted(
  2.     (count, letter) for (letter, count)
  3.     in frequency_1(TEXT).items()
  4. )
复制代码
  1. [(1, 'à'),
  2. (2, 'é'),
  3. (3, 'ê'),
  4. (111, 'z'),
  5. (419, 'q'),
  6. (471, 'j'),
  7. (561, 'x'),
  8. (2016, 'k'),
  9. (3530, 'v'),
  10. (5297, 'b'),
  11. (5404, 'p'),
  12. (6606, 'g'),
  13. (7639, 'w'),
  14. (7746, 'f'),
  15. (7806, 'y'),
  16. (8106, 'c'),
  17. (8628, 'm'),
  18. (9690, 'u'),
  19. (13431, 'l'),
  20. (14164, 'd'),
  21. (20675, 's'),
  22. (21107, 'r'),
  23. (21474, 'h'),
  24. (22862, 'i'),
  25. (24670, 'n'),
  26. (26385, 'a'),
  27. (26412, 'o'),
  28. (30003, 't'),
  29. (44251, 'e')]
复制代码
毫无意外,出现频率最高的字母是 "e"。
那我们如何让这个函数更快?
流程实践:测量与测试

软件开发不仅依赖于源代码、库、解释器、编译器这些“产物”,更离不开你的工作“流程”——也就是你做事的方法。性能优化同样如此。本文将介绍两种在优化过程中必不可少的流程实践:

  • 通过基准测试和性能分析来测量代码速度。
  • 测试优化后的代码,确保其行为与原始版本一致。
我们可以先用 line_profiler 工具分析函数,找出最耗时的代码行:
  1. Line #      Hits   % Time  Line Contents
  2. ========================================
  3.      3                     def frequency_1(text):
  4.      4                         # 一个当键不存在时默认值为0的字典
  5.      5                         # available:
  6.      6         1      0.0      counts = defaultdict(lambda: 0)
  7.      7    433070     30.4      for character in text:
  8.      8    433069     27.3          if character.isalpha():
  9.      9    339470     42.2              counts[character.lower()] += 1
  10.     10         1      0.0      return counts
复制代码
效率实践:减少无用功

效率实践的核心,是用更少的工作量获得同样的结果。这类优化通常在较高的抽象层面进行,无需关心底层CPU细节,因此适用于大多数编程语言。其本质是通过改变计算逻辑来减少浪费。
减少内循环的工作量

从上面的性能分析可以看出,函数大部分时间都花在 counts[character.lower()] += 1 这行。显然,对每个字母都调用 character.lower() 是种浪费。我们一遍遍地把 "I" 转成 "i",甚至还把 "i" 转成 "i"。
优化思路:我们可以先分别统计大写和小写字母的数量,最后再合并,而不是每次都做小写转换。
  1. def frequency_2(text):
  2.     split_counts = defaultdict(lambda: 0)
  3.     for character in text:
  4.         if character.isalpha():
  5.             split_counts[character] += 1
  6.     counts = defaultdict(lambda: 0)
  7.     for character, num in split_counts.items():
  8.         counts[character.lower()] += num
  9.     return counts
  10. # 确保新函数结果与旧函数完全一致
  11. assert frequency_1(TEXT) == frequency_2(TEXT)
复制代码
说明:这里的 assert 就是流程实践的一部分。一个更快但结果错误的函数毫无意义。虽然你在最终文章里看不到这些断言,但它们在开发时帮我抓出了不少bug。
基准测试(也是流程实践的一环)显示,这个优化确实让代码更快了:
| frequency_1(TEXT) | 34,592.5 µs |
| frequency_2(TEXT) | 25,798.6 µs |
针对特定数据和目标进行优化

我们继续用效率实践,这次针对具体目标和数据进一步优化。来看下最新代码的性能分析:
  1. Line #      Hits   % Time  Line Contents
  2. ========================================
  3.      3                     def frequency_2(text):
  4.      4         1      0.0      split_counts = defaultdict(lambda: 0)
  5.      5    433070     33.6      for character in text:
  6.      6    433069     32.7          if character.isalpha():
  7.      7    339470     33.7              split_counts[character] += 1
  8.      8
  9.      9         1      0.0      counts = defaultdict(lambda: 0)
  10.     10        53      0.0      for character, num in split_counts.items():
  11.     11        52      0.0          counts[character.lower()] += num
  12.     12         1      0.0      return counts
复制代码
可以看到,split_counts[character] += 1 依然是耗时大户。怎么加速?答案是用 list 替换 defaultdict(本质上是 dict)。list 的索引速度远快于 dict:

  • list 存储条目只需一次数组索引
  • dict 需要计算哈希、可能多次比较,还要内部数组索引
但 list 的索引必须是整数,不能像 dict 那样用字符串,所以我们要把字符转成数字。幸运的是,每个字符都能用 ord() 查到数值:
  1. ord('a'), ord('z'), ord('A'), ord('Z')
  2. # (97, 122, 65, 90)
复制代码
用 chr() 还能把数值转回字符:
  1. chr(97), chr(122)
  2. # ('a', 'z')
复制代码
所以可以用 my_list[ord(character)] += 1 计数。但前提是我们得提前知道 list 的大小。如果处理任意字母字符,list 可能会很大:
[code]ideograph = '
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册