Как "идеально" переопределить диктовку?

Как я могу сделать как можно более "совершенным" подклассом dict? Конечная цель состоит в том, чтобы иметь простой диктант, в котором ключи строчные.

Казалось бы, должен быть какой-то крошечный набор примитивов, которые я могу переопределить, чтобы сделать эту работу, но, судя по всем моим исследованиям и попыткам, похоже, что это не так:

  • Если я переопределить __getitem__ / __setitem__, затем get / set не работает Как я могу заставить их работать? Конечно, мне не нужно реализовывать их индивидуально?

  • Могу ли я предотвратить травление и нужно ли мне его внедрять? __setstate__ так далее?

  • Нужно ли мне repr , update а также __init__?

  • Должен ли я просто использовать 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)
    • исключает только MutableMapping, поэтому #1 недостаточно
    • Я от всей души рекомендую № 1, если вам это не нужно, это просто и предсказуемо
  • полностью контролируемое поведение
    • поэтому я не могу наследовать от 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)
Другие вопросы по тегам