PEP-008 Guidelines
Consult PEP-008 for style guideline for writing idiomatic Python. You can use various checkers, such as PyLint or PyFlakes to verify compliance with the PEP-008 guidelines.
Some highlight:
if
and while
loops do not need parenthesis (...)
around the expression. Write while k < num + 1:
, instead of while(k<(num+1))
.
- Use one space around operators. Ie, write
j += 1
instead of j+=1
.
- Testing if a value is
True
does not require comparison against the value True
. Simply use the value itself. Ie, write if primes[a]:
instead of if(primes[a] == True):
- Avoid
mixedCase
. Instead of subSum
, use sub_sum
.
Multiplying a List
This code is very inefficient:
primes = []
for i in range(num):
primes.append(True)
With 1 million candidates in your prime sieve, you will be growing the list by 1 value 1 million times. This may translate to 1 million reallocations, and 1 million copy operations, an \$O(N^2)\$ operation since the number of elements to copy to the new list keeps increasing.
Creating a list of 1 million True
values is trivial:
primes = [True] * num
Pro tip: since all but one even candidate is prime, you actually want a list of 500 thousand pairs of True
, False
values.
primes = [True, False] * (num // 2)
primes[0] = False # One isn't prime
primes[1] = True # But two is
... and then you can only check odd candidates in your sieve for half the work.
Don't evaluate expensive constant values in a loop
This loop:
while(j<(math.sqrt(num)+1)):
# ...
j += 1
will evaluate the square-root of num
one thousand times!
Since num
is not changing in the loop, the resulting \$\sqrt{n}\$ value won't change either. And square-roots are expensive to calculate. So instead:
limit = math.sqrt(num) + 1
while j < limit:
# ...
j += 1
Use for
instead of while
-with-increment
The loop:
j = 1
while j < limit:
# ...
j += 1
can easily be replaced by a for
loop:
for j in range(1, limit):
# ...
Pro-tip: Because we can initialize primes
with [False, True, False, True, False, True, ... ]
above, we can skip over the even candidates using a step-value in the range()
:
for j in range(1, limit, 2):
# ...
List comprehension
We again encounter the inefficient \$O(N^2)\$ list.append()
-in-loop code:
prime = []
for a in range(num):
if(primes[a] == True):
prime.append(a+1)
Here, we can't use list-multiplication to create the desired list. Instead, we can use list-comprehension:
prime = [ a + 1 for a in range(num) if primes[a] ]
Why is this better? The Python interpreter can "guess" the resulting list is no longer than num
entries, and allocate sufficient space. Then, it will loop through and populate the list with the successive a + 1
values where primes[a]
is true. At the end, it realizes the list is much shorter than it initially allocated, and can resize the list down to actual size.
But wait. We are looking up in primes[]
successive values, which involves 1 million indexing operations. Instead of looping over a range(num)
, we should loop over the primes
list itself. No indexing; just extracting each value from the list one after the other.
prime = [ a + 1 for a, prime in enumerate(primes) if prime ]
That +1
is annoying; that is several thousand addition operations. It would be better if we counted elements of primes
beginning with 1
for the primes[0]
value:
prime = [ a for a, prime in enumerate(primes, 1) if prime ]
Easy peasy.
Zero-based indexing
Your indexing scheme, which stores the primality of a
in primes[a-1]
is confusing, and causes more headaches than it's worth. Yes, you've saved 1 list element, but that tiny savings in memory is not worth the confusion.
Store the primality of a
in primes[a]
.
Realizing Slices
Consider the code:
sub = primes[i:i+count]
subSum = sum(sub)
It takes the slice primes[i:i+count]
, and then assigns it to a variable. At that moment, the Python interpreter MUST copy the slice into a real list. Then, you never use sub
again, after the next statement.
If you wrote:
subSum = sum(primes[i:i+count])
the Python interpreter may pass an iterator to the slice to the sum()
function, and add up the numbers. The copy of the slice into a real list may be avoided.
x "in" list
To test if subSum in primes:
, the interpreter checks the each element of the list to see if it matches the subSum
, until either a match is found, or the end of the list is encountered. This is an \$O(N)\$ operation. Not exactly slow, but it takes time.
If you converted primes
into a set
beforehand, the values are hashed into bins, and the in
operation drops to \$O(1)\$, which is fast.
primes = set(primes)
But ... creating the set is an \$O(N)\$ operation and you already generated a list of primality flags in sieve()
. If you returned that list of flags, in addition to your list of prime numbers, your prime test could be:
if primes_flags_from_sieve[subSum-1]:
which not only is \$O(1)\$, it is just a lookup! No hashing of the value to lookup. So this would be very fast.
Algorithmic Improvements
Repeatedly adding close to the same sequence of numbers together over and over again is a waste of time:
2 + 3 + 5 + 7 + 11 + 13 + 17 + 19 + 23 + 29 + ...
3 + 5 + 7 + 11 + 13 + 17 + 19 + 23 + 29 + ...
5 + 7 + 11 + 13 + 17 + 19 + 23 + 29 + ...
: : :
2 + 3 + 5 + 7 + 11 + 13 + 17 + 19 + 23 + 29
3 + 5 + 7 + 11 + 13 + 17 + 19 + 23 + 29
5 + 7 + 11 + 13 + 17 + 19 + 23
: : :
2 + 3 + 5 + 7 + 11 + 13 + 17 + 19 + 23
3 + 5 + 7 + 11 + 13 + 17 + 19 + 23
5 + 7 + 11 + 13 + 17 + 19 + 23
: : :
2 + 3 + 5 + 7 + 11 + 13 + 17 + 19
3 + 5 + 7 + 11 + 13 + 17 + 19
5 + 7 + 11 + 13 + 17 + 19
: : :
2 + 3 + 5 + 7 + 11 + 13 + 17
3 + 5 + 7 + 11 + 13 + 17
5 + 7 + 11 + 13 + 17
If only there was a simple way of performing the addition once, and then getting the individual sums of subsequences out of that pre-processed data in \$O(1)\$ time ...
Left to student.