如何对你的Python代码进行基准测试

啥叫做基准测试(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。就目前,你已经学会如何分析你的代码,查找代码瓶颈。

时间: 2024-10-22 16:12:35

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

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

随机森林入门攻略(内含R.Python代码) 简介 近年来,随机森林模型在界内的关注度与受欢迎程度有着显著的提升,这多半归功于它可以快速地被应用到几乎任何的数据科学问题中去,从而使人们能够高效快捷地获得第一组基准测试结果.在各种各样的问题中,随机森林一次又一次地展示出令人难以置信的强大,而与此同时它又是如此的方便实用. 需要大家注意的是,在上文中特别提到的是第一组测试结果,而非所有的结果,这是因为随机森林方法固然也有自己的局限性.在这篇文章中,我们将向你介绍运用随机森林构建预测模型时最令人感兴趣

10 行 Python 代码实现模糊查询/智能提示

10 行 Python 代码实现模糊查询/智能提示 1.导语: 模糊匹配可以算是现代编辑器(如 Eclipse 等各种 IDE)的一个必备特性了,它所做的就是根据用户输入的部分内容,猜测用户想要的文件名,并提供一个推荐列表供用户选择. 样例如下: Vim (Ctrl-P) Sublime Text (Cmd-P) '模糊匹配'这是一个极为有用的特性,同时也非常易于实现. 2.问题分析: 我们有一堆字符串(文件名)集合,我们根据用户的输入不断进行过滤,用户的输入可能是字符串的一部分.我们就以下面的

200行Python代码实现2048

200行Python代码实现2048 一.实验说明 1. 环境登录 无需密码自动登录,系统用户名shiyanlou 2. 环境介绍 本实验环境采用带桌面的Ubuntu Linux环境,实验中会用到桌面上的程序: LX终端(LXTerminal): Linux命令行终端,打开后会进入Bash环境,可以使用Linux命令 GVim:非常好用的编辑器,最简单的用法可以参考课程Vim编辑器 3. 环境使用 使用GVim编辑器输入实验所需的代码及文件,使用LX终端(LXTerminal)运行所需命令进行操

解决Python代码编码问题 SyntaxError: Non-UTF-8 code starting with &#39;\xc1&#39;

导致出错的根源就是编码问题. 解决方案是: 在程序最上面加上: view plai# coding=gbk 这样程序就可以正常运行了. 解决Python代码编码问题 SyntaxError: Non-UTF-8 code starting with '\xc1'

python代码风格指南:pep8 中文翻译

摘要 本文给出主Python版本标准库的编码约定.CPython的C代码风格参见?PEP7.本文和?PEP 257 文档字符串标准改编自Guido最初的<Python Style Guide>, 并增加了Barry的?GNU Mailman Coding Style Guide的部分内容.本文会随着语言改变等而改变.许多项目都有自己的编码风格指南,冲突时自己的指南为准. 本文给出主Python版本标准库的编码约定.CPython的C代码风格参见PEP7. 本文和PEP 257 文档字符串标准改

tf–idf算法解释及其python代码实现(下)

tf–idf算法python代码实现 这是我写的一个tf-idf的核心部分的代码,没有完整实现,当然剩下的事情就非常简单了,我们知道tfidf=tf*idf,所以可以分别计算tf和idf值在相乘,首先我们创建一个简单的语料库,作为例子,只有四句话,每句表示一个文档 copus=['我正在学习计算机','它正在吃饭','我的书还在你那儿','今天不上班'] 由于中文需要分词,jieba分词是python里面比较好用的分词工具,所以选用jieba分词,文末是jieba的链接.首先对文档进行分词: i

转:python代码缩进

习惯了java,c++之类的宽容,初学python,被它摆了道下马威,写if else,竟然必须要我正确用缩进格式,原来在python里不能用括号来表示语句块,也不能用开始/结束标志符来表示,而是靠缩进来表示,好吧,我以后多注意 空白在Python中是重要的.事实上行首的空白是重要的.它称为缩进.在逻辑行首的空白(空格和制表符)用来决定逻辑行的缩进层次,从而用来决定语句的分组.这意味着同一层次的语句必须有相同的缩进.每一组这样的语句称为一个块.我们将在后面的章节中看到有关块的用处的例子. 你需要

如何从python代码中直接访问Android的Service

在Kivy中,通过pyjnius扩展可以间接调用Java代码,而pyjnius利用的是Java的反射机制.但是在Python对象和Java对象中转来转去总让人感觉到十分别扭.好在android提供了binder这个进程间通信的功能,Java中的Service也是基于Binder的C++代码封装来实现进程间通信的,这也为从Python代码中绕开pyjnius直接访问Java代码提供了可能,既然Java的Service是基于C++的封装来实现的,也同样可以在Python中封装同样的C++代码,这篇文

将SCONS工具集成到Python代码中

SCONS是Python的自动智能结构化编译工具,将来或许能代替Make. 在Windows或者Linux下,SConstruct文件相当于MakeFile,使用SCONS编译,需输入scons.bat(scons),后面带上编译选项options(如SConstruct所在的路径).观察scons.bat这个脚本,可以在其他Python代码中,将SCONS工具当作一个函数来调用.这个函数就是Scons.Script.main(),这个main函数将解析命令行中的编译选项,通过sys.argv[