If we list all the natural numbers below 10 that are multiples of 3 or 5, we get 3, 5, 6 and 9. The sum of these multiples is 23.
Find the sum of all the multiples of 3 or 5 below 1000.
In [1]:
from __future__ import print_function
In [2]:
def foo(n):
return sum(filter(lambda x: x % 3 == 0 or x % 5 == 0, range(n)))
In [3]:
n=10
foo(n)
Out[3]:
In [4]:
n=1000
%timeit foo(n)
foo(n)
Out[4]:
In [5]:
def foo(n):
total = 0
for i in range(n):
if i % 3 == 0 or i % 5 == 0:
total += i
return total
In [6]:
n=1000
%timeit foo(n)
foo(n)
Out[6]:
It is surprising that the naive code immediately above is faster than the functional programming version.
In [7]:
def foo(n):
a = []
for i in range(n):
if i % 3 == 0 or i % 5 == 0:
a.append(i)
return sum(a)
In [8]:
n=1000
%timeit foo(n)
foo(n)
Out[8]:
In [9]:
def foo(n):
return sum([i for i in range(n) if i % 3 == 0 or i % 5 == 0])
In [10]:
n=1000
%timeit foo(n)
foo(n)
Out[10]:
In [11]:
def foo(n):
return sum((i for i in range(n) if i % 3 == 0 or i % 5 == 0))
In [12]:
n=1000
%timeit foo(n)
foo(n)
Out[12]:
In [13]:
def foo(n):
return sum((j for j in (i for i in range(n) if i % 3 == 0) if j % 5 == 0))
In [14]:
n=1000
%timeit foo(n)
foo(n)
Out[14]:
Wow, the nested generator expressions above were the fastest yet, also ugliest, and irredeemably wrong. The flaw is that only numbers that are multiples of both 3 and 5 are summed. I.e., the bug was 'and' instead of 'or'.
Thanks to Eric for catching that cells 13 through 16 are broken. It is easy to fast when one is wrong. So nevermind cells 13 through 16.
The unnested list comprehension was faster than the unnested generator expression, so let's try nested list comprehensions.
In [15]:
def foo(n):
return sum([j for j in [i for i in range(n) if i % 3 == 0] if j % 5 == 0])
In [16]:
n=1000
%timeit foo(n)
foo(n)
Out[16]:
I expected the nested list comprehensions to be a little bit faster than the nested generator expressions, so I was surprised by the big speed increase.
Let's play with generators more.
In [17]:
def threes_or_fives(gen):
for i in gen:
if i % 3 == 0:
yield i
elif i % 5 == 0:
yield i
def foo(n):
return sum(threes_or_fives(range(n)))
In [18]:
n=1000
%timeit foo(n)
foo(n)
Out[18]:
In [19]:
def threes_or_fives(gen):
for i in gen:
if i % 3 == 0 or i % 5 == 0:
yield i
def foo(n):
return sum(threes_or_fives(range(n)))
In [20]:
n=1000
%timeit foo(n)
foo(n)
Out[20]:
It is surprising that the naive verbose generator function with two if statements is faster than the generator function with the combined if statement.
I thought of one more way later, using sets, that should be much more elegant.
In [21]:
def foo(n):
return sum(set(range(0, n, 3)) | set(range(0, n, 5)))
In [22]:
n=1000
%timeit foo(n)
foo(n)
Out[22]:
Holy smokes! It is easier to read, but I did not expect it to be so fast.
I can not resist the temptation to generalize.
In [23]:
def foo(n, divisors):
all_multiples = set([])
for multiples in (set(range(0, n, divisor)) for divisor in divisors):
all_multiples |= multiples
return sum(multiples)
In [24]:
n=1000
divisors = (3, 5)
%timeit foo(n, divisors)
foo(n, divisors)
Out[24]:
Thanks to Eric for catching that cells 23 and 24 are broken. I summed the wrong thing. This is fixed below.
In [25]:
def foo(n, divisors):
all_multiples = set([])
for multiples in (set(range(0, n, divisor)) for divisor in divisors):
all_multiples |= multiples
return sum(all_multiples)
In [26]:
n=1000
divisors = (3, 5)
%timeit foo(n, divisors)
foo(n, divisors)
Out[26]:
That is terribly ugly. I was trying to do some kind of union of a set comprehension, but that is just not available as far as I know.
When I give up on that, it becomes simple below.
In [27]:
def foo(n, divisors):
multiples = set([])
for divisor in divisors:
multiples |= set(range(0, n, divisor))
return sum(multiples)
In [28]:
n=1000
divisors = (3, 5)
%timeit foo(n, divisors)
foo(n, divisors)
Out[28]:
I will have another go at the union thing, this time as a function.
In [29]:
def union(sets):
u = set([])
for s in sets:
u |= s
return u
In [30]:
def foo(n, divisors):
return sum(union(set(range(0, n, d)) for d in divisors))
In [31]:
n=1000
divisors = (3, 5)
%timeit foo(n, divisors)
foo(n, divisors)
Out[31]:
It works, but is not as readable, so forget it.
Of course, none of this compares to what I did last year.