Кілька рівнів 'collection.defaultdict' в Python


176

Завдяки великим людям на SO, я виявив можливості, що пропонуються collections.defaultdict, особливо в читанні та швидкості. Я з успіхом використовував їх.

Тепер я хотів би реалізувати три рівні словників, два найпотужніших - defaultdictнайнижчий int. Я не знаходжу відповідного способу це зробити. Ось моя спроба:

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]

Зараз це працює, але наступне, що є бажаною поведінкою, не робить:

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

Я підозрюю, що я мав десь заявити, що другий рівень defaultdictмає тип int, але я не знайшов, де чи як це зробити.

Причиною, якою я користуюсь defaultdictв першу чергу, є те, щоб уникнути необхідності ініціалізації словника для кожного нового ключа.

Якась більш елегантна пропозиція?

Дякую пітонерам!

Відповіді:


341

Використання:

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

Це створить нову, defaultdict(int)коли буде доступ до нового ключа d.


2
Проблема лише в тому, що він не маринує, а multiprocessingзначить незадоволений тим, що надсилати їх туди-сюди.
Ной

19
@Noah: Це буде маринувати, якщо ви використовуєте названу функцію рівня модуля замість лямбда.
interjay

4
@ScienceFriction Щось конкретне, для чого вам потрібна допомога? Коли d[new_key]доступ до нього, він викличе лямбду, який створить нове defaultdict(int). І коли d[existing_key][new_key2]буде доступ до нього, intбуде створено нове .
interjay

11
Це круто. Здається, я щодня поновлюю свої подружні обітниці на Python.
mVChr

3
Шукаєте детальнішу інформацію про використання цього методу multiprocessingта що таке функція на рівні модуля? Це питання випливає далі.
Сесілія

32

Ще один спосіб зробити вибір, вкладений за замовчуванням - використовувати частковий об'єкт замість лямбда:

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

Це буде працювати, тому що клас за замовчуванням доступний у всьому світі на рівні модуля:

"Ви не можете вибрати частковий об'єкт, якщо функція [або в цьому випадку клас], який він обгортає, є глобально доступною ... під своїм __name__ (в межах свого __module__)" - Підбір обернених часткових функцій


12

Подивіться на відповідь 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

Тестування:

a = AutoVivification()

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

print a

Вихід:

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

Дякуємо за посилання @ miles82 (та редагування, @voyager). Наскільки пітонескій та безпечний такий підхід?
Морлок

2
На жаль, це рішення не зберігає найзручнішу частину вироку за замовчуванням - це можливість написати щось на зразок D ['key'] + = 1, не турбуючись про існування ключа. Це головна особливість, яку я використовую за замовчуванням для ..., але я можу уявити, що словники, що динамічно поглиблюються, теж дуже зручні.
rschwieb

2
@rschwieb ви можете додати силу запису + = 1, додавши метод add .
spazm

5

Відповідно до запиту @ rschwieb D['key'] += 1, ми можемо розширити попередній додаток, додавши __add__метод, визначаючи метод, щоб зробити це більш схожим наcollections.Counter()

Спочатку __missing__буде викликано створення нового порожнього значення, яке буде передано в __add__. Ми перевіряємо значення, розраховуючи на порожні значенняFalse .

Див. Емуляцію числових типів для отримання додаткової інформації про переосмислення.

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

Приклади:

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

Замість перевірки аргументом є число (дуже непітон, амірит!), Ми могли б просто надати значення 0 за замовчуванням, а потім спробувати операцію:

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

повинні вони піднімати NotImplemented, а не ValueError?
spazm

5

Пізно до партії, але для довільної глибини я просто виявив, що роблю щось подібне:

from collections import defaultdict

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

Тут полягає в тому, щоб зробити DeepDictекземпляр дійсною фабрикою для побудови відсутніх значень. Тепер ми можемо робити такі речі

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

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

Знову немає помилок. Неважливо, скільки рівнів вкладено. також немає помилок

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.