Qual é a melhor maneira de implementar dicionários aninhados?

201

Eu tenho uma estrutura de dados que basicamente equivale a um dicionário aninhado. Digamos que seja assim:

{'new jersey': {'mercer county': {'plumbers': 3,
                                  'programmers': 81},
                'middlesex county': {'programmers': 81,
                                     'salesmen': 62}},
 'new york': {'queens county': {'plumbers': 9,
                                'salesmen': 36}}}

Agora, manter e criar isso é bastante doloroso; toda vez que tenho um novo estado / município / profissão, tenho que criar os dicionários da camada inferior por meio de blocos desagradáveis ​​de tentativa / captura. Além disso, tenho que criar iteradores aninhados irritantes se quiser passar por todos os valores.

Eu também poderia usar tuplas como chaves, assim:

{('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}

Isso torna a iteração sobre os valores muito simples e natural, mas é mais sintaticamente doloroso fazer coisas como agregações e olhar para subconjuntos do dicionário (por exemplo, se eu apenas quero ir estado a estado).

Basicamente, às vezes eu quero pensar em um dicionário aninhado como um dicionário simples, e às vezes eu quero pensar nele de fato como uma hierarquia complexa. Eu poderia agrupar tudo isso em uma aula, mas parece que alguém já deve ter feito isso. Como alternativa, parece que pode haver algumas construções sintáticas realmente elegantes para fazer isso.

Como eu poderia fazer isso melhor?

Adendo: Estou ciente, setdefault()mas não cria uma sintaxe limpa. Além disso, cada sub-dicionário que você cria ainda precisa ser setdefault()definido manualmente.

YGA
fonte

Respostas:

179

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.setdefaultonde 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 dictsubclasse 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 Vividictmim 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.setdefaultfunciona 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 dictpara 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 setdefaultas outras soluções e em todas as situações em que precisei desse tipo de comportamento.

Aaron Hall
fonte
Excelente resposta! Existe alguma maneira de especificar uma profundidade finita e um tipo de folha para a Vividict? Por exemplo, 3e listpara um ditado de um ditado de listas que podem ser preenchidas com d['primary']['secondary']['tertiary'].append(element). Eu poderia definir três classes diferentes para cada profundidade, mas adoraria encontrar uma solução mais limpa.
Eric Duminil
@EricDuminil d['primary']['secondary'].setdefault('tertiary', []).append('element')- ?? Obrigado pelo elogio, mas deixe-me ser honesto - eu nunca uso __missing__- eu sempre uso setdefault. Provavelmente devo atualizar minha conclusão / introdução ...
Aaron Hall
@AaronHall O comportamento correto é que o código deve criar um ditado, se necessário. Nesse caso, substituindo o valor atribuído anteriormente.
Nehem 13/03/19
@AaronHall Também você pode me ajudar a entender o que se entende por The bad lookup will remain in the dictionary.pensar em usar esta solução ?. Muito apreciado. Thx
nehem 13/03/19
@AaronHall O problema com isso falharia setdefault 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 para get_nested& um para o set_nestedqual aceitar uma referência para dict e lista de atributos aninhados.
Nehem 13/03/19
188
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

Teste:

a = AutoVivification()

a[1][2][3] = 4
a[1][3][3] = 5
a[1][2]['test'] = 6

print a

Resultado:

{1: {2: {'test': 6, 3: 4}, 3: {3: 5}}}
nosklo
fonte
Alguém tem esse problema ao mudar para o python 3.x? stackoverflow.com/questions/54622935/…
jason
@jason 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.
Nosklo 11/02/19
O que você usa para armazenar esses objetos? Meu objeto de autovivificação contém apenas quadros de dados e string de pandas.
jason
@jason Dependendo dos dados, eu gosto de usar JSON, arquivos csv ou mesmo um sqlitebanco de dados para armazená-lo.
Nosklo 12/02/19
30

Só porque eu não vi um tão pequeno, aqui está um ditado que fica tão aninhado quanto você gosta, sem suor:

# yo dawg, i heard you liked dicts                                                                      
def yodict():
    return defaultdict(yodict)
lata de tinta
fonte
2
@berry: Na verdade, tudo que você precisa é yodict = lambda: defaultdict(yodict).
27513 martineau
1
A versão aceita é uma subclasse de dict, para ser totalmente equivalente, precisaríamos x = Vdict(a=1, b=2)trabalhar.
wberry
@wberry: Independentemente do que está na resposta aceita, ser uma subclasse dictnã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.
22414 martineau
24

Você pode criar um arquivo YAML e lê-lo usando PyYaml .

Etapa 1: Crie um arquivo YAML, "Employment.yml":

new jersey:
  mercer county:
    pumbers: 3
    programmers: 81
  middlesex county:
    salesmen: 62
    programmers: 81
new york:
  queens county:
    plumbers: 9
    salesmen: 36

Etapa 2: leia-o em Python

import yaml
file_handle = open("employment.yml")
my_shnazzy_dictionary = yaml.safe_load(file_handle)
file_handle.close()

e agora my_shnazzy_dictionarytem todos os seus valores. Se você precisar fazer isso rapidamente, poderá criar o YAML como uma string e alimentá-lo yaml.safe_load(...).

Pete
fonte
4
O YAML é definitivamente minha escolha para inserir muitos dados profundamente aninhados (e arquivos de configuração, modelos de bancos de dados, etc ...). Se o OP não quiser arquivos extras por aí, use uma string Python comum em algum arquivo e analise-a com o YAML.
kmelvn
Bom ponto para criar seqüências YAML: essa seria uma abordagem muito mais limpa do que usar o módulo "tempfile" repetidamente.
Pete
18

Como você tem um design de esquema em estrela, você pode estruturá-lo mais como uma tabela relacional e menos como um dicionário.

import collections

class Jobs( object ):
    def __init__( self, state, county, title, count ):
        self.state= state
        self.count= county
        self.title= title
        self.count= count

facts = [
    Jobs( 'new jersey', 'mercer county', 'plumbers', 3 ),
    ...

def groupBy( facts, name ):
    total= collections.defaultdict( int )
    for f in facts:
        key= getattr( f, name )
        total[key] += f.count

Esse tipo de coisa pode ajudar bastante na criação de um design semelhante a um data warehouse, sem as despesas gerais do SQL.

S.Lott
fonte
14

Se o número de níveis de aninhamento for pequeno, eu uso collections.defaultdictpara isso:

from collections import defaultdict

def nested_dict_factory(): 
  return defaultdict(int)
def nested_dict_factory2(): 
  return defaultdict(nested_dict_factory)
db = defaultdict(nested_dict_factory2)

db['new jersey']['mercer county']['plumbers'] = 3
db['new jersey']['mercer county']['programmers'] = 81

Usando defaultdictcomo isto evita um monte de bagunça setdefault(), get()etc.

user26294
fonte
+1: defaultdict é uma das minhas adições favoritas de todos os tempos ao python. Chega de .setdefault ()!
John Fouhy
8

Esta é uma função que retorna um dicionário aninhado de profundidade arbitrária:

from collections import defaultdict
def make_dict():
    return defaultdict(make_dict)

Use-o assim:

d=defaultdict(make_dict)
d["food"]["meat"]="beef"
d["food"]["veggie"]="corn"
d["food"]["sweets"]="ice cream"
d["animal"]["pet"]["dog"]="collie"
d["animal"]["pet"]["cat"]="tabby"
d["animal"]["farm animal"]="chicken"

Itere através de tudo com algo como isto:

def iter_all(d,depth=1):
    for k,v in d.iteritems():
        print "-"*depth,k
        if type(v) is defaultdict:
            iter_all(v,depth+1)
        else:
            print "-"*(depth+1),v

iter_all(d)

Isso imprime:

- food
-- sweets
--- ice cream
-- meat
--- beef
-- veggie
--- corn
- animal
-- pet
--- dog
---- labrador
--- cat
---- tabby
-- farm animal
--- chicken

Você pode querer fazê-lo para que novos itens não possam ser adicionados ao ditado. É fácil converter recursivamente todos esses defaultdicts para dicts normais .

def dictify(d):
    for k,v in d.iteritems():
        if isinstance(v,defaultdict):
            d[k] = dictify(v)
    return dict(d)
JnBrymn
fonte
7

Eu acho setdefaultbastante útil; Ele verifica se uma chave está presente e a adiciona, se não:

d = {}
d.setdefault('new jersey', {}).setdefault('mercer county', {})['plumbers'] = 3

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:

def iterateStates(d):
    # Let's count up the total number of "plumbers" / "dentists" / etc.
    # across all counties and states
    job_totals = {}

    # I guess this is the annoying nested stuff you were talking about?
    for (state, counties) in d.iteritems():
        for (county, jobs) in counties.iteritems():
            for (job, num) in jobs.iteritems():
                # If job isn't already in job_totals, default it to zero
                job_totals[job] = job_totals.get(job, 0) + num

    # Now return an iterator of (job, number) tuples
    return job_totals.iteritems()

# Display all jobs
for (job, num) in iterateStates(d):
    print "There are %d %s in total" % (job, num)
andygeers
fonte
Eu gosto dessa solução, mas quando tento: count.setdefault (a, {}). Setdefault (b, {}). Setdefault (c, 0) + = 1 Recebo "expressão ilegal para atribuição aumentada"
dfrankow
6

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.

import sqlite3

c = sqlite3.Connection(':memory:')
c.execute('CREATE TABLE jobs (state, county, title, count)')

c.executemany('insert into jobs values (?, ?, ?, ?)', [
    ('New Jersey', 'Mercer County',    'Programmers', 81),
    ('New Jersey', 'Mercer County',    'Plumbers',     3),
    ('New Jersey', 'Middlesex County', 'Programmers', 81),
    ('New Jersey', 'Middlesex County', 'Salesmen',    62),
    ('New York',   'Queens County',    'Salesmen',    36),
    ('New York',   'Queens County',    'Plumbers',     9),
])

# some example queries
print list(c.execute('SELECT * FROM jobs WHERE county = "Queens County"'))
print list(c.execute('SELECT SUM(count) FROM jobs WHERE title = "Programmers"'))

Este é apenas um exemplo simples. Você pode definir tabelas separadas para estados, condados e cargos.

Roberto Bonvallet
fonte
5

collections.defaultdictpode ser subclassificado para fazer um ditado aninhado. Em seguida, adicione quaisquer métodos de iteração úteis a essa classe.

>>> from collections import defaultdict
>>> class nesteddict(defaultdict):
    def __init__(self):
        defaultdict.__init__(self, nesteddict)
    def walk(self):
        for key, value in self.iteritems():
            if isinstance(value, nesteddict):
                for tup in value.walk():
                    yield (key,) + tup
            else:
                yield key, value


>>> nd = nesteddict()
>>> nd['new jersey']['mercer county']['plumbers'] = 3
>>> nd['new jersey']['mercer county']['programmers'] = 81
>>> nd['new jersey']['middlesex county']['programmers'] = 81
>>> nd['new jersey']['middlesex county']['salesmen'] = 62
>>> nd['new york']['queens county']['plumbers'] = 9
>>> nd['new york']['queens county']['salesmen'] = 36
>>> for tup in nd.walk():
    print tup


('new jersey', 'mercer county', 'programmers', 81)
('new jersey', 'mercer county', 'plumbers', 3)
('new jersey', 'middlesex county', 'programmers', 81)
('new jersey', 'middlesex county', 'salesmen', 62)
('new york', 'queens county', 'salesmen', 36)
('new york', 'queens county', 'plumbers', 9)
A. Coady
fonte
1
Essa é a resposta que mais se aproxima do que eu estava procurando. Mas, idealmente, haveria todos os tipos de funções auxiliares, por exemplo, walk_keys () ou algo assim. Estou surpreso que não haja nada nas bibliotecas padrão para fazer isso.
YGA 14/03/09
4

Quanto a "blocos desagradáveis ​​de tentativa / captura":

d = {}
d.setdefault('key',{}).setdefault('inner key',{})['inner inner key'] = 'value'
print d

rendimentos

{'key': {'inner key': {'inner inner key': 'value'}}}

Você pode usar isso para converter do seu formato de dicionário simples para o formato estruturado:

fd = {('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}

for (k1,k2,k3), v in fd.iteritems():
    d.setdefault(k1, {}).setdefault(k2, {})[k3] = v
vartec
fonte
4

Você pode usar o Addict: https://github.com/mewwts/addict

>>> from addict import Dict
>>> my_new_shiny_dict = Dict()
>>> my_new_shiny_dict.a.b.c.d.e = 2
>>> my_new_shiny_dict
{'a': {'b': {'c': {'d': {'e': 2}}}}}
JnBrymn
fonte
4

defaultdict() é seu amigo!

Para um dicionário bidimensional, você pode:

d = defaultdict(defaultdict)
d[1][2] = 3

Para mais dimensões, você pode:

d = defaultdict(lambda :defaultdict(defaultdict))
d[1][2][3] = 4
Paula
fonte
Esta resposta funciona para apenas três níveis, na melhor das hipóteses. Para níveis arbitrários, considere esta resposta .
Acumenus 3/17
3

Para facilitar a iteração no seu dicionário aninhado, por que não escrever um gerador simples?

def each_job(my_dict):
    for state, a in my_dict.items():
        for county, b in a.items():
            for job, value in b.items():
                yield {
                    'state'  : state,
                    'county' : county,
                    'job'    : job,
                    'value'  : value
                }

Portanto, se você possui um dicionário aninhado compilado, a iteração sobre ele se torna simples:

for r in each_job(my_dict):
    print "There are %d %s in %s, %s" % (r['value'], r['job'], r['county'], r['state'])

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:

if not my_dict.has_key('new jersey'):
    return False

nj_dict = my_dict['new jersey']
...

Ou, um método talvez um tanto detalhado, é usar o método get:

value = my_dict.get('new jersey', {}).get('middlesex county', {}).get('salesmen', 0)

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.

import collections

def state_struct(): return collections.defaultdict(county_struct)
def county_struct(): return collections.defaultdict(job_struct)
def job_struct(): return 0

my_dict = collections.defaultdict(state_struct)

print my_dict['new jersey']['middlesex county']['salesmen']

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.

SpoonMeiser
fonte
2

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:

>>> d['new jersey/mercer county/plumbers'] = 3
>>> d['new jersey/mercer county/programmers'] = 81
>>> d['new jersey/mercer county/programmers']
81
>>> d['new jersey/mercer country']
<view which implicitly adds 'new jersey/mercer county' to queries/mutations>

Se você quiser ter uma fantasia, também pode implementar algo como:

>>> d['*/*/programmers']
<view which would contain 'programmers' entries>

mas principalmente acho que algo assim seria realmente divertido de implementar: D

Aaron Maenpaa
fonte
Acho que é uma péssima idéia - você nunca pode prever a sintaxe das chaves. Você ainda substituirá getitem e setitem, mas solicita que façam tuplas.
YGA
3
@YGA Você provavelmente está certo, mas é divertido pensar em implementar mini linguagens como essa.
Aaron Maenpaa 11/03/09
1

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.

allyourcode
fonte
1
class JobDb(object):
    def __init__(self):
        self.data = []
        self.all = set()
        self.free = []
        self.index1 = {}
        self.index2 = {}
        self.index3 = {}

    def _indices(self,(key1,key2,key3)):
        indices = self.all.copy()
        wild = False
        for index,key in ((self.index1,key1),(self.index2,key2),
                                             (self.index3,key3)):
            if key is not None:
                indices &= index.setdefault(key,set())
            else:
                wild = True
        return indices, wild

    def __getitem__(self,key):
        indices, wild = self._indices(key)
        if wild:
            return dict(self.data[i] for i in indices)
        else:
            values = [self.data[i][-1] for i in indices]
            if values:
                return values[0]

    def __setitem__(self,key,value):
        indices, wild = self._indices(key)
        if indices:
            for i in indices:
                self.data[i] = key,value
        elif wild:
            raise KeyError(k)
        else:
            if self.free:
                index = self.free.pop(0)
                self.data[index] = key,value
            else:
                index = len(self.data)
                self.data.append((key,value))
                self.all.add(index)
            self.index1.setdefault(key[0],set()).add(index)
            self.index2.setdefault(key[1],set()).add(index)
            self.index3.setdefault(key[2],set()).add(index)

    def __delitem__(self,key):
        indices,wild = self._indices(key)
        if not indices:
            raise KeyError
        self.index1[key[0]] -= indices
        self.index2[key[1]] -= indices
        self.index3[key[2]] -= indices
        self.all -= indices
        for i in indices:
            self.data[i] = None
        self.free.extend(indices)

    def __len__(self):
        return len(self.all)

    def __iter__(self):
        for key,value in self.data:
            yield key

Exemplo:

>>> db = JobDb()
>>> db['new jersey', 'mercer county', 'plumbers'] = 3
>>> db['new jersey', 'mercer county', 'programmers'] = 81
>>> db['new jersey', 'middlesex county', 'programmers'] = 81
>>> db['new jersey', 'middlesex county', 'salesmen'] = 62
>>> db['new york', 'queens county', 'plumbers'] = 9
>>> db['new york', 'queens county', 'salesmen'] = 36

>>> db['new york', None, None]
{('new york', 'queens county', 'plumbers'): 9,
 ('new york', 'queens county', 'salesmen'): 36}

>>> db[None, None, 'plumbers']
{('new jersey', 'mercer county', 'plumbers'): 3,
 ('new york', 'queens county', 'plumbers'): 9}

>>> db['new jersey', 'mercer county', None]
{('new jersey', 'mercer county', 'plumbers'): 3,
 ('new jersey', 'mercer county', 'programmers'): 81}

>>> db['new jersey', 'middlesex county', 'programmers']
81

>>>

Editar: agora retornando dicionários ao consultar caracteres curinga ( None) e valores únicos caso contrário.

Markus Jarderot
fonte
Por que retornar listas? Parece que ele deve retornar um dicionário (para que você saiba o que cada número representa) ou uma soma (já que é tudo o que você realmente pode fazer com a lista).
2111 Ben Blank
0

Eu tenho uma coisa semelhante acontecendo. Eu tenho muitos casos em que faço:

thedict = {}
for item in ('foo', 'bar', 'baz'):
  mydict = thedict.get(item, {})
  mydict = get_value_for(item)
  thedict[item] = mydict

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

value = mydict.get('foo', {}).get('bar', {}).get('baz', 0)

Então, em vez disso, eu fiz:

def dictgetter(thedict, default, *args):
  totalargs = len(args)
  for i,arg in enumerate(args):
    if i+1 == totalargs:
      thedict = thedict.get(arg, default)
    else:
      thedict = thedict.get(arg, {})
  return thedict

Que tem o mesmo efeito se você:

value = dictgetter(mydict, 0, 'foo', 'bar', 'baz')

Melhor? Acho que sim.

uzi
fonte
0

Você pode usar a recursão em lambdas e defaultdict, sem necessidade de definir nomes:

a = defaultdict((lambda f: f(f))(lambda g: lambda:defaultdict(g(g))))

Aqui está um exemplo:

>>> a['new jersey']['mercer county']['plumbers']=3
>>> a['new jersey']['middlesex county']['programmers']=81
>>> a['new jersey']['mercer county']['programmers']=81
>>> a['new jersey']['middlesex county']['salesmen']=62
>>> a
defaultdict(<function __main__.<lambda>>,
        {'new jersey': defaultdict(<function __main__.<lambda>>,
                     {'mercer county': defaultdict(<function __main__.<lambda>>,
                                  {'plumbers': 3, 'programmers': 81}),
                      'middlesex county': defaultdict(<function __main__.<lambda>>,
                                  {'programmers': 81, 'salesmen': 62})})})
topkara
fonte
0

Eu costumava usar essa função. é seguro, rápido e de fácil manutenção.

def deep_get(dictionary, keys, default=None):
    return reduce(lambda d, key: d.get(key, default) if isinstance(d, dict) else default, keys.split("."), dictionary)

Exemplo:

>>> from functools import reduce
>>> def deep_get(dictionary, keys, default=None):
...     return reduce(lambda d, key: d.get(key, default) if isinstance(d, dict) else default, keys.split("."), dictionary)
...
>>> person = {'person':{'name':{'first':'John'}}}
>>> print (deep_get(person, "person.name.first"))
John
>>> print (deep_get(person, "person.name.lastname"))
None
>>> print (deep_get(person, "person.name.lastname", default="No lastname"))
No lastname
>>>
Yuda Prawira
fonte