Qual é a melhor maneira de implementar dicionários aninhados em Python?
Esta é uma má ideia, não faça isso. Em vez disso, use um dicionário regular e use dict.setdefault
onde apropriado, para que quando as chaves estejam ausentes no uso normal, você obtenha o esperado KeyError
. Se você insiste em obter esse comportamento, veja como se dar um tiro no pé:
Implemente __missing__
em uma dict
subclasse para definir e retornar uma nova instância.
Essa abordagem está disponível (e documentada) desde o Python 2.5, e (particularmente valiosa para mim), ela é impressa como um ditado normal , em vez da impressão feia de um padrão ditado automaticamente:
class Vividict(dict):
def __missing__(self, key):
value = self[key] = type(self)() # retain local pointer to value
return value # faster to return than dict lookup
(A nota self[key]
está no lado esquerdo da tarefa, portanto não há recursão aqui.)
e diga que você tem alguns dados:
data = {('new jersey', 'mercer county', 'plumbers'): 3,
('new jersey', 'mercer county', 'programmers'): 81,
('new jersey', 'middlesex county', 'programmers'): 81,
('new jersey', 'middlesex county', 'salesmen'): 62,
('new york', 'queens county', 'plumbers'): 9,
('new york', 'queens county', 'salesmen'): 36}
Aqui está o nosso código de uso:
vividict = Vividict()
for (state, county, occupation), number in data.items():
vividict[state][county][occupation] = number
E agora:
>>> import pprint
>>> pprint.pprint(vividict, width=40)
{'new jersey': {'mercer county': {'plumbers': 3,
'programmers': 81},
'middlesex county': {'programmers': 81,
'salesmen': 62}},
'new york': {'queens county': {'plumbers': 9,
'salesmen': 36}}}
Crítica
Uma crítica a esse tipo de contêiner é que, se o usuário digitar incorretamente uma chave, nosso código poderá falhar silenciosamente:
>>> vividict['new york']['queens counyt']
{}
Além disso, agora teríamos um município com erros ortográficos em nossos dados:
>>> pprint.pprint(vividict, width=40)
{'new jersey': {'mercer county': {'plumbers': 3,
'programmers': 81},
'middlesex county': {'programmers': 81,
'salesmen': 62}},
'new york': {'queens county': {'plumbers': 9,
'salesmen': 36},
'queens counyt': {}}}
Explicação:
Estamos apenas fornecendo outra instância aninhada da nossa classe Vividict
sempre que uma chave é acessada, mas está ausente. (Retornar a atribuição de valor é útil, pois evita que se chame o getter pelo dict e, infelizmente, não podemos devolvê-lo conforme está sendo definido.)
Observe que estas são as mesmas semânticas da resposta mais votada, mas na metade das linhas de código - implementação do nosklo:
class AutoVivification(dict):
"""Implementation of perl's autovivification feature."""
def __getitem__(self, item):
try:
return dict.__getitem__(self, item)
except KeyError:
value = self[item] = type(self)()
return value
Demonstração de uso
Abaixo está apenas um exemplo de como esse ditado pode ser facilmente usado para criar uma estrutura de ditado aninhado em tempo real. Isso pode criar rapidamente uma estrutura de árvore hierárquica tão profundamente quanto você desejar.
import pprint
class Vividict(dict):
def __missing__(self, key):
value = self[key] = type(self)()
return value
d = Vividict()
d['foo']['bar']
d['foo']['baz']
d['fizz']['buzz']
d['primary']['secondary']['tertiary']['quaternary']
pprint.pprint(d)
Quais saídas:
{'fizz': {'buzz': {}},
'foo': {'bar': {}, 'baz': {}},
'primary': {'secondary': {'tertiary': {'quaternary': {}}}}}
E, como mostra a última linha, ela é impressa de maneira bonita e em ordem para inspeção manual. Mas se você deseja inspecionar visualmente seus dados, implementar __missing__
para definir uma nova instância de sua classe como chave e retornar é uma solução muito melhor.
Outras alternativas, por contraste:
dict.setdefault
Embora o solicitante pense que isso não está limpo, acho preferível a Vividict
mim mesmo.
d = {} # or dict()
for (state, county, occupation), number in data.items():
d.setdefault(state, {}).setdefault(county, {})[occupation] = number
e agora:
>>> pprint.pprint(d, width=40)
{'new jersey': {'mercer county': {'plumbers': 3,
'programmers': 81},
'middlesex county': {'programmers': 81,
'salesmen': 62}},
'new york': {'queens county': {'plumbers': 9,
'salesmen': 36}}}
Um erro de ortografia falharia ruidosamente e não sobrecarregaria nossos dados com informações incorretas:
>>> d['new york']['queens counyt']
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'queens counyt'
Além disso, acho que o setdefault funciona muito bem quando usado em loops e você não sabe o que obterá para chaves, mas o uso repetitivo se torna bastante oneroso e não acho que alguém queira manter o seguinte:
d = dict()
d.setdefault('foo', {}).setdefault('bar', {})
d.setdefault('foo', {}).setdefault('baz', {})
d.setdefault('fizz', {}).setdefault('buzz', {})
d.setdefault('primary', {}).setdefault('secondary', {}).setdefault('tertiary', {}).setdefault('quaternary', {})
Outra crítica é que o setdefault requer uma nova instância, seja ela usada ou não. No entanto, o Python (ou pelo menos o CPython) é bastante inteligente ao lidar com novas instâncias não utilizadas e não referenciadas, por exemplo, reutiliza o local na memória:
>>> id({}), id({}), id({})
(523575344, 523575344, 523575344)
Um padrão padrão vivificado automaticamente
Essa é uma implementação elegante e o uso em um script no qual você não está inspecionando os dados seria tão útil quanto a implementação __missing__
:
from collections import defaultdict
def vivdict():
return defaultdict(vivdict)
Mas se você precisar inspecionar seus dados, os resultados de um ditado padrão vivificado automaticamente preenchido com dados da mesma maneira serão:
>>> d = vivdict(); d['foo']['bar']; d['foo']['baz']; d['fizz']['buzz']; d['primary']['secondary']['tertiary']['quaternary']; import pprint;
>>> pprint.pprint(d)
defaultdict(<function vivdict at 0x17B01870>, {'foo': defaultdict(<function vivdict
at 0x17B01870>, {'baz': defaultdict(<function vivdict at 0x17B01870>, {}), 'bar':
defaultdict(<function vivdict at 0x17B01870>, {})}), 'primary': defaultdict(<function
vivdict at 0x17B01870>, {'secondary': defaultdict(<function vivdict at 0x17B01870>,
{'tertiary': defaultdict(<function vivdict at 0x17B01870>, {'quaternary': defaultdict(
<function vivdict at 0x17B01870>, {})})})}), 'fizz': defaultdict(<function vivdict at
0x17B01870>, {'buzz': defaultdict(<function vivdict at 0x17B01870>, {})})})
Essa saída é bastante deselegante e os resultados são bastante ilegíveis. A solução normalmente fornecida é a conversão recursiva em um ditado para inspeção manual. Essa solução não trivial é deixada como um exercício para o leitor.
atuação
Finalmente, vejamos o desempenho. Estou subtraindo os custos da instanciação.
>>> import timeit
>>> min(timeit.repeat(lambda: {}.setdefault('foo', {}))) - min(timeit.repeat(lambda: {}))
0.13612580299377441
>>> min(timeit.repeat(lambda: vivdict()['foo'])) - min(timeit.repeat(lambda: vivdict()))
0.2936999797821045
>>> min(timeit.repeat(lambda: Vividict()['foo'])) - min(timeit.repeat(lambda: Vividict()))
0.5354437828063965
>>> min(timeit.repeat(lambda: AutoVivification()['foo'])) - min(timeit.repeat(lambda: AutoVivification()))
2.138362169265747
Com base no desempenho, dict.setdefault
funciona melhor. Eu recomendo o código de produção, nos casos em que você se preocupa com a velocidade de execução.
Se você precisar disso para uso interativo (em um notebook IPython, talvez), o desempenho realmente não importa - nesse caso, eu usaria o Vividict para garantir a legibilidade da saída. Comparado com o objeto AutoVivification (que usa em __getitem__
vez de __missing__
, que foi feito para esse fim), é muito superior.
Conclusão
A implementação __missing__
de uma subclasse dict
para definir e retornar uma nova instância é um pouco mais difícil do que as alternativas, mas tem os benefícios de
- instanciação fácil
- população de dados fácil
- visualização de dados fácil
e por ser menos complicado e mais eficiente do que modificar __getitem__
, deve ser preferido a esse método.
No entanto, tem desvantagens:
- Pesquisas ruins falharão silenciosamente.
- A pesquisa incorreta permanecerá no dicionário.
Por isso, pessoalmente, prefiro setdefault
as outras soluções e em todas as situações em que precisei desse tipo de comportamento.
Vividict
? Por exemplo,3
elist
para um ditado de um ditado de listas que podem ser preenchidas comd['primary']['secondary']['tertiary'].append(element)
. Eu poderia definir três classes diferentes para cada profundidade, mas adoraria encontrar uma solução mais limpa.