Testando código – doctest

Em um post antigo, vimos o que são docstrings e para que servem. No post de hoje, veremos como utilizá-las para especificar testes sobre nossas funções.

Considere que estamos escrevendo um programa, e precisamos escrever uma função que calcule o dobro de um determinado número. Então, escrevemos o código:

def dobro(x):
    result = x * 2
    return result

Agora, para saber se está funcionando corretamente, vamos testar chamando a função dobro, passando como argumento alguns valores para os quais nós conhecemos o valor de retorno que a função deveria retornar:

>>> dobro(1)
2
>>> dobro(2)
4
>>> dobro(3)
6
>>> dobro(4)
8
>>> dobro(5)
10

Verificando visualmente os resultados, nos parece que a função está funcionando corretamente (e está, de fato). Daí, vem um colega e diz que não é necessário criarmos uma variável result, podendo retornar diretamente o retorno da expressão x * 2. Daí vamos editar nosso código para que se pareça com o código abaixo:

def dobro(x):
    return x * 2

Agora, teremos que rodar novamente os 5 testes acima e verificar, linha por linha, se o resultado está correto. Isso é chato, e também, em casos mais complexos, muito propenso a erros. Para não precisar rodar manualmente os testes e verificar visualmente se o resultado está correto, vamos utilizar o módulo doctest [1]. Com esse módulo, basta que colemos os testes (realizados no terminal interativo) dentro da docstring da função correspondente. Por exemplo, para aplicar doctests na nossa função dobro, vamos modificar o código para que se pareça com o código abaixo:

def dobro(x):
	"""
	Funcao que retorna o dobro do valor passado como argumento.
	>>> dobro(1)
	2
	>>> dobro(2)
	4
	>>> dobro(3)
	6
	>>> dobro(4)
	8
    >>> dobro(5)
    10
	"""
	return x * 2

Agora, vou executar os testes, chamando o módulo doctest pela linha de comando de um shell Linux (poderia ser por um terminal Windows/Mac):

user@host:~/ $ python -m doctest -v t.py
Trying:
 dobro(1)
Expecting:
 2
ok
Trying:
 dobro(2)
Expecting:
 4
ok
Trying:
 dobro(3)
Expecting:
 6
ok
Trying:
 dobro(4)
Expecting:
 8
ok
Trying:
 dobro(5)
Expecting:
 10
ok
1 items had no tests:
 t
1 items passed all tests:
 5 tests in t.dobro
5 tests in 2 items.
5 passed and 0 failed.
Test passed.

Ao chamar o módulo doctest pela linha de comando, passando como entrada o arquivo que contém a função dobro() (t.py, no caso), esse módulo vai varrer o código-fonte atual em busca de funções que contenham docstrings cujo conteúdo seja similar ao que aparece em uma tela de terminal interativo do Python. Ao se deparar com tal conteúdo (como por exemplo: >>> dobro(1)), o doctest vai executar tal código e ver se o resultado é igual ao que aparece na linha seguinte na docstring. Assim, se tivermos feito alguma alteração que quebrou o funcionamento da função, ao rodar o doctest, esse erro será acusado. Vamos ver isso na prática. Vou alterar a expressão de retorno da função dobro para x * 3:

def dobro(x):
    """
    Funcao que retorna o dobro do valor passado como argumento.
    >>> dobro(1)
    2
    >>> dobro(2)
    4
    >>> dobro(3)
    6
    >>> dobro(4)
    8
    >>> dobro(5)
    10
    """
    return x * 3

Agora, vou rodar o código acima e vamos ver o resultado.


user@host:~/ $ python -m doctest -v t.py
 Trying:
 dobro(1)
 Expecting:
 2
 **********************************************************************
 File "t.py", line 4, in t.dobro
 Failed example:
 dobro(1)
 Expected:
 2
 Got:
 3
 Trying:
 dobro(2)
 Expecting:
 4
 **********************************************************************
 File "t.py", line 6, in t.dobro
 Failed example:
 dobro(2)
 Expected:
 4
 Got:
 6
 Trying:
 dobro(3)
 Expecting:
 6
 **********************************************************************
 File "t.py", line 8, in t.dobro
 Failed example:
 dobro(3)
 Expected:
 6
 Got:
 9
 Trying:
 dobro(4)
 Expecting:
 8
 **********************************************************************
 File "t.py", line 10, in t.dobro
 Failed example:
 dobro(4)
 Expected:
 8
 Got:
 12
 Trying:
 dobro(5)
 Expecting:
 10
 **********************************************************************
 File "t.py", line 12, in t.dobro
 Failed example:
 dobro(5)
 Expected:
 10
 Got:
 15
 1 items had no tests:
 t
 **********************************************************************
 1 items had failures:
 5 of 5 in t.dobro
 5 tests in 2 items.
 0 passed and 5 failed.
 ***Test Failed*** 5 failures.
 

Ao final, podemos ver um mini-relatório da execução.

Assim, sempre que for preciso alterar minha função dobro(), não precisarei rodar e conferir os resultados manual e visualmente. Basta manter a doctring dentro da função e executar o o módulo doctest passando como entrada o arquivo que contém minha função, que as verificações serão realizadas por este.

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

O módulo getpass

Embora não seja um grande problema para quem desenvolve aplicativos web ou que utilizem algum outro tipo de GUI (graphical user interface), quando escrevemos um aplicativo que irá operar somente em modo texto, ficamos na dúvida de como fazer para ler do teclado um campo como a senha do usuário. Se utilizarmos uma função como raw_input(), cada caractere que o usuário digitar irá aparecer, deixando assim a senha visível no terminal. Por exemplo:

username = raw_input('username: ')
password = raw_input('password: ')
print 'OK.'

Ao executar um programa que tenha as linhas de código acima para ler nome do usuário e senha do teclado, irá acontecer o seguinte:

username: teste
password: minhasenha123
OK.

Como podemos ver, fica muito ruim, afinal a senha do usuário fica exposta. A solução para que o usuário possa digitar sua senha sem que ela apareça (seja ecoada) na tela pode ser encontrada no módulo getpass [1]. Esse módulo fornece duas funções:

  • getpass(): apresenta ao usuário um prompt solicitando a sua senha e não ecoa na tela os caracteres digitados por ele.
  • getuser(): retorna o username do usuário atual no sistema (para Linux e Windows).

O uso de ambas as funções é bem simples. A função getpass() pode receber um argumento que é um texto que será apresentado ao usuário como prompt. Teste o seguinte código:

import getpass
username = getpass.getuser()
password = getpass.getpass('Digite sua senha: ')

Como você verá, será apresentado ao usuário a mensagem “Digite sua senha: “, com o cursor ao lado esperando pela entrada do usuário. Ao digitar a senha, o cursor permanece parado, não dando indicativo visual algum sobre a senha digitada.

É isso. Se precisar escrever um programa que leia do teclado a senha do usuário e não quer que ela seja ecoada, use o módulo getpass.

 

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

Módulo timeit

Como já escrevi em um post anterior, podemos utilizar o módulo timeit (Time it!) para medir o tempo de execução de nossos programas em Python. Porém, às vezes queremos apenas verificar qual trecho de código obtém menor tempo de execução, para escolher qual abordagem seguir. Por exemplo, quero saber o que acarreta em um menor tempo de execução: fazer um for com a função range() ou com a função xrange()?

Nada melhor do que testar na prática para descobrir qual nos dá o menor tempo de resposta. Para isso, podemos usar a interface de linha de comando que o módulo timeit fornece, quando executado como módulo.

python -m timeit

Assim, podemos testar:

python -m timeit "for i in range(1, 10000): pass"
1000 loops, best of 3: 303 usec per loop
python -m timeit "for i in xrange(1, 10000): pass"
1000 loops, best of 3: 206 usec per loop

Como podemos ver, o uso da função xrange() nos deu um tempo de execução de 97 milisegundos a menos do que utilizando a função range(), considerando o melhor dos casos para ambos. No exemplo acima, o timeit executou 3 baterias de testes compostas por 1000 execuções do código passado como argumento.

Em muitos casos iremos precisar testar um código que possui mais de uma linha. Isso pode ser feito passando cada uma das linhas como um argumento separado para o programa. Por exemplo:

x = 0
for i in range(0, 1000):
    x = i * 2

O código acima poderia ser testado pela linha de comando da seguinte forma:

python -m timeit "x=0" "for i in range(1, 10000):" "    x = i * 2"

Ou seja, cada linha é uma string separada e a indentação é feita linha por linha.

Por que python -m?

Se você está se perguntando o que significa python -m timeit, eis a resposta: ao chamar o interpretador python com a opção -m seguida pelo nome de um módulo existente no ambiente, o interpretador irá buscar o arquivo .py que representa tal módulo e executá-lo como se fosse um programa. O módulo timeit, em meu sistema, fica localizado em /usr/lib/python2.7/timeit.py (como eu sei isso? leia aqui). O que é feito quando executamos python -m timeit é o mesmo que:

python /usr/lib/python2.7/timeit.py "for i in xrange(1, 10000): pass"

Vá em frente e teste.

 

O timeit pode ser usado também dentro de programs Python. Veja mais exemplos: http://docs.python.org/library/timeit.html#examples

Problemas importando módulos em Python

Sendo professor de disciplinas de introdução à programação com Python, percebi que muitos alunos cometem um erro muito comum e bastante difícil de ser descoberto quando estamos começando.

De vez em quando, apresento módulos Python aos alunos e peço que eles façam um pequeno exemplo utilizando aquele módulo. Ao dar nome ao arquivo-fonte que está digitando, o aluno acaba nomeando o seu arquivo com o mesmo nome do módulo a ser usado.

Por exemplo, peço aos alunos para escrever um programinha simples para conhecer melhor o módulo math. Então, o aluno cria um arquivo chamado math.py, onde digita seu código. Entre as linhas de código inseridas no arquivo, estão:

import math
print math.pi

Ao tentar executar o programa, o usuário receberá uma mensagem dizendo que não existe um atributo pi no módulo math. Daí, o aluno vai lá e pesquisa na documentação do módulo math e vê que o atributo pi de fato existe naquele módulo. O que há de errado?

Simples. Quando executa o programa recém escrito (math.py), a primeira linha faz import de um módulo chamado math(.py). Como o diretório atual faz parte dos caminhos onde o interpretador python busca os módulos que o programador importa, o interpretador acaba, inadvertidamente, importando o arquivo que o usuário criou, ao invés de importar o módulo math(.py) original. Para resolver esse problema, renomeie seu arquivo.

🙂

Quer descobrir em quais diretórios Python busca os módulos importados pelos programas? Execute o código abaixo:

import sys
print sys.path

Onde está localizado um módulo Python no disco?

Às vezes, quando importamos um módulo em Python, surge a curiosidade para ler o código-fonte desse módulo, para descobrir o que aquele código faz realmente. Existe uma forma bem simples de descobrir a localização de um módulo no disco. Após importar um módulo, basta verificar o atributo __file__ do módulo.

>>> import os
>>> print os.__file__
/usr/lib/python2.7/os.pyc

Esse arquivo, cujo caminho é impresso na tela é, na realidade, o arquivo binário contendo o bytecode compilado. Porém, se retirar o c do final do nome do arquivo, teremos o arquivo-fonte (.py). No exemplo acima, podemos ver o código-fonte do módulo em:

/usr/lib/python2.7/os.py

Outro exemplo, se quisermos ver o código-fonte do módulo timeit, fazemos o seguinte:

>>> import timeit
>>> print timeit.__file__
/usr/lib/python2.7/timeit.pyc

O arquivo-fonte então está em:

/usr/lib/python2.7/timeit.py

Também poderíamos procurar pelo módulo dentro da lista sys.path, que contém os caminhos onde o interpretador busca pelos módulos a serem importados.

maketrans – Tabela de tradução de Strings

Já ouviu falar do alfabeto Leet (ou l337)? É um alfabeto empregado principalmente na internet e usado para comunicação entre pessoas, onde algumas letras do alfabeto latino são substituídas por símbolos graficamente parecidos. Utilizando alfabeto leet, a palavra STREET ficaria 57r337, por exemplo. Ou seja, o número 5 substitui a letra S, o número 7 substitui a letra T, 3 substitui a letra E.

Vamos criar uma ferramenta que traduza texto escrito usando o alfabeto tradicional para texto usando alfabeto leet. Uma forma tosca de fazermos isso é substituir todas as ocorrências de determinada letra do alfabeto tradicional por seu correspondente leet. Exemplo:

s = raw_input('Digite uma frase:')
s = s.replace('a', '4')
s = s.replace('t', '7')
s = s.replace('e', '3')
s = s.replace('s', '5')
...
print s

Porém, existe outra forma mais simples e mais elegante, usando uma Tabela de Tradução. Considere a tabela abaixo. Nela temos duas colunas, uma chamada de Entrada e outra chamada de Saída. Usando a tabela de tradução, cada letra da coluna Entrada encontrada no texto será substituída pela sua correspondente na coluna Saída.

Entrada Saída
A 4
B 8
T 7
E 3
S 5
I 1
O 0
Z 2

No módulo string, fornecido juntamente com a biblioteca padrão do Python, temos um método chamado maketrans, que, dadas duas entradas, cria uma tabela de tradução. Por exemplo, para criar a tabela de tradução apresentada acima, utilizamos o código abaixo:

from string import maketrans
entrada = 'ABTESIOZ'
saida   = '48735102'
tabela = maketrans(entrada, saida)
s = raw_input('Digite uma frase para ser convertida para leet:')
print s.translate(tabela)

As variáveis entrada e saida são strings que irão representar as colunas da tabela de tradução. Para cada ocorrência de um caractere da string entrada em uma string que será traduzida, tal caractere será substituído pelo elemento correspondente na variável saida. Por exemplo, todo caractere ‘A’ em uma string a ser traduzida será substituído pelo caractere ‘4’, e assim por diante.

Usando a mesma idéia, podemos escrever um programinha que cifre uma mensagem usando a Cifra de Caesar. Nesse tipo de cifra, cada letra de uma frase é substituída por outra letra, de acordo com um deslocamento do alfabeto tradicional. Considere como exemplo os dois alfabetos abaixo:

a b c d e f g h i j k l m n o p q r s t u v w x y z
c d e f g h i j k l m n o p q r s t u v w x y z a b

O alfabeto da segunda linha possui um deslocamento de 2 em seus caracteres. Agora, podemos cifrar textos de acordo com tais alfabetos. Por exemplo:

hello world  ---->  jgnnq yqtnf

Cada caractere da string original é substituído pelo correspondente da tabela de tradução. ‘h’ é substituído por ‘j’, e assim por diante. Quem vê a mensagem  jgnnq yqtnf  não consegue descobrir qual o significado desta, a não ser que conheça ou descubra o algoritmo utilizado para cifrá-la. É claro que, nesse caso, é muito simples descobrir. Outro exemplo de cifragem usando os mesmos dois alfabetos:

estou saindo ---->  guvqw uckpfq

Como implementar uma Cifra de Caesar simples, com deslocamento 2, em Python?

from string import maketrans

entrada = 'abcdefghijklmnopqrstuvwxyz'
saida   = 'cdefghijklmnopqrstuvwxyzbc'

def cifra_de_caesar(texto):
    tabela = maketrans(entrada, saida)
    return texto.translate(tabela)

print cifra('hello world')

Como vimos nos exemplos anteriores, o método maketrans, combinado com o translate, nos facilita muito a vida na hora de fazer a cifragem/decifragem de uma mensagem. Poderíamos fazer o mesmo com o método replace da string, mas o código ficaria muito maior e difícil de manter.

Maiores informações sobre maketrans: http://docs.python.org/library/string.html#string-functions

Leitura de arquivos de configuração .ini em Python

Embora não seja a melhor forma possível de descrevermos a configuração de algo, os arquivos .INI ainda existem em grande quantidade. Quem nunca viu ou precisou que seu programa lesse um arquivo parecido com o seguinte?

[section1]
config1=100
config2=0
[section2]
confign=-1

Por mais que queiramos evitar tais arquivos, por serem até considerados um formado obsoleto, às vezes é necessário ler um arquivo desses para obter informações para nosso programa. Como fazer isso em Python? Ler o arquivo linha por linha? Não! Vamos usar o módulo ConfigParser.

Primeiramente, devemos importar o módulo, instanciar um objeto ConfigParser e realizar a leitura do arquivo de configuração desejado (no nosso caso, config.ini):

import ConfigParser
cfg = ConfigParser.ConfigParser()
cfg.read('config.ini')

Feito isso, o arquivo de configuração já está lido, agora basta que obtenhamos os valores que queremos extrair do arquivo. Para obter um valor do arquivo, é preciso especificar a seção e a propriedade que queremos obter. Por exemplo, o código abaixo obtém o valor da propriedade confign da seção section2 e o armazena na variável x:

x = cfg.getint('section2', 'confign')

Veja que utilizamos um método chamado getint( ) para fazer a leitura de um valor inteiro do arquivo .INI. Caso os dados a serem lidos fossem de outro tipo, poderíamos usar um dos seguintes métodos: getboolean( ), getfloat( ), ou simplesmente get( ) no caso de strings.

Com o mesmo módulo, também é possível realizarmos a escrita de arquivos .ini. Veja mais em: http://docs.python.org/library/configparser.html

Tratando argumentos com argparse

No post anterior, vimos o que são e como tratar manualmente os argumentos vindos para o programa pela linha de comando do usuário. Como vimos, é trabalhoso e chato tratar todas as possíveis opções de nosso programa. Para facilitar nossa vida, existem várias bibliotecas para tratamento dessas opções. Em Python, temos algumas opções, e no post de hoje iremos ver um pouco do módulo argparse. Basta informarmos a ele quais são os argumentos esperados para nosso programa, que o trabalho de realizar a verificação e atribuição de entradas à variáveis internas é feito pelo código do argparse. Além disso, o argparse gera automaticamente o texto de ajuda e o apresenta quando o usuário passa argumentos incorretos.

Para exemplificar, vamos escrever um programa simples que recebe como argumentos dois valores, identificados por opções, da seguinte forma:

$ prog.py --frase "ola mundo" -n 10

O programa então imprime na tela a frase recebida como entrada n vezes.

Em primero lugar, importamos o módulo argparse e instanciamos um objeto ArgumentParser, que será o responsável por fazer a análise dos argumentos fornecidos pela linha de comando.

1: import argparse
2:  parser = argparse.ArgumentParser(description = 'Um programa de exemplo.')

Após isso, devemos configurar nosso parser, informando a ele quais são os argumentos esperados pelo nosso programa.

3:  parser.add_argument('--frase', action = 'store', dest = 'frase',
                           default = 'Hello, world!', required = False,
                           help = 'A frase que deseja imprimir n vezes.')
4:  parser.add_argument('-n', action = 'store', dest = 'n', required = True,
                           help = 'O número de vezes que a frase será impressa.')

A linha 3 cria o argumento –frase (cujo valor será passado como argumento seguinte), sendo que a ação a ser tomada pelo argparse ao encontrar tal argumento será armazenar (action = ‘store’) o valor fornecido em uma variável interna do programa referenciada pelo nome frase (dest = ‘frase’). Tal argumento é opcional (required = False) e, caso o usuário não forneça ele, o valor padrão será ‘Hello, world!’. Caso o usuário solicite ajuda do programa (através do atributo padrão -h), será mostrado um texto que inclui a informação sobre o argumento –frase. O mesmo foi feito para o argumento -n, com a diferença de que este é um argumento obrigatório.

Após configurá-lo, devemos solicitar ao nosso parser para que faça a verificação dos argumentos.

5:  arguments = parser.parse_args()

A linha 5 mostra a chamada ao parser para que este faça a verificação dos argumentos. Como resultado, ele nos devolve um objeto (do tipo Namespace), que irá conter variáveis internas com os valores fornecidos pelo usuário pela linha de comando para os argumentos. Vejamos a sequência do programa:

6: for i in range(0, int(arguments.n)):
7:      print arguments.frase

Como podemos ver, os valores passados pelo usuário para frase e para n são acessados como atributos do objeto arguments. Pronto, temos um programinha bem simples que recebe argumentos pela linha de comando sem a necessidade de realizar o tratamento manual dos argumentos. Vamos ver a execução desse programa, que chamei de exemplo.py:

$ exemplo.py
usage: exemplo.py [-h] [--frase FRASE] -n N
exemplo.py: error: argument -n is required
$ exemplo.py --frase "olá"
usage: exemplo.py [-h] [--frase FRASE] -n N
exemplo.py: error: argument -n is required
$ exemplo.py --frase "olá" -n 3
olá
olá
olá

E, para finalizar, vamos ver o comportamento do programa quando o usuário fornece o argumento -h. Por padrão, o argparse monta um texto de ajuda, com base nas informações fornecidas na construção do parser.

 
$ exemplo.py -h
usage: exemplo.py [-h] [--frase FRASE] -n N

Um programa de exemplo.

optional arguments:
  -h, --help     show this help message and exit
  --frase FRASE  A frase que deseja imprimir n vezes.
  -n N           O número de vezes que a frase será impressa.

Além do argparse, o getopts também é um módulo muito utilizado para tratamento de opções de linha de comando. É isso, com módulos como esse, nossa vida fica muito mais fácil. 🙂

Obtendo a data e a hora atuais em Python

Para obtermos os valores de data e hora atuais, podemos utilizar o módulo datetime, que fornece formas bem simples para fazermos isso. Basta utilizar o método now() existente na classe:

from datetime import datetime
now = datetime.now()
print now.year
print now.month
print now.day
print now.hour
print now.minute
print now.second


Corrigido, de acordo com o comentário do eljunior.

Calculando o hash de strings em Python

Usamos o termo hashing para nos referirmos a uma técnica muito utilizada em programação, quando queremos garantir determinadas propriedades sobre os dados que estamos manipulando e transmitindo. Obter o hash de um conjunto de dados significa obter uma string de tamanho fixo que é calculada com base no conteúdo do conjunto de dados.

Por exemplo, o hash (calculado usando o algoritmo md5) da palavra “teste” é mostrado abaixo:

"teste" --> "1ceae7af21732ab80f454144a414f2fa"

Uma mínima modificação na string “teste” irá gerar um hash totalmente diferente:

"testa" --> "9dd18b1a48164eaec9979df1a6aa84aa"

O hashing é muito utilizado para verificar a integridade de um arquivo durante a transmissão deste pela rede. Ao disponibilizar um arquivo para download, uma empresa pode disponibilizar também o hash calculado sobre o arquivo. Assim, para ter certeza de que efetuou o download do arquivo correto (sem este ter sido corrompido), o usuário pode calcular o hash do arquivo recebido e comparar o hash calculado com o hash disponibilizado pela empresa. Se houverem diferenças entre os hashes, isso significa que o arquivo está corrompido.

hashlib

Python oferece um módulo chamado hashlib que fornece funções para cálculo de hash de dados. Como usá-lo?

import hashlib
h = hashlib.md5()
h.update("uma frase qualquer")
print h.hexdigest()

O código acima utiliza a função md5 para obter o hash da string “uma frase qualquer” e imprimí-lo utilizando o valor do hash obtido em hexadecimal. Além do md5, a hashlib implementa os seguintes algoritmos para cálculo de hash:

  • md5
  • sha1
  • sha224
  • sha256
  • sha384
  • sha512

Assim, para utilizar a função sha256, basta fazer:

import hashlib
h = hashlib.sha256()
h.update("uma frase qualquer")
print h.hexdigest()

Maiores informações podem ser obtidas na documentação oficial: http://docs.python.org/library/hashlib.html