Was ist der beste Weg, um verschachtelte Wörterbücher zu implementieren?


201

Ich habe eine Datenstruktur, die im Wesentlichen einem verschachtelten Wörterbuch entspricht. Nehmen wir an, es sieht so aus:

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

Das aufrechtzuerhalten und zu schaffen ist ziemlich schmerzhaft; Jedes Mal, wenn ich einen neuen Staat / Landkreis / Beruf habe, muss ich die Wörterbücher der unteren Ebene über widerwärtige Try / Catch-Blöcke erstellen. Außerdem muss ich nervige verschachtelte Iteratoren erstellen, wenn ich alle Werte durchgehen möchte.

Ich könnte auch Tupel als Schlüssel verwenden, wie zum Beispiel:

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

Dies macht das Durchlaufen der Werte sehr einfach und natürlich, aber es ist syntaktisch schmerzhafter, Dinge wie Aggregationen und das Betrachten von Teilmengen des Wörterbuchs zu tun (z. B. wenn ich nur von Staat zu Staat gehen möchte).

Grundsätzlich möchte ich manchmal ein verschachteltes Wörterbuch als flaches Wörterbuch betrachten, und manchmal möchte ich es tatsächlich als komplexe Hierarchie betrachten. Ich könnte das alles in eine Klasse einwickeln, aber es scheint, als hätte jemand dies bereits getan. Alternativ scheint es einige wirklich elegante syntaktische Konstruktionen zu geben, um dies zu tun.

Wie könnte ich das besser machen?

Nachtrag: Ich bin mir dessen bewusst, setdefault()aber es sorgt nicht wirklich für eine saubere Syntax. Außerdem muss jedes von Ihnen erstellte Unterwörterbuch noch setdefault()manuell eingestellt werden.

Antworten:


178

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.setdefaultwhere 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 dictUnterklasse, 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 Vividictbereit, 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 Vividictmir 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.setdefaultam 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 dictzum 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 setdefaultdie anderen Lösungen und habe in jeder Situation, in der ich diese Art von Verhalten benötigt habe.


Hervorragende Antwort! Gibt es eine Möglichkeit, eine endliche Tiefe und einen Blatttyp für a anzugeben Vividict? ZB 3und listfür ein Diktat von Diktat von Listen, mit denen gefüllt werden könnte d['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.
Eric Duminil

@EricDuminil d['primary']['secondary'].setdefault('tertiary', []).append('element')- ?? Vielen Dank für das Kompliment, aber lassen Sie mich ehrlich sein - ich benutze es nie wirklich __missing__- ich benutze es immer setdefault. Ich sollte wahrscheinlich meine Schlussfolgerung / mein Intro aktualisieren ...
Aaron Hall

@AaronHall Das richtige Verhalten ist, dass der Code bei Bedarf ein Diktat erstellen sollte. In diesem Fall durch Überschreiben des zuvor zugewiesenen Werts.
Nehem

@AaronHall Können Sie mir auch helfen, zu verstehen, was The bad lookup will remain in the dictionary.unter dieser Lösung zu verstehen ist?. Sehr geschätzt. Thx
nehem

@AaronHall Das Problem damit würde fehlschlagen, setdefaultwenn mehr als zwei Tiefenstufen verschachtelt würden . Es sieht so aus, als ob keine Struktur in Python eine echte Belebung bieten kann, wie beschrieben. Ich musste mich mit zwei Angabemethoden zufrieden geben, eine für get_nestedund eine, für set_nesteddie eine Referenz für das Diktat und eine Liste verschachtelter Attribute akzeptiert wurden.
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

Testen:

a = AutoVivification()

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

print a

Ausgabe:

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

Hat jemand dieses Problem, als er zu Python 3.x gewechselt ist? stackoverflow.com/questions/54622935/…
Jason

@ Jason pickleist schrecklich zwischen Python-Versionen. Vermeiden Sie es, Daten zu speichern, die Sie behalten möchten. Verwenden Sie es nur für Caches und Dinge, die Sie nach Belieben sichern und regenerieren können. Nicht als Langzeitspeicher- oder Serialisierungsmethode.
Nosklo

Womit speichern Sie diese Objekte? Mein Autovivifizierungsobjekt enthält nur Pandas-Datenrahmen und Zeichenfolgen.
Jason

@jason Abhängig von den Daten verwende ich gerne JSON, CSV-Dateien oder sogar eine sqliteDatenbank, um sie zu speichern.
Nosklo

30

Nur weil ich noch keinen so kleinen gesehen habe, ist hier ein Diktat, das so verschachtelt wird, wie Sie möchten, kein Schweiß:

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

2
@wberry: Eigentlich ist alles was du brauchst yodict = lambda: defaultdict(yodict).
Martineau

1
Die akzeptierte Version ist eine Unterklasse von dict. Um also vollständig gleichwertig zu sein, müssten wir x = Vdict(a=1, b=2)arbeiten.
Wberry

@wberry: Unabhängig davon, was in der akzeptierten Antwort steht, dictwar es keine Anforderung des OP, eine Unterklasse von zu sein , die nur nach dem "besten Weg" fragte, sie zu implementieren - und außerdem nicht / nicht sowieso so viel in Python.
Martineau

24

Sie können eine YAML-Datei erstellen und mit PyYaml einlesen .

Schritt 1: Erstellen Sie eine YAML-Datei "beschäftigung.yml":

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

Schritt 2: Lesen Sie es in Python

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

und hat jetzt my_shnazzy_dictionaryalle Ihre Werte. Wenn Sie dies im laufenden Betrieb tun müssen, können Sie die YAML als Zeichenfolge erstellen und in diese einspeisen yaml.safe_load(...).


4
YAML ist definitiv meine Wahl für die Eingabe vieler tief verschachtelter Daten (und Konfigurationsdateien, Datenbankmodellen usw.). Wenn das OP keine zusätzlichen Dateien haben möchte, verwenden Sie einfach eine normale Python-Zeichenfolge in einer Datei und analysieren Sie diese mit YAML.
kmelvn

Guter Punkt beim Erstellen von YAML-Zeichenfolgen: Dies wäre ein viel saubererer Ansatz als die wiederholte Verwendung des Moduls "tempfile".
Pete

18

Da Sie ein Sternschema-Design haben, möchten Sie es möglicherweise eher wie eine relationale Tabelle und weniger wie ein Wörterbuch strukturieren.

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

So etwas kann einen großen Beitrag zur Erstellung eines Data Warehouse-ähnlichen Designs ohne SQL-Overhead leisten.


14

Wenn die Anzahl der Verschachtelungsebenen gering ist, verwende ich Folgendes collections.defaultdict:

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

Mit defaultdictwie dies vermeidet eine Menge chaotisch setdefault(), get()etc.


+1: defaultdict ist eine meiner Lieblingsergänzungen zu Python. Nie mehr .setdefault ()!
John Fouhy

8

Dies ist eine Funktion, die ein verschachteltes Wörterbuch beliebiger Tiefe zurückgibt:

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

Verwenden Sie es so:

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"

Durchlaufen Sie alles mit so etwas:

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)

Dies druckt aus:

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

Möglicherweise möchten Sie es so gestalten, dass dem Diktat keine neuen Elemente hinzugefügt werden können. Es ist einfach, alle diese defaultdicts rekursiv in normale dicts umzuwandeln .

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

7

Ich finde das setdefaultsehr nützlich; Es prüft, ob ein Schlüssel vorhanden ist, und fügt ihn hinzu, wenn nicht:

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

setdefault Gibt immer den relevanten Schlüssel zurück, sodass Sie die Werte von 'd ' .

Wenn es um das Iterieren geht, können Sie sicher einen Generator leicht genug schreiben, wenn es in Python noch keinen gibt:

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)

Ich mag diese Lösung, aber wenn ich versuche: count.setdefault (a, {}). Setdefault (b, {}). Setdefault (c, 0) + = 1 Ich erhalte "illegaler Ausdruck für erweiterte Zuweisung"
dfrankow

6

Wie andere vorgeschlagen haben, könnte eine relationale Datenbank für Sie nützlicher sein. Sie können eine speicherinterne sqlite3-Datenbank als Datenstruktur verwenden, um Tabellen zu erstellen und diese dann abzufragen.

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

Dies ist nur ein einfaches Beispiel. Sie können separate Tabellen für Bundesstaaten, Landkreise und Berufsbezeichnungen definieren.


5

collections.defaultdictkann untergeordnet werden, um ein verschachteltes Diktat zu erstellen. Fügen Sie dieser Klasse dann alle nützlichen Iterationsmethoden hinzu.

>>> 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
Dies ist die Antwort, die dem am nächsten kommt, wonach ich gesucht habe. Idealerweise gibt es jedoch alle Arten von Hilfsfunktionen, z. B. walk_keys () oder dergleichen. Ich bin überrascht, dass es in den Standardbibliotheken nichts gibt, was dies tun könnte.
YGA

4

Wie für "widerliche Try / Catch-Blöcke":

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

ergibt

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

Sie können dies verwenden, um von Ihrem flachen Wörterbuchformat in ein strukturiertes Format zu konvertieren:

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

Sie können Addict verwenden: 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() ist dein Freund!

Für ein zweidimensionales Wörterbuch können Sie Folgendes tun:

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

Für weitere Dimensionen können Sie:

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

Diese Antwort funktioniert bestenfalls für nur drei Ebenen. Betrachten Sie diese Antwort für beliebige Ebenen .
Acumenus

3

Schreiben Sie einen einfachen Generator, um Ihr verschachteltes Wörterbuch einfach zu durchlaufen.

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
                }

Wenn Sie also Ihr kompiliertes verschachteltes Wörterbuch haben, wird es einfach, darüber zu iterieren:

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

Offensichtlich kann Ihr Generator jedes Datenformat liefern, das für Sie nützlich ist.

Warum verwenden Sie try catch-Blöcke, um den Baum zu lesen? Es ist einfach genug (und wahrscheinlich sicherer), abzufragen, ob ein Schlüssel in einem Diktat vorhanden ist, bevor Sie versuchen, ihn abzurufen. Eine Funktion, die Schutzklauseln verwendet, könnte folgendermaßen aussehen:

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

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

Oder eine vielleicht etwas ausführliche Methode ist die Verwendung der get-Methode:

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

Für eine etwas prägnantere Art sollten Sie sich jedoch die Verwendung eines collection.defaultdict ansehen , das seit Python 2.5 Teil der Standardbibliothek ist.

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

Ich mache hier Annahmen über die Bedeutung Ihrer Datenstruktur, aber es sollte einfach sein, sich an das anzupassen, was Sie tatsächlich tun möchten.


2

Ich mag die Idee, dies in eine Klasse zu packen __getitem__und __setitem__so zu implementieren, dass eine einfache Abfragesprache implementiert wird:

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

Wenn Sie Lust haben, können Sie auch Folgendes implementieren:

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

aber meistens denke ich, dass es wirklich Spaß machen würde, so etwas umzusetzen: D.


Ich denke, das ist eine schlechte Idee - man kann die Syntax von Schlüsseln niemals vorhersagen. Sie würden immer noch getitem und setitem überschreiben, aber sie Tupel nehmen lassen.
YGA

3
@YGA Sie haben wahrscheinlich Recht, aber es macht Spaß, über die Implementierung solcher Minisprachen nachzudenken.
Aaron Maenpaa

1

Wenn Ihr Dataset nicht sehr klein bleibt, sollten Sie eine relationale Datenbank verwenden. Es macht genau das, was Sie wollen: Machen Sie es sich einfach, Zählungen hinzuzufügen, Teilmengen von Zählungen auszuwählen und sogar Zählungen nach Bundesstaat, Landkreis, Beruf oder einer beliebigen Kombination davon zu aggregieren.


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

Beispiel:

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

>>>

Bearbeiten: Gibt jetzt Wörterbücher zurück, wenn Sie mit Platzhaltern ( None) abfragen , und ansonsten einzelne Werte.


Warum Listen zurückgeben? Es scheint, dass es entweder ein Wörterbuch (damit Sie wissen, was jede Zahl darstellt) oder eine Summe (da dies alles ist, was Sie wirklich mit der Liste tun können) zurückgeben sollte.
Ben Blank

0

Ich habe eine ähnliche Sache vor mir. Ich habe viele Fälle, in denen ich:

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

Aber viele Ebenen tief gehen. Es ist das ".get (item, {})", das der Schlüssel ist, da es ein anderes Wörterbuch erstellt, wenn es noch keines gibt. In der Zwischenzeit habe ich mir überlegt, wie ich besser damit umgehen kann. Im Moment gibt es viele

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

Also machte ich stattdessen:

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

Welches hat den gleichen Effekt, wenn Sie:

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

Besser? Ich glaube schon.


0

Sie können die Rekursion in Lambdas und Standarddict verwenden, ohne Namen definieren zu müssen:

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

Hier ist ein Beispiel:

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

Ich habe diese Funktion benutzt. Es ist sicher, schnell und leicht zu warten.

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)

Beispiel:

>>> 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
>>>
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.