I have to state right at the start, I don't know what your code does. That's ok. I'm going to stream-of-conciousness review the code, so you'll get all my thoughts and improvements as it goes on. Maybe that's a good thing.
Global Variables
Global variables are frowned upon. They make code harder to test and harder to reason about. The function user_in
modifies all_primes
. What value(s) can all_primes
have before the function runs? If the function is run more than once, does more stuff get added to all_primes
? Does stuff get replaced? Is the result corrupted?
In this case, the global variable is easy to get rid of. Instead of modifying a global all_primes
variable, how about returning it?
def user_in(num):
all_primes = []
while True:
...
all_primes.append(primes)
...
return all_primes
...
all_primes = user_in(x)
...
Now you can run user_in()
several times with different input values, and actually test that the right value is returned in your test cases.
Method Naming
What does user_in()
do? It sounds like it might ask the user for some input, but it isn't clear what that input might be.
But reading the user_in()
method, it doesn't ask for any input at all! Rather, the mainline code ask the user to enter a number, and this number is given as an input to the user_in()
method. A better name would be process_and_output()
, but that is still a horrible name; it doesn't say what kind of processing is being done, nor what kind of output is produced. Moreover, the method is producing output and it is (now) returning a value. Functions should do one logical thing, and be named after what it does.
Ranges, Lists, and Iterables
The statement numbers = [i for i in range(1, num + 1)]
constructs a range object with the numbers from 1 to num
inclusive, takes successive numbers in that range, and extracts them into a list. The remainder of the code iterates over the values stored in numbers
.
Why?
First of all, numbers = list(range(1, num + 1))
would be a shorter way of expressing the construction of numbers
.
Second, why realize a physical list of numbers when you are just going to iterator over it, and a range()
object itself is iterable? Just use the range object.
numbers = range(1, num + 1)
Factorization
factors = [i for i in numbers if num % i == 0]
This is fine, but it is inefficient. Factors come in pairs. If num % i == 0
, then i
is a factor and num // i
is also a factor. If you recorded both, then instead of looping over all numbers
, you could loop up to math.isqrt(num) + 1
. If num
is one million, this would save you nine hundred and ninety nine thousand trial divisions! Note, you would have to watch for the special cases of num
being a perfect square: you would want to generate that factor exactly once.
Move Constant Expressions Out of Loops
What does this statement do?
primes = [i for i in factors if len(factors) <= 2 and i != 1]
First, look at the len(factors) <= 2
condition. Can it change during the loop, or will it always be either True
or False
? The list of factors
doesn't change, so clearly the length is constant. So we could write this as a two statements:
if len(factors) <= 2:
primes = [i for i in factors if True and i != 1]
else:
primes = [i for i in factors if False and i != 1]
Simplifying if True and anything
to if anything
, as well as if False and anything
to "never", we get:
if len(factors) <= 2:
primes = [i for i in factors if i != 1]
else:
primes = []
Moreover, if there are 2 or less factors, those factors are 1
and num
. Since you are filtering out the i == 1
factor, you will always be left with the second factor, if it exists, which will always be num
:
if len(factors) == 2:
primes = [num]
else:
primes = []
Then, you are adding this list to all_primes
, and later flattening the list, to remove the empty lists which you added.
How about, just adding the prime you found to all_primes
, and not generating a list of lists?
if len(factors) == 2:
all_primes.append(num)
Printing
That is a rather long print statement. Any reason for making it that long? Maybe split it up into the equivalent 7 print statements? We've gotten rid of primes
, which contained 0 or 1 value, so computing the "sum of primes" needed to be adjusted. It begs the question as to what the "sum of primes" was supposed to be? Why does it produce 0 if the number wasn't prime?
print('Number: {}'.format(num))
print('Factors: {}'.format(factors))
print('Factor count: {}'.format(len(factors)))
print('Sum of factor: {}'.format(sum(factors)))
print('Sum of numbers: {}'.format(sum(numbers)))
print('Sum of Primes: {}'.format(num if len(factors) == 2 else 0))
print(''.format(primes))
Wait a second ... what is that last print statement printing? ''.format(primes)
? Did you have an argument to the format statement that didn't have a corresponding {}
code???
Python 3.6+ now has something called an f-string, which allows you to interpolate values and expressions directly into the string, so you will never mismatch .format(*args)
again!
print(f'Number: {num}')
print(f'Factors: {factors}')
print(f'Factor count: {len(factors)}')
print(f'Sum of factor: {sum(factors)}')
print(f'Sum of numbers: {sum(numbers)}')
print(f'Sum of Primes: {num if len(factors) == 2 else 0}')
print()
Looping a Fixed Number of Times
def user_in(num):
while True:
if num <= 2:
break
num -= 1
...
This code wants to loop starting from num - 1
, and go down and stop at 2
. This is what for variable in range():
statements were meant for. Don't recreate for
loops by using while True
loops and modifying the loop index yourself:
def user_in(start):
for num in range(start - 1, 2, -1):
...
Sum of all primes
print('... Sum of all primes: {} ...'.format(..., sum(flat_list) + x, ...))
What is that + x
doing there? If I give as input the number 4
, it computes the primes less than 4
to be [2, 3]
, and the sum to be 9
! I'm pretty sure this is wrong.
Reworked Code
I've reworked the code a little bit, trying to reason out what you were actually trying to do. For example "Sum of primes" I implemented a running total of the prime numbers being generated. I also implemented the faster factorization method, I mentioned above, as a separate function. For good measure, I added a sum_up_to
function for computing \$\sum_{i=1}^{n} i\$. Finally, the main function has been renamed to find_primes_up_to()
, since that seems to be what it is doing, and the mainline code has been moved into a if __name__ == '__main__':
guard, so the file can be imported into other files, and allows unit tests to be added.
import math
def factorize(num):
if num == 1:
return [1]
limit = math.isqrt(num) + 1
small = [1]
large = [num]
for i in range(2, limit):
if num % i == 0:
small.append(i)
large.append(num // i)
if small[-1] == large[-1]:
large.pop()
return small + large[::-1]
def sum_up_to(num):
return sum(range(1, num + 1))
def find_primes_up_to(limit):
sum_of_primes = 0
primes = []
for num in range(1, limit + 1):
factors = factorize(num)
print(f'Number: {num}')
print(f'Factors: {factors}')
print(f'Factor count: {len(factors)}')
print(f'Sum 1 to {num}: {sum_up_to(num)}')
if len(factors) == 2:
primes.append(num)
sum_of_primes += num
print(f'Sum of primes: {sum_of_primes}')
print()
return primes
if __name__ == '__main__':
limit = int(input("Enter upper limit: "))
primes = find_primes_up_to(limit)
print(f'Primes: {primes}')
print(f'Sum of all primes: {sum(primes)}')
num -= 1
, skipping the initial number? When user enters5
- the processing starts with4
\$\endgroup\$