timeit — 测量小段代码的执行时间

源代码: Lib/timeit.py


此模块提供了一种简单的方法来测量小段 Python 代码的执行时间。它既有命令行界面,也有可调用接口。它避免了测量执行时间时常见的陷阱。另请参阅 Tim Peters 在 O'Reilly 出版的第二版 *Python Cookbook* 中“算法”章的引言。

基本示例

以下示例展示了如何使用命令行界面来比较三个不同的表达式

$ python -m timeit "'-'.join(str(n) for n in range(100))"
10000 loops, best of 5: 30.2 usec per loop
$ python -m timeit "'-'.join([str(n) for n in range(100)])"
10000 loops, best of 5: 27.5 usec per loop
$ python -m timeit "'-'.join(map(str, range(100)))"
10000 loops, best of 5: 23.2 usec per loop

这可以通过Python 接口实现,代码如下

>>> import timeit
>>> timeit.timeit('"-".join(str(n) for n in range(100))', number=10000)
0.3018611848820001
>>> timeit.timeit('"-".join([str(n) for n in range(100)])', number=10000)
0.2727368790656328
>>> timeit.timeit('"-".join(map(str, range(100)))', number=10000)
0.23702679807320237

也可以通过Python 接口传递可调用对象

>>> timeit.timeit(lambda: "-".join(map(str, range(100))), number=10000)
0.19665591977536678

但是请注意,只有在使用命令行界面时,timeit() 才会自动确定重复次数。在示例部分可以找到更高级的示例。

Python 接口

该模块定义了三个便利函数和一个公共类

timeit.timeit(stmt='pass', setup='pass', timer=<default timer>, number=1000000, globals=None)

创建一个带有给定语句、*setup* 代码和 *timer* 函数的 Timer 实例,并使用 *number* 次执行运行其 timeit() 方法。可选的 *globals* 参数指定执行代码的命名空间。

3.5 版本中有所改变: 添加了可选的 *globals* 参数。

timeit.repeat(stmt='pass', setup='pass', timer=<default timer>, repeat=5, number=1000000, globals=None)

创建一个带有给定语句、*setup* 代码和 *timer* 函数的 Timer 实例,并使用给定的 *repeat* 次数和 *number* 次执行运行其 repeat() 方法。可选的 *globals* 参数指定执行代码的命名空间。

3.5 版本中有所改变: 添加了可选的 *globals* 参数。

3.7 版本中有所改变: *repeat* 的默认值从 3 更改为 5。

timeit.default_timer()

默认计时器始终为 time.perf_counter(),返回浮点秒数。另一种选择 time.perf_counter_ns 返回整数纳秒。

3.3 版本中有所改变: time.perf_counter() 现在是默认计时器。

class timeit.Timer(stmt='pass', setup='pass', timer=<timer function>, globals=None)

用于测量小段代码执行速度的类。

构造函数接受一个要计时的语句、一个用于设置的附加语句和一个计时器函数。两个语句都默认为 'pass';计时器函数取决于平台(参见模块的文档字符串)。*stmt* 和 *setup* 也可以包含由 ; 或换行符分隔的多个语句,只要它们不包含多行字符串字面量。默认情况下,语句将在 timeit 的命名空间中执行;此行为可以通过将命名空间传递给 *globals* 来控制。

要测量第一个语句的执行时间,请使用 timeit() 方法。repeat()autorange() 方法是多次调用 timeit() 的便利方法。

*setup* 的执行时间不包括在整体计时执行运行中。

*stmt* 和 *setup* 参数也可以接受不带参数的可调用对象。这将把对它们的调用嵌入到一个计时器函数中,该函数随后将由 timeit() 执行。请注意,在这种情况下,由于额外的函数调用,计时开销会稍大一些。

3.5 版本中有所改变: 添加了可选的 *globals* 参数。

timeit(number=1000000)

对主语句进行 *number* 次执行计时。这将执行一次设置语句,然后返回执行主语句多次所需的时间。默认计时器以浮点数形式返回秒数。参数是循环次数,默认为一百万次。主语句、设置语句和要使用的计时器函数都传递给构造函数。

备注

默认情况下,timeit() 在计时期间会暂时关闭垃圾回收。这种方法的优点是使独立的计时更具可比性。缺点是垃圾回收可能是被测量函数性能的重要组成部分。如果是这样,可以在 *setup* 字符串中的第一个语句中重新启用垃圾回收。例如

timeit.Timer('for i in range(10): oct(i)', 'gc.enable()').timeit()
autorange(callback=None)

自动确定调用 timeit() 的次数。

这是一个便利函数,它重复调用 timeit(),直到总时间 >= 0.2 秒,返回最终的(循环次数,该循环次数所花费的时间)。它以序列 1、2、5、10、20、50、... 中递增的数字调用 timeit(),直到所花费的时间至少为 0.2 秒。

如果给定了 *callback* 且不为 None,则在每次尝试后,它将以两个参数被调用:callback(number, time_taken)

在 3.6 版本加入。

repeat(repeat=5, number=1000000)

多次调用 timeit()

这是一个便利函数,它重复调用 timeit(),返回一个结果列表。第一个参数指定调用 timeit() 的次数。第二个参数指定 timeit() 的 *number* 参数。

备注

从结果向量中计算平均值和标准差并报告它们是很诱人的。然而,这并不是很有用。在典型情况下,最低值给出了您的机器运行给定代码片段的速度下限;结果向量中较高的值通常不是由 Python 速度的可变性引起的,而是由其他进程干扰您的计时精度引起的。因此,结果的 min() 可能是您应该唯一感兴趣的数字。之后,您应该查看整个向量并应用常识而不是统计数据。

3.7 版本中有所改变: *repeat* 的默认值从 3 更改为 5。

print_exc(file=None)

打印计时代码中的回溯的辅助函数。

典型用法

t = Timer(...)       # outside the try/except
try:
    t.timeit(...)    # or t.repeat(...)
except Exception:
    t.print_exc()

与标准回溯相比,它的优点是会显示编译模板中的源代码行。可选的 *file* 参数指定回溯的发送位置;它默认为 sys.stderr

命令行接口

作为程序从命令行调用时,使用以下形式

python -m timeit [-n N] [-r N] [-u U] [-s S] [-p] [-v] [-h] [statement ...]

其中以下选项是可以理解的

-n N, --number=N

“语句”执行的次数

-r N, --repeat=N

计时器重复的次数(默认为 5)

-s S, --setup=S

初始执行一次的语句(默认为 pass

-p, --process

测量进程时间,而不是墙钟时间,使用 time.process_time() 而不是默认的 time.perf_counter()

在 3.3 版本加入。

-u, --unit=U

为计时器输出指定时间单位;可以选择 nsecusecmsecsec

在 3.5 版本加入。

-v, --verbose

打印原始计时结果;重复以获得更高精度

-h, --help

打印简短的使用信息并退出

多行语句可以通过将每行指定为单独的语句参数来给出;缩进行可以通过将参数用引号括起来并使用前导空格来实现。多个 -s 选项也以类似方式处理。

如果未给出 -n,则通过尝试序列 1、2、5、10、20、50、... 中递增的数字来计算合适的循环次数,直到总时间至少为 0.2 秒。

default_timer() 的测量可能会受到在同一机器上运行的其他程序的影响,因此当需要精确计时时,最好的做法是重复计时几次并使用最佳时间。-r 选项对此很有用;默认的 5 次重复在大多数情况下可能就足够了。您可以使用 time.process_time() 来测量 CPU 时间。

备注

执行 pass 语句存在一定的基线开销。这里的代码没有试图隐藏它,但您应该意识到这一点。基线开销可以通过不带参数调用程序来测量,并且在不同的 Python 版本之间可能有所不同。

示例

可以提供一个仅在开始时执行一次的 setup 语句

$ python -m timeit -s "text = 'sample string'; char = 'g'" "char in text"
5000000 loops, best of 5: 0.0877 usec per loop
$ python -m timeit -s "text = 'sample string'; char = 'g'" "text.find(char)"
1000000 loops, best of 5: 0.342 usec per loop

输出中有三个字段。循环计数,它告诉您每个计时循环重复中语句体运行了多少次。重复计数(“最佳 5 次”)告诉您计时循环重复了多少次,最后是语句体在计时循环的最佳重复中平均花费的时间。也就是说,最快重复所花费的时间除以循环计数。

>>> import timeit
>>> timeit.timeit('char in text', setup='text = "sample string"; char = "g"')
0.41440500499993504
>>> timeit.timeit('text.find(char)', setup='text = "sample string"; char = "g"')
1.7246671520006203

使用 Timer 类及其方法可以完成相同的操作

>>> import timeit
>>> t = timeit.Timer('char in text', setup='text = "sample string"; char = "g"')
>>> t.timeit()
0.3955516149999312
>>> t.repeat()
[0.40183617287970225, 0.37027556854118704, 0.38344867356679524, 0.3712595970846668, 0.37866875250654886]

以下示例展示了如何对包含多行的表达式进行计时。这里我们比较了使用 hasattr()try/except 来测试缺失和存在的对象属性的开销

$ python -m timeit "try:" "  str.__bool__" "except AttributeError:" "  pass"
20000 loops, best of 5: 15.7 usec per loop
$ python -m timeit "if hasattr(str, '__bool__'): pass"
50000 loops, best of 5: 4.26 usec per loop

$ python -m timeit "try:" "  int.__bool__" "except AttributeError:" "  pass"
200000 loops, best of 5: 1.43 usec per loop
$ python -m timeit "if hasattr(int, '__bool__'): pass"
100000 loops, best of 5: 2.23 usec per loop
>>> import timeit
>>> # attribute is missing
>>> s = """\
... try:
...     str.__bool__
... except AttributeError:
...     pass
... """
>>> timeit.timeit(stmt=s, number=100000)
0.9138244460009446
>>> s = "if hasattr(str, '__bool__'): pass"
>>> timeit.timeit(stmt=s, number=100000)
0.5829014980008651
>>>
>>> # attribute is present
>>> s = """\
... try:
...     int.__bool__
... except AttributeError:
...     pass
... """
>>> timeit.timeit(stmt=s, number=100000)
0.04215312199994514
>>> s = "if hasattr(int, '__bool__'): pass"
>>> timeit.timeit(stmt=s, number=100000)
0.08588060699912603

要使 timeit 模块能够访问您定义的函数,您可以传递一个包含导入语句的 *setup* 参数

def test():
    """Stupid test function"""
    L = [i for i in range(100)]

if __name__ == '__main__':
    import timeit
    print(timeit.timeit("test()", setup="from __main__ import test"))

另一种选择是将 globals() 传递给 *globals* 参数,这将使代码在您当前的全局命名空间中执行。这可能比单独指定导入更方便

def f(x):
    return x**2
def g(x):
    return x**4
def h(x):
    return x**8

import timeit
print(timeit.timeit('[func(42) for func in (f,g,h)]', globals=globals()))