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