While discussing with some colleagues, one argued that a for
loop over a list of objects to call a method is a bad practice because it has bad performance compared to deque(map(methodcaller())
.
He did not support his claim with a benchmark. I did one myself.
Here is the setup:
import collections
import operator
import pyperf
class Do:
def nothing(self):
pass
dos = [Do() for _ in range(10000)]
And functions I'll benchmark:
def with_for_loop():
for do in dos:
do.nothing()
def with_deque_map():
collections.deque(
map(operator.methodcaller("nothing"), dos),
maxlen=0,
)
From a Pythonic point of view, if performance are not a requirement, I think the for loop is better by far. But I'm looking for a performance point of view.
I would expect the difference in an algorithm, minimal if significant, in favor of the deque/map/methodcaller.
But here are the results:
.....................
for_loop: Mean +- std dev: 244 us +- 8 us
.....................
deque_map: Mean +- std dev: 1.09 ms +- 0.02 ms
(same time difference with larger list)
Did I do something wrong with the benchmark?
Is the overhead of methodcaller
big enough to make this this slow?
I don't understand this result.
When Do.nothing()
is a static method:
class Do:
@staticmethod
def nothing():
pass
The performance gap get smaller:
.....................
for_loop: Mean +- std dev: 395 us +- 13 us
.....................
deque_map: Mean +- std dev: 712 us +- 10 us
Here, the for
loop get slower, and the other get faster than before. I think that the fact that Do.nothing
is a static method should make both faster, since there is no need to instantiate a bound method
I don't understand why the for
loop get slower.
If you want to run the benchmark yourself:
- Install pyperf:
pip install pyperf
- And ehre is the full script:
import collections
import operator
import pyperf
class Do:
def nothing(self):
pass
dos = [Do() for _ in range(10000)]
def with_for_loop():
for do in dos:
do.nothing()
def with_deque_map():
collections.deque(
map(operator.methodcaller("nothing"), dos),
maxlen=0,
)
def pyperf_bench():
runner = pyperf.Runner()
runner.bench_func(
name="for_loop",
func=with_for_loop,
)
runner.bench_func(
name="deque_map",
func=with_deque_map,
)
if __name__ == '__main__':
pyperf_bench()
Notes: deque with maxlen of 0 consume the map iterable without storing anything. docs.python.org/3/library/collections.html#deque-objects It's a known recipe to consume an generator (look at "consume" recipe): docs.python.org/3/library/itertools.html#itertools-recipes
Do.nothing
instead ofoperator.methodcaller('nothing')
should bring them closer (probably still slower though). \$\endgroup\$