The source content for blog.juliobiason.me
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

313 lines
12 KiB

+++
title = "Python 2 + 3 = Six"
date = 2016-11-21
category = "code"
[taxonomies]
tags = ["python", "six", "python 2", "python 3", "tchelinux", "pt-br"]
+++
"Six" é uma pequena biblioteca Python que pode ajudar você a passar o seu
código de Python 2 para Python 3.
<!-- more -->
{% note() %}
(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](http://presentations.juliobiason.net/python23six.html).)
{% end %}
Antes de mais nada, uma coisa que precisamos responder é: Porque alguém usaria
Python 3?
* 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!**
{% note() %}
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).
{% end %}
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](https://bitbucket.org/juliobiason/pyttracker) e o
Python não apresentou nada.
{% note() %}
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.
{% end %}
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](https://pythonhosted.org/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):
```python
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`
```python
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) é:
```python
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 é:
```python
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():`
```python
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:
```python
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')
```
{% note() %}
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.
{% end %}
Esse é um código de uma função utilizada para adicionar uma query string em
uma URL. 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`).
```python
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](https://rhettinger.wordpress.com/), 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.