web-dev-qa-db-fra.com

Quel est le meilleur moyen d'implémenter des dictionnaires imbriqués?

J'ai une structure de données qui équivaut essentiellement à un dictionnaire imbriqué. Disons que cela ressemble à ceci:

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

Maintenant, maintenir et créer ceci est assez pénible; chaque fois que j'ai un nouvel état/comté/profession, je dois créer les dictionnaires de couche inférieure via des blocs try/catch odieux. De plus, je dois créer des itérateurs imbriqués ennuyants si je veux passer en revue toutes les valeurs. 

Je pourrais aussi utiliser des n-uplets comme clés, comme par exemple:

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

Cela rend les itérations très simples et naturelles, mais il est plus difficile, du point de vue syntaxique, de faire des choses comme des agrégations et de regarder des sous-ensembles du dictionnaire (par exemple, si je veux juste aller état par état).

Fondamentalement, parfois, je veux penser à un dictionnaire imbriqué comme à un dictionnaire plat, et parfois, à une hiérarchie complexe. Je pourrais envelopper tout cela dans une classe, mais il semblerait que quelqu'un l'ait déjà fait. Alternativement, il semble qu'il y ait des constructions syntaxiques très élégantes pour faire cela. 

Comment pourrais-je faire cela mieux?

Addendum: Je connais setdefault() mais cela ne permet pas une syntaxe claire. De plus, chaque sous-dictionnaire que vous créez doit toujours avoir défini manuellement setdefault().

180
YGA

Quel est le meilleur moyen d'implémenter des dictionnaires imbriqués en Python?

Implémentez __missing__ sur une sous-classe dict pour définir et renvoyer une nouvelle instance.

Cette approche est disponible (et documentée) depuis Python 2.5, et (particulièrement précieuse pour moi) elle s’imprime joliment comme un dict normal, au lieu de l’impression moche d’un disque par défaut autovivifié:

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

(Remarque: self[key] est à gauche de l'affectation, il n'y a donc pas de récursivité ici.)

et dites que vous avez des données:

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}

Voici notre code d'utilisation:

vividict = Vividict()
for (state, county, occupation), number in data.items():
    vividict[state][county][occupation] = number

Et maintenant:

>>> 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}}}

Critique

Une critique de ce type de conteneur est que si l'utilisateur mal orthographié une clé, notre code pourrait échouer en silence:

>>> vividict['new york']['queens counyt']
{}

Et en plus maintenant, nous aurions un comté mal orthographié dans nos données:

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

Explication:

Nous fournissons simplement une autre instance imbriquée de notre classe Vividict chaque fois qu'une clé est accédée mais est manquante. (Renvoyer l'affectation de valeur est utile car cela nous évite d'appeler en plus le getter sur le dict, et nous ne pouvons malheureusement pas le renvoyer tel quel.)

Notez que ce sont les mêmes sémantiques que la réponse la plus votée mais en deux lignes de code - l'implémentation de 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

Démonstration de l'utilisation

Vous trouverez ci-dessous un exemple d'utilisation simple de ce dict pour créer une structure de dict imbriquée à la volée. Cela peut rapidement créer une arborescence hiérarchique aussi complète que vous le souhaitez.

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)

Quelles sorties: 

{'fizz': {'buzz': {}},
 'foo': {'bar': {}, 'baz': {}},
 'primary': {'secondary': {'tertiary': {'quaternary': {}}}}}

Et comme le montre la dernière ligne, il s’imprime joliment et permet une inspection manuelle. Mais si vous souhaitez inspecter visuellement vos données, implémentez __missing__ pour définir une nouvelle instance de sa classe sur la clé et la renvoyer est une solution bien meilleure.

Autres alternatives, pour le contraste:

dict.setdefault

Bien que le demandeur pense que cela n’est pas propre, je le trouve préférable à Vividict moi-même.

d = {} # or dict()
for (state, county, occupation), number in data.items():
    d.setdefault(state, {}).setdefault(county, {})[occupation] = number

et maintenant:

>>> 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}}}

Une faute d'orthographe échouerait bruyamment et n'encombrerait pas nos données de mauvaises informations:

>>> d['new york']['queens counyt']
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'queens counyt'

De plus, je pense que setdefault fonctionne très bien lorsqu'il est utilisé dans des boucles et que vous ne savez pas ce que vous obtiendrez pour les clés, mais un usage répétitif devient assez fastidieux et je ne pense pas que quiconque veuille suivre ce qui suit:

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', {})

Une autre critique est que setdefault nécessite une nouvelle instance, qu'elle soit utilisée ou non. Cependant, Python (ou au moins CPython) est plutôt intelligent pour gérer les nouvelles instances non utilisées et non référencées. Par exemple, il réutilise l'emplacement en mémoire:

>>> id({}), id({}), id({})
(523575344, 523575344, 523575344)

Un defaultdict auto-vivifié

Il s'agit d'une implémentation soignée, et l'utilisation dans un script sur lequel vous n'inspectez pas les données serait aussi utile que l'implémentation de __missing__:

from collections import defaultdict

def vivdict():
    return defaultdict(vivdict)

Mais si vous devez inspecter vos données, les résultats d'un defaultdict auto-vivifié, rempli de données de la même manière, ressemblent à ceci:

>>> 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>, {})})})

Cette sortie est assez inélégante et les résultats sont assez illisibles. La solution généralement donnée est de reconvertir récursivement en dict pour une inspection manuelle. Cette solution non triviale est laissée comme un exercice pour le lecteur.

Performance

Enfin, regardons la performance. Je soustrais les coûts de l'instanciation.

>>> 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

Sur la base des performances, dict.setdefault fonctionne le mieux. Je le recommande vivement pour le code de production, dans les cas où vous vous souciez de la vitesse d'exécution. 

Si vous en avez besoin pour une utilisation interactive (dans un ordinateur portable IPython, par exemple), les performances importent peu. Dans ce cas, j'utiliserais Vividict pour la lisibilité du résultat. Par rapport à l'objet AutoVivification (qui utilise __getitem__ au lieu de __missing__, créé à cet effet), il est de loin supérieur.

Conclusion

Implémenter __missing__ sur une dict de sous-classe pour définir et renvoyer une nouvelle instance est légèrement plus difficile que les alternatives, mais présente les avantages suivants:

  • instanciation facile
  • population de données facile
  • visualisation facile des données

et comme il est moins compliqué et plus performant que de modifier __getitem__, il convient de le préférer à cette méthode.

Néanmoins, il présente des inconvénients:

  • Les mauvaises recherches échoueront en silence.
  • La mauvaise recherche restera dans le dictionnaire.

Ainsi, je préfère personnellement setdefault aux autres solutions et, dans chaque situation, j’ai eu besoin de ce type de comportement.

149
Aaron Hall
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

Essai:

a = AutoVivification()

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

print a

Sortie:

{1: {2: {'test': 6, 3: 4}, 3: {3: 5}}}
186
nosklo

Juste parce que je n'en ai pas vu d'aussi petit, voici un dict qui est aussi imbriqué que vous le souhaitez, pas de panique:

# yo dawg, i heard you liked dicts                                                                      
def yodict():
    return defaultdict(yodict)
29
paint can

Vous pouvez créer un fichier YAML et le lire avec PyYaml .

Étape 1: Créez un fichier YAML, "emploi.yml":

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

Étape 2: Lisez-le en Python

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

et maintenant my_shnazzy_dictionary a toutes vos valeurs. Si vous deviez le faire à la volée, vous pouvez créer le YAML sous forme de chaîne et l'insérer dans yaml.safe_load(...).

22
Pete

Comme vous avez une conception en étoile, vous pouvez la structurer davantage comme une table relationnelle et moins comme un dictionnaire.

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

Ce genre de chose peut faire beaucoup pour créer une conception semblable à un entrepôt de données sans les frais généraux SQL.

17
S.Lott

Si le nombre de niveaux d'imbrication est faible, j'utilise collections.defaultdict pour cela:

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

Utiliser defaultdict comme ceci évite beaucoup de setdefault(), get(), etc.

13
user26294

C'est une fonction qui retourne un dictionnaire imbriqué de profondeur arbitraire:

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

Utilisez-le comme ceci:

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"

Itérez à travers tout avec quelque chose comme ça:

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)

Cela imprime:

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

Vous voudrez peut-être éventuellement faire en sorte que de nouveaux éléments ne puissent pas être ajoutés au dict. Il est facile de convertir récursivement tous ces defaultdicts en normal dicts.

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

Je trouve setdefault très utile; Il vérifie si une clé est présente et l'ajoute sinon:

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

setdefault renvoie toujours la clé appropriée, vous mettez donc à jour les valeurs de 'd' à la place.

En ce qui concerne l'itération, je suis sûr que vous pourriez écrire un générateur assez facilement s'il n'en existe pas déjà un en 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)
7
andygeers

Comme d'autres l'ont suggéré, une base de données relationnelle pourrait vous être plus utile. Vous pouvez utiliser une base de données sqlite3 en mémoire en tant que structure de données pour créer des tables, puis les interroger.

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"'))

Ceci est juste un exemple simple. Vous pouvez définir des tableaux distincts pour les états, les comtés et les intitulés de poste.

6
Roberto Bonvallet

defaultdict() est votre ami!

Pour un dictionnaire à deux dimensions, vous pouvez faire:

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

Pour plus de dimensions, vous pouvez: 

d = defaultdict(lambda :defaultdict(defaultdict))
d[1][2][3] = 4
5
Paula

collections.defaultdict peut être sous-classé pour créer un dict imbriqué. Ajoutez ensuite toutes les méthodes d'itération utiles à cette 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)
5
A. Coady

Pour ce qui est des "blocs d'essai/capture odieux":

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

les rendements

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

Vous pouvez l’utiliser pour convertir votre format de dictionnaire plat en format structuré:

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
4
vartec

Pour parcourir facilement votre dictionnaire imbriqué, pourquoi ne pas simplement écrire un simple générateur?

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
                }

Donc, si vous avez votre dictionnaire imbriqué compilé, itérer dessus devient simple:

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

Il est évident que votre générateur peut générer le format de données qui vous est utile.

Pourquoi utilisez-vous des blocs d'arrêt pour lire l'arbre? Il est assez facile (et probablement plus sûr) de demander si une clé existe dans un dict avant d'essayer de la récupérer. Une fonction utilisant des clauses de garde pourrait ressembler à ceci:

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

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

Ou, une méthode peut-être quelque peu verbeuse, consiste à utiliser la méthode get:

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

Mais pour une manière un peu plus succincte, vous voudrez peut-être utiliser un collections.defaultdict , qui fait partie de la bibliothèque standard depuis 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']

Je fais des hypothèses sur la signification de votre structure de données ici, mais il devrait être facile de s’adapter à ce que vous voulez réellement faire.

3
SpoonMeiser

Vous pouvez utiliser 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}}}}}
3
JnBrymn

J'aime l'idée de placer cela dans une classe et d'implémenter __getitem__ et __setitem__ de telle sorte qu'ils implémentent un langage de requête simple:

>>> 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>

Si vous voulez avoir l’imagination, vous pouvez aussi implémenter quelque chose comme:

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

mais surtout je pense qu'une telle chose serait vraiment amusante à mettre en œuvre: D 

2
Aaron Maenpaa

À moins que votre jeu de données ne reste assez petit, vous pouvez envisager d'utiliser une base de données relationnelle. Il fera exactement ce que vous voulez: il est facile d’ajouter des comptes, de sélectionner des sous-ensembles de comptes et même d’agréger des comptes par État, comté, profession ou une combinaison de ces éléments.

1
allyourcode
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

Exemple:

>>> 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

>>>

Édition: Renvoie maintenant les dictionnaires lors de requêtes avec des caractères génériques (None), et des valeurs uniques autrement.

1
Markus Jarderot

Vous pouvez utiliser la récursivité dans lambdas et defaultdict, inutile de définir des noms:

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

Voici un exemple:

>>> 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})})})
1
topkara

J'avais l'habitude d'utiliser cette fonction. c'est sûr, rapide, facilement maintenable.

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)

Exemple :

>>> 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
>>>
0
Yuda Prawira

J'ai une chose similaire va. J'ai beaucoup de cas où je fais:

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

Mais aller à plusieurs niveaux en profondeur. C'est le ".get (item, {})" qui est la clé car il fera un autre dictionnaire s'il n'y en a pas déjà un. En attendant, j'ai réfléchi à des façons de gérer .__ ceci mieux. En ce moment, il y a beaucoup de

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

Alors à la place, j'ai fait:

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

Ce qui a le même effet si vous faites:

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

Meilleur? Je le pense.

0
uzi