O estranho caso do else em loops

Uma das coisas que me chamou a atenção quando comecei a usar Python é o else. O seu uso “natural”, para definir um caminho alternativo para um if, não tem nada demais. O que é um pouco estranho é o fato de Python aceitar else em expressões de loop como for e while. Por exemplo, o código abaixo é perfeitamente válido:

# -*- coding:utf-8 -*-
import random
segredo = random.randint(0, 10)
for tentativa in range(1, 4):
    numero = input('Digite um número entre 0 e 9 (tentativa %d de 3):' % tentativa)
    if numero == segredo:
        print('você acertou!')
        break
else:
    print('você esgotou as suas tentativas')

Perceba que o else está alinhado com o for e não com o if. Neste caso, os comandos contidos nele somente serão executados se o loop não tiver sido encerrado por um break. O mesmo vale para o else em loops while. No exemplo abaixo, o else nunca será executado pois o loop é sempre encerrado por um break:

while True:
    break
else:
    print('nunca serei!')

Confesso que sempre tive uma certa dificuldade para lembrar o significado do else nesses casos, até porque raramente me deparo com eles. Então, certo dia assisti a palestra Transforming Code into Beautiful, Idiomatic Python do Raymond Hettinger, em que ele diz em um certo momento:

O else de loops em Python deveria ser chamado ‘nobreak’.

Pronto, nunca mais esqueci o significado do else em loops! 🙂

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