diff --git a/Doc/library/timeit.rst b/Doc/library/timeit.rst index 548a3ee0540506..d818357ea3a7d4 100644 --- a/Doc/library/timeit.rst +++ b/Doc/library/timeit.rst @@ -62,23 +62,30 @@ 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:: next + 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, the :meth:`autorange` + method is executed, and a convenience function calls :meth:`timeit` + repeatedly so that the total time >= *target_time* seconds. .. versionchanged:: 3.5 The optional *globals* parameter was added. @@ -86,6 +93,8 @@ The module defines three convenience functions and a public class: .. versionchanged:: 3.7 Default value of *repeat* changed from 3 to 5. + .. versionchanged:: next + The optional *target_time* parameter was added. .. function:: default_timer() @@ -96,7 +105,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. @@ -122,6 +131,9 @@ The module defines three convenience functions and a public class: .. versionchanged:: 3.5 The optional *globals* parameter was added. + .. versionchanged:: next + The optional *target_time* parameter was added. + .. method:: Timer.timeit(number=1000000) Time *number* executions of the main statement. This executes the setup @@ -143,15 +155,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 seconds. + 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)``. @@ -239,6 +251,12 @@ Where the following options are understood: .. versionadded:: 3.5 +.. option:: -t, --target-time=T + + calls :meth:`.timeit` repeatedly so that the total time >= *target_time* seconds + + .. versionadded:: next + .. option:: -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 f8bc306b455a5d..ec1a0994cbc60b 100644 --- a/Lib/test/test_timeit.py +++ b/Lib/test/test_timeit.py @@ -117,8 +117,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): @@ -149,7 +149,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_target_time(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 @@ -162,9 +167,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 @@ -176,8 +181,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): @@ -196,6 +201,10 @@ def test_repeat_callable_stmt(self): self.repeat(self.fake_callable_stmt, self.fake_setup, repeat=3, number=5) + def test_repeat_callable_target_time(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) @@ -218,7 +227,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 80791acdeca23f..c935c42d44432b 100644 --- a/Lib/timeit.py +++ b/Lib/timeit.py @@ -17,6 +17,9 @@ -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 T: if --number is 0 the code will run until it + takes *at least* this many seconds + (default: 0.2) -h/--help: print this usage message and exit --: separate options from statement, use when statement starts with - statement: statement to be timed (default 'pass') @@ -28,7 +31,7 @@ If -n is not given, a suitable number of loops is calculated by trying increasing numbers from the sequence 1, 2, 5, 10, 20, 50, ... until the -total time is at least 0.2 seconds. +total time is at least --target-time seconds. Note: there is a certain baseline overhead associated with executing a pass statement. It differs between versions. The code here doesn't try @@ -57,6 +60,7 @@ default_number = 1000000 default_repeat = 5 default_timer = time.perf_counter +default_target_time = 0.2 _globals = globals @@ -99,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 = '' @@ -176,6 +181,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() @@ -212,16 +219,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 target_time 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: @@ -229,21 +239,23 @@ 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): @@ -270,9 +282,10 @@ def main(args=None, *, _wrap_timer=None): colorize = _colorize.can_colorize() try: - opts, args = getopt.getopt(args, "n:u:s:r:pvh", + opts, args = getopt.getopt(args, "n:u:s:r:pt:vh", ["number=", "setup=", "repeat=", - "process", "verbose", "unit=", "help"]) + "process", "target-time=", + "verbose", "unit=", "help"]) except getopt.error as err: print(err) print("use -h/--help for command line help") @@ -281,6 +294,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 @@ -305,6 +319,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 = float(a) if o in ("-v", "--verbose"): if verbose: precision += 1 @@ -322,9 +338,9 @@ 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 + # determine number so that total time >= target_time callback = None if verbose: def callback(number, time_taken): @@ -333,7 +349,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(colorize=colorize) return 1 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..57921ca0c50c1c --- /dev/null +++ b/Misc/NEWS.d/next/Library/2019-04-25-21-11-37.bpo-36461.TO5YyP.rst @@ -0,0 +1,5 @@ +:mod:`timeit`: +- added a new parameter *target_time* to :func:`timeit.timeit` and + :func:`timeit.repeat` methods and :class:`timeit.Timer` class; +- had ``timeit`` and ``repeat`` methods (and functions) fall back + on ``autorange`` if the number is set to 0 or None.