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