Dicas de produtividade no IPython

O IPython é o meu shell Python favorito. É cheio de recursos que facilitam o dia-a-dia de quem passa parte de sua vida imerso em um shell Python. Neste post, vou listar alguns recursos que me ajudam bastante diariamente.

Recuperando o resultado da última operação

É coisa bem comum estar em uma sessão no shell Python, testar uma determinada operação e logo depois perceber que queria atribuir o resultado daquela operação para alguma variável. No IPython é barbada, com o _:

In [5]: 10 * 2 + 4 * 4
Out[5]: 36
In [6]: print _
36
In [7]: x = _
In [8]: print x
36

Ou seja, o resultado da execução do último comando é sempre referenciado pelo _ (underscore). Além disso, dois underscores referem-se sempre ao resultado obtido pela execução do penúltimo comando e três underscores ao resultado do antepenúltimo comando. Assim:

  • _: resultado do último comando.
  • __: resultado do penúltimo comando.
  • ___: resultado do antepenúltimo comando.

Isso é particularmente útl quando estamos imersos em uma sessão de descoberta usando o IPython. Torna o processo muito mais ágil. Veja:

In [18]: 1
Out[18]: 1
In [19]: 2
Out[19]: 2
In [20]: 3
Out[20]: 3
In [21]: _ + __ + ___
Out[21]: 6

Além disso, podemos nos referir à execuções específicas, usando a numeração que o IPython usa para diferenciar um par entrada-saída de outro. A sintaxe é bem simples: _ix, onde x é o número da entrada correspondente. Veja:

In [18]: 1
Out[18]: 1
In [19]: 2
Out[19]: 2
In [20]: 3
Out[20]: 3
In [21]: _ + __ + ___
Out[21]: 6
In [22]: print _i19 + 20
Out[22]: 22

Cool, huh?

Chamando o help de um objeto

Já falei sobre isso em um post anterior, mas veja de novo o uso do ponto de interrogação (?) para ver a documentação relacionada a determinado objeto:

In [31]: import math
In [32]: math.sqrt?
Type: builtin_function_or_method
String Form:<built-in function sqrt>
Docstring:
sqrt(x)
Return the square root of x.

Isso por si só já me faz usar o IPython ao invés do shell padrão.

As funções mágicas

O IPython é repleto de funções mágicas (magic functions) que fornecem enormes facilidades pro usuário. Elas são precedidas pelo caractere %. Vamos ver alguns exemplos.

Em um post anterior, falei sobre o módulo timeit que é usado para medir tempo de execução de programas Python. Existe uma função mágica pra quebrar esse galho pra gente. Por exemplo, se eu estiver na dúvida sobre qual trecho de código executaria de forma mais rápida, poderia fazer o seguinte:

In [35]: %%timeit sum = 0
 ...: for i in range(0, 10000):
 ...: sum += i
 ...: 
1000 loops, best of 3: 324 us per loop
In [36]: %%timeit sum = 0
 ...: for i in xrange(0, 10000):
 ...: sum += i
 ...: 
1000 loops, best of 3: 268 us per loop

Se o código a ser testado for de uma linha só, podemos usar %timeit (modo linha) ao invés de %%timeit (modo célula).

Outra função interessante é relacionada ao logging da sessão atual. %logstart faz com que a sessão atual passe a ser gravada em um arquivo .py com os comandos digitados dentro dele. %logon e %logoff servem para pausar e recomeçar o logging em uma sessão.

Executando comandos no sistema

Dentro de uma sessão IPython, podemos invocar comandos no sistema operacional usando o caractere !:

In[1]: !ls
 bin
 Desktop
 music
 pictures
 src
 videos
In[2]: !ps
 PID TTY TIME CMD
 25277 pts/0 00:00:00 bash
 25446 pts/0 00:00:00 ipython
 25458 pts/0 00:00:00 sh
 25459 pts/0 00:00:00 ps

Assim fica fácil interagir com o SO quando necessário for.

É isso. Se você tiver alguma outra dica interessante sobre o IPython, poste nos comentários.

Atributos em objetos do tipo function

Em um post anterior, nós vimos porque se diz que “tudo em Python é objeto”. Agora, vamos ver uma consequência interessante desse modelo.

Você já deve saber que o comando def é o comando de criação de um objeto do tipo function em Python. Ao interpretar o código abaixo

def funcao_qualquer():
    a = 0
    return a

o interpretador Python cria em memória um objeto chamado funcao_qualquer do tipo function. Isso ocorre da mesma maneira que o código abaixo cria um objeto do tipo int chamado x:

x = 42

A diferença entre o objeto function e o objeto int é que o primeiro é um objeto chamável (callable) e o segundo não. Dessa forma, o objeto function contém código executável dentro de si próprio. Vamos dar uma olhadinha no que temos dentro desse objeto:

>>> dir(funcao_qualquer)
['__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__doc__', '__format__', '__get__', '__getattribute__', '__globals__', '__hash__', '__init__', '__module__', '__name__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'func_closure', 'func_code', 'func_defaults', 'func_dict', 'func_doc', 'func_globals', 'func_name']

Sabendo dos campos e métodos disponíveis em objetos function, poderíamos fazer o seguinte (só para exemplificar):

>>> print funcao_qualquer.func_code.co_varnames
('a', )

Mas, mais do que acessar os atributos já existentes em um objeto function, nós também poderemos adicionar atributos a esses objetos. Por exemplo, se quisermos que a função que criamos (o objeto function) possua um contador indicando quantas vezes já foi chamada desde que foi criada, poderíamos defini-la da seguinte forma:

def minha_funcao():
    minha_funcao.contador_de_chamadas += 1
    # faça algo
    return 42
minha_funcao.contador_de_chamadas = 0

Assim, não precisamos criar variáveis globais para representar a contagem, e nem poluir a interface da função com parâmetros desnecessários. Cada vez que minha_funcao for chamada, o atributo interno contador_de_chamadas desse objeto será incrementado. Veja:

for i in range(0, 100):
    minha_funcao()
print minha_funcao.contador_de_chamadas  # imprime 100

Podemos até mesmo “pendurar” uma função como atributo de outra função:

>>> def f(x): return x*x
>>> minha_funcao.f = f
>>> minha_funcao.f(10)
100

Ou, usando uma função anônima:

>>> def f(): return 1
>>> minha_funcao.f = lambda x: x*x
>>> minha_funcao.f(9)
81

Dessa forma, podemos manipular nossas funções, isto é, nossas variáveis do tipo função, da mesma maneira que manipulamos variáveis do tipo lista, dicionário, string, etc. Isso porque elas realmente se tratam de um objeto como qualquer outro.

E lembre-se: com grandes poderes, vêm grandes responsabilidades. 🙂

Obrigado ao @eliasdorneles pela revisão.

Pegadinha com funções e variáveis globais

Diga aí, o que você acha que o código abaixo irá imprimir na tela?

def func():
    print x

x = 42
func()

A resposta é óbvia: 42. OK, sem pegadinhas por enquanto. E o código abaixo, o que irá imprimir?

def func():
    print x
    x = 1

x = 42
func()

Pelo que sabemos até então, somos levados a crer que o código acima irá imprimir 42 na tela, correto? Mas, veja a saída que recebemos na cabeça ao executar o código:

Traceback (most recent call last):
File "<pyshell#3>", line 6, in 
    func()
File "<pyshell#3>", line 2, in func
    print x
UnboundLocalError: local variable 'x' referenced before assignment

wat

UnboundLocalError significa que estamos acessando uma variável antes dela ter sido criada no escopo da função func(). A linha que está gerando o erro é a linha 2, que contém print x. Entretanto, observe com cuidado os dois exemplos de código acima. No primeiro deles também estamos fazendo print x na primeira linha da função. Inclusive, o valor impresso pela função foi o valor da variável x definida no escopo global.

A culpada por esse erro é a linha 3 (x = 1) e a explicação é simples: o código da função é analisado e x é definido estaticamente pelo compilador como uma variável (ou melhor, como um nome) local ao escopo da função quando este encontra a atribuição x = 1. E isso vale para a função inteira, mesmo que a atribuição ocorra somente na última linha dela. Essa análise é feita estaticamente durante a criação da função func, quando o compilador encontra a declaração de criação de função def. Assim, mais tarde quando func() é chamada, print x tenta imprimir o valor de uma variável local antes dela ter algum valor atribuído a si.

Existe uma regra bem simples:

Sempre que o compilador encontra uma atribuição a um nome dentro de uma função, ele assume que tal nome é local em toda a função, não importando onde a atribuição ocorre.

O código a seguir funciona

def func():
    x = 1
    print x

x = 42
func()

mas imprime 1, pois ao encontrar x = 1, o compilador assume que x é uma variável local. Mas e se precisarmos alterar o valor do x global dentro da função? Antes de qualquer coisa, vamos ver se o x global (de valor 42, inicialmente) é alterado:


def func():
    x = 1
    print x

x = 42
func()
print x

O programa acima irá imprimir:

1
42

Ou seja, o x global não foi alterado. Se quiser mesmo alterar o valor de uma variável global dentro de uma função, Python exige que identifiquemos a mesma como global dentro da função. Isso pode ser feito com a estranhíssima construção global:


def func():
    global x
    x = 1
    print x

x = 42
func()
print x

O código acima imprime:

1
1

Ou seja, a global x foi alterada pela atribuição feita dentro de func().

ALERTA: criar variáveis globais e alterar o valor delas dentro de funções é uma péssima prática.  O código fica mais difícil de ler e modificar (você precisa saber todos os lugares onde a variável global está sendo usada) e pode levar a bugs bem difíceis de serem encontrados. Não é por menos que Python força o programador a explicitar a referência a uma variável global quando quiser alterá-la fora do escopo global. Soa quase como uma autodeclaração do programador: “sim, eu sou tosco e estou prestes a fazer mer** no meu código”. 😀

O que mudou no Python 3?

Uma das primeiras coisas que alguém que vai testar o Python 3.x ao invés do Python 2.x percebe é a mudança do print(), que deixou de ser um comando e passou a ser uma função. O primeiro impacto disso é que o tradicional “Hello, world!” muda. O “Hello World” com Python 3 fica:

print("Hello, world!")

Muita gente, quando pensa em mudar pro Python 3, já lembra de cara dessa diferença. Mas, existem várias outras mudanças que são bem importantes. Vou listar aqui as que considero que vão ter maior impacto na maioria dos programadores.

Operador de divisão inteira

A partir do Python 3, 3/2 resulta em 1.5, e não mais em 1. Ou seja, o operador / não é mais dependente dos tipos dos operados. No Python 2.x, o resultado de uma divisão de dois números inteiros era também um número inteiro, arredondado para baixo, sempre.

O operador // foi inserido no Python 3 para representar a divisão inteira. Assim, podemos ver alguns exemplos abaixo:

>>> 3 / 2
1.5
>>> 3 // 2
1
>>> -3 // 2
-2

(se você ficou confuso com o resultado da última expressão, clique aqui e entenda o porquê)

Assim, programas Python 2.x que dependiam do arredondamento do operador /, não irão mais funcionar corretamente na versão 3.x. Fique de olho!

True, False agora são palavras reservadas (finalmente!)

Em Python 2.x, era possível fazermos coisas bizarras como:

>>> True = "Hello"
>>> False = "Hello"
>>> True == False
True

Ou então, tão estranho quanto:

>>> False = True
>>> True == False
True

Felizmente, em Python 3 isso não é mais possível. Veja uma tentativa:

>>> True = "Hello"
SyntaxError: assignment to keyword

xrange() se foi

A função xrange() deixou de existir no Python 3. Lembra que, no Python 2, range() retornava uma lista e xrange() retornava um objeto iterável? O impacto disso era que, para grandes sequências numéricas, xrange() acabava sendo mais eficiente do que range(), pois usava muito menos espaço em memória para gerar a sequência (leia mais aqui).

No Python 3, só existe a função range(), que retorna uma espécie de iterável (assim como xrange() fazia).

map, filter e zip também mudaram

As funções map, filter e zip, assim como range(), também retornavam uma lista com os valores de resultado. A partir do Python 3, elas passaram a retornar objetos iteráveis, ao invés de gerar listas enormes em memória. Isso impacta bastante em código Python 2.x, visto que coisas simples deixam de funcionar, como o exemplo abaixo:

>>> len(map(lambda x: x*x, [1, 2, 3]))
TypeError: object of type 'map' has no len()

Isso pode ser corrigido com uma “conversão” do iterável retornado pelo map() para lista:

>>> len(list(map(lambda x: x*x, [1,2,3])))
3

E o mesmo vale para as funções zip e filter também. Ah, outra mudança forte é que a função reduce() foi “rebaixada” para o módulo functools, deixando de ser uma função global do Python.

has_key não existe mais nos dicionários

Em Python 2.x, era comum verificar se um determinado elemento já era chave em um dicionário usando o método has_key():

>>> d = {'nome': 'jose', 'idade': 18}
>>> d.has_key('nome')
True
>>> d.has_key('email')
False

Em Python 3.x, para fazer a mesma verificação, usamos o operador in:

>>> 'nome' in d
True
>>> 'email' in d
False

Ainda falando sobre dicionários, os métodos que em Python 2.x retornavam listas, agora retornam espécies de iteráveis (na verdade, são views dinâmicas, sobre as quais pretendo falar em um próximo post).

Toda string é unicode

Em Python 3.x, só existe um tipo de String: as strings unicode. Assim, não é mais preciso especificar em uma string literal que ela é do tipo unicode. Em Python 2.x, um literal unicode era declarado como:

>>> string_unicode = u"olá mundo"

(repare no ‘u’ precedendo o literal)

Em Python 3.x, isso não é preciso, pois toda string é unicode:

>>> string_unicode = "olá mundo"

Agora só existe input()

Em Python 2.x, existiam duas funções para fazer a leitura de valores do teclado: input() e raw_input(). A primeira lia expressões Python e as executava e a segunda lia strings.

Em Python 3.x, só existe uma função: input(). Ela lê uma string do teclado, que então pode ser convertida para o tipo apropriado. Por exemplo, para ler um valor numérico inteiro:

>>> idade = int(input('Digite sua idade:'))
Digite sua idade:10
>>> type(idade)
int

Por fim…

Além das modificações apresentadas acima, foram feitas inúmeras outras, principalmente na reestruturação de bibliotecas, deixando-as com interfaces mais consistentes. Até agora, achei muito interessantes as alterações feitas da versão 2 para a 3, pois elas deixaram tudo mais Pythônico e consistente.

Para portar aquele seu programa escrito usando Python 2 para Python 3, foi criada uma ferramenta bem interessante chamada de 2to3, que pega seu código legado e o transforma em código compatível com a versão 3 da linguagem. É claro que ela não faz milagre e, na maioria dos casos, é preciso intervenção manual. Mas já é uma ajuda para códigos mais simples.

Por exemplo, o programa hello.py:

print "hello, world!"

Pode ser convertido usando o 2to3:

$ 2to3 -w hello.py

O resultado é o programa hello.py, agora pronto pra versão 3:

print("hello, world!")

😀

Leia mais