Acessando APIs REST com Python

Um serviço web (web service) é um mecanismo utilizado para comunicação entre dois ou mais programas através da infraestrutura da internet. Apesar do que o nome pode sugerir, um serviço web não oferece funcionalidades diretamente para o usuário final, mas sim para outros programas que precisam de sua ajuda para realizar alguma tarefa, seja para computar algo ou para fornecer dados úteis.

Você conhece o serviço de busca de endereços postais por CEP? O site dos correios fornece aos cidadãos um serviço que permite que estes façam consultas à base de dados de endereços através de uma página web. Esse não é um exemplo de web service, pois tem como foco servir diretamente ao usuário final. Um web service equivalente poderia ser um serviço oferecido pelos próprios correios para que outros programas, como por exemplo o software de alguma loja virtual, possam verificar qual é o endereço do CEP preenchido pelo usuário. Nesse caso, o software faz uma requisição ao web service, aguarda a resposta, decodifica a mesma e então inclui a resposta obtida na página mostrada ao usuário final.

Um web service pode ser fornecido de várias maneiras. Entretanto, foram definidos alguns padrões para facilitar a interoperabilidade entre programas de diferentes origens, sendo REST e SOAP os mais conhecidos. Este post irá mostrar como utilizar APIs web baseadas no padrão REST.

APIs REST

Uma API (application programming interface) é uma especificação que define como componentes de software devem interagir entre si (thanks, wikipedia!). APIs REST se utilizam do protocolo HTTP para fornecer determinadas funcionalidades aos seus clientes. Essas funcionalidades são descritas por conjuntos de recursos que podem ser acessados remotamente pelos clientes do serviço, através de requisições HTTP comuns.

Em uma API REST existem dois conceitos principais: os recursos (resources) e as coleções (collections). Um recurso é uma unidade que representa um objeto (composto por dados, relacionamentos com outros recursos e métodos). Já uma coleção é um conjunto de recursos que pode ser obtido acessando uma URL. Tal coleção poderia representar a coleção de todos os registros de determinado tipo, ou então, todos os registros que possuem relacionamento com determinado objeto, ou todos os registros que atendem à determinada condição, etc.

A API do twitter, por exemplo, fornece acesso a alguns recursos como os tweets enviados pelos usuários. Com ela, nossa aplicação pode enviar uma requisição HTTP ao twitter solicitando os últimos tweets de um determinado usuário ou até mesmo postar uma mensagem em nome do usuário autenticado.

Por exemplo, para obter uma lista com os últimos 10 tweets postados por mim, basta enviar uma requisição HTTP do tipo GET ao endereço https://api.twitter.com/1.1/statuses/user_timeline.json?screen_name=stummjr&count=10. Para que isso funcione, porém, é preciso de autenticação utilizando o mecanismo descrito na documentação da API.

Outro exemplo, com a API do próprio twitter, é o serviço de busca de tweets. Se quisermos buscar os últimos 20 tweets que contenham a hashtag #python, podemos enviar uma requisição à URL: https://api.twitter.com/1.1/search/tweets.json?q=#python&count=20. Como resposta às requisições, receberemos coleções de dados estruturados em formato JSON. A resposta dada por uma chamada a uma API REST normalmente é composta por dados em algum formato estruturado, como JSON ou XML. Esses formatos permitem a interoperabilidade entre plataformas e linguagens diferentes, pois o conteúdo é nada mais do que texto puro codificado com algum esquema de caracteres.

Muitos sites famosos (como twitter, facebook, reddit, etc) fornecem APIs REST para que terceiros possam escrever aplicativos que utilizem os dados armazenados nesses sites. Com essas APIs, fica fácil criar programas que interajam com redes sociais, lendo e postando dados para as mesmas.

Como você pôde ver, uma API REST nada mais é do que uma API que fornece acesso remoto a recursos via HTTP. Para podermos entender melhor e fazer requisições HTTP a um serviço REST, precisamos conhecer um pouquinho mais sobre o protocolo HTTP e como seus métodos são utilizados em uma API REST.

HTTP e seus métodos

O protocolo HTTP define que uma requisição de um cliente para um servidor é composta por:

  1. Uma linha descrevendo a requisição, composta por:
    1. Método: indica o que desejamos fazer com o recurso. Pode ser: GET, POST, PUT, DELETE, além de outros menos utilizados.
    2. URL: o endereço do recurso que se deseja acessar.
    3. Versão: a versão do protocolo a ser usada (1.1, atualmente).
  2. O corpo da requisição, que pode conter informações como o nome do host de onde desejamos obter o recurso solicitado, dados a serem enviados do cliente para o servidor, etc.

O exemplo abaixo mostra uma requisição HTTP do tipo GET para um recurso na web:

GET /r/programming/.json HTTP/1.1
Host: http://www.reddit.com

O método de uma requisição HTTP indica a ação que pretendemos realizar com aquela requisição, e cada método tem um significado próprio:

  • GET: utilizado para a obtenção de dados. É o tipo de requisição que o navegador faz a um servidor quando você digita uma URL ou clica em um link.
  • POST: utilizada na web para o envio de dados do navegador para o servidor, principalmente quando esses dados vão gerar alguma alteração no último. É o tipo de requisição que o navegador faz a um servidor quando você preenche um formulário na web e clica em “Enviar” (embora existam formulários na web que utilizem outros tipos de requisição, como GET).
  • PUT: serve para solicitar a criação de objetos no servidor caso esses ainda não existirem. Na prática, a maioria das páginas utiliza o método POST para isso também.
  • DELETE: serve para indicar que o usuário (emissor da requisição em questão) deseja apagar determinado recurso do servidor.

Após executar a requisição do cliente, o serviço responde com uma mensagem de resposta HTTP. O protocolo HTTP define que as mensagens de resposta devem possuir um campo indicando o status da requisição. O status mais conhecido na web é o 404 (not found – recurso não encontrado), mas existem vários, como: 200 (OK), 401 (not authorized - indicando falta de autenticação), 500 (internal server error – erro no servidor), dentre outros. Por ser baseado em HTTP, o padrão REST define que as mensagens de resposta devem conter um código de status, para que o cliente do serviço web possa verificar o que aconteceu com a sua requisição.

A seguir veremos como emitir requisições HTTP “programaticamente” em Python, acessando uma API REST disponível na web.

Acessando uma API REST

Para entender melhor, vamos utilizar como exemplo a API REST JSONPlaceHolder, disponível em jsonplaceholder.typicode.com, que é uma API fake criada para ser usada por quem estiver usando REST em seu programa e precisando de dados falsos (dummy data) para testes.

O JSONPlaceHolder disponibiliza acesso a alguns recursos, como: posts, comments, albums, photos, todos e users. Cada um dos recursos está disponível em uma URL específica:

Em nosso exemplo, vamos usar somente o recurso comments, mas o exemplo será válido para qualquer um dos recursos acima.

Como já foi mencionado anteriormente, as APIs REST fornecem suas funcionalidades através dos métodos existentes no protocolo HTTP (GET, POST, PUT e DELETE). Por exemplo, para listar todos os comments existentes, basta enviar uma requisição HTTP do tipo GET à URL http://jsonplaceholder.typicode.com/comments. Para listar algum registro comment em específico, basta enviar um GET à mesma URL, passando como parâmetro o id do comment que queremos obter: http://jsonplaceholder.typicode.com/comments/1. Uma requisição HTTP usando o método DELETE à URL http://jsonplaceholder.typicode.com/comments/1 irá remover o objeto comment em questão. Também é possível alterar um objeto através do método HTTP PUT ou incluir um novo objeto com o método POST.

Podemos resumir a semântica dos métodos HTTP em uma API REST da seguinte forma:

  • GET: obtenção de dados (seja um conjunto de objetos ou um em específico).
  • POST: criação de dados.
  • PUT: alteração de dados existentes.
  • DELETE: remoção de dados.

Obviamente, as APIs REST utilizam mecanismos de autenticação para evitar que alguém altere ou acesse dados de forma indevida.

Mãos na massa

Atenção: esta seção supõe que você tem uma certa familiaridade com JSON. Caso não conheça o formato, veja aqui um post anterior sobre o assunto.

Agora que já temos uma ideia sobre como uma API REST funciona, vamos ver na prática como nosso programa poderia utilizar uma API desse tipo para obtenção e manipulação de dados externos. Para fazer as requisições HTTP ao serviço, vamos utilizar a biblioteca requests (instalável via pip install requests) e para manipular o JSON retornado pelo serviço, vamos usar a biblioteca json (inclusa na biblioteca padrão).

Primeiramente, vamos importar as bibliotecas necessárias:

>>> import json, requests

Obtendo dados

Vamos começar testando a leitura de registros usando o método HTTP GET, que está disponível na requests através do método get().

>>> response = requests.get("http://jsonplaceholder.typicode.com/comments")
>>> print response.status_code
200
>>> print response.content
   [
    {
    "postId": 1,
    "id": 1,
    "name": "id labore ex et quam laborum",
    "email": "Eliseo@gardner.biz",
    "body": "laudantium enim quasi est quidem magnam voluptate ipsam eos\ntempora quo necessitatibus\ndolor quam autem     quasi\nreiciendis et nam sapiente accusantium"
    },
    {
    "postId": 1,
    "id": 2,
    "name": "quo vero reiciendis velit similique earum",
    "email": "Jayne_Kuhic@sydney.com",
    "body": "est natus enim nihil est dolore omnis voluptatem numquam\net omnis occaecati quod ullam at\nvoluptatem error expedita pariatur\nnihil sint nostrum voluptatem reiciendis et"
    }
    ...
    ,
    {
    "postId": 100,
    "id": 500,
    "name": "ex eaque eum natus",
    "email": "Emma@joanny.ca",
    "body": "perspiciatis quis doloremque\nveniam nisi eos velit sed\nid totam inventore voluptatem laborum et eveniet\naut aut aut maxime quia temporibus ut omnis"
    }
   ]

Agora que vimos que nossa requisição HTTP foi executada com sucesso (código 200) e que a string retornada como resposta está em formato JSON, vamos empacotar o resultado em um objeto Python para que possamos manipular os dados com maior facilidade:

>>> comments = json.loads(response.content)

A função json.loads() transformou a string JSON em um objeto Python de tipo correspondente, em nosso caso, um objeto list contendo vários dict dentro, onde cada dict representará um dos registros existentes no servidor.

>>> print comments[0]
    {u'body': u'laudantium enim quasi est quidem magnam voluptate ipsam eos\ntempora quo necessitatibus\ndolor quam autem quasi\nreiciendis et nam sapiente accusantium', u'email': u'Eliseo@gardner.biz', u'postId': 1, u'id': 1, u'name': u'id labore ex et quam laborum'}

>>> print comments[0]['body']
     laudantium enim quasi est quidem magnam voluptate ipsam eos
     tempora quo necessitatibus
     dolor quam autem quasi
     reiciendis et nam sapiente accusantium

Listando os nomes dos 10 primeiros comments:

>>> for comment in comments[0:10]:
        print comment['name']
     labore ex et quam laborum
     quo vero reiciendis velit similique earum
     odio adipisci rerum aut animi
     alias odio sit
     vero eaque aliquid doloribus et culpa
     et fugit eligendi deleniti quidem qui sint nihil autem
     repellat consequatur praesentium vel minus molestias voluptatum
     et omnis dolorem
     provident voluptas
     eaque et deleniti atque tenetur ut quo ut

Além da listagem de todos os objetos, também podemos obter um objeto em específico:

>>> response = requests.get("http://jsonplaceholder.typicode.com/comments/1")
>>> response.content
     '{\n  "postId": 1,\n  "id": 1,\n  "name": "id labore ex et quam laborum",\n  "email": "Eliseo@gardner.biz",\n  "body": "laudantium enim quasi est quidem magnam voluptate ipsam eos\\ntempora quo necessitatibus\\ndolor quam autem quasi\\nreiciendis et nam sapiente accusantium"\n}'
>>> comment = json.loads(response.content)
>>> print comment['name']
     labore ex et quam laborum

Se quisermos descobrir a qual post que o comment acima se refere, basta fazer uma requisição GET à http://jsonplaceholder.typicode.com/posts/X, sendo X o valor do campo postId do comment retornado. Veja:

>>> post = requests.get("http://jsonplaceholder.typicode.com/posts/%d" % comment['postId'])
>>> post.content
     '{\n  "userId": 1,\n  "id": 1,\n  "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",\n  "body": "quia et suscipit\\nsuscipit recusandae consequuntur expedita et cum\\nreprehenderit molestiae ut ut quas totam\\nnostrum rerum est autem sunt rem eveniet architecto"\n}'
>>> post = json.loads(post.content)
>>> post
     {u'body': u'quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto',
     u'id': 1,
     u'title': u'sunt aut facere repellat provident occaecati excepturi optio reprehenderit',
     u'userId': 1}
>>> post['title']
     u'sunt aut facere repellat provident occaecati excepturi optio reprehenderit'

Dessa forma, podemos navegar entre registros, obter objetos relacionados, etc.

Inserindo dados

Para a inserção de dados em um serviço que oferece uma API REST, precisamos utilizar o método POST do HTTP, disponível através da função post da requests. Como queremos inserir um novo registro no servidor, é necessário que passemos esse registro junto à requisição HTTP. Isso pode ser feito passando um dicionário com os dados ao parâmetro data:

>>> dados = data={"postId": 1, "name": "John Doe", "email": "john@doe.com", "body": "This is it!"}
>>> response = requests.post("http://jsonplaceholder.typicode.com/comments/", data=dados)

Feito isso, agora podemos verificar qual a resposta do serviço para a nossa requisição. O valor esperado depende de quem projetou a API, pois ele pode enviar uma resposta contendo o novo registro, a URL de acesso ao registro recém-criado, ou outra informação. Além disso, o código de resposta HTTP também pode variar (alguns serviços respondem com 200 — OK, outros com 201 – Created, embora o último faça muito mais sentido). O serviço que estamos usando para os exemplos envia uma resposta com o código 200 e o registro recém inserido como conteúdo.

Como ocorreu tudo dentro do esperado, o serviço respondeu com o registro criado (repare que foi adicionado um campo id que não havia nos dados que enviamos):

>>> print response.status_code
     200
>>> print response.content
     {
     "body": "This is it!",
     "postId": "1",
     "name": "John Doe",
     "email": "john@doe.com",
     "id": 501
     }

Para ter certeza sobre o funcionamento da API que você estiver usando, é preciso ler a especificação dela para descobrir o que esperar como resultado em caso de sucesso ou de erro. Como o protocolo HTTP já possui um conjunto pré-definido de códigos de status, os serviços baseados em REST devem usar tais códigos para indicar o status da requisição.

Alterando registros

O padrão REST define que o método HTTP PUT deve ser utilizado sobre um determinado recurso quando desejarmos alterá-lo. A biblioteca requests fornece a função put para o envio de requisições HTTP que utilizam o método PUT. Vamos a um exemplo, onde vamos alterar o campo email do comentário de id 10:

>>> dados = {"email": "john@doe.com"}
>>> response = requests.put("http://jsonplaceholder.typicode.com/comments/10", data=dados)

Ou seja, enviamos uma requisição PUT à URL que representa o comentário que queremos alterar, e passamos também um dicionário contendo o novo valor para o campo que desejamos alterar. Como resposta, obtivemos o recurso alterado.

Removendo registros

Para apagar um registro, o padrão REST define que uma requisção HTTP usando o método DELETE deve ser enviada ao serviço, passando como recurso o registro que deve ser removido. Para apagar o comment de id 10, utilizamos a função delete da requests:

>>> response = requests.delete("http://jsonplaceholder.typicode.com/comments/10")

Acessando recursos aninhados

Como já vimos pela estrutura dos dados retornados para os comments, cada registro desse tipo está associado a um registro post. Assim, uma necessidade que surge naturalmente é a de obter todos os comments pertencentes a um determinado post. O web service que estamos usando permite consultas a recursos relacionados. Para obter todos os comments relacionados ao post de id 2, fazemos:

>>> response = requests.get("http://jsonplaceholder.typicode.com/posts/2/comments")

Enfim

Apesar de o exemplo que seguimos ter focado em um web service específico, cada serviço possui uma interface de acesso própria. Ou seja, algumas APIs podem não permitir acesso a recursos aninhados, ou não permitir a remoção de registros, etc. É importante que você, antes de utilizar uma API REST, leia a documentação da mesma para saber o que é possível fazer com ela.

Apesar dessas diferenças entre uma API e outra, o mecanismo de acesso às mesmas não muda. Você vai precisar de uma biblioteca para emitir requisições HTTP (requests ou urllib2) e uma biblioteca para fazer a decodificação dos dados retornados (json, simplejson ou alguma biblioteca para manipulação de XML).

Obrigado ao Elias Dorneles pela revisão!

Manipulando strings como se fossem arquivos – StringIO

Há tempos atrás eu precisei extrair dados de pagamentos de arquivos pdf, mas a API que eu estava utilizando para extrair os dados do pdf trabalhava exclusivamente com arquivos. Isto é, a função que convertia o pdf em texto precisava receber um arquivo como argumento, para nele escrever o texto extraído do pdf. Entretanto, criar um arquivo (mesmo que temporário), escrever nele, e por fim, ler os dados que me interessavam a partir dele, não era algo muito conveniente, afinal, tudo que eu precisava eram os dados dos pagamentos para efetivar alguns registros no banco de dados. Foi aí que conheci o StringIO.

StringIO é uma classe Python que representa strings em estruturas que se comportam como se fossem arquivos (com a mesma interface dos objetos file), mas que ficam residentes em memória, como strings comuns. Isso pode ser útil quando lidamos com APIs cuja interface exige objetos file.

Para ter uma ideia melhor do que é a StringIO, veja o exemplo abaixo:

Importante: em Python 3.x, a classe StringIO foi movida para o módulo io (dica do alansteixeira). Para usá-la, faça: from io import StringIO

>>> from StringIO import StringIO
>>> fp = StringIO("uma string qualquer")
>>> fp.readline()
'uma string qualquer'
>>> fp.readline()
''

Também podemos criar um objeto StringIO, passar para alguma função escrever algo nele, e então obter os valores escritos chamando o método getvalue().

>>> fp = StringIO()
>>> def func(f):
>>>     f.write("hello")
>>> func(fp)
>>> fp.getvalue()
'hello'

Quando criamos uma API, às vezes é necessário fornecer uma mesma funcionalidade através de duas funções que diferem nos tipos dos parâmetros esperados: uma recebendo um arquivo e outra recebendo uma string. Isso acontece em algumas APIs conhecidas, como a de manipulação de JSON, que fornece as funções load()/dump() e loads()/dumps(), onde as primeiras recebem um arquivo como parâmetro e as últimas recebem uma string. O que uma classe como a StringIO nos permite fazer é implementar a lógica da função somente na função que recebe o arquivo. Dessa forma, a função que recebe uma string pode empacotá-la em um objeto StringIO e então chamar a função que recebe um arquivo, passando o objeto em questão para ela.

Uma outra coisa interessante que podemos fazer com a StringIO é interceptar a saída do programa que iria para a stdout (descobri isso aqui: http://effbot.org/librarybook/stringio.htm). Para fazer isso, temos que substituir a saída padrão (acessível via sys.stdout) por um objeto StringIO. Assim, tudo que for impresso via print será gravado em um objeto StringIO e não na saída-padrão. Veja:

# -*- encoding:utf-8 -*-
import sys
from StringIO import StringIO

backup_stdout = sys.stdout
sys.stdout = StringIO()

# será escrito em um StringIO
print "hello world"

# pega uma referência ao objeto StringIO antes de restaurar a stdout
fp = sys.stdout
sys.stdout = backup_stdout
print "stdout: " + fp.getvalue()

E, assim como fizemos com a stdout, poderíamos ter feito com a stderr (saída-padrão de erros), para interceptar as mensagens de erro e, quem sabe, analisá-las.

Enfim, sempre que se deparar com uma API que exige arquivos como parâmetros, lembre-se da StringIO caso não esteja a fim de acessar o disco e de lidar com arquivos.

Funções como objetos de primeira classe

Quando você começa a ler um pouquinho mais a fundo sobre linguagens de programação em geral, ou Python em específico, é comum encontrar referências dizendo que funções em python são objetos de primeira classe. Mas o que significa isso?

Dizer que funções são objetos de primeira classe em uma linguagem de programação significa que naquela linguagem uma função é um objeto como qualquer outro, podendo ser tratada da mesma forma que os demais objetos. Ou seja, podemos atribuir uma função a uma variável (ou melhor, dar um nome a uma função), passar uma função como parâmetro para outra função, além de outras operações. Como em Python as funções são objetos de primeira classe, vamos ver alguns exemplos:

 def soma(x, y): return x + y
>>> type(soma)
function
>>> s = soma
>>> s(1, 2)
3
>>> soma(1, 2)
3

No exemplo acima, criamos uma função, que nada mais é do que um objeto do tipo function, e inicialmente referenciamos ela pelo nome soma. Como essa função é um objeto como qualquer outro, podemos então criar uma nova referência a ela (s no exemplo acima).

O mais legal de podermos tratar funções assim é que podemos passar funções como argumentos para outras funções. Como exemplo, vamos criar uma função semelhante à função builtin filter. Ela irá receber uma lista de elementos (de qualquer tipo) e uma função que determina se o elemento deve ser incluso na lista de resultados ou não.

def filtra(lista, aceita):
    result = []
    for elemento in lista:
        if aceita(elemento):
            result.append(elemento)
    return result

No exemplo acima, aceita deve ser um objeto do tipo function, que verifique algo em um objeto qualquer e retorne True ou False (ou outros valores que podem ser avaliados como tal). Feito isso, agora vamos usar a função filtra recém definida.

def eh_positivo(valor):
    return valor > 0

lista_de_int = [-10, 10, 2, -7, 3, 5, 1]
nova_lista = filtra(lista_de_int, eh_positivo)
print nova_lista  # imprime [10, 2, 3, 5, 1]

No exemplo acima, passamos à função filtra uma lista de inteiros e uma função apropriada para aceitação de elementos do tipo int. O mais interessante de a função filtra receber uma função como parâmetro é que isso a deixa muito mais flexível, pois ela poderá funcionar para elementos de qualquer tipo e para as mais variadas semânticas de aceitação de valores, desde que seja fornecida uma função apropriada para isso. Abaixo, definimos uma função que aceita strings que contém espaços e rejeita as que não contém.

def tem_espacos(s):
    return ' ' in s

lista_de_str = ["olá mundo", "hello", "testando 123"]
print filtra(lista_de_str, tem_espacos)  # imprime ["olá mundo", "testando 123"]

Existem várias funções prontas que aceitam outras funções como argumentos. Anteriormente, aqui no blog, já vimos map, reduce e filter. Ao ordenar uma lista podemos passar uma função que será usada para comparar os elementos, e existem muitos outros exemplos.

Com a possibilidade de tratar funções como objetos, podemos escrever funções genéricas para lidar com dados independentemente do tipo.

Ordenação de uma lista

É comum termos uma lista toda bagunçada e querermos ordenar os elementos contidos nela. Para ordenar uma lista de valores, basta chamar o método sort da lista.

Vamos ver como isso funciona na prática. Primeiramente, vamos criar uma lista com 10 elementos e depois bagunçá-la usando a função shuffle, do módulo random.

>>> lista = range(10)
>>> lista
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> import random
>>> random.shuffle(lista)
>>> lista
[2, 5, 4, 1, 3, 6, 9, 7, 0, 8]

Tudo que precisamos fazer para ordenar uma lista desordenada é:

>>> lista.sort()
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Barbada! Também podemos ordená-la de forma descendente:

>>> lista.sort(reverse=True)
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

A ordenação de inteiros ou de valores de tipos de dados simples é bem trivial. Porém, se quisermos ordenar uma lista que contém instâncias de uma classe, precisaremos utilizar o parâmetro key do sort.

O parâmetro key

O parâmetro key do método sort espera uma função que será chamada uma vez para cada um dos elementos da lista e o retorno dessa função será utilizado na comparação com os outros elementos da lista.

Considere que temos uma classe Aluno, conforme o código abaixo:

class Aluno:
	def __init__(self, nome, matricula):
		self.nome = nome
		self.matricula = matricula

	def __str__(self):
		return "%s - %s" % (str(self.nome), str(self.matricula))

Dada uma lista chamada alunos contendo n objetos do tipo Aluno, como ordená-la? Se chamarmos alunos.sort(), sem especificar como queremos que ela seja ordenada, o sort irá ordená-la através de comparações dos endereços de memória dos objetos contidos na lista alunos. Se quisermos que a ordenação se dê por algum dos atributos da classe, devemos especificar isso através do parâmetro key.

Vamos primeiramente criar uma lista com objetos de conteúdo aleatório:

>>> alunos = [Aluno("".join(random.sample(string.ascii_letters, 5)), random.randint(0, 100)) for i in range(10)]
>>> for aluno in alunos:
		print aluno
zfbnu - 12
sxbIX - 77
vJCIN - 33
aBjZA - 70
fNLeS - 19

Bonitos os nomes deles, né? Agora, vamos ordená-los:

>>> alunos.sort(key=lambda a: a.nome)
>>> for aluno in alunos:
        print aluno
aBjZA - 70
fNLeS - 19
sxbIX - 77
vJCIN - 33
zfbnu - 12

O que fizemos foi especificar como queremos que os elementos sejam comparados.  Para isso, criamos uma função anônima que recebe como parâmetro um objeto e retorna o elemento a ser usado na comparação (o atributo nome). O mesmo poderia ser feito com uma função nomeada, como:

>>> def key_func(aluno):
...     return aluno.nome
>>> alunos.sort(key=key_func)
>>> for aluno in alunos:
...     print aluno

Porém, ter que criar uma função (anônima ou não) somente para indicar qual atributo deverá ser usado na ordenação é um pouco inconveniente. Por isso, vamos utilizar o outro mecanismo que permite que façamos a mesma coisa. Ao invés de criarmos uma função que recebe um objeto e retorna o atributo nome daquele objeto, vamos usar a função attrgetter do módulo operator, que retorna o valor do atributo solicitado no objeto em questão.

>>> from operator import attrgetter
>>> alunos.sort(key=attrgetter("nome"))

A função attrgetter irá retornar uma outra função que quando chamada sobre cada objeto x contido na lista alunos, irá retornar x.nome.

Ou seja, para cada objeto Aluno contido na lista, será chamado o método attrgetter, solicitando o atributo nome.

Ordenando uma lista de listas

Já vi muito código que utiliza lista ou tupla como mecanismo para agrupar dados. Ao invés de criar uma classe ou uma namedtuple, o cara vai lá e empacota os dados que deseja em uma tupla. Por exemplo, ao invés de criar uma classe Aluno, poderíamos ter empacotado os dados referentes a cada aluno em uma tupla. Veja:

>>> alunos = [("Jose", 12345), ("Maria", 28374), ("Joao", 11119), ("Joana", 12346)]

Para ordenar uma lista desse tipo, podemos continuar usando o método sort e o parâmetro key, e agora vamos especificar qual elemento das tuplas que compõem a lista será utilizado na comparação para definir qual elemento precede qual na ordem. No exemplo abaixo, estamos ordenando os alunos pelo número da matrícula.

>>> alunos.sort(key=lambda x: x[1])
>>> print alunos
[('Joao', 11119), ('Jose', 12345), ('Joana', 12346), ('Maria', 28374)]

A função anônima poderia ser evitada novamente usando a função itemgetter:

>>> from operator import itemgetter
>>> alunos.sort(key=itemgetter(1))

O itemgetter é bem parecido com o attrgetter, com a diferença de que passamos para ele o índice do elemento que queremos que seja usado na comparação que será feita ao ordenar a lista.

Mas fique atento, o método sort está presente somente nas listas. Para ordenar outros objetos iteráveis, dê uma olhada na função builtin sorted.

Gerenciando vários objetos com um with

Aviso: Python 2.7+

Hoje descobri que é possível gerenciar vários objetos em um context manager sem a necessidade de aninhar várias cláusulas with. Ou seja, pude transformar o código¹ que eu estava escrevendo de:

def ajeita_csv(path):
    with open(path, 'r') as fr:
        with open('u' + path, 'w') as fw:
            reader, writer = csv.reader(fr), csv.writer(fw)
            for row in reader:
                for i in range(3, len(row), 5):
                    new_row = row[:3] + row[i:i+5]
                    writer.writerow(new_row)

para:

def ajeita_csv(path):
    with open(path, 'r') as fr, open('u' + path, 'w') as fw:
        reader, writer = csv.reader(fr), csv.writer(fw)
        for row in reader:
            for i in range(3, len(row), 5):
                new_row = row[:3] + row[i:i+5]
                writer.writerow(new_row)

Ou seja, é possível gerenciar a abertura/fechamento de vários objetos/recursos em uma única cláusula with, separando os objetos por uma vírgula.

Fica a dica.

¹Código para dar uma ajustada em um CSV desformatado, que tinha vários registros por linha.

Sintonia fina do IPython

Quem já usou o IPython alguma vez sabe que ele é um tanto quanto “espaçoso”. Cada comando digitado gera uma nova linha, além de ele usar um prompt de entrada e saída com a contagem dos comandos digitados. Se, assim como eu, você não curte o visual do IPython padrão, este texto é para você.

Por padrão, o IPython se apresenta assim:

ipython1

Veja quantos espaços em branco. Ele é bem diferente do shell Python padrão, que é bem menos espaçoso.

Para deixá-lo mais parecido com o shell Python basicão, basta dar uma “tunadinha” nele. Em outro post, já mostrei como configurar alguns aspectos do IPython. Neste post, vou apresentar algumas configurações adicionais que podem ser feitas. Ao final dele, seu IPython vai ficar parecido com:

ipython2

Configurando o IPython

Antes de qualquer coisa é preciso criar um perfil, através do seguinte comando no shell do seu sistema operacional:

$ ipython profile create

Isso irá criar um perfil chamado de default e todas as configurações desse perfil estarão no diretório ~/.config/ipython/profile_default/ (ao menos no Ubuntu).

Feito isso, agora você pode editar o arquivo de configurações do IPython (~/.config/ipython/profile_default/ipython_config.py). Ele está repleto de linhas de código Python comentadas, mas vou me deter aqui apenas àquelas que descomentei e customizei.

Desabilitar o banner de apresentação

Toda vez que é iniciado, o IPython mostra um banner parecido com:

Python 2.7.4 (default, Sep 26 2013, 03:20:26) 
Type "copyright", "credits" or "license" for more information.

IPython 0.13.2 -- An enhanced Interactive Python.
?         -> Introduction and overview of IPython's features.
%quickref -> Quick reference.
help      -> Python's own help system.
object?   -> Details about 'object', use 'object??' for extra details.

Depois de ver duas ou três vezes, já decoramos essas informações e elas não são mais necessárias. Para desabilitar a apresentação dessas informações ao iniciar o IPython, utilizei a seguinte definição:

c.TerminalIPythonApp.display_banner = False

Removendo os \n entre as entradas

Uma das coisas mais irritantes no IPython é a quantidade de espaços em branco que ele coloca na tela. Muitos desses espaços são gerados pela separação entre as entradas através de um caractere de \n. Dá pra ajeitar isso, colocando um caractere vazio como separador:

c.TerminalInteractiveShell.separate_in = ''

Removendo a confirmação de fechamento

O IPython sempre pergunta se você deseja mesmo fechá-lo ao pressionar C-d. É claro que eu quero te fechar, ou tu tá achando que pressionei Ctrl e depois d sem querer? Dá pra evitar essa chateação com:

c.TerminalInteractiveShell.confirm_exit = False

Permitindo ao IPython modificar o título do terminal

Isso pode ser bem útil pra quem costuma ficar com várias abas de terminal abertas. Habilite isso com a seguinte linha:

c.TerminalInteractiveShell.term_title = True

Customizando o prompt de saída

Por padrão, linhas que contenham saída são precedidas por Out [x]:. Isso me incomodava um pouco, então resolvi alterá-lo e deixá-lo mais parecido com o shell padrão. Você também pode fazer isso com a seguinte linha:

c.PromptManager.out_template = ''

Customizando o prompt de entrada

O prompt de entrada no IPython apresenta o texto In [x]:. Para fazer com que o prompt de entrada seja igual ao clássico >>> do shell padrão, faça:

c.PromptManager.in_template = '>>> '

Completação por tab gulosa

Para habilitar a completação em resultados de chamadas de funções, ou em elementos de sequências, basta habilitar a configuração abaixo:

c.IPCompleter.greedy = True

Atenção: comentários no arquivo advertem que pode ser um pouco perigoso habilitar a configuração acima, pois para poder completar baseando-se no resultado de uma chamada de função, o ipython terá que chamá-la.

O ipython_config.py completo

# Configuration file for ipython.
c = get_config()

# Whether to display a banner upon starting IPython.
c.TerminalIPythonApp.display_banner = False

# Remove those annoying newlines between each input
c.TerminalInteractiveShell.separate_in = ''

# Set to confirm when you try to exit IPython with an EOF (Control-D in Unix)
c.TerminalInteractiveShell.confirm_exit = False

# Enable auto setting the terminal title.
c.TerminalInteractiveShell.term_title = True

# Output prompt.
c.PromptManager.out_template = ''

# Bring back the classic Python REPL prompt.
c.PromptManager.in_template = '>>> '

# Activate greedy completion
# This will enable completion on elements of lists, results of function calls,
# etc., but can be unsafe because the code is actually evaluated on TAB.
c.IPCompleter.greedy = True

Sugestões?

Se tiver mais alguma sugestão, envie um comentário.

Asteriscos em chamadas de funções

Diga aí, o que o código abaixo irá produzir?

def func(a, b, c, d):
    return (a + b) / (c + d)

lista_de_argumentos = [4, 6, 2, 3]
print func(*lista_de_argumentos)

Se você sabe a resposta (que é 2), provavelmente já conhece esse uso do * (asterisco ou estrela).
Caso não tenha entendido muito bem, leia o resto do texto.

A estrela (asterisco) na chamada de funções

A função func listada acima deve receber 4 parâmetros (a, b, c e d). Quando passamos um argumento precedido de um asterisco, esse argumento (se for uma lista ou tupla) será desempacotado e cada um dos elementos será passado como um dos argumentos posicionais da função. No exemplo acima, o primeiro elemento de lista_de_argumentos será passado ao argumento a, o segundo ao elemento b, e assim por diante. No exemplo acima, a chamada de função func(*lista_de_argumentos) dá no mesmo que func(4, 6, 2, 3), pois o desempacotamento é automático. A imagem abaixo ilustra o que acontece:

Exemplo de passagem de parâmetros

Se lista_de_argumentos tivesse uma quantidade de elementos diferente da quantidade de argumentos exigidos por func, uma exceção seria disparada. Veja:

>>> lista = [4, 6, 2, 3, 10]
>>> print func(*lista)
------------------------------------------------------------------
TypeError                        Traceback (most recent call last)
 in ()
----> 1 print func(*lista)
TypeError: func() takes exactly 4 arguments (5 given)

A estrela-dupla na chamada de funções

De forma semelhante, podemos também desempacotar dicionários em chamadas de funções. Nesses dicionários, as chaves deverão ser os nomes dos argumentos e os valores serão os valores que queremos passar como argumentos. Seguindo com o exemplo da função func, poderíamos passar um dicionário com os nossos parâmetros nomeados, precedido de **:

>>> argumentos = {'d': 3, 'c': 2, 'b': 6, 'a': 4}
>>> print func(**argumentos)
2

Assim, os valores armazenados no dicionário serão desempacotados direitinho como argumentos para a função, de acordo com as chaves correspondentes a eles. Assim, 4 será o argumento a, 6 será passado como argumento para b e assim por diante.

Quando usar isso?

Esse açúcar sintático oferecido por Python é bem útil quando temos uma lista contendo os valores que deverão ser passados para uma função. Por exemplo, ao invés de fazer:

>>> lista = [6, 4, 2, 3]
>>> func(lista[0], lista[1], lista[2], lista[3])

podemos fazer:

>>> lista = [6, 4, 2, 3]
>>> func(*lista)

O uso vai surgir da necessidade. Às vezes temos uma função que retorna uma tupla de n valores e queremos passar esses valores diretamente como argumentos para outra função, que exige n parâmetros. É aí que entra o *.

Python é lento? Que Python?

Já vi muita gente falando que Java é ruim porque é lento. Eu mesmo, há tempos atrás, falava isso. Esse é um dos muitos mitos que se propagam entre as pessoas, sem uma análise crítica mais aprofundada. Isso já foi verdade, láááá no começo. Hoje em dia, é possível conseguir melhor desempenho com programas escritos em Java do que programas escritos em C, que é o rei da performance, de acordo com o senso comum.

Hoje em dia, se fala muito que Python é uma ótima linguagem, com sintaxe e recursos excelentes, mas que possui desempenho ruim. Ou seja, dizem que é lento. Mas calma aí, vamos pensar um pouquinho e esclarecer algumas coisas.

Temos dois conceitos separados que muitas vezes são confundidos. Uma coisa é Python, a linguagem. Outra coisa é Python, o interpretador. A linguagem em si nada mais é do que a especificação, com as regras léxicas, sintáticas e semânticas. Já o interpretador Python é o programa que irá ler e executar os programas escritos usando a linguagem Python. Existem várias implementações, e não somente um único interpretador Python. Assim, não faz sentido afirmar que “Python é lento”. O que poderia ser dito é “todos os interpretadores Python existentes são lentos”, mas isso seria uma mentira.

Os interpretadores

O interpretador Python mais conhecido é o CPython, que é a implementação de referência da linguagem. É ele que vem instalado por padrão no Ubuntu, é ele que a grande maioria das pessoas instala quando vai aprender Python e é ele que é usado pelos desenvolvedores quando vão escrever programas em Python. É bem provável que você tenha o CPython instalado aí na sua máquina. Porém, apesar da popularidade, ele não é o único e também não é o mais performático.

Um interpretador que tem ganhado visibilidade é o PyPy. Diferentemente do CPython, que é escrito em linguagem C, o PyPy é escrito em um subconjunto de Python, o RPython. O que mais chama a atenção no PyPy é o desempenho. Ele consegue obter um desempenho bem melhor do que o CPython em muitos casos. Veja o gráfico abaixo, comparando a versão 2.2 do PyPy ao CPython em vários benchmarks (para a versão mais atual, acesse: speed.pypy.org).

Desempenho do PyPy vs CPython

Desempenho do PyPy vs CPython

Talvez você esteja pensando: como o PyPy, escrito em Python, pode ser mais rápido do que o CPython, que é escrito em C e compilado direto para código de máquina? O PyPy é escrito em Python, mas isso não quer dizer que ele é executado sobre um interpretador Python. O código do PyPy, escrito em RPython, é compilado para linguagem de máquina, podendo então ser executado diretamente sobre o hardware (sobre o SO, na verdade). Mas isso por si só não justifica o bom desempenho dele, afinal o CPython também é compilado para código de máquina.

O que diferencia o PyPy é o fato de ele utilizar um mecanismo chamado JIT (Just In Time compilation) durante a interpretação dos programas. O JIT fica analisando a execução do programa, pega as partes que são executadas com mais frequência e, dentre outras coisas, faz uma tradução em tempo de execução daquelas partes para código de máquina. Assim essas partes do programa não precisarão ser decodificadas e executadas pelo interpretador toda vez que tiverem que ser executadas. Além disso, o JIT pode fazer outros tipos de otimização no código, sempre em tempo de execução. Essa técnica é bem antiga, e é usada também em algumas implementações da JVM (Java Virtual Machine).

Mas o CPython e o PyPy não são as únicas implementações do interpretador Python. O Jython é um interpretador que roda sobre a JVM (que por sua vez roda sobre o SO, que roda sobre o hardware, o que acaba gerando problemas de desempenho). Outras implementações populares são: IronPython (implementação em C#), Stackless Python, Unladen Swallow (uma implementação da Google, que foi deixada meio de lado), dentre outros.

Enfim, o importante é perceber que não existe o tal interpretador Python. O que existe é uma variedade de implementações, cada uma com um objetivo. Enquanto para algumas o principal objetivo é oferecer integração com outras plataformas (Jython e IronPython), para outras o objetivo é levar o desempenho ao topo (PyPy, Stackless Python e Unladen Swallow).

O Django é um projeto de grande porte que é suportado pelos principais interpretadores existentes (CPython por padrão, PyPyJythonIronPython). A troca do interpretador que está por debaixo do Django irá impactar na performance dos aplicativos que estiverem rodando sobre o mesmo. Dá pra ter uma ideia mais prática do impacto que a troca do interpretador pode ter no desempenho de uma aplicação lendo esse post: http://tomvn.com/posts/load-testing-and-pypy-smoking-the-competition.html. No texto, o autor relata que implementou uma API e que obteve uma média de 600 requisições por segundo rodando a mesma sobre o CPython. Ao trocar para o PyPy, este número subiu para impressionantes 2k requisições por segundo.

Então Python não é lento?

Não necessariamente, embora em geral os interpretadores ainda não sejam tão bons de performance quanto código escrito em C/C++ ou Java (em 2013). Mas, para poder falar mais, seria necessário realizar experimentos comparando programas escritos em Python e interpretados pelo PyPy com programas escritos em C, por exemplo. Mas esse tipo de comparação é sempre muito subjetiva, pois depende dos recursos usados em cada implementação. De nada adianta comparar uma hashtable escrita em Java com uma hashtable escrita em Python, se uma delas for thread-safe e a outra não, por exemplo.

Acima de tudo, é importante perceber que a perda de performance é compensada pela agilidade possibilitada à equipe de desenvolvimento na hora de escrever uma aplicação e colocá-la em produção. Nem sempre o desempenho é o mais importante em um projeto, afinal, as horas de trabalho dos desenvolvedores também custam muito. Além disso, experimentos mostram que com PyPy é possível melhorar de forma significativa o desempenho de algumas aplicações que rodam no CPython.

Só não se esqueça do famoso mantra do Donald Knuth: “Premature optimization is the root of all evil“.

Java ou Python? Jython!

O interpretador Python padrão, também conhecido como CPython, é implementado em linguagem C, e nada mais é do que um programa que roda diretamente sobre o hardware. Além do CPython, existem implementações alternativas do interpretador Python. Talvez a mais popular dentre elas, o Jython é escrito em Java e roda sobre a JVM. Além de poder ser executado em qualquer ambiente que possua uma JVM instalada, Jython possibilita que APIs Java sejam utilizadas em Python. Ou seja, tenta unir o melhor de dois mundos: a rapidez no desenvolvimento de Python com a enorme quantidade de APIs maduras disponíveis para Java. Por se tratar de um interpretador Python, a sintaxe dos programa escritos para rodar sobre o Jython é idêntica a dos programas escritos para rodar sobre o CPython.

Mas como funciona isso?

É bem simples, basta importar as libs Java e sair usando, com a sintaxe de Python. Veja:

>>> from java.lang import Math, System
>>> raiz = Math.sqrt(9)
>>> System.out.println(raiz)
3.0

O trecho de código acima foi executado no interpretador Jython (disponível em jython.org).

Viu que maravilha? Importamos e executamos o código de bibliotecas Java, mas com a sintaxe de Python!

Herança

Outra coisa fantástica é a possibilidade de estender classes Java usando Python. Por exemplo, considere a seguinte classe Java (copiada descaradamente daqui):

public class Bicycle {

    public int cadence;
    public int gear;
    public int speed;

    public Bicycle(int startCadence, int startSpeed, int startGear) {
        gear = startGear;
        cadence = startCadence;
        speed = startSpeed;
    }

    public void setCadence(int newValue) {
        cadence = newValue;
    }

    public void setGear(int newValue) {
        gear = newValue;
    }

    public void applyBrake(int decrement) {
        speed -= decrement;
    }

    public void speedUp(int increment) {
        speed += increment;
    }
}

Se quisermos estender a classe acima, especializando-a para representar um tipo específico de bicicleta, como uma mountain bike, podemos estendê-la como se fosse uma classe Python. Veja abaixo o exemplo (MountainBike.py):

import Bicycle

class MountainBike(Bicycle):

    def __init__(self, seatHeight, startCadence, startSpeed, startGear):
        super(Bicycle, self).__init__(self, startCadence, startSpeed, startGear)
        self.seatHeight = seatHeight

    def setHeight(self, seatHeight):
        self.seatHeight = seatHeight

Se quiser testar os exemplos acima, não esqueça de:

  1. Compilar o arquivo Bicycle.java com um compilador Java:
    • javac Bicycle.java
  2. Rodar o arquivo MountainBike.py com o jython:
    • jython MountainBike.py

Todo programa Python vai rodar no Jython?

Não. Alguns programas rodam somente na implementação padrão do interpretador, pois dependem de módulos específicos que foram escritos em C e que ainda não foram portados para Java. Mas, de acordo com o FAQ, a grande maioria já está disponível.

Por que usar Jython?

Odeia a sintaxe e exigências semânticas de Java, mas não desgruda por causa das APIs que ela oferece? Manda ver com Jython! As possibilidades são muitas! Que tal reaproveitar aquela classe Java que você criou há tempos e que vem quebrando um galhão nos seus projetos? Basta importá-la como se fosse um módulo Python e chamar seus métodos.

Outros casos de uso para Jython podem ser protótipos de aplicações explorando bibliotecas Java, implantação de aplicações Django em containers Java como Tomcat/Jboss/Glassfish, usando recursos desses ambientes e integrando com outras aplicações Java já existentes, e por último mas não menos importante: tornar sua aplicação Java extensível permitindo que os usuários adicionem comportamento escrevendo scripts em Python.

Mais informações

Obrigado ao Elias Dorneles pela colaboração!

Pegadinha em valores default em funções

Se você é desenvolvedor Python, já deve ter visto muitas funções que definem valores padrão para alguns de seus argumentos. Por exemplo:

def power(base, exp=2):
    return base**exp

É bem comum usarmos esse artifício quando queremos que determinado parâmetro tenha um valor mesmo que o chamador não tenha passado valor algum para ele. Isso funciona muito bem, porém, pode gerar uma certa confusão quando o tipo do valor default do parâmetro em questão for mutável. Veja o trecho de código abaixo:

def func(valor, seq=[]):
    seq.append(valor)
    return seq

print func(10)
print func(20)
print func(30)

Antes de executar o código acima, responda: o que será impresso pelo código acima?

Se você respondeu

[10]
[20]
[30]

você está errado, pois o resultado é:

[10]
[10, 20]
[10, 20, 30]

Observe que, na segunda chamada à função func(), a lista seq manteve o valor 10 como elemento e então adicionou o valor 20 ao seu final. Mas por quê, se seq possui uma lista vazia [] como valor default? Este valor não deveria ser atribuído a seq a cada chamada de função?

A resposta é, como tudo em Python, consistente com a linguagem. Em Python, valores default de parâmetros são avaliados somente no momento em que a função estiver sendo avaliada (em sua definição), e não a cada chamada à mesma.

Ao encontrar a palavra-chave def, o interpretador Python avalia a expressão seguinte como uma função e então cria em memória um objeto function referente à função definida. Assim, a atribuição seq=[] é feita pelo interpretador nesse momento, e não a cada vez que func for chamada.

Vamos ver o que acontece: quando func é chamada pela primeira vez, a lista referenciada por seq possui o valor []. Então, dentro da função é feito um append em seq do valor recebido como parâmetro. Como seq é uma referência para um objeto mutável (uma lista), a lista referenciada por seq é quem tem adicionada a si o valor passado como parâmetro. Na chamada seguinte, seq continua apontando para o mesmo objeto lista, que agora possui um elemento (o valor 10) e então o valor 20 será, dentro da função, adicionado ao final da lista referenciada por seq. Na última chamada, é adicionado o valor 30 ao final da lista apontada por seq.

O que deve ser lembrado sempre é que valores default para parâmetros em uma função são avaliados somente na definição da mesma, e não a cada chamada.

Como driblar isso?

Se você precisar que um parâmetro tenha o valor [] quando o chamador não passar valor algum a ele, você pode fazer o seguinte:


def func(valor, seq=None):
    if seq is None:
        # chamador não forneceu valor para seq
        seq = []
    seq.append(valor)
    return seq

Leia mais

Detalhes como o apresentado neste post são tratadas em alguns livros, como os excelentes:

Obrigado ao Elias Dorneles pela revisão!

Seguir

Obtenha todo post novo entregue na sua caixa de entrada.

Junte-se a 62 outros seguidores

%d blogueiros gostam disto: