Let's imagine the following code
from pymaybe import maybe
def compute_something():
return np.sin(2)
def foo(observer=None):
observer = maybe(observer)
for i in range(0, int(1e3)):
something = compute_something()
observer.observe(something) # Comment this line for comparison
So basically I am attempting some kind of inversion of control where I pass an object implementing an interface (here a single method observe
), but I want to leave the possibility that the user just pass nothing (default argument value is thus is defined as maybe(None)
.
The question is: should I expect some performance impact?
I used timeit
to compare the two (with and without the call to observe
):
Without a call to
observe
:1.77 ms ± 5.38 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
With a call to
observe
:3.27 ms ± 22.6 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
The impact seems to be quite minimal and I assumed that pymaybe
is just a try block, so I compared with the following code as well:
def foo(observer=None):
observer = None
for i in range(0, int(1e3)):
something = compute_something()
try:
observer.observe(something)
except:
pass
foo()
With the following results:
With a call to
observe
:2.73 ms ± 66.5 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
So I conclude the following (no surprise):
- pymaybe
adds some overhead by being one additional layer around a try
block
- the try
block itself has a non negligible impact on the performance
If my assumptions/conclusions above are correct, what would be a way to further improve the performance?
Shall I duplicate the code?
Is there a way to "remove" calls to methods on None
at runtime (in a loop where the value won't change during the execution of the loop)?
The first approach would be to use an if
statement, as Scott Hunter has suggested in the comments:
def foo(observer=None):
for i in range(0, int(1e3)):
something = compute_something()
if observer:
observer.observe(something)
You could also use a wrapper around observer.observe
that defaults to doing nothing:
def foo(observer=None):
try:
observe = observer.observe
except AttributeError:
def observe(x):
pass
for i in range(0, int(1e3)):
something = compute_something()
observe(something)
In my experiments, both approaches have similar running times.