Depois de listas e tuplas, faltam duas estruturas para completar o repertório essencial do Python: o dicionário, que associa chaves a valores, e o set, que guarda elementos únicos. Este artigo mostra como usar cada uma, quais métodos realmente importam e por que a escolha entre elas costuma decidir a legibilidade — e às vezes a velocidade — do seu código.

Dicionários são difíceis de evitar em projetos de ML. Configurações de modelo e hiperparâmetros são dicionários; resultados de avaliação vêm como dicionários; vocabulários de NLP mapeiam palavras para índices com dicionários; batches no PyTorch chegam com chaves como "input_ids", "labels" e "attention_mask"; e JSON, formato padrão de APIs e datasets, converte diretamente para dicionário em Python.

Um dicionário é uma coleção de pares chave-valor, mutável, sem chaves repetidas e — desde o Python 3.7 — ordenada pela sequência de inserção. Há vários caminhos para criar um:

# Dicionário vazio
vazio = {}
vazio2 = dict()

# Com valores iniciais
modelo = {
    "nome": "ResNet50",
    "camadas": 50,
    "acuracia": 0.923,
    "treinado": True
}

# Usando dict() com argumentos nomeados
config = dict(learning_rate=0.001, batch_size=32, epocas=100)

# A partir de listas de tuplas
pares = [("a", 1), ("b", 2), ("c", 3)]
dicionario = dict(pares)
print(dicionario)  # {'a': 1, 'b': 2, 'c': 3}

Para ler um valor existem duas vias, e a diferença entre elas é o que separa código frágil de código robusto. O acesso por colchetes levanta KeyError quando a chave não existe; o método .get() devolve None ou um padrão que você escolhe.

modelo = {
    "nome": "ResNet50",
    "camadas": 50,
    "acuracia": 0.923
}

# Acesso direto — gera KeyError se a chave não existir
print(modelo["nome"])      # ResNet50
print(modelo["camadas"])   # 50

# Acesso seguro com .get() — retorna None ou valor padrão se não existir
print(modelo.get("acuracia"))          # 0.923
print(modelo.get("autor"))             # None
print(modelo.get("autor", "desconhecido"))  # desconhecido

Em código de produção, .get() costuma ser a escolha preferida justamente por evitar exceções inesperadas quando a presença da chave é incerta.

Escrever é mais simples: atribuir a uma chave inexistente a cria, atribuir a uma existente a sobrescreve, e update aplica várias mudanças de uma vez.

config = {"lr": 0.01, "epocas": 100}

# Adicionar nova chave
config["batch_size"] = 32
config["optimizer"] = "adam"

# Modificar valor existente
config["lr"] = 0.001

print(config)
# {'lr': 0.001, 'epocas': 100, 'batch_size': 32, 'optimizer': 'adam'}

# update: atualiza múltiplos valores de uma vez
config.update({"epocas": 200, "dropout": 0.5, "lr": 0.0001})
print(config)

Na remoção, cada método tem sua vocação: pop devolve o valor removido e aceita um padrão para chaves ausentes, del apaga sem devolver nada, popitem retira o último par inserido e clear zera tudo.

dados = {"a": 1, "b": 2, "c": 3, "d": 4}

# pop: remove e retorna o valor
valor = dados.pop("b")
print(valor)   # 2
print(dados)   # {'a': 1, 'c': 3, 'd': 4}

# pop com valor padrão — evita KeyError
valor = dados.pop("z", None)
print(valor)   # None — chave não existia

# del: remove por chave
del dados["c"]
print(dados)   # {'a': 1, 'd': 4}

# popitem: remove e retorna o último par inserido
ultimo = dados.popitem()
print(ultimo)  # ('d', 4)
print(dados)   # {'a': 1}

# clear: remove tudo
dados.clear()
print(dados)   # {}

Percorrer um dicionário pode significar três coisas diferentes, e o Python deixa você escolher: iterar direto sobre ele entrega as chaves, .values() entrega os valores e .items() entrega os pares — esta última sendo a forma mais usada no dia a dia.

modelo = {
    "nome": "BERT",
    "camadas": 12,
    "parametros": 110000000,
    "acuracia": 0.891
}

# Iterando sobre chaves (padrão)
for chave in modelo:
    print(chave)

# Iterando sobre valores
for valor in modelo.values():
    print(valor)

# Iterando sobre pares chave-valor (mais comum)
for chave, valor in modelo.items():
    print(f"{chave}: {valor}")

Saída de .items():

nome: BERT
camadas: 12
parametros: 110000000
acuracia: 0.891

Antes de acessar uma chave incerta, o operador in responde se ela existe — e é a alternativa ao .get() quando você precisa ramificar a lógica:

config = {"lr": 0.001, "batch_size": 32}

print("lr" in config)           # True
print("dropout" in config)      # False
print("dropout" not in config)  # True

# Sempre verifique antes de acessar diretamente se a chave pode não existir
if "dropout" in config:
    print(config["dropout"])
else:
    print("dropout não configurado, usando padrão 0.0")

Reunindo os métodos que valem memorizar, além dos três de iteração aparecem setdefault, que lê ou insere num gesto só, e copy, com as mesmas ressalvas de cópia superficial que já vimos nas listas:

metricas = {"acuracia": 0.92, "precisao": 0.89, "recall": 0.94, "f1": 0.91}

# keys: retorna todas as chaves
print(list(metricas.keys()))
# ['acuracia', 'precisao', 'recall', 'f1']

# values: retorna todos os valores
print(list(metricas.values()))
# [0.92, 0.89, 0.94, 0.91]

# items: retorna pares (chave, valor)
print(list(metricas.items()))
# [('acuracia', 0.92), ('precisao', 0.89), ('recall', 0.94), ('f1', 0.91)]

# setdefault: retorna o valor se a chave existir, senão insere com valor padrão
metricas.setdefault("auc", 0.0)
print(metricas["auc"])   # 0.0

# copy: cópia superficial
copia = metricas.copy()

Assim como listas têm suas comprehensions, dicionários têm as suas, escritas como {chave: valor for item in iteravel}. Elas constroem, invertem e filtram em uma linha:

# Sintaxe: {chave: valor for item in iteravel}

# Quadrados de 1 a 5
quadrados = {x: x**2 for x in range(1, 6)}
print(quadrados)
# {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}

# Invertendo um dicionário (troca chaves e valores)
original = {"a": 1, "b": 2, "c": 3}
invertido = {v: k for k, v in original.items()}
print(invertido)
# {1: 'a', 2: 'b', 3: 'c'}

# Com filtro
metricas = {"acuracia": 0.92, "precisao": 0.75, "recall": 0.94, "f1": 0.83}
boas_metricas = {k: v for k, v in metricas.items() if v >= 0.9}
print(boas_metricas)
# {'acuracia': 0.92, 'recall': 0.94}

Um uso concreto que você vai reencontrar em processamento de linguagem natural é a construção de vocabulário, mapeando cada palavra única a um índice numérico:

texto = "o gato comeu o peixe e o rato fugiu"
palavras = texto.split()

# Mapear cada palavra única para um índice
vocabulario = {palavra: idx for idx, palavra in enumerate(set(palavras))}
print(vocabulario)
# {'peixe': 0, 'rato': 1, 'o': 2, 'e': 3, 'comeu': 4, 'gato': 5, 'fugiu': 6}

Como os valores podem ser de qualquer tipo, nada impede que sejam outros dicionários — e é assim que se representam configurações hierárquicas de experimentos:

experimento = {
    "nome": "experimento_01",
    "modelo": {
        "arquitetura": "transformer",
        "camadas": 6,
        "heads": 8,
        "dim": 512
    },
    "treinamento": {
        "optimizer": "adam",
        "learning_rate": 0.0001,
        "batch_size": 64,
        "epocas": 50
    },
    "resultados": {
        "treino": {"loss": 0.12, "acuracia": 0.97},
        "validacao": {"loss": 0.18, "acuracia": 0.94}
    }
}

# Acessando valores aninhados
print(experimento["modelo"]["arquitetura"])
print(experimento["treinamento"]["learning_rate"])
print(experimento["resultados"]["validacao"]["acuracia"])

# Acesso seguro aninhado
val_loss = experimento.get("resultados", {}).get("validacao", {}).get("loss", None)
print(f"Validation loss: {val_loss}")

Saída:

transformer
0.0001
0.94
Validation loss: 0.18

Repare na última linha do exemplo: encadear .get() com dicionários vazios como padrão evita uma cascata de if aninhados quando qualquer nível da hierarquia pode faltar.

Três padrões com dicionário aparecem com tanta frequência que vale reconhecê-los de imediato. O primeiro é a contagem de frequência, em que .get() com padrão zero dispensa a inicialização manual — embora o Counter do módulo collections resolva ainda melhor:

palavras = ["python", "ia", "python", "ml", "ia", "python", "dados", "ia"]

contagem = {}
for palavra in palavras:
    contagem[palavra] = contagem.get(palavra, 0) + 1

print(contagem)
# {'python': 3, 'ia': 3, 'ml': 1, 'dados': 1}

# Ou usando collections.Counter (mais Pythonico)
from collections import Counter
contagem = Counter(palavras)
print(contagem)
print(contagem.most_common(2))  # [('python', 3), ('ia', 3)]

O segundo é o agrupamento, que organiza uma lista de registros por alguma chave comum:

alunos = [
    {"nome": "Ana", "turma": "A", "nota": 8.5},
    {"nome": "Bruno", "turma": "B", "nota": 7.0},
    {"nome": "Carla", "turma": "A", "nota": 9.0},
    {"nome": "Diego", "turma": "B", "nota": 6.5},
    {"nome": "Eva", "turma": "A", "nota": 7.5},
]

# Agrupar por turma
por_turma = {}
for aluno in alunos:
    turma = aluno["turma"]
    if turma not in por_turma:
        por_turma[turma] = []
    por_turma[turma].append(aluno["nome"])

print(por_turma)
# {'A': ['Ana', 'Carla', 'Eva'], 'B': ['Bruno', 'Diego']}

E o terceiro é o cache, ou memoização, que troca recomputação por memória — a mesma ideia que já apareceu na recursão do Fibonacci:

cache = {}

def fibonacci_cache(n):
    if n in cache:
        return cache[n]
    if n <= 1:
        return n
    resultado = fibonacci_cache(n-1) + fibonacci_cache(n-2)
    cache[n] = resultado
    return resultado

print(fibonacci_cache(50))  # 12586269025 — instantâneo

A outra estrutura desta aula é o set: uma coleção não ordenada de elementos únicos, calcada diretamente no conceito matemático de conjunto. Ele é mutável, mas seus elementos precisam ser imutáveis — strings, números, tuplas.

# Com chaves
frutas = {"maçã", "banana", "laranja", "maçã"}  # duplicata ignorada
print(frutas)   # {'banana', 'laranja', 'maçã'} — ordem não garantida

# Com set()
numeros = set([1, 2, 3, 2, 1, 4])
print(numeros)  # {1, 2, 3, 4}

# Set vazio — use set(), não {}
vazio = set()   # correto
nao_set = {}    # isso é um dicionário vazio!
print(type(vazio))    # <class 'set'>
print(type(nao_set))  # <class 'dict'>

# A partir de uma string
letras = set("abracadabra")
print(letras)  # {'r', 'b', 'c', 'd', 'a'} — apenas letras únicas

Atenção à armadilha das chaves vazias: {} cria um dicionário, não um set. Para um conjunto vazio é preciso escrever set().

O que torna os sets especiais é que as operações da teoria dos conjuntos vêm implementadas nativamente, cada uma com um operador e um método equivalente:

a = {1, 2, 3, 4, 5}
b = {3, 4, 5, 6, 7}

# União: todos os elementos de ambos
print(a | b)          # {1, 2, 3, 4, 5, 6, 7}
print(a.union(b))     # equivalente

# Interseção: elementos presentes em ambos
print(a & b)               # {3, 4, 5}
print(a.intersection(b))   # equivalente

# Diferença: elementos em a mas não em b
print(a - b)               # {1, 2}
print(a.difference(b))     # equivalente

# Diferença simétrica: elementos em um ou outro, mas não em ambos
print(a ^ b)                        # {1, 2, 6, 7}
print(a.symmetric_difference(b))    # equivalente

Na mesma linha, há verificações de relação entre conjuntos, úteis para validar se um grupo está contido em outro ou se dois grupos não se tocam:

todos = {1, 2, 3, 4, 5}
alguns = {2, 3}

print(alguns.issubset(todos))    # True — alguns está contido em todos
print(todos.issuperset(alguns))  # True — todos contém alguns
print(alguns.isdisjoint({6, 7})) # True — não têm elementos em comum

Para alterar o conteúdo, note a distinção entre remove e discard: o primeiro reclama se o elemento não existe, o segundo aceita a ausência em silêncio.

tecnologias = {"Python", "PyTorch", "NumPy"}

# add: adiciona um elemento
tecnologias.add("Pandas")
tecnologias.add("Python")  # ignorado, já existe
print(tecnologias)

# update: adiciona múltiplos elementos
tecnologias.update(["Matplotlib", "Seaborn", "Scikit-learn"])
print(tecnologias)

# remove: remove, gera KeyError se não existir
tecnologias.remove("Seaborn")

# discard: remove se existir, não gera erro se não existir
tecnologias.discard("TensorFlow")  # sem erro, mesmo não existindo

# pop: remove e retorna um elemento aleatório
elemento = tecnologias.pop()
print(f"Removido: {elemento}")

Além da deduplicação, existe uma razão de desempenho para usar sets, e ela é significativa: a verificação de pertencimento roda em tempo constante, O(1), independentemente do tamanho da coleção, enquanto numa lista o custo é O(n) e cresce proporcionalmente. O experimento abaixo torna a diferença visível:

import time

dados = list(range(1000000))
conjunto = set(dados)

# Busca em lista — lenta para elementos no final
inicio = time.time()
999999 in dados
tempo_lista = time.time() - inicio

# Busca em set — sempre rápida
inicio = time.time()
999999 in conjunto
tempo_set = time.time() - inicio

print(f"Lista: {tempo_lista:.6f}s")
print(f"Set:   {tempo_set:.6f}s")

Traduzindo isso para o trabalho com IA, dois usos se destacam. O primeiro é montar vocabulário a partir de um corpus, aproveitando a unicidade automática do set:

corpus = [
    "o gato sentou no tapete",
    "o cachorro correu no parque",
    "o gato e o cachorro brincaram"
]

# Coletar todas as palavras únicas
vocabulario = set()
for frase in corpus:
    vocabulario.update(frase.split())

print(f"Vocabulário: {sorted(vocabulario)}")
print(f"Tamanho: {len(vocabulario)} palavras únicas")

# Criar mapeamento palavra → índice
palavra_para_idx = {palavra: idx for idx, palavra in enumerate(sorted(vocabulario))}
print(palavra_para_idx)

O segundo é diagnosticar um classificador comparando o conjunto de classes reais com o de classes efetivamente preditas — a diferença revela as que o modelo nunca escolheu:

classes_reais = {"gato", "cachorro", "pássaro", "peixe"}
classes_preditas = {"gato", "cachorro", "pássaro"}

ausentes = classes_reais - classes_preditas
print(f"Classes nunca preditas: {ausentes}")  # {'peixe'}

presentes_em_ambos = classes_reais & classes_preditas
print(f"Classes preditas corretamente ao menos uma vez: {presentes_em_ambos}")

Com as quatro estruturas fundamentais já apresentadas, vale colocá-las lado a lado para orientar a escolha:

Estrutura  │ Ordenada │ Mutável │ Duplicatas │ Uso típico em IA
───────────┼──────────┼─────────┼────────────┼──────────────────────────────────
list       │ Sim      │ Sim     │ Sim        │ Dataset, histórico de perdas
tuple      │ Sim      │ Não     │ Sim        │ Dimensões de tensor, coordenadas
dict       │ Sim*     │ Sim     │ Não (key)  │ Config, métricas, vocabulário NLP
set        │ Não      │ Sim     │ Não        │ Vocabulário único, deduplicação

* desde Python 3.7

Para encerrar, um exemplo que junta dicionários, defaultdict e classes num registro de métricas de treinamento — próximo do que fazem bibliotecas de acompanhamento de experimentos:

from collections import defaultdict

class RegistroExperimento:
    """Simula o registro de métricas durante treinamento de um modelo."""

    def __init__(self, nome: str, config: dict):
        self.nome = nome
        self.config = config
        self.historico = defaultdict(list)
        self.melhor_epoca = None
        self.melhor_val_loss = float("inf")

    def registrar_epoca(self, epoca: int, metricas: dict):
        """Registra as métricas de uma época."""
        for metrica, valor in metricas.items():
            self.historico[metrica].append(valor)

        val_loss = metricas.get("val_loss", float("inf"))
        if val_loss < self.melhor_val_loss:
            self.melhor_val_loss = val_loss
            self.melhor_epoca = epoca

    def resumo(self):
        """Imprime um resumo do experimento."""
        print(f"\n{'='*40}")
        print(f"Experimento: {self.nome}")
        print(f"Configuração:")
        for k, v in self.config.items():
            print(f"  {k}: {v}")
        print(f"\nMelhor época: {self.melhor_epoca}")
        print(f"Melhor val_loss: {self.melhor_val_loss:.4f}")
        print(f"\nÚltimas métricas:")
        for metrica, valores in self.historico.items():
            print(f"  {metrica}: {valores[-1]:.4f}")

# Simulando um treinamento
config = {
    "learning_rate": 0.001,
    "batch_size": 32,
    "optimizer": "adam",
    "arquitetura": "LSTM"
}

exp = RegistroExperimento("sentimento_v1", config)

epocas_simuladas = [
    {"train_loss": 0.95, "val_loss": 0.98, "val_acuracia": 0.61},
    {"train_loss": 0.78, "val_loss": 0.81, "val_acuracia": 0.70},
    {"train_loss": 0.61, "val_loss": 0.65, "val_acuracia": 0.78},
    {"train_loss": 0.48, "val_loss": 0.52, "val_acuracia": 0.83},
    {"train_loss": 0.39, "val_loss": 0.44, "val_acuracia": 0.87},
]

for i, metricas in enumerate(epocas_simuladas, start=1):
    exp.registrar_epoca(i, metricas)

exp.resumo()

Saída:

========================================
Experimento: sentimento_v1
Configuração:
  learning_rate: 0.001
  batch_size: 32
  optimizer: adam
  arquitetura: LSTM

Melhor época: 5
Melhor val_loss: 0.4400

Últimas métricas:
  train_loss: 0.3900
  val_loss: 0.4400
  val_acuracia: 0.8700

Resumindo o que fica desta aula: dicionários guardam pares chave-valor e devem ser lidos com .get(chave, padrao) sempre que a chave for incerta; .keys(), .values() e .items() dão as três formas de percorrê-los; a dictionary comprehension escreve {k: v for k, v in iteravel} numa linha; o aninhamento representa configurações hierárquicas; e o Counter resolve contagens de frequência sem esforço. Sets guardam elementos únicos sem ordem, oferecem união, interseção, diferença e diferença simétrica pelos operadores |, &, - e ^, e entregam busca em tempo constante — o que os torna a escolha natural para deduplicação e verificação de pertencimento em grandes volumes. E lembre-se: conjunto vazio é set(), nunca {}.


Os seis exercícios a seguir cobrem acesso seguro, comprehensions, agrupamento e operações de conjunto. Resolva cada um antes de abrir a resposta comentada.

  1. Dado o dicionário abaixo, escreva um código que imprima apenas as métricas com valor acima de 0.85, formatadas como porcentagem com dois decimais.

    metricas = {
        "acuracia": 0.923,
        "precisao": 0.871,
        "recall": 0.834,
        "f1_score": 0.852,
        "auc_roc": 0.961
    }
    

    ✓ Resposta:
    metricas = {
        "acuracia": 0.923,
        "precisao": 0.871,
        "recall": 0.834,
        "f1_score": 0.852,
        "auc_roc": 0.961
    }
    
    print("Métricas acima de 85%:")
    for nome, valor in metricas.items():
        if valor > 0.85:
            print(f"  {nome}: {valor * 100:.2f}%")
    

    Saída:

    Métricas acima de 85%:
      acuracia: 92.30%
      precisao: 87.10%
      auc_roc: 96.10%
    
  2. Escreva uma função contar_palavras(texto) que receba uma string e retorne um dicionário com a frequência de cada palavra, ignorando maiúsculas e minúsculas. Ordene o resultado pelo valor (frequência) em ordem decrescente.

    ✓ Resposta:
    def contar_palavras(texto: str) -> dict:
        """Conta a frequência de cada palavra em um texto."""
        palavras = texto.lower().split()
        contagem = {}
        for palavra in palavras:
            contagem[palavra] = contagem.get(palavra, 0) + 1
        return dict(sorted(contagem.items(), key=lambda x: x[1], reverse=True))
    
    texto = "o gato viu o rato e o rato fugiu do gato mas o gato era mais rápido"
    resultado = contar_palavras(texto)
    for palavra, freq in resultado.items():
        print(f"  {palavra}: {freq}")
    

    Saída:

      o: 4
      gato: 3
      rato: 2
      e: 1
      fugiu: 1
      do: 1
      mas: 1
      era: 1
      mais: 1
      rápido: 1
      viu: 1
    
  3. Explique o que será impresso e por quê:

    a = {"x": 1, "y": 2}
    b = a
    c = a.copy()
    
    b["z"] = 3
    c["w"] = 4
    
    print(a)
    print(b)
    print(c)
    

    ✓ Resposta:
    a → {'x': 1, 'y': 2, 'z': 3}
    b → {'x': 1, 'y': 2, 'z': 3}
    c → {'x': 1, 'y': 2, 'w': 4}
    

    b = a faz b apontar para o mesmo objeto que a. Quando b["z"] = 3 é executado, o dicionário original é modificado, então a também reflete a mudança.

    c = a.copy() cria uma cópia independente do dicionário. Modificações em c não afetam a.

    O comportamento é análogo ao que vimos com listas na aula anterior: atribuição copia referência, não o objeto.

  4. Dados dois sets abaixo representando palavras conhecidas por dois modelos de NLP diferentes, use operações de conjunto para responder: quais palavras ambos conhecem? Quais apenas o modelo A conhece? Quais nenhum dos dois conhece do vocabulário total?

    modelo_a = {"gato", "cachorro", "peixe", "pássaro", "coelho", "hamster"}
    modelo_b = {"gato", "cachorro", "cavalo", "vaca", "peixe", "papagaio"}
    vocabulario_total = {"gato", "cachorro", "peixe", "pássaro", "coelho",
                         "hamster", "cavalo", "vaca", "papagaio", "tartaruga"}
    

    ✓ Resposta:
    modelo_a = {"gato", "cachorro", "peixe", "pássaro", "coelho", "hamster"}
    modelo_b = {"gato", "cachorro", "cavalo", "vaca", "peixe", "papagaio"}
    vocabulario_total = {"gato", "cachorro", "peixe", "pássaro", "coelho",
                         "hamster", "cavalo", "vaca", "papagaio", "tartaruga"}
    
    ambos_conhecem = modelo_a & modelo_b
    print(f"Ambos conhecem: {ambos_conhecem}")
    
    apenas_a = modelo_a - modelo_b
    print(f"Apenas modelo A conhece: {apenas_a}")
    
    nenhum_conhece = vocabulario_total - (modelo_a | modelo_b)
    print(f"Nenhum conhece: {nenhum_conhece}")
    

    Saída:

    Ambos conhecem: {'gato', 'cachorro', 'peixe'}
    Apenas modelo A conhece: {'pássaro', 'coelho', 'hamster'}
    Nenhum conhece: {'tartaruga'}
    
  5. Escreva uma função inverter_dicionario(d) que receba um dicionário e retorne um novo dicionário com chaves e valores invertidos. Se houver valores duplicados, agrupe as chaves originais em uma lista.

    Exemplo:

    entrada = {"a": 1, "b": 2, "c": 1, "d": 3}
    # saída esperada: {1: ["a", "c"], 2: ["b"], 3: ["d"]}
    

    ✓ Resposta:
    def inverter_dicionario(d: dict) -> dict:
        """Inverte chaves e valores, agrupando chaves com mesmo valor."""
        invertido = {}
        for chave, valor in d.items():
            if valor not in invertido:
                invertido[valor] = []
            invertido[valor].append(chave)
        return invertido
    
    entrada = {"a": 1, "b": 2, "c": 1, "d": 3}
    print(inverter_dicionario(entrada))
    # {1: ['a', 'c'], 2: ['b'], 3: ['d']}
    
    # Versão com setdefault (mais concisa)
    def inverter_dicionario_v2(d: dict) -> dict:
        invertido = {}
        for chave, valor in d.items():
            invertido.setdefault(valor, []).append(chave)
        return invertido
    
    print(inverter_dicionario_v2(entrada))
    # {1: ['a', 'c'], 2: ['b'], 3: ['d']}
    
  6. Explique a diferença de desempenho entre usar uma lista e um set para verificar se um elemento pertence à coleção. Por que isso importa em projetos de ML? Dê um exemplo concreto de quando essa diferença seria significativa.

    ✓ Resposta:

    A diferença está na complexidade de tempo de busca:

    • Lista: O(n) — o Python percorre elemento por elemento até encontrar o valor. Em uma lista de 1 milhão de elementos, pode verificar até 1 milhão de itens.
    • Set: O(1) — usa uma tabela hash internamente. Independentemente do tamanho, a verificação é praticamente instantânea.

    Em projetos de ML isso importa muito em pelo menos dois cenários concretos:

    Primeiro, na tokenização de texto em NLP. Quando você processa milhões de palavras e precisa verificar para cada palavra se ela está no vocabulário (que pode ter centenas de milhares de entradas), usar um set como vocabulário em vez de uma lista faz a tokenização ser ordens de magnitude mais rápida:

    # Lento para vocabulários grandes
    vocabulario_lista = ["gato", "cachorro", ...]  # 100.000 palavras
    if palavra in vocabulario_lista:  # O(n) — pode checar 100.000 itens
        ...
    
    # Rápido sempre
    vocabulario_set = {"gato", "cachorro", ...}
    if palavra in vocabulario_set:  # O(1) — verificação instantânea
        ...
    

    Segundo, na filtragem de exemplos duplicados em datasets. Ao processar um dataset enorme e querer garantir que não há IDs repetidos, usar um set para rastrear IDs já vistos é muito mais eficiente do que verificar em uma lista crescente.


Para aprofundar: