Unidade 4 - Listas

Enquanto lê o texto, lembre-se de reproduzir os exemplos e realizar os exercícios sugeridos. Para esta unidade, você deve voltar ao capítulo 3 do tutorial, particularmente na subseção 3.1.2 que trata de listas. Depois, leia o capítulo 4, pelo menos até a seção 4.5.

Comecemos refazendo nosso algoritmo para calcular o valor de uma lista de compras, mas dessa vez vamos guardar os valores dos itens na memória do computador, ao invés de escrever no paper.

Escreva um programa que leia uma sequência de valores de itens de compra e mostre o valor da soma de todos os itens. O usuário deverá escrever o valor de cada item, um por linha. Quando não houver mais itens, o usuário irá indicar esse fato escrevendo um número negativo qualquer.

Para ter certeza de que entendemos o problema, primeiro escrevemos um exemplo de entrada:

3.50
5.00
16.36
-1

Assim, usando uma calculadora fica claro que devemos somar os valores das três primeiras linhas e obter o seguinte resultado.

24.86

Como de costume, vamos escrever primeiro um algoritmo em português.

lista_compras ← crie uma lista de compras
valor ← leia o valor de um item
enquanto valor > 0:
    adicionar valor a lista_compras
    valor ← leia um valor de item

soma ← 0
para cada valor na lista_compras:
    soma ← soma + valor
mostrar o valor da soma total

Já sabemos implementar várias dessas linhas em Python, mas algumas têm instruções que não vimos. Já na primeira linha, topamos com a seguinte dificuldade: como criar uma lista de compras? Como você deve imaginar, não existe uma tal abstração lista de compras na linguagem de programação Python. Então para implementar esse programa, teremos que responder duas perguntas:

  1. como representar um item da lista de compras no contexto desse algoritmo?
  2. como armazenar um conjunto de itens da lista de compras em uma única variável?

A resposta da primeira pergunta reforça que o que armazenamos de fato na memória do computador não são itens de compra, mas dados relacionados a um item. Nesse caso, o único dado de interesse é o valor desse item, que devemos representar como um número de ponto flutuante. Assim, para responder a segunda pergunta precisamos de um mecanismo para armazenar um conjunto de números de ponto flutuante. Em Python, a maneira natural de armazenar uma coleção de dados é criando uma lista. Uma implementação do algoritmo seria a seguinte:

# leia uma sequência de valores de itens
lista_compras = []
valor = float(input())
while valor >= 0:
    lista_compras.append(valor)
    valor = float(input())

# somar todos os valores da lista
soma = 0.0
for valor in lista_compras:
    soma += valor
print(soma)

Há diversas novidades nesse trecho de código. Vamos explorá-lo em partes.

Listas

A expressão [] cria uma nova variável do tipo lista que inicialmente está vazia. Há várias maneiras de criar uma lista. Vejamos algumas. Experimente utilizando o modo interativo do interpretador:

>>> lista_vazia = []
>>> outra_lista_vazia = list()
>>> primos = [2, 3, 5, 7, 11]
>>> cinco_zeros = [0] * 5
>>> escritores = ["Vinicius de Moraes",
...               "Cecília Meireles",
...               "Mary Shelley",
...               "Cora Coralina",
...               "Pedro dos Anjos",]
>>> escritoras = escritores[1:4]
>>> notas = [10.0, 7.5, 3.14]
>>> palavras = " Ando por aí querendo te encontrar".split()

O número de referências que estão armazenadas em uma lista pode ser variável. Ela pode começar vazia ou com alguns elementos. Podemos inserir um elemento no final da lista com a operação append e remover um elemento do final da lista com a operação pop.

>>> primos = [2, 3, 5, 7, 11]
>>> type(primos)
<class 'list'>
>>> len(primos)
5
>>> primos.append(17)
>>> primos
[2, 3, 5, 7, 11, 17]
>>> primos.extend([19, 23])
>>> primos
[2, 3, 5, 7, 11, 17, 19, 23]
>>> primos.pop()
23
>>> primos
[2, 3, 5, 7, 11, 17, 19]
>>> primos = primos + [29, 31]
>>> primos
[2, 3, 5, 7, 11, 17, 19, 29, 31]

Procure na documentação outras funções para remover ou inserir em uma posição arbitrária e para remover um determinado elemento. Experimente tentar remover elementos de listas vazias ou remover elementos que não existem na lista.

Uma lista é uma variável que contém um conjunto de referências para outras variáveis. Você pode desenhar a variável lista_compras como um grande retângulo na memória que contém referências para diversas variáveis. Ao executar o programa com o exemplo de entrada, obteremos uma figura parecida com a seguinte:

Você pode imaginar que na verdade há diversos nomes distintos referenciando a variáveis distintas, como na figura:

Os motivos por que usamos uma lista ao invés de diversas variáveis soltas são:

  1. podemos armazenar um número variável de elementos na memória, todos representados por um mesmo nome;
  2. podemos acessar uma variável distinta dessa coleção por meio de um índice não constante.

Isso é importante porque, no momento em que estamos escrevendo um algoritmo, não sabemos quantos elementos a coleção deverá ter, nem qual posição será acessada. Vamos estudar o programa seguinte.

n = input("Digite quantos amigos você tem? ")
amigos = []
i = 0
while i < n:
    nome = input(f"Digite o nome do amigo número {i}: ")
    amigos.append(nome)
    i += 1

j = int(input("Digite um número: "))
print(f"Seu amigo número {j} chama-se {amigos[j]}")

Experimente executar esse programa. O número de amigos só será conhecido em tempo de execução, assim o tamanho da lista irá variar de execução para execução, bem como o índice j do amigo consultado. Observe que nada garante que o número armazenado na variável j corresponde a um número de amigo válido. O que acontece quando você digita um número negativo? E quando digita um número maior ou igual o número n?

Assim como as variáveis simples, também podemos mudar os valores a que se referem os elementos de uma lista. O seguinte programa multiplica cada elemento de uma lista de inteiros por um número diferente.

sequencia = [1] * 5
i = 1
while i < 10:
    sequencia[i] = sequencia[i - 1] * i
    i += 1
print(sequencia)

Um parêntese: debugger

Você consegue dizer o que o programa acima faz? Qual a saída desse programa? Para responder, temos que simular esse algoritmos usando lápis e papel. Quando você tiver mais experiência e estiver testando programas maiores, precisará usar uma ferramenta para auxiliar a simular algoritmos. Esse tipo de ferramenta chama-se depurador ou debugger.

Depois de ter simulado no papel, você pode tentar depurar esse trecho. Para isso, você precisa configurar uma IDE ou um editor de texto com suporte a debugger em Python (como o VSCode). Alternativamente, você pode utilizar o debugger de Python padrão no sistema. Guarde o programa seguinte como um arquivo sequencia.py,

sequencia = [1] * 5
i = 1
while i < 10:
    breakpoint()
    sequencia[i] = sequencia[i - 1] * i
    i += 1
print(sequencia)

e execute em um terminal usando python3 sequencia.py. Isso irá iniciar um sessão do Python debugger padrão (pdb) toda vez que a linha que contém breakpoint() for executada. Nessa sessão, você pode inspecionar os valores atuais das variáveis, assim como num terminal interativo. Depois de ver o valor das variáveis, digite continue, para continuar executando a próxima linha até a próxima instrução breakpoint().

Listas heterogêneas

Na grande maioria da vezes, vamos considerar apenas listas que contêm elementos de um determinado tipo. As listas em Python, no entanto, permitem listas que contêm elementos de tipos heterogêneos. Vejamos um exemplo:

>>> numeros = ["um", 2, 3.0]
>>> type(numeros[0])
<class 'str'>
>>> type(numeros[1])
<class 'int'>
>>> type(numeros[2])
<class 'float'>

Enquanto isso pode ser conveniente às vezes, você não deve criar listas desse tipo, pelo menos agora. Um dos motivos é que não podemos tratar os elementos dessa lista de maneira uniforme. Qual seria o resultado de numeros[0] + numeros[1]?

Percorrendo listas

Voltando ao exemplo da lista de compras, agora precisamos percorrer os elementos da lista. Para isso, usamos a construção for como seguinte:

# somar todos os valores da lista
soma = 0.0
for valor in lista_compras:
    soma += valor
print(soma)

Você pode ler o trecho acima como para cada valor de lista_compras, execute as seguintes instruções. O que esse trecho faz é associar um nome valor a um elemento da lista e executar corpo de comandos do for, uma vez para cada elemento da lista e em ordem. Em cada iteração, valor estará referenciando um elemento da lista.

Observe que embora a variável soma não faça parte da construção do for, ela é um elemento importante para entendermos esse laço. Ao final de cada iteração do for, o valor dessa variável será a soma parcial dos valores até o item considerado atualmente. Por esse motivo, ela é chamada de variável acumuladora. Faça os exercícios de fixação para descobrir mais usos de variáveis acumuladoras.

Na verdade, você pode usar o for para percorrer qualquer iterador em Python. Não vamos estudar iteradores em detalhes, aqui basta dizer que eles são objetos que se comportam como uma sequência de itens. Um iterador bastante comum é obtido pela função range, que representa um intervalo inteiro. Podemos usá-lo da seguinte forma:

for i in range(10):
    print(f"Executando iteração com i = {i}")

Isso irá imprimir a saída

Executando iteração com i = 0
Executando iteração com i = 1
Executando iteração com i = 2
Executando iteração com i = 3
Executando iteração com i = 4
Executando iteração com i = 5
Executando iteração com i = 6
Executando iteração com i = 7
Executando iteração com i = 8
Executando iteração com i = 9

Procure a documentação de range para ver outras formas de usá-la. Tente descobrir o que faz o programa seguinte. Tente primeiro usando apenas lápis e papel por pelo menos alguns minutos e depois execute com um computador. Uma dica: a instrução print(" ", end="") imprime um espaço, mas não insere a quebra a linha depois.

for i in range(5, 11):
    for j in range(10 - i):
        print(" ", end="")
    for j in range(2 * i):
        print("*", end="")
    for j in range(10 - i):
        print(" ", end="")
    print()

Enquanto o valor devolvido por range funciona como uma lista, ele não é uma lista. Mas se quiser, você pode converter um intervalo (ou qualquer sequência) em uma lista. Na maioria das vezes, isso não é necessário, mas você pode querer verificar:

>>> intervalo_pares = range(2, 11, 2)
>>> intervalo_pares
range(2, 11, 2)
>>> type(intervalo_pares)
<class 'range'>
>>> pares = list(intervalo_pares)
>>> pares
[2, 4, 6, 8, 10]
>>> minha_string = "Uma string"
>>> type(minha_string)
<class 'str'>
>>> sequencia_caracteres = list(minha_string)
>>> sequencia_caracteres
['U', 'm', 'a', ' ', 's', 't', 'r', 'i', 'n', 'g']
>>> type(sequencia_caracteres)
<class 'list'>

Um outro exemplo

Você pode se questionar porque precisamos guardar todos os valores da nossa lista de compras se apenas gostaríamos de somá-los. De fato, não precisamos: nesse exemplo, ter criado uma lista apenas fez com que utilizássemos mais memória (para armazenar uma lista) do que era necessário. Nesta disciplina, as entradas não serão tão grandes a ponto de precisarmos economizar memória, então sempre que for conveniente, vamos armazenar os dados em uma lista. Há algumas vantagens de escrever programas assim: primeiro, é mais fácil pensar sobre uma lista e, segundo, há uma série de funções prontas para tratar listas em Python. O último trecho de código poderia ser substituído pela seguinte linha

print(sum(lista_compras))

Pesquise sobre a função sum e outras funções agregadoras de Python, mas tenha em mente que nesta disciplina queremos aprender e exercitar os algoritmos explicitamente.

Um exercício em que é necessário manter uma lista em memória é o seguinte:

Escreva um programa que receba uma sequência de nomes digitados no teclado e imprima as iniciais. Para sinalizar que não há mais nomes, o usuário irá digitar um traço -.

Um exemplo de entrada é

Maria
João
Pedro
Catarina
Carlos
-

Com o que já aprendemos, podemos escrever o seguinte:

# ler uma sequência de nomes
lista_nomes = []
nome = input()
while nome != "-":
    lista_nomes.append(nome)
    nome = input()

# guardar as iniciais
lista_iniciais = []
for nome in lista_nomes:
    inicial = nome[0]
    lista_iniciais.append(inicial)

print(" ".join(lista_iniciais))

Preste atenção no argumento de print e pesquise sobre a função join, que nos auxilia a criar uma string separada por espaços. Ao testarmos esse programa e analisarmos a saída, no entanto, iremos notar um problema.

M J P C C

A saída contém iniciais repetidas. Isso é fácil de corrigir quando guardamos a lista de iniciais: basta testar se já encontramos a inicial antes de inserir. Podemos fazer isso usando um operador novo.

# ler uma sequência de nomes
lista_nomes = []
nome = input()
while nome != "-":
    lista_nomes.append(nome)
    nome = input()

# guardar as iniciais
lista_iniciais = []
for nome in lista_nomes:
    inicial = nome[0]
    if inicial not in lista_iniciais:
        lista_iniciais.append(inicial)

print(" ".join(lista_iniciais))

O operador in (ou sua versão negada not in) devolve um valor booleano indicando se o item à esquerda está na lista à direita.

Copiando uma lista e referências

Você deve ter reparado que sempre que falamos do operador de atribuição =, nós distinguimos entre o nome da variável e o valor da variável. Isso é particularmente importante quando trabalhamos com listas. Por exemplo, tente descobrir o que é impresso pelo seguinte trecho:

mamiferos = ["golfinho", "humano", "cachorro"]
animais = mamiferos
animais.append("sapo")
print(mamiferos)

Ao executar esse código você verá que não teremos impresso apenas espécies mamíferas, mas também um anfíbio. Muito embora uma tradução ingênua para o português poderia dizer que “sapo” foi adicionado apenas a lista de animais, na verdade a lista animais e mamiferos são uma só! Uma representação em memória desse trecho é

Quando escrevemos a linha animais = mamiferos o que fazemos é dar um novo nome à mesma lista que foi criada antes. Para fazer uma cópia de uma lista, precisamos de um pouco mais de trabalho. Observe e procure entender o código abaixo

mamiferos = ["golfinho", "humano", "cachorro"]
mammals = mamiferos

animais = []

for m in mamiferos:
    animais.append(m)

animais.append("sapo")

mammals[2] = "elefante"

print(mamiferos)
print(mammals)
print(animais)

Agora, a saída será:

['golfinho', 'humano', 'elefante']
['golfinho', 'humano', 'elefante']
['golfinho', 'humano', 'cachorro', 'sapo']

Uma representação da memória poderia ser:

List comprehension

Criar uma lista a partir de outra sequência é tão comum que Python tem uma maneira mais curta de escrever o mesmo código, que é chamada de list comprehension. Por exemplo, podemos criar uma cópia de uma lista de números, mas multiplicando por dois.

notas = [3.5, 6.0, 1.9, 10, 7.4, 4.3]
dobros = [2 * nota for nota in notas]
print(dobros)

Podemos, inclusive, filtrar um subconjunto de números:

notas = [3.5, 6.0, 1.9, 10, 7.4, 4.3]
dobros_notas_vermelhas = [2 * nota for nota in notas if nota < 5]
print(dobros_notas_vermelhas)

Estude e experimente trabalhar com list comprehensions. No entanto, por enquanto, prefira as versões mais explícitas apresentadas anteriormente quando for resolver os exercícios e tarefas desta disciplina.

Saindo antecipadamente

Vimos que um for executa uma iteração para todo elemento da sequência. Vamos ver mais um exemplo:

Escreva um programa que imprima todos os divisores não triviais de um número (isso é, todos os divisores que não são um ou o próprio número).

Como sempre, queremos escrever um algoritmo para esse problema em português.

n ← leia um número do teclado
divisores ← crie uma lista vazia
para d de 2 até n - 1:
    se d divide n:
        adicione d aos divisores
devolva divisores

Nesse ponto, deve ser trivial traduzir esse algoritmo em um código em Python:

n = int(input())
divisores = []
for d in range(2, n):
    if n % d == 0:
        divisores.append(d)

print(divisores)

Um número é primo se ele é maior do que um e não tém divisores não triviais. É fácil modificar o código acima e verificar se um número é primo:

n = int(input())
divisores = []
for d in range(2, n):
    if n % d == 0:
        divisores.append(d)

if n == 1 or divisores:
    print("O número é 1 ou tem divisores não triviais")
else:
    print("O número é primo")

Para entender esse código precisamos perceber uma sutileza: divisores é uma lista, mas ela foi usada no lugar em que esperaríamos um valor booleano. Em Python, coleções (como listas) podem ser usadas como valores de verdade: elas são consideradas True sempre que não forem vazias, e False caso contrário. Pesquise sobre as várias formas de testar valores de verdade em Python 3.

Se você é impaciente deve estar incomodado com o código acima: ele executa mais operações do que é necessário. Parece latente que um número como 1000000000 não é primo. Ainda assim, se executarmos o código acima e digitarmos esse valor, teremos uma surpresa desagradável — e não interessa que você tenha um computador top de linha ou mesmo um supercomputador!

O motivo é que desde o momento em que testamos o primeiro divisor, já sabíamos que o número 1000000000 não era primo, mas o programa é alheio ao seu sofrimento e continua obstinado em executar todas as iterações. Para terminar um laço antes do final, usamos um comando especial break. Isso irá terminar o laço e continuar na instrução imediatamente posterior.

n = int(input())
divisor_encontrado = False
for d in range(2, n):
    if n % d == 0:
        divisor_encontrado = True
        break

if n == 1 or divisor_encontrado:
    print("O número é 1 ou tem divisores não triviais")
else:
    print("O número é primo")

O comando for permite um comando else opcional, cujo bloco de comandos é executado sempre que o laço executa todas iterações completamente. Essa é uma peculiaridade de Python (não há muitas outras linguagens com esse tipo de construção) e você deve evitá-la até ter mais experiência. Poderíamos reescrever o código assim:

n = int(input())

for d in range(2, n):
    if n % d == 0:
        divisor_encontrado = True
        break
else:
    divisor_encontrado = False

if n == 1 or divisor_encontrado:
    print("O número é 1 ou tem divisores não triviais")
else:
    print("O número é primo")

Construindo um menu de opções

Vejamos um outro exemplo em que utilizar um break pode facilita a vida do programador. Resolva o seguinte exercício:

Escreva uma caculadora que realiza soma e subtração. Uma instrução começa com o operador e uma linha seguido de duas linhas com os operandos. O programa deve executar quantas operações forem fornecidas pelo usuário, que digitará F quando quiser terminar.

Aqui está um exemplo de entrada:

+
3
5
-
4
1
-
5
0
F

e a saída correspondente:

8
3
5

Tente escrever um algoritmo e um código em Python para esse exercício. Depois de ter feito a sua versão, estude como eu escreveria:


while True:
    operador = input()
    if operador == "+":
        num1 = float(input())
        num2 = float(input())
        soma = num1 + num2
        print(soma)
    elif operador == "-":
        num1 = float(input())
        num2 = float(input())
        diferenca = num1 - num2
        print(diferenca)
    elif operador == "F":
        break
    else:
        print("Operação inválida")

Experimente adicionar outras operações a sua calculadora.