timeit — 测量小代码片段的执行时间 — Python 文档

来自菜鸟教程
Python/docs/3.9/library/timeit
跳转至:导航、​搜索

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

源代码: :source:`Lib/timeit.py`



该模块提供了一种简单的方法来计时一小段 Python 代码。 它既有 命令行接口 ,也有 可调用 接口。 它避免了许多用于测量执行时间的常见陷阱。 另请参阅 O'Reilly 出版的 Python Cookbook 第二版中 Tim Peters 对“算法”章节的介绍。

基本示例

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

$ 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 实例,并使用 编号运行其 timeit() 方法 处决。 可选的 globals 参数指定执行代码的命名空间。

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

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

使用给定的语句、setup 代码和 timer 函数创建一个 Timer 实例,并使用给定的 运行其 repeat() 方法]repeat 计数和 number 次执行。 可选的 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 语句,然后返回多次执行主语句所需的时间,以秒为单位作为浮点数。 参数是循环的次数,默认为一百万。 main 语句、setup 语句和要使用的定时器函数被传递给构造函数。

笔记

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

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

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