Как "идеально" переопределить dict?
Как я могу создать максимально "совершенный" подкласс dict? Конечная цель - получить простой dict, в котором ключи написаны строчными буквами.
Казалось бы, должен быть какой-то крошечный набор примитивов, который я могу переопределить, чтобы заставить это работать, но, согласно всем моим исследованиям и попыткам, похоже, что это не так:
Если я переопределю
__getitem__
/__setitem__
, тоget
/set
не будут работать. Как я могу заставить их работать? Конечно, мне не нужно реализовывать их по отдельности?Я мешаю работе травления, и нужно ли мне это реализовать
__setstate__
и т.д.?Должен ли я просто использовать mutablemapping (кажется, не следует использовать
UserDict
илиDictMixin
)? Если да, то как? Документы не совсем информативны.
Вот моя первая попытка, get()
не работает, и, без сомнения, есть много других незначительных проблем:
class arbitrary_dict(dict):
"""A dictionary that applies an arbitrary key-altering function
before accessing the keys."""
def __keytransform__(self, key):
return key
# Overridden methods. List from
# https://pythonly.ru/questions/2390827/how-to-properly-subclass-dict
def __init__(self, *args, **kwargs):
self.update(*args, **kwargs)
# Note: I'm using dict directly, since super(dict, self) doesn't work.
# I'm not sure why, perhaps dict is not a new-style class.
def __getitem__(self, key):
return dict.__getitem__(self, self.__keytransform__(key))
def __setitem__(self, key, value):
return dict.__setitem__(self, self.__keytransform__(key), value)
def __delitem__(self, key):
return dict.__delitem__(self, self.__keytransform__(key))
def __contains__(self, key):
return dict.__contains__(self, self.__keytransform__(key))
class lcdict(arbitrary_dict):
def __keytransform__(self, key):
return str(key).lower()
Переведено автоматически
Ответ 1
Вы можете довольно легко написать объект, который ведет себя как dict
ABCs (абстрактные базовые классы) из collections.abc
модуля. Он даже сообщает вам, если вы пропустили метод, поэтому ниже приведена минимальная версия, которая отключает ABC.
from collections.abc import MutableMapping
class TransformedDict(MutableMapping):
"""A dictionary that applies an arbitrary key-altering
function before accessing the keys"""
def __init__(self, *args, **kwargs):
self.store = dict()
self.update(dict(*args, **kwargs)) # use the free update to set keys
def __getitem__(self, key):
return self.store[self._keytransform(key)]
def __setitem__(self, key, value):
self.store[self._keytransform(key)] = value
def __delitem__(self, key):
del self.store[self._keytransform(key)]
def __iter__(self):
return iter(self.store)
def __len__(self):
return len(self.store)
def _keytransform(self, key):
return key
Вы получаете несколько бесплатных методов из ABC:
class MyTransformedDict(TransformedDict):
def _keytransform(self, key):
return key.lower()
s = MyTransformedDict([('Test', 'test')])
assert s.get('TEST') is s['test'] # free get
assert 'TeSt' in s # free __contains__
# free setdefault, __eq__, and so on
import pickle
# works too since we just use a normal dict
assert pickle.loads(pickle.dumps(s)) == s
Я бы не стал создавать подкласс dict
(или другие встроенные функции) напрямую. Часто это не имеет смысла, потому что то, что вы на самом деле хотите сделать, это реализовать интерфейс dict
. И это именно то, для чего существуют ABCs.
Ответ 2
Как я могу создать максимально "совершенный" подкласс dict?
Конечная цель - получить простой dict, в котором ключи написаны строчными буквами.
Если я переопределю
__getitem__
/__setitem__
, то get / set не будут работать. Как мне заставить их работать? Конечно, мне не нужно реализовывать их по отдельности?Я мешаю работе травления, и нужно ли мне это реализовать
__setstate__
и т.д.?Нужны ли мне repr, update и
__init__
?Должен ли я просто использовать
mutablemapping
(кажется, не следует использоватьUserDict
илиDictMixin
)? Если да, то как? Документы не совсем информативны.
Принятым ответом был бы мой первый подход, но поскольку у него есть некоторые проблемы, и поскольку никто не рассматривал альтернативу, фактически подкласс a dict
, я собираюсь сделать это здесь.
Что не так с принятым ответом?
Мне это кажется довольно простым запросом:
Как я могу создать максимально "совершенный" подкласс dict? Конечная цель - получить простой dict, в котором ключи написаны строчными буквами.
Принятый ответ на самом деле не относится к подклассу dict
, и тест на это завершается неудачей:
>>> isinstance(MyTransformedDict([('Test', 'test')]), dict)
False
В идеале любой код для проверки типов должен тестироваться на ожидаемый интерфейс или абстрактный базовый класс, но если наши объекты данных передаются в функции, которые тестируются на dict
- и мы не можем "исправить" эти функции, этот код завершится ошибкой.
Другие замечания, которые можно сделать:
- В принятом ответе также отсутствует classmethod:
fromkeys
. Принятый ответ также имеет избыточный
__dict__
- следовательно, занимает больше места в памяти:>>> s.foo = 'bar'
>>> s.__dict__
{'foo': 'bar', 'store': {'test': 'test'}}
Фактически создание подклассов dict
Мы можем повторно использовать методы dict с помощью наследования. Все, что нам нужно сделать, это создать уровень интерфейса, который гарантирует, что ключи передаются в dict в нижнем регистре, если они являются строками.
Если я переопределю
__getitem__
/__setitem__
, то get / set не будут работать. Как мне заставить их работать? Конечно, мне не нужно реализовывать их по отдельности?
Что ж, реализация каждого из них по отдельности является недостатком этого подхода и плюсом использования 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__
etc?
маринование
И подкласс 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
)? Если да, то как? Документы не совсем информативны.
Да, это еще несколько строк кода, но они должны быть исчерпывающими. Моим первым желанием было бы использовать принятый ответ, и если бы с ним возникли проблемы, я бы тогда посмотрел на свой ответ - поскольку он немного сложнее, и нет ABC, который помог бы мне настроить мой интерфейс правильно.
Преждевременная оптимизация приводит к усложнению в поисках производительности. MutableMapping
проще - поэтому при прочих равных условиях он получает немедленное преимущество. Тем не менее, чтобы выявить все различия, давайте сравним.
Я должен добавить, что была попытка поместить аналогичный словарь в collections
модуль, но это было отклонено. Вероятно, вам следует просто сделать это вместо этого:
my_dict[transform(key)]
Его должно быть гораздо проще отлаживать.
Сравнивать и противопоставлять
В MutableMapping
(которая отсутствует fromkeys
) реализовано 6 интерфейсных функций, а в dict
подклассе - 11. Мне не нужно реализовывать __iter__
or __len__
, но вместо этого я должен реализовать get
, setdefault
, pop
update
, copy
__contains__
, fromkeys
,,, и,,, - но они довольно тривиальны, поскольку я могу использовать наследование для большинства этих реализаций.
MutableMapping
Реализует некоторые вещи в Python, которые dict
реализует в C - поэтому я ожидаю, что dict
подкласс в некоторых случаях будет более производительным.
Мы получаем free __eq__
в обоих подходах - оба из которых предполагают равенство, только если другой dict полностью состоит из нижнего регистра - но опять же, я думаю, что dict
подкласс будет сравниваться быстрее.
Краткие сведения:
- создание подклассов
MutableMapping
проще, меньше возможностей для ошибок, но медленнее, занимает больше памяти (см. Избыточный dict) и завершается сбоемisinstance(x, dict)
- создание подклассов
dict
выполняется быстрее, использует меньше памяти и проходит быстроisinstance(x, dict)
, но его сложнее реализовать.
Что более идеально? Это зависит от вашего определения perfect .
Ответ 3
После опробования обоих двух лучших предложений я остановился на сомнительно выглядящем среднем пути для Python 2.7. Возможно, 3 более разумно, но для меня:
class MyDict(MutableMapping):
# ... the few __methods__ that mutablemapping requires
# and then this monstrosity
@property
def __class__(self):
return dict
который я действительно ненавижу, но, кажется, соответствует моим потребностям, а именно:
- можно переопределить
**my_dict
- если вы наследуете от
dict
, это позволяет обойти ваш код. попробуйте. - это делает # 2 неприемлемым для меня во все времена, поскольку это довольно распространенное явление в коде python
- если вы наследуете от
- маскируется под
isinstance(my_dict, dict)
- полностью контролируемое поведение
- поэтому я не могу наследовать от
dict
- поэтому я не могу наследовать от
Если вам нужно отличить себя от других, лично я использую что-то вроде этого (хотя я бы порекомендовал имена получше):
def __am_i_me(self):
return True
@classmethod
def __is_it_me(cls, other):
try:
return other.__am_i_me()
except Exception:
return False
Пока вам нужно только узнавать себя внутренне, таким образом, будет сложнее случайно вызвать __am_i_me
из-за перегруженности имен python (это переименовано в _MyDict__am_i_me
из всего, что вызывает вне этого класса). Немного более приватный, чем _method
s, как на практике, так и в культурном плане.
Пока у меня нет жалоб, кроме серьезно подозрительного на вид __class__
переопределения. Я был бы взволнован, узнав о любых проблемах, с которыми другие сталкиваются при этом, хотя я не до конца понимаю последствия. Но до сих пор у меня не возникало никаких проблем, и это позволило мне перенести много кода среднего качества во многих местах без необходимости каких-либо изменений.
В качестве доказательства: https://repl.it/repls/TraumaticToughCockatoo
В основном: скопируйте текущий параметр # 2, добавьте print 'method_name'
строки к каждому методу, а затем попробуйте это и посмотрите результат:
d = LowerDict() # prints "init", or whatever your print statement said
print '------'
splatted = dict(**d) # note that there are no prints here
Вы увидите аналогичное поведение для других сценариев. Допустим, ваш fake-dict
является оболочкой для какого-то другого типа данных, поэтому нет разумного способа сохранить данные в backing-dict; **your_dict
будет пустым, независимо от того, что делает любой другой метод.
Это работает корректно для MutableMapping
, но как только вы наследуете от dict
, это становится неконтролируемым.
Редактировать: в качестве обновления это работает без единой проблемы уже почти два года, на нескольких сотнях тысяч (возможно, на паре миллионов) строк сложного, устаревшего python. Так что я этим вполне доволен :)
Правка 2: видимо, я неправильно скопировал это или что-то еще давным-давно. @classmethod __class__
не работает для isinstance
проверок - @property __class__
делает: https://repl.it/repls/UnitedScientificSequence
Ответ 4
Мои требования были немного строже:
- Мне пришлось сохранить информацию о регистре (строки представляют собой пути к файлам, отображаемым пользователю, но это приложение для Windows, поэтому внутри все операции должны выполняться без учета регистра)
- Мне нужно было, чтобы ключей было как можно меньше (это действительно повлияло на производительность памяти, сократив 110 МБ из 370). Это означало, что кэширование строчной версии ключей невозможно.
- Мне нужно было создавать структуры данных как можно быстрее (опять же, это повлияло на производительность, на этот раз скорость). Мне пришлось использовать встроенный
Моей первоначальной мыслью было заменить наш неуклюжий класс Path на подкласс unicode без учета регистра - но:
- оказалось сложно сделать это правильно - смотрите: Класс строк без учета регистра в python
- оказывается, что явная обработка ключей dict делает код подробным, беспорядочным и подверженным ошибкам (структуры передаются туда-сюда, и неясно, есть ли у них экземпляры CIStr в качестве ключей / элементов, их легко забыть, плюс
some_dict[CIstr(path)]
это некрасиво)
Итак, мне, наконец, пришлось записать этот dict без учета регистра. Благодаря code от @AaronHall это стало в 10 раз проще.
class CIstr(unicode):
"""See https://pythonly.ru/a/43122305/281545, especially for inlines"""
__slots__ = () # does make a difference in memory performance
#--Hash/Compare
def __hash__(self):
return hash(self.lower())
def __eq__(self, other):
if isinstance(other, CIstr):
return self.lower() == other.lower()
return NotImplemented
def __ne__(self, other):
if isinstance(other, CIstr):
return self.lower() != other.lower()
return NotImplemented
def __lt__(self, other):
if isinstance(other, CIstr):
return self.lower() < other.lower()
return NotImplemented
def __ge__(self, other):
if isinstance(other, CIstr):
return self.lower() >= other.lower()
return NotImplemented
def __gt__(self, other):
if isinstance(other, CIstr):
return self.lower() > other.lower()
return NotImplemented
def __le__(self, other):
if isinstance(other, CIstr):
return self.lower() <= other.lower()
return NotImplemented
#--repr
def __repr__(self):
return '{0}({1})'.format(type(self).__name__,
super(CIstr, self).__repr__())
def _ci_str(maybe_str):
"""dict keys can be any hashable object - only call CIstr if str"""
return CIstr(maybe_str) if isinstance(maybe_str, basestring) else maybe_str
class LowerDict(dict):
"""Dictionary that transforms its keys to CIstr instances.
Adapted from: https://pythonly.ru/a/39375731/281545
"""
__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, 'iteritems'):
mapping = getattr(mapping, 'iteritems')()
return ((_ci_str(k), v) for k, v in
chain(mapping, getattr(kwargs, 'iteritems')()))
def __init__(self, mapping=(), **kwargs):
# dicts take a mapping or iterable as their optional first argument
super(LowerDict, self).__init__(self._process_args(mapping, **kwargs))
def __getitem__(self, k):
return super(LowerDict, self).__getitem__(_ci_str(k))
def __setitem__(self, k, v):
return super(LowerDict, self).__setitem__(_ci_str(k), v)
def __delitem__(self, k):
return super(LowerDict, self).__delitem__(_ci_str(k))
def copy(self): # don't delegate w/ super - dict.copy() -> dict :(
return type(self)(self)
def get(self, k, default=None):
return super(LowerDict, self).get(_ci_str(k), default)
def setdefault(self, k, default=None):
return super(LowerDict, self).setdefault(_ci_str(k), default)
__no_default = object()
def pop(self, k, v=__no_default):
if v is LowerDict.__no_default:
# super will raise KeyError if no default and key does not exist
return super(LowerDict, self).pop(_ci_str(k))
return super(LowerDict, self).pop(_ci_str(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__(_ci_str(k))
@classmethod
def fromkeys(cls, keys, v=None):
return super(LowerDict, cls).fromkeys((_ci_str(k) for k in keys), v)
def __repr__(self):
return '{0}({1})'.format(type(self).__name__,
super(LowerDict, self).__repr__())
Неявное или явное по-прежнему остается проблемой, но как только пыль осядет, переименование атрибутов / переменных следует начинать с ci (и большого жирного комментария к документу, объясняющего, что ci расшифровывается как "нечувствительный к регистру") Я думаю, это идеальное решение - поскольку читатели кода должны быть полностью осведомлены, что мы имеем дело с базовыми структурами данных, не чувствительными к регистру.
Мы надеемся, что это исправит некоторые трудно воспроизводимые ошибки, которые, как я подозреваю, сводятся к чувствительности к регистру.
Комментарии / исправления приветствуются :)