Nesta aula vamos dominar as duas estruturas de dados mais presentes no Python do dia a dia: listas e tuplas. A ideia é entender não só a sintaxe de cada uma, mas o que as diferencia, quais métodos importam de verdade e por que essa escolha aparece o tempo todo em projetos de dados e IA.
Começando pela justificativa, porque ela dá sentido ao resto. Em Machine Learning você lida com estruturas de dados o tempo inteiro: datasets são listas de registros, as camadas de uma rede neural são listas de módulos, os pesos de um modelo ficam guardados em arrays — que por baixo são listas otimizadas —, predições chegam como listas de valores e um grid search nada mais é que listas de opções combinadas. Entender a fundo como listas funcionam em Python é a base para entender os arrays do NumPy e os tensores do PyTorch, que são evoluções diretas desse conceito.
Uma lista é uma coleção ordenada, mutável, capaz de guardar elementos de tipos diferentes — e é a estrutura mais usada da linguagem. Criar uma é questão de colchetes:
# Lista vazia
vazia = []
vazia2 = list()
# Lista de inteiros
notas = [7.5, 8.0, 6.5, 9.0, 5.5]
# Lista de strings
nomes = ["Ana", "Bruno", "Carla"]
# Lista mista (possível, mas evite em dados)
mista = [1, "texto", 3.14, True, None]
# Lista de listas (matriz)
matriz = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
O acesso é por índice, começando em zero. Índices negativos contam a partir do final, o que evita aquela aritmética com len() - 1 comum em outras linguagens:
frutas = ["maçã", "banana", "laranja", "uva", "melão"]
print(frutas[0]) # maçã — primeiro elemento
print(frutas[2]) # laranja — terceiro elemento
print(frutas[-1]) # melão — último elemento
print(frutas[-2]) # uva — penúltimo elemento
Um degrau acima do índice está o fatiamento, que devolve uma sublista. A sintaxe é lista[inicio:fim:passo], e o detalhe que mais confunde no começo é que o índice fim fica de fora:
numeros = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(numeros[2:5]) # [2, 3, 4] — do índice 2 ao 4
print(numeros[:4]) # [0, 1, 2, 3] — do início ao índice 3
print(numeros[6:]) # [6, 7, 8, 9] — do índice 6 ao final
print(numeros[::2]) # [0, 2, 4, 6, 8] — de 2 em 2
print(numeros[::-1]) # [9, 8, 7, 6, 5, 4, 3, 2, 1, 0] — invertida
print(numeros[1:8:3]) # [1, 4, 7] — do 1 ao 7, de 3 em 3
Esse recurso é a forma mais direta de separar um conjunto de dados em treino e teste, operação que você vai repetir à exaustão:
dados = list(range(100)) # 100 exemplos
treino = dados[:80] # 80% para treino
teste = dados[80:] # 20% para teste
print(f"Treino: {len(treino)} exemplos")
print(f"Teste: {len(teste)} exemplos")
Saída:
Treino: 80 exemplos
Teste: 20 exemplos
Por serem mutáveis, listas aceitam atribuição tanto em posições individuais quanto em intervalos inteiros:
notas = [7.0, 8.0, 6.0, 9.0]
notas[2] = 7.5 # altera o terceiro elemento
print(notas) # [7.0, 8.0, 7.5, 9.0]
notas[1:3] = [8.5, 8.0] # altera um intervalo
print(notas) # [7.0, 8.5, 8.0, 9.0]
O repertório de métodos é grande, mas se organiza bem por finalidade. Para acrescentar elementos há três caminhos, e a diferença entre eles importa: append coloca um item no fim, insert escolhe a posição e extend incorpora vários de uma vez a partir de outro iterável.
linguagens = ["Python", "Java"]
# append: adiciona um elemento ao final
linguagens.append("R")
print(linguagens) # ['Python', 'Java', 'R']
# insert: insere em uma posição específica
linguagens.insert(1, "Julia")
print(linguagens) # ['Python', 'Julia', 'Java', 'R']
# extend: adiciona múltiplos elementos de outro iterável
linguagens.extend(["Scala", "C++"])
print(linguagens) # ['Python', 'Julia', 'Java', 'R', 'Scala', 'C++']
Do lado da remoção, a escolha depende de você saber o valor ou a posição: remove apaga a primeira ocorrência de um valor, pop tira pelo índice e ainda devolve o elemento, del funciona por índice ou fatia e clear esvazia tudo.
frutas = ["maçã", "banana", "laranja", "banana", "uva"]
# remove: remove a primeira ocorrência do valor
frutas.remove("banana")
print(frutas) # ['maçã', 'laranja', 'banana', 'uva']
# pop: remove e retorna o elemento no índice (padrão: último)
ultimo = frutas.pop()
print(ultimo) # uva
print(frutas) # ['maçã', 'laranja', 'banana']
segundo = frutas.pop(1)
print(segundo) # laranja
print(frutas) # ['maçã', 'banana']
# del: remove por índice ou fatia
numeros = [1, 2, 3, 4, 5]
del numeros[2]
print(numeros) # [1, 2, 4, 5]
del numeros[1:3]
print(numeros) # [1, 5]
# clear: remove todos os elementos
numeros.clear()
print(numeros) # []
Para localizar e contabilizar valores, count e index resolvem, e o operador in responde à pergunta mais frequente de todas:
notas = [7.0, 8.5, 6.0, 9.0, 8.5, 7.0, 8.5]
print(notas.count(8.5)) # 3 — quantas vezes aparece
print(notas.index(9.0)) # 3 — índice da primeira ocorrência
print(9.0 in notas) # True — verifica se existe
print(10.0 in notas) # False
print(10.0 not in notas) # True
Na ordenação mora uma distinção que vale memorizar: sort() reordena a lista original no lugar, enquanto sorted() devolve uma lista nova e preserva a de partida. Ambos aceitam reverse e um key para critérios personalizados:
numeros = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3]
# sort: ordena in-place (modifica a lista original)
numeros.sort()
print(numeros) # [1, 1, 2, 3, 3, 4, 5, 5, 6, 9]
numeros.sort(reverse=True)
print(numeros) # [9, 6, 5, 5, 4, 3, 3, 2, 1, 1]
# sorted: retorna nova lista, não modifica a original
original = [3, 1, 4, 1, 5]
nova = sorted(original)
print(original) # [3, 1, 4, 1, 5] — inalterada
print(nova) # [1, 1, 3, 4, 5]
# Ordenando por critério customizado
alunos = [("Ana", 8.5), ("Bruno", 6.0), ("Carla", 9.2)]
alunos.sort(key=lambda a: a[1], reverse=True)
print(alunos) # [('Carla', 9.2), ('Ana', 8.5), ('Bruno', 6.0)]
Somam-se a isso as funções embutidas de agregação e mais dois métodos de uso constante, reverse e copy:
numeros = [1, 2, 3, 4, 5]
print(len(numeros)) # 5 — tamanho
print(sum(numeros)) # 15 — soma
print(min(numeros)) # 1 — mínimo
print(max(numeros)) # 5 — máximo
# reverse: inverte in-place
numeros.reverse()
print(numeros) # [5, 4, 3, 2, 1]
# copy: cópia superficial
copia = numeros.copy()
copia.append(0)
print(numeros) # [5, 4, 3, 2, 1] — original inalterado
print(copia) # [5, 4, 3, 2, 1, 0]
A menção a cópia leva direto a um dos erros mais comuns em Python, e especialmente perigoso em ML, onde você manipula datasets grandes: atribuir uma lista a outra variável não cria cópia alguma. Os dois nomes passam a apontar para o mesmo objeto, e modificar por um afeta o outro.
# ERRADO — ambas apontam para o mesmo objeto
original = [1, 2, 3, 4, 5]
alias = original # não é uma cópia!
alias.append(99)
print(original) # [1, 2, 3, 4, 5, 99] — original foi modificado!
# CORRETO — formas de fazer uma cópia
copia1 = original.copy()
copia2 = original[:] # slicing completo
copia3 = list(original)
copia1.append(100)
print(original) # inalterado
print(copia1) # tem o 100
E há uma segunda camada nesse problema: quando a lista contém outras listas, a cópia superficial duplica apenas o invólucro externo. As sublistas continuam compartilhadas, e é aí que entra o copy.deepcopy():
import copy
matriz = [[1, 2], [3, 4], [5, 6]]
rasa = matriz.copy()
rasa[0].append(99)
print(matriz) # [[1, 2, 99], [3, 4], [5, 6]] — matriz original afetada!
profunda = copy.deepcopy(matriz)
profunda[0].append(100)
print(matriz) # [[1, 2, 99], [3, 4], [5, 6]] — inalterada
O básico de list comprehension já apareceu em Loops — for, while, break, continue e range(); aqui vale explorar as formas mais elaboradas. Combinando com o operador ternário, a comprehension classifica em vez de apenas filtrar:
numeros = range(-5, 6)
classificados = ["positivo" if n > 0 else "zero" if n == 0 else "negativo"
for n in numeros]
print(classificados)
Saída:
['negativo', 'negativo', 'negativo', 'negativo', 'negativo', 'zero', 'positivo', 'positivo', 'positivo', 'positivo', 'positivo']
Aninhando uma comprehension dentro da outra, você constrói matrizes sem escrever loops explícitos:
# Criando uma matriz 3x3 de zeros
matriz = [[0 for j in range(3)] for i in range(3)]
print(matriz) # [[0, 0, 0], [0, 0, 0], [0, 0, 0]]
# Tabela de multiplicação
tabela = [[i * j for j in range(1, 4)] for i in range(1, 4)]
for linha in tabela:
print(linha)
Saída:
[1, 2, 3]
[2, 4, 6]
[3, 6, 9]
E com dois for na mesma comprehension, o caminho inverso: achatar uma lista de listas numa sequência única.
matriz = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
achatada = [elemento for linha in matriz for elemento in linha]
print(achatada) # [1, 2, 3, 4, 5, 6, 7, 8, 9]
Do outro lado da moeda estão as tuplas: parecidas com listas, mas imutáveis — depois de criadas, não mudam. São definidas com parênteses ou, na verdade, pela simples presença das vírgulas.
# Com parênteses
coordenada = (10.5, 20.3)
rgb = (255, 128, 0)
# Sem parênteses (a vírgula é que cria a tupla)
ponto = 3, 7
print(type(ponto)) # <class 'tuple'>
# Tupla com um elemento (a vírgula é obrigatória)
unitaria = (42,)
print(type(unitaria)) # <class 'tuple'>
nao_tupla = (42)
print(type(nao_tupla)) # <class 'int'>
# Tupla vazia
vazia = ()
vazia2 = tuple()
Repare no caso da tupla de um elemento só: sem a vírgula, os parênteses são apenas agrupamento matemático e você acaba com um inteiro. É uma pegadinha clássica.
Para leitura, tudo funciona como nas listas — indexação, fatiamento e len():
cores = ("vermelho", "verde", "azul")
print(cores[0]) # vermelho
print(cores[-1]) # azul
print(cores[1:]) # ('verde', 'azul')
print(len(cores)) # 3
A diferença aparece na escrita, que simplesmente não é permitida:
ponto = (3, 7)
ponto[0] = 10 # TypeError: 'tuple' object does not support item assignment
Convém entender bem o alcance dessa imutabilidade: ela vale para os vínculos da tupla, não para o conteúdo dos objetos que ela guarda. Se um deles for mutável, como uma lista, esse objeto continua modificável por dentro:
dados = ([1, 2, 3], [4, 5, 6])
dados[0].append(99) # isso funciona!
print(dados) # ([1, 2, 3, 99], [4, 5, 6])
dados[0] = [7, 8, 9] # isso não funciona — TypeError
Onde as tuplas realmente brilham é no desempacotamento, um dos recursos mais elegantes da linguagem. Ele distribui os elementos em variáveis de uma só vez, aceita um asterisco para capturar o meio e usa o sublinhado como convenção para descartar o que não interessa:
ponto = (10, 20)
x, y = ponto
print(f"x={x}, y={y}") # x=10, y=20
# Desempacotamento com *
primeiro, *meio, ultimo = (1, 2, 3, 4, 5)
print(primeiro) # 1
print(meio) # [2, 3, 4]
print(ultimo) # 5
# Muito usado para ignorar valores
_, segundo, _ = (10, 20, 30)
print(segundo) # 20
Em loops, esse mesmo mecanismo é o que permite iterar sobre pares já separando os componentes — padrão onipresente em código de ML:
pares = [(1, "um"), (2, "dois"), (3, "três")]
for numero, nome in pares:
print(f"{numero} = {nome}")
Saída:
1 = um
2 = dois
3 = três
Justamente por serem imutáveis, tuplas têm só dois métodos:
numeros = (1, 2, 3, 2, 4, 2, 5)
print(numeros.count(2)) # 3 — quantas vezes 2 aparece
print(numeros.index(4)) # 4 — índice da primeira ocorrência de 4
Com as duas estruturas na mesa, resta a decisão de projeto: quando usar cada uma. A lista é a escolha quando a coleção vai mudar — receber, perder ou alterar elementos —, quando os elementos são homogêneos e quando você a constrói dinamicamente. É o caso de exemplos de treino, do histórico de perdas ao longo do treinamento e dos resultados de predição.
A tupla entra quando os dados não devem mudar, quando você quer deixar explícito que aquilo é constante, quando as posições têm significado fixo — coordenadas x e y, um par chave-valor, um RGB — e quando a coleção precisa servir de chave de dicionário, algo que listas não podem fazer. Pense em dimensões de uma imagem, coordenadas geográficas ou configurações fixas de um modelo.
Esse último ponto é uma consequência direta da imutabilidade e vale ver funcionando:
# Tupla como chave de dicionário — funciona
cache = {}
cache[(3, 3)] = "valor para entrada (3,3)"
# Lista como chave — não funciona
cache[[3, 3]] = "valor" # TypeError: unhashable type: 'list'
Boa parte das operações, no entanto, é comum às duas estruturas: concatenar, repetir, testar pertencimento, comparar e converter de uma para a outra.
a = [1, 2, 3]
b = [4, 5, 6]
# Concatenação
print(a + b) # [1, 2, 3, 4, 5, 6]
# Repetição
print(a * 3) # [1, 2, 3, 1, 2, 3, 1, 2, 3]
# Verificação de pertencimento
print(2 in a) # True
print(10 not in a) # True
# Comparação
print([1, 2, 3] == [1, 2, 3]) # True
print([1, 2, 3] == [1, 2, 4]) # False
# Conversão entre lista e tupla
lista = [1, 2, 3]
tupla = tuple(lista)
de_volta = list(tupla)
Vale ainda reunir as funções embutidas que acompanham essas estruturas. Além das agregações e da ordenação, note any() e all(), que resumem uma lista de booleanos numa resposta só e são excelentes para validação de dados:
numeros = [4, 2, 7, 1, 9, 3, 6, 5, 8]
print(len(numeros)) # 9
print(sum(numeros)) # 45
print(min(numeros)) # 1
print(max(numeros)) # 9
print(sorted(numeros)) # [1, 2, 3, 4, 5, 6, 7, 8, 9]
print(list(reversed(numeros))) # [8, 5, 6, 3, 9, 1, 7, 2, 4]
# enumerate: índice + valor
for i, n in enumerate(numeros):
print(f"[{i}] = {n}")
# zip: combinar listas
nomes = ["Ana", "Bruno", "Carla"]
notas = [8.5, 7.0, 9.2]
for nome, nota in zip(nomes, notas):
print(f"{nome}: {nota}")
# any e all — muito úteis em validação de dados
flags = [True, True, False, True]
print(any(flags)) # True — pelo menos um True
print(all(flags)) # False — nem todos são True
dados_validos = [x > 0 for x in [1, 2, 3, 4]]
print(all(dados_validos)) # True — todos positivos
Para amarrar o conjunto, um exemplo que simula operações típicas sobre um dataset de ML — separar features e rótulos, dividir treino e teste, calcular estatísticas e normalizar — tudo com listas, tuplas e comprehensions:
# Dataset: lista de tuplas (features, label)
dataset = [
(1.2, 3.4, 0),
(2.1, 1.5, 1),
(0.8, 4.2, 0),
(3.3, 2.8, 1),
(1.9, 3.1, 0),
(2.7, 1.2, 1),
(0.5, 4.8, 0),
(3.1, 2.0, 1),
(1.5, 3.9, 0),
(2.9, 1.8, 1),
]
# Separar features e labels
features = [(x, y) for x, y, _ in dataset]
labels = [label for _, _, label in dataset]
print(f"Total de exemplos: {len(dataset)}")
print(f"Distribuição de classes: {labels.count(0)} negativos, {labels.count(1)} positivos")
# Dividir em treino (80%) e teste (20%)
corte = int(len(dataset) * 0.8)
treino = dataset[:corte]
teste = dataset[corte:]
print(f"\nTreino: {len(treino)} exemplos")
print(f"Teste: {len(teste)} exemplos")
# Extrair todas as features x1 para calcular estatísticas
x1_valores = [x for x, y, _ in dataset]
media_x1 = sum(x1_valores) / len(x1_valores)
print(f"\nEstatísticas de x1:")
print(f" Média: {media_x1:.2f}")
print(f" Mínimo: {min(x1_valores):.2f}")
print(f" Máximo: {max(x1_valores):.2f}")
# Normalizar x1 para intervalo [0, 1]
min_x1 = min(x1_valores)
max_x1 = max(x1_valores)
x1_normalizado = [(x - min_x1) / (max_x1 - min_x1) for x in x1_valores]
print(f"\nx1 normalizado: {[round(v, 2) for v in x1_normalizado]}")
Saída:
Total de exemplos: 10
Distribuição de classes: 5 negativos, 5 positivos
Treino: 8 exemplos
Teste: 2 exemplos
Estatísticas de x1:
Média: 2.00
Mínimo: 0.50
Máximo: 3.30
x1 normalizado: [0.25, 0.59, 0.11, 1.0, 0.5, 0.79, 0.0, 0.93, 0.36, 0.86]
Fixando o essencial: listas são coleções ordenadas e mutáveis, indexadas a partir do zero e com índices negativos contando do fim; o fatiamento [inicio:fim:passo] produz sublistas sem alterar a original; o núcleo de métodos gira em torno de append, insert, extend, remove, pop, sort, index e count. Atribuir uma lista a outra variável não copia nada — use .copy(), [:] ou, para estruturas aninhadas, copy.deepcopy(). Tuplas são imutáveis e brilham no desempacotamento e como chaves de dicionário. E any() com all() resolvem validações de forma compacta.
Os seis exercícios seguintes exercitam fatiamento, métodos de lista, a armadilha da cópia e o desempacotamento de tuplas. Resolva antes de abrir a resposta comentada.
-
Dada a lista abaixo, use slicing para extrair três sublistas: os três primeiros elementos, os três últimos elementos, e os elementos de índice par (0, 2, 4, 6...).
letras = ["a", "b", "c", "d", "e", "f", "g", "h"]✓ Resposta:letras = ["a", "b", "c", "d", "e", "f", "g", "h"] tres_primeiros = letras[:3] print(tres_primeiros) # ['a', 'b', 'c'] tres_ultimos = letras[-3:] print(tres_ultimos) # ['f', 'g', 'h'] indices_pares = letras[::2] print(indices_pares) # ['a', 'c', 'e', 'g'] -
Escreva uma função
remover_duplicatas(lista)que receba uma lista e retorne uma nova lista com os elementos únicos, mantendo a ordem original de aparição. Não useset()diretamente na solução principal — implemente com um loop. Depois mostre como ficaria comset()e explique por que a versão com set não garante a ordem.✓ Resposta:# Versão com loop — preserva a ordem def remover_duplicatas(lista): vistos = [] for elemento in lista: if elemento not in vistos: vistos.append(elemento) return vistos # Versão com set — NÃO garante ordem def remover_duplicatas_set(lista): return list(set(lista)) dados = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5] print(remover_duplicatas(dados)) # [3, 1, 4, 5, 9, 2, 6] — ordem preservada print(remover_duplicatas_set(dados)) # ordem imprevisível, ex: [1, 2, 3, 4, 5, 6, 9]A versão com
set()não garante ordem porque sets são coleções não ordenadas em Python. A partir do Python 3.7, dicionários preservam ordem de inserção, mas sets não têm essa garantia. Em ML, se você está removendo duplicatas de IDs de exemplos ou de categorias, a ordem pode ser importante para manter correspondência com outras listas. -
Explique o que será impresso e por quê:
a = [1, 2, 3] b = a c = a.copy() b.append(4) c.append(5) print(a) print(b) print(c)✓ Resposta:a → [1, 2, 3, 4] b → [1, 2, 3, 4] c → [1, 2, 3, 5]b = anão cria uma cópia —beaapontam para o mesmo objeto na memória. Quandob.append(4)é chamado, a lista subjacente é modificada, e tantoaquantobrefletem essa mudança.c = a.copy()cria uma cópia independente. Quandoc.append(5)é chamado, apenascé afetado. A lista originala(e portantobtambém) permanece sem o 5. -
Use list comprehension aninhada para criar a matriz identidade 4x4 (1s na diagonal principal, 0s no resto). O resultado deve ser:
[[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]✓ Resposta:identidade = [[1 if i == j else 0 for j in range(4)] for i in range(4)] for linha in identidade: print(linha)Saída:
[1, 0, 0, 0] [0, 1, 0, 0] [0, 0, 1, 0] [0, 0, 0, 1]A expressão
1 if i == j else 0coloca 1 quando o índice da linha é igual ao índice da coluna (diagonal principal) e 0 caso contrário. -
Dado o dataset abaixo como lista de tuplas
(nome, nota1, nota2, nota3), use desempacotamento e list comprehension para criar uma lista de tuplas(nome, media)ordenada da maior para a menor média.alunos = [ ("Ana", 8.0, 7.5, 9.0), ("Bruno", 6.0, 5.5, 7.0), ("Carla", 9.5, 9.0, 8.5), ("Diego", 7.0, 8.0, 7.5), ]✓ Resposta:alunos = [ ("Ana", 8.0, 7.5, 9.0), ("Bruno", 6.0, 5.5, 7.0), ("Carla", 9.5, 9.0, 8.5), ("Diego", 7.0, 8.0, 7.5), ] ranking = [(nome, (n1 + n2 + n3) / 3) for nome, n1, n2, n3 in alunos] ranking_ordenado = sorted(ranking, key=lambda x: x[1], reverse=True) for nome, media in ranking_ordenado: print(f"{nome}: {media:.2f}")Saída:
Carla: 9.00 Ana: 8.17 Diego: 7.50 Bruno: 6.17 -
Explique a diferença entre
lista.sort()esorted(lista). Em que situação você preferiria cada um? Dê um exemplo concreto de ML onde a escolha importa.✓ Resposta:lista.sort()ordena a lista in-place: modifica a lista original e retornaNone. Não cria nenhum objeto novo na memória.sorted(lista)retorna uma nova lista ordenada sem modificar a original. A lista original permanece intacta.Em ML, a escolha importa nas seguintes situações:
Prefira
sorted()quando você precisa manter a lista original, por exemplo ao dividir um dataset em treino e teste e querer ver tanto a versão original quanto a ordenada por algum critério:historico_perdas = [0.95, 0.82, 0.74, 0.68, 0.61] melhores_perdas = sorted(historico_perdas) # historico_perdas ainda preserva a ordem cronológicaPrefira
.sort()quando a lista original não é mais necessária e você quer economizar memória, por exemplo ao ordenar um grande dataset por data antes de processamento sequencial:eventos = carregar_eventos() # lista enorme eventos.sort(key=lambda e: e["timestamp"]) # processa na ordem correta sem criar cópia
Para aprofundar:
- Documentação oficial do Python — Listas: https://docs.python.org/3/tutorial/datastructures.html#more-on-lists
- Documentação oficial do Python — Tuplas e sequências: https://docs.python.org/3/tutorial/datastructures.html#tuples-and-sequences
- Documentação oficial do Python — Módulo copy: https://docs.python.org/3/library/copy.html
- PEP 202 — List Comprehensions: https://peps.python.org/pep-0202/
- Real Python — Lists and Tuples in Python: https://realpython.com/python-lists-tuples/
- Real Python — How to Use sorted() and sort() in Python: https://realpython.com/python-sort/
- Real Python — Shallow vs Deep Copy: https://realpython.com/copying-python-objects/