Notes On Python: Truth and Modulo
Today I learnt how to be less verbose when using the %
operator in Python.
SPOILER: I learnt this while trying to apply a functional approach to the first problem on Project Euler and I describe my answer here.
So, to set the scene, the first problem on Project Euler is presented as such:
“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.”
I initially answered this with the following function:
def sum_mul_three_five(end_num):
"""
Returns the sum of all the multiples of 3 or 5 below end_num
"""
total = []
for num in range(end_num):
if num % 3 == 0 or num % 5 == 0:
total.append(num)
return sum(total)
This code is pretty self-explanatory, but the line I want to focus on is the if
statement. The %
operator returns the remainder of a division, and is popularly used in Python for such things as sorting even numbers.
For instance:
>>> numbers = range(10)
>>> numbers
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> for number in numbers:
... if number % 2 == 0:
... print number
...
0
2
4
6
8
Here we iterate through a simple list of numbers. The if
statement uses the %
operator to check what the remainder will be when each number is divided by 2
. If there is no remainder, ie. the result is 0
then the number is equally divided by 2
and therefore even
…
>>> for number in range(10):
... if number % 2 == 0:
... print "%d is even" % number
... else:
... print "%d is odd" % number
...
0 is even
1 is odd
2 is even
3 is odd
4 is even
5 is odd
6 is even
7 is odd
8 is even
9 is odd
So, in my solution for Project Euler I am using %
to test whether each number is divisible by 3 or 5. When I tried to refactor this with a more Functional approach using a list comprehension, I got this long mess:
def sum_mul_three_five(end_num):
"""
Returns the sum of all the multiples of 3 or 5 below end_num
"""
total = []
[total.append(num) for num in range(end_num) if num % 3 == 0 or num % 5 == 0]
return sum(total)
Eeek! As I have been told, while this works, it’s probably the wrong way. What I didn’t realise is that we can be more concise with the modulo statements, and use them as a True/False
test. This is because 0
and 1
, the two results we can get from using %
, equate to False
and True
respectively.
EDIT See Below…
This may seem simple but it’s perhaps overlooked when learning Python, especially for those without a CS background, such as myself, or perhaps my view of the wood was blocked by the trees.
>>> 1 == True
True
>>> 0 == False
True
Therefore if num % 3 == 0
is specifying the condition is met when the remainder of dividing a number by 3 is 0
, which also corresponds to False
, or not True
.
>>> 0 != True
True
>>> 1 != True
False
Now, the if
condition of an if/else
conditional expression is met when the condition is True
, or not False
. This is where my mind started to fold over into itself. So, to return the numbers divisible by 3
in an if
statement the result needs to be True
, whereas with %
it is False
…
>>> for number in range(10):
... if number % 3:
... print number
...
1
2
4
5
7
8
>>> for number in range(10):
... if not number % 3:
... print number
...
0
3
6
9
Basically we’re replacing == 0
with the concept of True
and False
, which ends up as this:
def sum_mul_three_five(end_num):
"""
Returns the sum of all the multiples of 3 or 5 below end_num
"""
total = []
[total.append(num) for num in range(end_num) if not num % 3 or not num % 5]
return sum(total)
Which is saying return the number if it is not True
, ie. False
, a condition met when the remainder is 0
when the number is divided by 3
.
Though this is still a bit ugly, so let’s simplify the rest a bit…
def sum_mul_three_five(end_num):
"""
Returns the sum of all the multiples of 3 or 5 below end_num
"""
return sum([num for num in range(end_num) if not num % 3 or not num % 5])
Ahh yes ain’t that fresh. Everybody wants to get down like that.
I think that makes sense, I hope it does.
EDIT Ok, so I totally overlooked that the result of %
isn’t restricted to 0
or 1
, so this example is perhaps not as black and white as I first believed.
>>> for number in range(10):
... print number % 2
...
0
1
0
1
0
1
0
1
0
1
>>> for number in range(10):
... print number % 3
...
0
1
2
0
1
2
0
1
2
0
>>> for number in range(10):
... print number % 5
...
0
1
2
3
4
0
1
2
3
4
So, what happens with remainders that aren’t 0
or 1
? Well, they are interpreted as False
in this case
>>> for number in range(10):
... if number % 3:
... print "False: \tNumber: %d \tRemainder: %d" % (number, number % 3)
... else:
... print "True: \tNumber: %d \tRemainder: %d" % (number, number % 3)
...
True: Number: 0 Remainder: 0
False: Number: 1 Remainder: 1
False: Number: 2 Remainder: 2
True: Number: 3 Remainder: 0
False: Number: 4 Remainder: 1
False: Number: 5 Remainder: 2
True: Number: 6 Remainder: 0
False: Number: 7 Remainder: 1
False: Number: 8 Remainder: 2
True: Number: 9 Remainder: 0
I have to admit, this hasn’t totally clicked in my brain yet. I mean, I get it, but the whole is it True, is it False? thing is a little hard to keep track of y’know.