这几招,让你快速提升 Python 项目的性能

这几招,让你快速提升 Python 项目的性能

随谈#macOS

March 5, 20190

本文是《提升你的 Python 项目代码健壮性和性能》系列的第 4 篇文章。

本系列总计 8 篇文章目录

  1. 用 Type Annotation 提升你的 Python 代码健壮性
  2. 如何通过测试提升 Python 代码的健壮性
  3. 如何保证 Django 项目的数据一致性
  4. 这几招,让你快速提升 Python 项目的性能
  5. 为你的项目快速搭建 ELKFA 日志系统
  6. 如何写出整洁的 Python 代码 上
  7. 如何写出整洁的 Python 代码 中
  8. 如何写出整洁的 Python 代码 下

本文讲的是

**当你觉得某个地方运行比较慢了,此时此刻的你,有哪些小技巧可以快速的帮**
**你定位性能问题。**

0x00 前言

本文主要目的在于介绍一些 Python 项目常规的性能优化的姿势与技巧。

优化的最简单的途径就是,没用户 + 调用次数少

嗯?但这种优化方式...... 实在是没什么好说的。

  • 优化口诀 1: 先做对,布监控,再做好。
  • 优化口诀 2: 过早优化是万恶之源。
  • 优化口诀 3: 去优化那些需要优化的地方。

  • Step 1. Get it right.
  • Step 2. Test it's right.
  • Step 3. Monitor.
  • Step 4. Profile if slow.
  • Step 5. Try Optimize.
  • Step 6. Repeat from 2.

有的人站出来说,我写程序就是要一步到位,把能优化的点一次性搞定。

请不要听他的,因为优化是无止境的。唯快不破

能一次写出优雅清晰而且性能高的代码的人,一般很少见到。毕竟需要考虑的点太多了。

基于上面的认知,代码的可维护性是第一位的。

  • 写代码的首先应该是代码很清晰,非常容易维护。
  • 然后在没有过分降低可维护性的情况下,作出性能的优化。

0x01 Python 优化的五件武器

钟声响起归家的讯号,刚回到家。

公司群响起加班的讯号,用户反应服务响应总是超时。

你打开电脑,隐隐约约觉得是某个函数的问题。这个函数的功能比较多,调试了很久才调试通。

浏览代码。大致定位了这个问题可能会在下面的几个函数中。

def red_packet_calculation_algorithm():
	pass

def user_stats_calculation_algorithm():
	pass

def dashboard_calculation_algorithm():
	pass

如何确定是哪个函数需要优化呢?

很简单,到 IPython 里面执行一下就就知道了。感觉慢的就是目标函数。

总觉得执行一下这个操作有点不稳定。如果有个工具,可以直接执行很多次,然后作出统计就好了。

这就是 Python 代码优化第一件武器 timeit

第一件武器 timeit

通常某段代码有问题,最直接的方法就是跑一下这段代码。

在 IPython 里执行

# ipython
%time your-algorithm

timeit 将代码执行多次,取均值

一般这个时候,你就可以初步定位问题所在了。

比如,发现 user_stats_calculation_algorithm 在 一个 for 循环里面走了数据库查询。

也有一些函数并不是那么容易定位。

即,通过这个 timeit 知道了某个函数执行比较慢,但那个函数 里面还有很多函数,通过肉眼观察,还是没有办法来解决呀。

这个时候你想了,如果能看到哪些语句执行的次数多一些,耗时长一些,就好了。

这就是 Python 代码优化第二件武器 profile。

第二件武器 profile 与 cprofile

在 ipython 中运行

这么一看,耗时操作一览无遗。

语句级别的 Profile 有了,但其实,很多时候也并不能解决你的问题。

如果能有这么个东西,即,能在代码旁边注释一下,执行次数和耗时就好了。

这就是 Python 代码优化第三件武器 line profile。

第三件武器 line profiler

能在代码旁边注释,执行次数和耗时。如下

Pystone(1.1) time for 50000 passes = 2.48
This machine benchmarks at 20161.3 pystones/second
Wrote profile results to pystone.py.lprof
Timer unit: 1e-06 s

File: pystone.py
Function: Proc2 at line 149
Total time: 0.606656 s

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
   149                                           @profile
   150                                           def Proc2(IntParIO):
   151     50000        82003      1.6     13.5      IntLoc = IntParIO + 10
   152     50000        63162      1.3     10.4      while 1:
   153     50000        69065      1.4     11.4          if Char1Glob == 'A':
   154     50000        66354      1.3     10.9              IntLoc = IntLoc - 1
   155     50000        67263      1.3     11.1              IntParIO = IntLoc - IntGlob
   156     50000        65494      1.3     10.8              EnumLoc = Ident1
   157     50000        68001      1.4     11.2          if EnumLoc == Ident1:
   158     50000        63739      1.3     10.5              break
   159     50000        61575      1.2     10.1      return IntParIO

这个可谓是 Python 世界里时间性能优化的顶级工具了。

第四件武器 memory profiler

说完了时间上的优化,再说说空间上的优化。

如何检查内存呢?

这需要 Python 代码优化第四件武器 memory profiler。

这个工具用于查看 Python 程序的内存占用情况

但,知道了执行某些代码之后,内存是多少又能如何呢?

不见得能定位出来是什么东西

内存中这么多 objects 我上哪看去?

假设内存泄漏了,我再怎么 profile, 内存都是一直泄漏的呀。

总要想办法定位出是哪些类型的有问题。

第五件武器 pympler

这需要 Python 代码优化第五件武器 pympler。这是我从雨痕的《Python 学习笔记》里看到的

这个工具特别适合给当前所有的 objects 的内存占用情况做简单统计。

之前的一次线上代码出内存泄漏,检查了自己的代码确定没有问题之后,将目光放在了第三 方库上。

但第三方库也有不少,检查半天依旧没有什么进展。

from pympler import tracker

# 在多处打点,并且将结果打到日志里。
memory_tracker = tracker.SummaryTracker()

每次打印出来的结果大致是这样子的。

types |   # objects |   total size
================== | =========== | ============
              dict |           1 |     280    B
              list |           1 |     176    B
  _sre.SRE_Pattern |           1 |      88    B
             tuple |           1 |      80    B
               str |           0 |       7    B

刚开始都还挺正常,运行了一段时间之后,日志中的部分涉及到 flask-sqlalchemy 的 objects 和 total size 保持了坚挺的增长。

最后发现 flask-sqlalchemy 如果 设置了 SQLALCHEMY_RECORD_QUERIES 为 True 的话,

每次查询都会往 current_app.sqlalchemy_queries 里增加 DebugQueryTuple, 很快就内存泄漏了。

queries = _app_ctx_stack.top.sqlalchemy_queries
queries.append(_DebugQueryTuple((
		statement, parameters, context._query_start_time, _timer(),
		_calling_context(self.app_package)
)))

其他神器

可视化调用

当然,也有一些比较方便的工具是用来查看函数的调用信息的

效果大概是这样子

当然,也有其他的工具

https://stackoverflow.com/questions/582336/how-can-you-profile-a-python-script

0x02 优化 Web 项目

提前优化

在使用 Django 项目的时候,我必须要安装的第三方库就是 djangodebugtools

这个工具用起来有多舒服呢?

可以直接 Profile SQL 语句

甚至可以直接 explain sql 以及 查看缓存情况

做好监控

如何监控,监控什么指标?这属于日志的范畴了。

日志的道术器分别是什么,这将在下一篇文章来具体介绍一下如何打日志。

0x03 性能优化建议

笔者列了一些大方向上的优化建议,具体是要靠积累。

建议 1. 务必了解 Python 里面的负优化常识

  1. 不要在 for loop 里面不断的链接 string, 用列表 +JOIN 的方式会更加合适。

建议 2. 能用内置的模块就不要手动实现

  1. 比如,当你想做一些字符串上的变动的时候,不防先查看一下 string / textwrap / re / difflib 里是不是满足你的要求了
  2. 比如你操作一组比较类似的数据类型,可以考虑看下 enum / collection / itertools / array / heapq 里面是不是已经满足你的要求了。

笔者在 https://zhuanlan.zhihu.com/p/32504320 中曾经遇到过统计的问题。

当时遇到的问题场景是

有 400 组 UUID 集合,每个列表数量在 1000000 左右,列表和列表之间重复部分并不是很大。我想拿到去重之后的所有 UUID,应该怎么处理

# 版本一,运行遥遥无期
list_of_uuid_set = [ set1 , set2 ... set400 ]
all_uuid_set = reduce(lambda x: x | y, list_of_uuid_set)

# 版本二,运行遥遥无期

def merge(list1,list2):
    list1.append(list2)
    return list1

list_of_uuid_list = [ list1 , list2 ... list400 ]
all_uuid_set = set(reduce(merge, list_of_uuid_list))

# 版本三,5s

list_of_uuid_list = [ list1 , list2 ... list400 ]
all_uuid_set = set(list(itertools.chain(*list_of_uuid_list)))

合适的数据结构和合适的算法,确实能让代码变得清晰,高效,优雅。

建议 3. 能用优质的第三方库就不要手动实现

除了一些内置的模块,

  • 一些优秀的软件所依赖的第三方包也是非常值得留意的。
  • 一般能上 C 库的,用于解析的依赖包性能不错,比如 LXML/Numpy 这类包

0xDD 结论

本文讲的是,当你觉得某个地方运行比较慢了,此时此刻的你,有哪些小技巧可以快速的帮 你定位性能问题。

其实还有很多悬而未决的问题:

  1. 定位了问题,如何解决问题?
  2. 如何觉察到某个地方运行比较慢呢?

对于第一点,还是得多看多搜多练。用《亮剑》中的李云龙的话说:

真正的神枪手是战场上用子弹喂出来的。打得多了,感觉就有了,眼到手就到,抬枪就有,弹弹咬肉,这就叫神枪手。

对于第二点,就是下一篇文章需要解决的问题了。

  1. 通过日志来判断。
  2. 通过打点和结合 APMServer 来判断。

0xEE 参考链接