Listas

Terça, 7 de abril de 2020

Instruções: Enquanto você lê, você deve acompanhar os exemplos utilizando um console e Python. Para essa unidade, você deve ler o restante do capítulo 3 a partir de 3.1.2 e as seções do capítulo 4 até 4.5 do tutorial Python.


Vamos refazer nosso algoritmo para a lista de compras, mas dessa vez vamos guardar os valores dos itens na memória do computador. Vamos resolver o seguinte exercício:

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 um valor de 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 todas as linhas, com exceção da primeira: criar uma lista de compras: 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. Vamos experimentar algumas, experimente:

>>> 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]

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 lista_compras como grande quadrado 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 as 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 experimentar o seguinte programa:

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. 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 por um número diferente cada elemento de uma lista de inteiros:

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

Um parêntese

Você consegue dizer o que esse programa faz? Tente simular no papel e depois verifique executando esse programa com o auxílio de um debugger. Para isso, você pode utilizar uma IDE configurada apropriadamente como o VSCode. Alternativamente, salve o programa seguinte como 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 por enquanto. 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 como _para cada valor de lista*compras*, execute as seguinte instruções_. O que esse trecho faz é criar um novo nome de variável valore executar bloco de código indentado doforuma vez para cada elemento da lista, em ordem. Em cada iteração,valor` estará referenciando um elemento da lista.

Observe que embora não faça parte do for, a variável soma está associada a esse laço. Ao final de cada iteração ela terá o valor da soma parcial dos valores até o item corrente, por isso é 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 ou sequência em Python. Não vamos estudar iteradores em detalhes. 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 seguinte 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 seguinte programa:

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 pode converter um intervalo (ou qualquer sequência) em uma lista. Isso na maioria das vezes não é necessário, mas você pode querer verificar isso:

>>> 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. Nessa 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 nessa nessa disciplina queremos aprender e exercitar os algoritmos explicitamente.

Um exercícios 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 lista. 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)

Estudo 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 as tarefas.

Saindo de um laço 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 de um número não triviais (isso é, 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 (Truth Value Testing) 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. 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

Vamos ver outro exemplo em que utilizar um break pode facilitar escrever um programa. Vamos resolver 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 digitá F quando quiser terminar.

Aqio 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 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.