[Tutor] List comprehension question
Stefan Behnel
stefan_ml at behnel.de
Mon Nov 8 18:10:23 CET 2010
Hugo Arts, 08.11.2010 00:53:
> On Mon, Nov 8, 2010 at 12:36 AM, Richard D. Moores wrote:
>> def proper_divisors(n):
>> """
>> Return the sum of the proper divisors of positive integer n
>> """
>> return sum([x for x in range(1,n) if int(n/x) == n/x])
>>
>> The list comprehension is this function is inefficient in that it computes
>> n/x twice. I'd like to do an a = n/x and use a in
>> "if int(a) == a", but I don't know how.
>>
>
> You can't do that inside a list comprehension. Either get rid of the
> comprehension and do a regular loop, or get rid of the n/x expression.
>
> I'd suggest replacing the whole check with x % n == 0. n is a proper
> divisor of x if there is no remainder after division, after all. This
> also means you won't have to do a cast, which tend to be fairly
> expensive.
>
> On another note, getting rid of the list comprehension and using a
> generator expression will be even faster, since you won't have to
> build the list.
I gave this suggestion a try. It is true for me when run in CPython 3.2:
$ python3 -m timeit -s 'from divisors import forloop' 'forloop(1000000)'
10 loops, best of 3: 161 msec per loop
$ python3 -m timeit -s 'from divisors import genexp' 'genexp(1000000)'
10 loops, best of 3: 159 msec per loop
$ python3 -m timeit -s 'from divisors import listcomp' 'listcomp(1000000)'
10 loops, best of 3: 171 msec per loop
However, it is no longer true when I run the same thing in Cython:
$ python3 -m timeit -s 'from divisors import forloop' 'forloop(1000000)'
100 loops, best of 3: 13.6 msec per loop
$ python3 -m timeit -s 'from divisors import genexp' 'genexp(1000000)'
100 loops, best of 3: 13.6 msec per loop
$ python3 -m timeit -s 'from divisors import listcomp' 'listcomp(1000000)'
100 loops, best of 3: 12.6 msec per loop
Here, the listcomp clearly wins, i.e.
return sum([x for x in range(1,n) if n%x == 0])
is actually *faster* than the inlined loop for
result = sum(x for x in range(1,n) if n%x == 0)
return result
This totally surprised me, until I figured out what was going on here.
In the case of the listcomp, the inner loop is smaller, it does not contain
the adding. It only contains the division that filters out the
non-divisors. Then, from time to time, it calls into the list appender to
append a Python integer that actually matched the condition. But the final
list is tiny, so this is done extremely rarely compared to the number of
loop iterations. The tighter loop simply wins. And summing up a short list
in another tight loop is just another very fast operation.
Lesson learned:
Sometimes, it can be worth splitting a loop in two, one with a tight filter
for the values, and another one to work on the remaining values.
Stefan
More information about the Tutor
mailing list