4 de março de 2013

Named Tuples ao invés de classes ou dicionários

O Python 2.6 introduziu uma nova estrutura de dados, chamada namedtuple(), que pode ajudar a eliminar classes ou a deixar de usar dicionários aonde eles não deveriam ser usados.

As namedtuples são objetos imutáveis, assim como as tuplas e a grande novidade é que agora os campos têm um nome, lembrando uma struct da linguagem C, mas tendo a mesma facilidade de interação das tuplas do Python.

Vamos a um exemplo:
>>> import collections
>>> Dados = collections.namedtuple('Cadastro', 'nome, sobrenome, endereco, fones')
>>> eu = Dados(nome='Vinicius',
...            sobrenome='Assef',
...            endereco='Rua 7 de setembro, 123',
...            fones=['3322-1100', '9988-7766'])
...
>>> 
>>> type(eu) # note isso, é o nome de uma classe!
__main__.Cadastro
>>> 

Veja o que fizemos acima: criamos a variável Dados com as configurações da nossa namedtuple. Na verdade ela é do tipo type, assim como str, int, etc.

Depois, criamos nossa named tuple propriamente dita, na variável eu e o tipo dela é Cadastro, que informamos na criação da variável Dados.

Como vamos ver abaixo, a variável eu herda de tuple, ou seja, é uma tupla. Mas também é do tipo Cadastro:
>>> isinstance(eu, tuple) # eh uma tupla
True
>>> isinstance(eu, Cadastro) # tambem eh uma classe
True
>>> 

Agora vamos acessar as informações que guardamos:
>>> print(eu.nome, eu.sobrenome) # como uma classe
Vinicius Assef
>>> print(eu[0], eu[1]) # como uma tupla
Vinicius Assef
>>> print(eu._fields)
('nome', 'sobrenome', 'endereco', 'fones')
>>>
>>> for campo in eu._fields: # como uma classe
...     print('{} = {}'.format(campo, getattr(eu, campo)))
... 
nome = Vinicius
sobrenome = Assef
endereco = Rua 7 de setembro, 123
fones = ['3322-1100', '9988-7766']
>>>
>>> for conteudo in eu: # como uma tupla
...     print(conteudo)
...
Vinicius
Assef
Rua 7 de setembro, 123
['3322-1100', '9988-7766']
>>> 


Percebeu como temos poder para acessar os dados que criamos como uma classe ou como uma tupla?

Examinando como nossos dados estão guardados, vemos que eles estão dentro da classe Cadastro mesmo:
>>> repr(eu) # veja a estrutura da classe
"Cadastro(nome='Vinicius', sobrenome='Assef', endereco='Rua 7 de setembro, 123',
fones=['3322-1100', '9988-7766'])"
>>> 


Mas ao tentarmos mudar o conteúdo...
 >>> eu.nome = 'Pedro' # eh imutavel, lembra?
Traceback (most recent call last):
  File "<input>", line 1, in 
AttributeError: can't set attribute
>>> 


Não posso mudar o que já existe, mas posso criar outro com base nele:
>>> meu_irmao = eu._replace(nome='Pedro', fones=None) # agora sim
Cadastro(nome='Pedro', sobrenome='Assef', endereco='Rua 7 de setembro, 123',
fones=None)
>>> 
>>> print(eu.nome)
Vinicius
>>> print(meu_irmao.nome)
Pedro
>>> 

Transformando isso num dicionário ou numa tupla convencional:
>>> d = eu._asdict()
>>> print(d)
OrderedDict([('nome', 'Vinicius'), ('sobrenome', 'Assef'), ('endereco', 
'Rua 7 de setembro, 123'), ('fones', ['3322-1100', '9988-7766'])])
>>> isinstance(d, dict)
True
>>> t = tuple(eu)
>>> print(t)
('Vinicius', 'Assef', 'Rua 7 de setembro, 123', ['3322-1100', '9988-7766'])
>>> 

Se já tivermos uma lista, uma tupla ou um dicionário, podemos criar nossa named tuple com facilidade:
>>> lista = ['Maria', 'Souza', 'Rua das Acácias, 23', ['9876-5431']]
>>> maria = Dados._make(lista) # _make() aceita qualquer iteravel
>>> maria = Dados(*lista)
>>>
>>> d = dict(nome='Joao', sobrenome='das Couves', endereco='Av. Carlos Lima, 456',
...          fones=['3344-8877'])
>>> joao = Dados(**d)
>>> 


Com todas essas características, as named tuples nos ajudam a não criar complexidade aonde não deve existir. Por exemplo, não precisamos criar classes para implementar o padrão de projeto ValueObject, que é imutável por natureza.

Com elas, podemos melhorar a semântica de tuplas já existentes, dando significado aos dados, e evitamos usar dicionário aonde não é necessário.

As named tuples têm a mesma eficiência das tuplas, já que a semântica dos dados (os nomes dos campos) não estão em cada instância, como ocorre com os dicionários.

Você pode herdar de uma named tuple para criar métodos que agem sobre os dados de alguma maneira. Veja abaixo:
>>> class MeusDados(collections.namedtuple('MeusDados',
...                                        'nome, sobrenome, endereco, fones')):
...     @property
...     def nome_completo(self):
...         return self.nome + ' ' + self.sobrenome
... 
>>> eu = MeusDados(nome='Vinicius',
...                sobrenome='Assef',
...                endereco='Rua 7 de setembro, 123',
...                fones=['3322-1100', '9988-7766'])
...
>>> print(eu.nome_completo)
Vinicius Assef
>>> 

Curiosidade: passando o parâmetro verbose=True na criação da namedtuple(), podemos ver a estrutura da classe criada:
>>> collections.namedtuple('MinhaClasse', 'nome, fone', verbose=True)
class MinhaClasse(tuple):
    'MinhaClasse(nome, fone)'

    __slots__ = ()

    _fields = ('nome', 'fone')

    def __new__(_cls, nome, fone):
        'Create new instance of MinhaClasse(nome, fone)'
        return _tuple.__new__(_cls, (nome, fone))

    @classmethod
    def _make(cls, iterable, new=tuple.__new__, len=len):
        'Make a new MinhaClasse object from a sequence or iterable'
        result = new(cls, iterable)
        if len(result) != 2:
            raise TypeError('Expected 2 arguments, got %d' % len(result))
        return result

    def __repr__(self):
        'Return a nicely formatted representation string'
        return 'MinhaClasse(nome=%r, fone=%r)' % self

    def _asdict(self):
        'Return a new OrderedDict which maps field names to their values'
        return OrderedDict(zip(self._fields, self))

    __dict__ = property(_asdict)

    def _replace(_self, **kwds):
        'Return a new MinhaClasse object replacing specified fields with new values'
        result = _self._make(map(kwds.pop, ('nome', 'fone'), _self))
        if kwds:
            raise ValueError('Got unexpected field names: %r' % kwds.keys())
        return result

    def __getnewargs__(self):
        'Return self as a plain tuple.  Used by copy and pickle.'
        return tuple(self)

    nome = _property(_itemgetter(0), doc='Alias for field number 0')
    fone = _property(_itemgetter(1), doc='Alias for field number 1')
>>> 


Para mais informações, consulte a documentação oficial.

Nota: códigos testados no Python 2.7

Eu sou Vinicius Assef, um programador do século passado que gosta de Python, pratica Lean Development e acredita em Deus. Você pode me contactar por email ou twitter.

Nenhum comentário:

Postar um comentário

Marcadores