19

I played with timeit in Python, got a weird problem.

I define a simple function add. timeit works when I pass add two string parameters. But it raises ValueError: stmt is neither a string nor callable when I pass add two int parameters.

>>> import timeit
>>> def add(x,y):
...     return x + y
... 


>>> a = '1'
>>> b = '2'
>>> timeit.timeit(add(a,b))
0.01355926995165646


>>> a = 1
>>> b = 2
>>> timeit.timeit(add(a,b))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/anaconda/lib/python3.6/timeit.py", line 233, in timeit
    return Timer(stmt, setup, timer, globals).timeit(number)
  File "/anaconda/lib/python3.6/timeit.py", line 130, in __init__
    raise ValueError("stmt is neither a string nor callable")
ValueError: stmt is neither a string nor callable

Why does the parameter type matter at all here?

wim
  • 338,267
  • 99
  • 616
  • 750
liang li
  • 253
  • 1
  • 2
  • 9

6 Answers6

28

Your mistake is to assume that Python passes the expression add(a, b) to timeit(). That's not the case, add(a, b) is not a string, it is an expression so Python instead executes add(a, b) and the result of that call is passed to the timeit() call.

So for add('1', '2') the result is '12', a string. Passing a string to timeit() is fine. But add(1, 2) is 3, an integer. timeit(3) gives you an exception. Not that timing '12' is all that interesting, of course, but that is a valid Python expression producing the integer value 12:

>>> import timeit
>>> def add(x, y):
...     return x + y
...
>>> a = '1'
>>> b = '2'
>>> add(a, b)
'12'
>>> timeit.timeit('12')
0.009553937998134643
>>> a = 1
>>> b = 2
>>> add(a, b)
3
>>> timeit.timeit(3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/.../lib/python3.7/timeit.py", line 232, in timeit
    return Timer(stmt, setup, timer, globals).timeit(number)
  File "/.../lib/python3.7/timeit.py", line 128, in __init__
    raise ValueError("stmt is neither a string nor callable")
ValueError: stmt is neither a string nor callable

That's all perfectly normal; otherwise, how could you ever pass the result of a function to another function directly? timeit.timeit() is just another Python function, nothing so special that it'll disable the normal evaluation of expressions.

What you want is to pass a string with the expression to timeit(). timeit() doesn't have access to your add() function, or a or b, so you need to give it access with the second argument, the setup string. You can use from __main__ import add, a, b to import the add function object:

timeit.timeit('add(a, b)', 'from __main__ import add, a, b')

Now you get more meaningful results:

>>> import timeit
>>> def add(x, y):
...     return x + y
...
>>> a = '1'
>>> b = '2'
>>> timeit.timeit('add(a, b)', 'from __main__ import add, a, b')
0.16069997000158764
>>> a = 1
>>> b = 2
>>> timeit.timeit('add(a, b)', 'from __main__ import add, a, b')
0.10841095799696632

So adding integers is faster than adding strings. You probably want to try this with different sizes of integers and strings, but adding integers will remain the faster result.

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
20

With the string version add returns a string, which timeit can evaluate. So "12" is a valid python expression while 3 is not.

timeit.timeit("12") # works
timeit.timeit(3) # does not

The best way to use timeit, is to wrap the function you want to test with a lambda:

timeit.timeit(lambda: add(1,2))

This is much more elegant and less error prone than dealing with string.

Note that the lambda introduces some slight overhead to each call. If your function does anything remotely complex this will be negligible, but for very simple snippets (like "a+b") the overhead will have a significant impact.

felix-ht
  • 1,697
  • 15
  • 16
  • 1
    I've tested your version and compare it with the one with strings and using lambda adds some overhead. It took almost 50% more time compared with the regular string version.: timeit.timeit(stmt='add(2,3)', setup='from __main__ import add') vs timeit.timeit(lambda: add(2, 3)) – Guzman Ojero Oct 08 '21 at 21:36
  • For testing one method call, it could easily add a significant amount of time. It was my impression that you should benchmark a method by calling it many times. That will make the added time negligible. – John Glen Nov 18 '21 at 03:29
  • a function cannot get much simpler than this - as long as the code you are testing is a normal function one additional method call can be neglected. I'll add this information to the answer – felix-ht Nov 18 '21 at 14:43
7

my question is why the parameter type matters here?

Function arguments are fully evaluated before the function is called. That means when you do:

timeit.timeit(add(a,b))

Then add(a,b) has already been computed before timeit is used. So, it has nothing to time.

The reason timeit.timeit(add(a,b)) "works" when a and b are numeric strings is just a silly one: it's timing the evaluation of '12'. The result of calling add('1', '2') happens to be a valid string of Python code here. timeit compiles it and assumes you wanted to time the evaluation of the literal integer 12.

wim
  • 338,267
  • 99
  • 616
  • 750
  • thanks for your generous answer! Does it mean passing add('1', '2') is equivalent to the following code? timeit.timeit("add(1,2)","from __main__ import add") – liang li Jan 10 '19 at 19:52
  • 1
    How should the OP modify the code so that the time for evaluating the sum of integers can be measured? – Sheldore Jan 10 '19 at 19:55
  • @liangli Not exactly. But that is one correct way to time the actual execution of `add` using integer arguments. – wim Jan 10 '19 at 20:04
  • @liangli: no, it is the *return value* of `add()` that is passed to `timeit`. `add('1', '2')` returns the string `'12'`, so it is the equivalent of `timeit('12')`. – Martijn Pieters Jan 10 '19 at 20:07
  • since add(a,b) passed is not a string, i am curious what happens under the hood. say, why does timeit not take add(1,2) as a valid string of python code? – liang li Jan 10 '19 at 20:14
  • @liangli: `add(1, 2)` is **not a string**, it is an expression that returns a result. – Martijn Pieters Jan 10 '19 at 20:18
1

another way of solving the type "problem" is by passing the result of the function as a string!

timeit.timeit('%s'%(add(1,2)))
or
timeit.timeit(f'{add(1,2)}')
José Rosa
  • 29
  • 1
1

I know this is quite old topic, but I've found similar way using functools, when you want to check single function and pass its parameters not using strings or lambdas:

import timeit
import functools

def add(x,y):
    return x + y

a = 1
b = 2

t = timeit.Timer(functools.partial(add, a, b))
print(t.timeit(10))

Output:

4.565001290757209e-06
0

worked for me, great! used datetime before, but this way is far more comfortable!

  • "Me too" answers are not encouraged unless you add information to the other answers. – Tony Williams Apr 04 '22 at 04:58
  • Please don't add "thank you" as an answer. Once you have sufficient [reputation](https://stackoverflow.com/help/whats-reputation), you will be able to [vote up questions and answers](https://stackoverflow.com/help/privileges/vote-up) that you found helpful. - [From Review](/review/late-answers/31450172) – h4z3 Apr 05 '22 at 08:50