5  Estruturas de Dados

Até aqui, já usamos variáveis simples, decisões, laços e funções. Agora vamos organizar melhor os dados usando estruturas de dados.

Neste capítulo vamos estudar:

A ordem foi escolhida pela praticidade: começamos pelas estruturas que aparecem com mais frequência em problemas reais.


5.1 Listas: revisão e operações importantes

Uma lista guarda múltiplos valores em ordem. Ela é mutável, ou seja, podemos alterar seus elementos.

Exemplos Práticos:

Lista de materiais (lista_compras.py): Este é o mesmo programa do capítulo de laços de repetição, que monta uma lista de materiais de bancada. Agora, observe o papel da lista no programa.

materiais = []
print("Adicionando materiais para a bancada de teste...")
materiais.append("multimetro")
materiais.append("sensor de temperatura")
materiais.append("cabos de medicao")
print("Lista final de materiais:")
for material in materiais:
    print(f"Material necessario: {material}")

Exemplo de execução:

Adicionando materiais para a bancada de teste...
Lista final de materiais:
Material necessario: multimetro
Material necessario: sensor de temperatura
Material necessario: cabos de medicao

O .append() acrescenta cada item no final da lista, sem substituir os anteriores. Por isso a lista preserva a ordem de inserção: o primeiro material adicionado ocupa a primeira posição, e o for exibe os materiais na mesma ordem em que foram registrados.

Operações com lista (operacoes_lista.py): Crie um programa que leia três leituras de pressão em bar (inteiros) e as guarde em uma lista com append. Em seguida, exiba a lista completa, a primeira e a última leitura, remova a última leitura com pop e exiba a lista atualizada.

leituras_pressao = []
for i in range(3):
    pressao = int(input(f"Digite a {i + 1}a leitura de pressao em bar: "))
    leituras_pressao.append(pressao)
print(f"Leituras registradas: {leituras_pressao}")
print(f"Primeira leitura: {leituras_pressao[0]}")
print(f"Ultima leitura: {leituras_pressao[-1]}")
leituras_pressao.pop()
print(f"Lista apos remover a ultima leitura: {leituras_pressao}")

Exemplo de execução:

Digite a 1a leitura de pressao em bar: 5
Digite a 2a leitura de pressao em bar: 7
Digite a 3a leitura de pressao em bar: 6
Leituras registradas: [5, 7, 6]
Primeira leitura: 5
Ultima leitura: 6
Lista apos remover a ultima leitura: [5, 7]

O for com range(3) lê três leituras e acrescenta cada uma à lista com append. Em seguida, o programa acessa elementos pela posição. Cada elemento da lista ocupa uma posição numerada, chamada índice, e a contagem começa em 0. Assim, leituras_pressao[0] acessa a primeira leitura, e leituras_pressao[-1] acessa a última, já que índices negativos contam a partir do fim.

O .pop() remove o último elemento da própria lista, e a última linha mostra que ela ficou com duas leituras. Diferente de números e textos, listas podem ser alteradas depois de criadas, e por isso dizemos que elas são mutáveis.

Etapas de montagem (aplicador_operacoes_lista.py): Crie um programa, sem entrada do usuário, que comece com a lista de etapas “cortar chapa”, “furar base” e “montar suporte”. Adicione as etapas “instalar sensor” e “inspecionar conjunto” com append, remova a última com pop, troque a primeira etapa por “receber chapa” e exiba a lista depois de cada operação.

etapas_montagem = ["cortar chapa", "furar base", "montar suporte"]
print(f"Lista inicial: {etapas_montagem}")
etapas_montagem.append("instalar sensor")
print(f"Apos append('instalar sensor'): {etapas_montagem}")
etapas_montagem.append("inspecionar conjunto")
print(f"Apos append('inspecionar conjunto'): {etapas_montagem}")
etapas_montagem.pop()
print(f"Apos pop(): {etapas_montagem}")
etapas_montagem[0] = "receber chapa"
print(f"Apos alterar a primeira etapa: {etapas_montagem}")

Exemplo de execução:

Lista inicial: ['cortar chapa', 'furar base', 'montar suporte']
Apos append('instalar sensor'): ['cortar chapa', 'furar base', 'montar suporte', 'instalar sensor']
Apos append('inspecionar conjunto'): ['cortar chapa', 'furar base', 'montar suporte', 'instalar sensor', 'inspecionar conjunto']
Apos pop(): ['cortar chapa', 'furar base', 'montar suporte', 'instalar sensor']
Apos alterar a primeira etapa: ['receber chapa', 'furar base', 'montar suporte', 'instalar sensor']

Todas as operações alteram a mesma lista, sem criar uma nova, e os print mostram o estado dela depois de cada passo. Os dois append acrescentam etapas no final, e o pop remove a última, "inspecionar conjunto". A linha 9 mostra uma operação nova: um elemento pode ser trocado diretamente pelo índice, com etapas_montagem[0] = "receber chapa", que substitui a primeira etapa.


5.2 Dicionários: chave e valor

Dicionários são ideais quando queremos acessar informações por nome ou identificador.

Exemplos Práticos:

Estoque com dicionário (sistema_estoque.py): Crie um programa que leia o código (texto) e a quantidade (inteiro) de três materiais e os guarde em um dicionário. Depois, o programa pede um código para consulta, exibe o dicionário completo e mostra a quantidade do código consultado ou, se ele não existir, a mensagem “Codigo nao encontrado no estoque.”.

estoque = {}
for i in range(3):
    codigo = input(f"Digite o codigo do {i + 1}o material: ")
    quantidade = int(input("Digite a quantidade em estoque: "))
    estoque[codigo] = quantidade
codigo_procurado = input("Qual codigo deseja consultar? ")
print(f"Estoque completo: {estoque}")
if codigo_procurado in estoque:
    print(f"Quantidade de '{codigo_procurado}': {estoque[codigo_procurado]}")
else:
    print("Codigo nao encontrado no estoque.")

Exemplo de execução:

Digite o codigo do 1o material: P10
Digite a quantidade em estoque: 50
Digite o codigo do 2o material: P20
Digite a quantidade em estoque: 0
Digite o codigo do 3o material: P30
Digite a quantidade em estoque: 12
Qual codigo deseja consultar? P30
Estoque completo: {'P10': 50, 'P20': 0, 'P30': 12}
Quantidade de 'P30': 12

Um dicionário, criado com {}, guarda pares de chave e valor. A cada volta do laço, a atribuição estoque[codigo] = quantidade cria um par em que o código é a chave e a quantidade é o valor. Se o usuário digitar P10 e 50, o dicionário passa a ter o par 'P10': 50.

A consulta usa a chave, e não uma posição como nas listas: estoque[codigo_procurado] devolve a quantidade associada ao código. Antes disso, o in da linha 8 verifica se a chave existe, porque buscar uma chave inexistente faz o programa parar com um erro.

Frequência de palavras (contador_frequencia_palavras.py): Crie um programa que leia uma observação de inspeção e exiba um dicionário com o número de vezes que cada palavra aparece. Desconsidere espaços extras no início e no fim do texto e converta tudo para minúsculas, de modo que “Falha” e “falha” contem como a mesma palavra.

relatorio = input("Digite a observacao de inspecao: ").strip().lower()
palavras = relatorio.split()
frequencia = {}
for palavra in palavras:
    frequencia[palavra] = frequencia.get(palavra, 0) + 1
print(f"Frequencia de palavras: {frequencia}")

Exemplo de execução:

Digite a observacao de inspecao:   Falha no sensor falha no cabo  
Frequencia de palavras: {'falha': 2, 'no': 2, 'sensor': 1, 'cabo': 1}

A linha 1 prepara o texto lido: o .strip() remove os espaços do início e do fim, e o .lower() converte tudo para minúsculas, para que Falha e falha contem como a mesma palavra. Em seguida, o .split() separa o texto nos espaços e devolve uma lista de palavras.

No dicionário, cada palavra é uma chave, e o número de vezes que ela aparece é o valor. A expressão frequencia.get(palavra, 0) devolve a contagem atual da palavra ou 0, se ela ainda não apareceu, sem causar erro. O programa soma 1 a esse valor e guarda o resultado na mesma chave. Com o texto ok ok falha, o resultado é {'ok': 2, 'falha': 1}.

Ocorrências do turno (analisador_texto_palavras.py): Crie um programa, sem entrada do usuário, que conte quantas vezes cada etiqueta aparece no texto fixo “alarme temperatura pressao alarme vazamento pressao alarme” e exiba o dicionário com as contagens. Trabalhe com letras minúsculas.

ocorrencias_turno = "alarme temperatura pressao alarme vazamento pressao alarme"
palavras = ocorrencias_turno.lower().split()
frequencia = {}
for palavra in palavras:
    frequencia[palavra] = frequencia.get(palavra, 0) + 1
print(f"Ocorrencias no turno: {frequencia}")

Exemplo de execução:

Ocorrencias no turno: {'alarme': 3, 'temperatura': 1, 'pressao': 2, 'vazamento': 1}

O programa usa o mesmo padrão de contagem com get do exemplo anterior, agora sobre um texto fixo. O resultado, {'alarme': 3, 'temperatura': 1, 'pressao': 2, 'vazamento': 1}, mostra um uso comum de dicionários: resumir uma coleção grande de dados em categorias e quantidades, em vez de guardar cada ocorrência separadamente.


5.3 Conjuntos: elementos únicos

Conjuntos (set) guardam valores sem repetição. São úteis para deduplicar dados e testar pertencimento.

Exemplos Práticos:

Códigos de peças (gerenciador_convidados.py): Crie um programa que leia cinco códigos de peças, que podem ser repetidos, e guarde esses códigos em uma lista. Em seguida, converta a lista em um conjunto para remover as duplicatas, leia um código para consulta e exiba a lista original, o conjunto e se o código consultado está no conjunto. Use letras minúsculas para padronizar a comparação.

codigos_pecas = []
for i in range(5):
    codigo = input(f"Digite o {i + 1}o codigo de peca: ").lower()
    codigos_pecas.append(codigo)
codigos_unicos = set(codigos_pecas)
codigo_procurado = input("Digite um codigo para consulta: ").lower()
print(f"Lista original: {codigos_pecas}")
print(f"Conjunto sem duplicatas: {codigos_unicos}")
print(f"'{codigo_procurado}' esta no conjunto? {codigo_procurado in codigos_unicos}")

Exemplo de execução:

Digite o 1o codigo de peca: A1
Digite o 2o codigo de peca: b2
Digite o 3o codigo de peca: a1
Digite o 4o codigo de peca: C3
Digite o 5o codigo de peca: B2
Digite um codigo para consulta: c3
Lista original: ['a1', 'b2', 'a1', 'c3', 'b2']
Conjunto sem duplicatas: {'a1', 'b2', 'c3'}
'c3' esta no conjunto? True

A lista guarda todos os códigos digitados, inclusive as repetições. Antes de cada código entrar na lista, o .lower() converte esse código para minúsculas, para que A1 e a1 sejam considerados iguais.

A função set() cria um conjunto a partir da lista. Um conjunto mantém cada valor uma única vez, então as repetições desaparecem automaticamente. O in da última linha responde, com True ou False, se o código consultado pertence ao conjunto. Como conjuntos não guardam ordem, a ordem dos elementos exibidos pode variar entre execuções.

Limpador de texto (limpador_de_texto.py): Este é o mesmo programa do capítulo de laços de repetição, que remove a pontuação de um texto. Agora, observe o teste de pertencimento.

texto_original = "Sensor-3, pressao alta! Reavaliar? Sim."
pontuacao = ",.!?-"
texto_limpo = ""
print(f"Texto original: {texto_original}")
print("Limpando o texto...")
for caractere in texto_original:
    if caractere in pontuacao:
        print(f"Pontuacao '{caractere}' removida.")
        continue
    texto_limpo = texto_limpo + caractere
print(f"Texto limpo: {texto_limpo}")

Exemplo de execução:

Texto original: Sensor-3, pressao alta! Reavaliar? Sim.
Limpando o texto...
Pontuacao '-' removida.
Pontuacao ',' removida.
Pontuacao '!' removida.
Pontuacao '?' removida.
Pontuacao '.' removida.
Texto limpo: Sensor3 pressao alta Reavaliar Sim

Neste capítulo, o ponto de interesse é o teste de pertencimento. A string pontuacao funciona como uma pequena coleção de referência, e caractere in pontuacao pergunta se o caractere atual faz parte dela. O mesmo operador in funciona com listas, conjuntos e chaves de dicionário, como no exemplo sistema_estoque.py.

Equipamentos das bancadas (operacoes_conjuntos.py): Crie um programa, sem entrada do usuário, com um conjunto de equipamentos da bancada A (“multimetro”, “sensor termico”, “bomba de teste”) e outro da bancada B (“sensor termico”, “fonte dc”, “cabos de medicao”). Calcule e exiba a interseção e a união dos conjuntos e verifique se “multimetro” pertence à bancada B.

bancada_a = {"multimetro", "sensor termico", "bomba de teste"}
bancada_b = {"sensor termico", "fonte dc", "cabos de medicao"}
intersecao = bancada_a.intersection(bancada_b)
uniao = bancada_a.union(bancada_b)
multimetro_esta_na_b = "multimetro" in bancada_b
print(f"Intersecao entre as bancadas: {intersecao}")
print(f"Uniao das bancadas: {uniao}")
print(f"'multimetro' pertence a bancada B? {multimetro_esta_na_b}")

Exemplo de execução:

Intersecao entre as bancadas: {'sensor termico'}
Uniao das bancadas: {'bomba de teste', 'multimetro', 'sensor termico', 'cabos de medicao', 'fonte dc'}
'multimetro' pertence a bancada B? False

Os dois conjuntos são criados com chaves, assim como os dicionários, mas contêm apenas valores, sem pares de chave e valor. O intersection devolve os elementos presentes nos dois conjuntos ao mesmo tempo, que aqui é apenas o "sensor termico". O union junta todos os elementos, e o "sensor termico" aparece uma única vez no resultado, porque conjuntos não guardam repetições. Por fim, o in verifica se "multimetro" pertence à bancada B, e o resultado é False.


5.4 Tuplas: registros fixos

Tuplas são parecidas com listas, mas imutáveis. São úteis para representar dados fixos.

Exemplos Práticos:

Ponto de medição (sistema_coordenadas.py): Crie um programa que leia as coordenadas x e y de um ponto (decimais), guarde os dois valores em uma tupla ponto_medicao e depois separe a tupla em duas variáveis. Exiba a tupla completa e cada coordenada separada.

x = float(input("Digite a coordenada x do ponto: "))
y = float(input("Digite a coordenada y do ponto: "))
ponto_medicao = (x, y)
coordenada_x, coordenada_y = ponto_medicao
print(f"Ponto completo: {ponto_medicao}")
print(f"Coordenada x: {coordenada_x}")
print(f"Coordenada y: {coordenada_y}")

Exemplo de execução:

Digite a coordenada x do ponto: 2.5
Digite a coordenada y do ponto: 4
Ponto completo: (2.5, 4.0)
Coordenada x: 2.5
Coordenada y: 4.0

Os parênteses da linha 3 criam uma tupla, que agrupa os dois valores em um único registro. Diferente da lista, a tupla não pode ser alterada depois de criada: ela não tem append nem pop, e seus elementos não podem ser trocados. Isso faz da tupla uma boa escolha para dados fixos, como as coordenadas de um ponto.

A linha 4 faz o desempacotamento: cada variável à esquerda do = recebe o valor da posição correspondente da tupla, coordenada_x o primeiro e coordenada_y o segundo.

Média de temperaturas (media_temperaturas.py): Crie um programa que guarde em uma tupla os nomes de três sensores (“tanque_1”, “tanque_2”, “tanque_3”), leia a temperatura de cada um (decimal) e monte uma lista de tuplas no formato (nome_do_sensor, temperatura). Depois, calcule e exiba a média das temperaturas e a lista de registros.

sensores = ("tanque_1", "tanque_2", "tanque_3")
registros = []
for sensor in sensores:
    temperatura = float(input(f"Digite a temperatura do sensor {sensor}: "))
    registros.append((sensor, temperatura))
soma = 0
for sensor, temperatura in registros:
    soma += temperatura
media = soma / len(registros)
print(f"Registros de temperatura: {registros}")
print(f"Media geral: {media}")

Exemplo de execução:

Digite a temperatura do sensor tanque_1: 21.5
Digite a temperatura do sensor tanque_2: 23
Digite a temperatura do sensor tanque_3: 22.5
Registros de temperatura: [('tanque_1', 21.5), ('tanque_2', 23.0), ('tanque_3', 22.5)]
Media geral: 22.333333333333332

O exemplo combina estruturas de acordo com o papel de cada uma. Os nomes dos sensores são fixos, por isso ficam em uma tupla. Os registros são criados durante a execução, por isso ficam em uma lista. Cada registro é uma tupla com o nome e a temperatura; na linha 5, os parênteses internos de append((sensor, temperatura)) criam essa tupla.

O segundo for desempacota cada registro em sensor e temperatura e soma as temperaturas. A função len() devolve a quantidade de elementos da lista, que é o divisor no cálculo da média.

Registro de sensor (desempacotador_tuplas.py): Crie um programa, sem entrada do usuário, com a tupla ("sensor_7", 18.5, 4.2), que representa o código, a vazão e a pressão de um sensor. Desempacote a tupla em três variáveis e exiba a tupla completa e cada valor separado.

registro_sensor = ("sensor_7", 18.5, 4.2)
codigo_sensor, vazao, pressao = registro_sensor
print(f"Tupla completa: {registro_sensor}")
print(f"Codigo do sensor: {codigo_sensor}")
print(f"Vazao: {vazao}")
print(f"Pressao: {pressao}")

Exemplo de execução:

Tupla completa: ('sensor_7', 18.5, 4.2)
Codigo do sensor: sensor_7
Vazao: 18.5
Pressao: 4.2

A tupla funciona como uma ficha com posições fixas, em que cada posição tem um significado: código, vazão e pressão. A linha 2 desempacota os três campos em variáveis com nomes claros. O número de variáveis precisa ser igual ao número de elementos da tupla; com duas ou quatro variáveis, o programa pararia com um erro.


5.5 Iterando sobre estruturas de dados

Todas as estruturas que estudamos podem ser percorridas com for. Cada tipo tem formas idiomáticas de iteração:

  • Listas e tuplas: for elemento in sequencia percorre os valores. Use enumerate() quando precisar do índice junto com o valor.
  • Dicionários: for chave, valor in dicionario.items() percorre pares. Use .keys() para só chaves ou .values() para só valores.
  • Conjuntos: for elemento in conjunto percorre os valores (a ordem não é garantida).

Exemplo Prático (iteracao_estruturas.py): Crie um programa, sem entrada do usuário, que percorra uma lista com for simples e com enumerate, um dicionário com items, keys e values, um conjunto, uma tupla e, por fim, uma lista de tuplas com desempacotamento.

etapas = ["coletar amostra", "medir vazao", "registrar resultado"]
print("Lista com for simples:")
for etapa in etapas:
    print(f"  {etapa}")
print("Lista com enumerate:")
for indice, etapa in enumerate(etapas):
    print(f"  {indice}: {etapa}")
estoque = {"parafuso": 20, "arruela": 35, "porca": 18}
print("Dicionario com items:")
for material, quantidade in estoque.items():
    print(f"  {material}: {quantidade}")
print("Dicionario com keys:")
for material in estoque.keys():
    print(f"  {material}")
print("Dicionario com values:")
for quantidade in estoque.values():
    print(f"  {quantidade}")
areas = {"laboratorio", "oficina", "almoxarifado"}
print("Conjunto com for:")
for area in areas:
    print(f"  {area}")
coordenadas = (12.0, 7.5, 2.0)
print("Tupla com for simples:")
for valor in coordenadas:
    print(f"  {valor}")
registros = [("sensor_1", 18.2), ("sensor_2", 19.1), ("sensor_3", 17.8)]
print("Lista de tuplas com desempacotamento:")
for sensor, temperatura in registros:
    print(f"  {sensor}: {temperatura}")

Exemplo de execução:

Lista com for simples:
  coletar amostra
  medir vazao
  registrar resultado
Lista com enumerate:
  0: coletar amostra
  1: medir vazao
  2: registrar resultado
Dicionario com items:
  parafuso: 20
  arruela: 35
  porca: 18
Dicionario com keys:
  parafuso
  arruela
  porca
Dicionario com values:
  20
  35
  18
Conjunto com for:
  oficina
  laboratorio
  almoxarifado
Tupla com for simples:
  12.0
  7.5
  2.0
Lista de tuplas com desempacotamento:
  sensor_1: 18.2
  sensor_2: 19.1
  sensor_3: 17.8

O for percorre qualquer uma dessas estruturas, mas cada uma tem a sua forma mais comum de iteração. Nas listas, o enumerate() entrega o índice junto com o valor, e o for desempacota os dois em indice e etapa, sem a necessidade de um contador manual. Nos dicionários, o .items() entrega os pares de chave e valor, enquanto o .keys() e o .values() entregam só as chaves ou só os valores.

Conjuntos e tuplas são percorridos como listas, mas, nos conjuntos, a ordem dos elementos não é garantida. Por fim, em uma lista de tuplas, o for pode desempacotar cada tupla diretamente, como em for sensor, temperatura in registros.


5.6 Cuidados ao modificar estruturas durante iteração

Remover ou adicionar elementos enquanto percorre uma estrutura pode causar comportamento inesperado. Em listas, elementos são pulados silenciosamente. Em dicionários, um RuntimeError é levantado.

A regra geral é: nunca modifique a estrutura que está sendo iterada diretamente. Em vez disso, use uma destas estratégias:

  • Construa uma nova estrutura com apenas os elementos desejados.
  • Itere sobre uma cópia (com list()) e modifique a original.
  • Colete as modificações e aplique depois do laço.

Exemplo Prático (filtro_lista_seguro.py): Crie um programa, sem entrada do usuário, que remova as leituras negativas da lista [12, -1, -2, 15, -3, 18] de três formas: removendo os elementos durante a iteração, construindo uma nova lista e percorrendo uma cópia. Compare os resultados com o esperado, [12, 15, 18].

leituras = [12, -1, -2, 15, -3, 18]
copia_problema = list(leituras)
for leitura in copia_problema:
    if leitura < 0:
        copia_problema.remove(leitura)
print(f"Lista original: {leituras}")
print("Esperado: [12, 15, 18]")
print(f"Apos remocao insegura: {copia_problema}")
leituras_validas = []
for leitura in leituras:
    if leitura >= 0:
        leituras_validas.append(leitura)
print(f"Solucao 1 - nova lista: {leituras_validas}")
copia_segura = list(leituras)
for leitura in list(copia_segura):
    if leitura < 0:
        copia_segura.remove(leitura)
print(f"Solucao 2 - iterando sobre copia: {copia_segura}")

Exemplo de execução:

Lista original: [12, -1, -2, 15, -3, 18]
Esperado: [12, 15, 18]
Apos remocao insegura: [12, -2, 15, 18]
Solucao 1 - nova lista: [12, 15, 18]
Solucao 2 - iterando sobre copia: [12, 15, 18]

O programa remove as leituras negativas de três formas e compara os resultados. Na primeira, o for percorre a lista ao mesmo tempo em que o remove apaga elementos dela. Cada remoção desloca os elementos seguintes uma posição para a esquerda, enquanto o for avança para a próxima posição, e o elemento que ocupou a vaga é pulado. Por isso o -2 sobra no resultado.

As duas soluções evitam alterar a lista que está sendo percorrida. A primeira lê a lista original e monta outra apenas com os valores válidos. A segunda percorre uma cópia, criada com list(), e remove os elementos da lista original; como a cópia não muda durante o laço, nenhum elemento é pulado.

Exemplo Prático (modificacao_dicionario_seguro.py): Crie um programa, sem entrada do usuário, que parta do estoque {"parafuso": 20, "arruela": 0, "porca": 15, "bucha": 0, "rebite": 8}, remova os materiais com quantidade zero e, em uma etapa separada, adicione 25 unidades de “arruela” e “bucha”. Exiba o estoque depois de cada fase.

estoque = {"parafuso": 20, "arruela": 0, "porca": 15, "bucha": 0, "rebite": 8}
print(f"Estoque inicial: {estoque}")
for material in list(estoque.keys()):
    if estoque[material] == 0:
        del estoque[material]
        print(f"Removido '{material}' por quantidade zero")
print(f"Estoque apos remocao: {estoque}")
reabastecimento = {}
materiais_novos = ["arruela", "bucha"]
for material in materiais_novos:
    reabastecimento[material] = 25
estoque.update(reabastecimento)
print(f"Estoque apos reposicao: {estoque}")

Exemplo de execução:

Estoque inicial: {'parafuso': 20, 'arruela': 0, 'porca': 15, 'bucha': 0, 'rebite': 8}
Removido 'arruela' por quantidade zero
Removido 'bucha' por quantidade zero
Estoque apos remocao: {'parafuso': 20, 'porca': 15, 'rebite': 8}
Estoque apos reposicao: {'parafuso': 20, 'porca': 15, 'rebite': 8, 'arruela': 25, 'bucha': 25}

Apagar chaves de um dicionário enquanto ele é percorrido faz o programa parar com um erro (RuntimeError). Por isso o for da linha 3 percorre list(estoque.keys()), uma cópia das chaves, e o del pode apagar as chaves do dicionário original com segurança.

A reposição segue a estratégia de coletar antes e aplicar depois. O programa monta um dicionário separado, reabastecimento, com as novas quantidades e, só depois do laço, usa o .update() para copiar todos esses pares para o estoque de uma vez.


5.7 Qual estrutura escolher?

  • Use lista quando a ordem importa e você precisa alterar os dados.
  • Use dicionário quando precisa de acesso por chave.
  • Use conjunto quando quer evitar repetição e testar pertencimento.
  • Use tupla quando os dados são fixos e não devem ser alterados.

Exemplo Prático (seletor_estrutura.py): Crie um programa, sem entrada do usuário, que guarde em um dicionário a estrutura de dados mais adequada para cada cenário de engenharia e exiba esse mapeamento. Considere como critérios a ordem, o acesso por chave, a repetição e a imutabilidade.

recomendacoes = {
    "leituras na ordem em que chegam": "lista",
    "codigo de material para quantidade": "dicionario",
    "equipamentos sem repeticao": "conjunto",
    "coordenada fixa de um ponto": "tupla",
}
print("Estrutura recomendada por cenario:")
for cenario, estrutura in recomendacoes.items():
    print(f"- {cenario}: {estrutura}")

Exemplo de execução:

Estrutura recomendada por cenario:
- leituras na ordem em que chegam: lista
- codigo de material para quantidade: dicionario
- equipamentos sem repeticao: conjunto
- coordenada fixa de um ponto: tupla

O dicionário associa cada cenário (a chave) à estrutura mais adequada para ele (o valor) e foi escrito com um par por linha para facilitar a leitura. O for com .items() exibe cada cenário com a sua recomendação. O próprio programa é um exemplo de dicionário usado para associar uma pergunta a uma resposta.


5.8 Passando estruturas de dados para funções

No capítulo de funções vimos que o parâmetro é uma variável local: ele só existe durante a execução da função, e alterar o parâmetro não altera a variável global de mesmo nome. Isso vale para números e textos.

Com estruturas de dados a história muda, e é aqui que muita gente se confunde. Quando passamos uma lista, um dicionário ou um conjunto como argumento, a função não recebe uma cópia: ela recebe a mesma estrutura. O parâmetro é apenas um segundo nome para a estrutura que já existia fora.

Pense na estrutura como uma caixa de peças na bancada e no nome da variável como uma etiqueta colada nela. Passar a caixa para a função é entregar uma segunda etiqueta para a mesma caixa. A partir daí, duas coisas bem diferentes podem acontecer:

  • Modificar o conteúdo (lista.append(...), dicionario[chave] = valor, conjunto.add(...), lista.remove(...)) mexe dentro da caixa. Como a caixa é a mesma, a mudança é visível fora da função — mesmo sem return.
  • Reatribuir o parâmetro (lista = [...]) apenas cola a etiqueta local em uma caixa nova. A caixa original continua intacta, e quem está fora não percebe nada.

Números, textos e tuplas são imutáveis: não existe “mexer no conteúdo” deles. Qualquer operação produz um valor novo, então uma função nunca consegue alterar o valor original que recebeu.

Esse comportamento é útil quando queremos justamente que a função atualize uma estrutura compartilhada, como um estoque. Mas ele também é a origem de bugs difíceis: uma função que “só deveria consultar” acaba alterando os dados do programa inteiro. Quando quiser evitar isso, escolha uma destas estratégias:

  • Monte uma estrutura nova dentro da função e devolva com return, sem tocar na recebida.
  • Passe uma cópia (list(...), dict(...), set(...)) para a função modificar à vontade.

Exemplos Práticos:

Lista e número como argumentos (funcao_modifica_lista.py): Crie um programa, sem entrada do usuário, com a lista materiais = ["cabo", "sensor"] e a variável quantidade = 10. A função adicionar_material recebe uma lista e um item, acrescenta o item com append e exibe a lista. A função aumentar_quantidade recebe um número, soma 5 e exibe o resultado. Exiba a lista e o número antes e depois de cada chamada.

materiais = ["cabo", "sensor"]
quantidade = 10
def adicionar_material(lista, item):
    lista.append(item)
    print(f"Dentro de adicionar_material: {lista}")
def aumentar_quantidade(numero):
    numero = numero + 5
    print(f"Dentro de aumentar_quantidade: {numero}")
print(f"Lista antes da chamada: {materiais}")
adicionar_material(materiais, "multimetro")
print(f"Lista depois da chamada: {materiais}")
print(f"Quantidade antes da chamada: {quantidade}")
aumentar_quantidade(quantidade)
print(f"Quantidade depois da chamada: {quantidade}")

Exemplo de execução:

Lista antes da chamada: ['cabo', 'sensor']
Dentro de adicionar_material: ['cabo', 'sensor', 'multimetro']
Lista depois da chamada: ['cabo', 'sensor', 'multimetro']
Quantidade antes da chamada: 10
Dentro de aumentar_quantidade: 15
Quantidade depois da chamada: 10

Quando uma lista é passada para uma função, o parâmetro passa a ser um segundo nome para a mesma lista. Por isso, o append dentro de adicionar_material altera a lista materiais do programa principal, e a mudança aparece fora da função mesmo sem return.

Com o número, o comportamento é diferente. Em aumentar_quantidade, a soma cria um valor novo, guardado apenas no parâmetro local, já que números não podem ser alterados por dentro. Depois da chamada, quantidade continua valendo 10.

Funções que alteram o estoque (funcao_modifica_dicionario.py): Crie um programa, sem entrada do usuário, com o dicionário estoque = {"cabo": 4, "sensor": 0, "fusivel": 12}. A função repor_material recebe o dicionário, o nome de um material e uma quantidade e soma a quantidade ao valor atual. A função remover_esgotados recebe o dicionário e apaga as chaves com valor zero. Nenhuma das duas usa return. Exiba o estoque no início, depois de repor 6 cabos e depois de remover os esgotados.

estoque = {"cabo": 4, "sensor": 0, "fusivel": 12}
def repor_material(inventario, material, quantidade):
    inventario[material] = inventario[material] + quantidade
def remover_esgotados(inventario):
    for material in list(inventario.keys()):
        if inventario[material] == 0:
            del inventario[material]
print(f"Estoque inicial: {estoque}")
repor_material(estoque, "cabo", 6)
print(f"Depois da reposicao: {estoque}")
remover_esgotados(estoque)
print(f"Depois de remover esgotados: {estoque}")

Exemplo de execução:

Estoque inicial: {'cabo': 4, 'sensor': 0, 'fusivel': 12}
Depois da reposicao: {'cabo': 10, 'sensor': 0, 'fusivel': 12}
Depois de remover esgotados: {'cabo': 10, 'fusivel': 12}

As duas funções recebem o dicionário no parâmetro inventario, que é um segundo nome para o mesmo estoque. A função repor_material soma a quantidade ao valor da chave, e remover_esgotados apaga as chaves zeradas. Nenhuma delas tem return, mas as mudanças aparecem no programa principal, porque as funções alteram o conteúdo do dicionário que receberam. Em remover_esgotados, o laço percorre uma cópia das chaves pelo mesmo motivo do exemplo modificacao_dicionario_seguro.py.

Reatribuir ou modificar (reatribuicao_vs_modificacao.py): Crie um programa, sem entrada do usuário, com a lista leituras = [10, 12, 15] e duas funções que tentam “zerar” a lista recebida. A função zerar_por_reatribuicao atribui [0] ao parâmetro; a função zerar_por_modificacao esvazia a lista com clear e acrescenta 0 com append. Ambas exibem o parâmetro. Exiba a lista antes das chamadas e depois de cada uma. Antes de executar, escreva a saída que você espera.

leituras = [10, 12, 15]
def zerar_por_reatribuicao(lista):
    lista = [0]
    print(f"Dentro de zerar_por_reatribuicao: {lista}")
def zerar_por_modificacao(lista):
    lista.clear()
    lista.append(0)
    print(f"Dentro de zerar_por_modificacao: {lista}")
print(f"Leituras antes: {leituras}")
zerar_por_reatribuicao(leituras)
print(f"Depois de zerar_por_reatribuicao: {leituras}")
zerar_por_modificacao(leituras)
print(f"Depois de zerar_por_modificacao: {leituras}")

Exemplo de execução:

Leituras antes: [10, 12, 15]
Dentro de zerar_por_reatribuicao: [0]
Depois de zerar_por_reatribuicao: [10, 12, 15]
Dentro de zerar_por_modificacao: [0]
Depois de zerar_por_modificacao: [0]

As duas funções tentam “zerar” a lista recebida, e a diferença está no uso do =. Em zerar_por_reatribuicao, a atribuição lista = [0] cria uma lista nova e faz apenas o nome local apontar para ela; a lista do programa principal não é tocada e continua [10, 12, 15].

Em zerar_por_modificacao, nenhuma lista nova é criada. O .clear() esvazia a lista recebida e o .append(0) acrescenta o zero, alterando o conteúdo da mesma lista do programa principal, que passa a valer [0].

Preservando o original (funcao_protege_original.py): Crie um programa, sem entrada do usuário, com a lista leituras = [12, -1, 15, -3] e a tupla limites = (0, 100).

  1. A função filtrar_validas recebe uma lista e retorna uma lista nova apenas com os valores maiores ou iguais a zero.
  2. A função descartar_invalidas recebe uma lista e remove dela os valores negativos. Chame essa função com uma cópia feita com list().
  3. A função ampliar_limites recebe uma tupla e retorna uma tupla nova com o segundo valor somado a 50.

Mostre, em cada caso, que a estrutura original não foi alterada.

leituras = [12, -1, 15, -3]
limites = (0, 100)
def filtrar_validas(lista):
    validas = []
    for leitura in lista:
        if leitura >= 0:
            validas.append(leitura)
    return validas
def descartar_invalidas(lista):
    for leitura in list(lista):
        if leitura < 0:
            lista.remove(leitura)
def ampliar_limites(faixa):
    nova_faixa = (faixa[0], faixa[1] + 50)
    return nova_faixa
leituras_validas = filtrar_validas(leituras)
print(f"Lista original: {leituras}")
print(f"Lista retornada: {leituras_validas}")
copia = list(leituras)
descartar_invalidas(copia)
print(f"Copia modificada: {copia}")
print(f"Lista original preservada: {leituras}")
novos_limites = ampliar_limites(limites)
print(f"Tupla original: {limites}")
print(f"Tupla retornada: {novos_limites}")

Exemplo de execução:

Lista original: [12, -1, 15, -3]
Lista retornada: [12, 15]
Copia modificada: [12, 15]
Lista original preservada: [12, -1, 15, -3]
Tupla original: (0, 100)
Tupla retornada: (0, 150)

O programa mostra as duas estratégias para preservar a estrutura original. Na primeira, filtrar_validas não altera a lista recebida: ela monta uma lista nova com os valores válidos e devolve essa lista com return. Na segunda, descartar_invalidas altera a lista que recebe, mas o programa entrega a ela uma cópia feita com list(), de modo que só a cópia é modificada.

Com a tupla, não há o que proteger. Tuplas não podem ser alteradas, então ampliar_limites só consegue criar e devolver uma tupla nova, (0, 150), e a original continua (0, 100).