Tratamento de Exceções

Exceções em Python

Alguma vez, enquanto testando seus programas Python, você recebeu na tela uma mensagem de erro parecida com a seguinte?

Traceback (most recent call last):
File "<stdin>", line 1, in \<module\>
IndexError: list index out of range

Ou com essa?

Traceback (most recent call last):
File "<stdin>", line 1, in \<module\>
NameError: name 'x' is not defined

Em caso positivo, Python lhe atirou uma exceção e você nada fez com ela. Imagine que você esteja escrevendo uma calculadora bem simples usando Python como linguagem para a implementação. Tudo que ela faz são as quatro operações básicas (soma, subtração, multiplicação, divisão) e o cálculo da raiz quadrada. Ao terminar a implementação, você mostra ao seu irmão, cheio de orgulho a calculadora que acabou de implementar. O guri já vai direto ao ponto, seleciona a opção de raiz quadrada e fornece o valor:

-1

Antes de ele pressionar o enter para confirmar a operação, você já percebeu que esqueceu de tratar a entrada do usuário em operações de raiz quadrada. Você já dever saber que raiz quadrada de número negativo só é possível se utilizarmos números complexos, não? E como tal, já imagina que a função sqrt() do módulo math que você usou vai ter problemas para lidar com isso. Quando seu irmão confirma a operação, lá vem a mensagem:

Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: math domain error

ValueError. Guarde esse nome para daqui a pouco…

Prontamente, você abre o seu editor de texto e corrige o código, verificando se número do qual será calculada a raiz quadrada é menor que zero.

if op1 < 0:
    print 'Atenção! Não é possível calcular a raiz quadrada de um número negativo!'

OK, tudo certo. Seu programa voltou a funcionar e o chato do seu irmão pode parar de te incomodar.

Mas e o tal do ValueError que pedi para você lembrar? Então, ele é uma exceção que Python usa para lhe indicar que algo anormal aconteceu durante a execução do seu código. E ela não serve apenas como uma simples mensagem de erro. Muito pelo contrário, o objetivo é que o programador, sabendo que determinado trecho de código pode disparar uma exceção dessas, utilize uma estrutura específica para tratamento de exceções (try-except, para os íntimos).

try-except

A estrutura básica do bloco try-except é:

try:
    # código a ser executado "sob vigilância"
except Exception:
    # caso ocorrer alguma exceção no código acima,
    # trate-a aqui.

O código localizado entre o try: e o except é executado de forma que se algo de errado acontecer ali, o except Exception vai imediatamente chamar o código localizado abaixo de si para realizar o tratamento da exceção. Ou seja, se algo de errado acontecer na linha 2, as linhas 4-5 serão executadas para tratar esse erro.

Resolvendo o problema

Sabendo um pouco como as exceções funcionam, e sabendo que a tentativa de calcular a raiz quadrada de um número negativo gera um ValueError, você irá envolver o trecho de código que poderá gerar tal exceção em um bloco try-except. Veja:

try:
    result = math.sqrt(op1)
except Exception:
    print 'Atenção! Não é possível calcular a raiz quadrada de um número negativo!'

Ao executar o código acima e passar um valor negativo para o segundo operando da divisão, você vê que funcionou. Agora, ao invés da mensagem feia na tela, você recebe sua mensagem de erro personalizada (mais tarde veremos que o mecanismo de exceção não serve só para mandarmos mensagens de erro para o usuário, mas também (e principalmente) para tratar as exceções.).

E aí, a mala do seu irmão vem testar o programa novamente. Desta vez, ele fornece o próprio nome como entrada para a raiz quadrada. Você já imagina que lá vem outro erro, afinal Python não é mágico para calcular a raiz quadrada de "Joaozinho". 😛 Para sua surpresa, eis que aparece na tela a mensagem:

Atenção! Não é possível calcular a raiz quadrada de um número negativo!

Aí você e seu irmão não entendem mais nada, afinal ninguém passou um número negativo como entrada. Na realidade, o que aconteceu é que o bloco:

except Exception:
    print 'Atenção! Não é possível calcular a raiz quadrada de um número negativo!'

significa mais ou menos o seguinte: se ocorrer uma exceção qualquer, nas linhas entre o try e a linha except Exception:, trate-a com o bloco de código que vem após o : (que é o nosso print).

Perceba, except Exception: irá agarrar QUALQUER exceção que Python atirar, pois Exception é a classe-base da qual todas as outras exceções são descendentes. Se você quiser que a mensagem 'Atenção! Não é possível calcular a raiz quadrada de um número negativo!' seja mostrada ao usuário apenas quando for passado um valor negativo à raiz quadrada, então teremos que especializar o nosso tratamento de exceções, isto é, especificar qual é a exceção que queremos tratar realmente. Como vimos anteriormente, a exceção que Python atira quando ocorre uma tentativa de cálculo de raiz quadrada de um valor negativo se chama ValueError. Então, fazemos:

except ValueError:
    print 'Atenção! Não é possível calcular a raiz quadrada de um número negativo!'

Opa, agora sim, a mensagem só vai aparecer quando houver o erro supracitado. É claro que Python não fornece uma exceção para cada um de todos os possíveis tipos de erros, mas ele já vem de fábrica com um bocado. Às vezes, pode ser preciso que você implemente sua própria exceção, como por exemplo, se o seu programa recebesse do usuário o valor do CPF. Você poderia criar uma exceção CPFInvalidError, ou seja qual for o nome que quisesse dar. Mas, criação de exceções customizadas é assunto para outro post.

Tratando as Exceções

Talvez você esteja pensando: “mas jogar uma mensagem de erro na tela não é bem o que eu entendia por tratamento de exceção”. E você está certo, podemos fazer muito mais do que isso. No exemplo anterior, podemos corrigir o erro do usuário, transformando o número negativo em um número positivo. O código a ser executado ao tratarmos uma exceção é código Python como qualquer outro, então você pode fazer o que for preciso para corrigir o problema.

try:
    result = math.sqrt(op1)
except ValueError:
    print 'Atenção! Tranformando entrada negativa em número positivo.'
    op1 = -op1
    result = math.sqrt(op1)

Nota: Esse exemplo é meramente didático, para você entender como funcionam as exceções e como elas podem ter vários usos, mas ele não é um bom exemplo do uso de exceções. Leia a seção das boas práticas e entenda por quê.

Assim, além de avisar ao usuário sobre o erro, já estamos fazendo a correção, de forma que o programa possa seguir seu fluxo normal de execução.

Por fim, talvez você esteja se perguntando como vai adivinhar o nome da exceção para colocar no except. Uma das formas de descobrir quais exceções determinadas funções/operações disparam quando acontece uma situação imprevista, é lendo a documentação da função/operação que você estiver utilizando. Outra modo de descobrir é encarando a mensagem de erro quando acontece uma exceção não tratada. Por exemplo:

>>> math.sqrt("hello")
Traceback (most recent call last):
File "", line 1, in
TypeError: a float is required

Ao executar o comando math.sqrt("hello"), acabamos de descobrir que ele atira uma exceção do tipo TypeError quando recebe uma entrada de um tipo diferente do numérico.

Boas práticas no tratamento de exceções

Agora que você (e o seu irmão) já estão manjando como funcionam exceções e o que dá pra fazer com elas, está na hora de conhecerem algumas boas práticas, alguns conselhos que geralmente é uma boa idéia seguir, na labuta diária com exceções.

  1. Não use exceções para o fluxo principal da aplicação. Ou, dizendo de outro modo, use exceções apenas para o que é realmente excepcional, inesperado. A idéia é que, enquanto seu programa esteja executando o caminho feliz, isto é, enquanto está tudo dentro do esperado — o céu está azul, os planetas estão girando em torno do Sol e a Lua em torno da Terra — ele não precise voltar atrás na sua execução devido a uma exceção ter sido lançada. Isso é uma boa idéia por que o seu código ficará mais direto ao ponto e você ainda evita o eventual custo adicional que programar “orientado a exceções” acarreta (por exemplo, por tentar fazer uma coisa, voltar e tentar de novo). No exemplo utilizado nesse post, poderíamos simplesmente evitar que a exceção ocorra fazendo uma simples verificação sobre o valor, com um if.
  2. Interceptar exceções somente quando você sabe como tratá-la. Ou seja, não adianta de muita coisa encher o seu código de try-except se tudo o que o seu tratamento de exceção faz é imprimir uma mensagem de erro na tela. Trate exceções específicas, e quando você souber como lidar com ela. Senão você pode acabar obscurecendo uma exceção importante (como KeyboardInterrupt ou SystemExit, por exemplo) que era melhor lançá-la mesmo.
  3. Conhecer as exceções da biblioteca-padrão. Tire um tempo para dar uma olhada com cuidado nos tipos de exceções que Python já traz de fábrica, pois além de conhecer algumas exceções que você pode usar no seu programa, você passa a conhecer os principais tipos de situações inesperadas que o seu programa está sujeito.

Programe defensivamente com asserções

O trânsito é um dos ambientes em que o ser humano reconhece que precisa de ajuda pra que funcione direito pra todo mundo. Digo aqui “reconhece” querendo dizer na verdade o contrário: todo mundo acha que é o único bom motorista no mundo, que os outros é que estão querendo ferrá-lo furando sinal fechado, correndo que nem loucos e mudando de pista sem sinalizar, bando egoístas e mal-educados… Enfim, por causa disso a gente aprende a dirigir defensivamente, isto é, se prevenir dos problemas antes que eles aconteçam, evitando situações que coloquem em risco você ou os demais. 1

Em programação não é diferente. Programadores são humanos, cometem muitos erros, e em geral têm dificuldade em reconhecer que seu código não está legal, colocam a culpa no código dos outros, na linguagem, na biblioteca utilizada, no compilador e até no sistema operacional — não obstante o fato de existirem muitos outros programas usando as mesmas coisas e sem apresentar o problema do programa deles.

Assim como a direção defensiva envolve não só seguir as regras de trânsito mas também se preparar para condições adversas, na programação defensiva nosso código precisa mais do que compilar na nossa máquina e funcionar com os testes “caminho feliz” que a gente faz usualmente.

Uma boa prática de programação defensiva é verificar pré-condições do seu código, isto é, coisas que o código precisa que sejam verdadeiras para poder funcionar corretamente. Por exemplo, você pode querer verificar para um certo método se um argumento é não-nulo, ou se não é vazio para o caso de uma lista, ou se representa um objeto de determinado tipo. Em Python, você pode fazer isso criando uma asserção, usando o comando assert:

def buscaPorCpf(cpf):
    assert cpf != None, "argumento cpf nao pode ser None"
    print 'cpf:', cpf

A asserção é uma afirmação sobre o que deve ser verdadeiro em dado momento da execução do seu código. Na prática, ela funciona parecido com uma validação para se defender de erros de um usuário: a diferença é que a asserção é pra se defender de erros do programador, incluindo você mesmo. Quando a condição de uma asserção é verificada sendo falsa, o interpretador Python lança uma AssertionError, opcionalmente com a mensagem que especifique o erro.

É sempre bom usar mensagens que explicitem o tipo de erro, principalmente quando a condição é mais específica, para facilitar a detecção dos problemas, além de facilitar a leitura do código. Por exemplo, você pode querer também verificar se o argumento cpf é do tipo string:

def buscaPorCpf(cpf):
    assert cpf != None, "argumento cpf nao pode ser None"
    assert isinstance(cpf, basestring), "argumento cpf precisa ser uma string"
    print 'cpf:', cpf

Nota: A checagem aqui é feita de tal forma que aceita tanto instâncias de str (ex.: '000.000.000-00') e de unicode (ex.: u'000.000.000-00'), que são subclasses de basestring

Repare que a asserção aqui está se defendendo de coisas básicas, pré-condições que se forem falsas, são obviamente um erro na programação. Nesse exemplo que estamos lidando com um CPF, é importante notar a diferença para uma validação de dados digitados pelo usuário, por exemplo, se ele digita um CPF inválido (que também é uma forma de programação defensiva). Para a validação, o seu programa deve tratar o erro (por exemplo, lançando uma exceção pré-definida ou simplesmente retornando None) de forma consistente com o restante do seu código. Asserções são para se defender de erros de programação e documentar pré-condições ou pós-condições do código, e não para implementar lógica de tratamento de erros.

Outra coisa a ser tomada nota, que pode ser vista como desvantagem de usar asserções para checar pré-condições, é que elas não são propagadas para subclasses do seu código, por exemplo. É muito fácil alguém criar uma classe que estende a sua, e avacalhar a linda defesa que você colocou no __init__. Se você acha interessante esse tipo de recurso, você talvez queira usar ferramentas que implementem programação por contrato, como a pydbc ou a pycontract. 2

Finalizando, se o seu código tiver muitas asserções e você acha que elas podem estar fazendo processamento desnecessário, você pode rodar o interpretador Python com a opção -O que habilita otimizações básicas como pular execução dos comandos assert. Mas… não faça isso! Existe uma lei da natureza que diz que alguns problemas só acontecem quando o cliente usa seu programa, então você provavelmente quer as asserções ajudando lá também. 🙂 3

Notas:

1 Li a metáfora do trânsito a primeira vez no livro O Programador Pragmático, altamente recomendado por sinal!
2 pycontract é a implementação de referência da PEP-0316, uma proposta de embutir tratamento de programação por contrato na linguagem, colocando as pré-condições e pós-condições nas strings de documentação dos métodos. A PEP é antiga e ainda não foi aceita, mas a implementação parece ser estável, e é facilmente instalável no Ubuntu com apt-get install python-contract.
3 Citação quase-literal roubada descaradamente do texto em inglês: Usando Asserções de Forma Efetiva.

Generator Expressions

tl;dr

Generator expressions são mais adequadas do que list comprehensions quando queremos apenas gerar sequências de elementos para iterar sobre, pois geram um elemento de cada vez, ao contrário das list comprehensions que geram uma lista inteira em memória.

Generator Expressions

Frequentemente, você pode economizar memória e processamento em seus programas fazendo:

lista = (x**2 for x in xrange(1, 1000001))

ao invés do costumeiro:

lista = [x**2 for x in xrange(1, 1000001)]

O primeiro trecho de código usa uma generator expression,  que são expressões que retornam um generator (calma aí que já já vou descrever o que é um generator). O segundo trecho de código usa uma list comprehension expression, que retorna uma lista. Para entendermos a diferença entre as duas expressões, primeiro é necessário que saibamos o que é um generator. Se você já sabe, pode pular a próxima seção.

Generators

Não são raras as situações em que precisamos criar uma sequência de elementos, para depois iterar sobre essa sequência realizando alguma operação sobre esses elementos. Por exemplo, queremos gerar uma sequência contendo os n primeiros números primos. Poderíamos criar uma funçãozinha que gera uma lista com esses valores:

def primos(n):
    lista = []
    i = 0
    while len(lista) < n:
        if eh_primo(i):
            lista.append(i)
        i = i + 1
    return lista

E quando precisarmos usar tal lista, simplesmente chamamos a função primos(). Assim, podemos iterar sobre o resultado da chamada a essa função:

for i in primos(1000):
    # faça algo com o i
    print i,

O problema é que a função primos() gera uma lista de n elementos em memória — e isso pode ser um tanto quanto caro, dependendo do valor de n. Como estamos apenas querendo iterar sobre um conjunto de elementos (ou seja, usar cada um de uma vez), poderíamos usar um generator. Para entender o que é um generator, vamos primeiro reescrever a função anterior:

def primos_gen(n):
    i = 1
    count = 0
    while count < n:
        if eh_primo(i):
            count = count + 1
            yield i
        i = i + 1

Repare na palavra-chave yield. Em Python, toda função que possui em seu corpo a instrução yield, retorna um generator quando for chamada. O yield pode ser lido como um pause, que retorna um objeto do tipo generator. Veja:

>>> x = primos_gen(100)
>>> print type(x)
<type 'generator'>

Sendo um objeto do tipo generator, x possui um método next(), que irá retornar um elemento apenas. Veja que a cada chamada ao next(), o generator x retorna o valor seguinte da sequência que está sendo gerada:

>>> x.next()
1
>>> x.next()
2
>>> x.next()
3
>>> x.next()
5
>>> x.next()
7
>>> x.next()
11

E assim sucessivamente… O que deve ser entendido sobre o comando yield é que ele é parecido com o return, pois acaba retornando um elemento para quem chamar a função next(). Porém, a execução da função generator fica “pausada” após o yield, sem perder o contexto atual, como ocorreria no caso de um return. Assim, quando chamarmos novamente o método next() do objeto, a execução irá continuar na linha seguinte ao yield, não reiniciando a execução da função desde o seu início. Ou seja, a função volta a ser executada do ponto onde estava parada. Legal, né?

Isso permite que façamos uma iteração sobre uma sequência de elementos sem ter que gerar toda essa sequência em memória antecipadamente. Assim, geramos elemento por elemento e o retornamos, e, quando quisermos outro elemento, a execução da função geradora será retomada de onde parou na execução anterior. Caso ainda não tenha entendido o yield, observe e teste o seguinte código (emprestado daqui):

def func():
    yield 1
    yield 2
    yield 3
x = func()
print x.next()
print x.next()
print x.next()

Se você testar o código, verá que quando executamos a primeira chamada à função x.next(), teremos como valor de retorno o número 1. Quando executamos a segunda chamada à x.next(), teremos como retorno o valor 2. Por fim, quando executamos a terceira chamada à x.next(), teremos como resposta o inteiro 3.

Mas, ao invés de invocar explicitamente o método next() de x, poderíamos usar um laço for (repare que não estamos chamando o next()). Veja:

def func():
    yield 1
    yield 2
    yield 3

x = func()
for i in x:
    print i,

Execute o código acima e verá que teremos a seguinte saída:

1 2 3

Parabéns, você acaba de aprender a “mágica” que o laço for usa para percorrer sequências de elementos! Na verdade, o for espera que o objeto a ser percorrido retorne para ele um iterator, para que ele possa “conversar” com tal objeto, chamando o método next() a cada iteração para obter o próximo valor.

Enfim, generators são objetos que retornam objetos e que mantém o estado da função geradora entre a geração de um objeto e outro.

Generator Expressions

Como comentei anteriormente, muitas vezes usamos list comprehensions quando na verdade elas não são a melhor alternativa. Quando usamos list comprehensions, a lista inteira é gerada de uma vez e armazenada na memória. Por exemplo, se quisermos gerar uma lista contendo os quadrados de todos os números inteiros de 1 a 1.000.000, podemos fazer o seguinte:

>>> lista = [x**2 for x in xrange(1, 1000001)]

Ao usarmos uma list comprehension, estamos gerando uma lista inteira na memória e armazenando-a em uma variável chamada lista. Tudo bem, às vezes é mesmo necessário manter essa lista na memória para usar seu conteúdo posteriormente, mas muitas vezes fazendo mau uso da ferramenta e usamos esse tipo de expressão quando não precisamos da lista inteira de uma vez só.

Vamos seguir o raciocínio utilizando um exemplo. Estudando o módulo random, desejamos verificar, em um conjunto de 1000000 (um milhão) de números aleatórios gerados através da função random.randint(), onde limitamos os números gerados à faixa de 0 a 1000000, se alguma vez o valor 0 é gerado pela função. Usando uma list comprehension e a função any(), poderíamos verificar se algum dentre os 1000000 números gerados é igual a 0. (Perceba que o resultado vai variar de execução para execução, dependendo dos números gerados.)

>>> any([random.randint(0, 1000000) == 0 for x in xrange(0, 1000000)])
True
>>> any([random.randint(0, 1000000) == 0 for x in xrange(0, 1000000)])
False

O código acima basicamente executa os seguintes passos:

  1. A cada volta do loop é gerado um número aleatório entre 0 e 1000000.
  2. Para cada número gerado, verificamos se ele é igual a 0 e armazenamos o booleano correspondente a tal verificação em uma lista (assim, temos uma lista de 1000000 de booleanos, por exemplo: [False, True, False, False, …, False]).
  3. Após gerada a lista, usamos a função any(), que verifica se algum dos valores armazenados na lista é True.

Nosso código tem um sério problema. Antes de dizer qual é, pergunto a vocês: “Precisamos realmente armazenar na memória uma lista de 1000000 de elementos para fazer a operação acima?”. A resposta é: “Nesse caso, não!”. A função any() recebe um objeto iterável como parâmetro e irá chamar o método next() do iterator retornado por esse objeto a cada iteração necessária. Poderíamos usar uma generator expression nesse caso. Esse tipo de expressão se comporta de forma semelhante a uma função do tipo generator. Ao contrário de quando iteramos sobre uma list comprehension, onde geramos uma lista inteira antes de percorrê-la, com uma generator expression, os elementos são gerados “sob demanda”. Podemos escrever o mesmo exemplo acima utilizando a sintaxe das generator expressions (muito semelhante à list comprehensions, trocando o [] por ()):

>>> any((random.randint(0, 1000000) == 0 for x in xrange(0, 1000000)))
True
>>> any((random.randint(0, 1000000) == 0 for x in xrange(0, 1000000)))
False

A função any() itera sobre o objeto gerado pela generator expression. Temos então a geração de um elemento a cada iteração. Dessa forma, estamos economizando memória, pois os elementos são gerados e, assim que não são mais necessários, podem ser descartados.

As generator expressions estão disponíveis a partir da versão 2.4 do Python.

List comprehensions são do mal?

Claro que não. Em muitos casos, list comprehensions são exatamente o que precisamos. Elas geram listas, que permitem acesso aleatório a elementos dela (ou seja, podemos acessar o n-ésimo elemento dela sem antes passar pelos n-1 elementos anteriores). Também permite que façamos uso do operador de slicing, dentre outras vantagens das listas.

Mais informações

Veja mais na PEP que propôs a criação de generator expressions. Outro material muito bom são os slides da palestra que o Luciano Ramalho fez no FISL/2012 sobre esse assunto.

Apresentando o SQLite com Python

(este é o primeiro post do eljunior, o novo colaborador do pythonhelp)

Então você vem acompanhando há algum tempo o pythonhelp, e já está cheio de idéias para criar seus programas usando Python, sua linguagem preferida. Se você já começou a fazer alguma coisa, provavelmente já se deu conta que existem muitas questões relacionadas a guardar e acessar os dados utilizados pelo seu programa, seja ele uma mega-aplicação para controle de estoque ou um programa para fazer o gerenciamento da sua coleção de tazos (nossa, isso é muito anos 90..!).

Felizmente, muitos desses problemas já foram resolvidos, e você só precisa selecionar a ferramenta certa que irá ajudá-lo a completar sua tarefa com sucesso. Entram em cena os poderosos sistemas de bancos de dados. Sistemas de bancos de dados são para informações o que Python é para código: você pode viver sem eles, mas sua vida é muuuito mais fácil com eles. Usando banco de dados, você se preocupa com O QUE você vai armazenar, e PARA QUE você vai usar essas informações, sem ter que se preocupar demais em COMO você irá guardar tudo no disco, o leiaute dos arquivos, acessá-los eficientemente e ainda deixar tudo isso flexível pra o caso de você ter que alterar alguma coisa depois.

De uma forma bem resumida, trabalhar com banco de dados geralmente envolve o seguinte:

  1. Definir um modelo de dados (schema), isto é, quais tipos de dados você vai armazenar no banco, e como eles se relacionam. Você decide, por exemplo: “Vou armazenar informações sobre álbuns, que devem conter título, ano de lançamento, gênero e artistas. Artistas, por sua vez, conterão nome artístico, nome completo opcional, data provável de nascimento, e data de falecimento opcional.” Você faz isso usando a linguagem SQL (“Structured Query Language” == “Linguagem Estruturada para Pesquisa”), definindo tabelas e colunas, e os tipos de dados relacionados.
  2. Definir operações a serem feitas no banco de dados, a partir do schema definido. Por exemplo: “Em dado instante, a aplicação deverá adicionar informações de um álbum ou artista no banco de dados.” Ou, “quando o usuário clicar nesse botão, a aplicação deverá listar todos os álbuns ordenados por ano de lançamento”. Essas operações são chamadas de “queries” (consultas), e usualmente envolvem alterar os dados no banco ou buscar dados que estão guardados nele.
  3. Repetir os passos acima tantas vezes quanto for necessário — ou seja: eternamente… :).

Pois bem, hoje queremos apresentar-lhe um desses sistemas de banco de dados, o SQLite, que possui algumas características interessantes que o diferenciam de outros mais tradicionais (tipo MySQL, PostgreSQL, SQL Server, etc), é muito fácil de usar em Python e pode muito bem ser útil no seu próximo programa em Python.

O SQLite é o que chamamos de um banco de dados embutido, ou seja, é um banco de dados que cabe inteirinho em sua aplicação, sem precisar de um processo servidor como os sistemas de banco de dados mais tradicionais. Seu uso é muito popular em diversas aplicações para armazenar dados na máquina de um usuário. De fato, se você usa o Firefox, saiba que suas preferências de usuário, o histórico de sua navegação e diversas outras informações, são todas armazenadas em arquivos SQLite. Isso é legal porque fica muito fácil de carregar as informações desses arquivos e cruzá-las conforme for necessário, inclusive a partir de programas externos que “conheçam” SQLite. 1

Como você é inteligente e está usando Python, que vem com baterias inclusas, você já tem a sua disposição o módulo sqlite3, ao alcance de um simples import sqlite3. 2

O código abaixo usa o módulo sqlite3 para ler o arquivo que contém o banco de dados com o histórico de downloads do Firefox, e imprime no console o nome dos arquivos e o tipo MIME:

import os, sqlite3
from glob import glob

user_directory = os.path.expanduser('~')
files = glob(user_directory + '/.mozilla/firefox/*/downloads.sqlite')
if len(files) > 0:
    # abre o banco de dados, 'conecta':
    conn = sqlite3.connect(files[0])
    # executa uma consulta, selecionando o nome e tipo da tabela moz_downloads:
    cursor = conn.execute('SELECT name, mimeType FROM moz_downloads')

    # percorre os resultados da consulta, mostrando-os na tela:
    download = cursor.fetchone()
    while download:
        print download
        download = cursor.fetchone()
else:
    print 'Nao consegui achar nenhum arquivo com historico de downloads :('

É isso aí, a partir de agora você está oficialmente autorizado a explorar o SQLite nos seus próximos programas Python. 🙂

Você também pode querer testar aplicativos como o SQLite Browser, ou a extensão pro Firefox SQLite Manager, que permite você visualizar e modificar qualquer banco de dados SQLite no seu computador.

Veja mais sobre o SQLite em:

Notas:

1 Originalmente, o SQLite é uma biblioteca escrita em C, mas existem bibliotecas disponíveis para muitas outras linguagens. O suporte em Python é diferenciado porque já o inclui na instalação padrão.

2 O módulo sqlite3 está disponível por padrão em qualquer instalação Python, com versão maior ou igual 2.5. Caso você estiver usando um Python mais antigo, você ainda pode instalar o módulo sqlite3 disponível aqui

glob — listando arquivos de diretórios

De vez en quando, é necessário que obtenhamos uma lista com os arquivos presentes em um diretório, para que, por algum motivo possamos abri-los e fazer alguma operação sobre eles. A forma mais simples de fazer isso é usando a função listdir(), do módulo os.

>>> import os
>>> print os.listdir('/')
['home', 'media', 'lib64', 'tmp', 'mnt', 'opt', 'boot', 'sys', 'srv', 'dev', 'selinux', 'proc', 'root', 'lib32', 'etc', 'bin', 'usr', 'vmlinuz', 'lib', 'run', 'sbin', 'var', 'initrd.img']

Mas, poderíamos querer listar somente os arquivos .py, por exemplo, para abrí-los, em sequência. É aí que entra o módulo glob. Ele permite que listemos os arquivos de um diretório, usando expressões semelhantes as que usamos no shell, como por exemplo: *.py.

>>> import glob
>>> print glob.glob('*.py')
['a.py', 'b.py', 'novo.py']

Assim, se eu quisesse abrir e imprimir o conteúdo de todos os arquivos .py que estão em determinado diretório, excluindo as linhas comentadas, poderia fazer o seguinte:

>>> import glob
>>> for file in glob.glob('*.py'):
...     for line in open(file):
...         if not line.strip().startswith('#'):
...             print line

Mais informações sobre o glob em: http://docs.python.org/library/glob.html

Dict comprehensions

Anteriormente, escrevi sobre as list comprehensions, que são um recurso bem interessante da linguagem Python (se você não leu, veja ). Elas permitem que criemos listas usando uma sintaxe muito simplificada. Assim como existem as list comprehensions para listas, existem os dict comprehensions para dicionários. A sintaxe é bem simples. Se quisermos criar um dicionário com base nos valores de uma lista, onde cada elemento da lista será chave para um valor que representa o dobro de si, podemos utilizar a seguinte estrutura:

>>> d = {i:i**2 for i in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]}
>>> print d
{1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36, 7: 49, 8: 64, 9: 81, 10: 100}

Para fazermos um dicionário que utilize como chave o número inteiro que representa o código ASCII de determinado caractere e que tenha tal caractere como respectivo valor, podemos usar o seguinte dict comprehension:

>>> {i : chr(i) for i in range(65, 91)}
{65: 'A', 66: 'B', 67: 'C', 68: 'D', 69: 'E', 70: 'F', 71: 'G', 72: 'H', 73: 'I', 74: 'J', 75: 'K', 76: 'L', 77: 'M', 78: 'N', 79: 'O', 80: 'P', 81: 'Q', 82: 'R', 83: 'S', 84: 'T', 85: 'U', 86: 'V', 87: 'W', 88: 'X', 89: 'Y', 90: 'Z'}

Mais em: http://www.python.org/dev/peps/pep-0274/

Customizando o IPython

Dias atrás estive tentando descobrir como trocar o prompt-padrão do IPython [1]. Para quem não sabe, o prompt-padrão de comandos do IPython é:

In [x]:

Onde x é um contador de entradas fornecidos pelo usuário. O que eu queria era substituir tal prompt pelo prompt padrão do shell Python:

>>>

Para isso, é preciso antes criar um perfil de configurações de usuário para o IPython, com o comando (em um shell Linux):

$ ipython profile create

Assim, será criado um diretório com as configurações do IPython em seu diretório de usuário, onde está armazenado o arquivo de configurações ipython_config.py (no meu caso está em ~/.config/ipython/profile_default/ipython_config.py).

Para substituir o prompt do IPython, procure pela linha com o seguinte conteúdo dentro do arquivo de configurações e descomente-a:

# c.PromptManager.in_template = 'In [\\#]: '

Após descomentar tal linha, substitua o valor ‘In [\\#]: ‘ por ‘>>> ‘, para que tal linha fique como a linha abaixo:

c.PromptManager.in_template = '>>> '

Uma coisa que acho irritante é o IPython sempre pedindo confirmação quando desejo fechá-lo, após ter pressionado ^-d. Para evitar que ele pergunte se você deseja mesmo fechá-lo, basta descomentar a seguinte linha no mesmo arquivo e trocar o valor True para False:

# c.TerminalInteractiveShell.confirm_exit = True

A linha deverá ficar assim:

c.TerminalInteractiveShell.confirm_exit = False

Além dessas, existem muitas coisas que podem ser personalizadas no IPython. Você pode vê-las comentadas no arquivo supracitado. Para personalizar outros aspectos relacionados ao prompt de comandos, vá até a seção “PromptManager configuration” do arquivo e altere de acordo com o seu gosto.

[1] http://ipython.org

Trabalhando com datas e horas em Python – datetime

Módulos utilizados nesse post:

  • date
  • datetime
  • timedelta

Esse post vai mostrar alguns exemplos simples de como utilizar o módulo datetime [1] para manipularmos, em Python, dados que representam datas. Antes de qualquer coisa, vou enumerar algumas operações que são corriqueiras quando precisamos lidar com datas:

  1. Obter a data atual;
  2. Obter a data antes ou após X dias da data atual;
  3. Calcular a diferença de dias entre duas datas;
  4. Descobrir o dia da semana em que determinada data cai;

Agora vamos ver como resolvê-las, usando o módulo datetime.

1. Obter a data atual

O módulo datetime possui dentro de si uma classe date [2] definida. Nessa classe, existem alguns métodos para manipulação de datas, como a função today, que retorna um objeto do tipo datetime.date.

>>> from datetime import date
>>> hj = date.today()
>>> print hj
2012-07-10
>>> print hj.day
10
>>> print hj.month
07
>>> print hj.year
2012

2. Obter a data há ou daqui a X dias

Para isso, iremos converter primeiramente a nossa data em um número ordinal, através do método toordinal(), que nos retorna a quantidade de dias passados desde o dia 1/1/1 até a data recebida como argumento. Depois disso, basta somar (ou subtrair) a esse número inteiro o número de dias da diferença que queremos calcular e então reconverter o inteiro para data, através do método fromordinal(). Abaixo, obtivemos a data a daqui exatos 45 dias.

>>> from datetime import date
>>> hj = date.today()
>>> print hj.toordinal()
734694
>>> futuro = date.fromordinal(hj.toordinal()+45) # hoje + 45 dias
>>> print futuro
2012-08-24

3. Calcular a diferença de dias entre datas

Para realizar essa, vamos obter as duas datas entre as quais queremos saber o intervalo de dias e depois usar o operador de subtração (-) para fazer a operação. O operador subtração, quando aplicado a datas, retorna um objeto do tipo timedelta, contendo a diferença entre as datas. Esse objeto possui um atributo chamado days, que obviamente nos dá o número de dias representados pelo delta.

>>> from datetime import date
>>> hj = date.today()
>>> print hj.toordinal()
734694
>>> futuro = date.fromordinal(hj.toordinal()+45) # hoje + 45 dias</pre>
>>> diferenca = futuro - hj
>>> print diferenca.days
45

4. Descobrir o dia da semana de uma data

Essa é fácil. Após construir uma data, podemos chamar o método weekday() do objeto date. Ele retornará um número inteiro entre 0 (represendo segunda-feira) e 6 (representando domingo).

>>> from datetime import date
>>> hj = date.today()
>>> print hj.weekday()
1
Para que apareça o dia da semana por extenso, em português, podemos usar uma tupla para armazenar os dias da semana, de acordo  com os valores retornados pelo método weekday().
>>> from datetime import date
>>> hj = date.today()
>>> dias = ('Segunda-feira', 'Terça-feira', 'Quarta-feira', 'Quinta-feira', 'Sexta-feira', 'Sábado', 'Domingo')
>>> print "Hoje é", dias[hj.weekday()]
Hoje é, Terça-feira

Os módulos datetime e date fazem muito mais do que o que foi mostrado aqui. Agora, acesse a documentação dos módulos e faça você mesmo alguns testes.

[1] http://docs.python.org/library/datetime.html

[2] http://docs.python.org/library/datetime.html#datetime.date

Um contador de elementos em sequências

Às vezes precisamos realizar a contagem do número de aparições de determinados elementos em uma sequência, como uma string ou uma lista. Por exemplo, quero saber quantas vezes cada letra apareceu em uma string. Como fazer isso em Python? Primeiro, vamos fazer “na mão” e depois vamos conhecer uma solução “pronta”.

def conta_ocorrencias(s):
    ocorrencias = {}
    for c in s:
        if c in ocorrencias:
            ocorrencias[c] = ocorrencias[c] + 1
        else:
            ocorrencias[c] = 1
    return ocorrencias

A solução acima apresentada utiliza um dicionário armazenando como chaves as letras encontradas e, como valor para cada letra, a quantidade de ocorrências desta. Percorremos a string s letra por letra e, se a letra atual já estiver sendo usada como chave no dicionário ocorrencias, o valor correspondente a tal letra é incrementado em um (ou seja, encontramos mais uma ocorrência da letra na string recebida). Se a letra ainda não estiver aparecendo como chave do dicionário, então é criada uma entrada neste com a letra sendo usada como chave e o valor associado 1 (ocorrencias[c] = 1). Vamos analisar o teste feito dentro do for:

...
        if c in ocorrencias:
            ocorrencias[c] = ocorrencias[c] + 1
        else:
            ocorrencias[c] = 1
...

Esse teste é necessário porque se tentarmos acessar uma chave inexistente de um dicionário, é retornado um KeyError. Assim, precisamos testar para verificar se a letra atual já foi anteriormente inserida no dicionário ou não. Se foi, daí sim podemos incrementar o valor associado. Se não foi, daí temos que incluir tal valor com o número 1 associado.

Para simplificar isso, podemos usar o módulo collections [1]. Esse módulo nos fornece um tipo especial de dicionário chamado de defaultdict [2]. Esse dicionário permite que especifiquemos, ao construir um dicionário, uma função que será chamada para retornar um valor padrão para quando a chave solicitada não existir no dicionário. Com ele, é possível fazer o seguinte:

    ocorrencias = collections.defaultdict(int)
    ...
        ocorrencias[c] = ocorrencias[c] + 1
    ...
A função int(), quando chamada sem argumentos, retorna 0. Como passamos ela ao construtor do dicionário ocorrências, é ela que será chamada quando houver um acesso a uma chave inexistente. Isso possibilita que usemos esse valor para fazer a soma no código acima. Na primeira vez que é executado para determinado caractere, o código acima será executado como se fosse:
    ocorrencias[c] = 0 + 1
Nas vezes seguintes, ao invés de 0 (valor obtido ao tentarmos acessar o valor de uma chave inexistente), teremos como valor o número atual de ocorrências para o caractere contido na variável c. O código completo segue abaixo:
import collections
def conta_ocorrencias(s):
    ocorrencias = collections.defaultdict(int)
    for c in s:
        ocorrencias[c] = ocorrencias[c] + 1
    return ocorrencias

E a solução “pronta”?

O jeito mais simples de fazer a contagem de ocorrências dos elementos de uma sequência é através da classe Counter [3], também presente no módulo collections. Vamos ver um exemplo de utilização:

>>> import collections
>>> s = "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut"
>>> c = collections.Counter(s)
>>> print c.most_common(5)
[(' ', 13), ('i', 11), ('e', 8), ('t', 8), ('d', 7)]

Um dos métodos mais interessantes é o most_common() [4], que retorna uma lista contendo os elementos mais comuns na sequência. Se passarmos um número n como argumento, ele irá retornar os n valores mais comuns na sequência, como no exemplo acima.

Além do Counter e do defaultdict, o módulo collections fornece várias outras estruturas úteis, que veremos em posts futuros.

[1] http://docs.python.org/library/collections.html

[2] http://docs.python.org/library/collections.html#collections.defaultdict

[3] http://docs.python.org/library/collections.html#collections.Counter

[4] http://docs.python.org/library/collections.html#collections.Counter.most_common

pydoc – Documentação de módulos Python

Em mais um post sobre documentação, vamos conhecer hoje o pydoc [1]. O pydoc é um módulo que gera documentação (em formato amigável) sobre módulos Python, a partir dos docstrings que estão presentes nestes. A documentação pode ser apresentada em mais de uma forma. Uma delas, é o formato manpage [2]. Para ver a documentação de um módulo em formato manpage, podemos invocar o pydoc através de um shell do sistema operacional:

$ pydoc timeit

A documentação pode ser vista na tela abaixo:

Uma vantagem óbvia disso é não precisar abrir um shell python para apenas tirar uma dúvida sobre algum método. Além da visualização manpage-style, o pydoc também gera documentação em formato HTML.

$ pydoc -w timeit
wrote timeit.html

O comando acima gera um arquivo com a extensão html com o nome do módulo em questão, para ser visualizado em um browser.

Além de mostrar a documentação em formato manpage e de gerar arquivos html com a documentação, também é possível “levantar” um servidor web que irá servir as páginas html com a documentação dos módulos disponíveis no sistema. Para isso, basta chamar o pydoc com a opção -p, passando o número da porta onde o servidor irá escutar como argumento.
$ pydoc -p 8080
pydoc server ready at http://localhost:8080/

Acessando no browser o endereço localhost:8080, é possível navegar nas páginas de documentação dos módulos.

Por fim, existe outra opção que abre uma interface gráfica onde o usuário poderá procurar pelo módulo sobre o qual deseja ver a documentação e abrir o html correspondente no browser.

Na figura acima, ao dar duplo-clique sobre um dos itens retornados pela busca, é aberto no browser o documento html correspondente ao módulo escolhido.

É claro que, além dessas formas de visualizar a documentação dos módulos, também podemos usar o tradicional builtin help() para ver a documentação dentro do shell Python.

[1] http://docs.python.org/library/pydoc.html

[2] https://en.wikipedia.org/wiki/Man_page