Как я могу сделать как можно более "совершенным" подклассом dict?
Конечная цель - создать простой диктант, в котором ключи строчные.
Если я переопределю __getitem__
/ __setitem__
, тогда get / set не работает.Как мне заставить их работать?Конечно, мне не нужно реализовывать их по отдельности?
Я не допускаю работу травления, и мне нужно внедрить __setstate__
и т. Д.
Нужно ли repr, update и __init__
?
Должен ли я просто использовать mutablemapping
(кажется, не следует использовать UserDict
или DictMixin
)?Если так, то как?Документы не совсем поучительны.
Мой первый подход - принятый ответ, но поскольку у него есть некоторые проблемы, и поскольку никто не рассматривал альтернативу, фактически подклассыa dict
, я собираюсь сделать это здесь.
Что не так с принятым ответом?
Мне кажется, это довольно простой запрос:
Как я могу сделать как можно более "совершенным" подклассом dict?Конечная цель состоит в том, чтобы иметь простой диктант, в котором ключи строчные.
Принятый ответ на самом деле не подкласс dict
, и проверка для этого не проходит:
>>> isinstance(MyTransformedDict([('Test', 'test')]), dict)
False
В идеале любой код проверки типа должен проверять интерфейс, который мы ожидаем, или абстрактный базовый класс, но если наши объекты данных передаются в функции, которые проверяют dict
- и мы не можем «исправить»«Эти функции, этот код не будет работать.
Другие споры, которые можно сделать:
- В принятом ответе также отсутствует метод класса:
fromkeys
. Принятый ответ также имеет избыточный __dict__
- поэтому занимает больше места в памяти:
>>> s.foo = 'bar'
>>> s.__dict__
{'foo': 'bar', 'store': {'test': 'test'}}
Фактически подклассы dict
Мы можем повторно использовать методы dict через наследование.Все, что нам нужно сделать, это создать интерфейсный слой, который обеспечивает передачу ключей в dict в нижнем регистре, если они являются строками.
Если я переопределить __getitem__
/ __setitem__
, то get / setне работаетКак мне заставить их работать?Конечно, мне не нужно реализовывать их по отдельности?
Что ж, их реализация по отдельности является недостатком этого подхода и преимуществом использования MutableMapping
(см. Принятый ответ), но это действительноне намного больше работы.
Во-первых, давайте вычеркнем разницу между Python 2 и 3, создадим синглтон (_RaiseKeyError
), чтобы убедиться, что мы знаем, получим ли мы аргумент для dict.pop
, исоздайте функцию, чтобы наши строчные ключи были строчными:
from itertools import chain
try: # Python 2
str_base = basestring
items = 'iteritems'
except NameError: # Python 3
str_base = str, bytes, bytearray
items = 'items'
_RaiseKeyError = object() # singleton for no-default behavior
def ensure_lower(maybe_str):
"""dict keys can be any hashable object - only call lower if str"""
return maybe_str.lower() if isinstance(maybe_str, str_base) else maybe_str
Теперь мы реализуем - я использую super
с полными аргументами, чтобы этот код работал для Python 2 и 3:
class LowerDict(dict): # dicts take a mapping or iterable as their optional first argument
__slots__ = () # no __dict__ - that would be redundant
@staticmethod # because this doesn't make sense as a global function.
def _process_args(mapping=(), **kwargs):
if hasattr(mapping, items):
mapping = getattr(mapping, items)()
return ((ensure_lower(k), v) for k, v in chain(mapping, getattr(kwargs, items)()))
def __init__(self, mapping=(), **kwargs):
super(LowerDict, self).__init__(self._process_args(mapping, **kwargs))
def __getitem__(self, k):
return super(LowerDict, self).__getitem__(ensure_lower(k))
def __setitem__(self, k, v):
return super(LowerDict, self).__setitem__(ensure_lower(k), v)
def __delitem__(self, k):
return super(LowerDict, self).__delitem__(ensure_lower(k))
def get(self, k, default=None):
return super(LowerDict, self).get(ensure_lower(k), default)
def setdefault(self, k, default=None):
return super(LowerDict, self).setdefault(ensure_lower(k), default)
def pop(self, k, v=_RaiseKeyError):
if v is _RaiseKeyError:
return super(LowerDict, self).pop(ensure_lower(k))
return super(LowerDict, self).pop(ensure_lower(k), v)
def update(self, mapping=(), **kwargs):
super(LowerDict, self).update(self._process_args(mapping, **kwargs))
def __contains__(self, k):
return super(LowerDict, self).__contains__(ensure_lower(k))
def copy(self): # don't delegate w/ super - dict.copy() -> dict :(
return type(self)(self)
@classmethod
def fromkeys(cls, keys, v=None):
return super(LowerDict, cls).fromkeys((ensure_lower(k) for k in keys), v)
def __repr__(self):
return '{0}({1})'.format(type(self).__name__, super(LowerDict, self).__repr__())
Мы используем подход, который почти полностью подходит для любого метода или специального метода, который ссылается на ключ, но в противном случае, по наследству, мы получаем методы: len
, clear
, items
, keys
,popitem
и values
бесплатно.В то время как это потребовало некоторых тщательных размышлений, чтобы понять, как это работает, просто увидеть, что это работает.
(Обратите внимание, что haskey
устарел в Python 2, удален в Python 3.)
ВотНекоторое использование:
>>> ld = LowerDict(dict(foo='bar'))
>>> ld['FOO']
'bar'
>>> ld['foo']
'bar'
>>> ld.pop('FoO')
'bar'
>>> ld.setdefault('Foo')
>>> ld
{'foo': None}
>>> ld.get('Bar')
>>> ld.setdefault('Bar')
>>> ld
{'bar': None, 'foo': None}
>>> ld.popitem()
('bar', None)
Могу ли я предотвратить работу травления, и мне нужно реализовать __setstate__
и т. д.
травление
Иподкласс dict просто отлично справляется:
>>> import pickle
>>> pickle.dumps(ld)
b'\x80\x03c__main__\nLowerDict\nq\x00)\x81q\x01X\x03\x00\x00\x00fooq\x02Ns.'
>>> pickle.loads(pickle.dumps(ld))
{'foo': None}
>>> type(pickle.loads(pickle.dumps(ld)))
<class '__main__.LowerDict'>
__repr__
Нужно ли мне repr, update и __init__
?
Мы определилиupdate
и __init__
, но по умолчанию у вас есть прекрасный __repr__
:
>>> ld # without __repr__ defined for the class, we get this
{'foo': None}
Однако хорошо написать __repr__
для улучшения отладки вашего кода.Идеальный тест - eval(repr(obj)) == obj
.Если это легко сделать для вашего кода, я настоятельно рекомендую это:
>>> ld = LowerDict({})
>>> eval(repr(ld)) == ld
True
>>> ld = LowerDict(dict(a=1, b=2, c=3))
>>> eval(repr(ld)) == ld
True
Видите ли, это именно то, что нам нужно для воссоздания эквивалентного объекта - это то, что может отображаться в наших журналах или в следах:
>>> ld
LowerDict({'a': 1, 'c': 3, 'b': 2})
Заключение
Должен ли я просто использовать mutablemapping
(кажется, не следует использовать UserDict
или DictMixin
)? Если так, то как? Документы не совсем поучительны.
Да, это еще несколько строк кода, но они должны быть всеобъемлющими. Мое первое желание было бы использовать принятый ответ,
и если бы возникли проблемы с ним, я бы посмотрел на мой ответ - так как он немного сложнее, и нет никакой азбуки, помогающей мне правильно настроить интерфейс.
Преждевременная оптимизация усложняет поиск производительности.
MutableMapping
проще - так что он сразу получает преимущество, при прочих равных условиях. Тем не менее, чтобы выложить все различия, давайте сравним и сопоставим.
Я должен добавить, что была попытка вставить подобный словарь в модуль collections
, но был отклонен . Вы, вероятно, должны просто сделать это вместо:
my_dict[transform(key)]
Это должно быть гораздо проще отлаживать.
Сравнить и контраст
Существует 6 интерфейсных функций, реализованных с MutableMapping
(отсутствует fromkeys
) и 11 с подклассом dict
. Мне не нужно реализовывать __iter__
или __len__
, но вместо этого я должен реализовать get
, setdefault
, pop
, update
, copy
, __contains__
и fromkeys
- но это довольно тривиально, так как я могу использовать наследование для большинства этих реализаций.
MutableMapping
реализует некоторые вещи в Python, которые dict
реализует в C - так что я ожидал бы, что подкласс dict
будет более производительным в некоторых случаях.
Мы получаем бесплатное __eq__
в обоих подходах - оба из которых предполагают равенство только в том случае, если другой dict является строчными - но опять же, я думаю, подкласс dict
будет сравниваться быстрее.
Резюме:
- создание подклассов
MutableMapping
проще с меньшим количеством возможностей для ошибок, но медленнее, занимает больше памяти (см. Избыточный запрос) и завершается ошибкой isinstance(x, dict)
- создание подклассов
dict
быстрее, использует меньше памяти и передает isinstance(x, dict)
, но имеет большую сложность для реализации.
Что лучше? Это зависит от вашего определения идеального.