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.setdefault
où 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 dict
sous - 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 Vividict
chaque 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 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 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.setdefault
fonctionne 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 dict
pour 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 setdefault
les autres solutions, et j'ai dans toutes les situations où j'ai eu besoin de ce type de comportement.
Vividict
? Par exemple,3
etlist
pour un dict de dict de dict de listes qui pourraient être rempliesd['primary']['secondary']['tertiary'].append(element)
. Je pourrais définir 3 classes différentes pour chaque profondeur mais j'aimerais trouver une solution plus propre.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 toujourssetdefault
. Je devrais probablement mettre à jour ma conclusion / intro ...The bad lookup will remain in the dictionary.
lorsque j'envisage d' utiliser cette solution?. Très appréciée. Thxsetdefault
s'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 pourget_nested
et une pourset_nested
lesquelles accepter une référence pour dict et une liste d'attributs imbriqués.Essai:
Production:
la source
pickle
est 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.sqlite
base de données pour les stocker.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:
la source
yodict = lambda: defaultdict(yodict)
.dict
, donc pour être totalement équivalente, nous devonsx = Vdict(a=1, b=2)
travailler.dict
n'é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.Vous pouvez créer un fichier YAML et le lire à l'aide de PyYaml .
Étape 1: créez un fichier YAML, "Employment.yml":
Étape 2: lisez-le en Python
et a maintenant
my_shnazzy_dictionary
toutes 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égreryaml.safe_load(...)
.la source
Comme vous avez une conception en étoile, vous souhaiterez peut-être la structurer davantage comme une table relationnelle et moins comme un dictionnaire.
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.
la source
Si le nombre de niveaux d'imbrication est petit, j'utilise
collections.defaultdict
pour cela:En utilisant
defaultdict
comme cela évite beaucoup de désordresetdefault()
,get()
etc.la source
Il s'agit d'une fonction qui renvoie un dictionnaire imbriqué de profondeur arbitraire:
Utilisez-le comme ceci:
Parcourez tout avec quelque chose comme ceci:
Cela imprime:
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
defaultdict
s endict
s normaux .la source
Je trouve
setdefault
assez utile; Il vérifie si une clé est présente et l'ajoute sinon:setdefault
renvoie 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:
la source
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.
Ceci est juste un exemple simple. Vous pouvez définir des tables distinctes pour les États, les comtés et les titres d'emploi.
la source
collections.defaultdict
peut être sous-classé pour faire un dict imbriqué. Ajoutez ensuite toutes les méthodes d'itération utiles à cette classe.la source
Quant aux "blocs try / catch odieux":
les rendements
Vous pouvez l'utiliser pour convertir votre format de dictionnaire plat en format structuré:
la source
Vous pouvez utiliser Addict: https://github.com/mewwts/addict
la source
defaultdict()
est votre ami!Pour un dictionnaire en deux dimensions, vous pouvez faire:
Pour plus de dimensions, vous pouvez:
la source
Pour itérer facilement sur votre dictionnaire imbriqué, pourquoi ne pas simplement écrire un simple générateur?
Donc, si vous avez votre dictionnaire imbriqué compilé, son itération devient simple:
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:
Ou, une méthode peut-être quelque peu verbeuse, consiste à utiliser la méthode get:
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.
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.
la source
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:Si vous vouliez devenir sophistiqué, vous pouvez également implémenter quelque chose comme:
mais surtout je pense qu'une telle chose serait vraiment amusante à mettre en œuvre: D
la source
À 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.
la source
Exemple:
Edit: retourne maintenant les dictionnaires lors d'une requête avec des caractères génériques (
None
), et des valeurs uniques sinon.la source
J'ai une chose similaire en cours. J'ai beaucoup de cas où je fais:
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
Au lieu de cela, j'ai fait:
Ce qui a le même effet si vous le faites:
Mieux? Je le pense.
la source
Vous pouvez utiliser la récursivité dans lambdas et defaultdict, pas besoin de définir de noms:
Voici un exemple:
la source
J'avais l'habitude d'utiliser cette fonction. c'est sûr, rapide, facile à entretenir.
Exemple :
la source