Found a problem

dn PythonList at DancesWithMice.info
Tue Mar 9 20:36:31 EST 2021


On 10/03/2021 09.16, Victor Dib wrote:
> Olá, comunidade do Python!


Olá Victor!

At which point we should switch to English, because this is an
English-language list. (there may be Portuguese lists - I haven't looked)

Please do not be afraid to write in English. There are many here who are
quite aware of how difficult it can be to use a second, third, ...
language, and thus no-one will embarrass you - but, few here understand
Portuguese and so writing in English means that more people will
consider the question and attempt to help you...


> Meu nome é Victor Dib, e sou um estudante brasileiro de programação.

We wish you well in your studies.


> Já entrei em contato com vocês hoje, e vocês solicitaram que eu me inscrevesse na lista de e-mails de vocês primeiro. Bom, isso já foi feito, então espero que agora vocês possam dar atenção ao meu caso.

The list-community is a group of people who volunteer their time, and
are happy to help if they can (and if they want to do so).


> Já sou um usuário de nível iniciante pra médio da linguagem Python. Mas recentemente tive um problema no uso da linguagem.
> Ao construir uma função que encontra números perfeitos, percebi que a linguagem não estava realizando corretamente a inserção de novos dados inteiros à uma lista. Encaminharei imagens para demonstrar o meu problema.

Unfortunately, the list-server (software) removes graphics. Please
copy-paste code-snippets (as you did) and (all of) any error-messages,
sample outputs, etc.


> Tive problemas com a versão 3.9.2 da linguagem. Mas também usei a versão 3.8.7 (por ser marcada como estável no site da linguagem), e igualmente tive problemas.

The changes and improvements between the two versions of Python are
unlikely to affect this code.


> Acredito que não haja problemas em minha lógica, e por isso gostaria que vocês desse m uma olhada para ter certeza de que não é um problema na linguagem.

Let's have a look and see if there is an error in the code-provided, or
if it's in the Python language itself...


> Odiaria que uma linguagem tão incrível como o Python não funcionasse como deveria. Por isso, por favor, verifiquem meu caso!

We would not like to think that there is a fault in our favorite language!


> Obrigado!

You are welcome. Thanks for joining our community, and as time goes by,
we look forward to seeing you help others...


> O programa que escrevi para encontrar números perfeitos:

Let's start with the definition of a "Perfect Number":
<<<
In number theory, a perfect number is a positive integer that is equal
to the sum of its positive divisors, excluding the number itself. For
instance, 6 has divisors 1, 2 and 3 (excluding itself), and 1 + 2 + 3 =
6, so 6 is a perfect number.

The sum of divisors of a number, excluding the number itself, is called
its aliquot sum, so a perfect number is one that is equal to its aliquot
sum...
The first few perfect numbers are 6, 28, 496 and 8128
>>>
(https://en.wikipedia.org/wiki/Perfect_number)


> def num_perf_inf(n):
>     divisors = []
>     perfects = []
>     limit = n - 1
>     for i in range(1, limit):
>         dividend = i + 1
>         for j in range(i):
>             divisor = j + 1
>             if dividend % divisor == 0:
>                 divisors.append(divisor)
>         print(f'Divisors of {i + 1}: {divisors}')
> 
>         divisors.pop()
>         if sum(divisors) == dividend:
>             perfects.append(i)
>         divisors.clear()
>     print(perfects)
> 
> 
> num_perf_inf(28)
> 
> E o resultado da execução desse código, em Python 3.9.2 e em Python 3.8.7 (como já mencionado, testei as duas versões da linguagem:
> 
> 
> Divisors of 2: [1]
> Divisors of 3: [1]
> Divisors of 4: [1, 2]
> Divisors of 5: [1]
> Divisors of 6: [1, 2, 3]
...

> Divisors of 27: [1, 3, 9]
> [23]


My thinking is different to yours, but it is how I solve problems.
Secondly, there is much in this code which is not idiomatic Python. I
want to make changes to make the code easier to understand...

The first thing I want to change is the function call. Human-thinking
says: give me all the "perfect numbers" up to 28. The code uses
Python-thinking, eg range( 28 ) is integers >0 and <28 (not <=28)

The second thing is that the first "perfect number" is six, so let's
start by making sure that it is correctly-reported.

num_perf_inf( 6 )

which requires another code-change:

def num_perf_inf( last_number_to_be_considered ):

Note: in Python "style" spaces before "(" are 'definitely-wrong', spaces
inside "(" and ")" are considered either 'wrong' or 'unnecessary'. I use
them to help my aged-eyes read the line of code.

The parameter "n" is only used in one place - to calculate "limit", and
"limit" is only used in one place. So, let's get rid of "limit" and
change the outer for-loop to:

for i in range( 1, last_number_to_be_considered + 1 ):

For our first test, "i" will cover the range: 1, 2, 3, 4, 5, and 6.
Are we happy?

My mind is better with words. I prefer to call you "Victor"!

Plus, I've already changed the function's parameter, so let me continue
to help my understanding by changing "i" to a name which relates to the
parameter's name and its purpose:

for number_to_be_considered in range( 1, last_number_to_be_considered + 1 ):

Unfortunately, long identifiers start to exceed the line-length,
particularly in my email client. Apologies!

The 'message' is: use meaningful names!


Let's have a look at the loop now. What is it doing?

It performs two functions:
1 find the divisors from number_to_be_considered, and
2 asks if the number_to_be_considered is a "perfect number"

Let's separate those two tasks into separate sub-routines (functions),
so that each has only one responsibility - and can be tested separately
(and thus, more easily) and in great detail (with different source
data), etc, etc.

To get started, the function might now appear as:

    for number_to_be_considered in range( 1,
last_number_to_be_considered + 1 ):
        divisors = find_divisors( number_to_be_considered )
        is_perfect_number( number_to_be_considered, divisors )

Note that there is no longer any need to define "divisors" before the loop!

Yes, we could go one step further and combine these into a single line
with two function-calls. However, might that be unnecessarily complicated?


Let's start detailing the second function: "is_perfect_number()". Note
the choice of name appears to be a question. That's because it's result
should be a boolean condition - either the number is "perfect" or it is
not!

Remember what I said about "testing"? We can test this function, on its
own, eg

print( "Should be True :", is_perfect_number( 6, [ 1, 2, 3 ] ) )
print( "Should be False :", is_perfect_number( 4, [ 1, 2 ] ) )
(etc)

Let's copy the relevant lines of code into the new function.

Note moving the "perfects" list definition and replacing the "i"!

Also, added a debug-print so that we can see what's happening, and a
final line to return a boolean result:

def is_perfect_number( candidate_number:int, divisors:list[ int ] )->bool:
    perfects = []
    divisors.pop ()
    if sum (divisors) == candidate_number:
        perfects.append ( candidate_number )
    divisors.clear ()
    print( candidate_number, perfects )
    return bool( perfects )

Run the tests and this is the result:

6 []
Should be True : False
4 []
Should be False : False

Uh-oh!

Why is there a call to the pop() list-method? Let's jump into the Python
REPL (a terminal running Python):

>>> divisors = [ 1, 2, 3 ]
>>> divisors.pop()
3
>>> divisors
[1, 2]

Is this what we want?

So, after the .pop() when the sum() is calculated the list is missing
the 3 - yet the "perfect number" formula is: 1 + 2 + 3 = 6

Let's get rid of the pop() line and re-run the tests:

6 [6]
Should be True : True
4 []
Should be False : False

Additionally, as a matter of style, see the two parameters to the
function, and then how they are used in the if-statement? My mind wants
to see them used in the same order. Thus:

def is_perfect_number( candidate_number:int, divisors:list[ int ] )->bool:
    ...
    if candidate_number == sum( divisors ):
    ...

Note: this "consistency" was less apparent before I added the idea of a
separate function.


Thinking a bit more, why do we need the if-statement at all? Could we
turn the if's condition-clause into a return statement and remove
everything else from the function?

(and now some people will discuss: if the function is essentially only a
return-statement, why have the function at all? An answer is "testing"!)


Happy with that?

Let's turn our attention to the first function. There's not much to be
done here apart from copying the original code. However, I've changed
"j" into "candidate_divisor" because (again) that is a more descriptive
name.

Please refer to my comment (above) about whether or not to include zero
in a range. Does it 'hurt' (the extra execution time is negligible) or
create an incorrect result?

Another thought: is the maximum value of a divisor the
"number_to_be_considered" or could you halve the number of times that
loop is run?

The finished code has moved all of the adjustments to a range() function
to the range() call, rather than having "intermediate-variables".

I must admit, because the ranges all start from one, I might replace
range() with a Python generator which returns "positive integers" up to
a specified maxima, because that call will read more smoothly than
range( 1, ... ). However, if you don't know about generators, that's an
unnecessary complication.

Similarly, the code within find_divisors() could be reduced to a single
list-comprehension - but only if we are all comfortable with that
advanced-Python technique!


My version now consists of:

def num_perf_inf( last_number_to_be_considered:int ):
    for number_to_be_considered in range( 1,
last_number_to_be_considered + 1 ):
        divisors = find_divisors( number_to_be_considered )
        if is_perfect_number( number_to_be_considered, divisors ):
            print( number_to_be_considered )

find_divisors() with its explicit loop, is about five lines long
is_perfect_number() is one line, as mentioned
the perfects list has been dropped because we may as well print each
"perfect number" as it is 'found' rather than saving them and printing
them all at the end. (according to the spec[ification] as-presented)

Thus:

num_perf_inf (28)

results:

6
28


I haven't given you a copy of all of my code. You will only learn if you
do it yourself!


So, to wrap-up: Before considering if Python has made a mistake, it is
worth breaking down the larger problem into smaller units of code
("subroutines"), and checking that each of them works (correctly).

Alternately, I follow a practice called "TDD" (Test-Driven Development),
whereby each large problem is broken-down into subroutines, and then
each subroutine is individually created and tested. Further tests will
ensure that as the smaller-units are combined, they work-together
correctly. Finally, when all are combined, we expect that the entire
system will work.

The original code appeared to work, but one small part (ie do these
divisors reveal that the number is "perfect"?) was not correct.
Accordingly, the whole program[me] failed.

The (great) people who create the Python language and its interpreter
did not let us down!
-- 
Regards,
=dn


More information about the Python-list mailing list