Python 2 + 3 = Six

(Esse post é relacionado com a apresentação que eu fiz no dia 19 de novembro no TchêLinux. Os slides podem ser encontrados na área de apresentações .)

Antes de mais nada, uma coisa que precisamos responder é: Porque alguém usaria Python 3 [1]?

  • Todas as strings são unicode por padrão; isso resolve a pilha de problemas macabros, chatos, malditos, desgraçádos do UnicodeDecodeError;
  • Mock é uma classe padrão do Python; ainda é possível instalar usando pip e a sintaxe é exatamente igual, mas é uma dependência a menos;
  • Enum é uma classe padrão do Python; Enum é um dos abusos mais interessantes de classes em Python e realmente útil;
  • AsyncIO e toda a parte de lazy-evaluation que o Python 3 trouxe; muita coisa no Python 3 deixou de ser "gerar uma lista" para ser um retorno de um iterador ou um generator; com AsyncIO, tem-se um passo a frente nessa idéia de geração lazy das coisas e, segundo pessoas mais inteligentes que eu, com PyUV, o Python consegue ser tão ou mais rápido que o Node;
  • E, principalmente, o suporte ao Python 2 termina em 2020!

O último ponto é o mais importante. Você pode pensar "mas ainda tem três anos até lá", mas natal está chegando, daqui a pouco é carnaval e, quando menos se espera, é 2020.

O caminho para Python 3

Quem quiser já começar a portar seus aplicativos para Python 3, existem duas formas:

A primeira é executar seus aplicativos com python -3 [script]; isso irá fazer com que o interpretador Python avise quando qualquer instrução de código que ele não consiga converter corretamente seja alertado. Eu executei um script pessoal com data de 2003 e o Python não apresentou nada [2]. Existem vários motivos pra isso:

  1. As pessoas se acostumaram a escrever código "Pythonico"; a linguagem em si não sofreu grandes alterações.
  2. Apesar da linguagem Python ter algumas coisas removidas, essas foram lentamente reintroduzidas na linguagem; um exemplo é o operador de interpolação de strings (%) que havia sido removido em favor do str.format mas acabou voltando.

A segunda forma para portar seu código para Python 3 é usar a ferramenta 2to3. Ela irá verificar as alterações conhecidas para Python 3 (por exemplo, a transformação de print para função, a alteração de alguns pacotes da STL) e ira apresentar um patch para ser aplicado depois.

Entre as conversões que o 2to3 irá fazer, está a troca de chamadas de iter-alguma-coisa para a versão sem o prefixo (por exemplo, iteritems() irá se tornar simplesmente items()); print será convertido para função; serão feitos vários ajustes nas chamadas das bibliotecas urllib e urlparse (estas duas foram agrupadas no Python 3 e a primeira teve várias reorganizações internas); xrange passa a ser range; raw_input agora se chama input e tem um novo tratamento de saída, entre outros.

Existe apenas um pequeno problema nessa conversão de Python 2 para Python 3: Como pode ser visto na lista acima, alguns comandos existem nas duas versões, mas com funcionalidades diferentes; por exemplo, iteritems() é convertido para simplesmente items(), mas os dois métodos existem em Python 2: o primeiro retorna um iterador e o segundo retorna uma nova lista com as tuplas de todos os elementos do dicionário (no caso do Python 3, é retornado um iterador). Assim, apesar do código ser gramaticalmente igual tanto em Python 2 quanto Python 3, semanticamente os dois são diferentes.

Esse problema de "comandos iguais com resultados diferentes" pode ser um grande problema se o sistema está sendo executado em ambientes que não permitem modificação fácil -- por exemplo, o mesmo é executando num Centos 4 ou ainda necessita compabilidade com Python 2.6, ambos "problemas" sendo, na verdade, requisitos do grupo de infraestrutura.

Six (e __future__) ao Resgate

Para resolver o problema de termos código que precisa executar nas duas versões, existe a biblioteca Six; ela faz o "meio de campo" entre Python 2 e Python 3 e fornece uma interface para que código Python 2 seja portado para Python 3 mantendo a compatibilidade.

Num exemplo (relativamente idiota):

import collections

class Model(object):
    def __init__(self, word):
        self._count = None
        self.word = word
        return

    @property
    def word(self):
        return self._word

    @word.setter
    def word(self, word):
        self._word = word
        self._count = collections.Counter(word)

    @property
    def letters(self):
        return self._count

    def __getitem__(self, pos):
        return self._count[pos]

if __name__ == "__main__":
    word = Model('This is an ex-parrot')
    for letter, count in word.letters.iteritems():
        print letter, count

Nesse exemplo, temos uma classe que guarda uma frase e a quantidade de vezes que cada letra aparece, utilizando Counter para fazer isso (já que Counter conta a quantidade de vezes que um elemento aparece em um iterável e strings são iteráveis).

Nesse exemplo, temos os seguintes problemas:

  1. class Model(object): em Python 3, todas as classes são "new class" e o uso do object não é mais necessário (mas não afeta o funcionamento da classe);
  2. for letter, count in word.letter.iteritems() Conforme discutido anteriormente, iteritems() deixou de existir e passou a ser items(); items() existe no Python 2, mas a funcionalidade é diferente. No nosso caso aqui, o resultado da operação continua sendo o mesmo, mas o consumo de memória irá subir cada vez que a chamada for feita.
  3. print leter, count: print agora é uma função e funciona levemente diferente da versão com Python 2.

Então, para deixar esse código compatível com Python 2 e Python 3 ao mesmo tempo, temos que fazer o seguinte:

class Model(object)
Não é preciso fazer nada.
print letter, count
from __future__ import print_function
print('{} {}'.format(letter, count))

print como função pode ser "trazido do futuro" usando o módulo __future__ (apenas disponível para Python 2.7); como a apresentação de várias variáveis não é recomenando usando-se vírgulas, usar o str.format é a forma recomendada.

Uma opção melhor (na minha opinião) é:

from __future__ import print_function
print('{letter} {count}'.format(letter=letter
                                count=count))

Assim, os parâmetros usados na saída são nomeados e podem ser alterados. Isto gera um erro estranho quando um nome usado na string de formato não for passada na lista de parâmetros do format, mas em strings mais complexas, o resultado é mais fácil de ser entendido (por exemplo, eu acho mais fácil entender {letters} aparece {count} vezes do que {} aparece {} vezes; ainda, é possível mudar a ordem das variáveis na string de formato sem precisar alterar a ordem na lista de parâmetros).

Uma opção melhor ainda é:

import six
six.print_('{letter} {count}'.format(letter=letter,
                                     count=count))

Com Six, remove-se a dependência com __future__ e assim pode-se usar o mesmo código em Python 2.6.

for letter, count in word.letters.iteritems():
import six
for letter, count in six.iteritems(word.letters):

Six provê uma interface unificada para iterador de itens tanto em Python 2 quanto Python 3: six.iteritems() irá chamada iteritems() se estiver rodando em Python e items() se estiver rodando com Python 3.

E, assim, nosso código relativamente idiota agora é compatível com Python 2 e Python 3 roda de forma idêntica nos dois.

Mas vamos para um exemplo real:

import urllib
import urlparse

def add_querystring(url, querystring, value):
         frags = list(urlparse.urlsplit(url))
         query = frags[3]
         query_frags = urlparse.parse_qsl(query)
         query_frags.append((querystring, value))
         frags[3] = urllib.urlencode(query_frags)
         return urlparse.urlunsplit(frags)

if __name__ == "__main__":
         print add_querystring('http://python.org', 'doc', 'urllib')
         print add_querystring('http://python.org?doc=urllib',
                                                                  'page', '2')

Esse é um código de uma função utilizada para adicionar uma query string em uma URL [3]. O problema com essa função é que tanto urlib quanto urlparse sofreram grandes modificações, ficando, inclusive, sob o mesmo módulo (agora é tudo urllib.parse).

Para fazer esse código ficar compatível com Python 2 e 3 ao mesmo tempo, é preciso usar o módulo six.moves, que contém todas essas mudanças de escopo das bibliotecas da STL (incluindo, nesse caso, a urllib e urlparse).

import six

def add_querystring(url, querystring, value):
         frags = list(six.moves.urllib.parse.urlsplit(url))
         query = frags[3]
         query_frags = six.moves.urllib.parse.parse_qsl(query)
         query_frags.append((querystring, value))
         frags[3] = six.moves.urllib.parse.urlencode(query_frags)
         return six.moves.urllib.parse.urlunsplit(frags)

if __name__ == "__main__":
         six.print_(add_querystring('http://python.org', 'doc', 'urllib'))
         six.print_(add_querystring('http://python.org?doc=urllib',
                                                                                 'page', '2'))

O que foi feito, aqui, foi usar six.moves.urllib.parse. Essa estrutura não vêm por acaso: no Python 3, as funções de urlparse agora se encontram em urllib.parse; Six assumiu que a localização correta para as funções dentro "de si mesma" seriam os pacotes utilizados no Python 3.

E, assim, temos dois exemplos de programas que conseguem rodar de forma igual tanto em Python 3 quanto Python 2.

Ainda, fica a dica: Se houver algum software que você utiliza que não roda corretamente com Python 3, utilizar o Six pode ajudar a manter o código atual até que uma escrita resolva o problema.

Outras Perguntas

Como fica a questão de ficar sempre com o Six?
Boa parte das aplicações hoje botaram uma "quebra" do suporte às suas versões que rodam em Python 2. Por exemplo, Django anunciou que em 2020 vai sair a versão 2.0 do framework e essa versão vai suportar Python 3 apenas.
Quão difícil é portar para Python 3?
Não muito difícil -- agora. Muitas das coisas que foram removidas que davam dor de cabeça na conversão retornaram; o caso mais clássico é o que operador de interpolação de strings %, que foi removido e teria que ser substituído por str.format, mas acabou retornando. Outro motivo é que os scripts são mais "pythônicos" atualmente, muito por causa de gente como Raymond Hettinger, que tem feito vídeos excelentes de como escrever código em Python com Python (ou seja, código "pythônico"). E, como anedota pessoal, eu posso comentar que meu código de 2003 rodou com python -3 sem levantar nenhum warning.
[1]Existe ainda a interpolação de strings com o novo identificador f; a funcionalidade é semelhante à chamada str.format usando locals(), por exemplo, f'{element} {count} é equivalmente à '{element} {count}'.format(locals()) (desde que você tenha element e count como variáveis locais da sua função).
[2]Apenas para fins de melhor elucidação: o código que eu estava gerando já estava mais correto e seguindo os padrões mais pythônicos; em 2014 eu ainda estava vendo casos em que código rodando em Python 2.6 ainda usava has_keys(), que foi deprecado no Python 2.3.
[3]Sim, sim, o código poderia ser um simples "verificar se tem uma interrogação na URL; se tiver, adicionar & e a query string; se não tiver, adicionar ? e a query string". A questão é: dessa forma, eu consigo fazer uma solução que vai aceitar qualquer URL, em qualquer formato, com qualquer coisa no meio porque as bibliotecas do STL do Python vão me garantir que a mesma vai ser parseada corretamente.
Go Top