Este artigo fecha o bloco de fundamentos da linguagem e trata de três assuntos que costumam ser deixados para depois — e não deveriam: lidar com erros de forma elegante usando exceções, organizar código em módulos e pacotes, e adotar boas práticas de estruturação de projetos. É o que separa um notebook que roda na sua máquina de um projeto que outras pessoas conseguem usar.
Vale começar pelo motivo. Em projetos de ML e IA, erros acontecem o tempo todo e em lugares que você não previu: arquivos de dados corrompidos ou com formato errado, valores ausentes ou de tipo inesperado no dataset, divisão por zero ao calcular métricas quando uma classe não aparece, timeout ao baixar modelos pré-treinados, memória insuficiente ao carregar um dataset gigante, chaves que faltam num dicionário de configuração. Código sem tratamento de erros quebra em silêncio ou de forma catastrófica; código bem escrito falha de maneira controlada, com mensagem clara e, quando dá, se recupera sozinho.
O Python separa os problemas em duas famílias. Os erros de sintaxe acontecem antes de qualquer execução, quando o interpretador nem consegue analisar o arquivo:
# SyntaxError — faltou o dois pontos
if x > 0
print(x)
# SyntaxError — parêntese não fechado
print("olá"
Esses o editor aponta na hora, e não há como capturá-los com try/except — o programa sequer chega a rodar. Já as exceções ocorrem durante a execução, num código sintaticamente perfeito onde algo deu errado em tempo real:
# ZeroDivisionError
resultado = 10 / 0
# TypeError
soma = "5" + 3
# IndexError
lista = [1, 2, 3]
print(lista[10])
# KeyError
dicionario = {"a": 1}
print(dicionario["b"])
# ValueError
numero = int("abc")
# FileNotFoundError
with open("arquivo_inexistente.txt") as f:
pass
# AttributeError
texto = None
texto.upper()
# NameError
print(variavel_nao_definida)
São essas que se pode interceptar, e a ferramenta para isso é o try/except. Na forma mínima, você cerca o trecho arriscado e diz o que fazer quando o problema previsto aparecer:
try:
resultado = 10 / 0
except ZeroDivisionError:
print("Erro: divisão por zero!")
Saída:
Erro: divisão por zero!
Capturar a exceção como objeto, com as, dá acesso à mensagem original — informação que costuma ser essencial no log:
try:
numero = int("abc")
except ValueError as e:
print(f"Erro de valor: {e}")
Saída:
Erro de valor: invalid literal for int() with base 10: 'abc'
Quando um mesmo trecho pode falhar de maneiras diferentes, cada tipo ganha seu próprio bloco e seu próprio tratamento:
def dividir(a, b):
try:
resultado = a / b
return resultado
except ZeroDivisionError:
print("Erro: divisão por zero")
return None
except TypeError as e:
print(f"Erro de tipo: {e}")
return None
print(dividir(10, 2)) # 5.0
print(dividir(10, 0)) # Erro: divisão por zero → None
print(dividir(10, "a")) # Erro de tipo → None
Se o tratamento for o mesmo para vários tipos, uma tupla resolve sem repetição:
def converter_para_float(valor):
try:
return float(valor)
except (ValueError, TypeError) as e:
print(f"Não foi possível converter '{valor}': {e}")
return None
print(converter_para_float("3.14")) # 3.14
print(converter_para_float("abc")) # erro
print(converter_para_float(None)) # erro
A estrutura completa tem ainda duas cláusulas menos conhecidas. O else roda somente quando nenhuma exceção ocorreu, e o finally roda sempre — dando ou não erro:
def ler_arquivo(caminho):
try:
arquivo = open(caminho, "r")
except FileNotFoundError:
print(f"Arquivo '{caminho}' não encontrado.")
return None
else:
# executado apenas se não houve exceção
conteudo = arquivo.read()
arquivo.close()
print("Arquivo lido com sucesso!")
return conteudo
finally:
# executado SEMPRE, com ou sem exceção
print("Operação de leitura finalizada.")
ler_arquivo("dados.csv")
Essa garantia do finally é justamente o que o torna o lugar certo para liberar recursos — fechar conexões, arquivos, handles — independentemente de a operação ter dado certo.
Existe também a captura genérica, que pega qualquer exceção:
try:
# código arriscado
resultado = operacao_complexa()
except Exception as e:
print(f"Erro inesperado: {type(e).__name__}: {e}")
Use-a com parcimônia. Capturar tudo tende a esconder bugs reais atrás de uma mensagem genérica; sempre que souber o que pode dar errado, nomeie a exceção específica.
Além de capturar erros, você pode provocá-los deliberadamente com raise, e isso é uma forma de validação: em vez de deixar o cálculo produzir um resultado sem sentido, interrompa cedo com uma mensagem que explique o problema.
def calcular_acuracia(corretos: int, total: int) -> float:
if total <= 0:
raise ValueError(f"Total deve ser positivo, recebeu: {total}")
if corretos < 0:
raise ValueError(f"Corretos não pode ser negativo, recebeu: {corretos}")
if corretos > total:
raise ValueError(f"Corretos ({corretos}) não pode ser maior que total ({total})")
return corretos / total
try:
print(calcular_acuracia(90, 100)) # 0.9
print(calcular_acuracia(90, 0)) # ValueError
except ValueError as e:
print(f"Erro de validação: {e}")
Um raise sem argumento, dentro de um except, relança a exceção original — útil quando você quer registrar o ocorrido mas deixar a decisão final para quem chamou:
def processar_dado(dado):
try:
return float(dado)
except ValueError:
print(f"Aviso: '{dado}' não é numérico, usando 0.0")
raise # re-lança a exceção original para o chamador decidir
Em projetos maiores, vale ir além das exceções embutidas e criar as suas. Organizadas em hierarquia, elas permitem que quem consome seu código escolha o nível de granularidade que quer tratar — capturando a classe base para pegar tudo, ou uma folha específica para reagir a um caso particular:
# Hierarquia de exceções customizadas
class ErroML(Exception):
"""Exceção base para erros de Machine Learning."""
pass
class ErroDataset(ErroML):
"""Erro relacionado ao dataset."""
pass
class ErroDadosInsuficientes(ErroDataset):
"""Dataset tem exemplos de menos para treinar."""
def __init__(self, qtd_atual: int, qtd_minima: int):
self.qtd_atual = qtd_atual
self.qtd_minima = qtd_minima
super().__init__(
f"Dataset tem {qtd_atual} exemplos, mínimo necessário: {qtd_minima}"
)
class ErroModeloNaoTreinado(ErroML):
"""Tentativa de usar modelo antes de treinar."""
pass
# Usando as exceções customizadas
def validar_dataset(dados: list, minimo: int = 100):
if len(dados) < minimo:
raise ErroDadosInsuficientes(len(dados), minimo)
return True
def predizer(modelo_treinado, dados):
if modelo_treinado is None:
raise ErroModeloNaoTreinado("Chame .fit() antes de .predict()")
try:
validar_dataset([1, 2, 3], minimo=100)
except ErroDadosInsuficientes as e:
print(f"Dataset inválido: {e}")
print(f" Atual: {e.qtd_atual}, Mínimo: {e.qtd_minima}")
try:
predizer(None, [])
except ErroModeloNaoTreinado as e:
print(f"Modelo não treinado: {e}")
Saída:
Dataset inválido: Dataset tem 3 exemplos, mínimo necessário: 100
Atual: 3, Mínimo: 100
Modelo não treinado: Chame .fit() antes de .predict()
Repare que ErroDadosInsuficientes guarda os números como atributos, e não apenas na mensagem. Isso permite que o código que trata o erro tome decisões com base neles, em vez de tentar extrair valores de um texto.
Ainda no território da robustez, o with resolve o problema de recursos que precisam ser liberados. Ele garante o fechamento mesmo que uma exceção interrompa o bloco no meio:
# Sem with — arriscado
arquivo = open("dados.txt", "w")
arquivo.write("conteúdo")
arquivo.close() # e se ocorrer uma exceção antes daqui?
# Com with — seguro e idiomático
with open("dados.txt", "w") as arquivo:
arquivo.write("conteúdo")
# arquivo é fechado automaticamente ao sair do bloco, mesmo com exceções
E aceita mais de um recurso na mesma linha, o que evita aninhar blocos desnecessariamente:
with open("entrada.txt", "r") as entrada, open("saida.txt", "w") as saida:
for linha in entrada:
saida.write(linha.upper())
Da robustez passamos à organização. Um módulo em Python é simplesmente um arquivo .py: ao importá-lo, você ganha acesso a tudo que ele define. A biblioteca padrão já traz um arsenal considerável:
import math
import random
import os
import sys
import datetime
# Usando funções do módulo
print(math.pi) # 3.141592653589793
print(math.sqrt(16)) # 4.0
print(math.ceil(4.2)) # 5
print(math.floor(4.8)) # 4
print(math.log(100, 10)) # 2.0
# Random — muito usado em ML para embaralhar dados e definir seeds
print(random.random()) # float entre 0 e 1
print(random.randint(1, 10)) # inteiro entre 1 e 10
print(random.choice([1,2,3])) # elemento aleatório da lista
random.seed(42) # garante reprodutibilidade
# OS — manipulação de arquivos e diretórios
print(os.getcwd()) # diretório atual
print(os.path.exists("arquivo.txt"))
os.makedirs("pasta/subpasta", exist_ok=True)
Há várias formas de importar, e a escolha afeta a legibilidade. Os apelidos np, pd e plt são convenções tão estabelecidas que fogem delas confunde quem lê:
# Importar o módulo inteiro — acessa com math.sqrt
import math
print(math.sqrt(25))
# Importar com alias — útil para nomes longos
import numpy as np # convenção padrão
import pandas as pd # convenção padrão
import matplotlib.pyplot as plt # convenção padrão
# Importar funções específicas
from math import sqrt, pi, ceil
print(sqrt(25)) # sem prefixo math.
print(pi)
# Importar tudo (evite — polui o namespace)
from math import *
A última forma merece o aviso: importar tudo com asterisco despeja nomes no seu espaço global e cria colisões silenciosas. Evite.
Criar o próprio módulo é só uma questão de salvar funções num arquivo. Um metricas.py com as métricas clássicas de classificação, por exemplo:
# metricas.py
def acuracia(predicoes: list, rotulos: list) -> float:
"""Calcula a acurácia de um classificador."""
if len(predicoes) != len(rotulos):
raise ValueError("Listas devem ter o mesmo tamanho")
corretos = sum(p == r for p, r in zip(predicoes, rotulos))
return corretos / len(rotulos)
def precisao(predicoes: list, rotulos: list, classe_positiva: int = 1) -> float:
"""Calcula a precisão para uma classe."""
vp = sum(p == classe_positiva and r == classe_positiva
for p, r in zip(predicoes, rotulos))
fp = sum(p == classe_positiva and r != classe_positiva
for p, r in zip(predicoes, rotulos))
return vp / (vp + fp) if (vp + fp) > 0 else 0.0
def recall(predicoes: list, rotulos: list, classe_positiva: int = 1) -> float:
"""Calcula o recall para uma classe."""
vp = sum(p == classe_positiva and r == classe_positiva
for p, r in zip(predicoes, rotulos))
fn = sum(p != classe_positiva and r == classe_positiva
for p, r in zip(predicoes, rotulos))
return vp / (vp + fn) if (vp + fn) > 0 else 0.0
def f1_score(predicoes: list, rotulos: list, classe_positiva: int = 1) -> float:
"""Calcula o F1-score para uma classe."""
p = precisao(predicoes, rotulos, classe_positiva)
r = recall(predicoes, rotulos, classe_positiva)
return 2 * p * r / (p + r) if (p + r) > 0 else 0.0
# Código que só roda quando o arquivo é executado diretamente
if __name__ == "__main__":
preds = [1, 0, 1, 1, 0, 1, 0, 0]
labels = [1, 0, 1, 0, 0, 1, 1, 0]
print(f"Acurácia: {acuracia(preds, labels):.4f}")
print(f"Precisão: {precisao(preds, labels):.4f}")
print(f"Recall: {recall(preds, labels):.4f}")
print(f"F1-Score: {f1_score(preds, labels):.4f}")
Uma vez salvo, ele vira dependência de qualquer outro arquivo do projeto:
# main.py
from metricas import acuracia, f1_score
preds = [1, 0, 1, 1, 0]
labels = [1, 0, 0, 1, 0]
print(acuracia(preds, labels)) # 0.8
print(f1_score(preds, labels)) # 0.8
Aquele if __name__ == "__main__" no fim do módulo merece explicação, porque é um dos padrões mais importantes da linguagem. Ele delimita o código que só deve rodar quando o arquivo é executado diretamente, e que precisa ser ignorado quando o arquivo é importado por outro:
# utilidades.py
def carregar_dados(caminho):
print(f"Carregando dados de {caminho}")
return []
def preprocessar(dados):
print("Pré-processando dados...")
return dados
# Este bloco SÓ executa quando você roda: python utilidades.py
# Quando outro arquivo importa utilidades, este bloco é IGNORADO
if __name__ == "__main__":
dados = carregar_dados("dataset.csv")
dados = preprocessar(dados)
print(f"Pronto! {len(dados)} registros processados.")
Sem essa proteção, importar utilidades em outro arquivo dispararia o código de teste automaticamente — o que quase nunca é o que se quer.
Quando os módulos se multiplicam, o passo seguinte é agrupá-los em pacotes: diretórios contendo um arquivo __init__.py. Um projeto de ML bem organizado costuma ter esta cara:
meu_projeto_ml/
│
├── dados/
│ ├── treino.csv
│ └── teste.csv
│
├── src/
│ ├── __init__.py
│ ├── preprocessamento.py
│ ├── modelos.py
│ ├── metricas.py
│ └── visualizacao.py
│
├── notebooks/
│ └── exploracao.ipynb
│
├── testes/
│ ├── __init__.py
│ ├── test_preprocessamento.py
│ └── test_metricas.py
│
├── requirements.txt
└── main.py
O __init__.py pode ficar vazio ou servir de vitrine, reexportando o que interessa:
# src/__init__.py
from .preprocessamento import limpar_dados, normalizar
from .modelos import treinar, avaliar
from .metricas import acuracia, f1_score
Com isso, no lugar de:
from src.metricas import acuracia
Você escreve simplesmente:
from src import acuracia
Projetos também dependem de bibliotecas de terceiros, e essas dependências precisam estar declaradas. O requirements.txt lista o que o projeto exige, com versões fixadas para garantir que a instalação de hoje seja igual à de daqui a seis meses:
numpy==1.26.0
pandas==2.1.0
matplotlib==3.8.0
scikit-learn==1.3.0
torch==2.1.0
transformers==4.35.0
jupyter==1.0.0
Instalar tudo é um comando:
pip install -r requirements.txt
E gerar o arquivo a partir do ambiente já montado, outro:
pip freeze > requirements.txt
Essas dependências, porém, não devem ser instaladas no Python do sistema. Ambientes virtuais isolam cada projeto, evitando que a versão de biblioteca exigida por um quebre o outro:
# Criar ambiente virtual
python -m venv venv
# Ativar (Linux/Mac)
source venv/bin/activate
# Ativar (Windows)
venv\Scripts\activate
# Desativar
deactivate
No mundo de ML, o Anaconda costuma ser preferido, por lidar melhor com pacotes que têm dependências compiladas:
# Criar ambiente
conda create -n meu_projeto python=3.11
# Ativar
conda activate meu_projeto
# Instalar pacotes
conda install numpy pandas matplotlib scikit-learn
# Listar ambientes
conda env list
Resta falar de hábitos de escrita, e o primeiro deles é o princípio da responsabilidade única: cada função faz uma coisa, e faz bem. Compare as duas versões abaixo — a segunda é mais longa em linhas, mas cada peça pode ser testada, reaproveitada e entendida isoladamente:
# Ruim — função faz muitas coisas
def processar_tudo(caminho):
dados = open(caminho).readlines()
dados = [d.strip() for d in dados]
dados = [d for d in dados if d]
numeros = [float(d) for d in dados]
media = sum(numeros) / len(numeros)
print(f"Média: {media}")
return media
# Bom — responsabilidades separadas
def ler_arquivo(caminho: str) -> list[str]:
with open(caminho) as f:
return [linha.strip() for linha in f if linha.strip()]
def converter_para_float(linhas: list[str]) -> list[float]:
return [float(linha) for linha in linhas]
def calcular_media(numeros: list[float]) -> float:
return sum(numeros) / len(numeros)
def processar_arquivo(caminho: str) -> float:
linhas = ler_arquivo(caminho)
numeros = converter_para_float(linhas)
media = calcular_media(numeros)
print(f"Média: {media}")
return media
O segundo é dar nomes que dispensem comentário. Um nome bem escolhido documenta a intenção melhor que qualquer explicação em volta:
# Ruim
def f(x, y, z):
return x * y / z
# Bom
def calcular_imc(peso_kg: float, altura_m: float) -> float:
return peso_kg / (altura_m ** 2)
# Ruim
d = {"a": 0.92, "p": 0.88}
# Bom
metricas_validacao = {"acuracia": 0.92, "precisao": 0.88}
O terceiro é tirar os números mágicos de dentro da lógica, promovendo-os a constantes nomeadas no topo do arquivo, em maiúsculas:
# Constantes no topo do arquivo, em maiúsculas
LEARNING_RATE = 0.001
BATCH_SIZE = 32
MAX_EPOCAS = 100
CAMINHO_DADOS = "dados/treino.csv"
SEED = 42
# Usar as constantes em vez de magic numbers
random.seed(SEED)
E o quarto é abandonar o print em favor do módulo logging assim que o código deixa de ser exploratório. O logger carimba data e hora, classifica a severidade e pode ser silenciado ou redirecionado sem que você toque no código:
import logging
# Configurar o logger
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s"
)
logger = logging.getLogger(__name__)
def treinar_modelo(dados, epocas):
logger.info(f"Iniciando treinamento com {len(dados)} exemplos")
for epoca in range(epocas):
# ... treinamento ...
perda = 0.5 / (epoca + 1) # simulado
if epoca % 10 == 0:
logger.info(f"Época {epoca}: perda = {perda:.4f}")
logger.info("Treinamento concluído!")
treinar_modelo(list(range(1000)), 30)
Saída (o %(asctime)s imprime data e hora reais da execução):
AAAA-MM-DD HH:MM:SS,000 [INFO] Iniciando treinamento com 1000 exemplos
AAAA-MM-DD HH:MM:SS,001 [INFO] Época 0: perda = 0.5000
AAAA-MM-DD HH:MM:SS,002 [INFO] Época 10: perda = 0.0455
AAAA-MM-DD HH:MM:SS,003 [INFO] Época 20: perda = 0.0238
AAAA-MM-DD HH:MM:SS,004 [INFO] Treinamento concluído!
Para fechar, um pipeline que reúne tudo o que foi visto nos fundamentos da linguagem: constantes nomeadas, exceções customizadas em hierarquia, validação com raise, tratamento granular por tipo de erro, type hints, funções de responsabilidade única, logging e o bloco de entrada protegido.
import logging
import random
from typing import Optional
# Configuração
logging.basicConfig(level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger(__name__)
SEED = 42
PROPORCAO_TREINO = 0.8
MIN_EXEMPLOS = 10
# Exceções customizadas
class ErroDataset(Exception):
pass
class ErroDadosInsuficientes(ErroDataset):
def __init__(self, atual: int, minimo: int):
super().__init__(f"Dataset tem {atual} exemplos, mínimo: {minimo}")
self.atual = atual
self.minimo = minimo
# Funções do pipeline
def carregar_dataset(caminho: str) -> list[dict]:
"""Simula o carregamento de um dataset CSV."""
logger.info(f"Carregando dataset de '{caminho}'")
# Simulação — em produção usaria pandas
dados = [
{"texto": f"exemplo_{i}", "label": i % 2, "valor": float(i)}
for i in range(50)
]
logger.info(f"Carregados {len(dados)} registros")
return dados
def validar_dataset(dados: list[dict]) -> None:
"""Valida se o dataset tem qualidade mínima."""
if len(dados) < MIN_EXEMPLOS:
raise ErroDadosInsuficientes(len(dados), MIN_EXEMPLOS)
campos_obrigatorios = {"texto", "label"}
for i, registro in enumerate(dados):
ausentes = campos_obrigatorios - set(registro.keys())
if ausentes:
raise ErroDataset(f"Registro {i} sem campos: {ausentes}")
labels = {r["label"] for r in dados}
if len(labels) < 2:
raise ErroDataset(f"Dataset tem apenas uma classe: {labels}")
logger.info("Dataset validado com sucesso")
def preprocessar(dados: list[dict]) -> list[dict]:
"""Aplica pré-processamento básico."""
processados = []
erros = 0
for registro in dados:
try:
processado = {
"texto": str(registro["texto"]).strip().lower(),
"label": int(registro["label"]),
"valor": float(registro.get("valor", 0.0))
}
processados.append(processado)
except (ValueError, TypeError) as e:
erros += 1
logger.warning(f"Registro ignorado por erro de conversão: {e}")
logger.info(f"Pré-processamento: {len(processados)} ok, {erros} erros")
return processados
def dividir_dataset(
dados: list[dict],
proporcao_treino: float = PROPORCAO_TREINO,
seed: int = SEED
) -> tuple[list[dict], list[dict]]:
"""Divide o dataset em treino e teste."""
random.seed(seed)
embaralhado = dados.copy()
random.shuffle(embaralhado)
corte = int(len(embaralhado) * proporcao_treino)
treino = embaralhado[:corte]
teste = embaralhado[corte:]
logger.info(f"Divisão: {len(treino)} treino, {len(teste)} teste")
return treino, teste
def executar_pipeline(caminho: str) -> Optional[tuple]:
"""Executa o pipeline completo com tratamento de erros."""
try:
dados = carregar_dataset(caminho)
validar_dataset(dados)
dados = preprocessar(dados)
treino, teste = dividir_dataset(dados)
logger.info("Pipeline concluído com sucesso!")
return treino, teste
except ErroDadosInsuficientes as e:
logger.error(f"Dataset insuficiente: {e}")
logger.error(f" Atual: {e.atual}, Mínimo: {e.minimo}")
return None
except ErroDataset as e:
logger.error(f"Erro no dataset: {e}")
return None
except FileNotFoundError:
logger.error(f"Arquivo não encontrado: {caminho}")
return None
except Exception as e:
logger.error(f"Erro inesperado: {type(e).__name__}: {e}")
return None
# Ponto de entrada
if __name__ == "__main__":
resultado = executar_pipeline("dados/treino.csv")
if resultado is not None:
treino, teste = resultado
print(f"\nResumo final:")
print(f" Treino: {len(treino)} exemplos")
print(f" Teste: {len(teste)} exemplos")
print(f" Primeiro exemplo de treino: {treino[0]}")
else:
print("Pipeline falhou. Verifique os logs.")
Note a ordem dos except nesse pipeline: do mais específico ao mais genérico. Como ErroDadosInsuficientes herda de ErroDataset, inverter a ordem faria o bloco genérico capturar tudo primeiro, e o tratamento detalhado nunca rodaria.
Recapitulando os pontos que sustentam esta aula: exceções são erros de execução capturáveis com try/except, e vale sempre nomear o tipo específico em vez de capturar Exception indiscriminadamente. O else roda só na ausência de erro e o finally roda sempre; raise sinaliza problemas deliberadamente e exceções customizadas tornam essa sinalização expressiva; o with cuida da liberação de recursos. Do lado da organização, módulo é um arquivo .py e pacote é um diretório com __init__.py, o bloco if __name__ == "__main__" separa execução de importação, ambientes virtuais isolam dependências, e no código de produção o logging substitui o print — sempre com funções de responsabilidade única e nomes que se explicam sozinhos.
Os seis exercícios a seguir passam por captura de exceções, validação com raise, exceções customizadas e organização de módulos. Resolva antes de abrir a resposta comentada.
-
Escreva uma função
converter_dado(valor, tipo)que tente convertervalorpara o tipo indicado ("int","float"ou"bool"). Se a conversão falhar, retorneNonee imprima uma mensagem de aviso. Se o tipo não for reconhecido, levante umValueErrorcom mensagem clara.✓ Resposta:def converter_dado(valor, tipo: str): """ Converte valor para o tipo indicado. Tipos aceitos: 'int', 'float', 'bool' """ conversores = { "int": int, "float": float, "bool": bool } if tipo not in conversores: raise ValueError( f"Tipo '{tipo}' não reconhecido. " f"Tipos aceitos: {list(conversores.keys())}" ) try: return conversores[tipo](valor) except (ValueError, TypeError) as e: print(f"Aviso: não foi possível converter '{valor}' para {tipo}: {e}") return None print(converter_dado("42", "int")) # 42 print(converter_dado("3.14", "float")) # 3.14 print(converter_dado("abc", "int")) # aviso → None print(converter_dado(None, "float")) # aviso → None try: converter_dado("42", "lista") except ValueError as e: print(f"Erro: {e}") -
O código abaixo tem três problemas relacionados a exceções. Identifique cada um e explique como corrigir:
try: dados = [1, 2, 3] print(dados[10]) resultado = 10 / 0 except: pass finally: print("Sempre executa")✓ Resposta:Problema 1:
except:nu (sem tipo) captura absolutamente tudo, incluindoSystemExit,KeyboardInterrupte erros de programação graves. O correto é usarexcept Exception:ou, melhor ainda, capturar tipos específicos.Problema 2: O
passsilencia o erro completamente, sem nenhuma mensagem ou ação de recuperação. Isso esconde bugs e dificulta o debugging. O mínimo seria logar o erro.Problema 3: Há dois erros possíveis no bloco try (
IndexErroreZeroDivisionError), mas só o primeiro será capturado pois o código para na primeira exceção. O segundo nunca é alcançado. Para tratar ambos, seria necessário separá-los em blocos try distintos ou reestruturar o código.# Versão corrigida try: dados = [1, 2, 3] print(dados[10]) except IndexError as e: print(f"Índice inválido: {e}") try: resultado = 10 / 0 except ZeroDivisionError as e: print(f"Divisão por zero: {e}") finally: print("Sempre executa") -
Crie uma hierarquia de exceções customizadas para um sistema de carregamento de modelos de ML. Deve ter uma exceção base
ErroModelo, e pelo menos três exceções derivadas com mensagens descritivas. Demonstre o uso de cada uma.✓ Resposta:class ErroModelo(Exception): """Exceção base para erros relacionados a modelos de ML.""" pass class ErroArquivoModelo(ErroModelo): """Arquivo do modelo não encontrado ou corrompido.""" def __init__(self, caminho: str): self.caminho = caminho super().__init__(f"Arquivo de modelo não encontrado: '{caminho}'") class ErroVersaoIncompativel(ErroModelo): """Versão do modelo incompatível com o framework atual.""" def __init__(self, versao_modelo: str, versao_framework: str): super().__init__( f"Modelo versão {versao_modelo} incompatível " f"com framework versão {versao_framework}" ) class ErroFormatoEntrada(ErroModelo): """Dados de entrada com formato inválido para o modelo.""" def __init__(self, esperado: tuple, recebido: tuple): super().__init__( f"Formato de entrada inválido. " f"Esperado: {esperado}, Recebido: {recebido}" ) # Demonstração def carregar_modelo(caminho: str): if not caminho.endswith(".pt"): raise ErroArquivoModelo(caminho) print(f"Modelo carregado de {caminho}") def verificar_versao(versao: str): if versao != "2.0": raise ErroVersaoIncompativel(versao, "2.0") def predizer(entrada: tuple): esperado = (1, 128) if entrada != esperado: raise ErroFormatoEntrada(esperado, entrada) for teste in [ lambda: carregar_modelo("modelo.pkl"), lambda: verificar_versao("1.5"), lambda: predizer((1, 64)), ]: try: teste() except ErroModelo as e: print(f"{type(e).__name__}: {e}") -
Explique a diferença entre
import mathefrom math import sqrt. Quando você preferiria cada forma? Existe algum problema comfrom math import *?✓ Resposta:import mathimporta o módulo inteiro. Para usar qualquer função, você precisa do prefixo:math.sqrt(9). Isso é mais explícito — ao ler o código, fica imediatamente claro de onde vem a função.from math import sqrtimporta apenas a funçãosqrtdiretamente no namespace atual. Você usasqrt(9)sem prefixo. É mais conveniente quando você usa pouquíssimas funções de um módulo com muita frequência.Prefira
import mathquando usar muitas funções do módulo ou quando a clareza de origem importa. Prefirafrom math import sqrtquando usar uma ou duas funções repetidamente e a origem for óbvia pelo contexto.from math import *importa tudo do módulo sem prefixo. O problema é que polui o namespace: se você depois importar outra biblioteca com uma função de mesmo nome, haverá conflito silencioso e difícil de rastrear. Por isso é evitado em código de produção. É aceitável apenas em sessões interativas rápidas ou Jupyter Notebooks exploratórios. -
Dado o código abaixo, explique o que
if __name__ == "__main__"faz e o que aconteceria se esse bloco não existisse e o arquivo fosse importado por outro módulo.# processador.py def processar(dados): return [x * 2 for x in dados] def validar(dados): return all(isinstance(x, (int, float)) for x in dados) if __name__ == "__main__": teste = [1, 2, 3, 4, 5] if validar(teste): resultado = processar(teste) print(f"Resultado: {resultado}")✓ Resposta:A variável especial
__name__recebe o valor"__main__"quando o arquivo é executado diretamente (comopython processador.py). Quando o arquivo é importado por outro módulo,__name__recebe o nome do módulo (neste caso,"processador").Então
if __name__ == "__main__":cria um bloco que só executa quando o arquivo é o ponto de entrada, nunca quando é importado.Se esse bloco não existisse, ao executar
from processador import processarem outro arquivo, o Python executaria imediatamente o código de teste — imprimindo"Resultado: [2, 4, 6, 8, 10]"no console sem que o desenvolvedor tenha pedido isso. Em projetos grandes com muitos imports, isso causaria execuções indesejadas e confusão sobre o que está sendo executado. -
Explique o que são ambientes virtuais em Python, por que são importantes em projetos de ML e qual a diferença entre usar
venveconda. Quando você usaria cada um?✓ Resposta:Ambientes virtuais são espaços isolados onde cada projeto tem suas próprias versões de bibliotecas, independentes do sistema e de outros projetos. Sem eles, instalar uma biblioteca para um projeto pode quebrar outro que depende de uma versão diferente da mesma biblioteca.
Em ML isso é especialmente crítico porque bibliotecas como PyTorch e TensorFlow têm dependências rígidas entre si e com CUDA (drivers de GPU). Um projeto pode precisar de PyTorch 1.x e outro de PyTorch 2.x.
venvé a solução nativa do Python. Leve, sem dependências externas, funciona bem para projetos Python puros. Não gerencia versões do Python em si, apenas pacotes.python -m venv venv source venv/bin/activate pip install numpy pandascondaé da Anaconda e vai além: gerencia versões do Python, pacotes não-Python (como bibliotecas C++ usadas pelo PyTorch) e drivers de sistema. É mais pesado mas muito mais robusto para ML.conda create -n projeto_ml python=3.11 conda activate projeto_ml conda install pytorch torchvision -c pytorchUse
venvpara projetos Python simples ou quando o ambiente de deploy é restrito. Usecondapara projetos de ML e data science, especialmente se trabalhar com GPU, pois ele lida com as dependências de CUDA automaticamente.
Para aprofundar:
- Documentação oficial do Python — Erros e exceções: https://docs.python.org/3/tutorial/errors.html
- Documentação oficial do Python — Exceções embutidas: https://docs.python.org/3/library/exceptions.html
- Documentação oficial do Python — Módulos: https://docs.python.org/3/tutorial/modules.html
- Documentação oficial do Python — Módulo logging: https://docs.python.org/3/library/logging.html
- Documentação oficial do Python — venv: https://docs.python.org/3/library/venv.html
- PEP 8 — Organização de imports: https://peps.python.org/pep-0008/#imports
- Real Python — Python Exceptions: https://realpython.com/python-exceptions/
- Real Python — Python Modules and Packages: https://realpython.com/python-modules-packages/
- Real Python — Logging in Python: https://realpython.com/python-logging/
- Conda — Documentação oficial: https://docs.conda.io/en/latest/