Agora que você dominou as habilidades básicas necessárias para escrever programas organizados que sejam fáceis de usar, é hora de pensar em tornar seus programas ainda mais relevantes e utilizáveis. Neste capítulo, você aprenderá a trabalhar com arquivos para que seus programas possam analisar rapidamente muitos dados.
Você aprenderá a lidar com erros para que seus programas não travem quando encontrarem situações inesperadas. Você aprenderá sobre exceções, que são objetos especiais que o Python cria para gerenciar erros que surgem enquanto um programa estiver em execução. Você também aprenderá sobre o módulo JSON, que permite salvar os dados do usuário para que não sejam perdidos quando seu programa para de funcionar.
Aprender a trabalhar com arquivos e salvar dados facilitará seus programas para as pessoas usarem. Os usuários poderão escolher quais dados inserir e quando inseri-los. As pessoas poderão executar seu programa, fazer algum trabalho e fechar o programa e pegar de onde pararam. Aprender a lidar com exceções ajudará você a lidar com situações em que os arquivos não existem e lidar com outros problemas que podem fazer com que seus programas falhem. Isso tornará seus programas mais robustos quando encontrarem dados ruins, seja de erros inocentes ou de tentativas maliciosas de quebrar seus programas. Com as habilidades que você aprenderá neste capítulo, tornará seus programas mais aplicáveis, utilizáveis e estáveis.
Lendo um arquivo
Uma quantidade incrível de dados está disponível nos arquivos de texto. Os arquivos de texto podem conter dados climáticos, dados de tráfego, dados socioeconômicos, obras literárias e muito mais. A leitura de um arquivo é particularmente útil nos aplicativos de análise de dados, mas também é aplicável a qualquer situação em que você deseja analisar ou modificar as informações armazenadas em um arquivo. Por exemplo, você pode escrever um programa que lê no conteúdo de um arquivo de texto e reescreva o arquivo com formatação que permite que um navegador o exiba.
Quando você deseja trabalhar com as informações em um arquivo de texto, a primeira etapa é ler o arquivo na memória. Em seguida, você pode trabalhar com todo o conteúdo do arquivo de uma só vez ou trabalhar com o conteúdo linha por linha.
Lendo o conteúdo de um arquivo
Para experimentar os seguintes exemplos, você pode inserir essas linhas em um editor e salvar o arquivo como digitos_pi.txt ou baixar o arquivo deste link. Salve o arquivo no mesmo diretório em que você armazenará os programas deste capítulo.
Aqui está um programa que abre este arquivo, lê e imprime o conteúdo do arquivo na tela:
from pathlib import Path
caminho = Path('digitos_pi.txt') # 1
conteudo = caminho.read_text() # 2
print(conteudo)
Para trabalhar com o conteúdo de um arquivo, precisamos dizer ao Python o caminho para o arquivo. Um caminho é o local exato de um arquivo ou pasta em um sistema. O Python fornece um módulo chamado pathlib que facilita o trabalho com arquivos e diretórios, independentemente do sistema operacional que você ou os usuários do seu programa estão trabalhando. Um módulo que fornece funcionalidades específicas como essa é frequentemente chamado de biblioteca, daí o nome pathlib.
Começamos importando a classe Path da pathlib. Há muito que você pode fazer com um objeto de caminho que aponta para um arquivo. Por exemplo, você pode verificar se o arquivo existe antes de trabalhar com ele, ler o conteúdo do arquivo ou escrever novos dados no arquivo. Aqui, construímos um objeto de caminho que representa o arquivo digitos_pi.txt, que atribuímos ao caminho da variável (1). Como esse arquivo é salvo no mesmo diretório do arquivo .py que estamos escrevendo, o nome do arquivo é tudo o que Path precisa para acessar o arquivo.
Depois de termos um objeto Path que representa digitos_pi.txt, usamos o método read_text() para ler todo o conteúdo do arquivo (2). O conteúdo do arquivo é retornado como uma única string, que atribuímos a variável conteudo. Quando imprimimos o valor de conteudo, vemos todo o conteúdo do arquivo de texto.
A única diferença entre essa saída e o arquivo original é a linha em branco extra no final da saída. A linha em branco aparece porque read_text() retorna uma string vazia quando atingir o final do arquivo; esta string vazia aparece como uma linha em branco. Podemos remover a linha em branco extra usando rstrip() na sequência de conteúdo:
from pathlib import Path
caminho = Path('digitos_pi.txt')
conteudo = caminho.read_text()
conteudo = conteudo.rstrip()
print(conteudo)
Lembre-se do Capítulo 2 de que o método rstrip() do Python remove ou tira, qualquer espaço em branco do lado direito de uma string. Agora a saída corresponde exatamente ao conteúdo do arquivo original.
Podemos retirar o caractere de nova linha à direita quando lemos o conteúdo do arquivo, aplicando o método rstrip() imediatamente após chamar read_text():
conteudo = caminho.read_text().rstrip()
Esta linha diz a Python para chamar o método read_text() no arquivo com o qual estamos trabalhando. Em seguida, ele aplica o método rstrip() à string que read_text() retorna. A sequência limpa é então atribuída a variável conteudo. Essa abordagem é chamada de encadeamento de métodos e você verá que ela é bem comum na programação.
Caminhos de arquivo relativos e absolutos
Quando você passa um nome de arquivo simples como digitos_pi.txt para Path, o Python olha no diretório em que o arquivo que está sendo executado atualmente (ou seja, seu arquivo .py do programa) é armazenado.
Às vezes, dependendo de como você organiza seu trabalho, o arquivo que você deseja abrir não estará no mesmo diretório do seu arquivo de programa. Por exemplo, você pode armazenar seus arquivos de programa em uma pasta chamada programas_python. Dentro do programas_python, você pode ter outra pasta chamada arquivos_texto para distinguir seus arquivos de programa dos arquivos de texto que eles estão manipulando. Embora arquivos_texto esteja em programas_python, apenas passando o nome do nome de um arquivo em arquivos_texto não funcionará, porque o python só procurará em programas_python e parará por aí; ele não continuará e não procurará na pasta arquivos_texto. Para fazer com que o Python abra arquivos de um diretório diferente do que o arquivo do seu programa é armazenado, você precisa fornecer o caminho correto.
Existem duas maneiras principais de especificar caminhos na programação. Um caminho de arquivo relativo diz ao Python para procurar um determinado local em relação ao diretório em que o arquivo de programa atualmente em execução é armazenado. Como o arquivos_texto está dentro do programas_python, precisamos criar um caminho que começa com o diretório arquivos_texto e termina com o nome do arquivo. Veja como construir este caminho:
caminho = Path('arquivos_texto/arquivo.txt')
Você também pode dizer ao Python exatamente onde o arquivo está no seu computador, independentemente de onde o programa que está sendo executado é armazenado. Isso é chamado de caminho de arquivo absoluto. Você pode usar um caminho absoluto se um caminho relativo não funcionar. Por exemplo, se você colocou arquivos_texto em uma pasta que não seja o programas_python, basta passar o caminho do caminho 'arquivos_texto/arquivo.txt' não funcionará porque o Python procurará apenas esse local dentro do programas_python. Você precisará escrever um caminho absoluto para esclarecer onde deseja que o Python olhe.
Os caminhos absolutos geralmente são mais longos que os caminhos relativos, porque começam na pasta raiz do seu sistema:
caminho = Path('C:/User/Gui/Documentos/arquivos_texto/arquivo.txt')
Usando caminhos absolutos, você pode ler arquivos de qualquer local do seu sistema. Por enquanto, é mais fácil armazenar arquivos no mesmo diretório que seus arquivos de programa ou em uma pasta como arquivos_texto dentro do diretório que armazena seus arquivos de programa.
Os sistemas Windows usam uma barra invertida (\) em vez de uma barra normal (/) ao exibir caminhos de arquivo, mas você deve usar barras para a frente no seu código, mesmo no Windows. A biblioteca Pathlib usará automaticamente a representação correta do caminho quando interage com o seu sistema ou qualquer sistema de qualquer usuário.
Acessando as linhas de um arquivo
Ao trabalhar com um arquivo, geralmente deseja examinar cada linha do arquivo. Você pode estar procurando certas informações no arquivo ou pode modificar o texto no arquivo de alguma forma. Por exemplo, você pode querer ler um arquivo de dados climáticos e trabalhar com qualquer linha que inclua a palavra ensolarado na descrição do clima daquele dia. Em uma reportagem, você pode procurar qualquer linha com a tag <head> e reescrever essa linha com um tipo específico de formatação.
Você pode usar o método splitlines() para transformar uma string longa em um conjunto de linhas e, em seguida, usar um loop para examinar cada linha de um arquivo, um de cada vez:
from pathlib import Path
caminho = Path('digitos_pi.txt')
conteudo = caminho.read_text() # 1
linhas = conteudo.splitlines() # 2
for linha in linhas:
print(linha)
Começamos lendo todo o conteúdo do arquivo, como fizemos anteriormente (1). Se você planeja trabalhar com as linhas individuais em um arquivo, não precisa retirar nenhum espaço em branco ao ler o arquivo. O método splitlines() retorna uma lista de todas as linhas do arquivo e atribuímos à variável linha cada linha da lista (2). Em seguida, fazemos um loop sobre essas linhas e imprimimos cada uma.
Como não modificamos nenhuma das linhas, a saída corresponde exatamente ao arquivo de texto original.
Trabalhando com o conteúdo de um arquivo
Depois de ler o conteúdo de um arquivo na memória, você pode fazer o que quiser com esses dados, então vamos explorar brevemente os dígitos de pi. Primeiro, tentaremos construir uma única string contendo todos os dígitos no arquivo sem espaço em branco:
from pathlib import Path
caminho = Path('digitos_pi.txt')
conteudo = caminho.read_text()
linhas = conteudo.splitlines()
string_pi = ''
for linha in linhas: # 1
string_pi += linha
print(string_pi)
print(len(string_pi))
Começamos lendo o arquivo e armazenando cada linha de dígitos em uma lista, assim como fizemos no exemplo anterior. Em seguida, criamos uma variável, string_pi, para manter os dígitos de pi. Escrevemos um loop que adiciona cada linha de dígitos ao string_pi (1). Imprimimos essa string e também mostramos qual o tamanho da string.
A variável string_pi contém o espaço em branco que estava no lado esquerdo dos dígitos em cada linha, mas podemos nos livrar disso usando lstrip() em cada linha:
string_pi += linha.lstrip()
Agora temos uma string contendo pi com 30 casas decimais. A string tem 32 caracteres de comprimento, porque também inclui o inteiro 3 e um ponto decimal.
Quando o Python lê em um arquivo de texto, ele interpreta todo o texto no arquivo como uma string. Se você ler em um número e quiser trabalhar com esse valor em um contexto numérico, precisará convertê-lo em um número inteiro usando a função int() ou em um ponto flutuante usando a função float().
Arquivos grandes: um milhão de dígitos
Até agora, focamos em analisar um arquivo de texto que contém apenas três linhas, mas o código nesses exemplos funcionaria tão bem em arquivos muito maiores. Se começarmos com um arquivo de texto que contém pi com 1.000.000 de casas decimais, em vez de apenas 30, podemos criar uma única string contendo todos esses dígitos. Não precisamos alterar nosso programa, exceto para passar por um arquivo diferente. Também imprimiremos apenas as primeiras 50 casas decimais, para não ter que assistir a um milhão de dígitos rolar no terminal. Você pode baixar esse arquivo aqui. Vejamos como resestruturar o código:
from pathlib import Path
caminho = Path('1_milhao_digitos_pi.txt')
conteudo = caminho.read_text()
linhas = conteudo.splitlines()
string_pi = ''
for linha in linhas:
string_pi += linha.lstrip()
print(f'{string_pi[:52]}...')
print(len(string_pi))
A saída mostra que realmente temos uma string contendo as 1.000.000 de casas decimais de pi.
O Python não tem limite inerente a quanto dados você pode trabalhar; você pode trabalhar com o máximo de dados que a memória do seu sistema pode suportar.
Seu aniversário está contido no pi?
Sempre fiquei curioso para saber se meu aniversário aparece em qualquer lugar dos dígitos de pI. Vamos usar o programa que acabamos de escrever para descobrir se o aniversário de alguém aparece em qualquer lugar dos primeiros milhões de dígitos de pi. Podemos fazer isso expressando cada aniversário como uma série de dígitos e vendo se essa string aparece em qualquer lugar em string_pi:
from pathlib import Path
caminho = Path('1_milhao_digitos_pi.txt')
conteudo = caminho.read_text()
linhas = conteudo.splitlines()
string_pi = ''
for linha in linhas:
string_pi += linha.lstrip()
aniversario = input('Digite a data de aniversário como ddmmaaa: ')
if aniversario in string_pi:
print('O seu aniversário aparece nos primeiros 1 milhão de dígitos de pi!')
else:
print('O seu aniversário não aparece nos primeiros 1 milhlão de digitos de pi')
Primeiro, solicitamos o aniversário do usuário e depois verificamos se essa string está no string_pi.
Exercício 01: Abra um arquivo em branco em seu editor de texto e escreva algumas linhas resumindo o que você aprendeu sobre o Python até agora. Inicie cada linha com a frase em Com Python você pode... . Salve o arquivo como aprendedo_python.txt no mesmo diretório que seus exercícios deste capítulo. Escreva um programa que lê o arquivo e imprime o que você escreveu duas vezes: imprima o conteúdo uma vez lendo em todo o arquivo e, uma vez, armazenando as linhas em uma lista e, em seguida, iterando sobre cada linha.
Exercício 02: Você pode usar o método replace() para substituir qualquer palavra em uma string por uma palavra diferente. Aqui está um exemplo rápido mostrando como substituir 'cachorro' por 'gato' em uma frase:
mensagem = 'Eu gosto de cachorro'
mensagem.replace('cachorro', 'gato')
Leia em cada linha a partir do arquivo que você acabou de criar, aprendedo_python.txt e substitua a palavra Python pelo nome de outro idioma, como C. Imprima cada linha modificada na tela.
Exercício 03: O programa que escrevemos nesta seção usa uma variável temporária, linhas, para mostrar como funciona o splitlines(). Você pode pular a variável temporária e fazer um loop diretamente sobre a lista que splitlines() retorna.
Escrevendo em um arquivo
Uma das maneiras mais simples de salvar dados é gravá-lo em um arquivo. Quando você escreve texto em um arquivo, a saída ainda estará disponível depois que você fecha o terminal que contém a saída do seu programa. Você pode examinar a saída depois que um programa terminar de execução e compartilhar os arquivos de saída com outras pessoas. Você também pode escrever programas que leem o texto de volta à memória e trabalhem com ele novamente mais tarde.
Escrevendo uma única linha
Depois de definir um caminho, você pode gravar em um arquivo usando o método write_text(). Para ver como isso funciona, vamos escrever uma mensagem simples e armazená-la em um arquivo em vez de imprimi-lo na tela:
from pathlib import Path
caminho = Path('programa.txt')
caminho.write_text('Eu adoro programar!')
O método write_text() leva um único argumento: a string que você deseja gravar no arquivo. Este programa não possui saída de terminal, mas se você abrir o arquivo programa.txt, verá uma linha.
Este arquivo se comporta como qualquer outro arquivo no seu computador. Você pode abri-lo, escrever um novo texto, copiar, colar para ele e assim por diante.
O Python só pode escrever strings em um arquivo de texto. Se você deseja armazenar dados numéricos em um arquivo de texto, precisará converter os dados para configurar o formato primeiro usando a função str().
Escrevendo várias linhas
O método write_text() faz algumas coisas nos bastidores. Se o arquivo que o caminho apontar não existir, ele cria esse arquivo. Além disso, depois de escrever a string no arquivo, ele garante que o arquivo esteja fechado corretamente. Os arquivos que não estão fechados adequadamente podem levar a dados ausentes ou corrompidos.
Para gravar mais de uma linha em um arquivo, você precisa criar uma string contendo todo o conteúdo do arquivo e, em seguida, chame write_text() com essa string. Vamos escrever várias linhas para o arquivo programa.txt:
from pathlib import Path
conteudo = 'Eu adoro programar!\n'
conteudo += 'Estou aprendendo Python!\n'
conteudo += 'Trabalhar com dados é muito legal!'
caminho = Path('programa.txt')
caminho.write_text(conteudo)
Definimos uma variável chamada conteudo que manterá todo o conteúdo do arquivo. Na próxima linha, usamos o operador += para adicionar a essa sequência. Você pode fazer isso quantas vezes for necessário, para construir seqüências de comprimento. Nesse caso, incluímos caracteres \n no final de cada linha, para garantir que cada declaração apareça em sua própria linha.
Se você executar isso e abrir o programa.txt, verá cada uma dessas linhas no arquivo de texto.
Você também pode usar espaços, caracteres de guia e linhas em branco para formatar sua saída, assim como você está fazendo com a saída baseada no terminal. Não há limite para o comprimento de suas strings, nem a quantidade de documentos gerados.
Tenha cuidado ao chamar write_text() em um objeto de caminho. Se o arquivo já existir, write_text() apagará o conteúdo atual do arquivo e gravará novos conteúdos no arquivo. Mais tarde neste capítulo, você aprenderá a verificar se existe um arquivo usando o pathlib.
Tente você mesmo!
Exercício 04: Escreva um programa que solicite ao usuário o nome deles. Quando eles responderem, escreva seu nome em um arquivo chamado usuario.txt.
Exercício 05: Escreva um loop while que peça ao usuário o seu nome. Colete todos os nomes inseridos e, em seguida, escreva esses nomes em um arquivo chamado doc_usuarios.txt. Verifique se cada entrada aparece em uma nova linha no arquivo.
Exceções
O Python usa objetos especiais chamados exceções para gerenciar erros que surgem durante a execução de um programa. Sempre que ocorre um erro que torna o Python sem saber o que fazer a seguir, ele cria um objeto de exceção. Se você escrever código que lida com a exceção, o programa continuará em execução. Se você não lidar com a exceção, o programa interromperá e mostrará um traceback, que inclui um relatório da exceção que foi gerada.
Exceções são tratadas com blocos try-except. Um bloco try-except pede a Python para fazer alguma coisa, mas também diz a Python o que fazer se uma exceção for gerada. Quando você usa blocos try-except, seus programas continuarão em execução, mesmo que as coisas comecem a dar errado. Em vez de tracebacks, que podem ser confusos para os usuários lerem, os usuários verão mensagens de erro amigáveis que você escreveu.
Lidando com a exceção do ZerodivisisionError
Vejamos um erro simples que faz com que Python gere uma exceção. Você provavelmente sabe que é impossível dividir um número por zero, mas vamos pedir a Python para fazer isso de qualquer maneira:
print(5 / 0)
Python não pode fazer isso, então temos um traceback.
O erro relatado no traceback, ZerodivisionError, é um objeto de exceção. O Python cria esse tipo de objeto em resposta a uma situação em que não pode fazer o que solicitamos. Quando isso acontece, Python interrompe o programa e nos diz o tipo de exceção que foi gerada. Podemos usar essas informações para modificar nosso programa. Vamos dizer a Python o que fazer quando esse tipo de exceção ocorrer; dessa forma, se isso acontecer novamente, estaremos preparados.
Usando blocos try-except
Quando você acha que pode ocorrer um erro, você pode escrever um bloco try-except para lidar com a exceção que pode ser gerada. Você diz ao Python para tentar executar algum código e diz o que fazer se o código resultar em um tipo específico de exceção.
Aqui está como se parece um bloco try-except para lidar com a exceção do ZerodivisionError:
try:
print(5 / 0)
except ZeroDivisionError:
print('Você não pode dividir por zero!')
Colocamos print(5 / 0), a linha que causou o erro, dentro de um bloco try. Se o código em um bloco try funcionar, o Python pulará o bloco except. Se o código no bloco try causar um erro, o Python procurará um bloco except cujo erro corresponde ao que foi gerado e executa o código nesse bloco.
Neste exemplo, o código no bloco try produz um ZerodivisionError, então o Python procura um bloco except informando o que deve ser respondifo. Python então executa o código nesse bloco e o usuário vê uma mensagem de erro amigável em vez de um traceback.
Se mais código seguisse o bloco try-except, o programa continuará sendo executado porque dissemos ao Python como lidar com o erro. Vejamos um exemplo em que capturar um erro pode permitir que um programa continue em execução.
Usando exceções para evitar quebras no código
Os erros de manuseio corretamente são especialmente importantes quando o programa tem mais trabalho a fazer após a ocorrência do erro. Isso acontece frequentemente em programas que pedem os usuários informações. Se o programa responder à entrada inválida adequadamente, ele poderá solicitar uma entrada mais válida em vez de travar. Vamos criar uma calculadora simples que faça apenas divisão:
print('Me passe dois números e eu farei a divisão deles.')
print('Aperte "q" para sair')
while True:
num_1 = input('\nPrimeiro número: ') # 1
if num_1 == 'q':
break
num_2 = input('\nSegundo número: ') # 2
if num_2 == 'q':
break
resposta = int(num_1) / int(num_2) # 3
print(resposta)
Este programa leva ao usuário a inserir um valor para num_1 (1) e, se o usuário não digitar 'q' para sair, o num_2 é solicitado (2). Em seguida, dividimos esses dois números para obter resposta (3). Este programa não faz nada para lidar com erros; portanto, pedindo que ele divida por zero faz com que ele trave.
É ruim que o programa tenha travado, mas também não é uma boa ideia permitir que os usuários vejam os tracebacks. Os usuários não técnicos ficarão confusos com eles e, em um ambiente malicioso, os invasores aprenderão mais do que você deseja. Por exemplo, eles saberão o nome do seu arquivo de programa e verão uma parte do seu código que não está funcionando corretamente. Às vezes, um invasor qualificado pode usar essas informações para determinar que tipo de ataques a serem usados contra seu código.
O bloco else
Podemos tornar esse programa mais resistente a erros, envolvendo a linha que pode produzir erros em um bloco try-except. O erro ocorre na linha que executa a divisão, e é aí que colocaremos o bloco try-except. Este exemplo também inclui um bloco else. Qualquer código que dependa do bloco try executando com sucesso entra no bloco else:
print('Me passe dois números e eu farei a divisão deles.')
print('Aperte "q" para sair')
while True:
num_1 = input('\nPrimeiro número: ')
if num_1 == 'q':
break
num_2 = input('\nSegundo número: ')
if num_2 == 'q':
break
try: # 1
resposta = int(num_1) / int(num_2)
except ZeroDivisionError: # 2
print('Você não pode dividir por zero!')
else: # 3
print(resposta)
Pedimos ao Python que tente concluir a operação de divisão em um bloco try (1), que inclui apenas o código que pode causar um erro. Qualquer código que dependa do bloco try é adicionado ao bloco else. Nesse caso, se a operação da divisão for bem sucedida, usamos o bloco else para mostrar o resultado (3).
O bloco except, diz a Python como responder quando um ZeroDivisionError surgir (2). Se o bloco try não for bem sucedido devido a um erro de divisão por zero, imprimimos uma mensagem amigável dizendo ao usuário como evitar esse tipo de erro. O programa continua a ser executado e o usuário nunca verá um traceback.
O único código que deve ir em um bloco try é o código que pode causar uma exceção a ser gerada. Às vezes, você terá um código adicional que deve ser executado apenas se o bloco try for bem sucedido; este código vai no bloco else. O bloco except, diz ao Python o que fazer caso surja uma certa exceção quando tenta executar o código no bloco try.
Ao antecipar fontes prováveis de erros, você pode escrever programas robustos que continuam a funcionar mesmo quando encontram dados inválidos e recursos ausentes. Seu código será resistente a erros inocentes do usuário e ataques maliciosos.
Lidando com a exceção do FileNotFoundError
Um problema comum ao trabalhar com arquivos é lidar com arquivos ausentes. O arquivo que você está procurando pode estar em um local diferente, o nome do arquivo pode estar errado ou o arquivo pode não existir. Você pode lidar com todas essas situações com um bloco try-except.
Vamos tentar ler um arquivo que não existe. O programa a seguir tenta ler no conteúdo de Alice no País das Maravilhas, mas não salvei o arquivo alice.txt no mesmo diretório do programa:
from pathlib import Path
caminho = Path('alice.txt')
conteudo = caminho.read_text(encoding='utf-8')
Observe que estamos usando read_text() de uma maneira um pouco diferente aqui do que você viu anteriormente. O argumento de codificação é necessário quando a codificação padrão do seu sistema não corresponde à codificação do arquivo que está sendo lido. É mais provável que isso aconteça ao ler um arquivo que não foi criado no seu sistema. O Python não pode ler em um arquivo ausente, por isso gera uma exceção.
Este é um traceback mais longo do que os que vimos anteriormente, então vamos ver como você pode entender os tracebacks mais complexos. Muitas vezes, é melhor começar no final do traceback . Na última linha, podemos ver que uma exceção do FileNotFoundError foi gerada. Isso é importante porque nos diz que tipo de exceção usar no bloco except que escreveremos.
Olhando para o início do traceback, podemos ver que o erro ocorreu na linha 4 do programa. A próxima linha mostra a linha de código que causou o erro. O restante do traceback mostra algum código das bibliotecas envolvidas na abertura e na leitura de arquivos. Você geralmente não precisa ler ou entender todas essas linhas em um traceback.
Para lidar com o erro que está sendo gerado, o bloco try começará com a linha que foi identificada como problemática no traceback. Em nosso exemplo, esta é a linha que contém read_text():
from pathlib import Path
caminho = Path('alice.txt')
try:
conteudo = caminho.read_text(encoding='utf-8')
except FileNotFoundError: # 1
print(f'Desculpe, o arquivo {caminho} não existe')
Neste exemplo, o código no bloco try produz um FileNotFoundError, por isso escrevemos um bloco except que corresponde a esse erro (1). O Python então executa o código nesse bloco quando o arquivo não pode ser encontrado, e o resultado é uma mensagem de erro amigável em vez de um traceback.
O programa não tem mais nada a fazer se o arquivo não existir, então essa é toda a saída que vemos. Vamos desenvolver este exemplo e ver como o manuseio de exceções pode ajudar quando você estiver trabalhando com mais de um arquivo.
Analisando texto
Você pode analisar arquivos de texto contendo livros inteiros. Muitas obras clássicas de literatura estão disponíveis como arquivos de texto simples, porque estão em domínio público. Os textos usados nesta seção vêm do Projeto Gutenberg. O Projeto Gutenberg mantém uma coleção de obras literárias disponíveis em domínio público e é um ótimo recurso se você estiver interessado em trabalhar com textos literários em seus projetos de programação.
Vamos baixar o texto de Alice no País das Maravilhas (pode ser baixado o arquivo .txt aqui)e tentar contar o número de palavras no texto. Para fazer isso, usaremos o método string split(), que, por padrão, divide uma string onde quer que encontre qualquer espaço em branco:
from pathlib import Path
caminho = Path('alice.txt')
try:
conteudo = caminho.read_text(encoding='utf-8')
except FileNotFoundError:
print(f'Desculpe, o arquivo {caminho} não existe')
else:
# --- Contar o número de palavras no livro --- #
palavras = caminho.split() # 1
num_palavras = len(palavras) # 2
print(f'O arquivo {caminho} possui {num_palavras} palavras')
Eu movi o arquivo alice.txt para o diretório correto, então o bloco try funcionará desta vez. Tomamos a string conteudo, que agora contém todo o texto de Alice no País das Maravilhas como string longa, e usamos split() para produzir uma lista de todas as palavras no livro (1). O uso de len() nesta lista (2) nos fornece uma boa aproximação do número de palavras no texto original. Por fim, imprimimos uma declaração que relata quantas palavras foram encontradas no arquivo. Esse código é colocado no bloco else porque só funciona se o código no bloco try foi executado com sucesso. A saída nos diz quantas palavras existem em alice.txt.
A contagem é um pouco alta porque informações extras são fornecidas pelo editor no arquivo de texto usado aqui, mas é uma boa aproximação do tamanho de Alice no País das Maravilhas.
Trabalhando com vários arquivos
Vamos adicionar mais livros para analisar, mas antes de fazer, vamos mover a maior parte deste programa para uma função chamada contar_palavras(). Isso facilitará a execução da análise para vários livros:
from pathlib import Path
def contar_palavras(caminho):
"""Função que retorna a quantidade de palavras do livro."""
try:
conteudo = caminho.read_text(encoding='utf-8')
except FileNotFoundError:
print(f'Desculpe, o arquivo {caminho} não existe')
else:
# --- Contar o número de palavras no livro --- #
palavras = caminho.split()
num_palavras = len(palavras)
print(f'O arquivo {caminho} possui {num_palavras} palavras')
caminho = Path('alice.txt')
contar_palavras(caminho)
A maior parte desse código permanece inalterada. Só foi recuado e mudou-se para o corpo de contar_palavras(). É um bom hábito manter os comentários atualizados quando você está modificando um programa, para que o comentário também tenha sido alterado para um docstring e reformulado um pouco.
Agora, podemos escrever um loop curto para contar as palavras em qualquer texto que queremos analisar. Fazemos isso armazenando os nomes dos arquivos que queremos analisar em uma lista e, em seguida, chamamos o contar_palavras() para cada arquivo na lista. Tentaremos contar as palavras para Alice no País das Maravilhas, Siddhartha, Moby Dick e Os Contos dos Irmãos Grimm, que estão todas disponíveis em domínio público. Intencionalmente, deixei siddhartha.txt fora do diretório, para que possamos ver como nosso programa lida com um arquivo ausente:
from pathlib import Path
def contar_palavras(caminho):
"""Função que retorna a quantidade de palavras do livro."""
...
arquivos = ['alice.txt', 'siddhartha.txt',
'moby_dick.txt', 'grimm.txt']
for arquivo in arquivos:
caminho = Path(caminho) # 1
contar_palavras(caminho)
Os nomes dos arquivos são armazenados como seqüências simples. Cada string é então convertida em um objeto Path (1), antes da chamada para contar_palavras(). O arquivo siddhartha.txt ausente não tem efeito no restante da execução do programa.
O uso do bloco try-except neste exemplo fornece duas vantagens significativas. Impedimos que nossos usuários vejam um traceback e deixamos o programa continuar analisando os textos que ele pode encontrar. Se não gerarmos o FileNotFoundError que siddhartha.txt cria, o usuário veria um traceback completo e o programa pararia de executar depois de tentar analisar Siddhartha. Isso nunca permitiria a análise de Moby Dick ou Os Contos dos Irmãos Grimm.
Falhando silenciosamente
No exemplo anterior, informamos nossos usuários que um dos arquivos não estava disponível. Mas você não precisa relatar todas as exceções que você pega. Às vezes, você deseja que o programa falhe silenciosamente quando ocorre uma exceção e continue como se nada tivesse acontecido. Para fazer com que um programa falhe silenciosamente, você escreve um bloco try como de costume, mas diz explicitamente a Python para não fazer nada no bloco except. Python tem uma declaração pass que diz para não fazer nada em um bloco:
def contar_palavras(caminho):
"""Função que retorna a quantidade de palavras do livro."""
try:
...
except FileNotFoundError:
pass
else:
...
A única diferença entre esta listagem e a anterior é a instrução pass no bloco except. Agora, quando um FileNotFoundError é gerado, o código no bloco except é executado, mas nada acontece. Nenhum traceback é produzido e não há saída em resposta ao erro gerado. Os usuários veem a contagem de palavras para cada arquivo que existe, mas não vêem nenhuma indicação de que um arquivo não foi encontrado.
A declaração pass também atua como um espaço reservado. É um lembrete de que você está escolhendo não fazer nada em um ponto específico na execução do seu programa e que você pode querer fazer algo lá mais tarde. Por exemplo, neste programa, podemos decidir escrever quaisquer nomes de arquivo ausentes em um arquivo chamado arquivos_faltantes.txt. Nossos usuários não veriam esse arquivo, mas poderíamos ler o arquivo e lidar com os textos ausentes.
Decidir quais erros relatar
Como você sabe quando relatar um erro aos seus usuários e quando deixar seu programa falhar silenciosamente? Se os usuários souberem quais textos devem ser analisados, eles podem apreciar uma mensagem informando-os por que alguns textos não foram analisados. Se os usuários esperam ver alguns resultados, mas não sabem quais livros devem ser analisados, eles podem não precisar saber que alguns textos não estavam disponíveis. Dar às informações dos usuários que eles não estão procurando pode diminuir a usabilidade do seu programa. As estruturas de manipulação de erros da Python oferecem controle de pente fino sobre quanto compartilhar com os usuários quando as coisas dão errado; cabe a você decidir quanta informação compartilhar.
O código bem escrito e testado corretamente não é muito propenso a erros internos, como sintaxe ou erros lógicos. Mas toda vez que seu programa depende de algo externo, como entrada do usuário, a existência de um arquivo ou a disponibilidade de uma conexão de rede, existe a possibilidade de uma exceção gerada. Um pouco de experiência ajudará você a saber onde incluir blocos de manutenção de exceção em seu programa e quanto relatar aos usuários sobre erros que surgem.
Tente você mesmo!
Exercício 06: Um problema comum ao solicitar a entrada numérica ocorre quando as pessoas fornecem texto em vez de números. Quando você tenta converter a entrada em um int, você obterá um ValueError. Escreva um programa que solicite dois números. Adicione-os juntos e imprima o resultado. Pegue o ValueError se um valor de entrada não for um número e imprima uma mensagem de erro amigável. Teste seu programa inserindo dois números e depois digitando algum texto em vez de um número.
Exercício 07: Desenvolva seu código do exercício 6 em um loop while para que o usuário possa continuar digitando números, mesmo que cometam um erro e digite o texto em vez de um número.
Exercício 08: Faça dois arquivos, gatos.txt e cachorros.txt. Armazene pelo menos três nomes de gatos no primeiro arquivo e três nomes de cães no segundo arquivo. Escreva um programa que tenta ler esses arquivos e imprimir o conteúdo do arquivo na tela. Desenvolva seu código em um bloco try-except para evitar o erro FileNotFoundError e imprima uma mensagem amigável se um arquivo estiver ausente. Mova um dos arquivos para um local diferente no seu sistema e verifique se o código no bloco except é executado corretamente.
Exercício 09: Modifique o seu bloco except no exercício 8 para falhar silenciosamente se um arquivo estiver ausente.
Exercício 10: Visite o Projeto Gutenberg e encontre alguns textos que você deseja analisar. Faça o download dos arquivos de texto para esses trabalhos ou copie o texto bruto do seu navegador em um arquivo de texto no seu computador. Você pode usar o método count() para descobrir quantas vezes uma palavra ou frase aparece em uma string. Por exemplo, o código a seguir conta o número de vezes 'digui' aparece em uma string:
frase = 'digui digui digui ê'
print(frase.lower().count('digui'))
Armazenando dados
Muitos de seus programas solicitarão aos usuários que inseram certos tipos de informações. Você pode permitir que os usuários armazenem preferências em um jogo ou forneçam dados para uma visualização. Qualquer que seja o foco do seu programa, você armazenará as informações que os usuários fornecem em estruturas de dados, como listas e dicionários. Quando os usuários fecham um programa, você quase sempre deseja salvar as informações que eles inseriram. Uma maneira simples de fazer isso envolve armazenar seus dados usando o módulo json.
O módulo json permite converter estruturas de dados Python simples em sequências de caracteres formatadas por JSON e, em seguida, carregue os dados desse arquivo na próxima vez que o programa for executado. Você também pode usar o JSON para compartilhar dados entre diferentes programas Python. Melhor ainda, o formato de dados JSON não é específico para o Python, para que você possa compartilhar dados que você armazena no formato JSON com pessoas que trabalham em muitas outras linguagens de programação. É um formato útil, portátil e é fácil de aprender.
O formato JSON (JavaScript Object Notation) foi originalmente desenvolvido para JavaScript. No entanto, desde então se tornou um formato comum usado por muitas linguagens, incluindo Python.
Usando json.dumps() e json.loads()
Vamos escrever um programa curto que armazena um conjunto de números e outro programa que lê esses números de volta à memória. O primeiro programa usará o json.dumps() para armazenar o conjunto de números, e o segundo programa usará json.loads().
A função json.dumps() exige um argumento: os dados que deve ser convertidos no formato JSON. A função retorna uma string, que podemos gravar em um arquivo de dados:
import json
from pathlib import Path
numeros = [2, 3, 5, 7, 11, 13]
caminho = Path('numeros.json') # 1
conteudo = json.dumps(numeros) # 2
caminho.write_text(conteudo)
Primeiro, importamos o módulo json e, em seguida, criamos uma lista de números. Em seguida, escolhemos um nome de arquivo para armazenar a lista de números (1). É costume usar a extensão do arquivo .json para indicar que os dados no arquivo são armazenados no formato JSON. Em seguida, usamos a função json.dumps() (2) para gerar uma string contendo a representação JSON dos dados com os quais estamos trabalhando. Depois de termos essa string, escrevemos no arquivo usando o mesmo método write_text() que usamos anteriormente.
Este programa não possui saída, mas vamos abrir o arquivo numeros.json. Os dados são armazenados em um formato que se parece com Python.
Agora, escreveremos um programa separado que usa json.loads() para ler a lista de volta:
import json
from pathlib import Path
caminho = Path('numeros.json') # 1
conteudo = caminho.read_text() # 2
numeros = json.loads(conteudo) # 3
print(numeros)
Certifique-se de ler do mesmo arquivo que escrevemos anteriormente (1). Como o arquivo de dados é apenas um arquivo de texto com formatação específica, podemos lê-lo com o método read_text() (2). Depois, passamos o conteúdo do arquivo para json.loads() (3). Esta função recebe uma string JSON e retorna um objeto Python neste caso, uma lista. Por fim, imprimimos a lista recuperada de números e vemos que é a mesma lista criada anteriormente. Esta é uma maneira simples de compartilhar dados entre dois programas.
Salvar e ler dados gerados pelo usuário
Salvar dados com o json é útil quando você está trabalhando com dados gerados pelo usuário, porque se você não armazenar as informações do seu usuário de alguma forma, você os perderá quando o programa parar de funcionar. Vejamos um exemplo em que solicitamos ao usuário o nome deles na primeira vez em que eles executam um programa e depois lembram o nome deles quando executarem o programa novamente. Vamos começar armazenando o nome do usuário:
import json
from pathlib import Path
usuario = input('Digite o seu nome: ') # 1
caminho = Path('usuario.json') # 2
conteudo = json.dumps(usuario)
caminho.write_text(conteudo)
print(f'Nos lembraremos de você quando voltar, {usuario}!') # 3
Primeiro, solicitamos um nome de usuário para armazenar (1). Em seguida, escrevemos os dados que acabamos de coletar em um arquivo chamado usuario.json (2). Depois imprimimos uma mensagem informando ao usuário que armazenamos suas informações (3).
Agora, vamos escrever um novo programa que cumprimenta um usuário cujo nome já tenha sido armazenado:
import json
from pathlib import Path
caminho = Path('usuario.json') # 1
conteudo = caminho.read_text(caminho)
usuario = json.loads(conteudo) # 2
print(f'Bem vindo novamente, {usuario}!')
Lemos o conteúdo do arquivo de dados (1) e, em seguida, usamos o json.loads() para atribuir os dados recuperados à variável usuario (2). Como recuperamos o nome de usuário, podemos receber o usuário de volta com uma saudação personalizada.
Precisamos combinar esses dois programas em um arquivo. Quando alguém executar esse script, queremos recuperar o nome de usuário da memória, se possível; caso contrário, solicitaremos um nome de usuário e o armazenaremos em nome de usuario.json para a próxima vez. Poderíamos escrever um bloco try-except aqui para responder adequadamente se o arquivo usuario.json não existir, mas, em vez disso, usaremos um método útil do módulo pathlib:
import json
from pathlib import Path
caminho = Path('usuario.json')
if caminho.exists(): # 1
conteudo = caminho.read_text()
usuario = json.loads(conteudo)
print(f'Bem vindo novamente, {usuario}!')
else: # 2
usuario = input('Digite o seu nome: ')
conteudo = json.dumps(usuario)
caminho.write_text(conteudo)
print(f'Nos lembraremos de você quando voltar, {usuario}!')
Existem muitos métodos úteis que você pode usar com objetos de caminho. O método exists() retorna True se existir um arquivo ou pasta e False se não o existir. Aqui, usamos o caminho.exists() para descobrir se um nome de usuário já foi armazenado (1). Se existir o arquivo usuario.json, carregamos o nome de usuário e imprimimos uma saudação personalizada ao usuário.
Se o arquivo usuario.json não existir (2), solicitamos um nome de usuário e armazenamos o valor que o usuário insere. Também imprimimos a mensagem amigável de que nos lembraremos dele quando ele voltar. Qualquer que seja o bloco, o resultado é um nome de usuário e uma saudação apropriada.
Embora os dados nesta seção sejam apenas uma única string, o programa funcionaria tão bem com quaisquer dados que possam ser convertidos em uma string JSON.
Refatorando
Muitas vezes, você chega a um ponto em que seu código funcionará, mas reconhece que pode melhorar o código dividindo-o em uma série de funções que têm trabalhos específicos. Esse processo é chamado de refatoração. A refatoração facilita a compreensão do seu código, mais fácil de entender e mais fácil de estender.
Podemos refatorar o código anterior movendo a maior parte de sua lógica para uma ou mais funções. O foco do script é cumprimentar o usuário, então vamos mover todo o nosso código existente para uma função chamada saudar_usuario():
import json
from pathlib import Path
def saudar_usuario():
"""Saudar o usuário pelo nome.""" # 1
caminho = Path('usuario.json')
if caminho.exists():
conteudo = caminho.read_text()
usuario = json.loads(conteudo)
print(f'Bem vindo novamente, {usuario}!')
else:
usuario = input('Digite o seu nome: ')
conteudo = json.dumps(usuario)
caminho.write_text(conteudo)
print(f'Nos lembraremos de você quando voltar, {usuario}!')
saudar_usuario()
Como estamos usando uma função agora, reescrevemos os comentários como um documento que reflete como o programa funciona atualmente (1). Esse arquivo é um pouco mais limpo, mas a função saudar_usuario() está fazendo mais do que apenas cumprimentar o usuário, também está recuperando um nome de usuário armazenado se houver e criar o arquivo para um novo nome de usuário.
Vamos refatorar o saudar_usuario() para que ele não esteja executando tantas tarefas diferentes. Começaremos movendo o código para recuperar um nome de usuário armazenado para uma função separada:
import json
from pathlib import Path
def obter_nome_usuario_armazenado(caminho):
"""Obter o nome do usuário armazenado no arquivo JSON."""
if caminho.exists(): # 1
conteudo = caminho.read_text()
usuario = json.loads(conteudo)
return usuario
else:
return None # 2
def saudar_usuario():
"""Saudar o usuário pelo nome."""
caminho = Path('usuario.json')
usuario = obter_nome_usuario_armazenado(caminho)
if usuario: # 3
print(f'Bem vindo novamente, {usuario}')
else:
usuario = input('Digite o seu nome: ')
conteudo = json.dumps(usuario)
caminho.write_text(conteudo)
print(f'Nos lembraremos de você quando voltar, {usuario}!')
saudar_usuario()
A nova função obter_nome_usuario_armazenado() (1) tem um objetivo claro, conforme declarado na docstring. Esta função recupera um nome de usuário armazenado e retorna o nome de usuário se encontrar um. Se o caminho que for passado para obter_nome_usuario_armazenado() não existir, a função retorna None (2). Isso é uma boa prática: uma função deve retornar o valor que você está esperando ou não deverá retornar None. Isso nos permite executar um teste simples com o valor de retorno da função. Imprimimos uma mensagem de volta ao usuário se a tentativa de recuperar um nome de usuário for bem-sucedida (3) e, se não for, solicitamos um novo nome de usuário.
Devemos levar em consideração mais um bloco de código de saudar_usuario(). Se o nome de usuário não existir, devemos mover o código que solicita um novo nome de usuário para uma função dedicada a esse objetivo:
import json
from pathlib import Path
def obter_nome_usuario_armazenado(caminho):
"""Obter o nome do usuário armazenado no arquivo JSON."""
...
def obter_novo_usuario(caminho):
"""Pede ao usuário o seu nome."""
usuario = input('Digite o seu nome: ')
conteudo = json.dumps(usuario)
caminho.write_text(conteudo)
return usuario
def saudar_usuario():
"""Saudar o usuário pelo nome."""
caminho = Path('usuario.json')
usuario = obter_nome_usuario_armazenado(caminho) # 1
if usuario:
print(f'Bem vindo novamente, {usuario}')
else:
usuario = obter_novo_usuario(caminho) # 2
print(f'Nos lembraremos de você quando voltar, {usuario}!')
saudar_usuario()
Cada função nesta versão final do código possui um objetivo único e claro. Chamamos saudar_usuario(), e essa função imprime uma mensagem apropriada: ela recebe de volta um usuário existente ou cumprimenta um novo usuário. Faz isso chamando obter_nome_usuario_armazenado() (1), responsável apenas por recuperar um nome de usuário armazenado se houver um. Por fim, se necessário, saudar_usuario() chama obter_novo_usuario() (2), responsável apenas por obter um novo nome de usuário e armazená-lo. Essa compartimentação do trabalho é uma parte essencial da redação de código claro que será fácil de manter e estender.
Tente você mesmo!
Exercício 11: Escreva um programa que solicite o número favorito do usuário. Use json.dumps() para armazenar esse número em um arquivo. Escreva um programa separado que lê este arquivo e imprime a mensagem "Eu sei o seu número favorito! É _____".
Exercício 12: O exemplo do código usado no tópico de refatoração armazena apenas uma informação, o nome de usuário. Expanda este exemplo solicitando mais duas informações sobre o usuário e guarde todas as informações que você coleta em um dicionário. Escreva este dicionário em um arquivo usando json.dumps() e leia-o novamente usando json.loads(). Imprima o que seu programa se lembra do usuário.
Resumo
Neste capítulo, você aprendeu a trabalhar com arquivos. Você aprendeu a ler o conteúdo inteiro de um arquivo e depois trabalhar com o conteúdo uma linha de cada vez, se necessário. Você aprendeu a escrever o máximo de texto que deseja um arquivo. Você também leu sobre exceções e como lidar com as exceções que provavelmente verá em seus programas. Por fim, você aprendeu a armazenar estruturas de dados do Python para armazenar informações que seus usuários fornecem, impedindo que eles tenham que começar cada vez que executam um programa. Com todo o conteúdo passado em todos os capítulos, você está apto a criar seus próprios programas!