啥叫做基准测试(benchmark)代码?其实主要就是测试(benchmarking)和分析(profiling)你的代码执行多快,并且找到代码瓶颈(bottlenecks)在哪里。 执行该动作的主要的目的就是优化(optimization)。也许因为业务需要你并且让你的代码执行更快一些。 当面临这种情况时,你就需要找出你的代码是在哪些部分慢的。

本文覆盖如何使用不同工具进行代码测试。

timeit

Python再带一个模块timeit。你可以使用它来对小代码进行计时。timeit模块使用针对平台的时间函数,你可以得到非常准确的一个时间结果。

timeit模块有个命令行接口,也可以通过导入的方式。我们将从命令行中使用开始。打开终端执行如下命令:

$ python3 -m timeit -s "[ord(x) for x in 'abcdfghi']"
100000000 loops, best of 3: 0.00907 usec per loop $ python3 -m timeit -s "[chr(int(x)) for x in '123456789']"
100000000 loops, best of 3: 0.00897 usec per loop

发生啥啦?好吧,当你执行Python命令并且传递-m参数时,你是在告诉解释器去找该模块并且作为主要程序执行。 -s告知timeit模块执行一次代码。然后timeit就行执行代码三次,并且返回该三次的平均结果。该例子太过减少,它们 之间的区别可能不太明显。

你的机器上运行的结果可能跟我的会有一些偏差。

我们再写个简单函数,看看是否可以从命令行测试

# simple_func.py
def my_function():
try:
1 / 0
except ZeroDivisionError:
pass

整个函数的功能就是抛出一个异常然后直接忽略。很烂的例子。要通过命令使用timeit来测试,我们需要在命令空间中导入它,确保工作目录在当前文件目录下,然后执行:

$ python3 -m timeit "import simple_func; simple_func.my_function()"
1000000 loops, best of 3: 0.753 usec per loop

这里我们就导入该模块并且直接my_function()函数。注意我们使用了;来分来导入和执行代码。现在可以开始学习如何在Python脚本代码内部使用timeit了。

导入timeit测试

在代码中使用timeit模块也是相当容易的。我们还是使用之前很烂的例子:

def my_function():
try:
1 / 0
except ZeroDivisionError:
pass if __name__ == "__main__":
import timeit
setup = "from __main__ import my_function"
print(timeit.timeit("my_function()", setup=setup))

我们执行通过执行该脚本来验证。我们导入了timeit模块,然后调用timeit函数,参数的setup字符串将导入要测试函数到timeit的作用域中。现在 我们实现自己的装饰器测试函数。

使用装饰器

实现自己的装饰器计时函数是一件很有趣的事情,虽然不一定有timeit精确。

import random
import time def timerfunc(func):
"""
A timer decorator
"""
def function_timer(*args, **kwargs):
"""
A nested function for timing other functions
"""
start = time.time()
value = func(*args, **kwargs)
end = time.time()
runtime = end - start
msg = "The runtime for {func} took {time} seconds to complete"
print(msg.format(func=func.__name__, time=runtime))
return value
return function_timer @timerfunc
def long_runner():
for x in range(5):
sleep_time = random.choice(range(1, 5))
time.sleep(sleep_time) if __name__ == "__main__":
long_runner()

该例子中,我们导入了Python标准库的random和time模块。然后我们创建了装饰器函数。你将注意到它接收了一个函数以及 包含了另一函数。嵌套的函数在调用实际函数时将抓取时间,然后等待函数返回并抓取结束时间。现在我们知道函数所花费的时间,我们将它 打印出来。当然该装饰器函数也需要将实际函数执行返回的结果返回回来。

接下来的函数就是包装了该计时装饰器函数。你可以看到这里使用了随机休眠,该操作只是用来模拟耗时长程序。 也许你用来执行连接数据库(或者较大查询),跑线程或其他事情。

每次跑这段代码,结果都会有些不同。自己试试!

创建计时上下文管理器

一些码农可能更喜欢使用上下文的方式来测试小段代码。我们来构建看看:

import random
import time class MyTimer:
def __init__(self):
self.start = time.time() def __enter__(self):
return self def __exit__(self, exc_type, exc_val, exc_tb):
end = time.time()
runtime = end - self.start
msg = 'The function took {time} seconds to complete'
print(msg.format(time=runtime)) def long_runner():
for x in range(5):
sleep_time = random.choice(range(1, 5))
time.sleep(sleep_time) if __name__ == "__main__":
with MyTimer():
long_runner()

本例中,我们使用__init__方法来开始我们计时,__enter__方法不做任何事情,仅仅返回它自己。__exit__方法 中抓取了结束时间,计算了总执行时间并打印。

然后我们就使用上下文管理器来测试我们执行的代码。

cProfile

Python内建了分析器。profile和cProfile模块。profile模块是纯Python实现,在测试过程中可能会增加很多额外的开支。所以推荐还是使用更快的cProfile。

我们不会过细的讲解该模块,我们来看看一些例子。

>>> import cProfile
>>> cProfile.run("[x for x in range(1500)]")
4 function calls in 0.000 seconds Ordered by: standard name ncalls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 0.000 0.000 <string>:1(<listcomp>)
1 0.000 0.000 0.000 0.000 <string>:1(<module>)
1 0.000 0.000 0.000 0.000 {built-in method exec}
1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}

我们来了解一下内容。第一行显示了有4个函数调用。下一行告诉结果排序方式。根据文档,standard name指的是最右边的列。 这里我们有好多列数据:

  • ncalls: 调用了多少次
  • tottime: 函数所花费的总时间
  • percall: ncalls / tottime
  • cumtime: 涵盖了该函数下的所有子函数(甚至地柜)函数所话费的时间
  • 第二个percall: cumtime / 原始钓鱼那个
  • filename: fileno (function) 每个函数响应信息

你也可以像timeit模块一样在命令行执行cProfile。主要的区别就是需要传递Python脚本而不是代码作为参数。

python3 -m cProfile test.py

尝试执行下该命令,看看执行结果。

line_profiler

有个第三方提供的项目叫line_profiler,它用来分析每行代码执行时间。它也有一个叫做kenprof的脚本使用line_profiler来分析Python应用和脚本。只需使用pip来安装该包。

 pip install line_profiler

在使用line_profiler之前我们需要一些代码来分析。不过我向先告诉你命令行中如何调用line_profiler.实际上需要执行kernprof脚本来调用line_profiler。

 kernprof -l silly_functions.py

执行完后将会打印出:Wrote profile results to silly_functions.py.lpro。该文件不能直接查看,是一个二进制文件。 当我们执行kernprof时,其实是在__builtins__命名空间中注入LineProfiler实例。该实例称为profile,用来作为装饰器函数。让我们来写实际脚本:

import time

@profile
def fast_function():
print("I'm fast function!") @profile
def slow_function():
time.sleep(2)
print("I'm slow function!") if __name__ == "__main__":
fast_function()
slow_function()

现在我们拥有使用了profile装饰的函数。如果你直接执行该脚本将会报NameError错误,因为并没有定义profile。 所以记得再实际使用时移除@profile装饰器函数。

我们来学习如果查看分析器的结果。有两种办法,第一使用line_profiler模块读结果文件:

$ python -m line_profiler silly_functions.py.lprof

还有种是在执行kernprof时带上-v参数:

$ kernprof -l -v silly_functions.py

无论哪种办法你都将得到如下结果:

I'm fast function!
I'm slow function!
Wrote profile results to silly_functions.py.lprof
Timer unit: 1e-06 s Total time: 4e-05 s
File: silly_functions.py
Function: fast_function at line 3 Line # Hits Time Per Hit % Time Line Contents
==============================================================
3 @profile
4 def fast_function():
5 1 40 40.0 100.0 print("I'm fast function!") Total time: 2.00227 s
File: silly_functions.py
Function: slow_function at line 7 Line # Hits Time Per Hit % Time Line Contents
==============================================================
7 @profile
8 def slow_function():
9 1 2002114 2002114.0 100.0 time.sleep(2)
10 1 157 157.0 0.0 print("I'm slow function!")

我们可以看到结果中显示了每行代码的执行时间相关信息。有6列内容。我们来看看每列代表的意思。

  • Line#: 分析的代码行数
  • Hits: 执行次数
  • Time: 该行代码执行时间
  • Per Hit: 每次执行时间
  • % Time: 函数中所占时间比率
  • Line Contents: 执行的代码

如果你用IPython,你可能想知道IPython的魔术方法%lprun可以分析制定函数,甚至是具体代码。

memory_profiler

另一个用来分析的第三方包叫做memory_profiler.它用来分析每行代码在进程中所消耗的内存。我们也使用pip来安装它。

pip install memory_profiler

装好后,我们也需要代码来评测。memory_prifiler执行方式和line_profiler非常相似,它将在__builtins__注入profiler装饰器来测试函数。

@profile
def mem_func():
lots_of_numbers = list(range(1500))
x = ['letters'] * (5 ** 10)
del lots_of_numbers
return None if __name__ == "__main__":
mem_func()

本例中我们创建了1500整形list。然后我们又创建了9765625(5的10次方)字符串李彪。最后我们删除了第一个list并返回。memory_profiler没有提供另外一个脚本来让你执行该程序,而是直接通过-m参数来调用模块。

$ python -m memory_profiler memo_prof.py
Filename: memo_prof.py Line # Mem usage Increment Line Contents
================================================
1 13.133 MiB 0.000 MiB @profile
2 def mem_func():
3 13.387 MiB 0.254 MiB lots_of_numbers = list(range(1500))
4 87.824 MiB 74.438 MiB x = ['letters'] * (5 ** 10)
5 87.938 MiB 0.113 MiB del lots_of_numbers
6 87.938 MiB 0.000 MiB return None

结果的提供的信息就非常直观了。包含代码行数和改行执行完后的内存使用,接下来是内存增加量,最后是执行的代码。

memory_profiler也提供mprof用来创建完整的内存使用报表,而不是每行结果。

$ mprof run memo_prof.py
mprof: Sampling memory every 0.1s
running as a Python program...

mprof也提供来图示来查看应用内存消耗,需要查看图表前我们需要先安装matplotlib

$ pip install matplotlib

然后执行:

$ mprof plot

我们得到如下图:

profilehooks

最后个要介绍的第三方包是profilehooks.它拥有一系列包装器来分期函数。安装:

pip install profilehooks

安装完后,我们修改上次例子替换成profilehooks:

from profilehooks import profile

@profile
def mem_func():
lots_of_numbers = list(range(1500))
x = ['letters'] * (5 ** 10)
del lots_of_numbers
return None if __name__ == "__main__":
mem_func()

仅仅要做的是导入profilehooks并且包装到要测试的函数中。执行后:

*** PROFILER RESULTS ***
mem_func (profhooks.py:3)
function called 1 times 2 function calls in 0.036 seconds Ordered by: cumulative time, internal time, call count ncalls tottime percall cumtime percall filename:lineno(function)
1 0.036 0.036 0.036 0.036 profhooks.py:3(mem_func)
1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
0 0.000 0.000 profile:0(profiler)

输出的结果和cProfile是一样的,你可以看看我之前对这些列的描述。profilehooks提供了两个装饰器。我们来看看第一个timecall,它提供了函数执行时间。

from profilehooks import timecall

@timecall
def mem_func():
lots_of_numbers = list(range(1500))
x = ['letters'] * (5 ** 10)
del lots_of_numbers
return None if __name__ == "__main__":
mem_func()

执行后你将看到如下输出:

  mem_func (profhooks2.py:3):
0.056 seconds

整个包装其输出整个函数执行时间,和timeit是一样的。

另一个profilehooks提供的装饰器是coverage。它用来打印函数每行代码涵盖数。不过我看来用处到不是很大,你可以自己试试:

*** COVERAGE RESULTS ***
mem_func (profhooks3.py:3)
function called 1 times @coverage
def mem_func():
1: lots_of_numbers = list(range(1500))
1: x = ['letters'] * (5 ** 10)
1: del lots_of_numbers
1: return None

最后要告诉你的是,你也可以通过-m参数来从命令行中执行profilehooks.

python -m profilehooks mymodule.py

profilehooks是一个新包,不过我到觉得应该会很有前途。

总结

本文涵盖了需要信息。分享了如何使用Python内建模块,timeit和cProfile来测试和分期你的代码,我还学会了如何自己包装计时包装 器和上下文管理器。然后我们还学习了一些第三方包:line_profiler, memory_profiler和profilehooks。就目前,你已经学会如何分析你的代码,查找代码瓶颈。

如何对你的Python代码进行基准测试的更多相关文章

  1. 随机森林入门攻略(内含R、Python代码)

    随机森林入门攻略(内含R.Python代码) 简介 近年来,随机森林模型在界内的关注度与受欢迎程度有着显著的提升,这多半归功于它可以快速地被应用到几乎任何的数据科学问题中去,从而使人们能够高效快捷地获 ...

  2. 改改Python代码,运行速度还能提升6万倍

    这份最新研究指出,在后摩尔定律时代,人类所获得的的算力提升将更大程度上来源于计算堆栈的「顶层」,即软件.算法和硬件架构,这将成为一个新的历史趋势. 很多人学习python,不知道从何学起.很多人学习p ...

  3. 可爱的豆子——使用Beans思想让Python代码更易维护

    title: 可爱的豆子--使用Beans思想让Python代码更易维护 toc: false comments: true date: 2016-06-19 21:43:33 tags: [Pyth ...

  4. if __name__== "__main__" 的意思(作用)python代码复用

    if __name__== "__main__" 的意思(作用)python代码复用 转自:大步's Blog  http://www.dabu.info/if-__-name__ ...

  5. Python 代码风格

    1 原则 在开始讨论Python社区所采用的具体标准或是由其他人推荐的建议之前,考虑一些总体原则非常重要. 请记住可读性标准的目标是提升可读性.这些规则存在的目的就是为了帮助人读写代码,而不是相反. ...

  6. 一行python代码实现树结构

    树结构是一种抽象数据类型,在计算机科学领域有着非常广泛的应用.一颗树可以简单的表示为根, 左子树, 右子树. 而左子树和右子树又可以有自己的子树.这似乎是一种比较复杂的数据结构,那么真的能像我们在标题 ...

  7. [Dynamic Language] 用Sphinx自动生成python代码注释文档

    用Sphinx自动生成python代码注释文档 pip install -U sphinx 安装好了之后,对Python代码的文档,一般使用sphinx-apidoc来自动生成:查看帮助mac-abe ...

  8. 上传自己的Python代码到PyPI

    一.需要准备的事情 1.当然是自己的Python代码包了: 2.注册PyPI的一个账号. 二.详细介绍 1.代码包的结构: application \application __init__.py m ...

  9. 如何在batch脚本中嵌入python代码

    老板叫我帮他测一个命令在windows下消耗的时间,因为没有装windows那个啥工具包,没有timeit那个命令,于是想自己写一个,原理很简单: REM timeit.bat echo %TIME% ...

随机推荐

  1. Zabbix触发器函数(取前后差值)

    获取最新值last zabbix触发器方法last用于获取item最新值或者第几个值以及某个时间的哪一个值. Last (most recent) T value is > N Last (mo ...

  2. vue 过滤与全文索引

    过滤 与 全文索引 <template> <div> <input type="text" v-model="query"> ...

  3. emcas自己所熟悉的快捷键

    刚开始用emacs,看完Tutorial了后又用emcas做了一些笔记. 现将自己脑海中觉得比较重要的快捷键一一列出,该列表将持续更新: C = Ctrl  M = Alt 查找或打开(新)文件 C- ...

  4. Atitit.attilax的 case list 项目经验 案例列表

    Atitit.attilax的 case list 项目经验 案例列表 1. Atian inputmethod 输入法3 2. Ati desktop engine桌面引擎3 3. Acc资金账户系 ...

  5. Atitit.python web环境的配置 attilax 总结

    Atitit.python web环境的配置 attilax 总结 1. 下载modpython/1 1.1. 安装python2.5.11 1.2. 安装modpython1 2. 设置py文件的u ...

  6. YUV格式详细解释与FFMPEG的关系

    YUV主要的采样格式 主要的采样格式有YCbCr 4:2:0.YCbCr 4:2:2.YCbCr 4:1:1和 YCbCr 4:4:4.其中YCbCr 4:1:1 比较常用,其含义为:每个点保存一个 ...

  7. driver makefile

    1.单模块单文件//*********************************************obj-m := hello.o KDIR := /home/akaedu/kernel/ ...

  8. 如何在GitHub上删除某个文件夹?

    步骤: (以删除.idea文件夹为例) git rm -r --cached .idea #--cached不会把本地的.idea删除 git commit -m 'delete .idea dir' ...

  9. Mysql 5.7 liunx 忘记密码的补救方法

    linux下mysql的root密码忘记解决方 1.修改MySQL的登录设置 # vim /etc/my.cnf 在[mysqld]的段中加上一句: skip-grant-tables 例如: [my ...

  10. Easyui tree方法扩展getImperfectCheck

    $.extend($.fn.tree.methods, { getImperfectCheck : function (jq) { var checked = jq.find("span.t ...