Могу ли я сделать заказной по умолчанию в Python?

Я хотел бы объединить OrderedDict() а также defaultdict() от collections в одном объекте, который должен быть упорядоченным, по умолчанию dict. Это возможно?

11 ответов

Решение

Следующее (с использованием модифицированной версии этого рецепта) работает для меня:

from collections import OrderedDict, Callable

class DefaultOrderedDict(OrderedDict):
    # Source: http://stackru.com/a/6190500/562769
    def __init__(self, default_factory=None, *a, **kw):
        if (default_factory is not None and
           not isinstance(default_factory, Callable)):
            raise TypeError('first argument must be callable')
        OrderedDict.__init__(self, *a, **kw)
        self.default_factory = default_factory

    def __getitem__(self, key):
        try:
            return OrderedDict.__getitem__(self, key)
        except KeyError:
            return self.__missing__(key)

    def __missing__(self, key):
        if self.default_factory is None:
            raise KeyError(key)
        self[key] = value = self.default_factory()
        return value

    def __reduce__(self):
        if self.default_factory is None:
            args = tuple()
        else:
            args = self.default_factory,
        return type(self), args, None, None, self.items()

    def copy(self):
        return self.__copy__()

    def __copy__(self):
        return type(self)(self.default_factory, self)

    def __deepcopy__(self, memo):
        import copy
        return type(self)(self.default_factory,
                          copy.deepcopy(self.items()))

    def __repr__(self):
        return 'OrderedDefaultDict(%s, %s)' % (self.default_factory,
                                               OrderedDict.__repr__(self))

Вот еще одна возможность, основанная на супер () Raymond Hettinger, рассмотренном Super, протестированном на Python 2.7.X и 3.4.X:

from collections import OrderedDict, defaultdict

class OrderedDefaultDict(OrderedDict, defaultdict):
    def __init__(self, default_factory=None, *args, **kwargs):
        #in python3 you can omit the args to super
        super(OrderedDefaultDict, self).__init__(*args, **kwargs)
        self.default_factory = default_factory

Если вы посмотрите MRO класса (иначе, help(OrderedDefaultDict)), вы увидите это:

class OrderedDefaultDict(collections.OrderedDict, collections.defaultdict)
 |  Method resolution order:
 |      OrderedDefaultDict
 |      collections.OrderedDict
 |      collections.defaultdict
 |      __builtin__.dict
 |      __builtin__.object

Это означает, что когда экземпляр OrderedDefaultDict инициализируется, это относится к OrderedDictинициат, но этот в свою очередь назовет defaultdictметоды перед вызовом __builtin__.dictчто именно то, что мы хотим.

Если вы хотите простое решение, которое не требует класса, вы можете просто использовать OrderedDict.setdefault(key, default=None) или же OrderedDict.get(key, default=None), Если вы получаете / устанавливаете только из нескольких мест, скажем, в цикле, вы можете легко установить значение по умолчанию.

totals = collections.OrderedDict()

for i, x in some_generator():
    totals[i] = totals.get(i, 0) + x

Это даже проще для списков с setdefault:

agglomerate = collections.OrderedDict()

for i, x in some_generator():
    agglomerate.setdefault(i, []).append(x)

Но если вы используете его несколько раз, вероятно, лучше настроить класс, как в других ответах.

Вот еще одно решение, чтобы подумать, если ваш вариант использования прост, как мой, и вы не обязательно хотите добавить сложность DefaultOrderedDict реализация класса в вашем коде.

from collections import OrderedDict

keys = ['a', 'b', 'c']
items = [(key, None) for key in keys]
od = OrderedDict(items)

( None мое желаемое значение по умолчанию.)

Обратите внимание, что это решение не будет работать, если одним из ваших требований является динамическая вставка новых ключей со значением по умолчанию. Компромисс простоты.

Обновление 13.03.17 - я узнал об удобной функции для этого варианта использования. То же, что и выше, но вы можете пропустить строку items = ... и просто:

od = OrderedDict.fromkeys(keys)

Выход:

OrderedDict([('a', None), ('b', None), ('c', None)])

И если ваши ключи состоят из одного символа, вы можете просто передать одну строку:

OrderedDict.fromkeys('abc')

Это имеет тот же результат, что и два примера выше.

Вы также можете передать значение по умолчанию в качестве второго аргумента OrderedDict.fromkeys(...),

Другой простой подход заключается в использовании словаря get метод

>>> from collections import OrderedDict
>>> d = OrderedDict()
>>> d['key'] = d.get('key', 0) + 1
>>> d['key'] = d.get('key', 0) + 1
>>> d
OrderedDict([('key', 2)])
>>> 

Простое и элегантное решение, основанное на @NickBread. Имеет немного другой API для установки фабрики, но хорошие значения по умолчанию всегда приятно иметь.

class OrderedDefaultDict(OrderedDict):
    factory = list

    def __missing__(self, key):
        self[key] = value = self.factory()
        return value

Более простая версия ответа @zeekay:

from collections import OrderedDict

class OrderedDefaultListDict(OrderedDict): #name according to default
    def __missing__(self, key):
        self[key] = value = [] #change to whatever default you want
        return value

defaultdictупорядочивается по порядку вставки в Python 3.7+ (и CPython 3.6+).

Я создал слегка исправленную и более упрощенную версию принятого ответа, актуальную для python 3.7.

      from collections import OrderedDict
from copy import copy, deepcopy
import pickle
from typing import Any, Callable


class DefaultOrderedDict(OrderedDict):
    def __init__(
            self,
            default_factory: Callable[[], Any],
            *args,
            **kwargs,
    ):
        super().__init__(*args, **kwargs)
        self.default_factory = default_factory

    def __getitem__(self, key):
        try:
            return super().__getitem__(key)
        except KeyError:
            return self.__missing__(key)

    def __missing__(self, key):
        self[key] = value = self.default_factory()
        return value

    def __reduce__(self):
        return type(self), (self.default_factory, ), None, None, iter(self.items())

    def copy(self):
        return self.__copy__()

    def __copy__(self):
        return type(self)(self.default_factory, self)

    def __deepcopy__(self, memo):
        return type(self)(self.default_factory, deepcopy(tuple(self.items()), memo))

    def __repr__(self):
        return f'{self.__class__.__name__}({self.default_factory}, {OrderedDict(self).__repr__()})'

И, что может быть даже важнее, при условии проведения некоторых тестов.

      a = DefaultOrderedDict(list)

# testing default
assert a['key'] == []
a['key'].append(1)
assert a['key'] == [1, ]

# testing repr
assert repr(a) == "DefaultOrderedDict(<class 'list'>, OrderedDict([('key', [1])]))"

# testing copy
b = a.copy()
assert b['key'] is a['key']
c = copy(a)
assert c['key'] is a['key']
d = deepcopy(a)
assert d['key'] is not a['key']
assert d['key'] == a['key']

# testing pickle
saved = pickle.dumps(a)
restored = pickle.loads(saved)
assert restored is not a
assert restored == a

# testing order
a['second_key'] = [2, ]
a['key'] = [3, ]
assert list(a.items()) == [('key', [3, ]), ('second_key', [2, ])]

Я протестировал dict по умолчанию и обнаружил, что он также отсортирован! возможно, это было просто совпадение, но в любом случае вы можете использовать отсортированную функцию:

sorted(s.items())

я думаю, что это проще

Вдохновленный другими ответами в этой теме, вы можете использовать что-то вроде:

from collections import OrderedDict

class OrderedDefaultDict(OrderedDict):
    def __missing__(self, key):
        value = OrderedDefaultDict()
        self[key] = value
        return value

Я хотел бы знать, есть ли какие-либо недостатки инициализации другого объекта того же класса в отсутствующем методе.

Другие вопросы по тегам