Ficheiros, exceções e testes

Módulos, persistência em texto, CSV, tratamento de falhas, asserções e depuração.

TLDR

Perguntar sobre esta página

ChatGPTClaudePerplexityGrokGeminiCopiar e abrirDeepSeekCopiar e abrir

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.

ModoComportamento
"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.

Guardar e ler linhas
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))
Resultado

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 é (12+16)/2=14(12+16)/2=14, 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.

Ler e escrever CSV
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"]))
Resultado

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.

  • try envolve a operação que pode falhar.
  • except Tipo trata esse tipo de falha.
  • else corre apenas se o corpo de try terminar normalmente, sem exceção e sem sair por return, break ou continue.
  • finally corre sempre na saída, haja ou não exceção.

Por exemplo, um return dentro de try salta o else, mas executa o finally.

Tratar entrada inválida
texto = input()
try:
    numero = int(texto)
except ValueError:
    print("não é inteiro")
else:
    print(numero * 2)
finally:
    print("fim")
Resultado

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:

TextoPercurso
abcexcept: escreve não é inteiro.
12else: 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 (10+14)/2(10+14)/2, 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
Uma vírgula pode fazer parte do conteúdo delimitado por aspas.
Mais uma pista
Um leitor CSV interpreta separadores e aspas em conjunto.
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

Assumir que todas as vírgulas são separadores de campos.
Voltar à explicação
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
O else executa no caso sem exceção; finally executa nos dois casos.
Solução
Para 7, escreve 14 e fim; executa try, else e finally. Para abc, escreve não é inteiro e fim; executa try até à falha, except e finally.

Confere a tua resposta:

  • Previ 14 e fim para 7.
  • Previ não é inteiro e fim para abc.
  • Distingui else de finally.

Erros frequentes

Pensar que finally só executa quando o erro não é tratado.
Voltar à explicação
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 nota

Soluçã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 nota

Os 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.
Voltar à explicação
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.
Voltar à explicação
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.

Python
def soma_validos(linhas):
    return 0
Testes e resultado

Verifica o código para comparar o resultado com os testes.

Testes
  1. Valores válidos e inválidos
    Código do teste
    print(soma_validos(["3", "x", "-1", "4"]))
    Esperado:
    6
  2. Lista vazia
    Código do teste
    print(soma_validos([]))
    Esperado:
    0
  3. Só inválidos
    Código do teste
    print(soma_validos(["", "abc", "1.5"]))
    Esperado:
    0
  4. Espaços, sinais e zero
    Código do teste
    print(soma_validos([" -2 ", "+7", "0", "3.0"]))
    Esperado:
    5
  5. Entrada preservada
    Código do teste
    xs = ["4", "erro", "-3"]
    print(soma_validos(xs))
    print(xs)
    Esperado:
    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 total

Erros frequentes

Usar isdigit e perder negativos ou sinais, converter primeiro em float e aceitar decimais, ou esconder todas as exceções.

Voltar à explicação
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.

Python
def ler_inteiros(caminho):
    return []
Testes e resultado

Verifica o código para comparar o resultado com os testes.

Testes
  1. Linha em branco no meio
    Código do teste
    with 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]
  2. Ficheiro vazio
    Código do teste
    with open("entrada.txt", "w", encoding="utf-8") as f:
        f.write("")
    print(ler_inteiros("entrada.txt"))
    Esperado:
    []
  3. Última linha sem mudança de linha
    Código do teste
    with open("entrada.txt", "w", encoding="utf-8") as f:
        f.write("2\n2\n5")
    print(ler_inteiros("entrada.txt"))
    Esperado:
    [2, 2, 5]
  4. Conteúdo preservado
    Código do teste
    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()))
    Esperado:
    [7]
    '7\n'
  5. Caminho inexistente
    Código do teste
    try:
        ler_inteiros("inexistente.txt")
    except FileNotFoundError:
        print("ficheiro em falta")
    else:
        print("devia falhar")
    Esperado:
    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 resultado

Erros frequentes

Abrir com w, parar na primeira linha em branco ou devolver uma lista vazia para esconder um caminho inexistente.

Voltar à explicação
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
O retorno sai da função, mas o bloco 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.
Voltar à explicação

À tua maneira

Aparência

Modo de cor
Tema
Cor de destaque

Leitura

Fonte de leitura

Álgebra, lógica e uma ideia de cada vez.

Código

Fonte do código

Navegação

Cursor de leitura, sem edição. h, j, k, l movem; / pesquisa nesta página. v seleciona caracteres; V seleciona linhas; y copia. Tab permite destacar ou comentar. Espaço ou Enter abre blocos recolhíveis. Nos cartões, h, j, k, l mudam o foco.

CSS personalizado

Ativa uma sugestão ou guarda o teu CSS. Só muda este navegador.

Usa --page, --text e --accent para acompanhar o tema.

Seletores e recuperação

.site-header, .prose, .course-card, [data-reading-history], [data-playground] e .page-actions.

Abre a aparência com a tecla vírgula ou no rodapé. Se o CSS esconder os controlos, abre esta página sem CSS personalizado.

Pesquisar

Escreve para pesquisar em todo o site.

para escolher · Enter para abrir · Esc para fechar

Copiar fórmula

A cópia automática não está disponível. Copia o LaTeX abaixo.

Pesquisar nesta página

Enter procura · n / N repetem · Esc cancela. Ctrl / Cmd K pesquisa todo o site.

Atalhos de teclado

Clica numa tecla para a mudar. Esc cancela. Backspace desativa.

PesquisarCtrl / Cmd K

Os atalhos não interferem enquanto escreves. Tab e Enter funcionam sempre.