Как «идеально» переопределить диктовку? - PullRequest
187 голосов
/ 02 августа 2010

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

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

  • Если I переопределить __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 
    # /2224760/kak-pravilno-sozdat-podklass-dict-i-pereopredelit-getitem-setitem

    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()

Ответы [ 5 ]

195 голосов
/ 02 августа 2010

С помощью ABC (абстрактные базовые классы) из модуля collection вы можете написать объект, который ведет себя как диктовку.Он даже сообщает вам, если вы пропустили метод, поэтому ниже приведена минимальная версия, которая закрывает 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 .И это именно то, для чего азбука.

77 голосов
/ 07 сентября 2016

Как я могу сделать как можно более "совершенным" подклассом dict?

Конечная цель - создать простой диктант, в котором ключи строчные.

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

  • Я не допускаю работу травления, и мне нужно внедрить __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__, то get / setне работаетКак мне заставить их работать?Конечно, мне не нужно реализовывать их по отдельности?

Что ж, их реализация по отдельности является недостатком этого подхода и преимуществом использования 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, 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)? Если так, то как? Документы не совсем поучительны.

Да, это еще несколько строк кода, но они должны быть всеобъемлющими. Мое первое желание было бы использовать принятый ответ, и если бы возникли проблемы с ним, я бы посмотрел на мой ответ - так как он немного сложнее, и нет никакой азбуки, помогающей мне правильно настроить интерфейс.

Преждевременная оптимизация усложняет поиск производительности. 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 проще с меньшим количеством возможностей для ошибок, но медленнее, занимает больше памяти (см. Избыточный запрос) и завершается ошибкой isinstance(x, dict)
  • создание подклассов dict быстрее, использует меньше памяти и передает isinstance(x, dict), но имеет большую сложность для реализации.

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

3 голосов
/ 17 апреля 2017

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

  • Мне пришлось сохранить информацию о регистре (строки представляют собой пути к файлам, отображаемым для пользователя, но это приложение для 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: https://stackoverflow.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__())

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

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

2 голосов
/ 06 октября 2017

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

class BatchCollection(dict):
    def __init__(self, *args, **kwargs):
        dict.__init__(*args, **kwargs)

OR

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

1 голос
/ 18 ноября 2017

Попробовав оба top два предложения, я остановился на тенистом среднем маршруте для 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 из-за именования в 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

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

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

...