diff --git a/Doc/library/timeit.rst b/Doc/library/timeit.rst index 8ca37034f79b7c..72a41987240c71 100644 --- a/Doc/library/timeit.rst +++ b/Doc/library/timeit.rst @@ -58,23 +58,29 @@ Python Interface The module defines three convenience functions and a public class: -.. function:: timeit(stmt='pass', setup='pass', timer=, number=1000000, globals=None) +.. function:: timeit(stmt='pass', setup='pass', timer=, number=1000000, globals=None, target_time=0.2) Create a :class:`Timer` instance with the given statement, *setup* code and *timer* function and run its :meth:`.timeit` method with *number* executions. The optional *globals* argument specifies a namespace in which to execute the - code. + code. If *number* is 0, :meth:`.autorange` method is executed, a convenience + function that calls :meth:`.timeit` repeatedly so that the total time >= *target_time* second. .. versionchanged:: 3.5 The optional *globals* parameter was added. + .. versionchanged:: 3.8 + The optional *target_time* parameter was added. -.. function:: repeat(stmt='pass', setup='pass', timer=, repeat=5, number=1000000, globals=None) + +.. function:: repeat(stmt='pass', setup='pass', timer=, repeat=5, number=1000000, globals=None, target_time=0.2) Create a :class:`Timer` instance with the given statement, *setup* code and *timer* function and run its :meth:`.repeat` method with the given *repeat* count and *number* executions. The optional *globals* argument specifies a - namespace in which to execute the code. + namespace in which to execute the code. If *number* is 0, :meth:`.autorange` + method is executed, a convenience function that calls :meth:`.timeit` repeatedly + so that the total time >= *target_time* second. .. versionchanged:: 3.5 The optional *globals* parameter was added. @@ -82,6 +88,9 @@ The module defines three convenience functions and a public class: .. versionchanged:: 3.7 Default value of *repeat* changed from 3 to 5. + .. versionchanged:: 3.8 + The optional *target_time* parameter was added. + .. function:: default_timer() The default timer, which is always :func:`time.perf_counter`. @@ -90,7 +99,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. @@ -116,6 +125,9 @@ 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) Time *number* executions of the main statement. This executes the setup @@ -137,15 +149,15 @@ 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=None) 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 >= *Timer.target_time* seconds, 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* seconds. If *callback* is given and is not ``None``, it will be called after each trial with two arguments: ``callback(number, time_taken)``. @@ -233,6 +245,12 @@ Where the following options are understood: .. versionadded:: 3.5 +.. cmdoption:: -t, --target_time=T + + calls :meth:`.timeit` repeatedly so that the total time >= *target_time* seconds + + .. versionadded:: 3.8 + .. cmdoption:: -v, --verbose print raw timing results; repeat for more digits precision diff --git a/Lib/test/test_timeit.py b/Lib/test/test_timeit.py index e02d4a71a9ba7c..c86692c4fc7ef0 100644 --- a/Lib/test/test_timeit.py +++ b/Lib/test/test_timeit.py @@ -107,8 +107,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 or 1) + self.assertEqual(delta_time, number or (1, 1.0)) # Takes too long to run in debug build. #def test_timeit_default_iters(self): @@ -139,7 +139,12 @@ 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_function_max_time_taken(self): + delta_time = timeit.timeit(self.fake_stmt, self.fake_setup, number=0, + timer=FakeTimer(), target_time=1) + self.assertEqual(delta_time, (1, 1.0)) def test_timeit_globals_args(self): global _global_timer @@ -152,9 +157,9 @@ def test_timeit_globals_args(self): timeit.timeit(stmt='local_timer.inc()', timer=local_timer, globals=locals(), number=3) - def repeat(self, stmt, setup, repeat=None, number=None): + def repeat(self, stmt, setup, repeat=None, number=None, target_time=0.5): self.fake_timer = FakeTimer() - t = timeit.Timer(stmt=stmt, setup=setup, timer=self.fake_timer) + t = timeit.Timer(stmt=stmt, setup=setup, timer=self.fake_timer, target_time=target_time) kwargs = {} if repeat is None: repeat = DEFAULT_REPEAT @@ -166,8 +171,8 @@ def repeat(self, stmt, setup, repeat=None, number=None): 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.count, (repeat * number) if number else repeat) + self.assertEqual(delta_times, repeat * [float(number) or (1, 1.0)]) # Takes too long to run in debug build. #def test_repeat_default(self): @@ -186,6 +191,10 @@ def test_repeat_callable_stmt(self): self.repeat(self.fake_callable_stmt, self.fake_setup, repeat=3, number=5) + def test_repeat_callable_max_time_taken(self): + self.repeat(self.fake_callable_stmt, self.fake_setup, + repeat=3, number=5, target_time=1) + def test_repeat_callable_setup(self): self.repeat(self.fake_stmt, self.fake_callable_setup, repeat=3, number=5) @@ -208,7 +217,7 @@ 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, DEFAULT_REPEAT * [(1, 1.0)]) def assert_exc_string(self, exc_string, expected_exc_name): exc_lines = exc_string.splitlines() diff --git a/Lib/timeit.py b/Lib/timeit.py index c0362bcc5f3e24..e02ca859a43b9a 100755 --- a/Lib/timeit.py +++ b/Lib/timeit.py @@ -19,6 +19,8 @@ -p/--process: use time.process_time() (default is time.perf_counter()) -v/--verbose: print raw timing results; repeat for more digits precision -u/--unit: set the output time unit (nsec, usec, msec, or sec) + -t/--target_time: if number is 0 the code will run until it + takes *at least* ``target_time`` seconds -h/--help: print this usage message and exit --: separate options from statement, use when statement starts with - statement: statement to be timed (default 'pass') @@ -59,6 +61,7 @@ default_number = 1000000 default_repeat = 5 default_timer = time.perf_counter +default_target_time = 0.2 _globals = globals @@ -75,10 +78,12 @@ def inner(_it, _timer{init}): return _t1 - _t0 """ + def reindent(src, indent): """Helper to reindent a multi-line statement.""" return src.replace("\n", "\n" + " "*indent) + class Timer: """Class for timing execution speed of small code snippets. @@ -98,9 +103,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 +175,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 not number: + return self.autorange() it = itertools.repeat(None, number) gcold = gc.isenabled() gc.disable() @@ -205,16 +213,19 @@ def repeat(self, repeat=default_repeat, number=default_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 >= target_time (default is 0.2 seconds). 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 - second. Returns (number, time_taken). + 1, 2, 5, 10, 20, 50, ... until the max_time_taken is reached. + Returns (number, time_taken). 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,19 +233,24 @@ 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 + def timeit(stmt="pass", setup="pass", timer=default_timer, - number=default_number, globals=None): + number=default_number, globals=None, + target_time=default_target_time): """Convenience function to create Timer object and call timeit method.""" - return Timer(stmt, setup, timer, globals).timeit(number) + return Timer(stmt, setup, timer, globals, target_time).timeit(number) + def repeat(stmt="pass", setup="pass", timer=default_timer, - repeat=default_repeat, number=default_number, globals=None): + repeat=default_repeat, number=default_number, + globals=None, target_time=default_target_time): """Convenience function to create Timer object and call repeat method.""" - return Timer(stmt, setup, timer, globals).repeat(repeat, number) + return Timer(stmt, setup, timer, globals, target_time).repeat(repeat, number) + def main(args=None, *, _wrap_timer=None): """Main program, used when run as a script. @@ -259,7 +275,7 @@ def main(args=None, *, _wrap_timer=None): try: opts, args = getopt.getopt(args, "n:u:s:r:tcpvh", ["number=", "setup=", "repeat=", - "time", "clock", "process", + "time", "clock", "process", "max_time_taken=" "verbose", "unit=", "help"]) except getopt.error as err: print(err) @@ -269,6 +285,7 @@ def main(args=None, *, _wrap_timer=None): timer = default_timer stmt = "\n".join(args) or "pass" number = 0 # auto-determine + target_time = default_target_time setup = [] repeat = default_repeat verbose = 0 @@ -293,6 +310,8 @@ def main(args=None, *, _wrap_timer=None): repeat = 1 if o in ("-p", "--process"): timer = time.process_time + if o in ("-t", "--target_time"): + target_time = a if o in ("-v", "--verbose"): if verbose: precision += 1 @@ -310,7 +329,7 @@ def main(args=None, *, _wrap_timer=None): if _wrap_timer is not None: timer = _wrap_timer(timer) - t = Timer(stmt, setup, timer) + t = Timer(stmt, setup, timer, target_time=target_time) if number == 0: # determine number so that 0.2 <= total time < 2.0 callback = None @@ -321,7 +340,7 @@ def callback(number, time_taken): print(msg.format(num=number, s='s' if plural else '', secs=time_taken, prec=precision)) try: - number, _ = t.autorange(callback) + number, _ = t.autorange(callback, target_time) except: t.print_exc() return 1 @@ -370,5 +389,6 @@ def format_time(dt): UserWarning, '', 0) return None + if __name__ == "__main__": sys.exit(main()) diff --git a/Misc/NEWS.d/next/Library/2019-04-25-21-11-37.bpo-36461.TO5YyP.rst b/Misc/NEWS.d/next/Library/2019-04-25-21-11-37.bpo-36461.TO5YyP.rst new file mode 100644 index 00000000000000..41422d6e7fa315 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2019-04-25-21-11-37.bpo-36461.TO5YyP.rst @@ -0,0 +1,4 @@ +In timeit module: +- added a new parameter *target_time* to ``timeit.timeit()`` and ``timeit.replace()`` methods and ``timeit.Timer()`` class; +- had `timeit` and `repeat` methods (and functions) fall back + on `autorange` if the number is set to 0 or None.