Как "идеально" переопределить диктовку?
Как я могу сделать как можно более "совершенным" подклассом 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://stackru.com/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()
6 ответов
Вы можете легко написать объект, который ведет себя как диктовку с помощью ABC(абстрактных базовых классов) из модуля коллекций. Он даже сообщает вам, если вы пропустили метод, поэтому ниже приведена минимальная версия, которая закрывает ABC.
import collections
class TransformedDict(collections.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
assert pickle.loads(pickle.dumps(s)) == s
# works too since we just use a normal dict
Я бы не подкласс dict
(или другие встроенные функции) напрямую. Часто это не имеет смысла, потому что на самом деле вы хотите реализовать интерфейс dict. И это именно то, для чего азбука.
Как я могу сделать как можно более "совершенным" подклассом dict?
Конечная цель состоит в том, чтобы иметь простой диктант, в котором ключи строчные.
Если я переопределить
__getitem__
/__setitem__
, то получить / установить не работает. Как мне заставить их работать? Конечно, мне не нужно реализовывать их индивидуально?Могу ли я предотвратить травление и нужно ли мне его внедрять?
__setstate__
так далее?Нужно ли repr, обновить и
__init__
?Должен ли я просто использовать
mutablemapping
(кажется, не следует использоватьUserDict
или жеDictMixin
)? Если так, то как? Документы не совсем поучительны.
Принятый ответ был бы моим первым подходом, но так как у него есть некоторые проблемы, и так как никто не обратился к альтернативе, фактически подкласс 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, создадим синглтон (_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, обновить и
__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)]
Это должно быть гораздо проще отлаживать.
Сравнивать и противопоставлять
Есть 6 интерфейсных функций, реализованных с MutableMapping
(чего не хватает fromkeys
) и 11 с dict
подкласс. Мне не нужно реализовывать __iter__
или же __len__
, но вместо этого я должен реализовать get
, setdefault
, pop
, update
, copy
, __contains__
, а также fromkeys
- но это довольно тривиально, так как я могу использовать наследование для большинства этих реализаций.
MutableMapping
реализует некоторые вещи в Python, которые dict
реализует в C - так что я бы ожидал dict
Подкласс должен быть более производительным в некоторых случаях.
Мы получаем бесплатно __eq__
в обоих подходах - оба из которых предполагают равенство только в том случае, если другой dict является строчными - но опять же, я думаю dict
Подкласс будет сравнивать быстрее.
Резюме:
- подклассов
MutableMapping
проще с меньшим количеством возможностей для ошибок, но медленнее, занимает больше памяти (см. избыточный dict) и терпит неудачуisinstance(x, dict)
- подклассов
dict
быстрее, использует меньше памяти и проходитisinstance(x, dict)
, но это имеет большую сложность для реализации.
Что является более совершенным? Это зависит от вашего определения идеального.
После опробования обоих из двух лучших предложений, я остановился на тенистом внешнем среднем маршруте для Python 2.7. Возможно 3 более разумно, но для меня:
class MyDict(MutableMapping):
# ... the few __methods__ that mutablemapping requires
# and then this monstrosity
@classmethod
def __class__(cls):
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
из-за подмены имени питона (переименовывается в _MyDict__am_i_me
от всего, что звонит за пределы этого класса). Чуть более приватно, чем _method
с, как на практике, так и в культурном отношении.
До сих пор у меня нет претензий, кроме серьезно выглядящих __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
Вы увидите похожее поведение для других сценариев. Скажи свою подделкуdict
является оберткой вокруг какого-либо другого типа данных, поэтому нет разумного способа сохранить данные в обратном ключе; **your_dict
будет пустым, независимо от того, что делает любой другой метод.
Это работает правильно для MutableMapping
, но как только вы наследуете от dict
это становится неуправляемым.
Мои требования были немного строже:
- Мне пришлось сохранить информацию о регистре (строки представляют собой пути к файлам, отображаемым пользователю, но это приложение для Windows, поэтому внутренне все операции должны быть без учета регистра)
- Мне нужно, чтобы ключи были как можно меньше (это имело значение для производительности памяти, отрубив 110 МБ из 370). Это означало, что кэширование строчной версии ключей не вариант.
- Мне нужно, чтобы создание структур данных было как можно более быстрым (опять же, на этот раз разница в производительности, скорости). Я должен был пойти со встроенным
Моей первоначальной мыслью было заменить наш неуклюжий класс Path на нечувствительный к регистру подкласс юникода - но:
- оказалось трудно понять это правильно - см.: регистр строк без учета регистра в python
- Оказывается, что явная обработка ключей dict делает код многословным и грязным - и подвержен ошибкам (структуры передаются туда-сюда, и неясно, имеют ли они экземпляры CIStr в качестве ключей / элементов, плюс легко забыть плюс
some_dict[CIstr(path)]
некрасиво)
Так что мне наконец-то пришлось записать этот нечувствительный к регистру диалог. Благодаря коду @AaronHall это было сделано в 10 раз проще.
class CIstr(unicode):
"""See https://stackru.com/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://stackru.com/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 означает нечувствительный к регистру), я думаю, является идеальным решением, так как читатели кода должны полностью осознавать, что мы имеем дело с нечувствительными к регистру базовыми структурами данных. Надеюсь, это исправит некоторые трудно воспроизводимые ошибки, которые, я подозреваю, сводятся к чувствительности к регистру.
Комментарии / исправления приветствуются:)
Все, что вам нужно сделать, это
class BatchCollection(dict):
def __init__(self, *args, **kwargs):
dict.__init__(*args, **kwargs)
ИЛИ ЖЕ
class BatchCollection(dict):
def __init__(self, inpt={}):
super(BatchCollection, self).__init__(inpt)
Пример использования для моего личного использования
### EXAMPLE
class BatchCollection(dict):
def __init__(self, inpt={}):
dict.__init__(*args, **kwargs)
def __setitem__(self, key, item):
if (isinstance(key, tuple) and len(key) == 2
and isinstance(item, collections.Iterable)):
# self.__dict__[key] = item
super(BatchCollection, self).__setitem__(key, item)
else:
raise Exception(
"Valid key should be a tuple (database_name, table_name) "
"and value should be iterable")
Примечание: проверено только в python3
collections.UserDict
часто является самым простым вариантом, когда вам нужен пользовательский файл .
Как показано в другом ответе, очень сложно правильно перезаписать, но сделать это легко. Чтобы ответить на исходный вопрос, вы можете получить словарь с нижними клавишами:
import collections
class LowercaseDict(collections.UserDict):
def __getitem__(self, key):
return super().__getitem__(key.lower())
def __setitem__(self, key, value):
return super().__setitem__(key.lower(), value)
def __delitem__(self, key):
return super().__delitem__(key.lower())
# Unfortunately, __contains__ is required currently due to
# https://github.com/python/cpython/issues/91784
def __contains__(self, key):
return key.lower() in self.data
d = LowercaseDict(MY_KEY=0) # Keys normalized in .__init__
d.update({'OTHER_KEY': 1}) # Keys normalized in .update
d['Hello'] = d['other_KEY']
assert 'HELLO' in d
print(d) # All keys normalized {'my_key': 0, 'other_key': 1, 'hello': 1}
И вопреки
collections.abc.MutableMapping
, вам не нужно
__iter__
,
__len__
,
__init__
,... Создание подклассов намного проще.
Однако
UserDict
это
MutableMapping
, а не
dict
, так:
assert not isinstance(collections.UserDict(), dict)
assert isinstance(collections.UserDict(), collections.abc.MutableMapping)