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. 🙂

Argumentos da linha de comando

Como é comum em programas de linha de comando em sistemas baseados em UNIX, é muito útil que nossos programas possuam uma “interface” de linha de comando, de forma que o usuário possa fornecer entradas ao programa diretamente pela linha de comando. Tais entradas são comumente chamadas de argumentos e são muito mais práticas para o usuário do que encher o programa de input() e raw_input(). Vejamos um exemplo de passagem de argumentos em um famoso programa para Linux:

$ ifconfig eth0 address 192.168.0.10

No exemplo acima, ifconfig é o nome do programa, e o restante (eth0 address 192.168.0.10) são os argumentos para esse programa. Por que isso é mais prático do que o seguinte?

$ ifconfig
Digite a interface de rede que deseja configurar: eth0
Digite o endereço para a interface de rede: 192.168.0.10
$

Simples. O primeiro exemplo permite que seja criado um script com entradas dinâmicas, baseadas no conteúdo de variáveis, permite fácil repetição da execução dos comandos (juntamente com as entradas) através das funções de histórico do shell, sem falar que possibilita comunicação entre programas de forma facilitada. OK, mas como fazemos para obter as entradas do usuário vindas da linha de comando em Python?

Existe mais de uma resposta para essa pergunta. Vamos à mais comum:

import sys
print sys.argv[0]
print sys.argv[1]
print sys.argv[2]

O exemplo acima mostra o uso do atributo argv do módulo sys. Através desse atributo, podemos acessar as entradas passadas pelo usuário. Considere que o código acima é o conteúdo de um arquivo chamado args.py. Se esse arquivo for executado da seguinte forma:

$ python args.py Hello world

Produzirá a seguinte saída:

args.py
Hello
world

Isso mesmo, como podemos ver, o primeiro argumento para o programa é o próprio nome do programa, e é acessado através de sys.argv[0]. A variável argv nada mais é do que uma lista. Sendo assim, seus elementos podem ser acessados via índice. E o que acontece se chamarmos o programa acima sem argumentos?

$ python args.py
args.py
Traceback (most recent call last):
  File "args.py", line 5, in <module>
    print sys.argv[1]
IndexError: list index out of range

Temos um erro de execução, pois nosso código tenta acessar elementos inexistentes na lista argv. O que fazer? Antes de acessar um elemento, devemos checar se a lista o contém.

import sys
if len(sys.argv) >= 3:
    print sys.argv[0]
 print sys.argv[1]
 print sys.argv[2]

Mas e aqueles programas que possuem diversas opções pela linha de comando, sendo algumas obrigatórias, outras opcionais, como fazem? Tratam opção por opção?

Até podem fazer isso de forma manual, mas existem ferramentas para manipulação de argumentos vindos da linha de comando que facilitam bastante o trabalho. Uma delas será o tema do próximo post: a argparse.

Até breve.

Fatiamento (slicing) de Strings em Python

Antes de falarmos de slicing, vamos ver rapidamente o que são as strings em Python. Strings em Python são objetos como outros quaisquer. Podem ser construídos com uma atribuição simples:

>>> s = "hello, world!"

Tendo feito isso, o objeto s possui disponíveis vários métodos:

>>> print s.upper()
HELLO, WORLD!
>>> print s.split(",")
['hello', ' world!']
>>> print s.split(",")[0]
'hello'
>>> print s.replace("world", "dude")
hello, dude!

Acima, são mostrados apenas alguns dos métodos disponíveis para as strings, nativamente em Python. Assim como em outras linguagens, elementos individuais de uma string podem ser acessados via índice:

>>> print s[0]
h
>>> print s[2]
l
>>> print s[12]
!

Para acessar o último elemento de uma string, podemos proceder de duas formas:

>>> print s[len(s)]
!
>>> print s[-1]
!

Uma operação muito interessante que Python fornece para manipulação de strings é o fatiamento (slicing). Fatiamento significa extrair apenas uma parte da string, ou seja, uma substring. Com essa operação, podemos delimitar os limites inferior e superior do pedaço da string que queremos acessar. Por exemplo, se quisermos acessar a substring da posição 0 até a posição 4 na string s original, podemos fazer o seguinte:

>>> print s[0:5]
'hello'
>>> print s[:5]
'hello'
>>> print s[2:4]
ll
>>> print s[7:13]
'world!'
>>> print s[7:]
'world!'
>>> print s[:]
'hello, world!'

Repare que o elemento que reside na posição do limite superior não é retornado juntamente com a string. Veja a primeira linha do código acima, s[0:5] retorna os elementos que residem entre as posições 0 e 5, incluindo a primeira e excluindo a segunda. Como mostrado acima, também podemos omitir um dos limites, ou ambos, quando queremos algo do início até posição x, ou da posição x até o fim.

OK, mas pra que serve isso? Vamos a um exemplo bem simples: extrair o protocolo de uma URL (formato: protocolo://servidor:porta/caminho/para/recurso).

url = "http://localhost:8000/arquivo.iso"
protocolo = url[0:url.index(':')]

Esse é apenas um simples exemplo do poder que essa operação tem. Cabe ressaltar aqui, que o fatiamento cria uma nova string contendo o conteúdo solicitado na operação. Ou seja, a cada operação de fatiamento, uma nova string é criada.

É isso. Faça bom proveito desse recurso que torna o código muito mais limpo e de fácil entendimento.

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.

Ferramenta web para visualização de execução

Muitas vezes procurei por uma ferramenta legal para que os alunos pudessem visualizar a execução passo a passo de um programa escrito em Python. Nunca encontrava nada, a não ser os tradicionais depuradores. Hoje me deparei com a seguinte ferramenta: http://people.csail.mit.edu/pgbovine/python, desenvolvida no MIT, que cria uma representação visual do programa em execução, mostrando de forma didática as variáveis alocadas, bem como a execução do programa.

 

Sugiro a todos que estão iniciando seus estudos em programação que dêem uma olhada nesse projeto.

http://people.csail.mit.edu/pgbovine/python

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

range() vs xrange()

(válido somente para Python 2.x)

A função range()

Em Python, é muito comum usarmos a seguinte estrutura para realizar uma repetição baseada em um contador:

for i in range(0, 10):
    print i,

A função range(x, y) gera uma lista de números inteiros de x até y (sem incluir o segundo). Assim, range(0, 10), gera a seguinte lista:

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Desse modo, a variável i é iterada sobre essa lista, através da estrutura de repetição for. O trecho de código:

for i in range(0, 10):
    print i,

pode ser lido como:

"Para cada elemento na lista [0,1,2,3,4,5,6,7,8,9], imprima tal elemento"

Usar range() ou xrange()? Eis a questão…

É comum ouvirmos ou lermos algum desenvolvedor Python aconselhando a utilização da função xrange() ao invés da função range(), por questões de desempenho. Mas o que é essa tal de xrange()?

A xrange() nada mais é do que uma função que pode, em muitos casos (não sempre), substituir o uso da função range(), fornecendo ainda melhor desempenho. Veja o código abaixo:

for i in xrange(0, 10):
    print i,

O resultado dessa execução é o mesmo de quando utilizamos a função range(), porém, por gerar um elemento de cada vez, o código que utiliza a função xrange() apresenta um desempenho superior.

Quando executamos:

for i in range(0, 1000000000):
    pass

A função range() irá imediatamente gerar uma lista contendo um bilhão de inteiros e alocar essa lista na memória. Uma lista contendo um bilhão de inteiros é capaz de encher a memória de um computador pessoal.

Já com a função xrange(), ao executarmos:

for i in xrange(0, 1000000000):
    pass

Cada um dos inteiros (dos 1 bilhão) será gerado de uma vez, economizando memória e tempo de startup.

Vamos então testar o desempenho usando o módulo timeit().

 

A hora da verdade

junior@qwerty:~ $ python -m timeit "for i in xrange(10000000): pass"
 10 loops, best of 3: 246 msec per loop
junior@qwerty:~-$ python -m timeit "for i in range(10000000): pass"
 10 loops, best of 3: 342 msec per loop

Como podemos ver, o loop que utiliza a função xrange() foi quase 100 milisegundos mais rápido do que o loop que utiliza a função range(). Além disso, se fizermos uma análise de consumo de memória, veremos que a o código que utiliza a função range() utiliza uma quantidade de memória muito maior, pois gera a lista inteira antes de executar a iteração do for.

Então nunca mais vou usar o range(), certo?

Errado! Existe uma diferença fundamental entre as duas funções: a função xrange() não gera uma lista. Isso torna inviável, por exemplo, o slicing e a gravação de seu resultado como uma lista em uma variável. Vejamos:

>>> x = range(0, 10)
>>> print x
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> y = xrange(0, 10)
>>> print y
xrange(10)

Ou seja, apesar de nos fornecer um desempenho superior ao desempenho obtido com a range(), a função xrange() não substitui a anterior em todos os seus casos.

Medindo tempo de execução de código Python

Muitas vezes, é necessário que comparemos duas implementações diferentes de uma mesma função em nosso código para saber qual apresenta melhor performance, com relação ao tempo de resposta. Por exemplo, escrevemos duas funções que fazem a soma de todos os números inteiros entre x e y. Ambas funcionam corretamente, mas, gostaríamos de descobrir qual delas nos fornece o menor tempo de resposta. As duas implementações para a mesma função são apresentadas abaixo:

def soma1(x, y):
    return sum(range(x, y+1))

def soma2(x, y):
    soma = 0
    for i in range(x, y+1):
        soma += i
    return soma

Tanto a função soma1, quanto a função soma2 realizam a soma de todos os elementos entre x e y. Como descobrir qual delas nos fornece o menor tempo de resposta? A primeira solução que vem em mente é marcar o tempo antes de chamar a função e marcar o tempo depois de chamar a função. Assim o tempo de resposta seria a diferença entre os dois tempos marcados. Veja o exemplo:

import time

# verifica o tempo de resposta da função soma1
ini = time.time()
soma1(1, 1000000)
fim = time.time()
print "Função soma1: ", fim-ini

# verifica o tempo de resposta da função soma2
ini = time.time()
soma2(1, 1000000)
fim = time.time()
print "Função soma2: ", fim-ini

Assim, você obtém o tempo de execução de cada uma das funções. O problema dessa abordagem é a precisão do tempo medido. A função time.time() retorna o tempo atual em segundos passados desde 1/1/1970. Apesar de ser um número de ponto flutuante, a precisão desse número não é muito grande, além de a própria chamada da função time() já trazer consigo um overhead.

Uma alternativa mais precisa é a utilização do módulo timeit. Esse módulo é incluído com Python e foi projetado especificamente para fazer a medição de tempo de execução de programas/trechos de código Python. Por isso, é mais recomendado que utilizemos o timeit no lugar de uma solução “caseira”. Vejamos a seguir como utilizar o timeit.

Pela linha de comando

O timeit pode ser utilizado para medir o tempo de execução de código python diretamente na linha de comando. O exemplo abaixo ilustra isso:

$ python -m timeit "range(1,10000)"

A linha de comando acima executa o módulo timeit como um script e passa como entrada a expressão Python que está entre aspas. A execução do comando acima em meu sistema retornou o seguinte:

10000 loops, best of 3: 156 usec per loop

Ou seja, o timeit realizou 3 baterias compostas de um conjunto de 10000 repetições cada, executando o código range(1,10000) e, dessas 3 tentativas, a melhor delas obteve uma média de 156 microsegundos por execução do código. Através dele, podemos, por exemplo, verificar na prática que o uso do xrange() ao invés do range() nos dá um tempo de execução melhor. Veja e compare:

$ python -m timeit "xrange(1,10000)"
1000000 loops, best of 3: 0.357 usec per loop

Além do recurso de teste pela linha de comando, também podemos executar o timeit dentro de scripts Python.

Importando o módulo e usando dentro do código Python

Voltando ao exemplo das funções soma1 e soma2, no qual queremos descobrir qual delas nos dá melhor tempo de resposta, podemos fazer o seguinte, no mesmo arquivo onde estão definidas as funções:

import timeit

def soma1(x, y):
    return sum(range(x, y+1))

def soma2(x, y):
    soma = 0
    for i in range(x, y+1):
        soma += i
    return soma

t = timeit.Timer("soma1(1,1000)", "from __main__ import soma1")
print t.repeat()

t = timeit.Timer("soma2(1,1000)", "from __main__ import soma2")
print t.repeat()

O programa, quando executado, irá fazer o teste de ambas as funções. Vamos analisar as duas chamadas a funções do módulo timeit:

t = timeit.Timer("soma1(1,1000)", "from __main__ import soma1")
print t.repeat()

Na primeira linha, passamos duas entradas para o método Timer(). A primeira string indica o código a ser testado, ou seja, o código do qual estamos interessados em saber o tempo de resposta. A segunda string indica um código de inicialização, necessário para que o timeit consiga executar o código que queremos. Chamando a função repeat() sem parâmetros, são executadas 3 baterias de 1000000 execuções e, como retorno, recebemos uma lista contendo os tempos médios de execução das 3 baterias de testes. Poderíamos especificar, como no exemplo abaixo, a quantidade de baterias (2) e o número de execuções por bateria (1000).

t = timeit.Timer("soma1(1,1000)", "from __main__ import soma1")
print t.repeat(2, 1000)

Assim, podemos utilizar o módulo timeit para descobrir qual o tempo de resposta de nosso código, sem a necessidade de construir o nosso próprio módulo de testes de tempo de execução. Uma das vantagens do timeit é que, por exemplo, ele desabilita o Garbage Collector, para evitar que este interfira nos resultados.

Curioso em saber qual das duas funções (soma1 e soma2) apresentou melhor tempo de execução? Faça o teste. 🙂

Acentuação em programas Python

Python, por padrão, interpreta seus programas usando a codificação de caracteres ASCII. Tal codificação não é capaz de representar os caracteres acentuados (á, á, â, ã, …), que são muito utilizados na nossa língua portuguesa. Então, como escrever um comentário no código utilizando a grafia correta, sem precisar escrever ‘é’ como ‘eh’? Como escrever mensagens a serem passadas ao usuário utilizando acentuação nos caracteres? Como evitar que a mensagem de erro abaixo apareça como saída do programa?

File "foo.py", line 2
SyntaxError: Non-ASCII character '\xc3' in file foo.py on line 2, but no
encoding declared;see http://www.python.org/peps/pep-0263.html for details

É preciso indicar ao Python qual é a codificação de caracteres que nosso arquivo de código está utilizando. Isso pode ser feito incluindo a seguinte linha de código no cabeçalho do arquivo.

# coding=<nome da codificação>

ou:

# -*- coding: <nome da codificação> -*-

As distribuições Linux, em sua maioria, utilizam a codificação UTF-8 para representação de caracteres. Assim, para utilizar caracteres com acentuação em um arquivo Python, usando Linux, é preciso adicionar ao começo* do arquivo:

# coding=UTF-8

ou:

# -*- coding: UTF-8 -*-

* A linha que indica a codificação pode ser a primeira linha do arquivo, ou a segunda, caso o arquivo contenha a indicação do binário do interpretador na primeira linha. Ex.:

#!/usr/bin/python
# -*- coding: UTF-8 -*-

Mais informações em: http://www.python.org/dev/peps/pep-0263

Seleção condicional de valores

Em diversas situações, o valor a ser atribuído a uma variável, ou retornado por um método depende de uma condição. Por exemplo:

if x >= 0:
    y = 1
else:
    y = -1

O valor de y depende da condição estabelecida sobre o valor de x. O código acima pode ser representado através de uma sintaxe mais concisa e de fácil leitura:

y = 1 if x >= 0 else -1

A expressão acima poderia ser lida da seguinte forma:

O valor de y será 1 se x for maior ou igual a zero. Caso contrário, será -1.

Podemos nos aproveitar dessa sintaxe simplificada para determinar o valor de retorno de funções:

def f(x):
    return 1 if x >= 0 else -1

Esse tipo de estrutura lembra, embora seja mais legível, as expressões ternárias em C:

int func(int x)
{
    return x >= 0 ? 1 : -1;
}