Ficheiros, exceções e testes
Módulos, persistência em texto, CSV, tratamento de falhas, asserções e depuração.
Para guardar as notas [12, 16] entre execuções, podemos escrever um inteiro por linha num ficheiro; ao lê-lo, é preciso distinguir um caminho inexistente, uma linha em branco e um número inválido, e cada caso exige uma decisão do contrato.
Módulos e organização
Um módulo é normalmente um ficheiro .py com definições e instruções. import math permite usar math.sqrt; from math import sqrt introduz sqrt no âmbito atual. Evita import *, porque torna a origem dos nomes menos clara.
import math
print(math.isclose(math.sqrt(2) ** 2, 2.0))
A saída é True, pois a comparação aceita uma pequena diferença numérica.
Para separar um módulo reutilizável da execução direta, usa if __name__ == "__main__": junto do código de entrada e saída. Ao importar, as instruções no nível do módulo executam, e esse guarda evita correr a interface de entrada como efeito da importação.
Trabalha com dois ficheiros: estatistica.py define media(valores) e relatorio.py importa-a com from estatistica import media. Se relatorio.py correr como programa, o bloco guardado lê o ficheiro e imprime o resultado; se outro programa importar relatorio, esse bloco não corre.
Ler e escrever ficheiros
open(caminho, modo, encoding="utf-8") abre um ficheiro de texto. Um caminho relativo é interpretado a partir da pasta de trabalho do processo, não automaticamente da pasta onde está o programa.
| Modo | Comportamento |
|---|---|
"r" | Ler; falha se o ficheiro não existir |
"w" | Criar ou substituir todo o conteúdo existente |
"a" | Acrescentar no fim, criando se faltar |
"x" | Criar e falhar se o ficheiro já existir |
with fecha o recurso ao sair do bloco, incluindo quando há uma exceção. read() lê tudo; percorrer for linha in ficheiro lê linha a linha; readline() lê a próxima linha e devolve "" ao chegar ao fim. Uma linha em branco costuma ser "\n", portanto difere do fim do ficheiro.
with open("notas.txt", "w", encoding="utf-8") as ficheiro:
ficheiro.write("12\n16\n")
with open("notas.txt", "r", encoding="utf-8") as ficheiro:
notas = [int(linha.strip()) for linha in ficheiro]
print(notas)
print(sum(notas) / len(notas))Executa o código para ver a saída e os gráficos.
A escrita produz duas linhas, 12 e 16, e a compreensão lê cada linha, remove a mudança de linha com strip e converte-a com int, recuperando [12, 16]; a média é , apresentada como 14.0 pela divisão. Aqui as linhas são não vazias e contêm inteiros; se o formato permitir outros casos, trata-os explicitamente. Num ficheiro vazio, a lista seria [] e o cálculo da média dividiria por zero.
No editor, o ficheiro existe apenas durante esta execução. Num programa local, fica no disco. write recebe texto, não acrescenta uma mudança de linha por si só e devolve o número de caracteres escritos.
CSV sem dividir à mão
Um ficheiro CSV representa linhas de campos. Os campos podem conter separadores e aspas, por isso split(",") não é um leitor CSV completo. Usa o módulo csv, que conhece essas regras.
import csv
with open("alunos.csv", "w", encoding="utf-8", newline="") as ficheiro:
escritor = csv.writer(ficheiro)
escritor.writerow(["nome", "nota"])
escritor.writerow(["Ana, Silva", 15])
with open("alunos.csv", encoding="utf-8", newline="") as ficheiro:
leitor = csv.DictReader(ficheiro)
for linha in leitor:
print(linha["nome"], int(linha["nota"]))Executa o código para ver a saída e os gráficos.
DictReader usa a primeira linha como nomes das colunas. O nome conserva a vírgula porque o escritor delimitou o campo. Os valores lidos são strings, por isso convertemos a nota. newline="" deixa o módulo tratar as mudanças de linha do formato.
Exceções, try, except, else e finally
Uma exceção interrompe o percurso normal e procura um tratador compatível nas chamadas ativas.
tryenvolve a operação que pode falhar.except Tipotrata esse tipo de falha.elsecorre apenas se o corpo detryterminar normalmente, sem exceção e sem sair porreturn,breakoucontinue.finallycorre sempre na saída, haja ou não exceção.
Por exemplo, um return dentro de try salta o else, mas executa o finally.
texto = input()
try:
numero = int(texto)
except ValueError:
print("não é inteiro")
else:
print(numero * 2)
finally:
print("fim")Executa o código para ver a saída e os gráficos.
Para abc, a conversão lança ValueError, enquanto para 12 devolve o inteiro 12; a tabela mostra o bloco executado a seguir e a primeira linha de saída, e em ambas as entradas finally acrescenta ainda uma segunda linha com fim:
| Texto | Percurso |
|---|---|
abc | except: escreve não é inteiro. |
12 | else: escreve 24. |
Uma falha dentro de else não volta ao except associado: esse tratador cobre o corpo de try. Um except ValueError também não trata ZeroDivisionError. Escolhe o erro esperado em vez de esconder qualquer falha com except:.
Alguns erros comuns são ValueError para um valor inadequado, TypeError para tipos incompatíveis, IndexError para índices inválidos, KeyError para chaves em falta e FileNotFoundError para um ficheiro inexistente. raise ValueError("mensagem") comunica uma violação do contrato. Depois de uma falha, define se a operação deve pedir novos dados, ignorar a linha, devolver um sinal ou terminar; não inventes um resultado válido.
Ordem dos tratadores e propagação
Os tipos de exceção formam uma hierarquia. FileNotFoundError é um caso de
OSError; um except OSError também o trata. Python escolhe o primeiro
except compatível, por isso uma resposta específica deve aparecer antes
de uma resposta mais geral.
try:
raise FileNotFoundError("ficheiro em falta")
except FileNotFoundError:
print("confere o caminho")
except OSError:
print("falha de acesso")
A saída é confere o caminho. Se inverteres os tratadores, passa a ser
falha de acesso: a exceção continua do mesmo tipo, mas já foi tratada pelo
ramo geral. Uma exceção sem tratador compatível propaga-se ao chamador,
que pode tratá-la num try exterior. O código normal depois da operação
que falhou não retoma no meio desse try.
Asserções e testes
assert condicao exprime uma hipótese interna e lança AssertionError se for falsa. Pode ser desativado ao correr Python com otimizações, por isso não o uses como única validação de entradas do utilizador.
Um teste compara o resultado real com um esperado calculado independentemente. Escolhe um caso normal, os limites e as entradas inválidas previstas pelo contrato.
def media(valores):
if not valores:
raise ValueError("sequência vazia")
return sum(valores) / len(valores)
assert media([10, 14]) == 12
assert media([7]) == 7
try:
media([])
except ValueError:
print("entrada vazia rejeitada")
else:
raise AssertionError("devia rejeitar a entrada vazia")
A saída é entrada vazia rejeitada. O resultado 12 vem de , não de copiar o cálculo interno para o teste. A entrada de um elemento e a entrada vazia testam decisões diferentes.
Depurar um resultado errado
Desenvolve por partes pequenas, como no cálculo da distância entre pontos: começa com uma função que devolve 0.0, confirma que corre, acrescenta as diferenças, confirma, e só depois a raiz. Reproduz cada falha com a menor entrada útil e segue os valores antes e depois da instrução suspeita, usando um depurador, uma tabela de execução ou print temporários; confirma o primeiro ponto onde o estado diverge do esperado, porque um teste que passa num exemplo não demonstra todos os casos.
Para problemas numéricos, verifica unidades, domínio e precisão. Para coleções, verifica ordem, repetições e mutação. Para ficheiros, verifica pasta de trabalho, modo e formato antes de alterar o algoritmo de cálculo.
Exercícios
Conservar um separador no campo
Uma linha CSV contém um nome “Ana, Silva” numa só coluna. Porque não chega
usar split(”,”)?
Pista
Mais uma pista
Solução
split separaria também a vírgula dentro do nome e criaria
campos a mais. csv.reader ou DictReader respeita a
delimitação do formato e conserva o nome numa coluna.
Confere a tua resposta:
- Identifiquei a vírgula dentro do campo.
- Expliquei o campo extra produzido por split.
- Escolhi o módulo csv.
Erros frequentes
Seguir uma exceção
No programa publicado, troca abc por 7. Indica a saída e as partes executadas. Depois compara com a entrada abc.
Pista
A conversão de 7 não levanta ValueError.
Mais uma pista
Solução
Confere a tua resposta:
- Previ 14 e fim para 7.
- Previ não é inteiro e fim para abc.
- Distingui else de finally.
Erros frequentes
Separar validação de asserção interna
Uma função recebe uma nota inteira de um utilizador. O contrato exige 0 a 20 e
manda lançar ValueError fora desse intervalo. Diagnostica esta implementação
para -1 e 21, e explica o que muda quando Python corre com asserções desativadas.
Corrige a validação.
def validar_nota(nota):
assert nota >= 0
return notaSolução
Com asserções ativas, -1 lança AssertionError, que já difere do erro pedido;
21 passa porque o limite superior não é verificado. Com asserções desativadas,
ambos chegam ao retorno. A validação necessária deve ser explícita:
def validar_nota(nota):
if not 0 <= nota <= 20:
raise ValueError("nota fora de 0 a 20")
return notaOs extremos 0 e 20 são válidos. A condição rejeita -1 e 21 em ambos os modos.
assert continua útil para hipóteses internas e testes, cujo funcionamento não
se confunde com a validação exigida pelo contrato.
Confere a tua resposta:
- Expliquei que -1 é aceite se asserções forem desativadas.
- Usei uma condição com ValueError.
- Validei o intervalo completo de 0 a 20.
Separar importação de execução direta
medidas.py contém o código seguinte. relatorio.py executa
from medidas import dobro. Explica o que acontece durante essa importação
e reorganiza medidas.py para a função continuar disponível sem pedir dados
nem imprimir ao importar. A execução direta deve continuar a ler e mostrar
o dobro.
def dobro(x):
return 2 * x
n = int(input())
print(dobro(n))Solução
Importar executa as instruções no nível do módulo: define dobro, pede
entrada e imprime o resultado antes de continuar relatorio.py. Colocamos
a função fora do guarda e toda a interface dentro dele:
def dobro(x):
return 2 * x
if __name__ == "__main__":
n = int(input())
print(dobro(n))Na execução direta, __name__ é "__main__" e o bloco executa. Ao importar
como medidas, a definição continua disponível e o bloco é ignorado.
Guardar só o print deixaria a leitura como efeito da importação; guardar
também a definição impediria a sua disponibilidade ao importar.
Confere a tua resposta:
- Identifiquei a leitura ao importar.
- Guardei toda a interface de entrada e saída.
- Mantive a função reutilizável fora do guarda.
Tratar só a falha esperada
Define soma_validos(linhas): recebe uma lista de strings, soma as que int
consegue converter e ignora apenas as que levantam ValueError. Aceita
números negativos, espaços em torno do inteiro e zero. Devolve a soma sem
imprimir.
def soma_validos(linhas):
return 0Verifica o código para comparar o resultado com os testes.
Testes
Valores válidos e inválidos
Código do testeprint(soma_validos(["3", "x", "-1", "4"]))Esperado:6
Lista vazia
Código do testeprint(soma_validos([]))Esperado:0
Só inválidos
Código do testeprint(soma_validos(["", "abc", "1.5"]))Esperado:0
Espaços, sinais e zero
Código do testeprint(soma_validos([" -2 ", "+7", "0", "3.0"]))Esperado:5
Entrada preservada
Código do testexs = ["4", "erro", "-3"] print(soma_validos(xs)) print(xs)Esperado:1 ['4', 'erro', '-3']
print(soma_validos(["3", "x", "-1", "4"]))6
print(soma_validos([]))0
print(soma_validos(["", "abc", "1.5"]))0
print(soma_validos([" -2 ", "+7", "0", "3.0"]))5
xs = ["4", "erro", "-3"]
print(soma_validos(xs))
print(xs)1 ['4', 'erro', '-3']
Pista
A conversão pode levantar ValueError.
Mais uma pista
Coloca a conversão em try. Acumula só depois de obter um inteiro.
Solução
As entradas válidas do primeiro caso são 3, -1 e 4, cuja soma é 6. O
tratador salta a string x. Os testes públicos não provam que só ValueError é
tratado; confere também esse requisito na solução.
def soma_validos(linhas):
total = 0
for linha in linhas:
try:
numero = int(linha)
except ValueError:
continue
total += numero
return totalErros frequentes
Usar isdigit e perder negativos ou sinais, converter primeiro em float e
aceitar decimais, ou esconder todas as exceções.
Ler sem confundir uma linha em branco
Define ler_inteiros(caminho): lê um ficheiro UTF-8 com um inteiro por linha
e devolve uma lista pela ordem do ficheiro. Ignora linhas só com espaços ou
mudanças de linha. As restantes linhas são inteiros válidos. Conserva o
conteúdo; deixa FileNotFoundError propagar se o caminho faltar.
def ler_inteiros(caminho):
return []Verifica o código para comparar o resultado com os testes.
Testes
Linha em branco no meio
Código do testewith open("entrada.txt", "w", encoding="utf-8") as f: f.write("12\n\n -3 \n0\n") print(ler_inteiros("entrada.txt"))Esperado:[12, -3, 0]
Ficheiro vazio
Código do testewith open("entrada.txt", "w", encoding="utf-8") as f: f.write("") print(ler_inteiros("entrada.txt"))Esperado:[]
Última linha sem mudança de linha
Código do testewith open("entrada.txt", "w", encoding="utf-8") as f: f.write("2\n2\n5") print(ler_inteiros("entrada.txt"))Esperado:[2, 2, 5]
Conteúdo preservado
Código do testewith open("entrada.txt", "w", encoding="utf-8") as f: f.write("7\n") print(ler_inteiros("entrada.txt")) with open("entrada.txt", encoding="utf-8") as f: print(repr(f.read()))Esperado:[7] '7\n'
Caminho inexistente
Código do testetry: ler_inteiros("inexistente.txt") except FileNotFoundError: print("ficheiro em falta") else: print("devia falhar")Esperado:ficheiro em falta
with open("entrada.txt", "w", encoding="utf-8") as f:
f.write("12\n\n -3 \n0\n")
print(ler_inteiros("entrada.txt"))[12, -3, 0]
with open("entrada.txt", "w", encoding="utf-8") as f:
f.write("")
print(ler_inteiros("entrada.txt"))[]
with open("entrada.txt", "w", encoding="utf-8") as f:
f.write("2\n2\n5")
print(ler_inteiros("entrada.txt"))[2, 2, 5]
with open("entrada.txt", "w", encoding="utf-8") as f:
f.write("7\n")
print(ler_inteiros("entrada.txt"))
with open("entrada.txt", encoding="utf-8") as f:
print(repr(f.read()))[7] '7\n'
try:
ler_inteiros("inexistente.txt")
except FileNotFoundError:
print("ficheiro em falta")
else:
print("devia falhar")ficheiro em falta
Pista
Percorrer o ficheiro com for termina no fim, sem confundir uma linha em branco com esse fim.
Mais uma pista
Abre com with em modo de leitura. Aplica strip à linha e só converte se restar texto.
Solução
strip remove espaços e mudanças de linha. Uma linha em branco é ignorada, mas o for continua. O modo r conserva o conteúdo; with fecha o ficheiro mesmo se a leitura falhar.
def ler_inteiros(caminho):
resultado = []
with open(caminho, "r", encoding="utf-8") as ficheiro:
for linha in ficheiro:
texto = linha.strip()
if texto:
resultado.append(int(texto))
return resultadoErros frequentes
Abrir com w, parar na primeira linha em branco ou devolver uma lista vazia para esconder um caminho inexistente.
Seguir return e finally
Prevê a saída completa. Depois move return numero para o fim de else e prevê a nova saída. Compara também a entrada "abc" nas duas versões.
def converter(texto):
try:
numero = int(texto)
return numero
except ValueError:
return None
else:
print("sucesso")
finally:
print("fim")
print(converter("12"))Pista
finally executa antes dessa saída.Solução
Na versão original, o return dentro de try salta else. A saída é fim e depois 12, porque finally corre antes de a função entregar o resultado ao print exterior.
Movendo o retorno para depois de print("sucesso") no else, try acaba normalmente. A saída passa a sucesso, fim e 12, por essa ordem. Com "abc", a conversão lança ValueError; nas duas versões, except devolve None depois de executar finally. A saída é fim e depois None.
Confere a tua resposta:
- Previ fim seguido de 12 na versão original.
- Expliquei que return salta else e executa finally.
- Movi o return para else e previ sucesso, fim e 12.