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
Python 接口¶
该模块定义了三个便捷函数和一个公共类
- timeit.timeit(stmt='pass', setup='pass', timer=<default timer>, number=1000000, globals=None)¶
使用给定的语句、setup 代码和 timer 函数创建一个
Timer
实例,并运行其timeit()
方法,执行 number 次。可选的 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(),返回浮点秒数。另一个选择 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 次的时间。这会执行一次设置语句,然后返回执行主语句多次所花费的时间。默认计时器以浮点数形式返回秒数。参数是循环的次数,默认为一百万次。要使用的主语句、设置语句和计时器函数将传递给构造函数。
- 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¶
执行 ‘statement’ 的次数
- -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
在输出中,有三个字段。循环计数,告诉你每次计时循环重复时语句主体运行的次数。重复计数(“best of 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
模块访问你定义的函数,你可以传递一个包含 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* 参数,这将导致代码在当前全局命名空间中执行。这可能比单独指定导入更方便
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()))