Несколько уровней «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

Еще один способ сделать вложенный defaultdict с возможностью выбора - использовать частичный объект вместо лямбды:

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

Это будет работать, потому что класс defaultdict доступен глобально на уровне модуля:

«Вы не можете выбрать частичный объект, если только функция [или, в данном случае, класс], которую он оборачивает, не доступна глобально ... под его __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
К сожалению, это решение не сохраняет самую простую часть defaultdict, которая позволяет писать что-то вроде D ['key'] + = 1, не беспокоясь о существовании ключа. Это основная функция, для которой я использую defaultdict ... но я могу себе представить, что динамически расширяющиеся словари тоже очень удобны.
rschwieb

2
@rschwieb вы можете добавить возможность написать + = 1, добавив метод add .
спазм

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

Вместо того, чтобы проверять аргумент - это Number (очень не python, amirite!), Мы можем просто предоставить значение по умолчанию 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?
спазм

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

Нет ошибок снова. Неважно, сколько уровней вложено. не появляется также ошибка

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.