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

源码: Lib/timeit.py


此模块提供了一种简单的方法来计算一小段 Python 代码的耗时。 它有 命令行接口 以及一个 可调用 方法。 它避免了许多测量时间的常见陷阱。 另见 Tim Peter 在 O'Reilly 出版的 Python Cookbook 第二版中“算法”章节的概述。

基本示例

以下示例显示了如何使用 命令行接口 来比较三个不同的表达式:

$ python3 -m timeit '"-".join(str(n) for n in range(100))'
10000 loops, best of 5: 30.2 usec per loop
$ python3 -m timeit '"-".join([str(n) for n in range(100)])'
10000 loops, best of 5: 27.5 usec per loop
$ python3 -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()

在 3.3 版更改: time.perf_counter() 现在是默认计时器。

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

用于小代码片段的计数执行速度的类。

构造函数接受一个将计时的语句、一个用于设置的附加语句和一个定时器函数。两个语句都默认为 'pass' ;计时器函数与平台有关(请参阅模块文档字符串)。 stmtsetup 也可能包含多个以 ; 或换行符分隔的语句,只要它们不包含多行字符串文字即可。该语句默认在 timeit 的命名空间内执行;可以通过将命名空间传递给 globals 来控制此行为。

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

setup 的执行时间从总体计时执行中排除。

stmtsetup 参数也可以使用不带参数的可调用对象。这将在一个计时器函数中嵌入对它们的调用,然后由 timeit() 执行。请注意,由于额外的函数调用,在这种情况下,计时开销会略大一些。

在 3.5 版更改: 添加可选参数 globals

timeit(number=1000000)

执行 number 次主要语句。这将执行一次 setup 语句,然后返回执行主语句多次所需的时间,以秒为单位测量为浮点数。参数是通过循环的次数,默认为一百万。要使用的主语句、 setup 语句和 timer 函数将传递给构造函数。

注解

默认情况下, timeit() 暂时关闭 garbage collection 。这种方法的优点在于它使独立时序更具可比性。缺点是GC可能是所测量功能性能的重要组成部分。如果是这样,可以在 setup 字符串中的第一个语句重新启用GC。例如:

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

自动决定调用多少次 timeit()

这是一个便利函数,它反复调用 timeit() ,以便总时间 >= 0.2 秒,返回最终(循环次数,循环所用的时间)。它调用 timeit() 的次数以序列 1, 2, 5, 10, 20, 50, ... 递增,直到所用的时间至少为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] [-h] [statement ...]

如果了解以下选项:

-n N, --number=N

执行 '语句' 多少次

-r N, --repeat=N

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

-s S, --setup=S

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

-p, --process

测量进程时间,而不是 wallclock 时间,使用 time.process_time() 而不是 time.perf_counter() ,这是默认值

3.3 新版功能.

-u, --unit=U

指定定时器输出的时间单位;可以选择 nsec,usec,msec或sec

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

In the output, there are three fields. The loop count, which tells you how many times the statement body was run per timing loop repetition. The repetition count ('best of 5') which tells you how many times the timing loop was repeated, and finally the time the statement body took on average within the best repetition of the timing loop. That is, the time the fastest repetition took divided by the loop count.

>>> 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 模块访问你定义的函数,你可以传递一个包含 import 语句的 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 参数,这将导致代码在当前的全局命名空间中执行。这比单独指定 import 更方便

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()))