diff --git a/Doc/library/timeit.rst b/Doc/library/timeit.rst index ef7a4e40be6590..26a9b4ffde4ff3 100644 --- a/Doc/library/timeit.rst +++ b/Doc/library/timeit.rst @@ -94,7 +94,7 @@ The module defines three convenience functions and a public class: :func:`time.perf_counter` is now the default timer. -.. class:: Timer(stmt='pass', setup='pass', timer=, globals=None) +.. class:: Timer(stmt='pass', setup='pass', timer=, globals=None, target_time=0.2) Class for timing execution speed of small code snippets. @@ -119,6 +119,8 @@ The module defines three convenience functions and a public class: .. versionchanged:: 3.5 The optional *globals* parameter was added. + .. versionchanged:: 3.8 + The optional *target_time* parameter was added. .. method:: Timer.timeit(number=1000000) @@ -141,20 +143,22 @@ The module defines three convenience functions and a public class: timeit.Timer('for i in range(10): oct(i)', 'gc.enable()').timeit() - .. method:: Timer.autorange(callback=None) + .. method:: Timer.autorange(callback=None, target_time=0.2) Automatically determine how many times to call :meth:`.timeit`. This is a convenience function that calls :meth:`.timeit` repeatedly - so that the total time >= 0.2 second, returning the eventual + so that the total time is greater than or equal to *target_time*, returning the eventual (number of loops, time taken for that number of loops). It calls :meth:`.timeit` with increasing numbers from the sequence 1, 2, 5, - 10, 20, 50, ... until the time taken is at least 0.2 second. + 10, 20, 50, ... until the time taken is at least *target_time*. If *callback* is given and is not ``None``, it will be called after each trial with two arguments: ``callback(number, time_taken)``. .. versionadded:: 3.6 + .. versionchanged:: 3.8 + The optional *target_time* parameter was added. .. method:: Timer.repeat(repeat=5, number=1000000) diff --git a/Lib/test/test_timeit.py b/Lib/test/test_timeit.py index e02d4a71a9ba7c..65992fd6f70e6a 100644 --- a/Lib/test/test_timeit.py +++ b/Lib/test/test_timeit.py @@ -10,6 +10,9 @@ # timeit's default number of iterations. DEFAULT_NUMBER = 1000000 +# timeit's default target time. +DEFAULT_TARGET_TIME = 0.2 + # timeit's default number of repetitions. DEFAULT_REPEAT = 5 @@ -107,8 +110,8 @@ def timeit(self, stmt, setup, number=None, globals=None): kwargs['number'] = number delta_time = t.timeit(**kwargs) self.assertEqual(self.fake_timer.setup_calls, 1) - self.assertEqual(self.fake_timer.count, number) - self.assertEqual(delta_time, number) + self.assertEqual(self.fake_timer.count, number if number else 1) + self.assertEqual(delta_time, number if number else (1, 1.0)) # Takes too long to run in debug build. #def test_timeit_default_iters(self): @@ -139,7 +142,7 @@ def test_timeit_callable_stmt_and_setup(self): def test_timeit_function_zero_iters(self): delta_time = timeit.timeit(self.fake_stmt, self.fake_setup, number=0, timer=FakeTimer()) - self.assertEqual(delta_time, 0) + self.assertEqual(delta_time, (1, 1.0)) def test_timeit_globals_args(self): global _global_timer @@ -165,9 +168,13 @@ def repeat(self, stmt, setup, repeat=None, number=None): else: kwargs['number'] = number delta_times = t.repeat(**kwargs) - self.assertEqual(self.fake_timer.setup_calls, repeat) - self.assertEqual(self.fake_timer.count, repeat * number) - self.assertEqual(delta_times, repeat * [float(number)]) + self.assertEqual(self.fake_timer.setup_calls, repeat if number > 0 + else 1) + # self.assertEqual(self.fake_timer.setup_calls, ) + self.assertEqual(self.fake_timer.count, + (repeat * number) if number else 1) + self.assertEqual(delta_times, + repeat * [float(number)] if number else (1, 1.0)) # Takes too long to run in debug build. #def test_repeat_default(self): @@ -179,6 +186,9 @@ def test_repeat_zero_reps(self): def test_repeat_zero_iters(self): self.repeat(self.fake_stmt, self.fake_setup, number=0) + def test_repeat_with_iters(self): + self.repeat(self.fake_stmt, self.fake_setup, number=1) + def test_repeat_few_reps_and_iters(self): self.repeat(self.fake_stmt, self.fake_setup, repeat=3, number=5) @@ -208,7 +218,12 @@ def test_repeat_function_zero_reps(self): def test_repeat_function_zero_iters(self): delta_times = timeit.repeat(self.fake_stmt, self.fake_setup, number=0, timer=FakeTimer()) - self.assertEqual(delta_times, DEFAULT_REPEAT * [0.0]) + self.assertEqual(delta_times, (1, 1.0)) + + def test_repeat_function_with_iters(self): + delta_times = timeit.repeat(self.fake_stmt, self.fake_setup, number=1, + timer=FakeTimer()) + self.assertEqual(delta_times, [1.0, 1.0, 1.0, 1.0, 1.0]) def assert_exc_string(self, exc_string, expected_exc_name): exc_lines = exc_string.splitlines() @@ -354,38 +369,66 @@ def test_main_exception_fixed_reps(self): s = self.run_main(switches=['-n1', '1/0']) self.assert_exc_string(error_stringio.getvalue(), 'ZeroDivisionError') - def autorange(self, seconds_per_increment=1/1024, callback=None): + def autorange(self, seconds_per_increment=1/1024, callback=None, + target_time=0.2): timer = FakeTimer(seconds_per_increment=seconds_per_increment) t = timeit.Timer(stmt=self.fake_stmt, setup=self.fake_setup, timer=timer) - return t.autorange(callback) + return t.autorange(callback, target_time) + + def autorange_with_callback(self, loop_count, expected_output, + target_time=0.2): + def callback(a, b): + print("{} {:.3f}".format(a, b)) + with captured_stdout() as s: + num_loops, time_taken = self.autorange(callback=callback, + target_time=target_time) + self.assertEqual(num_loops, loop_count) + self.assertEqual(time_taken, loop_count/1024) + self.assertEqual(s.getvalue(), expected_output) def test_autorange(self): num_loops, time_taken = self.autorange() self.assertEqual(num_loops, 500) self.assertEqual(time_taken, 500/1024) + def test_autorange_with_target_time(self): + num_loops, time_taken = self.autorange(target_time=0.6) + self.assertEqual(num_loops, 1000) + self.assertEqual(time_taken, 1000/1024) + def test_autorange_second(self): num_loops, time_taken = self.autorange(seconds_per_increment=1.0) self.assertEqual(num_loops, 1) self.assertEqual(time_taken, 1.0) def test_autorange_with_callback(self): - def callback(a, b): - print("{} {:.3f}".format(a, b)) - with captured_stdout() as s: - num_loops, time_taken = self.autorange(callback=callback) - self.assertEqual(num_loops, 500) - self.assertEqual(time_taken, 500/1024) - expected = ('1 0.001\n' - '2 0.002\n' - '5 0.005\n' - '10 0.010\n' - '20 0.020\n' - '50 0.049\n' - '100 0.098\n' - '200 0.195\n' - '500 0.488\n') - self.assertEqual(s.getvalue(), expected) + expected = dedent('''\ + 1 0.001 + 2 0.002 + 5 0.005 + 10 0.010 + 20 0.020 + 50 0.049 + 100 0.098 + 200 0.195 + 500 0.488 + ''') + self.autorange_with_callback(500, expected) + + def test_autorange_with_callback_and_target_time(self): + expected = dedent('''\ + 1 0.001 + 2 0.002 + 5 0.005 + 10 0.010 + 20 0.020 + 50 0.049 + 100 0.098 + 200 0.195 + 500 0.488 + 1000 0.977 + ''') + self.autorange_with_callback(1000, expected, target_time=0.6) if __name__ == '__main__': diff --git a/Lib/timeit.py b/Lib/timeit.py index c0362bcc5f3e24..955f0c3a9bf62c 100755 --- a/Lib/timeit.py +++ b/Lib/timeit.py @@ -59,6 +59,7 @@ default_number = 1000000 default_repeat = 5 default_timer = time.perf_counter +default_target_time = 0.2 _globals = globals @@ -98,9 +99,10 @@ class Timer: """ def __init__(self, stmt="pass", setup="pass", timer=default_timer, - globals=None): + globals=None, target_time=default_target_time): """Constructor. See class doc string.""" self.timer = timer + self.target_time = target_time local_ns = {} global_ns = _globals() if globals is None else globals init = '' @@ -169,6 +171,8 @@ def timeit(self, number=default_number): to one million. The main statement, the setup statement and the timer function to be used are passed to the constructor. """ + if number == 0 or number is None: + return self.autorange(target_time=self.target_time) it = itertools.repeat(None, number) gcold = gc.isenabled() gc.disable() @@ -199,14 +203,17 @@ def repeat(self, repeat=default_repeat, number=default_number): interested in. After that, you should look at the entire vector and apply common sense rather than statistics. """ + if number == 0 or number is None: + return self.autorange(target_time=self.target_time) r = [] for i in range(repeat): t = self.timeit(number) r.append(t) return r - def autorange(self, callback=None): - """Return the number of loops and time taken so that total time >= 0.2. + def autorange(self, callback=None, target_time=None): + """Return the number of loops and time taken so that total time + is greater than or equal to *target_time*. Calls the timeit method with increasing numbers from the sequence 1, 2, 5, 10, 20, 50, ... until the time taken is at least 0.2 @@ -215,6 +222,8 @@ def autorange(self, callback=None): If *callback* is given and is not None, it will be called after each trial with two arguments: ``callback(number, time_taken)``. """ + if target_time is None: + target_time = self.target_time i = 1 while True: for j in 1, 2, 5: @@ -222,7 +231,7 @@ def autorange(self, callback=None): time_taken = self.timeit(number) if callback: callback(number, time_taken) - if time_taken >= 0.2: + if time_taken >= target_time: return (number, time_taken) i *= 10 diff --git a/Misc/ACKS b/Misc/ACKS index 06e288dfcb2f18..6fea3a5f1e4471 100644 --- a/Misc/ACKS +++ b/Misc/ACKS @@ -55,6 +55,7 @@ Juancarlo Añez Chris Angelico Jérémy Anger Jon Anglin +Michele Angrisano Ankur Ankan Heidi Annexstad Ramchandra Apte diff --git a/Misc/NEWS.d/next/Library/2019-04-25-19-15-11.bpo-36461.BRX4xA.rst b/Misc/NEWS.d/next/Library/2019-04-25-19-15-11.bpo-36461.BRX4xA.rst new file mode 100644 index 00000000000000..9b738f77d8aca3 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2019-04-25-19-15-11.bpo-36461.BRX4xA.rst @@ -0,0 +1,2 @@ +A new parameter *total_time* was added to the ``timeit.autorange()`` +function.