Python: defaultdict von defaultdict?

320

Gibt es eine Möglichkeit, eine zu haben defaultdict(defaultdict(int)), damit der folgende Code funktioniert?

for x in stuff:
    d[x.a][x.b] += x.c_int

dmuss ad-hoc erstellt werden, abhängig von x.aund x.bElementen.

Ich könnte benutzen:

for x in stuff:
    d[x.a,x.b] += x.c_int

aber dann könnte ich nicht verwenden:

d.keys()
d[x.a].keys()
Jonathan
quelle
6
Siehe ähnliche Frage Was ist der beste Weg, um verschachtelte Wörterbücher in Python zu implementieren? . Es gibt auch einige möglicherweise nützliche Informationen in Wikipedia Artikel über Autovivification .
Martineau

Antworten:

567

Ja genau so:

defaultdict(lambda: defaultdict(int))

Das Argument von a defaultdict(in diesem Fall is lambda: defaultdict(int)) wird aufgerufen, wenn Sie versuchen, auf einen nicht vorhandenen Schlüssel zuzugreifen. Der Rückgabewert wird als neuer Wert dieses Schlüssels festgelegt, was in unserem Fall bedeutet, dass der Wert von sein d[Key_doesnt_exist]wird defaultdict(int).

Wenn Sie versuchen, von diesem letzten Standarddikt auf einen Schlüssel zuzugreifen d[Key_doesnt_exist][Key_doesnt_exist], gibt er 0 zurück. Dies ist der Rückgabewert des Arguments des letzten Standarddikts, d int(). H.

Mouad
quelle
7
es funktioniert super! Können Sie das Rationale hinter dieser Syntax erklären?
Jonathan
37
@Jonathan: Ja sicher, das Argument von a defaultdict(in diesem Fall ist lambda : defaultdict(int)) wird aufgerufen, wenn Sie versuchen, auf einen nicht vorhandenen Schlüssel zuzugreifen, und der Rückgabewert wird als neuer Wert dieses Schlüssels festgelegt, der in bedeutet unser Fall der Wert d[Key_dont_exist]sein wird defaultdict(int), und wenn Sie versuchen , einen Schlüssel aus diesem letzten defaultdict zugreifen dh d[Key_dont_exist][Key_dont_exist]0 zurückgegeben wird , die der Rückgabewert des Arguments des letzten ist , defaultdictdh int(), Hoffnung , das war hilfreich.
Mouad
25
Das Argument to defaultdictsollte eine Funktion sein. defaultdict(int)ist ein Wörterbuch, während lambda: defaultdict(int)eine Funktion ein Wörterbuch zurückgibt.
has2k1
27
@ has2k1 Das ist falsch. Das Argument für defaultdict muss aufrufbar sein. Ein Lambda ist ein Callable.
Niels Bom
2
@ RickyLevi, wenn Sie wollen, dass das funktioniert, können Sie einfach sagen: defaultdict(lambda: defaultdict(lambda: defaultdict(int)))
Darophi
51

Der Parameter für den defaultdict-Konstruktor ist die Funktion, die zum Erstellen neuer Elemente aufgerufen wird. Verwenden wir also ein Lambda!

>>> from collections import defaultdict
>>> d = defaultdict(lambda : defaultdict(int))
>>> print d[0]
defaultdict(<type 'int'>, {})
>>> print d[0]["x"]
0

Seit Python 2.7 gibt es eine noch bessere Lösung mit Counter :

>>> from collections import Counter
>>> c = Counter()
>>> c["goodbye"]+=1
>>> c["and thank you"]=42
>>> c["for the fish"]-=5
>>> c
Counter({'and thank you': 42, 'goodbye': 1, 'for the fish': -5})

Einige Bonusfunktionen

>>> c.most_common()[:2]
[('and thank you', 42), ('goodbye', 1)]

Weitere Informationen finden Sie unter PyMOTW - Sammlungen - Containerdatentypen und Python-Dokumentation - Sammlungen

Yanjost
quelle
5
Um den Kreis hier zu schließen, möchten Sie das ursprünglich gestellte Problem d = defaultdict(lambda : Counter())eher verwenden als d = defaultdict(lambda : defaultdict(int))spezifisch ansprechen.
Kaugummi
3
@gumption Sie können d = defaultdict(Counter())in diesem Fall einfach keine Notwendigkeit für ein Lambda verwenden
Deb
3
@Deb Sie haben einen kleinen Fehler - entfernen Sie die inneren Klammern, damit Sie einen aufrufbaren anstelle eines CounterObjekts übergeben. Das heißt:d = defaultdict(Counter)
Dillon Davis
29

Ich finde es etwas eleganter zu benutzen partial:

import functools
dd_int = functools.partial(defaultdict, int)
defaultdict(dd_int)

Dies ist natürlich dasselbe wie ein Lambda.

Katriel
quelle
1
Partial ist hier auch besser als Lambda, da es rekursiv angewendet werden kann :) Eine generische verschachtelte defaultdict-Factory-Methode finden Sie in meiner Antwort unten.
Campi
@Campi brauchen Sie nicht teilweise für rekursive Anwendungen, AFAICT
Clément
10

Als Referenz ist es möglich, eine generische verschachtelte defaultdictFactory-Methode zu implementieren , indem Sie:

from collections import defaultdict
from functools import partial
from itertools import repeat


def nested_defaultdict(default_factory, depth=1):
    result = partial(defaultdict, default_factory)
    for _ in repeat(None, depth - 1):
        result = partial(defaultdict, result)
    return result()

Die Tiefe definiert die Anzahl der verschachtelten Wörterbücher, bevor der in definierte Typ default_factoryverwendet wird. Zum Beispiel:

my_dict = nested_defaultdict(list, 3)
my_dict['a']['b']['c'].append('e')
Campi
quelle
Können Sie ein Anwendungsbeispiel geben? Funktioniert nicht so, wie ich es erwartet hatte. ndd = nested_defaultdict(dict) .... ndd['a']['b']['c']['d'] = 'e'WürfeKeyError: 'b'
David Marx
Hey David, Sie müssen die Tiefe Ihres Wörterbuchs in Ihrem Beispiel 3 definieren (da Sie die default_factory auch als Wörterbuch definiert haben. Nested_defaultdict (dict, 3) wird für Sie funktionieren.
Campi
Das war super hilfreich, danke! Eine Sache, die mir aufgefallen ist, ist, dass dadurch ein default_dict bei erstellt wird depth=0, was möglicherweise nicht immer erwünscht ist, wenn die Tiefe zum Zeitpunkt des Aufrufs unbekannt ist. Einfach durch Hinzufügen einer Zeile if not depth: return default_factory()am oberen Rand der Funktion zu reparieren, obwohl es wahrscheinlich eine elegantere Lösung gibt.
Brendan
8

Frühere Antworten befassten sich mit der Erstellung von zwei oder n Ebenen defaultdict. In einigen Fällen möchten Sie eine unendliche:

def ddict():
    return defaultdict(ddict)

Verwendungszweck:

>>> d = ddict()
>>> d[1]['a'][True] = 0.5
>>> d[1]['b'] = 3
>>> import pprint; pprint.pprint(d)
defaultdict(<function ddict at 0x7fcac68bf048>,
            {1: defaultdict(<function ddict at 0x7fcac68bf048>,
                            {'a': defaultdict(<function ddict at 0x7fcac68bf048>,
                                              {True: 0.5}),
                             'b': 3})})
Clément
quelle
1
Ich liebe es. Es ist teuflisch einfach, aber unglaublich nützlich. Vielen Dank!
Rosstex
6

Andere haben Ihre Frage, wie Sie Folgendes zum Laufen bringen können, richtig beantwortet:

for x in stuff:
    d[x.a][x.b] += x.c_int

Eine Alternative wäre die Verwendung von Tupeln für Schlüssel:

d = defaultdict(int)
for x in stuff:
    d[x.a,x.b] += x.c_int
    # ^^^^^^^ tuple key

Das Schöne an diesem Ansatz ist, dass er einfach ist und leicht erweitert werden kann. Wenn Sie eine dreistufige Zuordnung benötigen, verwenden Sie einfach ein Tupel mit drei Elementen für den Schlüssel.

Steven Rumbalski
quelle
4
Diese Lösung bedeutet, dass es nicht einfach ist, alle d [xa] zu erhalten, da Sie jeden Schlüssel überprüfen müssen, um festzustellen, ob xa das erste Element des Tupels ist.
Matthew Schinckel
5
Wenn Sie 3 Ebenen tief verschachteln möchten, definieren Sie es einfach als 3 Ebenen: d = defaultdict (Lambda: Standarddict (Lambda: Standarddict (int)))
Matthew Schinckel