2010-04-08 2 views
116

Благодаря отличным людям на SO я обнаружил возможности, предлагаемые collections.defaultdict, особенно в читабельности и скорости. Я использовал их для успеха.Множество уровней 'collection.defaultdict' в Python

Теперь я хотел бы реализовать три уровня словарей, два верхних - 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, заключается в том, чтобы избежать необходимости инициализировать словарь для каждого нового ключа.

Не более элегантное предложение?

Спасибо, pythoneers!

ответ

244

Использование:

d = defaultdict(lambda: defaultdict(int)) 

Это создаст новый defaultdict(int) всякий раз, когда новый ключ доступен в d.

+0

только проблема в том, что не будет рассол, а это означает 'multiprocessing' несчастен об отправке их обратно и вперед. – Noah

+15

@Noah: он будет мариновать, если вы используете именованную функцию уровня модуля вместо лямбда. – interjay

+0

, конечно, глупо меня. – Noah

10

Посмотрите на ответ nosklo here для более общего решения.

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 

Testing:

a = AutoVivification() 

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

print a 

Output:

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

Спасибо за ссылку @ miles82 (и править, @voyager). Насколько питонезким и безопасным является такой подход? – Morlock

+0

К сожалению, это решение не сохраняет самую удобную часть defaultdict, которая позволяет записывать что-то вроде D ['key'] + = 1, не беспокоясь о существовании ключа. Это основная функция, которую я использую defaultdict для ... но я могу представить, что динамически углубляющиеся словари тоже очень удобны. – rschwieb

+1

@rschwieb вы можете добавить мощность для записи + = 1, добавив метод __add__. – spazm

3

По просьбе @ rschwieb для D['key'] += 1, мы можем расширить previous путем переопределения дополнение определяя __add__ метод, чтобы сделать это вести себя как collections.Counter()

Первый __missing__ будет называться, чтобы создать новый пустой значение, которое будет передано в __add__. Мы тестируем значение, считая пустые значения False.

См. emulating numeric types для получения дополнительной информации об переопределении.

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

Вместо того, чтобы проверять аргумент является числом (очень непитоновские, 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 
+0

должны ли они повышать NotImplemented, а не ValueError? – spazm

13

Другой способ сделать pickleable, вложенная defaultdict является использование частичного объекта вместо лямбда:

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

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

"You can't pickle a partial object unless the function [or in this case, class] it wraps is globally accessible ... under its __name__ (within its __module__)" -- Pickling wrapped partial functions

Смежные вопросы