Dicionários em Python


Dicionários em Python são conjuntos de chave-valor. Assim como um dicionário chaves (termos) que estão associadas a valores (significados dos termos), dicionários em Python são uma estrutura de dados que nos permite mapear chaves a valores.

A sintaxe para criação de dicionários em Python é {chave1 : valor1, chave2 : valor2, ...​}.

# O comando dicionario_vazio = dict() possui o mesmo efeito do comando abaixo.
dicionario_vazio = {}
print("Dicionário vazio: ", dicionario_vazio)

paises = {'BRA': 'Brasil', 'EUA': 'Estados Unidos', 'FRA': 'França'}
print("Exemplo de dicionário: ", paises)

print("Tipo de um dicionário: ", type(paises))
Dicionário vazio:  {}
Exemplo de dicionário:  {'BRA': 'Brasil', 'EUA': 'Estados Unidos', 'FRA': 'França'}
Tipo de um dicionário:  <class 'dict'>

Modificando dicionários em Python

Ao contrário de tuplas, dicionários podem ser modificados:

# Modificando um dicionário.
paises["BRA"] = "Brazil"
paises["FRA"] = "France"

# Adicionando um elemento.
paises["ESP"] = "Espanha"

print("Dicionário modificado: ", paises)
Dicionário modificado:  {'BRA': 'Brazil', 'EUA': 'Estados Unidos', 'FRA': 'France', 'ESP': 'Espanha'}

Pesquisando valores em dicionários

Dicionários nos permitem pesquisar facilmente por valores quando sabemos a chave correspondente:

print("EUA: ", paises['EUA'])
EUA:  Estados Unidos

Vejamos agora como percorrer pares chave-valor em um dicionário:

paises = {'BRA': 'Brasil', 'EUA': 'Estados Unidos', 'FRA': 'França', 'ESP': 'Espanha'}
for chave, valor in paises.items():
    print(f"{chave} = {valor}")
BRA = Brazil
EUA = Estados Unidos
FRA = França
ESP = Espanha

Um dos usos mais comuns de dicionários é para contar a frequência de items em uma coleção. Vejamos um exemplo:

palavra = "abracadabra"
freqs = dict()
for ch in palavra:
    if not ch in freqs:
        freqs[ch] = 1
    else:
        freqs[ch] += 1

print(freqs)
{'a': 5, 'b': 2, 'r': 2, 'c': 1, 'd': 1}

No código acima, estamos basicamente iterando em todas as letras da palavra e contando o número de ocorrências de cada letra. Se for a primeira vez que estamos encontramos a letra, colocamos sua frequência como 1 no dicionário. Se já tivermos visto a letra antes, incrementamos sua frequência.

Esse tipo de operação (contar ocorrências) é um uso tão comum de dicionários que Python possui alguns recursos para simplificar a tarefa. O mais comum desses recursos é conhecido como Counter. Vejamos um exemplo.

from collections import Counter

palavra = "abracadabra"
freqs = Counter(palavra)
print(freqs)
Counter({'a': 5, 'b': 2, 'r': 2, 'c': 1, 'd': 1})

O que fazer quando a chave não está presente?

Nos exemplos vistos até aqui, sempre acessamos chaves que estão presentes no dicionário ou então inserimos uma lógica adicional para lidar com o caso em que a chave não está presente.

Por padrão, quando tentamos acessar uma chave que não está no dicionário, Python retorna um erro, como ilustrado abaixo.

palavra = "abracadabra"
freqs = dict()
for ch in palavra:
    if not ch in freqs:
        freqs[ch] = 1
    else:
        freqs[ch] += 1

print(freqs["e"])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'e'

Nesse caso, em vez de retornar um erro, uma opção interessante seria imprimir 0, visto que a letra “e” não está presente na palavra, e portanto ocorre zero vezes. Python possui duas formas para expressarmos isso: uma é usar a função get() para acessar as chaves, a outra é usar a estrutura de dados conhecida como defaultdict (dicionário com valores default). Vejamos exemplos desses dois recursos.

palavra = "abracadabra"
freqs = dict()
for ch in palavra:
    freqs[ch] = freqs.get(ch, 0) + 1

print(freqs)
{'a': 5, 'b': 2, 'r': 2, 'c': 1, 'd': 1}

O que a função get() acima faz é o seguinte. Se a chave passada como parâmetro estiver no dicionário, ela retorna o valor associado àquela chave. Se a chave não estiver presente, a função retorna o valor que passamos como o segundo parâmetro (1 no exemplo acima). Com isso simplificamos um pouco nosso código.

Como mencionamos, a outra forma de lidarmos com chaves ausentes em um dicionário é por meio da estrutura de dados chamada defaultdict, que são dicionários com valores padrão para chaves ausentes. Vejamos um exemplo e então ficará mais fácil entendermos o funcionamento dessa estrutura de dados.

from collections import defaultdict

palavra = "abracadabra"
freqs = defaultdict(int) # Note o tipo passado como parâmetro
for ch in palavra:
    freqs[ch] += 1

print(freqs)
defaultdict(<class 'int'>, {'a': 5, 'b': 2, 'r': 2, 'c': 1, 'd': 1})

Perceba que ao criar um defaultdict, passamos um tipo como parâmetro. Esse tipo é o tipo dos valores que iremos armazenar no dicionário. Com essa informação, ao tentarmos acessar uma chave que não está presente no dicionário, Python irá retornar o valor padrão para esse tipo. No caso de ints, o valor padrão é zero.

Assim, no exemplo acima, ao tentarmos acessar uma letra que não vimos ainda, Python retornará 0, e nós então incrementamos esse valor de uma unidade, fazendo então com que a frequência de uma letra que estamos vendo pela primeira vez se torne 1. Caso a letra já esteja no dicionário, Python irá nos retornar a frequência atual da letra, e então iremos incrementar essa frequência. No final, obtemos o mesmo resultado que nos exemplos anteriores.

Como o defaultdict é uma estrutura de dados diferente dos dicionários comuns de Python, ao imprimirmos um defaultdict, o interpretador Python nos mostra mais informações além dos elementos. No código acima, vemos que estamos lidando com um defaultdict e que os valores padrão são do tipo int, por exemplo.

Estudo de caso: encontrando anagramas

Nessa seção veremos um breve estudo de caso sobre o uso de dicionários na prática. Criaremos um programa que faz o download de uma base de textos grande e encontraremos grupos de palavras que sejam anagramas entre si. Duas palavras são consideradas anagramas se elas possuem as mesmas letras e cada letra aparece na mesma frequência nas duas palavras. Por exemplo, os nomes “Ronaldo” e “Orlando” são anagramas (se desconsiderarmos maiúsculas e minúsculas). Aqui estão as frequências de cada letra nos dois nomes:

from collections import Counter

freqs_ronaldo = Counter("Ronaldo".lower()) # A função lower() converte para minúsculas
freqs_orlando = Counter("Orlando".lower())

print(freqs_ronaldo)
print(freqs_orlando)
print(freqs_ronaldo == freqs_orlando)
Counter({'o': 2, 'r': 1, 'n': 1, 'a': 1, 'l': 1, 'd': 1})
Counter({'o': 2, 'r': 1, 'n': 1, 'a': 1, 'l': 1, 'd': 1})
True

Para escrever um programa de computador que verifica se duas palavras são anagramas, é necessário estabelecermos um procedimento (um algoritmo). Vamos tentar desenvolver tal procedimento.

Um procedimento para determinar se duas palavras são anagramas envolve duas perguntas básicas:

  1. As duas palavras são formadas pelas mesmas letras?
  2. Cada uma das letras ocorre o mesmo número de vezes em ambas as palavras?

Se a resposta a ambas as perguntas for “sim”, as duas palavras são anagramas uma da outra.

A ideia-chave desse procedimento, proposto originalmente por Jon Bentley em seu livro Programming Pearls, é criar uma assinatura para cada palavra. Duas palavras são anagramas uma da outra se e somente se tiverem a mesma assinatura.

Para obter a assinatura de uma palavra, basta colocar as letras da palavra em ordem alfabética (ordenar as letras). Voltando ao nosso exemplo, ao ordenar as letras da palavra “orlando”, obtemos “adlnoor”, que é a mesma assinatura da palavra “ronaldo”, portanto essas duas palavras são anagramas.

Assim, temos um algoritmo para determinar se duas palavras p1 e p2 são anagramas:

  • Ordene p1 para obter sua assinatura sigw1.
  • Ordene p2 para obter sua assinatura sigw2.

Retorne verdadeiro se sigw1 for igual a sigw2 (as palavras são anagramas). Caso contrário, retorne falso.

Vamos agora transformar nosso algoritmo em um programa.

def assinatura(p):
    """Retorna a string p ordenada"""
    return ''.join(sorted(p))

def anagrama(p1, p2):
  """Verifica se p1 é um anagrama de p2."""
  return assinatura(p1) == assinatura(p2)

print(anagrama("ronaldo", "orlando"))
True

A implementação da função assinatura acima pode parecer um pouco misteriosa, mas não se assuste. A função sorted() retorna uma lista. Assim, sorted(orlando) retornará ['a', 'd', 'l', 'n', 'o', 'o', 'r']. A função join recebe uma lista concatena os elementos dessa lista. Logo, ao invocar a função join em uma lista de caracteres, iremos concatenar os caracteres da lista em uma string. No código acima, fizemos ’’.join() porque não precisamos de separador nenhum entre os caracteres da string, por isso usamos ’'.

Agora que sabemos como verificar se duas palavras são anagramas, gostaríamos de construir um procedimento mais amplo para encontrar todos os anagramas em um conjunto de palavras. Mais especificamente, dado uma palavra p, gostaríamos de retornar todos os anagramas de p em um dado conjunto de palavras. Como devemos proceder?

Aqui está uma ideia:

  1. Construir uma tabela que mapeia uma determinada assinatura para todas as palavras no conjunto com a mesma assinatura. Para isso, iremos:
    • Baixar um dicionário em Português.
    • Para simplificar nossa tarefa, vamos converter as palavras no dicionário para minúsculas.
    • Construir dicionário mapeando assinaturas para listas de palavras.
  2. Para uma palavra p, retornar a entrada da tabela contendo palavras que têm a mesma assinatura que p.

O Passo 1 opera em duas estruturas: um conjunto de palavras e uma tabela que mapeia assinaturas para palavras. Como conjunto de palavras, usaremos um dicionário contendo os textos da obra de Machado de Assis. E como tabela, usaremos o conceito de dicionário em Python (não a ser confundido com um dicionário em Português).

Vamos agora tentar codificar o procedimento acima.

# Construir uma tabela que mapeia assinaturas para palavras.

# Baixar os textos da obra de Machado de Assis.
import nltk
nltk.download('machado')

# Converter todas as palavras no dicionário para minúsculas.
from nltk.corpus import machado
palavras_machado = [w.lower() for w in machado.words()]

# Construir uma tabela mapeando assinaturas para listas de palavras.
from collections import defaultdict

def gera_assinaturas(palavras):
  """Construir um mapa de assinaturas para palavras."""
  tabela_assinaturas = defaultdict(set)
  for p in palavras:
    tabela_assinaturas[assinatura(p)].add(p)
  return tabela_assinaturas

tabela_assinaturas = gera_assinaturas(palavras_machado)
[nltk_data] Downloading package machado to /root/nltk_data...
True

No código acima, usamos a biblioteca nltk do Python. Para mais detalhes, confira a documentação desta biblioteca em https://www.nltk.org/howto/portuguese_en.html.

Para imprimir todos os anagramas de uma palavra específica, basta obter sua assinatura e inspecionar nossa tabela, assim:

# Imprimir palavras que têm a mesma assinatura que p.
p = 'amor'
print(tabela_assinaturas[assinatura(p)])
{'amor', 'ramo', 'roam', 'omar', 'roma', 'armo', 'mora'}

Vimos que com apenas um pouco de código podemos inspecionar um dicionário inteiro em busca de anagramas.

Esse foi um exemplo mais extenso, mas que ilustra uma aplicação real de dicionários em Python. Nesse exemplo, vimos também como decompor um problema em passos menores e como traduzir a solução do problema para código Python.