Was ist der beste Weg, um verschachtelte Wörterbücher in Python zu implementieren?
Das ist eine schlechte Idee, tu es nicht. Verwenden Sie stattdessen ein reguläres Wörterbuch und verwenden Sie dict.setdefault
where apropos. Wenn also bei normaler Verwendung Schlüssel fehlen, erhalten Sie die erwarteten Ergebnisse KeyError
. Wenn Sie darauf bestehen, dieses Verhalten zu erreichen, gehen Sie wie folgt in den Fuß:
Implementieren Sie __missing__
in einer dict
Unterklasse, um eine neue Instanz festzulegen und zurückzugeben.
Dieser Ansatz ist seit Python 2.5 verfügbar (und dokumentiert) und druckt (für mich besonders wertvoll) hübsch wie ein normales Diktat anstelle des hässlichen Drucks eines autovivifizierten Standarddiktats:
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
(Hinweis self[key]
befindet sich auf der linken Seite der Zuweisung, daher gibt es hier keine Rekursion.)
und sagen Sie, Sie haben einige Daten:
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}
Hier ist unser Verwendungscode:
vividict = Vividict()
for (state, county, occupation), number in data.items():
vividict[state][county][occupation] = number
Und nun:
>>> 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}}}
Kritik
Ein Kritikpunkt an diesem Containertyp ist, dass unser Code lautlos fehlschlagen kann, wenn der Benutzer einen Schlüssel falsch schreibt:
>>> vividict['new york']['queens counyt']
{}
Und außerdem hätten wir jetzt einen falsch geschriebenen Landkreis in unseren Daten:
>>> 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': {}}}
Erläuterung:
Wir stellen nur eine weitere verschachtelte Instanz unserer Klasse Vividict
bereit, wenn auf einen Schlüssel zugegriffen wird, dieser jedoch fehlt. (Die Rückgabe der Wertzuweisung ist nützlich, da wir den Getter beim Diktat nicht zusätzlich aufrufen müssen und sie leider nicht zurückgeben können, während sie festgelegt wird.)
Beachten Sie, dass dies dieselbe Semantik wie die am besten bewertete Antwort ist, jedoch in der Hälfte der Codezeilen - die Implementierung von 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
Demonstration der Verwendung
Im Folgenden finden Sie nur ein Beispiel dafür, wie dieses Diktat leicht verwendet werden kann, um im Handumdrehen eine verschachtelte Diktatstruktur zu erstellen. Auf diese Weise können Sie schnell eine hierarchische Baumstruktur erstellen, die so tief ist, wie Sie möchten.
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)
Welche Ausgänge:
{'fizz': {'buzz': {}},
'foo': {'bar': {}, 'baz': {}},
'primary': {'secondary': {'tertiary': {'quaternary': {}}}}}
Und wie die letzte Zeile zeigt, druckt es hübsch und zur manuellen Überprüfung. Wenn Sie Ihre Daten jedoch visuell überprüfen möchten, ist die Implementierung __missing__
, um eine neue Instanz ihrer Klasse auf den Schlüssel zu setzen und zurückzugeben, eine weitaus bessere Lösung.
Andere Alternativen zum Kontrast:
dict.setdefault
Obwohl der Fragesteller der Meinung ist, dass dies nicht sauber ist, finde ich es Vividict
mir selbst vorzuziehen .
d = {} # or dict()
for (state, county, occupation), number in data.items():
d.setdefault(state, {}).setdefault(county, {})[occupation] = number
und nun:
>>> 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}}}
Ein Rechtschreibfehler würde lautstark fehlschlagen und unsere Daten nicht mit schlechten Informationen überladen:
>>> d['new york']['queens counyt']
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'queens counyt'
Außerdem denke ich, dass setdefault großartig funktioniert, wenn es in Schleifen verwendet wird und Sie nicht wissen, was Sie für Schlüssel erhalten werden, aber die wiederholte Verwendung wird ziemlich lästig, und ich glaube nicht, dass irgendjemand Folgendes beibehalten möchte:
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', {})
Ein weiterer Kritikpunkt ist, dass setdefault eine neue Instanz erfordert, unabhängig davon, ob sie verwendet wird oder nicht. Python (oder zumindest CPython) ist jedoch ziemlich schlau im Umgang mit nicht verwendeten und nicht referenzierten neuen Instanzen. Beispielsweise wird der Speicherort im Speicher wiederverwendet:
>>> id({}), id({}), id({})
(523575344, 523575344, 523575344)
Ein automatisch belebter Standarddikt
Dies ist eine ordentlich aussehende Implementierung, und die Verwendung in einem Skript, in dem Sie die Daten nicht überprüfen, ist genauso nützlich wie die Implementierung __missing__
:
from collections import defaultdict
def vivdict():
return defaultdict(vivdict)
Wenn Sie jedoch Ihre Daten überprüfen müssen, sehen die Ergebnisse eines automatisch belebten Standarddikts, das auf die gleiche Weise mit Daten gefüllt ist, folgendermaßen aus:
>>> 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>, {})})})
Diese Ausgabe ist ziemlich unelegant und die Ergebnisse sind ziemlich unlesbar. Die normalerweise gegebene Lösung besteht darin, zur manuellen Überprüfung rekursiv in ein Diktat umzuwandeln. Diese nicht triviale Lösung bleibt dem Leser als Übung.
Performance
Schauen wir uns zum Schluss die Leistung an. Ich subtrahiere die Kosten der Instanziierung.
>>> 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
Funktioniert je nach Leistung dict.setdefault
am besten. Ich würde es für Produktionscode sehr empfehlen, wenn Sie Wert auf Ausführungsgeschwindigkeit legen.
Wenn Sie dies für die interaktive Verwendung benötigen (möglicherweise in einem IPython-Notebook), spielt die Leistung keine Rolle. In diesem Fall würde ich Vividict verwenden, um die Lesbarkeit der Ausgabe zu gewährleisten. Im Vergleich zum AutoVivification-Objekt (das __getitem__
anstelle von verwendet wird __missing__
, das für diesen Zweck erstellt wurde) ist es weit überlegen.
Fazit
Die Implementierung __missing__
in einer Unterklasse dict
zum Festlegen und Zurückgeben einer neuen Instanz ist etwas schwieriger als Alternativen, bietet jedoch die Vorteile von
- einfache Instanziierung
- einfache Datenpopulation
- einfache Datenanzeige
und weil es weniger kompliziert und leistungsfähiger als das Modifizieren ist __getitem__
, sollte es diesem Verfahren vorgezogen werden.
Trotzdem hat es Nachteile:
- Schlechte Suchvorgänge schlagen stillschweigend fehl.
- Die schlechte Suche bleibt im Wörterbuch.
Daher bevorzuge ich persönlich setdefault
die anderen Lösungen und habe in jeder Situation, in der ich diese Art von Verhalten benötigt habe.
Vividict
? ZB3
undlist
für ein Diktat von Diktat von Listen, mit denen gefüllt werden könnted['primary']['secondary']['tertiary'].append(element)
. Ich könnte 3 verschiedene Klassen für jede Tiefe definieren, aber ich würde gerne eine sauberere Lösung finden.