Как я могу сделать как можно более «совершенным» подклассом dict?
Конечная цель состоит в том, чтобы иметь простой диктант, в котором ключи строчные.
Если я переопределить __getitem__
/ __setitem__
, то получить / установить не работает. Как мне заставить их работать? Конечно, мне не нужно реализовывать их индивидуально?
Предотвращаю ли я травление от работы, и нужно ли его внедрять и
__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__
, то получить / установить не работает. Как мне заставить их работать? Конечно, мне не нужно реализовывать их индивидуально?
Что ж, их реализация по отдельности является недостатком этого подхода и преимуществом использования MutableMapping
(см. Принятый ответ), но на самом деле это не так уж много работы.
Во-первых, давайте выясним разницу между Python 2 и 3, создадим singleton ( _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}
Тем не менее, полезно написать a __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
)? Если да, то как? Документы не совсем поучительны.
Да, это еще несколько строк кода, но они должны быть всеобъемлющими. Первым делом я хотел бы использовать принятый ответ, и если с ним возникнут проблемы, я бы посмотрел на свой ответ - так как он немного сложнее, и не было ABC, чтобы помочь мне правильно настроить интерфейс.
Преждевременная оптимизация усложняет поиск производительности.
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
подкласс будет сравниваться быстрее.
Резюме:
- создание подклассов
MutableMapping
проще с меньшим количеством возможностей для ошибок, но медленнее, занимает больше памяти (см. избыточный dict) и терпит неудачуisinstance(x, dict)
- создание подклассов
dict
быстрее, использует меньше памяти и проходит isinstance(x, dict)
, но имеет большую сложность для реализации.
Что является более совершенным? Это зависит от вашего определения идеального.