Menu fechado

Python3 08 – Tratamento de Exceções e Arquivos

Este texto será um adendo aos tópicos apresentados até o momento, com a adição de dois conteúdos que acabaram ficando de fora dos textos anteriores: o tratamento de exceções/erros e leitura e escrita em arquivos.

No texto seguinte será adicionado mais um tópico para completar a base de conhecimentos para a programação em Python, com a Orientação a Objetos, mas por agora este pequeno adendo.

1. Tratamento de Erros/Exceções

Exceções são situações atípicas em que o código eventualmente possa operar, as quais envolvem tanto o tratamento de erros de fato quanto situações especiais. Algumas exceções envolvem: divisão por zero; abrir um arquivo inexistente; gravar um arquivo em uma pasta inexistente; disco cheio; fazer uma operação não suportada entre objetos diferentes; acessar dados reservados do programa; fazer uma operação não suportada por um objeto, entre outras.

Algumas destas situações não são necessariamente um erro e, em alguns casos, é comum o emprego de “erros” de execução para fazer o código sair de um laço e executar um conjunto de ações específicas. Algumas exceções se referem a situações para as quais seu programa não foi preparado para lidar e nem mesmo é intenção de que o seja. Nestes casos, pode ser mais conveniente que o código trate adequadamente estas ocorrências do que a mera interrupção do programa.

Já em outras situações, a mensagem de erro padrão do interpretador pode não ser a mais adequada ao usuário ou mesmo ao programador, sendo aconselhável customizar a saída de erro para algo mais informativo que uma divisão por zero.

Neste texto, será mostrado como tratar adequadamente estas situações, seja erguendo exceções quando necessário ou mesmo dando uma resolução diferenciada para cada exceção encontrada.

1.1. Raise Exception

O código a seguir implementa o gerador do enésimo elemento da série de Fibonacci, empregando uma função recursiva:

#!/bin/env python3
#
# Emprega uma função recursiva para gerar o enésimo
# elemento da série de Fibonacci
#

def fib_rec(n):
    if n < 2:
        return n
    else:
        return fib_rec(n-1) + fib_rec(n-2)


while True:
    n = int(input('Entre com um número: '))

    print(fib_rec(n))

O código é um sucesso para os primeiros 20 inteiros, dependendo de sua máquina, mas começa a ficar lento para elementos bem superiores a 20. Não se trata de uma limitação da linguagem, pois o Python suporta recursividade, por padrão, até 994 chamadas recursivas e, após isto, o interpretador interrompe a execução, gerando o erro RecursionError. A função recursiva a seguir ilustra isto:

Isto significa que a função fibonacci() acima poderia calcular os elementos da série até o elemento 994, já que a profundidade máxima da função recursiva fibonacci(n), como está proposta, é de n recursões. No entanto, o número de chamadas à função fibonacci(n) dobra a cada chamada, fazendo o número de execuções da função crescer exponenciante, ou seja, para fibonacci(2), a função é chamada para resolver fibonacci(1) e fibonacci(0), totalizando 3 chamadas.

Com fibonacci(3), será chamado fibonacci(2) e fibonacci(1), e fibonacci(2) chamará novamente fibonacci(1) e fibonacci(0), o que totalizará 5 chamadas.

Com fibonacci(4) será chamado fibonacci(3) e fibonacci(2). Como visto acima, a chamada fibonacci(3) executa 5 chamadas à função, enquanto fibonacci(2) executa 3 chamadas. Ao final, fibonacci(4) realizará 3 chamadas à função fibonacci() para calcular o fibonacci(2) e 5 chamadas para calcular o fibonacci(3). Isto resultará em 1 + 3 + 5 = 9 chamadas para calcular o fibonacci(4). O 1 representa a primeira chamada da função, fibonacci(4).

A figura a seguir expressa estas três chamadas à função fibonacci() descritas acima.

Apenas para registro, uma chamada de fibonacci(20) executaria 21.891 chamadas na função fibonacci(), enquanto que fibonacci(21) executaria 35.421, por fim, fibonacci(994) executaria 7,84E+207, chamadas para entregar o resultado.

É possível fazer está função recursiva ficar bem mais eficiente, criando uma ‘memória’ dos valores já calculados com um dicionário, mas isto não será feito aqui. Da forma como está, calcular o fibonacci(994) é algo proibitivo. Para uma resposta satisfatória desta função, é razoável limitar o seu uso a um valor de n de no máximo 20. Uma das formas de fazer isto é monitorar a entrada da função fibonacci() e erguer uma exceção com o comando raise, para gerar uma mensagem de erro e interrompendo sua execução caso n > 20. A sintaxe do comando raise é apresentada a seguir:

 
raise baseException('Mensagem')

Onde baseException pode ser qualquer um das exceções tratadas pelo Python: BaseException list. A Mensagem, se houver, será apresentada ao final, logo após o exceção acionada.

Para limitar a execução recursiva da função fibonacci(), são adicionadas as linhas 8 e 9 ao código, como apresentado abaixo:

def fib_rec(n):
    if n > 20:
        raise ValueError('Número deve ser inferior a 21')
    if n < 2:
        return n
    else:
        return fib_rec(n-1) + fib_rec(n-2)

Após isto, um nova execução do programa irá gera um erro ValueError caso o argumento passado exceda a 20.

1.1.1. Exceções Customizadas

Em alguns momentos, pode ser necessário criar suas próprias exceções. Isto é feito criando uma classe herdeira de algumas das exceções da lista baseException do Python. Embora não tenha falado sobre orientação a objetos até o momento, a classe proposta aqui é muito simples para ser deixada para depois. Paras as necessidades atuais, será passado apenas com um comando pass para a classe, o qual não faz nada. Veja a sintaxe abaixo:

class exceptionName(baseException): pass

O exceptionName será o nome da nova exceção criada e o baseException, novamente, é uma das exceções padrões do Python. A alteração a seguir modifica o programa fibonacci.py para criar uma nova exceção baseada em RecursionError e a emprega para interromper o programa.

#!/bin/env python3
#
# Emprega uma função recursiva para gerar o enésimo
# elemento da série de Fibonacci
#

class RecursionLimited(RecursionError): pass

def fib_rec(n):
    if n > 20:
        raise RecursionLimited('Número deve ser inferior a 21')
    if n in (0, 1):
        return n
    else:
        return fib_rec(n-1) + fib_rec(n-2)


while True:
    n = int(input('Entre com um número: '))

    print(fib_rec(n))

Sua execução deve gerar uma saída semelhante à apresentada abaixo:

1.2. Try

Para deixar o programa de fibonacci.py mais eficiente, é mais funcional empregar um laço for para gerar os elementos da série de Fibonacci, como abaixo:

#!/bin/env python3
#
# Calcula o enésimo elemento da série de Fibonacci
#

def fibonacci(n):
    a, b = 1, 1
    for i in range(n-1):
        a, b = b, a+b
    return a

while True:
    n = int(input('Entre com um número: '))

    print(fibonacci(n), '\n')

Na forma que se encontra, é possível gerar o milésimo elemento da série de Fibonacci sem dificuldade.

No entanto, o programa entra em um laço while infinto, carregando um inteiro n capturado pelo teclado no comando input() que, em seguida, é passado à função fibonacci(n) para calcular o elemento da série. Não existe uma saída natural do laço, a menos que se envie um código de interrupção ao programa, Control+C, ou se crie um erro ValueError, gerado pela função int() ao tentar fazer uma transformação indevida de uma string, como uma letra 'q', em um inteiro.

Uma possível solução é utilizar este erro gerado para criar uma saída “limpa” do programa. Isto pode ser feito através do comando de tratamento de exceções, try.

O comando try permite filtrar os erros gerados na seção de código do bloco try, desviando a execução para um bloco alternativo. Segue abaixo o comando na sua sintaxe mais básica:

try:
    <código_0>
except:
    <código_1>

Neste caso, o bloco de comandos código_0 é executado normalmente. Porém, se ocorrer qualquer exceção neste bloco, a sua execução é interrompida e o interpretador passa a execução para o bloco de comandos após a sentença except, o bloco de comandos código_1.

Numa sintaxe mais completa, é possível filtrar diversos tipos de exceções diferentes. Veja abaixo uma sintaxe mais completa:

try:
    <código_0>
except SomeException_1:
    <código_1>
except SomeException_2:
    <código_2>
except SomeException_3:
    <código_3>
...
except:
    <código_geral>
else:
    <código_else>
finally:
    <código_finally>

Os SomeException_i podem ser qualquer uma das exceções da lista baseException.

Neste caso, o bloco de comandos código 0 é executado até que alguma exceção ocorra. Caso ocorra, seu processamento é interrompido e é passado para a execução do bloco de comandos da exceção gerada correspondente. Se nenhuma das exceções tratadas corresponder à exceção gerada, o bloco código_geral será executado.

O comando try também suporta as cláusulas else e finally. Na forma como está, a cláusula else será executada apenas se nenhuma exceção ocorrer na execução do bloco de comandos código_0. Já a cláusula finally será executada independentemente do fato de alguma exceção ser gerada ou não no bloco de comandos código_0.

Para ilustrar esta estrutura na prática, considere o código da função div(a, b), a seguir, para executar uma mera divisão entre duas variáveis:

Observe que a cláusula finally é executada sempre, independente do resultado da divisão, enquanto que a else somente é executada quando nenhuma exceção é gerada no primeiro bloco de comandos do try. A função int() empregada neste código é apenas para gerar a exceção ValueError ao tentar transformar uma string 'aa' em um inteiro. A intenção foi a de gerar uma exceção diferente das já filtradas.

Voltando ao programa fibonacci.py, a adição do comando try dentro do laço de repetição while cria uma saída eficaz para o programa:

#!/bin/env python3
#
# Calcula o enésimo elemento da série de Fibonacci
#

def fibonacci(n):
    a, b = 1, 1
    for i in range(n-1):
        a, b = b, a+b
    return a

while True:
    try:
        n = int(input('Entre com um número: '))
    except:
        exit()

    print(fibonacci(n))

Agora, qualquer coisa diferente de um número faz com que o programa termine sua execução.

2. Arquivos no Python

Dentre os conteúdos esquecidos nestes textos, de certo o manuseio de arquivos foi dos mais críticos. Escrever e ler arquivos é parte essencial de qualquer trabalho em computação, estando presente em quase todas as atividades realizadas em frente a um computador, seja escrevendo um texto, planilha, alterando um banco de dados, jogando um jogo e outras muitas tarefas cotidianas. O Python possui suporte a diversos tipo de arquivos, seja nativamente ou através de bibliotecas de terceiros. No entanto, neste texto será tratado apenas do acesso básico de escrita e gravação em arquivos texto e binário.

2.1 Comando Open

O acesso padrão a arquivos texto e binário no Python é feito através de um objeto criado pelo comando open(). Na sua forma mais básica, sua sintaxe é:

open(filename, mode='r')

Onde filename é no nome do arquivo com o caminho (path) e mode, o modo de acesso que pode conter os seguintes valores:

  • ‘r’ – abre o arquivo para leitura (read, modo padrão);
  • ‘w’ – abre o arquivo para escrita (write);
  • ‘x’ – cria um arquivo novo e o abre para escrita;
  • ‘a’ – abre um arquivo existente para adicionar (append) novo conteúdo ao seu final;
  • ‘b’ – abre um arquivo no modo binário;
  • ‘t’ – abre um arquivo no modo texto (padrão);
  • ‘+’ – abre um arquivo para atualizações, leitura e escrita;

O modo padrão é ‘rt’, ou seja, abre um arquivo em modo leitura e texto. O modo ‘x’ é semelhante ao ‘w’, mas gera a exceção FileExistsError, se o arquivo já existir.

A sintaxe completa do comando é apresentada a seguir:

open(file, mode='r', buffering=-1, encoding=None, errors=None, 
     newline=None, closefd=True, opener=None)

Onde:

  • buffering – passa o tamanho do buffer de leitura e escrita. O valor 0 desabilita o buffer e o 1 seleciona o line buffering, usado apenas no modo texto. Qualquer inteiro > 1 indica o tamanho do buffer reservado para leitura/escrita do arquivo. Por padrão, o buffer é configurado em ‘automático’ (-1), acionando o modo line buffering para arquivos texto e 4096 ou 8192 bytes para arquivos binários, dependendo do tamanho de blocos do disco;
  • encoding – é o nome do encode usado no arquivo. O encode padrão é buscado no sistema. Para outros encodes, seu nome deve ser passado pela variável encoding. Geralmente, usa-se o comando como segue:

    ou na sequência adequada, sem especificar a variável,

    Este comando irá abrir o arquivo ‘exemplo.txt’ no modo ‘rt’, com line buffering, e encode ‘utf-8’, que é o padrão dos arquivos criados na maioria dos sistemas;

  • newline – controla como os finais de linha são implementados no arquivo texto. O padrão None translada qualquer final de linha padrão como '\n' (new line), '\r' (return) e '\r\n' (return + new line – usando no MS Windows) para um '\n', que é o padrão no unix. Se newline for igual a '' ou '\n', não haverá translado do final de linha;

Para mais informações dos demais argumentos e mais alguns complementos, aconselho acessar o manual interno do Python pela linha de comando no interpretador: help(open).

Além de abrir o arquivo para escrita/leitura, o comando open() ainda retorna um objeto que depende das informações passadas ao comando.

No caso de um arquivo aberto no modo texto, seja leitura ou escrita, o objeto retornado será sempre um TextIOWrapper.

Para um arquivo aberto no modo binário + leitura, será retornado um objeto BufferedReader. Já no modo binário + escrita, ele retorna um objeto BufferedWriter. Por fim, para um arquivo aberto no modo binário + leitura e escrita (acesso randômico), o comando retorna um objeto BufferedRandom.

Cada objeto contém seu conjunto de métodos e atributos necessários para o devido manuseio do arquivo. Este texto será mais focado nos métodos dos objetos gerados pela classe TextIOWrapper, no entanto, vários métodos e atributos apresentados aqui são comuns aos dos objetos gerados pelas classes BufferedReader, BufferedWriter e BufferedRandom, ficando pouco a acrescentar.

Caso tenha interesse em mais informações, um bom ponto de partida são as documentações internas do Python, que podem ser bem exploradas com a combinação dos comandos:

  • dir(objeto), para listas os métodos e atributos do objeto;
  • help(objeto.método), para ver a documentação do método específico.

Outra boa fonte de informação é a documentação online em Python 3 – Documentation, além de diversos livros e sites na internet.

2.2. Métodos e Atributos do objeto TextIOWrapper

Aqui seguirá apenas uma lista dos atributos e métodos mais essenciais para a tarefa de acesso e manuseio de um arquivo. Dentre os atributos, foram destacados:

  • closed – verdadeiro se o arquivo estiver fechado;
  • mode – modo em que o arquivo foi aberto;
  • name – nome do arquivo aberto, com o caminho;

No exemplo acima, o arquivo '/etc/fstab' é aberto para leitura no modo texto, 'tr', e o objeto TextIOWrapper do arquivo é criado e passado para o apontador fin.

Agora os métodos:

  • close() – fecha o arquivo e altera o atributo closed para True. Se o arquivo foi aberto para escrita, o conteúdo pendente em buffer é descarregado e, depois, o arquivo é fechado.

    Aqui, o arquivo apontado por fin é fechado. Em seguida, é apresentada a mudança de status do seu atributo closed, indicando que o arquivo está fechado;

  • detach() – remove o BufferedReader do objeto TextIOWrapper e o retorna.

    Na prática, é como se descarregasse o objeto apontado por fin e transferisse seu conteúdo para o apontador conteúdo.

  • fileno() – retorna um descritor de arquivo (um inteiro) diferente para cada arquivo aberto.
  • Um número descritor diferente é gerado a cada novo arquivo aberto;

  • flush() – aplicado somente em arquivos abertos no modo escrita, este método descarrega os bytes no buffer para serem gravados;
  • read(size=-1) – geralmente é usado size &gt 0 para a leitura de arquivos binários. Neste caso, o comando irá ler o conteúdo do arquivo o número de caracteres/bytes passado. Com size igual a -1, será lido até alcançar o final do arquivo, EOF (End Of File);

    No primeiro read(10), são lidos os primeiros 10 caracteres do arquivo fibonacci.py. Já na segunda execução do método read(), o restante do arquivo é lido;

  • readable() – retorna verdadeiro se o arquivo puder ser lido;
  • writable() – retorna verdadeiro se o arquivo puder ser escrito.

    Como o arquivo foi aberto no modo leitura, este não aceita escrita;

  • readline() – ler uma linha do arquivo. A linha é lida até ser encontrado um caractere de final de linha, no caso um ‘\n’.
  • readlines() – retorna uma lista com todas as linhas do arquivo.

    As linhas do arquivo são lidas para a lista lines. O laço for, seguinte, imprime as linhas removendo o último caractere de cada linha, um '\n'. Isto foi feito apenas para não imprimir duas quebras de linha seguidas, já que o comando print() adiciona um '\n' ao final de cada impressão;

  • seek(n) e tell() – o método seek(n) permite posicionar a leitura do arquivo no enésimo caractere, ou byte, caso o arquivo tenha sido aberto no modo binário. O tell() retorna a posição atual de leitura no arquivo.

    Na segunda linha, todo o conteúdo do arquivo é lido para a memória e o apontador para este conteúdo é passado para conteúdo. Feito isto, qualquer leitura posterior do arquivo retorna uma string vazia, uma vez que o seu final já foi atingido. O acesso ao método tell() retorna a posição do final do arquivo, 261.

    Como usei acentuação no texto, alguns caracteres acentuados, como o é e o ú, são representados por um código de dois bytes, embora correspondam a apenas um caractere no texto. Por este motivo, o comprimento do apontamento conteúdo, medido pela função len(), possui 3 bytes a menos (258) que a última posição do arquivo (261).

    O método seek(0) reposiciona ao início do arquivo, permitindo que seu conteúdo possa ser lido novamente;

  • seekable() – retorna verdadeiro se o arquivo suportar acesso randômico. Se retornar falso, os métodos seek(), tell() e truncate() irão gerar a excessão OSError;
  • truncate(size=None) – no modo escrita, este método redimensiona o tamanho do arquivo em size bytes e retorna seu novo tamanho;
  • write(text) – escreve o conteúdo passado em text no arquivo de saída e retorna o comprimento de bytes escritos.

    Nas linhas acima, o arquivo fibonacci.py foi copiado para o fib_copy.py, mas antes de fechar o arquivo fib_copy.py, este foi truncado em apenas 10 caracteres com o método truncate(10). Por isso, o arquivo fib_copy.py terminou com apenas os caracteres ‘#!/bin/env‘ em seu conteúdo;

  • writelines(lines) – escreve o conteúdo da lista lines no arquivo, sendo cada elemento de lines uma linha escrita no arquivo.

    Neste último exemplo, é empregado o método readlines() para ler as linhas do arquivo fibonacci.py e o writelines(list), para gravá-las no arquivo de cópia fib_copy.py.

3. Considerações

Este texto foi a primeira adição a assuntos esquecidos e, possivelmente, outros devem ser adicionados à medida que forem identificados. Na lista de textos a serem escritos, está uma discussão mais objetiva sobre iteradores e geradores, seguido pelas funções zip, map e lambda, a serem adicionados no mesmo contexto, adições.

Deixe uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

This site uses Akismet to reduce spam. Learn how your comment data is processed.