Welcome to Python!
"Project Euler exists to encourage, challenge, and develop the skills and enjoyment of anyone with an interest in the fascinating world of mathematics."
Like you, I went to Project Euler when I was learning Python as yet another language for my toolbox. Unfortunately, Project Euler is primarily a mathematics challenge site, not a programming challenge site. The emphasis is on solving the problem and getting the right answer; not on programming skills. More over, the site asks that you not post your solution, which really discourages you from getting feedback on your programming skills, and proper best practices for the language. So while you can get problems that you can actually try writing code to solve the problem for, you’re still discouraged from getting feedback on your approach. Not exactly an ideal site for learning a new language on.
Still, you have violated their request and posted your solution, so let’s try and give you some useful feedback.
Write Functions
You’ve got two sets of data to try your solution on. The first being the “example” data in the problem itself; the second being the dataset you are being asked to solve. If you’re going to do something twice, write a function:
def sum_of_multiples_of_3_or_5_below(limit):
total = 0
for n in range(0, limit):
if n % 3 == 0 or n % 5 == 0:
total += n
return total
Then you can test your function with the example data, as well as solve the problem:
assert sum_of_multiples_of_3_or_5_below(10) == 23
answer = sum_of_multiples_of_3_or_5_below(1000)
print(f"sum of all multiples of 3 or 5 below 1000 is {answer}")
This gives you confidence in your solution. Usually the example data is fairly trivial, so the time needed to solve the problem twice isn’t noticeably increased.
Use a __main__
guard
Now that we have a function, it is possible to import this “module” into another program to reuse the function. Except, it runs that pesky code at the bottom, generating unexpected output. Using a __main__
guard, the code will only execute when we run this script, not when this scripted is imported:
if __name__ == '__main__':
assert sum_of_multiples_of_3_or_5_below(10) == 23
answer = sum_of_multiples_of_3_or_5_below(1000)
print(f"sum of all multiples of 3 or 5 below 1000 is {answer}")
Generalization
This function is still perhaps too specific. Why just below a limit
. Why just multiples of 3
or 5
? We can generalize things a wee bit, and maybe actually increase the possibility of reusing the function elsewhere. And perhaps more importantly, explore the capabilities of Python.
First, instead of passing in the limit, let’s pass in the range
.
def sum_of_multiples_of_3_or_5_in(iterable):
total = 0
for n in iterable:
if n % 3 == 0 or n % 5 == 0:
total += n
return total
if __name__ == '__main__':
assert sum_of_multiples_of_3_or_5_in(range(0, 10)) == 23
answer = sum_of_multiples_of_3_or_5_in(range(0, 1000))
print(f"sum of all multiples of 3 or 5 below 1000 is {answer}")
range
is a first class object in Python. It can be passed as an argument. So now you can easily compute the sum of the multiples of 3 or 5 in range(1000, 2000)
.
Or ... any iterable
object, actually, such as lists.
print(sum_of_multiples_of_3_or_5_in([10, 12, 15, 17, 18, 19, 20])
How about those multiples? Let’s make them more general:
def sum_of_multiples_in(iterable, m1, m2):
total = 0
for n in iterable:
if n % m1 == 0 or n % m2 == 0:
total += n
return total
if __name__ == '__main__':
m1 = 3
m2 = 5
assert sum_of_multiples_in(range(0, 10), m1, m2) == 23
answer = sum_of_multiples_in(range(0, 1000), m1, m2)
print(f"sum of all multiples of {m1} or {m2} below 1000 is {answer}")
You had a formula for computing the answer before. Sum of multiples of 3, plus sum of multiples of 5, minus sum of multiples of 15. Now it is harder, because m1
could be a multiple of m2
or vis versa. More cases to check for. But the above works just fine regardless of whether m1
and m2
are mutually prime or not.
any
Why only multiples of 2 numbers? Why not multiples of 3
, 5
or 7
? Passing yet another argument to the function seems wrong, because we’ll then need another function for 4 multiples, and yet another for 5 multiples. Let’s instead pass a list.
def sum_of_multiples_in(iterable, multiples):
total = 0
for n in iterable:
for m in multiples:
if n % m == 0:
total += n
break
return total
That’s a good start. For each value of n
, we start looping of the multiples, and if we find one, we add n
to total
and break out of the inner loop, to continue with the next n
value.
But we can make it clearer. We want to know if n
is a multiple of any
of the multiples. Python has an any()
function, which is true of any of the terms is true:
def sum_of_multiples_in(iterable, multiples):
total = 0
for n in iterable:
if any(n % m == 0 for m in multiples)
total += n
return total
There is also an all(...)
function which returns true only if all of the terms are true. Not needed here, but good to have in your back pocket.
sum
Now that we have a loop, an accumulator, and a filter condition, we can combine the three into a single sum()
operation:
def sum_of_multiples_in(iterable, multiples):
return sum(n for n in iterable if any(n % m == 0 for m in multiples))
Variable arguments
Using our above function, we have to pass in a list
of multiples:
assert sum_of_multiples_in(range(0, 10), [3, 5]) == 23
It may be desirable to get rid of that explicit list [3, 5]
, and just pass in the arguments 3, 5
like we did earlier. We can do this by using a variable argument list syntax.
def sum_of_multiples_in(iterable, *multiples):
return sum(n for n in iterable if any(n % m == 0 for m in multiples))
assert sum_of_multiples_in(range(0, 10), 3, 5) == 23
After all explicit arguments (iterable
in this case), all remaining (non-keyword) arguments are rolled up into one list and assigned to the *args
argument ... named multiples
in this case.
"""Docstrings"""
Comments are used to describe the code to someone reading the source code. Doc-strings are used to describe how to use the code you’ve written, without the user needing to read your code. Various tools exist to extract the doc-strings, and turn them into webpages, PDF documents and so on. The simplest is Python’s built-in help()
command.
"""
A collection of functions for solving problems from Project Euler.
(Currently, only Problem 1)
"""
def sum_of_multiples_in(iterable, *multiples):
"""
From a list of numbers, return the sum of those numbers which
are a multiple of one or more of the remaining arguments.
"""
return sum(n for n in iterable if any(n % m == 0 for m in multiples))
if __name__ == '__main__':
m1 = 3
m2 = 5
assert sum_of_multiples_in(range(0, 10), m1, m2) == 23
answer = sum_of_multiples_in(range(0, 1000), m1, m2)
print(f"sum of all multiples of {m1} or {m2} below 1000 is {answer}")
A doc string is a string appearing at the top of a module, class, and/or function. It can be a single quoted string ("docstring"
or 'docstring'
) or a triple quoted string ("""docstring"""
or '''docstring'''
). Triple quoted strings are typically used since they can span multiple lines and can contain quotes without needing escaping.
Save the file as pe1.py
, then from a Python interpreter, type:
>>> import pe1
>>> help(pe1)
to see your help documentation.
Type Hints
Coming from C++, you will be used to a more “type safe” environment. Python’s fast and loose rules for type safety may be a wee bit difficult to get used too. Fortunately (or unfortunately), Python 3.6 and later allows you to specify “type hints”. These do absolutely nothing ... at least, as far as the Python interpreter is concerned. They can be read by static analysis tools, which can reason about them and ensure variables are being used in their intended fashion. If used for nothing else, they can provide additional “documentation” about the types of arguments for functions, and the return type of the function.
def sum_of_multiples_in(iterable: int, *multiples: int) -> int:
...
You can use type hints on local variables as well:
total: int = 0
Hope this jump starts your exploration of Python. And once again, welcome to Python!