Unidade 3 - Estruturas elementares em Python

Vamos começar a escrever algoritmos com a linguagem de programação Python. Enquanto você lê o texto desta unidade, você deve acompanhar os exemplos utilizando um console Python. Mas não se contente em apenas copiar os exemplos! Experimente escrever outras instruções análogas e vá além do que está escrito. Após a leitura do conteúdo abaixo, você deve ler todos os capítulos de 1 até 3 do tutorial Python. Você pode deixar para ler a subseção 3.1.3 depois de falarmos sobre listas, na próxima unidade.

Variáveis, tipos e operações

Suponha que precisamos somar dois números de vários algarismos, digamos, 124682 e 2468. Esses números são pequenos, então você pode facilmente escrevê-los no papel, alinhados pelo último dígito, e executar o algoritmo de soma tradicional.

  124682
+   2468
--------
  127150

Melhor ainda, para esse problema pode ser razoável tomar uma calculadora de mesa. Uma calculadora nada mais é do que um computador que realiza operações aritméticas e em que escrevemos as instruções diretamente no teclado. É muito fácil usar uma calculadora, mas ela tem a desvantagem de que não faz muito mais do que operações aritméticas. Se quisermos utilizar um computador moderno, então precisamos decidir duas coisas:

  1. Em que linguagem de programação escreveremos nossas instruções?
  2. Qual o mecanismo utilizaremos para executar essas instruções?

A resposta da primeira pergunta para esta disciplina é Python 3. Como Python é uma linguagem interpretada, a resposta da segunda pergunta é invocando um interpretador. Existem duas maneiras de invocar o interpretador do Python, interativa e não interativa.

Interpretador de comandos interativo

Usamos o interpretador de comandos interativo quando queremos realizar operações simples e curtas apenas uma vez. Muitas vezes, também utilizamos esse interpretador para experimentar e explorar a linguagem, alguns algoritmos ou alguma biblioteca de funções existente. Para isso, primeiro invocamos o interpretador de comandos python3 sem argumentos a partir do terminal de comandos de seu computador.

user@host:~$ python3

Iremos ver o console do Python 3 em que podemos digitar instruções, como

Python 3.7.3 (default, Oct  7 2019, 12:56:13)
[GCC 8.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>

Podemos digitar expressões aritméticas usuais (soma, subtração, divisão e multiplicação) nesse console. Experimente várias delas.

>>> 124682 + 2468
127150

Ao contrário do computador moderno, a nossa calculadora é bem limitada. Você deve saber que uma das principais limitações da calculadora é que a memória dela é muito limitada — só armazena os operandos e, normalmente, um resultado anterior. Se você tiver realizando operações com vários números, você precisa anotar cada resultado intermediário no papel. Em Python, podemos criar uma variável. Por exemplo, o índice de massa corporal (IMC) é dado pela fórmula

$$ \mbox{IMC} = \frac{{\mbox{massa}}}{({\mbox{altura}}\cdot {\mbox{altura}})}. $$

Então podemos calcular nosso índice como

>>> peso = 73
>>> altura = 1.75
>>> imc = peso / (altura * altura)
>>> imc
23.836734693877553

Devemos ler “peso recebe 73”, “altura recebe 1.75”, etc. O que as três primeiras linhas fazem é criar variáveis que guardam os valores do lado direto. Lembrem-se: uma variável corresponde a uma caixa na memória que guarda um valor de um determinado tipo. Cada uma dessas três linhas faz o seguinte:

Observe que apenas após a última linha, o interpretador mostra algum resultado. O motivo é que o console interativo só mostra o resultado de expressões, e uma atribuição não é uma expressão em Python.

Tipos de variáveis

Você deve se lembrar de que todo valor armazenado na memória do computador tem um tipo associado. Para descobrir o tipo associado a cada uma das variáveis, fazemos o seguinte:

>>> type(peso)
<class 'int'>
>>> type(altura)
<class 'float'>
>>> type(imc)
<class 'float'>

Vemos que peso tem tipo int. Um tipo int corresponde a um número inteiro (positivo, negativo ou zero). Em Python 3 (mas não em Python 2), um número inteiro pode ter quantos dígitos forem necessários. Experimente, por exemplo, calcular a décima potência de -99999999 em uma calculadora comum. Certamente ela terminará com um erro. Em Python, podemos fazer o seguinte:

>>> -99999999 ** 10
-99999990000000449999988000000209999997480000020999999880000000449999999000000001

Você acabou de aprender que ** corresponde ao operador de exponenciação de Python. Mas tem algo estranho: a potência par de um número não pode ser negativa, mas o resultado obtido foi negativo! Na verdade, o que acabamos de calcular foi -(99999999 ** 10). O operador ** tem precedência ou prioridade sobre o operador de negação -. Portanto, deveríamos ter escrito

>>> (-99999999) ** 10
99999990000000449999988000000209999997480000020999999880000000449999999000000001

Já as variáveis altura e imc têm tipo float. Um float é um tipo numérico de ponto flutuante, que é utilizada para guardar uma aproximação de um número real. Observe que falamos ponto, e não vírgula: ao contrário do português, na maioria das linguagens de programação, utilizamos um ponto . para indicar a parte fracionária de um número.

O fato de que guardamos uma aproximação ao usarmos um número fracionário é importante. Por exemplo, é evidente que a soma 0.1 + 0.2 deve valer exatamente 0.3, mas o interpretador Python parece discordar.

>>> 0.1 + 0.2
0.30000000000000004

Ao contrário dos números inteiros, em que sempre guardamos uma representação exata no número, não é possível guardar uma representação exata de cada número real. Pense, por exemplo, em como representar o número $\pi$ e cada um dos outros números irracionais: não podemos enumerar todos os números irracionais! Para a maioria das aplicações que veremos, usar uma aproximação é mais do que suficiente. Mas devemos tomar muito cuidado sempre que

  1. compararmos números de ponto flutuante; ou
  2. armazenarmos números astronomicamente grandes e pequenos.

Para utilizar melhor uma aproximação de ponto flutuante, precisamos entender um pouquinho sobre como esses valores são armazenados. As variáveis de ponto flutuante são armazenadas guardando-se os valores de três números inteiros, sinal, mantissa e expoente, que correspondem ao número racional $$ (-1)^{sinal} \cdot mantissa \cdot 2^{expoente}$$

Por exemplo, podemos reescrever o número $0.5$ como $0.5 = (-1)^0 \cdot 1 \cdot 2^{-1}$. Tente identificar sinal, mantissa e expoente. No entanto, não podemos reescrever o número $0.1$ dessa forma de maneira exata. O motivo é que a mantissa é representada na memória como um número em binário. Qual é o número em binário que corresponde ao número decimal $0.1$? Descubra usando a calculadora de seu computador.

Para a programadora, normalmente é indiferente essa representação, desde que ela se atente ao fato de que cada número de ponto flutuante é uma aproximação de algum número real sendo representado!

É claro que o tipo da variável peso é inteiro, pois o valor atribuído é 73. Podemos forçar a utilização de float adicionando um ponto depois do número:

>>> peso_float = 73.
>>> type(peso_float)
<class 'float'>

Mas por que imc tem tipo float? O motivo disso é que a expressão do lado direito é avaliada para um número de ponto flutuante: sempre que dividimos dois números, obtemos um float em Python 3, mesmo que a divisão seja exata! Se quisermos obter apenas o quociente inteiro de uma divisão, usamos o operador //.

>>> 5 / 2
2.5
>>> 6 / 2
3.0
>>> 6 // 2
3

Quanto vale 5 // 2?

Erros

Quando programamos, pode ocorrer uma série de erros. Um dos erros mais comuns é o erro de sintaxe e pode acontecer mesmo com operações bem simples como as que aprendermos. Por exemplo, se nos esquecermos de um operador.

>>> imc = peso / (altura  altura)
  File "<stdin>", line 1
    imc = peso / (altura  altura)
                               ^
SyntaxError: invalid syntax

Preste atenção na mensagem de erro. O símbolo ^ está apontando para o elemento do programa (token) que não era esperado naquela posição. De fato, ali deveríamos ter um símbolo de multiplicação *.

Outro erro de programação bastante comum é utilizar uma variável não definida, algumas vezes porque escrevemos o nome da variável com a grafia incorreta.

>>> imc = peso / (altura * autura)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'autura' is not defined

Outros erros são chamados erros de execução e acontecem apenas no momento da execução. Por exemplo, se tivéssemos digitado um valor inválido para a altura:

>>> peso = 73
>>> altura = 0
>>> imc = peso / (altura * altura)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero

Os erros acima são chamados de exceções. Quando uma exceção ocorre, o interpretador para a execução do seu programa com uma mensagem de erro e se recursa a continuar. Algumas vezes, é preciso tratar exceções, mas por agora não falaremos disso. Nos nossos primeiros programas, as exceções indicam simplesmente que o programa está incorreto é deve ser corrigido.

Finalmente, podemos ter erros de lógica, que são erros que não são detectados pelo interpretadores. Ao contrário dos erros anteriores, pode ser difícil encontrar esse erro e, algumas vezes, um erro de lógica pode passar desapercebido por muito tempo. Já vimos esse problema com a precedência acima. Vamos ver outro caso de descuido com a ordem das operações:

>>> peso = 73
>>> altura = 1.75
>>> imc = peso / altura*altura
>>> imc
73.0

Alguém que tomasse esse resultado como correto iria com certeza estar bastante preocupado com sua saúde!

Um parênteses: módulos

Uma razão comum para se usar uma sessão interativa do interpretador é experimentar com novos módulos e funções. Não vamos ver como criar módulos agora, mas podemos desde já utilizá-los. Suponha que queremos calcular a raiz quadrada de um número. Não existe operador em Python dedicado para isso, mas podemos importar esse operador de um módulo math. Por exemplo, para calcular o tamanho da diagonal de um retângulo, fazemos o seguinte:

>>> import math
>>> cateto1 = 10
>>> cateto2 = 5
>>> hipotenusa = math.sqrt(cateto1**2 + cateto2**2)
>>> hipotenusa
11.180339887498949

Uma dica que vale ouro. Enquanto estiver explorando nas sessões interativas, as teclas para cima e para baixo navegam no histórico de comandos digitados. Além disso, você pode usar o tab para completar uma palavra, depois de escrever o prefixo. Por exemplo, para ver quais nomes ou operadores estão disponíveis no módulo importado, digite math. e aperte tab duas vezes. Para ver o que cada operação faz, use help. Executar help(math.cos) irá mostrar a documentação dessa operação. Talvez seja necessário digitar q para sair da documentação.

Escrevendo um programa

Embora o modo interativo do interpretador Python seja útil, ele só serve em situações limitadas, quando queremos executar as instruções uma única vez. Na maior parte das vezes, quando queremos executar instruções Python, primeiro escrevemos um programa em Python. Os programas em Python também são chamados de script, em parte porque Python é utilizado muitas vezes com ferramenta de automação de tarefas cotidianas, em parte para distinguir de outros programas já compilados em linguagem de máquina.

Vamos construir um programa que

  1. pede para o usuário digitar um nome
  2. imprime uma mensagem de bom dia para o usuário

Criamos o seguinte programa, i.e., criarmos um arquivo de texto chamado bomdia.py e digitamos

usuario = input("Digite seu nome: ")
print("Bom dia", usuario)

Após salvar o arquivo, podemos executar o programa invocando o interpretador Python em um terminal de execução. Esse programa irá imprimir uma mensagem pedindo o nome. Ao escrevermos algum texto, digamos, Maria, e apertarmos Enter, obteremos o seguinte terminal

user@host:~/$ python3 bomdia.py
Digite seu nome: Maria
Bom dia Maria

No programa vemos dois comandos que fazem parte da Linguagem Python. A função input lê um texto digitado pelo usuário no teclado até o momento em que ele digita a tecla Enter. O que essa função devolve é uma variável que guarda a sequência de caracteres digitada pelo usuário. Essa variável, que é do tipo string, ou str, é associada ao identificador usuario, que é o nome do usuário.

A função print recebe uma sequência de argumentos, cada um pode ter um valor e um tipo diferente. O que ela faz é mostrar (ou imprimir) na tela os valores na forma de texto, isso é, de uma string. Se houver vários argumentos, a função print irá mostra um espaço entre eles.

Strings

No código acima, temos "Bom dia" que é uma string literal. As strings literais representam valores do tipo str que não mudam e correspondem à sequência de caracteres entre as aspas. Se quisermos inserir um caractere de aspas dentro da string, precisamos escapar do significado especial que esse caractere tem utilizando uma contra-barra, ou backslash, antes.

Por exemplo, considere um programa pensamento.py com o seguinte conteúdo

frase = "Descartes disse:\n\"penso, logo existo\""
print(frase)

O primeiro comando de pensamento.py cria uma string com aspas. Poderíamos também utilizar aspas simples,

frase = 'Descartes disse:\n"penso, logo existo"'
print(frase)

já que o caractere " não têm significado especial dentro da string literal. Se executarmos esse programa, iremos obter

Descartes disse:
"penso, logo existo"

Repare que o \n não foi impresso, mas sim foi mostrada uma quebra de linha: de fato, \n é um caractere de controle que instrui o terminal a saltar uma linha. Se quiséssemos a própria barra, deveríamos escapar do significado especial da barra, usando

frase = 'Descartes disse:\\n"penso, logo existo"'
print(frase)

que imprimiria

Descartes disse:\n"penso, logo existo"

Operações de string

Assim como podemos fazer operações com números inteiros e números de ponto flutuante, também podemos fazer várias operações com string. Uma operação comum é criar uma substring ou acessar um determinado caractere. Veja o exemplo:

frase = "O essencial é invisível aos olhos"
print("O primeiro caractere é: ", frase[0])
print("O segundo é um espaço: ", frase[1])
print("A segunda palavra é: ", frase[2:11])
print("Um sufixo é:", frase[16:])
print("A última palavra é:", frase[-5:])

Executando o código acima, obtemos

O primeiro caractere é:  O
O segundo é um espaço:
A segunda palavra é:  essencial
Um sufixo é: visível aos olhos
A última palavra é: olhos

Execute o exemplo, releia o código e reflita. Procure entender porque essa saída é mostrada e procure a documentação se necessário.

Uma outra operação comum com strings é a concatenação. Por exemplo, poderíamos reescrever o exemplo acima da maneira abaixo — dessa vez, usando a pontuação adequadamente.

usuario = input("Digite seu nome: ")
mensagem = "Bom dia, " + usuario + "."
print(mensagem)

Reutilizamos o símbolo +, mas perceba que nesse caso ambos operandos são strings. Assim, o significado de + é concatenar as duas strings, em ordem. Qual a saída será obtida?

Convertendo tipos

Vamos agora usar o que aprendemos para criar um programa que calcula a hipotenusa de um triângulo retângulo. Escrevemos o seguinte

import math

cateto1 = input()
cateto2 = input()

hipotenusa = math.sqrt(cateto1*cateto1 + cateto2*cateto2)

print("A hipotenusa é " + hipotenusa)

Como você já deve imaginar, esse programa não funciona. Experimente. O motivo é fácil de entender: ninguém garante que o usuário irá escrever dois números válidos! E de fato, input apenas lê uma sequência de caracteres e não tenta interpretá-la de nenhuma maneira. O tipo de ambos cateto1 e cateto2 no programa acima é str. Ou seja, são strings: não podemos multiplicar duas strings!

Para que nosso programa funcione, precisamos que o valor das variáveis que representam os catetos sejam números de ponto flutuante. Mas como transformar uma string, que contém a sequência de dígitos decimais e, possivelmente um ponto, no número fracionário que ele representa em ponto flutuante? Para isso, usamos uma função de conversão para float.

import math

cateto1 = float(input())
cateto2 = float(input())

hipotenusa = math.sqrt(cateto1*cateto1 + cateto2*cateto2)

print("A hipotenusa é " + hipotenusa)

Agora sim, a hipotenusa será calculada corretamente! Mas o programa ainda está incorreto. O erro é que tentamos fazer uma concatenação com um operando que não é uma string! Para converter um valor em uma string, usamos a função str. Corrija esse programa usando conversão de float para string e depois fazendo a concatenação adequada!

Formatando a saída

Em Python 3, existe uma maneira bem conveniente de converter valores em strings, que são as strings de modelo para formatação. A ideia é escrever um texto e deixar marcações, placeholders, para substituir com o valor formatado. Por exemplo, se quiséssemos sempre escrever o valor da hipotenusa com duas casas decimais, poderíamos fazer o seguinte

import math

cateto1 = float(input())
cateto2 = float(input())

hipotenusa = math.sqrt(cateto1*cateto1 + cateto2*cateto2)

print("Os catetos são {} e {}!".format(cateto1, cateto2))
print("A hipotenusa é {:0.2f}.".format(hipotenusa))

Procure na documentação as várias formas de usar format. Nas versões mais novas de Python, há também uma maneira mais compacta de escrever esse código, usando as f-strings:

import math

cateto1 = float(input())
cateto2 = float(input())

hipotenusa = math.sqrt(cateto1*cateto1 + cateto2*cateto2)

print(f"Os catetos são {cateto1} e {cateto2}!")
print(f"A hipotenusa é {hipotenusa:0.2f}.")

Repare que as f-strings começam com um símbolo f.

Comandos condicionais

Até agora já sabemos ler e escrever dados de entrada e saída, realizar uma sequência de operações sobre esses dados e armazenar esses dados em variáveis. Pode ser que a sequência de operações a serem realizadas dependa dos valores das variáveis, então precisamos também das estruturas condicionais. Vamos resolver o seguinte exercício.

Escreva um programa que lê um número inteiro do teclado e imprime “sim” se o número for par e maior do que 10, ou for ímpar e menor do que 50. Caso contrário o programa deve imprimir “não”.

Primeiro, vamos tentar escrever um algoritmo em português:

numero ← leia um número do teclado
se número for par
    se numero > 10
        imprima sim
    senão
        imprima não
do contrário, se for ímpar
    se numero < 50
        imprima sim
    senão
        imprima não

Antes de continuar, simulamos o algoritmo rapidamente para verificar se não escrevemos alguma coisa errada. Como esse algoritmo é muito simples, depois de um par de testes, conseguimos nos convencer de que ele está correto. Agora, reescrevemos o algoritmo, dessa vez em Python.

numero = int(input())
if numero % 2 == 0:
    if numero > 10:
        print("sim")
    else:
        print("não")
else:
    if numero < 50:
        print("sim")
    else:
        print("não")

Há muitas novidades aqui, vamos parte a parte. Primeiro, o recuo em que os comandos são escritos é fundamental para identificar a estrutura do algoritmo. Temos duas palavras-chaves novas: if e else. O comando if significa se e é seguido por uma condição. Se essa condição for verdadeira, e somente nesse caso, o corpo de comandos que segue os dois pontos e que está recuado é executado. O comando else é opcional; o corpo desse comando é executando sempre que a condição do if falhar.

Ah, Python não tem uma instrução nativa para verificar se um número é par: o que fizemos é verificar se o resto da divisão por dois é zero. Como você faria para verificar se um número é múltiplo de três?

Valores booleanos

Para debulhar cada pedaço do programa acima, precisamos entender o que é uma condição. Uma condição é uma expressão cujo resultado é um valor do tipo bool. Um valor do tipo bool, por sua vez, é um valor de verdade que pode ser ou True ou False e nada mais.

Obtemos valores booleanos normalmente fazendo perguntas! Em uma linguagem de programação, essas perguntas estão na forma de comparações e resultados de funções. Por exemplo, se quisermos verificar se o usuário digitou apenas números decimais, podemos escrever

numero_string = input()
if numero_string.isdigit():
    print("O texto contém apenas dígitos decimais.")
    numero = int(numero_string)
else:
    print("O texto não é um número decimal válido.")

Poderíamos reescrever o programa anterior da seguinte maneira:

numero = int(input())

par = numero % 2 == 0
maior_10 = numero > 10
menor_50 = numero < 50

if par:
    if maior_10:
        print("sim")
    else:
        print("não")
else:
    if menor_50:
        print("sim")
    else:
        print("não")

O tipo dessas três novas variáveis é bool. Confira usando um console Python e digitando

>>> var = True
>>> type(var)
>>> par = numero % 2 == 0
>>> type(var)

Operadores booleanos

Às vezes, queremos fazer duas perguntas ao mesmo tempo, outras vezes precisamos satisfazer apenas uma de várias condições. Pensando nisso, vamos reescrever o programa.

numero = int(input())

par = numero % 2 == 0
maior_10 = numero > 10
menor_50 = numero < 50

if par and maior_10:
    print("sim")
elif not par and menor_50:
    print("sim")
else:
    print("não")

Observamos algumas novas palavras-chaves: a primeira é and (a conjunção e escrita em inglês) que significa que queremos que ambas condições sejam verdadeiras, a da esquerda e a da direita. A segunda é not (não em inglês), que nega o valor de verdade de uma expressão. A última é elif, que significa, do contrário, verifique a condição e execute. O elif vem da contração de else com if e nada mais é do que uma forma mais compacta de escrever um código equivalente:

numero = int(input())

par = numero % 2 == 0
maior_10 = numero > 10
menor_50 = numero < 50

if par and maior_10:
    print("sim")
else:
  if par and menor_50:
      print("sim")
  else:
      print("não")

Mas, como essa sequência de if seguido de else é bastante comum, fica mais fácil escrever todos os corpos de comando no mesmo recuo. Podemos usar quantos elif após um if quantos forem necessários.

No nosso exemplo, o corpo de if e elif são iguais. Assim, podemos simplificar ainda mais o programa acima.

numero = int(input())

par = numero % 2 == 0
maior_10 = numero > 10
menor_50 = numero < 50

if par and maior_10 or not par and menor_50:
    print("sim")
else:
    print("não")

A palavra-chave or (ou em inglês) é um operador que devolve verdadeiro bastando que pelo menos um dos seus operandos seja True.

Nem sempre é fácil entender qual a ordem em que as operações serão executadas. Para isso, é necessário conhecer a precedência dos operadores (e um bocado de experiência). No caso do if acima, not é executado primeiro e or é executado por último. Em expressões complicadas, como a acima, é sempre mais claro (e mais prático), usar parênteses:

numero = int(input())

par = numero % 2 == 0
maior_10 = numero > 10
menor_50 = numero < 50

if (par and maior_10) or (not par and menor_50):
    print("sim")
else:
    print("não")

O nome booleano, que em Python corresponde a bool, é uma homenagem a um matemático George Bool, a quem é frequentemente atribuída a criação da álgebra booleana. Por ora, basta conhecermos bem as chamadas tabelas-verdades das operações:

A not A
True False
False True
A B A and B
True True True
True False False
False True False
False False False
A B A or B
True True True
True False True
False True True
False False False

Relembrar algumas equivalências bem conhecidas também é importante:

E refletir sobre algumas formas que às vezes passam desapercebido:

Essas últimas duas equivalências são conhecidas como Teoremas de De Morgan.

Repetindo condicionalmente

O corpo de comandos das construções if e else pode ser executado uma ou nenhuma vez dependendo do valor da condição no momento da execução. Muitas vezes, queremos repetir um conjunto de comandos enquanto determinada condição é satisfeita.

As várias linguagens de programações têm as mais diversas construções para expressar comandos repetitivos, mas quase sempre identificamos pelo menos duas formas mais comuns que chamamos anteriormente de iteração condicional e iteração limitada. A seguir, vamos ver como utilizar essa primeira forma, mas veremos mais sobre comandos e algoritmos repetitivos depois. Por enquanto, resolvamos um exercício:

Escreva um programa que leia uma sequência de pares de números inteiros e pare apenas quando a soma de ambos for 42.

Aqui está como eu resolveria esse exercício.

print("Escreva dois números")
a = int(input())
b = int(input())
soma = a + b

while soma != 42:
    print("Escreva dois números")
    a = int(input())
    b = int(input())
    soma = a + b

print(f"Parabéns, a soma de {a} e {b} é um número fundamental")

Há uma novidade: um comando while (enquanto em inglês) tem a mesma forma de um comando if, mas é um comando repetitivo. Assim como o if, o while precede uma condição e é seguido por um corpo de comandos recuado à direita. No entanto, o corpo de comandos é executado repetidamente enquanto a condição avaliada for verdadeira. Uma consequência disso é que o corpo de comandos do while deve mudar o valor das variáveis que afetam a condição.

Enquanto o programa acima executa corretamente, há um aspecto desagradável nesse código: escrevemos as mesmas instruções duas vezes. Como sempre precisamos executar pelo menos uma vez, podemos substituir a expressão da condição por uma variável booleana, assim:

procurando = True

while procurando:
    print("Escreva dois números")
    a = int(input())
    b = int(input())
    soma = a + b

    if soma == 42:
        procurando = False

print(f"Parabéns, a soma de {a} e {b} é um número fundamental")

Utilizamos while porque não sabemos quantas vezes iremos executar o corpo de comandos, ou, em outros termos, não sabemos quantas iterações serão executadas. Pode acontecer também que nenhuma iteração seja executada.

Escreva um programa que leia dois números e calcule o quociente da divisão usando apenas operações de soma e subtração.

O que queremos nesse exercício é mostrar que mesmo que Python não tivesse o operador de divisão inteira //, poderíamos calcular o quociente — embora de forma muito mais lenta!

print("Escreva dois números")
dividendo = int(input())
divisor = int(input())
quociente = 0

while dividendo >= divisor:
    quociente += 1
    dividendo -= divisor

print(f"O quociente da divisão é {quociente}.")

Há dois operadores estranhos, mas inofensivos: += e -=. Eles servem só para simplificar atribuições. Escrevemos quociente += 1 ao invés de quociente = quociente + 1 e dividendo -= divisor ao invés de dividendo = dividendo - divisor. Beleza, continuemos.

Pare um pouquinho para entender esse algoritmo e tente convencer algum colega (aquele seu amigo cético) de que o algoritmo está correto. Para começar a entender esse algoritmo, temos que testar com alguns números de entrada. Experimente simular tentando dividir 6 por 2, depois 20 por 3. Depois tente 3 por 20.

A operação inversa da divisão é a multiplicação. De novo, vamos fingir que Python não tem operadores de multiplicação.

Escreva um programa que leia dois números e calcule o produto usando apenas a operação de soma.

Não é difícil escrever um algoritmo que faça isso: basta começar com zero e somar um multiplicando repetidas vezes. A diferença é que agora nós sabemos precisamente quantas vezes devemos executar uma iteração antes de começar. Python tem uma estrutura bastante apropriada para essa situação, mas nada nos impede de usar while para resolver esse exercício. Faça isso: escreva um algoritmo em português, traduza esse algoritmo para Python, depois simule seu algoritmo com alguns exemplos de entrada, usando lápis e papel.