Wiele poziomów „collection.defaultdict” w Pythonie

176

Dzięki wspaniałym ludziom z SO odkryłem możliwości oferowane przez collections.defaultdict, zwłaszcza w zakresie czytelności i szybkości. Z powodzeniem je wykorzystałem.

Teraz chciałbym zaimplementować trzy poziomy słowników, z których dwa najwyższe to, defaultdicta najniższy to int. Nie znajduję odpowiedniego sposobu, aby to zrobić. Oto moja próba:

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]

Teraz to działa, ale następujące, które są pożądane, nie:

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

Podejrzewam, że powinienem był gdzieś zadeklarować, że drugi poziom defaultdictjest typowy int, ale nie znalazłem, gdzie ani jak to zrobić.

Powodem, dla którego używam defaultdictw pierwszej kolejności, jest uniknięcie konieczności inicjowania słownika dla każdego nowego klucza.

Masz bardziej elegancką sugestię?

Dzięki pythoneers!

Morlok
źródło

Odpowiedzi:

341

Posługiwać się:

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

Spowoduje to utworzenie nowego defaultdict(int)za każdym razem, gdy uzyskuje się dostęp do nowego klucza d.

interjay
źródło
2
Jedynym problemem jest to, że nie trawi się, co oznacza, że ​​nie multiprocessingjest zadowolony z wysyłania ich tam iz powrotem.
Noah
19
@Noah: To będzie piklować, jeśli użyjesz nazwanej funkcji na poziomie modułu zamiast lambdy.
interjay
4
@ScienceFriction Czy potrzebujesz pomocy w konkretnej sprawie? Po uzyskaniu d[new_key]dostępu wywoła lambdę, która utworzy nowy defaultdict(int). A kiedy d[existing_key][new_key2]zostanie uzyskany, intzostanie utworzony nowy .
interjay
11
To jest niesamowite. Wygląda na to, że codziennie odnawiam śluby małżeńskie złożone Pythonowi.
mVChr
3
Szukasz więcej szczegółów na temat używania tej metody z multiprocessingnazwaną funkcją na poziomie modułu? To pytanie idzie dalej.
Cecilia
32

Innym sposobem na utworzenie zagnieżdżonego, dającego się trawić defaultdict jest użycie częściowego obiektu zamiast lambda:

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

To zadziała, ponieważ klasa defaultdict jest globalnie dostępna na poziomie modułu:

„Nie można wytrawiać częściowego obiektu, chyba że funkcja [lub w tym przypadku klasa], którą opakowuje, jest globalnie dostępna ... pod jej __nazwa__ (w ramach jej __modułu__)” - wytrawianie opakowanych funkcji częściowych

Nathaniel Gentile
źródło
12

Spójrz na odpowiedź nosklo jest tutaj bardziej ogólnego rozwiązania.

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

Testowanie:

a = AutoVivification()

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

print a

Wynik:

{1: {2: {'test': 6, 3: 4}, 3: {3: 5}}}
mile82
źródło
Dzięki za link @ miles82 (i edycję, @voyager). Jak pytoniczne i bezpieczne jest to podejście?
Morlock
2
Niestety to rozwiązanie nie zachowuje najbardziej poręcznej części defaultdict, czyli możliwości pisania czegoś takiego jak D ['klucz'] + = 1 bez martwienia się o istnienie klucza. To główna funkcja, do której używam defaultdict ... ale mogę sobie wyobrazić, że dynamicznie pogłębiające się słowniki też są całkiem przydatne.
rschwieb
2
@rschwieb możesz dodać moc zapisu + = 1, dodając metodę add .
spazm
5

Zgodnie z żądaniem @ rschwieb D['key'] += 1, możemy rozszerzyć poprzednie , nadpisując dodawanie przez zdefiniowanie __add__metody, aby zachowywała się bardziej jakcollections.Counter()

Pierwsza __missing__zostanie wywołana w celu utworzenia nowej pustej wartości, która zostanie przekazana do __add__. Testujemy wartość, licząc na puste wartości False.

Aby uzyskać więcej informacji na temat zastępowania, zobacz emulowanie typów liczbowych .

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

Przykłady:

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

Zamiast sprawdzać argument jest liczbą (bardzo nie-Pythona, amirite!), Możemy po prostu podać domyślną wartość 0, a następnie spróbować wykonać operację:

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
źródło
czy powinny one wywoływać NotImplemented zamiast ValueError?
spazm
5

Spóźniony na imprezę, ale dla dowolnej głębi po prostu stwierdziłem, że robię coś takiego:

from collections import defaultdict

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

Trik polega na tym, aby DeepDictsama instancja była prawidłową fabryką do konstruowania brakujących wartości. Teraz możemy robić takie rzeczy jak

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
źródło
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]: ''

Znowu żadnych błędów. Bez względu na to, ile poziomów jest zagnieżdżonych. pop też nie ma błędu

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

ACE Fly
źródło