Simplificando as coisas com namedtuples

Antes de conhecer e até mesmo nos primeiros projetos que desenvolvi usando Python, era bem comum que eu escrevesse classes que não continham método algum (a não ser os famigerados getters/setters e o construtor). Elas eram normalmente usadas para representar objetos que tinham um papel mais passivo, como por exemplo, uma classe para representar as mensagens que o cliente passava para o servidor:

class Message(object):
    headers = []
    content = None
    from_addr = None
    to_addr = None

Ou então:

class Message(object):
    def __init__(self, content, from_addr, to_addr):
        self.headers = []
        self.content = content
        self.from_addr = from_addr
        self.to_addr = to_addr

    def add_header(self, info):
        self.headers.append(info)

Além disso, eu definia os setters/getters para cada atributo (muitas vezes sem pensar se iria precisar deles ou não, afinal, a maioria das IDEs já geravam tudo pra mim).

Mas aos poucos veio a sensação de que eu estava subutilizando Python criando classes somente para representação de objetos simples assim. Afinal, além de ser uma linguagem com tipagem dinâmica, Python ainda oferece várias estruturas para representação dos dados. Minha primeira idéia foi utilizar tuplas para representar os objetos a serem trocados entre os elementos. Por exemplo, poderia representar uma mensagem através de uma tupla de 4 elementos:

message = ([], content, from_addr, to_addr)

Assim, para imprimir o conteúdo da mensagem, eu teria que fazer:

print message[1]

Assim, seria sempre necessário saber em qual posição da tupla determinado campo está armazenado. Um atentado à legibilidade e manutenção do código! Depois, pensei em usar dicionários:

message = {'headers': [], 'content': content, 'from_addr': from_addr, 'to_addr': to_addr}

Feito! O problema da legibilidade tinha acabado. Agora eu poderia acessar o campo content usando o nome do campo como chave:

print message['content']

Mas, ainda não me parecia uma solução muito adequada. Foi então que veio a luz: descobri a namedtuple, que faz parte do pacote collections. O próprio Guido Van Rossum, criador da linguagem Python, recomendou recentemente o uso de namedtuples para representação de objetos. Além de evitar a sobrecarga sintática que a definição de uma classe dá ao código, ela também oferece melhor desempenho.

Usando as namedtuples

A definição de estruturas para representação de objetos com namedtuples é bem simples. Veja o exemplo abaixo:

>>> import collections
>>> Message = collections.namedtuple('Message', 'headers content from_addr to_addr')

A segunda linha acima mostra a criação de uma namedtuple que chamamos de Message, e que possui 4 campos: headers, content, from_addr e to_addr. O primeiro argumento que passamos para a factory namedtuple() é o nome para o novo tipo (em nosso caso, Message) e o segundo argumento é uma string contendo os nomes dos campos da estrutura, separados por espaços em branco ('headers content from_addr to_addr'). A chamada à collections.namedtuple() retorna uma nova subclasse de tuple, podendo assim ser usado como se fosse um novo tipo.

Tendo a estrutura definida, podemos então criar objetos do tipo Message da seguinte forma:

>>> m = Message([], "Hello, server!", "me", "some.address.com")

A partir daí, o acesso aos campos da mensagem é feito exatamente como faríamos com instâncias de classes que definimos:

>>> print m.content
Hello, server!
>>> m.headers.append("alguma informação")
>>> print m.headers
["alguma informação"]
>>> m.to_addr = "localhost"
>>> print m.to_addr
localhost

Barbadinha, né? A partir de agora, quando for criar uma nova classe para representação de algo em seu projeto, verifique se não é o caso de definir o novo tipo usando namedtuples ao invés de uma nova classe. Se for possível, use a namedtuple. Mas, nem sempre uma namedtuple será suficiente para representar determinados objetos. Nesses casos, as classes são a solução mais adequada.

Desempacotamento de tupla

Tuple unpacking, ou em bom português, desempacotamento de tupla. Tá aí algo que usamos pra caramba, mas sobre o qual pouco ouvimos falar. Desempacotar uma tupla pode ser explicado como o ato de atribuir os elementos dela individualmente a outros objetos. Veja um exemplo de desempacotamento de tupla:

>>> t = (1, 2, 3, 4)
>>> a, b, c, d = t  # "desempacotando" uma tupla
>>> print b * c
6

No exemplo acima, o primeiro elemento da tupla t é atribuído para o nome a. O segundo elemento de t é atribuído para b, e assim por diante. É óbvio que, para desempacotar uma tupla, é necessário que tenhamos no lado esquerdo da expressão a quantidade necessária de variáveis para receber os valores. Veja uma situação que gera um erro por tentarmos desempacotar uma tupla de 4 elementos para 3 variáveis:

>>> t = (1, 2, 3, 4)
>>> a, b, c = t
Traceback (most recent call last):
  File "", line 1, in
ValueError: too many values to unpack

"ValueError: too many values to unpack", que significa “valores demais para desempacotar".

Mas isso é útil para quê?

Já viu uma função retornando mais de um valor em um único return em Python?

def f(x):
    return x, x*2, x*3

valor, dobro, triplo = f(2)

Isso é algo bastante comum em código Python. O código da segunda linha (return x, x*2, x*3) na realidade retorna apenas um valor, que é uma tupla. A expressão x, x*2, x*3 cria uma tupla de 3 elementos, apesar de não estar envolta em parênteses. Assim sendo, a chamada de função f(2) retorna uma tupla de 3 valores, que são então desempacotados para as variáveis valor, dobro e triplo, respectivamente.

Percorrendo listas de tuplas

Quando temos uma lista contendo tuplas, podemos percorrê-la assim como qualquer outra lista. Veja:

>>> lista = [(1, 2, 3), (2, 4, 6), (4, 8, 12), (8, 16, 24)]
>>> for tupla in lista:
...     print tupla
...
(1, 2, 3)
(2, 4, 6)
(4, 8, 12)
(8, 16, 24)

Dentro do for, poderíamos desempacotar os valores de cada tupla, se precisássemos usá-los individualmente:

>>> lista = [(1, 2, 3), (2, 4, 6), (4, 8, 12), (8, 16, 24)]
>>> for tupla in lista:
...     a, b, c = tupla
...     print a * b * c
...
6
48
384
3072

Mas, poderíamos fazer melhor, desempacotando os elementos dentro da expressão do for:

>>> lista = [(1, 2, 3), (2, 4, 6), (4, 8, 12), (8, 16, 24)]
>>> for (a, b, c) in lista:
...     print a * b * c
...
6
48
384
3072

Poderíamos também omitir os parênteses na hora de desempacotar as tuplas:

>>> lista = [(1, 2, 3), (2, 4, 6), (4, 8, 12), (8, 16, 24)]
>>> for a, b, c in lista:
...     print a * b * c
...
6
48
384
3072

Mas nem tudo interessa

Considere que nas tuplas da lista lista acima, somente nos interessa o primeiro elemento de cada tupla. Para não precisar criar dois novos nomes (b e c) que nunca serão usados, podemos utilizar um idioma que é um tanto comum em código python: usar o caractere de undescore _ como o nome da variável que vai receber o primeiro e último valores das tuplas:

>>> lista = [(1, 2, 3), (2, 4, 6), (4, 8, 12), (8, 16, 24)]
>>> for a, _, _ in lista:
...     print a * 2
...
2
4
8
16

Lembre-se: usamos isso somente em casos em que os elementos não serão necessários no escopo do for.

O _ é utilizado comumente como um nome para não me interessa. Por exemplo*, em uma tupla contendo nome, sobrenome e apelido, em um momento em que estamos interessados apenas no nome e no sobrenome, poderíamos usar o _ para indicar que o apelido não é importante neste momento:

dados_pessoais = ('Joao', 'Silva', 'joaozinho')
...
nome, sobrenome, _ = dados_pessoais

*Exemplo adaptado dessa resposta no StackOverflow.com

Como o autor da resposta no link acima comenta, é preciso tomar cuidado com o uso do _, pois ele também é utilizado em outros contextos e isso pode gerar confusão e mau-funcionamento. Ele é bastante usado com a biblioteca de internacionalização gettext como um atalho para funções usadas com muita frequência.

Então é isso. O desempacotamento de tuplas é um dos recursos que eu considero mais legais em Python, pois elimina bastante código desnecessário. Até a próxima! 🙂