Quelle est la meilleure façon d'implémenter des dictionnaires imbriqués?


201

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 cela est assez douloureux; chaque fois que j'ai un nouvel état / comté / profession, je dois créer les dictionnaires des couches inférieures via des blocs try / catch odieux. De plus, je dois créer des itérateurs imbriqués ennuyeux si je veux passer en revue toutes les valeurs.

Je pourrais également utiliser des tuples comme clés, comme celles-ci:

{('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 l'itération des valeurs très simple et naturelle, mais il est plus pénible sur le plan syntaxique de faire des choses comme des agrégations et de regarder des sous-ensembles du dictionnaire (par exemple, si je veux simplement passer état par état).

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

Comment pourrais-je faire mieux?

Addendum: je suis au courant setdefault()mais cela ne fait pas vraiment de syntaxe propre. De plus, chaque sous-dictionnaire que vous créez doit encore être setdefault()défini manuellement.

Réponses:


179

Quelle est la meilleure façon d'implémenter des dictionnaires imbriqués en Python?

C'est une mauvaise idée, ne le fais pas. Au lieu de cela, utilisez un dictionnaire normal et utilisez dict.setdefaultoù apropos, donc lorsque les clés sont manquantes dans des conditions normales d'utilisation, vous obtenez le résultat attendu KeyError. Si vous insistez pour obtenir ce comportement, voici comment vous tirer une balle dans le pied:

Implémentez __missing__sur une dictsous - classe 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 comme un dict normal , au lieu de l'impression laide d'un dicton 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

(La note se self[key]trouve à 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 Vividictchaque fois qu'une clé est accessible mais manquante. (Le retour de l'affectation de valeur est utile car il nous évite également d'appeler le getter sur le dict, et malheureusement, nous ne pouvons pas le renvoyer tel qu'il est défini.)

Remarque, ce sont la même sémantique que la réponse la plus votée, mais dans la moitié des 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 d'utilisation

Voici un exemple de la façon dont ce dict pourrait être facilement utilisé pour créer une structure de dict imbriquée à la volée. Cela peut rapidement créer une arborescence hiérarchique aussi profondément 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 imprime joliment et magnifiquement pour une inspection manuelle. Mais si vous souhaitez inspecter visuellement vos données, l'implémentation __missing__pour définir une nouvelle instance de sa classe sur la clé et la renvoyer est une bien meilleure solution.

Autres alternatives, par contraste:

dict.setdefault

Bien que le demandeur pense que ce n'est pas propre, je le trouve préférable au Vividictmoi - 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 avec 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 vous ne savez pas ce que vous obtiendrez pour les clés, mais l'utilisation répétitive devient assez contraignante, et je ne pense pas que quiconque voudrait 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 inutilisées et non référencées, par exemple, il réutilise l'emplacement en mémoire:

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

Un défaut par défaut vivifié automatiquement

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

from collections import defaultdict

def vivdict():
    return defaultdict(vivdict)

Mais si vous avez besoin d'inspecter vos données, les résultats d'un défaut par défaut 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 proposée consiste à reconvertir récursivement en dict pour une inspection manuelle. Cette solution non triviale est laissée en exercice au lecteur.

Performance

Enfin, regardons les performances. 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

Basé sur la performance, dict.setdefaultfonctionne le mieux. Je le recommande fortement 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, peut-être), les performances n'ont pas vraiment d'importance - dans ce cas, j'irais avec Vividict pour la lisibilité de la sortie. Par rapport à l'objet AutoVivification (qui utilise à la __getitem__place de __missing__, qui a été conçu à cet effet), il est de loin supérieur.

Conclusion

La mise __missing__en œuvre sur une sous-classe dictpour définir et renvoyer une nouvelle instance est légèrement plus difficile que les alternatives mais présente les avantages de

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

et parce qu'elle est moins compliquée et plus performante que la modification __getitem__, elle devrait être préférée à cette méthode.

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

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

Ainsi, je préfère personnellement setdefaultles autres solutions, et j'ai dans toutes les situations où j'ai eu besoin de ce type de comportement.


Excellente réponse! Existe-t-il un moyen de spécifier une profondeur finie et un type de feuille pour un Vividict? Par exemple, 3et listpour un dict de dict de dict de listes qui pourraient être remplies d['primary']['secondary']['tertiary'].append(element). Je pourrais définir 3 classes différentes pour chaque profondeur mais j'aimerais trouver une solution plus propre.
Eric Duminil

@EricDuminil d['primary']['secondary'].setdefault('tertiary', []).append('element')- ?? Merci pour le compliment, mais permettez-moi d'être honnête - je n'utilise jamais réellement __missing__- j'utilise toujours setdefault. Je devrais probablement mettre à jour ma conclusion / intro ...
Aaron Hall

@AaronHall Le comportement correct est que le code doit créer un dict si nécessaire. Dans ce cas, en remplaçant la valeur affectée précédente.
nehem

@AaronHall Pouvez-vous également m'aider à comprendre ce que l'on veut dire The bad lookup will remain in the dictionary.lorsque j'envisage d' utiliser cette solution?. Très appréciée. Thx
nehem

@AaronHall Le problème avec celui-ci échouerait setdefaults'il emboîtait plus de deux niveaux de profondeur. Il semble qu'aucune structure en Python ne puisse offrir une véritable vivification comme décrit. J'ai dû me contenter de deux méthodes indiquant une pour get_nestedet une pour set_nestedlesquelles accepter une référence pour dict et une liste d'attributs imbriqués.
nehem

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

Essai:

a = AutoVivification()

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

print a

Production:

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

Quelqu'un a-t-il ce problème lorsqu'il est passé à Python 3.x? stackoverflow.com/questions/54622935/…
jason

@jason pickleest terrible entre les versions de python. Évitez de l'utiliser pour stocker des données que vous souhaitez conserver. Utilisez-le uniquement pour les caches et les trucs que vous pouvez vider et régénérer à volonté. Pas comme une méthode de stockage ou de sérialisation à long terme.
nosklo

Qu'utilisez-vous pour stocker ces objets? Mon objet d'autovivification ne contient que des cadres de données et une chaîne pandas.
jason

@jason En fonction des données, j'aime utiliser JSON, des fichiers csv ou même une sqlitebase de données pour les stocker.
nosklo

30

Juste parce que je n'en ai pas vu un aussi petit, voici un dicton qui s'emboîte autant que vous le souhaitez, pas de sueur:

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

2
@wberry: En fait, tout ce dont vous avez besoin est yodict = lambda: defaultdict(yodict).
martineau

1
La version acceptée est une sous-classe de dict, donc pour être totalement équivalente, nous devons x = Vdict(a=1, b=2)travailler.
wberry

@wberry: Indépendamment de ce qui est dans la réponse acceptée, être une sous-classe de dictn'était pas une exigence énoncée par le PO, qui n'a demandé que la "meilleure façon" de les mettre en œuvre - et en plus, cela ne doit / ne devrait pas importe autant en Python de toute façon.
martineau

24

Vous pouvez créer un fichier YAML et le lire à l'aide de PyYaml .

Étape 1: créez un fichier 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

É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 a maintenant my_shnazzy_dictionarytoutes vos valeurs. Si vous aviez besoin de le faire à la volée, vous pouvez créer le YAML sous forme de chaîne et l'intégrer yaml.safe_load(...).


4
YAML est définitivement mon choix pour entrer de nombreuses données profondément imbriquées (et des fichiers de configuration, des maquettes de données, etc.). Si l'OP ne veut pas de fichiers supplémentaires, utilisez simplement une chaîne Python régulière dans certains fichiers et analysez-la avec YAML.
kmelvn

Bon point sur la création de chaînes YAML: Ce serait une approche beaucoup plus propre que l'utilisation répétée du module "tempfile".
Pete

18

Comme vous avez une conception en étoile, vous souhaiterez peut-être 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 grandement contribuer à créer une conception de type entrepôt de données sans les frais généraux SQL.


14

Si le nombre de niveaux d'imbrication est petit, j'utilise collections.defaultdictpour 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

En utilisant defaultdictcomme cela évite beaucoup de désordre setdefault(), get()etc.


+1: defaultdict est l'un de mes ajouts préférés de tous les temps à python. Plus de .setdefault ()!
John Fouhy

8

Il s'agit d'une fonction qui renvoie 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"

Parcourez tout avec quelque chose comme ceci:

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 pourriez éventuellement vouloir 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 dicts normaux .

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

7

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

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

setdefaultrenvoie toujours la clé appropriée, donc vous mettez à jour les valeurs de ' d' en 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à 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)

J'aime cette solution mais quand j'essaye: count.setdefault (a, {}). Setdefault (b, {}). Setdefault (c, 0) + = 1
J'obtiens

6

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 comme 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 tables distinctes pour les États, les comtés et les titres d'emploi.


5

collections.defaultdictpeut être sous-classé pour faire 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)

1
C'est la réponse qui se rapproche le plus de ce que je cherchais. Mais idéalement, il y aurait toutes sortes de fonctions d'aide, par exemple walk_keys () ou autres. Je suis surpris qu'il n'y ait rien dans les bibliothèques standard pour ce faire.
YGA

4

Quant aux "blocs try / catch 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

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

4

defaultdict() est votre ami!

Pour un dictionnaire en 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

Cette réponse ne fonctionne que pour trois niveaux au mieux. Pour les niveaux arbitraires, considérez cette réponse .
Acumenus

3

Pour itérer facilement sur 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é, son itération 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'])

De toute évidence, votre générateur peut fournir le format de données qui vous est utile.

Pourquoi utilisez-vous des blocs try catch pour lire l'arborescence? Il est assez facile (et probablement plus sûr) de rechercher 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 envisager d'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.


2

J'aime l'idée d'envelopper cela dans une classe et de l'implémenter __getitem__et de __setitem__telle sorte qu'ils ont implémenté 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 vouliez devenir sophistiqué, vous pouvez également 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


Je pense que c'est une mauvaise idée - vous ne pouvez jamais prédire la syntaxe des clés. Vous devez toujours remplacer getitem et setitem mais leur demander de prendre des tuples.
YGA

3
@YGA Vous avez probablement raison, mais c'est amusant de penser à implémenter des mini langages comme celui-ci.
Aaron Maenpaa

1

À moins que votre ensemble 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: faciliter l'ajout de comptes, la sélection de sous-ensembles de comptes, et même agréger les comptes par état, comté, profession ou toute combinaison de ceux-ci.


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

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

>>>

Edit: retourne maintenant les dictionnaires lors d'une requête avec des caractères génériques ( None), et des valeurs uniques sinon.


Pourquoi renvoyer des listes? Il semble qu'il devrait renvoyer un dictionnaire (pour que vous sachiez ce que chaque nombre représente) ou une somme (car c'est tout ce que vous pouvez vraiment faire avec la liste).
Ben Blank

0

J'ai une chose similaire en cours. 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. Pendant ce temps, j'ai réfléchi à des moyens de mieux gérer cela. En ce moment, il y a beaucoup de

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

Au lieu de cela, 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 le faites:

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

Mieux? Je le pense.


0

Vous pouvez utiliser la récursivité dans lambdas et defaultdict, pas besoin de définir de 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})})})

0

J'avais l'habitude d'utiliser cette fonction. c'est sûr, rapide, facile à entretenir.

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
>>>
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.