Dicas para lidar com JSON

Você já deve ter descoberto como funciona o formato JSON — muito usado para trocar informações entre aplicações Web, como já foi mostrado aqui no blog anteriormente. Hoje vamos mostrar algumas dicas para facilitar sua vida quando estiver lidando com esse formato.

1) Use python -mjson.tool para formatar saídas JSON na linha de comando.

Às vezes cai no nosso colo um conteúdo JSON não-formatado, com todo o conteúdo em uma linha só, algo parecido com isso:

{"assunto":"Dicas para lidar com JSON","metadados":{"data":"07/07/2013 14:10","site":"https://pythonhelp.wordpress.com","numero_acessos":3},"conteudo":"Voc\u00ea j\u00e1 deve ter descoberto como funciona o formato JSON -- muito usado para trocar informa\u00e7\u00f5es entre aplica\u00e7\u00f5es Web..."}

Geralmente, trata-se da resposta de uma API, que é “limpado” para economizar alguns bytes e por conseguinte, reduzir o uso de banda do servidor. Até aí tudo bem, o problema é que fica bem mais complicado de ver a estrutura dos dados retornados. Fear not! O módulo json da API padrão de Python contém uma ferramenta para você formatar os resultados diretamente na linha de comando. Se o conteúdo acima estiver dentro de um arquivo com o nome post.json, você pode fazer:

$ python -m json.tool post.json
{
    "assunto": "Dicas para lidar com JSON",
    "conteudo": "Voc\u00ea j\u00e1 deve ter descoberto como funciona o formato JSON -- muito usado para trocar informa\u00e7\u00f5es entre aplica\u00e7\u00f5es Web...",
    "metadados": {
        "data": "07/07/2013 14:10",
        "numero_acessos": 3,
        "site": "https://pythonhelp.wordpress.com"
    }
}

That’s cool, right?

Se você é que nem eu, provavelmente vai querer colocar um alias (apelido ou atalho) no ~/.bashrc, para ficar ainda mais fácil:

$ echo "alias jsonfmt='python -mjson.tool'" >> ~/.bashrc
$ source ~/.bashrc
$ echo "[1, 2, 3]" | jsonfmt
[
    1,
    2,
    3
]
$ curl -s http://api.joind.in | jsonfmt
{
    "events": "http://api.joind.in/v2.1/events",
    "hot-events": "http://api.joind.in/v2.1/events?filter=hot",
    "open-cfps": "http://api.joind.in/v2.1/events?filter=cfp",
    "past-events": "http://api.joind.in/v2.1/events?filter=past",
    "upcoming-events": "http://api.joind.in/v2.1/events?filter=upcoming"
}

2) Estenda json.JSONEncoder para converter objetos em JSON:

Como você já sabe, é fácil converter dicionários Python no formato JSON. Mas e no caso de variáveis de classes que você mesmo definiu?

Observe esse exemplo:

import json, datetime

class BlogPost:
    def __init__(self, titulo):
        self.titulo = titulo
        self.data = datetime.datetime.now()

post = BlogPost('Dicas para lidar com JSON')

Se tentarmos fazer:

print json.dumps(post)

obtemos um erro parecido com:

TypeError: <__main__.BlogPost instance at 0x1370ab8> is not JSON serializable

Isto é porque o método json.dumps não sabe converter objetos do tipo BlogPost em strings no formato JSON. Felizmente, o método json.dumps permite que você informe um “encodificador” JSON alternativo, de forma que você pode customizar a geração do resultado JSON para permitir a conversão de outros objetos:

class BlogPostEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, BlogPost):
            return {'titulo': obj.titulo, 'data': obj.data}
        if isinstance(obj, datetime.datetime):
            return obj.isoformat()
        return json.JSONEncoder.default(self, obj)

print json.dumps(post, cls=BlogPostEncoder)

Agora sim, funciona:

{"titulo": "Dicas para lidar com JSON", "data": "2013-07-07T16:26:40.636950"}

Minha sugestão é usar um encodificador mais genérico, que permita converter em JSON qualquer objeto que implemente um método to_json, segue um exemplo completo:

import json, datetime

class Site:
    def __init__(self, url):
        self.url = url
    def to_json(self):
        return {"url": self.url}

class BlogPost:
    def __init__(self, titulo, site):
        self.titulo = titulo
        self.data = datetime.datetime.now()
        self.site = site
    def to_json(self):
        return {"titulo": self.titulo, "data": self.data.isoformat(), "site": self.site}

class GenericJsonEncoder(json.JSONEncoder):
    def default(self, obj):
        if hasattr(obj, 'to_json'):
            return obj.to_json()
        if isinstance(obj, datetime.datetime):
            return obj.isoformat()
        return json.JSONEncoder.default(self, obj)

print json.dumps(Site("http://www.google.com.br"), cls=GenericJsonEncoder)
post = BlogPost('Dicas para lidar com JSON', Site('https://pythonhelp.wordpress.com'))

print json.dumps(post, cls=GenericJsonEncoder)

Note que você não precisa de nada disso se estiver usando o framework Django e tentando serializar uma instância de um modelo. Nesse caso, basta usar o mecanismo de serialização para XML e JSON do próprio Django.

3) Para necessidades mais complexas de serialização, mude a estratégia

Se você precisar converter objetos em JSON e vice-versa para coisas mais complicadas que o nosso exemplo, você pode considerar usar o módulo jsonpickle. Este módulo consegue converter quase qualquer objeto Python em JSON e vice-versa, usando uma representação própria baseada no módulo pickle para serialização de objetos. Essa representação própria acaba tendo algumas desvantagens, porque nem sempre o JSON gerado fica legível ou facilmente tratado no caso de outras linguagens, a portabilidade é garantida praticamente só para outras aplicações Python.

Por isso, se a serialização e deserialização de objetos complicados for um ponto muito importante para o seu projeto, considere usar outros formatos — JSON pode não ser o formato mais adequado. (UPDATE: aqui tem uma visão geral sobre alternativas de serialização em Python).

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