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.d['primary']['secondary'].setdefault('tertiary', []).append('element')
- ?? Obrigado pelo elogio, mas deixe-me ser honesto - eu nunca uso__missing__
- eu sempre usosetdefault
. Provavelmente devo atualizar minha conclusão / introdução ...The bad lookup will remain in the dictionary.
pensar em usar esta solução ?. Muito apreciado. Thxsetdefault
quando aninhava mais de dois níveis de profundidade. Parece que nenhuma estrutura no Python pode oferecer verdadeira vivificação, conforme descrito. Eu tive que me contentar com dois métodos de declaração, um paraget_nested
& um para oset_nested
qual aceitar uma referência para dict e lista de atributos aninhados.Teste:
Resultado:
fonte
pickle
é terrível entre as versões python. Evite usá-lo para armazenar dados que você deseja manter. Use-o apenas para caches e outras coisas que você pode despejar e regenerar à vontade. Não como um método de armazenamento ou serialização de longo prazo.sqlite
banco de dados para armazená-lo.Só porque eu não vi um tão pequeno, aqui está um ditado que fica tão aninhado quanto você gosta, sem suor:
fonte
yodict = lambda: defaultdict(yodict)
.dict
, para ser totalmente equivalente, precisaríamosx = Vdict(a=1, b=2)
trabalhar.dict
não era um requisito declarado pelo OP, que apenas pedia a "melhor maneira" de implementá-los - e, além disso, ele não deveria / não deveria importa muito em Python de qualquer maneira.Você pode criar um arquivo YAML e lê-lo usando PyYaml .
Etapa 1: Crie um arquivo YAML, "Employment.yml":
Etapa 2: leia-o em Python
e agora
my_shnazzy_dictionary
tem todos os seus valores. Se você precisar fazer isso rapidamente, poderá criar o YAML como uma string e alimentá-loyaml.safe_load(...)
.fonte
Como você tem um design de esquema em estrela, você pode estruturá-lo mais como uma tabela relacional e menos como um dicionário.
Esse tipo de coisa pode ajudar bastante na criação de um design semelhante a um data warehouse, sem as despesas gerais do SQL.
fonte
Se o número de níveis de aninhamento for pequeno, eu uso
collections.defaultdict
para isso:Usando
defaultdict
como isto evita um monte de bagunçasetdefault()
,get()
etc.fonte
Esta é uma função que retorna um dicionário aninhado de profundidade arbitrária:
Use-o assim:
Itere através de tudo com algo como isto:
Isso imprime:
Você pode querer fazê-lo para que novos itens não possam ser adicionados ao ditado. É fácil converter recursivamente todos esses
defaultdict
s paradict
s normais .fonte
Eu acho
setdefault
bastante útil; Ele verifica se uma chave está presente e a adiciona, se não:setdefault
sempre retorna a chave relevante; portanto, você está atualizando os valores de 'd
' no local.Quando se trata de iterar, tenho certeza de que você poderia escrever um gerador com bastante facilidade se ainda não existir no Python:
fonte
Como outros sugeriram, um banco de dados relacional pode ser mais útil para você. Você pode usar um banco de dados sqlite3 na memória como uma estrutura de dados para criar tabelas e consultá-las.
Este é apenas um exemplo simples. Você pode definir tabelas separadas para estados, condados e cargos.
fonte
collections.defaultdict
pode ser subclassificado para fazer um ditado aninhado. Em seguida, adicione quaisquer métodos de iteração úteis a essa classe.fonte
Quanto a "blocos desagradáveis de tentativa / captura":
rendimentos
Você pode usar isso para converter do seu formato de dicionário simples para o formato estruturado:
fonte
Você pode usar o Addict: https://github.com/mewwts/addict
fonte
defaultdict()
é seu amigo!Para um dicionário bidimensional, você pode:
Para mais dimensões, você pode:
fonte
Para facilitar a iteração no seu dicionário aninhado, por que não escrever um gerador simples?
Portanto, se você possui um dicionário aninhado compilado, a iteração sobre ele se torna simples:
Obviamente, seu gerador pode produzir qualquer formato de dados que seja útil para você.
Por que você está usando blocos try catch para ler a árvore? É fácil o suficiente (e provavelmente mais seguro) consultar se existe uma chave em um ditado antes de tentar recuperá-lo. Uma função usando cláusulas de guarda pode ser assim:
Ou, um método talvez um tanto detalhado, é usar o método get:
Mas, de uma maneira um pouco mais sucinta, você pode querer usar um arquivo collections.defaultdict , que faz parte da biblioteca padrão desde o python 2.5.
Estou fazendo suposições sobre o significado da sua estrutura de dados aqui, mas deve ser fácil ajustar o que você realmente deseja fazer.
fonte
Eu gosto da idéia de agrupar isso em uma classe e implementá
__getitem__
-lo, de__setitem__
modo que eles implementem uma linguagem de consulta simples:Se você quiser ter uma fantasia, também pode implementar algo como:
mas principalmente acho que algo assim seria realmente divertido de implementar: D
fonte
A menos que seu conjunto de dados permaneça pequeno, convém usar um banco de dados relacional. Ele fará exatamente o que você deseja: facilitar a adição de contagens, a seleção de subconjuntos de contagens e até a agregação de contagens por estado, município, ocupação ou qualquer combinação delas.
fonte
Exemplo:
Editar: agora retornando dicionários ao consultar caracteres curinga (
None
) e valores únicos caso contrário.fonte
Eu tenho uma coisa semelhante acontecendo. Eu tenho muitos casos em que faço:
Mas indo muitos níveis profundamente. É o ".get (item, {})" que é a chave, pois criará outro dicionário se ainda não houver um. Enquanto isso, estive pensando em maneiras de lidar melhor com isso. No momento, há muitos
Então, em vez disso, eu fiz:
Que tem o mesmo efeito se você:
Melhor? Acho que sim.
fonte
Você pode usar a recursão em lambdas e defaultdict, sem necessidade de definir nomes:
Aqui está um exemplo:
fonte
Eu costumava usar essa função. é seguro, rápido e de fácil manutenção.
Exemplo:
fonte