17.10. timeit — 计算小段 Python 代码的运行时间

目的:测量一小段 Python 代码的执行时间

timeit 模块提供了一个简单的接口来测量一小段 Python 代码的执行时间。它使用平台相关的时间函数来提供尽可能精确的时间计算,并通过反复执行代码来减少启动或关闭的开销对时间计算的影响。

模块内容

timeit 定义了一个公共类 TimerTimer 的构造函数接受要计时的语句和「设置」语句(例如,用于初始化变量)。Python 语句是字符串并且可以包含换行。

timeit() 方法首先执行「设置」语句一次,然后重复执行被测语句并返回使用的总时间。传递给 timeit() 的参数控制语句执行的次数,默认值是 1,000,000。

基本示例

为了说明如何使用 Timer 的各种参数,这里有一个简单的示例,在执行每个语句时打印一个标识值。

timeit_example.py

import timeit

# using setitem
t = timeit.Timer("print('main statement')", "print('setup')")

print('TIMEIT:')
print(t.timeit(2))

print('REPEAT:')
print(t.repeat(3, 2))

当运行时,输出对 print() 重复调用的结果。

$ python3 timeit_example.py

TIMEIT:
setup
main statement
main statement
3.7070130929350853e-06
REPEAT:
setup
main statement
main statement
setup
main statement
main statement
setup
main statement
main statement
[1.4499528333544731e-06, 1.1939555406570435e-06,
1.1870870366692543e-06]

timeit() 执行设置语句一次,然后重复调用主语句 count 次。它返回一个浮点数,表示运行主语句所花费的总时间。

当调用 repeat() 时,它多次调用 timeit() (示例中是3次),以列表形式返回所有调用的结果。

在字典中存储数据

这个更复杂的示例比较使用不同的方法向字典中填充大量数据所需的时间。首先, 需要一些常量来配置 Timer
setup_statement 变量初始化包含字符串和整数的元组列表,主语句将使用这些字符串和整数来填充字典,使用字符串作为键,将整数存储为值。

# 一些常量
range_size = 1000
count = 1000
setup_statement = ';'.join([
    "l = [(str(x), x) for x in range(1000)]",
    "d = {}",
])

定义一个函数 show_results(),格式化并打印输出结果。timeit() 方法返回重复执行主语句所需的时间。show_results() 将其转化为每次迭代所需的时间,然后进一步缩减为在字典中插入一条数据所需的平均时间。

def show_results(result):
    "以微秒为单位打印每轮测试的时间和插入一条数据的时间"
    global count, range_size
    per_pass = 1000000 * (result / count)
    print('{:6.2f} usec/pass'.format(per_pass), end=' ')
    per_item = per_pass / range_size
    print('{:6.2f} usec/item'.format(per_item))

print("{} items".format(range_size))
print("{} iterations".format(count))
print()

为了建立测试基准,第一个测试使用 __setitem__()。后续的其他测试都要避免覆盖字典中已经存在的值,所以这个简单的版本应该是速度最快的。

Timer 的第一个参数是一个多行字符串,保留空白,以确保能够正确解析执行。第二个参数是一个常量,用来初始化值列表和字典。

# 使用 __setitem__ 不检测字典中是否已存在指定的键
print('__setitem__:', end=' ')
t = timeit.Timer(
    textwrap.dedent(
        """
        for s, i in l:
            d[s] = i
        """),
    setup_statement,
)
show_results(t.timeit(number=count))

下一个测试使用 setdefault(),确保字典中已经存在的值不会被覆盖。

# 使用 setdefault
print('setdefault :', end=' ')
t = timeit.Timer(
    textwrap.dedent(
        """
        for s, i in l:
            d.setdefault(s, i)
        """),
    setup_statement,
)
show_results(t.timeit(number=count))

下面这个方法只有在查找现有值时引发 KeyError 异常时,才向字典中添加该值。

# 使用 exceptions
print('KeyError   :', end=' ')
t = timeit.Timer(
    textwrap.dedent(
        """
        for s, i in l:
            try:
                existing = d[s]
            except KeyError:
                d[s] = i
        """),
    setup_statement,
)
show_results(t.timeit(number=count))

最后一个方法使用 in 操作确定字典中是否存在指定的键。

# 使用 "in"
print('"not in"   :', end=' ')
t = timeit.Timer(
    textwrap.dedent(
        """
        for s, i in l:
            if s not in d:
                d[s] = i
        """),
    setup_statement,
)
show_results(t.timeit(number=count))

运行时,脚本产生以下输出。

$ python3 timeit_dictionary.py

1000 items
1000 iterations

__setitem__:  91.79 usec/pass   0.09 usec/item
setdefault : 182.85 usec/pass   0.18 usec/item
KeyError   :  80.87 usec/pass   0.08 usec/item
"not in"   :  66.77 usec/pass   0.07 usec/item

这些时间是在一台 MacMini 上运行测试时上给出的,并将根据所使用的硬件和系统上运行的其他程序而有所不同。range_sizecount 的不同大小,以及不同的组合,将产生不同的结果。

从命令行进行计时

程序接口方面,timeit 提供了命令行中可用的模块测试接口,无需额外的测试程序就能测试模块。

要使用这个模块,需要在启动 Python 解释器时使用 -m 参数,这样解释器就会找到模块,并将其作为主程序执行。

$ python3 -m timeit

举个例子,输出模块的帮助信息:

$ python3 -m timeit -h

Tool for measuring execution time of small code snippets.

This module avoids a number of common traps for measuring execution
times.  See also Tim Peters' introduction to the Algorithms chapter in
the Python Cookbook, published by O'Reilly.

...

Timer 类相比,命令行中的 statement 参数有一点点差异。传递参数时,不需要将参数都放在一个长字符串中,只需要将每一行指令作为命令行参数中单独的一行。如果有循环,需要表示缩进时,则将表示缩进的空格与指令放在双引号中。

$ python3 -m timeit -s\
"d={}"\
"for i in range(1000):"\
"  d[str(i)] = i"

1000 loops, best of 3: 306 usec per loop

也可以把更复杂的代码定义成函数,然后在命令行中调用。

timeit_setitem.py


def test_setitem(range_size=1000):
    l = [(str(x), x) for x in range(range_size)]
    d = {}
    for s, i in l:
        d[s] = i

要测试以上函数,只需将导入模块以及运行函数的语句作为参数。

$ python3 -m timeit\
"import timeit_setitem; timeit_setitem.test_setitem()"

1000 loops, best of 3: 401 usec per loop

参见

本文章首发在 LearnKu.com 网站上。
上一篇 下一篇
讨论数量: 0
发起讨论 只看当前版本


暂无话题~