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.