O autor selecionou a COVID-19 Relief Fund​​​​​ para receber uma doação como parte do programa Write for DOnations.

Introdução

O Python 3 possui uma série de estruturas de dados integrados, incluindo tuplas, dicionários e listas. As estruturas de dados nos fornecem uma maneira de organizar e armazenar dados. O módulo collections (coleções) nos ajuda a preencher e manipular eficientemente as estruturas de dados.

Neste tutorial, vamos abordar três classes no módulo collections para ajudá-lo a trabalhar com tuplas, dicionários e listas. Usaremos o namedtuples para criar tuplas com campos nomeados, defaultdict para agrupar informações de forma concisa em dicionários e deque para adicionar com eficiência elementos a qualquer um dos lados de um objeto do tipo lista.

Para este tutorial, trabalharemos principalmente com um inventário de peixes que precisaremos modificar à medida que peixes são adicionados a ou removidos de um aquário fictício.

Pré-requisitos

Para aproveitar ao máximo este tutorial, é recomendável ter alguma familiaridade com os tipos de dados tupla, dicionário e lista. Tanto com suas sintaxes, quanto com como recuperar dados deles. Você pode revisar estes tutoriais para as informações básicas necessárias:

Como adicionar campos nomeados a tuplas

As tuplas em Python são uma sequência ordenada imutável, ou inalterável, de elementos. As tuplas são frequentemente usadas para representar dados colunares. Por exemplo, linhas de um arquivo CSV ou linhas de um banco de dados SQL. Um aquário pode acompanhar seu inventário de peixes como uma série de tuplas.

Uma tupla de peixe individual:

("Sammy", "shark", "tank-a")

Esta tupla é composta por três elementos string.

Embora seja útil de algumas maneiras, esta tupla não indica com clareza o que cada um de seus campos representa. Na verdade, o elemento 0 é um nome, o elemento 1 é uma espécie e o elemento 2 é o tanque onde está localizado.

Explicação dos campos da tupla de peixe:

nome espécie tanque
Sammy tubarão tanque-a

Essa tabela deixa claro que cada um dos três elementos da tupla possui um significado claro.

O namedtuple do módulo collections permite que você adicione nomes explícitos a cada elemento de uma tupla para tornar seus significados claros em seu programa Python.

Vamos usar o namedtuple para gerar uma classe que nomeia com clareza cada elemento da tupla de peixe:

from collections import namedtuple

Fish = namedtuple("Fish", ["name", "species", "tank"])

from collections import namedtuple dá ao seu programa Python acesso à função de fábrica namedtuple. A chamada de função namedtuple() retorna uma classe que está ligada ao nome Fish (peixe). A função namedtuple() possui dois argumentos: o nome desejado da nossa nova classe "Fish" e uma lista de elementos nomeados ["name", "species", "tank"] (“nome”, “espécie”, “tanque”).

Podemos usar a classe Fish para representar a tupla de peixe de antes:

sammy = Fish("Sammy", "shark", "tank-a")

print(sammy)

Se executarmos esse código, veremos o seguinte resultado:

Output
Fish(name='Sammy', species='shark', tank='tank-a')

A sammy é instanciada usando a classe Fish. sammy é uma tupla com três elementos claramente nomeados.

Os campos de sammy podem ser acessados pelo seu nome ou com um índice tradicional de tupla:

print(sammy.species)
print(sammy[1])

Se executarmos essas duas chamadas de print, veremos o seguinte resultado:

Output
shark shark

Acessar .species retorna o mesmo valor que quando acessa-se o segundo elemento de sammy usando [1].

Usar o namedtuple do módulo collections torna seu programa mais legível, ao mesmo tempo em que mantém as propriedades importantes de uma tupla (que são imutáveis e ordenadas).

Além disso, a função de fábrica namedtuple adiciona diversos métodos extras para as instâncias de Fish.

Use ._asdict() para converter uma instância em um dicionário:

print(sammy._asdict())

Se executarmos print, você verá um resultado como o seguinte:

Output
{'name': 'Sammy', 'species': 'shark', 'tank': 'tank-a'}

Chamar .asdict() em sammy retorna um dicionário que mapeia cada um dos três nomes de campo aos seus valores correspondentes.

As versões do Python mais antigas que 3.8 podem gerar esta linha ligeiramente diferente como resultado. Você pode, por exemplo, ver um OrderedDict em vez do dicionário evidente mostrado aqui.

Nota: em Python, os métodos com underline à esquerda são geralmente considerados “privados”. Métodos adicionais disponibilizados pelo namedtuple (como _asdict(), ._make(), ._replace(), etc), no entanto, são públicos.

Como coletar dados em um dicionário

Muitas vezes, pode ser útil coletar dados em dicionários Python. O defaultdict do módulo collections pode nos ajudar a reunir as informações em dicionários de maneira rápida e concisa.

O defaultdict nunca provoca um KeyError. Se uma chave não estiver presente, o defaultdict simplesmente insere e retorna um valor de espaço reservado:

from collections import defaultdict

my_defaultdict = defaultdict(list)

print(my_defaultdict["missing"])

Se executarmos esse código, veremos um resultado como o seguinte:

Output
[]

O defaultdict insere e retorna um valor de espaço reservado ao invés de lançar um KeyError. Neste caso, especificamos o valor de espaço reservado como uma lista.

Os dicionários regulares, por outro lado, lançarão um KeyError para chaves que estejam faltando:

my_regular_dict = {}

my_regular_dict["missing"]

Se executarmos esse código, veremos um resultado como o seguinte:

Output
Traceback (most recent call last): File "<stdin>", line 1, in <module> KeyError: 'missing'

O dicionário regular my_regular_dict provoca um KeyError quando tentamos acessar uma chave que não está presente.

O defaultdict comporta-se de maneira diferente de um dicionário regular. Em vez de criar um KeyError para uma chave que esteja faltando, o defaultdict chama o valor de espaço reservado com nenhum argumento para criar um novo objeto. Neste caso, o list() para criar uma lista vazia.

Voltando ao nosso exemplo de aquário fictício, vamos supor que temos uma lista de tuplas de peixe representando o inventário de um aquário.

fish_inventory = [
    ("Sammy", "shark", "tank-a"),
    ("Jamie", "cuttlefish", "tank-b"),
    ("Mary", "squid", "tank-a"),
]

Existem três peixes no aquário — seus nomes, espécies e tanques são anotados nestas três tuplas.

Nosso objetivo é organizar nosso inventário por tanque — queremos saber a lista de peixes presentes em cada tanque. Em outras palavras, queremos um dicionário que mapeie "tank-a" para ["Jamie, "Mary"], e "tank-b" para ["Jamie"].

Podemos usar o defaultdict para agrupar peixes por tanque:

from collections import defaultdict

fish_inventory = [
    ("Sammy", "shark", "tank-a"),
    ("Jamie", "cuttlefish", "tank-b"),
    ("Mary", "squid", "tank-a"),
]
fish_names_by_tank = defaultdict(list)
for name, species, tank in fish_inventory:
    fish_names_by_tank[tank].append(name)

print(fish_names_by_tank)

Ao executarmos esse código, veremos o seguinte resultado:

Output
defaultdict(<class 'list'>, {'tank-a': ['Sammy', 'Mary'], 'tank-b': ['Jamie']})

O fish_names_by_tank é declarado como um defaultdict, que utiliza-se do padrão para inserir o list() ao invés de lançar um KeyError. Como isso garante que todas as chaves em fish_names_by_tank apontarão para uma list (lista), temos a liberdade para chamar o .append() para adicionar nomes à lista de cada tanque.

O defaultdict é útil aqui porque isso reduz as chances de KeyErrors inesperados. A redução dos KeyErrors inesperados significa que seu programa pode ser escrito com maior clareza e com menos linhas. Mais concretamente, o idioma defaultdict permite que você evite instanciar manualmente uma lista vazia para cada tanque.

Sem o defaultdict, o loop for poderia ter ficado parecido com isto:

More Verbose Example Without defaultdict
...

fish_names_by_tank = {}
for name, species, tank in fish_inventory:
    if tank not in fish_names_by_tank:
      fish_names_by_tank[tank] = []
    fish_names_by_tank[tank].append(name)

O uso de apenas um dicionário regular (em vez de um defaultdict) significa que o loop for precisa sempre verificar se o tank indicado em fish_names_by_tank existe. Só depois de termos verificado que o tank já está presente em fish_names_by_tank, ou que acabou de ser inicializado com um [], é que podemos acrescentar o nome do peixe.

O defaultdict pode ajudar a diminuir o código boilerplate ao preencher os dicionários, pois ele nunca causa um KeyError.

Como usar o deque para adicionar elementos de maneira eficiente a qualquer lado de uma coleção

As listas Python são uma sequência ordenada mutável, ou alterável, de elementos. O Python pode acrescentar elementos às listas constantemente (o comprimento da lista não tem efeito no tempo necessário para a inserção). Entretanto, inserir elementos no início de uma lista pode demorar mais — o tempo necessário aumenta à medida que a lista aumenta de tamanho.

Em termos de notação O-grande, acrescentar um elemento a uma lista é uma operação de tempo constante O(1). A inserção de um elemento no início de uma lista, por outro lado, é mais lenta com um desempenho O(n).

Nota: os engenheiros de softwares geralmente medem o desempenho de procedimentos usando algo conhecido como notação “O-grande”. Quando o tamanho de uma entrada não tem efeito no tempo necessário para se executar um procedimento, diz-se que a execução ocorre em tempo constante, ou O(1) (“O-grande de 1”). Assim como você aprendeu acima, o Python pode acrescentar elementos a listas com um desempenho temporal constante, também conhecido como O(1).

Às vezes, o tamanho de uma entrada afeta diretamente o tempo necessário para se executar um procedimento. A inserção de um elemento no início de uma lista Python, por exemplo, é executada mais lentamente à medida que existam mais elementos na lista. A notação O-grande utiliza a letra n para representar o tamanho da entrada. Isso significa que a adição de itens no início de uma lista Python é executada em “tempo linear” ou O(n) (“O-grande de n”).

De um modo geral, os procedimentos O(1) são mais rápidos que os procedimentos O(n).

Somos capazes inserir elementos no início de uma lista Python:

favorite_fish_list = ["Sammy", "Jamie", "Mary"]

# O(n) performance
favorite_fish_list.insert(0, "Alice")

print(favorite_fish_list)

Se executarmos isso, veremos um resultado como o seguinte:

Output
['Alice', 'Sammy', 'Jamie', 'Mary']

O método .insert(index, object) em lista permite que adicionemos "Alice" no início de favorite_fish_list. No entanto, deve-se notar que a inserção de um elemento no início de uma lista possui um desempenho O(n). À medida que o comprimento de favorite_fish_list cresce, o tempo necessário para inserir um peixe no início da lista crescerá proporcionalmente e demorará mais tempo.

O deque (pronunciado como “deck”) do módulo collections é um objeto do tipo lista que nos permite inserir elementos no início ou final de uma sequência com performance temporal constante (O(1)).

Insira um item no início de um deque:

from collections import deque

favorite_fish_deque = deque(["Sammy", "Jamie", "Mary"])

# O(1) performance
favorite_fish_deque.appendleft("Alice")

print(favorite_fish_deque)

Ao executarmos esse código, veremos o seguinte resultado:

Output
deque(['Alice', 'Sammy', 'Jamie', 'Mary'])

Podemos instanciar um deque usando uma coleção preexistente de elementos, neste caso, uma lista de três nomes de peixes favoritos. Chamar o método appendleft de favorite_fish_deque nos permite inserir um item no início de nossa coleção com o desempenho O(1). Ter um desempenho O(1) significa que o tempo necessário para adicionar um item no início de favorite_fish_deque não aumentará, mesmo se favorite_fish_deque possuir milhares ou milhões de elementos.

Nota: embora o deque adicione entradas no início de uma sequência mais eficientemente que uma lista, o deque não realiza todas as suas operações com maior eficiência que uma lista. Por exemplo, acessar um item aleatório em um deque possui um desempenho O(n), mas acessar um item aleatório em uma lista possui um desempenho O(1). Use o deque quando for importante inserir ou remover elementos de um dos lados de sua coleção rapidamente. Uma comparação completa do desempenho temporal está disponível na wiki do Python.

Conclusão

O módulo collections é uma parte poderosa da biblioteca padrão Python que permite que você trabalhe com dados de maneira concisa e eficiente. Este tutorial abordou três das classes disponibilizadas pelo módulo collections, incluindo namedtuple, defaultdict e deque.

A partir daqui, utilize a documentação do módulo collection para aprender mais sobre outras classes e utilitários disponíveis. Para aprender mais sobre o Python em geral, consulte nossa série de tutoriais sobre Como programar em Python 3.

0 Comments

Creative Commons License