Как "отлично" переопределить dict?

Как я могу сделать как можно более "совершенным" подклассом 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://stackoverflow.com/info/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) does not 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, с ABC s (Абстрактные базовые классы) из модуля collections. Это даже говорит вам если вы пропустили метод, поэтому ниже приведена минимальная версия, которая закрывает 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. И это именно то, для чего предназначены ABC.

Ответ 2

Как я могу сделать "идеальным" подкласс dict, насколько возможно?

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

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

  • Я предотвращаю травление от работы, и мне нужно реализовать __setstate__ и т.д.

  • Нужно ли мне обновлять, обновлять и __init__?

  • Должен ли я использовать mutablemapping (кажется, нельзя использовать UserDictили DictMixin)? Если да, то как? Документы не совсем просвещают.

Принятый ответ будет моим первым подходом, но поскольку у него есть некоторые проблемы, и поскольку никто не обратился к альтернативе, фактически подклассифицируя dict, я собираюсь сделать это здесь.

Что случилось с принятым ответом?

Это кажется довольно простой просьбой ко мне:

Как я могу сделать "идеальным" подкласс 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, создайте 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__ и т.д.

травление

И диктовский подкласс соленья прекрасно:

>>> 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__

Нужно ли перепечатать, обновить и __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 будет сравнивать быстрее.

Резюме:

  • subclassing mutablemapping проще с меньшим количеством возможностей для ошибок, но медленнее, занимает больше памяти (см. избыточный dict) и не работает isinstance(x, dict)
  • subclassing dict работает быстрее, использует меньше памяти и передает isinstance(x, dict), но имеет большую сложность для реализации.

Что лучше? Это зависит от вашего определения совершенства.

Ответ 3

Мои требования были немного более строгими:

  • Мне пришлось сохранить информацию о событии (строки - это пути к файлам, отображаемым для пользователя, но это приложение для Windows, поэтому внутренне все операции должны быть нечувствительны к регистру)
  • Мне нужны были ключи как можно меньше (это действительно повлияло на производительность памяти, отрубило 110 мб из 370). Это означало, что кэширование версии с нижним регистром ключей не является вариантом.
  • Мне нужно было создать структуры данных как можно быстрее (опять-таки изменилась производительность, скорость на этот раз). Мне пришлось пойти со встроенным

Моя первоначальная мысль заключалась в том, чтобы заменить наш неуклюжий класс Path для нечувствительного к регистру юникодного подкласса, но:

  • оказалось, что это правильно - см.: Строковый класс, нечувствительный к регистру в python
  • получается, что явная обработка ключей dict делает код многословным и беспорядочным - и склонность к ошибкам (структуры передаются туда и сюда, и неясно, имеют ли они экземпляры CIStr как ключи/элементы, легко забыть плюс some_dict[CIstr(path)] некрасиво)

Итак, я, наконец, записал этот регистр без учета ограничений. Благодаря коду от @AaronHall, который был сделан в 10 раз легче.

class CIstr(unicode):
    """See https://stackoverflow.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: /questions/42081/how-to-perfectly-override-a-dict/303207#303207
    """
    __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__())

Неявная vs-явная проблема все еще остается проблемой, но как только пыль оседает, переименование атрибутов/переменных начинается с ci (и большой толковый комментарий к доктору, объясняющий, что ci означает регистр без учета регистра). Я думаю, что это идеальное решение - как читатели код должен быть полностью осведомлен о том, что мы имеем дело с нечувствительными к регистру базовыми структурами данных. Это, надеюсь, исправит некоторые трудности с воспроизведением ошибок, которые, как я подозреваю, сводятся к чувствительности к регистру.

Комментарии/исправления приветствуются:)

Ответ 4

Все, что вам нужно сделать, это

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

Ответ 5

Попробовав оба главных двух предложения, я остановился на теневом внешнем среднем маршруте для 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)
    • исключает только 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 из-за подмены имен python (это переименовано в _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

Вы увидите похожее поведение для других сценариев. Скажем, ваш fake- dict является оберткой вокруг какого-либо другого типа данных, поэтому нет разумного способа сохранить данные в обратном ключе; **your_dict будет пустым, независимо от того, что делает любой другой метод.

Это правильно работает для MutableMapping, но как только вы наследуете от dict, оно становится неуправляемым.


Изменение: в качестве обновления, это работает без единой проблемы в течение почти двух лет, на нескольких сотнях тысяч (а может быть, пару миллионов) строк сложного, устаревшего Python. Так что я очень доволен этим :)

Редактировать 2: по-видимому, я неправильно скопировал это или что-то давно. @classmethod __class__ не работает для проверок isinstance - @property __class__ делает: https://repl.it/repls/UnitedScientificSequence