本文是《提升你的 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 参考链接