5  Controle de fluxo e iteração

5.1 Introdução

Até este ponto do curso, trabalhamos com objetos e operações básicas em Python, executando instruções de forma sequencial, isto é, uma após a outra. No entanto, programas reais raramente seguem apenas uma sequência linear de comandos. Em aplicações práticas – como filtragem de dados, simulações, cálculos iterativos ou implementação de regras de decisão – é necessário que o programa seja capaz de executar instruções com base em condições e repetir determinadas operações múltiplas vezes. Essas tarefas são realizadas por meio das estruturas de controle de fluxo.

Figura 5.1: Setup usual de um computador pessoal no começo dos anos 2000. Fonte: Wikimedia Commons

Neste capítulo, serão apresentados os principais mecanismos de decisão e repetição em Python: a construção de expressões booleanas, o uso de estruturas condicionais, os laçõs de repetição (loops, em inglês), instruções de controle que indicam um ponto de parada ou continuação do código, e as chamadas list comprehensions como forma compacta de iteração. Esses instrumentos formam o núcleo da programação imperativa no Python e são indispensáveis para qualquer aplicação computacional em Economia e análise de dados.

5.1.1 Pré-requisitos

Embora as funções e estruturas necessárias para a implementação de expressões condicionais e loops no Python sejam nativas na linguagem, para algumas das aplicações utilizaremos um dos pacotes externos que nos permitem trabalhar com números aleatórios, o NumPy. Além disso, em alguma aplicações utilizaremos o pacote time para contagem de tempo de máquina.

  • numpy
  • time
import numpy as np
import time 

5.2 Execução condicional

5.2.1 Expressões booleanas

Antes de conseguirmos controlar o fluxo de execução, é preciso aprender a definir condições através das expressões booleanas. Essas expressões exprimem condições e comparações que resultam em um valor booleano, True ou False, e são resultado da combinação entre operadores relacionais, que comparam valores, e operadores lógicos, que nos permitem encadear comparações. Os operadores relacionais utilizados na linguagem são:

  • x == y: x é igual a y
  • x != y: x não é igual a y
  • x > y: x é maior que y
  • x < y: x é menor que y
  • x >= y: x é maior ou igual a y
  • x <= y: x é menor ou igual a y

Os exemplos a seguir usam o operador de igualdade para comparara dois operandos quaisquer, que podem ser simples valores ou mesmo objetos.

5 == 5
True
5 == 6
False
lista1 = [1,2,3,4,5]
lista2 = [5,4,3,2,1]

lista1 == lista2
False

Há três operadores lógicos: and, or e not. O significado destes operadores é semelhante ao seu significado em inglês. Por exemplo, x>=0 and x<=10 só é verdade se x for maior que 0 e x for menor que 10. n%2 == 0 or n%3 == 0 é verdadeiro se uma ou as duas condição(ões) for(em) verdadeira(s), isto é, se o número n for divisível por 2 ou 3 (caso você não esteja familiarizado com a divisão pelo piso e o operador módulo, tente brincar um pouco com // e com %). Finalmente, o operador not nega uma expressão booleana, então not (x > y) é verdade se x > y for falso, isto é, se x for menor que ou igual a y.

Falando estritamente, os operandos dos operadores lógicos devem ser expressões booleanas, mas o Python não é muito estrito. Qualquer número que não seja zero é interpretado como True:

42 and True
True

Esta flexibilidade tem sua utilidade, mas há algumas sutilezas relativas a ela que podem ser confusas. Assim, pode ser uma boa ideia evitá-la (a menos que você tenha certeza absoluta do que está fazendo).

5.2.2 Operador condicional

Para escrever programas úteis, quase sempre precisamos da capacidade de verificar condições e mudar o comportamento do programa de acordo com elas. Instruções condicionais nos dão esta capacidade. A forma mais simples é a instrução if, que tem a seguinte sintaxe básica:

if <condição>:
    <instruções>

A expressão booleana depois do if é chamada de condição. Se for verdadeira, o código identado e presente em <instruções> é executado. Caso o resultado da expressão booleana em <condição> seja False, o código em <instruções> não é executado e o script segue.

x=5

if x > 0:
    print('x é positivo')
x é positivo
ImportanteImportante

Uma característica muito importante da sintaxe do Python é justamente a identação. Diferentemente de outras linguagens de programação, a identação exerce papel importante aqui, já que é através dela que se determina onde se inicia e onde termina um bloco de código, que pode ser uma expressão condicional ou mesmo uma função, como veremos mais a frente. Além de economizar várias chaves (“{” e “}”) – elementos que delimitam blocos de código em outras linguagens, como o R – a identação exerce um papel estético importante ao permitir uma melhor visualização do código como um todo.

Uma segunda forma de utilizar a instrução if é a chamada execução alternativa, na qual há duas possibilidades e a condição determina qual será executada.

idade = 17

if idade >= 18:
    print('Indivíduo é maior de idade')
else:
    print('Indivíduo é menor de idade')
Indivíduo é menor de idade

Se o valor atribuído ao objeto idade for maior ou igual a 18, então sabemos que o indivíduo já atingiu a maioridade e o programa exibe a respectiva mensagem. Se a condição for falsa, o segundo conjunto de instruções é executado, informando que o indivíuo é menor de idade. Como a condição deve ser verdadeira ou falsa, exatamente uma das alternativas será executada. As alternativas são chamadas de ramos (branches), porque são ramos no fluxo da execução.

5.2.3 Condicionais encadeadas

Às vezes, há mais de duas possibilidades e precisamos de mais que dois ramos de condições. Esta forma de expressar uma operação de computação é uma condicional encadeada.

x = 5
y = 6

if x < y:
    print('x é menor do que y')
elif x > y:
    print('x é maior do que y')
else:
    print('x e y são iguais')
x é menor do que y

Novamente, exatamente um ramo será executado. Não há nenhum limite para o número de instruções elif1. Se houver uma cláusula else, ela deve estar no fim, mas não é preciso haver uma. Cada condição é verificada em ordem. Se a primeira for falsa, a próxima é verificada, e assim por diante. Se uma delas for verdadeira, o ramo correspondente é executado e a instrução é encerrada. Mesmo se mais de uma condição for verdade, só o primeiro ramo verdadeiro é executado. Em condicionais encadeados, a ordem é importante.

5.3 Repetição simples

Os computadores muitas vezes são usados para automatizar tarefas repetitivas. A repetição de tarefas idênticas ou semelhantes sem cometer erros é algo que os computadores fazem muito bem e as pessoas não. Em um programa de computador, a repetição também é chamada de iteração.

De maneira um pouco mais formal, iteração significa executar o mesmo bloco de código repetidamente, potencialmente muitas vezes. Uma estrutura de programação que implementa a iteração é chamada de loop. A forma mais simples de iteração é a chamada iteração definida, em que o número de vezes que o bloco designado será executado é especificado explicitamente no momento em que o loop é iniciado.

Para atingir o objetivo proposto pela iteração definida utilizaremos no Python a instrução for. Um loop for tem duas partes: um cabeçalho especificando a iteração, que termina em dois pontos, e um corpo identado que é executado uma vez por iteração. O corpo pode conter qualquer número de instruções, mas o Python só reconhecerá como parte do corpo o código que estiver identado em relação ao cabeçalho.

No Python o for tem a cara abaixo

for <elemento> in <objeto>:
    <instruções>

Note que <objeto> em geral se refere à uma sequência, seja ela uma lista, uma tupla, um string ou qualquer outro objeto pelo qual seja possível percorrer. Para cada <elemento> dentro da sequência <objeto> o conjunto de instruções em <instruções>será executado. Após percorrer todos os elementos pertencentes à sequência <objeto>, o Python para a execução do loop.

Imagine agora que estejamos interessados em criar um for loop que printa 10 números aleatórios sorteados de uma distribuição uniforme que vai de 0 a 1. Utilizarei para esse exemplo o objeto range(), a lógica de listas e a função uniform do NumPy, geradora de números aleatórios distribuídos de acordo com uma Distribuição Uniforme padrão (definida no intervalo \([0,1]\)).

print(list(range(0,10)))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
for i in list(range(0,10)):
    
    u = np.random.uniform()
    print(u)
0.15630625435856316
0.7342573862724575
0.7844006617445016
0.35898873825024136
0.05799299684187942
0.8615242997273715
0.6266015487703178
0.6864396000308859
0.6710230074920631
0.9223583622144309

No loop acima não utilizamos o elemento i em nenhum momento,a lista nos serviu apenas para ditar o número de vezes que gostaríamos de rodar o bloco de instruções abaixo do cabeçalho, no caso 10. Podemos ir um pouco além.

for i in list(range(0,10)):
    
    u = np.random.uniform()
    string='Essa é a iteração {:.0f} e o número sorteado foi: {:.2f}'

    print(string.format(i,u)) 
Essa é a iteração 0 e o número sorteado foi: 0.45
Essa é a iteração 1 e o número sorteado foi: 0.92
Essa é a iteração 2 e o número sorteado foi: 0.27
Essa é a iteração 3 e o número sorteado foi: 0.38
Essa é a iteração 4 e o número sorteado foi: 0.61
Essa é a iteração 5 e o número sorteado foi: 0.23
Essa é a iteração 6 e o número sorteado foi: 0.81
Essa é a iteração 7 e o número sorteado foi: 0.92
Essa é a iteração 8 e o número sorteado foi: 0.24
Essa é a iteração 9 e o número sorteado foi: 0.27

Note que nesse caso utilizamos tanto o elemento iterável i, quanto o resultado de cada sorteio individual u.

5.3.1 Iteração definida e instruções condicionais

Podemos utilizar instruções condicionais dentro de um loop for também. Mais do que isso, é possível utilizar esse tipo de instrução para interromper a repetição do código, mesmo antes do Python percorrer todos os elementos da sequência alvo. Vejamos um exemplo da brincadeira do C e S, em que os jogadores definem um tema e perde o primeiro jogador a falar uma palavra relacionada ao tema que comece com a letra c ou com a letra s.

for i in ['banana','uva','cenoura','melão','salsinha','couve-flor']:
    
    cond1=i.startswith('c')
    cond2=i.startswith('s')

    if cond1 or cond2:
        break

    print(i)
banana
uva

Note que no caso acima, o terceiro elemento da lista ['banana','uva','cenoura','melão','salsinha','couve-flor'] começa com a letra ‘c’ mas os dois primeiros não. A expressão boleana após o if, portanto, retorna False nas duas primeiras iterações e True na segunda. No momento em que ela retorna True a instrução break é executada e o loop para aí! Dessa forma, apenas os dois primeiros termos da lista serão impressos pela instrução print.

5.3.2 Usando enumerate

Às vezes pode ser útil, dentro de um loop for, ter uma variável que muda em cada iteração do loop e que possibilite fazer um controle mais direto sobre qual iteração está sendo executada em determinado momento. Em vez de criar e incrementar uma variável você mesmo, você pode usar a função nativa enumerate() para obter um contador e o valor do objeto iterável ao mesmo tempo. Para deixar claro como funciona a sintaxe nesse caso, vamos trabalhar em cima de uma lista de strings.

str1 = 'Água mole em pedra dura, tanto bate até que fura'

# Algumas linhas de código adicionais para excluir pontuação e transformar o string em uma lista de palavras
str1 = str1.replace('.','')
str1 = str1.replace(',','')
str1 = str1.replace('?','')
lista1 = str1.split()

for c in lista1:
    print(c)
Água
mole
em
pedra
dura
tanto
bate
até
que
fura

Utilizando o enumerate() podemos, em uma mesma linha de código, printar não apenas cada string individual da lista, mas também a posição desse string dentro da lista!

for count,value in enumerate(lista1):
    
    print('Elemento '+str(count)+' = '+value)
Elemento 0 = Água
Elemento 1 = mole
Elemento 2 = em
Elemento 3 = pedra
Elemento 4 = dura
Elemento 5 = tanto
Elemento 6 = bate
Elemento 7 = até
Elemento 8 = que
Elemento 9 = fura

Uma outra forma de criar esses mesmos resultados seria utilizando a função nativa zip(). Essa função nos permite iterar ao longo de duas ou mais sequências ao mesmo tempo, contanto que as sequências possuam o mesmo comprimento. Porém, seu escopo de ação é muito mais geral já que nos permite percorrer dois objetos distintos ao mesmo tempo, sejam quais forem esses objetos. A única obrigatoriedade é que esses objetos tenham o mesmo tamanho.

professores = ['Danilo Souza','Danilo Souza','Artur Viaro','Artur Viaro']
turmas = ['2026101','2026102','2026121','2026122']

for p,t in zip(professores,turmas):
    
    print('Nesse curso, a turma '+t+' é de responsabilidade do professor '+p)
Nesse curso, a turma 2026101 é de responsabilidade do professor Danilo Souza
Nesse curso, a turma 2026102 é de responsabilidade do professor Danilo Souza
Nesse curso, a turma 2026121 é de responsabilidade do professor Artur Viaro
Nesse curso, a turma 2026122 é de responsabilidade do professor Artur Viaro

5.3.3 List comprehensions

List comprehensions é uma das várias formas que o Python nos oferece para de criar, alterar e manipular listas. Sua sintaxe é bastante concisa e nos permite olhar para um exerício de iteração através de algo parecido com uma fórmula. Imagine que estejamos interessados em criar uma lista que contenha a parte inteira da raiz quadrada de todo número de uma outra lista original. Usualmente faríamos isso utilizando um loop for:

lista1 = [1,4,9,16,25,36,49,64,81,100,121,144,169,196,225]
lista2 = []

for x in lista1:
    lista2.append(int(x**0.5))
    
print(lista2)
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]

Alternativamente, podemos utilizar list comprehensions através da seguinte sintaxe:

lista2 = [int(x**0.5) for x in lista1] 

print(lista2)
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]

Em termos práticos isso funciona como se fosse um loop definido dentro de uma única linha de código. Mas se fazem a mesma coisa, qual a diferença entre as duas formas? Vamos fazer um teste para esse exercicío da raiz quadrada, porém para uma lista maior. Utilizaremos também a função time do pacote time para calcular o tempo utilizado por cada um dos métodos.

lista1 = list(range(1,1000000))

# list comprehension
start_comp = time.time()

list_comp  = [int(x**0.5) for x in lista1]

end_comp   = time.time()


# loop
start_loop = time.time()

list_loop = []
for x in lista1:
    list_loop.append(int(x**0.5))  
    
end_loop   = time.time()

ratio = (start_loop - end_loop) / (start_comp - end_comp)

Qual abordagem será que levou menos tempo?

print('Tempo necessário com list comprehensions: {:.4f} segundos'.format(end_comp - start_comp))
print('Tempo necessário com loop: {:.4f} segundos'.format(end_loop - start_loop))
print('\nA abordagem de loop demorou {:.1%} mais tempo!\nDê uma chance para list comprehensions ;)'.format(ratio - 1))
Tempo necessário com list comprehensions: 0.0935 segundos
Tempo necessário com loop: 0.1261 segundos

A abordagem de loop demorou 34.8% mais tempo!
Dê uma chance para list comprehensions ;)

5.4 Repetição condicional

Em programação existem dois tipos de iteração, indefinidas e definidas:

  • Com iteração definida, o número de vezes que o bloco designado será executado é especificado explicitamente no momento em que o loop é iniciado. É o exemplo do for loop com o qual trabalhamos anteriormente.

  • Com iteração indefinida, o número de vezes que o loop é executado não é especificado explicitamente com antecedência. Em vez disso, o bloco designado é executado repetidamente enquanto alguma condição for atendida.

Para interação indefinida, a construção típica no Python é o loop while. A forma mais simples desse loop é a seguinte:

while <condição>:
    <instruções>

Em que <instruções> representam uma ou mais linhas de código a serem executadas. Da mesma forma que nas outras estruturas, a indentação determina quais linhas vão ser executadas enquanto a expressão boleana em <condição> for satisfeita. Quando um loop while é encontrado, o primeiro passo da linguagem é avaliar <condição>. Se for verdadeiro, o corpo do loop é executado. Em seguida, <condição> é verificado novamente e, se ainda for verdadeiro, o corpo é executado novamente. Isso continua até a expressão se tornar falsa, momento em que a execução do programa prossegue para a primeira instrução além do corpo do loop. É comum, dentro do corpo do loop, ter algum objeto que atualize elementos que são avaliados na expressão booleana do loop. Por vezes, esse objeto representa ou um determinado número máximo de iterações ou algum nível de tolerância que é atualizado a cada nova iteração. É preciso ter muita atenção a esse ponto para que o loop não fique rodando para sempre.2

Veja o que acontece no exemplo abaixo:

n = 5

while n > 0:
    n = n-1
    print(n)
    
print('\nÀ partir de agora as instruções fora do loop serão executadas.')
4
3
2
1
0

À partir de agora as instruções fora do loop serão executadas.

A expressão no cabeçalho do loop é n > 0, o que é True inicialmente já que 5 é um número positivo. Dentro do corpo do loop redefinimos o valor de n para que seja igual ao valor anterior de n menos 1, e então o novo valor de n é impresso. Quando o corpo do loop termina, a execução do programa retorna ao topo do loop na linha 2 e a expressão é avaliada novamente. Como n ainda é positivo, o corpo é executado novamente. Isso continua até n se tornar 0. Nesse ponto, quando a expressão é testada, ela é falsa (0 não é maior do que 0) e o loop termina. A execução é retomada na primeira instrução após o corpo do loop, no caso a instrução print.

A seguir temos um loop que usa uma lista como condição. Nesse caso, o Python entende, dentro de um contexto booleano, que a condição é verdadeira enquanto a lista não for vazia. Neste exemplo, a é uma condição verdadeira desde que a lista tenha elementos. Quando todos os itens forem removidos com o método .pop() e a lista estiver vazia, a será falsa e o loop terminará.

a = ['Eu','Tu','Ele','Nós','Vós','Eles']
print(a)

while a:
    a.pop(-1)
    print(a)
['Eu', 'Tu', 'Ele', 'Nós', 'Vós', 'Eles']
['Eu', 'Tu', 'Ele', 'Nós', 'Vós']
['Eu', 'Tu', 'Ele', 'Nós']
['Eu', 'Tu', 'Ele']
['Eu', 'Tu']
['Eu']
[]

5.4.1 Formas de Interromper um Loop

Em cada exemplo que você viu até agora, todo o corpo do loop while é executado em cada iteração. Python fornece duas palavras-chave que encerram uma iteração de loop prematuramente:

  • A instrução break encerra um loop imediatamente. Já vimos como ela funciona no caso do for loop e instruções condicionais.
n = 5

while n > 0:
    n = n - 1
    if n == 2:
        break
    print(n)
    
print('Loop ended.')
4
3
Loop ended.
  • A instrução continue, por outro lado, encerra imediatamente a iteração atual. A execução salta para o topo do loop e a expressão de controle é reavaliada para determinar se o loop será executado novamente ou terminará.
n = 5

while n > 0:
    n = n - 1
    if n == 2:
        continue
    print(n)

print('Loop ended.')
4
3
1
0
Loop ended.

5.5 Aplicação: Teorema Central do Limite

Esse exercício é o mais complexo que fizemos até agora no curso. Nele utilizamos grande parte dos conceitos que estudamos até aqui, desde listas a loops e também a importação de bibliotecas de comandos. Você pode pular essa parte por enquanto, mas aconselho que aos poucos você tente replicá-lo, de forma a entender todas suas etapas com o tempo.

O Teorema Central do Limite é um resultado super importante em estatística e com aplicações nas mais diversas áreas. Em sua formulação mais simples, o teorema pode ser resumido pela caixa abaixo.

DicaTeorema Central do Limite

Suponha que estejamos amostrando de uma variável aleatória \(X\) com média finita e igual a \(\mu\) e desvio padrão finito e igual a \(\sigma\). Independentemente de qual seja a distribuição original de \(X\), a distribuição da média amostral de \(X\) aproxima-se cada vez mais de uma distribuição normal conforme aumenta o tamanho das amostras para o cálculo da média. Nessa caso, média e desvio padrão da distribuição das médias amostrais são representados por:

\[\mu_{\bar{X}}=\mu\]

\[\sigma_{\bar{X}}=\frac{\sigma}{\sqrt{N}}\]

Vamos apresentar uma implementação desse Teorema em várias etapas. Na primeira etapa iremos sortear uma amostra de 10 números aleatórios entre -40 e 40. Assumirei que a distribuição original de \(X\) é uniforme e, portanto, utilizarei a função uniform() do NumPy para o sorteio dos números.

x = np.random.uniform(-40, 40, 10)

print(x)
[-10.87133541  -9.20989176 -30.03710394 -19.83468209 -13.493112
 -11.06413225   1.13668314   1.39301435 -27.06432484  18.95704748]

Note que se sorteamos 10 números novamente, o resultado será diferente

x = np.random.uniform(-40, 40, 10)

print(x)
[39.56098573 -3.00326979 -7.10917808 24.57926091  1.22709979  1.10064788
  8.95892138 12.62747289 -7.24387521  8.6101158 ]

Para tornar o nosso exemplo mais previsível, vamos usar a função seed, também do NumPy, que faz com o que o sorteio dos números parta do mesmo lugar e, portanto, o resultado seja o mesmo. Isso é uma boa prática de código quando se pretende tornar os resultados facilmente replicáveis por outras pessoas.

np.random.seed(1)

x = np.random.uniform(-40, 40, 10)
print(x)
[ -6.63823962  17.62595948 -39.99085001 -15.81339419 -28.25952873
 -32.61291242 -25.09918309 -12.35514184  -8.25860206   3.10533872]
np.random.seed(1)

x = np.random.uniform(-40, 40, 10)
print(x)
[ -6.63823962  17.62595948 -39.99085001 -15.81339419 -28.25952873
 -32.61291242 -25.09918309 -12.35514184  -8.25860206   3.10533872]

Feito o sorteio de números aleatórios, o próximo passo é tirar a média desse conjunto de números.

print(np.mean(x))
-14.829655377323416

Como vocês podem notar, a média não deu igual a zero, embora a média da distribuição uniforme com números inteiros que vai de -40 a 40 seja igual a 0. E aí surge a mágica do Teorema Central do Limite. Ainda que uma amostra não tenha a média igual à média populacional, a média das médias vai convergindo para zero à medida em que extraímos novas amostras e calculamos novas médias. Vamos implementar isso através de um loop, sorteando 50 números aleatórios por amostra e primeiro com 10 amostras:

np.random.seed(1)

# número de amostras
n = 10 
means = [] 

for j in list(range(0,n)):
    
    lista_sorteio = np.random.uniform(-40, 40, 50)
    x = np.mean(lista_sorteio)
    means.append(x)

means = [np.round(elem,2) for elem in means]

print(means)
print(np.mean(means))
[np.float64(-2.64), np.float64(0.38), np.float64(-2.3), np.float64(-0.13), np.float64(3.95), np.float64(4.81), np.float64(1.16), np.float64(0.46), np.float64(-2.02), np.float64(2.95)]
0.6619999999999999

Agora com 100 amostras e mantendo o número de 50 sorteios por amostra:

np.random.seed(1)

# número de amostras
n = 100 
means = [] 

np.random.seed(1)

for j in list(range(0,n)):
    
    lista_sorteio = np.random.uniform(-40, 40, 50)
    x = np.mean(lista_sorteio)
    means.append(x)

means = [np.round(elem,2) for elem in means]

print(means)
print(np.mean(means))
[np.float64(-2.64), np.float64(0.38), np.float64(-2.3), np.float64(-0.13), np.float64(3.95), np.float64(4.81), np.float64(1.16), np.float64(0.46), np.float64(-2.02), np.float64(2.95), np.float64(-2.02), np.float64(-1.95), np.float64(-1.69), np.float64(1.3), np.float64(2.67), np.float64(1.64), np.float64(-2.17), np.float64(0.71), np.float64(-3.31), np.float64(-0.82), np.float64(-1.1), np.float64(2.47), np.float64(-0.34), np.float64(1.6), np.float64(0.52), np.float64(-0.78), np.float64(2.61), np.float64(2.68), np.float64(0.98), np.float64(-2.24), np.float64(-4.57), np.float64(5.56), np.float64(2.17), np.float64(3.94), np.float64(4.72), np.float64(3.43), np.float64(2.47), np.float64(2.82), np.float64(1.24), np.float64(0.82), np.float64(0.28), np.float64(-7.24), np.float64(0.77), np.float64(-5.73), np.float64(0.17), np.float64(5.3), np.float64(3.94), np.float64(-8.14), np.float64(-4.17), np.float64(-2.82), np.float64(-1.41), np.float64(-7.05), np.float64(1.03), np.float64(1.15), np.float64(2.35), np.float64(-2.7), np.float64(-3.12), np.float64(0.67), np.float64(-1.7), np.float64(-0.95), np.float64(3.93), np.float64(1.95), np.float64(2.65), np.float64(-2.48), np.float64(-5.42), np.float64(4.43), np.float64(1.88), np.float64(-0.04), np.float64(-1.46), np.float64(1.04), np.float64(-1.54), np.float64(-2.82), np.float64(-2.62), np.float64(-3.85), np.float64(-0.56), np.float64(1.08), np.float64(8.73), np.float64(5.43), np.float64(1.0), np.float64(-3.13), np.float64(-1.05), np.float64(3.14), np.float64(-5.01), np.float64(-1.15), np.float64(-4.29), np.float64(5.22), np.float64(-5.29), np.float64(1.37), np.float64(-8.62), np.float64(-7.99), np.float64(-3.2), np.float64(3.04), np.float64(0.36), np.float64(3.64), np.float64(3.93), np.float64(6.98), np.float64(-1.4), np.float64(-3.84), np.float64(0.81), np.float64(7.0)]
0.024600000000000007

A média das médias parece ter se aproximado da média amostral verdadeira \(\mu\), que é igual a zero, conforme aumentamos de 10 para 100 amostras. Mas e se aumentarmos para 200 ou 1000 amostras? Vamos deixar essa tentativa de aproximar a média das médias de \(\mu\) mais automatizada:

np.random.seed(1)

expoentes = [1,2,3,4,5,6,7,8,9,10]
N = [2**exp for exp in expoentes]

means = [] 

np.random.seed(1)
for n in N:
    
    means_atual = [] 
    for j in list(range(0,n)):
        
        lista_sorteio = np.random.uniform(-40, 40, 100)
        x = np.mean(lista_sorteio)
        means_atual.append(x)
        
    means.append(np.mean(means_atual))
for m in means:
    
    print(np.round(m,2))
-1.17
0.92
0.37
-0.25
0.31
-0.32
0.23
-0.15
-0.06
0.03

Olha só a convergência aí, minha gente!

Note que até aqui a gente só mostrou que a média das médias converge para a média populacional. O Teorema, no entanto, é mais completo do que isso. Ele diz que a distribuição da média amostral é uma Normal e, além disso, possui aquelas características em relação à distribuição original. Ainda precisamos de mais algumas aulas para ir além e olhar essas outrs questões do Teorema, mas logo logo chegamos lá.

5.6 Conclusão

Neste capítulo, foram apresentados os principais mecanismos de controle de fluxo em Python, responsáveis por determinar como e quando determinadas instruções são executadas. Você aprendeu a utilizar estruturas condicionais com base em expressões booleanas, bem como estruturas de repetição definidas e indefinidas para executar blocos de código múltiplas vezes. Também foram discutidos recursos de controle fino de loops, além das list comprehensions como forma sinteticamente mais compacta e eficiente de realizar loops no contexto de listas. Essas ferramentas constituem a base de praticamente qualquer algoritmo aplicado, permitindo automatizar cálculos, implementar regras lógicas e estruturar procedimentos repetitivos. A partir desses fundamentos, torna-se possível organizar o código de forma mais modular e reutilizável, tema que será desenvolvido no próximo capítulo por meio do estudo de funções.

5.7 Exercícios

  1. Escreva um programa que receba a lista de rendas mensais abaixo e classifique cada observação nas seguintes categorias:

    • Dados: rendas = [1200, 3240, 3250, 7100, 12000, 5000, 7500, 1800, 4500]
    • “Baixa renda” caso a renda mensal seja menor do que 2 salários mínimos.
    • “Renda média” caso a renda mensal esteja entre 2 e 5 salários mínimos.
    • “Alta renda” caso contrário.

    O programa deve retornar uma nova lista contendo as classificações na mesma ordem da lista original. Você deve utilizar as instruções if e for. Considere o salário mínimo de \(R\$ 1.621\) (valor de 2026)

  2. Utilize o loop while para construir um código que realize uma soma infinita de uma progressão geométrica em que o primeiro termo é igual a \(10\) e a razão é igual a \(0.9\).Lembre-se de definir um critério de parada para que o loop não rode para sempre.

  3. Escreva um programa que, a partir de uma lista de dados, percorra todos os elementos um a um ignorando valores negativos, interrompendo o loop caso encontre um número maior do que \(8\), e somando todos os valores processados pelo loop antes da interrupção. Aplique esse programa à lista numeros = [4,7,-2,9,1,2,-10,6,-5,3,7,-0.5,12,1,3,-2].

  4. Simule 10.000 lançamentos de um dado justo utilizando a função randint do numpy.random. Utilize um loop for e expressões condicionais para (i) calcular a frequência relativa de cada face e (ii) a média dos valores obtidos dentro da amostra de lançamentos.

  5. O último teorema de Fermat diz que não há nenhum número inteiro positivo a,b,c tal que \[ a^n + b^n = c^n \]

    para quaisquer valores de n maiores do que 2.

    Use o que você aprendeu com condicionais, operações aritméticas e instruções print para testar se o teorema se mantém, dada a lista de números inteiros abaixo. Note que ao fim de cada iteração, o programa deve exibir “Holy smokes! Fermat was wrong” caso o teorema não valha e “Fermat was right” caso você não tenha refutado um gênio dos tempos modernos.

    • a = [1,2,3,4,5,6,7,8,9,10]
    • b = [1,2,3,4,5,6,7,8,9,10]
    • n = [3,4,5,6,7,8,9,10,37,52,89,100]

  1. É uma abreviação da expressão else if.↩︎

  2. Uma das formas de interromper à força a execução de um loop é por meio das teclas Ctrl+C.↩︎