Measure runtime of function with arguments using timeit.timeit - python

I would like to create a function. The code doesn't work, but you can get the idea what I want to get:
def time_test(func, test_data: int) -> float:
# should return a runtime of given function
return timeit(stmt=func(test_data), number=10000)
Obviously, I do have to pass as stmt something callable or a string with executable code. That's why it doesn't work, but I don't know how to do this in a correct way.
Example how I want to use time_test() function:
from timeit import timeit
# To be tested
def f1(argument_ops):
result = 0
for i in range(argument_ops):
result += 4
return result
def f2(argument_ops):
result = 0
for i in range(argument_ops):
for j in range(argument_ops):
result += 4
return result
# test function to be implemented
def time_test(func, test_data: int) -> float:
runtime = 0
# implement this, it should return a runtime of a given function. Function needs
# argument test_data.
return runtime
# example of usage
print(time_test(f1, 96))
print(time_test(f2, 24))

Actually, I think you can use the globals argument, which takes a namespace in which to execute the statement, to do this relatively easily. Something to the effect of:
def time_test(func, test_data: int) -> float:
gs = dict(func=func, test_data=test_data)
runtime = timeit("func(test_data)", globals=gs)
# implement this, it should return a runtime of a given function. Function needs argument test_data.
return runtime
Note, by default this times how long it takes to the statement 1000000 times.

I would suggest doing it this way which involves passing the function definition to the time_test() function as well as the function name and arguments:
from timeit import timeit
# To be tested
f1_def = """
def f1(argument_ops):
result = 0
for i in range(argument_ops):
result += 4
return result
"""
f2_def = """
def f2(argument_ops):
result = 0
for i in range(argument_ops):
for j in range(argument_ops):
result += 4
return result
"""
# test function
def time_test(func, test_data: int, setup: str) -> float:
stmt = f'{func}({test_data!r})'
runtime = timeit(stmt, setup)
return runtime
# example of usage
print(time_test('f1', 96, setup=f1_def)) # -> 4.069019813000001
print(time_test('f2', 24, setup=f2_def)) # -> 34.072881441999996

Related

Caching function results with numba

I have a program in Python and I use numba to compile the code to native and run faster.
I want to accelerate the run even further, and implement a cache for function results - if the function is called twice with the same parameters, the first time the calculation will run and return the result and the same time the function will return the result from the cache.
I tried to implement this with a dict, where the keys are tuples containing the function parameters, and the values are the function return values.
However, numba doesn't support dictionaries and the support for global variables is limited, so my solution didn't work.
I can't use a numpy.ndarray and use the indices as the parameters, since some of my parameters are floats.
The problem i that both the function with cached results and and the calling function are compiled with numba (if the calling function was a regular python function, I could cache using just Python and not numba)
How can I implement this result cache with numba?
============================================
The following code gives an error, saying the Memoize class is not recognized
from __future__ import annotations
from numba import njit
class Memoize:
def __init__(self, f):
self.f = f
self.memo = {}
def __call__(self, *args):
if args not in self.memo:
self.memo[args] = self.f(*args)
#Warning: You may wish to do a deepcopy here if returning objects
return self.memo[args]
#Memoize
#njit
def bla(a: int, b: float):
for i in range(1_000_000_000):
a *= b
return a
#njit
def caller(x: int):
s = 0
for j in range(x):
s += bla(j % 5, (j + 1) % 5)
return s
if __name__ == "__main__":
print(caller(30))
The error:
Untyped global name 'bla': Cannot determine Numba type of <class '__main__.Memoize'>
File "try_numba2.py", line 30:
def caller(x: int):
<source elided>
for j in range(x):
s += bla(j % 5, (j + 1) % 5)
^
Changing the order of the decorators for bla gives the following error:
TypeError: The decorated object is not a function (got type <class '__main__.Memoize'>).

Python function "sum" with syntax like: sum() -> 0 sum(1)(2)(3) -> 6

I am new to Python and I need to write function sum with similar syntax:
print(sum())
print(sum(1)(2)(3))
print(sum(1)(2)(3)(-4)(-5))
The output is:
0
6
-3
I tried to write decorator like this:
def sum(a=0):
res = a
def helper(b=0):
nonlocal res
res += b
return res
return helper
But it helps only with fixed quantity of (). So this code works only for: sum(1)(2)->3 and doesn't for sum()-><function sum.<locals>.helper at 0x7fca6de44160> or sum(1)()(3) -> TypeError: 'int' object is not callable
I think there should be a decorator with recursion, but i don't know how to realize it
That syntax choice looks very odd to me - should the result of sum(foo) be a number or a function? The built-in sum just takes an iterable and returns a number, which feels much less surprising.
However, assuming that you are certain you indeed want to create something that looks like an integer, walks like an integer, swims like an integer but is also callable, the language does let you create it:
class sum(int):
def __call__(self, x=0):
return sum(self + x)
The output is as you specified:
print(sum())
print(sum(1)(2)(3))
print(sum(1)(2)(3)(-4)(-5))
0
6
-3
Finaly, I found my way of solving this task (using only functions):
def sum(x=None):
value = 0
def helper(y=None):
nonlocal value
if y is None:
return value
value += y
return helper
return helper(x)

Parameters of parsy parser

Consider the following code, which parses and evaluates strings like 567 +223 in Python.
import parsy as pr
from parsy import generate
def lex(p):
return p << pr.regex('\s*')
numberP = lex(pr.regex('[0-9]+').map(int))
#generate
def sumP():
a = yield numberP
yield lex(pr.string('+'))
b = yield numberP
return a+b
exp = sumP.parse('567 + 323')
print(exp)
The #generate is a total mystery for me. Does anyone have more information on how that trick works? It does allow us to write in a similar style to Haskell's monadic do notation. Is code reflection needed to make your own #generate, or is there a clever way to interpret that code literally.
Now here comes my main problem, I want to generalize sumP to opP that also takes an operator symbol and a combinator function:
import parsy as pr
from parsy import generate
def lex(p):
return p << pr.regex('\s*')
numberP = lex(pr.regex('[0-9]+').map(int))
#generate
def opP(symbol, f):
a = yield numberP
yield lex(pr.string(symbol))
b = yield numberP
return f(a,b)
exp = opP('+', lambda x,y:x+y).parse('567 + 323')
print(exp)
This gives an error. It seems that the generated opP already has two arguments, which I do not know how to deal with.
The way that decorators work in Python is that they're functions that are called with the decorated method as an argument and then their return value is assigned to the method name. In other words this:
#foo
def bar():
bla
Is equivalent to this:
def bar():
bla
bar = foo(bar)
Here foo can do anything it wants with bar. It may wrap it in something, it may introspect its code, it may call it.
What #generate does is to wrap the given function in a parser object. The parser object, when parsing, will call the function without arguments, which is why you get an error about missing arguments when you apply #generate to a function that takes arguments.
To create parameterized rules, you can apply #generate to an inner 0-argument function and return that:
def opP(symbol, f):
#generate
def op():
a = yield numberP
yield lex(pr.string(symbol))
b = yield numberP
return f(a,b)
return op

map with lambda vs map with function - how to pass more than one variable to function?

I wanted to learn about using map in python and a google search brought me to http://www.bogotobogo.com/python/python_fncs_map_filter_reduce.php which I have found helpful.
One of the codes on that page uses a for loop and puts map within that for loop in an interesting way, and the list used within the map function actually takes a list of 2 functions. Here is the code:
def square(x):
return (x**2)
def cube(x):
return (x**3)
funcs = [square, cube]
for r in range(5):
value = map(lambda x: x(r), funcs)
print value
output:
[0, 0]
[1, 1]
[4, 8]
[9, 27]
[16, 64]
So, at this point in that tutorial, I thought "well if you can write that code with a function on the fly (lambda), then it could be written using a standard function using def". So I changed the code to this:
def square(x):
return (x**2)
def cube(x):
return (x**3)
def test(x):
return x(r)
funcs = [square, cube]
for r in range(5):
value = map(test, funcs)
print value
I got the same output as the first piece of code, but it bothered me that variable r was taken from the global namespace and that the code is not tight functional programming. And there is where I got tripped up. Here is my code:
def square(x):
return (x**2)
def cube(x):
return (x**3)
def power(x):
return x(r)
def main():
funcs = [square, cube]
for r in range(5):
value = map(power, funcs)
print value
if __name__ == "__main__":
main()
I have played around with this code, but the issue is with passing into the function def power(x). I have tried numerous ways of trying to pass into this function, but lambda has the ability to automatically assign x variable to each iteration of the list funcs.
Is there a way to do this by using a standard def function, or is it not possible and only lambda can be used? Since I am learning python and this is my first language, I am trying to understand what's going on here.
You could nest the power() function in the main() function:
def main():
def power(x):
return x(r)
funcs = [square, cube]
for r in range(5):
value = map(power, funcs)
print value
so that r is now taken from the surrounding scope again, but is not a global. Instead it is a closure variable instead.
However, using a lambda is just another way to inject r from the surrounding scope here and passing it into the power() function:
def power(r, x):
return x(r)
def main():
funcs = [square, cube]
for r in range(5):
value = map(lambda x: power(r, x), funcs)
print value
Here r is still a non-local, taken from the parent scope!
You could create the lambda with r being a default value for a second argument:
def power(r, x):
return x(r)
def main():
funcs = [square, cube]
for r in range(5):
value = map(lambda x, r=r: power(r, x), funcs)
print value
Now r is passed in as a default value instead, so it was taken as a local. But for the purposes of your map() that doesn't actually make a difference here.
Currying is another option. Because a function of two arguments is the same as a function of one argument that returns another function that takes the remaining argument, you can write it like this:
def square(x):
return (x**2)
def cube(x):
return (x**3)
def power(r):
return lambda(x): x(r) # This is where we construct our curried function
def main():
funcs = [square, cube]
for y in range(5):
value = map(power(y), funcs) # Here, we apply the first function
# to get at the second function (which
# was constructed with the lambda above).
print value
if __name__ == "__main__":
main()
To make the relation a little more explicit, a function of the type (a, b) -> c (a function that takes an argument of type a and an argument of type b and returns a value of type c) is equivalent to a function of type a -> (b -> c).
Extra stuff about the equivalence
If you want to get a little deeper into the math behind this equivalence, you can see this relationship using a bit of algebra. Viewing these types as algebraic data types, we can translate any function a -> b to ba and any pair (a, b) to a * b. Sometimes function types are called "exponentials" and pair types are called "product types" because of this connection. From here, we can see that
c(a * b) = (cb)a
and so,
(a, b) -> c ~= a -> (b -> c)
Why not simply pass the functions as part of the argument to power(), and use itertools.product to create the required (value, func) combinations?
from itertools import product
# ...
def power((value, func)):
return func(value)
for r in range(5):
values = map(power, product([r], funcs))
print values
Or if you don't want / require the results to be grouped by functions, and instead want a flat list, you could simply do:
values = map(power, product(range(5), funcs))
print values
Note: The signature power((value, func)) defines power() to accept a single 2-tuple argument that is automatically unpacked into value and func.
It's equivalent to
def power(arg):
value, func = arg

Function which computes once, caches the result, and returns from cache infinitely (Python)

I have a function which performs an expensive operation and is called often; but, the operation only needs to be performed once - its result could be cached.
I tried making an infinite generator but I didn't get the results I expected:
>>> def g():
... result = "foo"
... while True:
... yield result
...
>>> g()
<generator object g at 0x1093db230> # why didn't it give me "foo"?
Why isn't g a generator?
>>> g
<function g at 0x1093de488>
Edit: it's fine if this approach doesn't work, but I need something which performs exactly like a regular function, like so:
>>> [g() for x in range(3)]
["foo", "foo", "foo"]
g() is a generator function. Calling it returns the generator. You then need to use that generator to get your values. By looping, for example, or by calling next() on it:
gen = g()
value = next(gen)
Note that calling g() again will calculate the same value again and produce a new generator.
You may just want to use a global to cache the value. Storing it as an attribute on the function could work:
def g():
if not hasattr(g, '_cache'):
g._cache = 'foo'
return g._cache
A better way: #functools.lru_cache(maxsize=None). It's been backported to python 2.7, or you could just write your own.
I am occasionally guilty of doing:
def foo():
if hasattr(foo, 'cache'):
return foo.cache
# do work
foo.cache = result
return result
Here's a dead-simple caching decorator. It doesn't take into account any variations in parameters, it just returns the same result after the first call. There are fancier ones out there that cache the result for each combination of inputs ("memoization").
import functools
def callonce(func):
result = []
#functools.wraps(func)
def wrapper(*args, **kwargs):
if not result:
result.append(func(*args, **kwargs))
return result[0]
return wrapper
Usage:
#callonce
def long_running_function(x, y, z):
# do something expensive with x, y, and z, producing result
return result
If you would prefer to write your function as a generator for some reason (perhaps the result is slightly different on each call, but there's still a time-consuming initial setup, or else you just want C-style static variables that allow your function to remember some bit of state from one call to the next), you can use this decorator:
import functools
def gen2func(generator):
gen = []
#functools.wraps(generator)
def wrapper(*args, **kwargs):
if not gen:
gen.append(generator(*args, **kwargs))
return next(gen[0])
return wrapper
Usage:
#gen2func
def long_running_function_in_generator_form(x, y, z):
# do something expensive with x, y, and z, producing result
while True:
yield result
result += 1 # for example
A Python 2.5 or later version that uses .send() to allow parameters to be passed to each iteration of the generator is as follows (note that **kwargs are not supported):
import functools
def gen2func(generator):
gen = []
#functools.wraps(generator)
def wrapper(*args):
if not gen:
gen.append(generator(*args))
return next(gen[0])
return gen[0].send(args)
return wrapper
#gen2func
def function_with_static_vars(a, b, c):
# time-consuming initial setup goes here
# also initialize any "static" vars here
while True:
# do something with a, b, c
a, b, c = yield # get next a, b, c
A better option would be to use memoization. You can create a memoize decorator that you can use to wrap any function that you want to cache the results for. You can find some good implementations here.
You can also leverage Beaker and its cache.
Also it has a tons of extensions.

Categories