Mehrere Ebenen von 'collection.defaultdict' in Python

176

Dank einiger großartiger Leute auf SO habe ich die Möglichkeiten entdeckt, die sich bieten collections.defaultdict, insbesondere in Bezug auf Lesbarkeit und Geschwindigkeit. Ich habe sie mit Erfolg eingesetzt.

Jetzt möchte ich drei Ebenen von Wörterbüchern implementieren, die zwei obersten defaultdictund die unterste int. Ich finde keinen geeigneten Weg, dies zu tun. Hier ist mein Versuch:

from collections import defaultdict
d = defaultdict(defaultdict)
a = [("key1", {"a1":22, "a2":33}),
     ("key2", {"a1":32, "a2":55}),
     ("key3", {"a1":43, "a2":44})]
for i in a:
    d[i[0]] = i[1]

Nun funktioniert dies, aber das Folgende, was das gewünschte Verhalten ist, funktioniert nicht:

d["key4"]["a1"] + 1

Ich vermute, ich hätte irgendwo erklären sollen, dass die zweite Ebene defaultdictvom Typ ist int, aber ich habe nicht gefunden, wo oder wie ich das tun soll.

Der Grund, den ich defaultdictin erster Linie verwende, besteht darin, zu vermeiden, dass das Wörterbuch für jeden neuen Schlüssel initialisiert werden muss.

Noch eleganterer Vorschlag?

Danke Pythononeer!

Morlock
quelle

Antworten:

341

Verwenden:

from collections import defaultdict
d = defaultdict(lambda: defaultdict(int))

Dadurch wird bei defaultdict(int)jedem Zugriff auf einen neuen Schlüssel ein neuer Schlüssel erstellt d.

Interjay
quelle
2
Das einzige Problem ist, dass es nicht gebeizt wird, was bedeutet, dass multiprocessinges unglücklich ist, diese hin und her zu senden.
Noah
19
@Noah: Wenn Sie eine benannte Funktion auf Modulebene anstelle eines Lambdas verwenden, wird dies behoben.
Interjay
4
@ScienceFriction Gibt es etwas Spezielles, bei dem Sie Hilfe benötigen? Beim d[new_key]Zugriff wird das Lambda aufgerufen, wodurch ein neues erstellt wird defaultdict(int). Und wenn darauf d[existing_key][new_key2]zugegriffen wird, wird eine neue interstellt.
Interjay
11
Das ist fantastisch. Es scheint, dass ich meine Eheversprechen an Python täglich erneuere.
MVChr
3
Suchen Sie nach weiteren Details zur Verwendung dieser Methode mit multiprocessingund was ist eine benannte Funktion auf Modulebene? Diese Frage folgt.
Cecilia
32

Eine andere Möglichkeit, ein auswählbares, verschachteltes Standarddikt zu erstellen, besteht darin, ein Teilobjekt anstelle eines Lambda zu verwenden:

from functools import partial
...
d = defaultdict(partial(defaultdict, int))

Dies funktioniert, da auf die defaultdict-Klasse auf Modulebene global zugegriffen werden kann:

"Sie können ein Teilobjekt nur dann auswählen, wenn auf die Funktion [oder in diesem Fall die Klasse], die es umschließt, global zugegriffen werden kann ... unter seinem __name__ (innerhalb seines __moduls__)" - Beizen umschlossener Teilfunktionen

Nathaniel Gentile
quelle
12

Schauen Sie sich hier die Antwort von nosklo an , um eine allgemeinere Lösung zu finden.

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}}}
Meilen82
quelle
Vielen Dank für den Link @ miles82 (und die Bearbeitung, @voyager). Wie pythonesk und sicher ist dieser Ansatz?
Morlock
2
Leider bewahrt diese Lösung nicht den handlichsten Teil von defaultdict, dh die Fähigkeit, etwas wie D ['key'] + = 1 zu schreiben, ohne sich um die Existenz des Schlüssels zu sorgen. Das ist die Hauptfunktion, für die ich defaultdict verwende ... aber ich kann mir vorstellen, dass dynamisch vertiefende Wörterbücher auch ziemlich praktisch sind.
Rschwieb
2
@rschwieb Sie können die Fähigkeit zum Schreiben von + = 1 hinzufügen , indem Sie die Methode add hinzufügen .
Spazm
5

Gemäß der Anfrage von @ rschwieb D['key'] += 1können wir die vorherige erweitern, indem wir die Addition durch Definieren der __add__Methode überschreiben , damit sich diese eher wie eine verhältcollections.Counter()

Zuerst __missing__wird aufgerufen, um einen neuen leeren Wert zu erstellen, an den übergeben wird __add__. Wir testen den Wert und zählen auf leere Werte False.

Weitere Informationen zum Überschreiben finden Sie unter Emulieren numerischer Typen .

from numbers import Number


class autovivify(dict):
    def __missing__(self, key):
        value = self[key] = type(self)()
        return value

    def __add__(self, x):
        """ override addition for numeric types when self is empty """
        if not self and isinstance(x, Number):
            return x
        raise ValueError

    def __sub__(self, x):
        if not self and isinstance(x, Number):
            return -1 * x
        raise ValueError

Beispiele:

>>> import autovivify
>>> a = autovivify.autovivify()
>>> a
{}
>>> a[2]
{}
>>> a
{2: {}}
>>> a[4] += 1
>>> a[5][3][2] -= 1
>>> a
{2: {}, 4: 1, 5: {3: {2: -1}}}

Anstatt zu überprüfen, ob das Argument eine Zahl ist (sehr nicht Python, Amirite!), Könnten wir einfach einen Standardwert von 0 angeben und dann die Operation versuchen:

class av2(dict):
    def __missing__(self, key):
        value = self[key] = type(self)()
        return value

    def __add__(self, x):
        """ override addition when self is empty """
        if not self:
            return 0 + x
        raise ValueError

    def __sub__(self, x):
        """ override subtraction when self is empty """
        if not self:
            return 0 - x
        raise ValueError
spazm
quelle
sollten diese eher NotImplemented als ValueError auslösen?
Spazm
5

Spät zur Party, aber für eine willkürliche Tiefe habe ich gerade so etwas gemacht:

from collections import defaultdict

class DeepDict(defaultdict):
    def __call__(self):
        return DeepDict(self.default_factory)

Der Trick dabei besteht im Wesentlichen darin, die DeepDictInstanz selbst zu einer gültigen Factory zum Erstellen fehlender Werte zu machen. Jetzt können wir Dinge wie tun

dd = DeepDict(DeepDict(list))
dd[1][2].extend([3,4])
sum(dd[1][2])  # 7

ddd = DeepDict(DeepDict(DeepDict(list)))
ddd[1][2][3].extend([4,5])
sum(ddd[1][2][3])  # 9
Rad Haring
quelle
1
def _sub_getitem(self, k):
    try:
        # sub.__class__.__bases__[0]
        real_val = self.__class__.mro()[-2].__getitem__(self, k)
        val = '' if real_val is None else real_val
    except Exception:
        val = ''
        real_val = None
    # isinstance(Avoid,dict)也是true,会一直递归死
    if type(val) in (dict, list, str, tuple):
        val = type('Avoid', (type(val),), {'__getitem__': _sub_getitem, 'pop': _sub_pop})(val)
        # 重新赋值当前字典键为返回值,当对其赋值时可回溯
        if all([real_val is not None, isinstance(self, (dict, list)), type(k) is not slice]):
            self[k] = val
    return val


def _sub_pop(self, k=-1):
    try:
        val = self.__class__.mro()[-2].pop(self, k)
        val = '' if val is None else val
    except Exception:
        val = ''
    if type(val) in (dict, list, str, tuple):
        val = type('Avoid', (type(val),), {'__getitem__': _sub_getitem, 'pop': _sub_pop})(val)
    return val


class DefaultDict(dict):
    def __getitem__(self, k):
        return _sub_getitem(self, k)

    def pop(self, k):
        return _sub_pop(self, k)

In[8]: d=DefaultDict()
In[9]: d['a']['b']['c']['d']
Out[9]: ''
In[10]: d['a']="ggggggg"
In[11]: d['a']
Out[11]: 'ggggggg'
In[12]: d['a']['pp']
Out[12]: ''

Wieder keine Fehler. Egal wie viele Ebenen verschachtelt sind. Pop auch kein Fehler

dd = DefaultDict ({"1": 333333})

ACE Fly
quelle