Как реализовать dict с абстрактными базовыми классами в Python? [дубликат]

этот вопрос уже есть ответ здесь:

Я попытался реализовать сопоставление в Python, используя абстрактный базовый класс MutableMapping, но я получил ошибку при создании экземпляра. Как я могу сделать рабочую версию этого словаря, которая будет эмулировать встроенный dict класс, как можно больше способов, чтобы быть ясным, с Абстрактные Базовые Классы?

>>> class D(collections.MutableMapping):
...     pass
... 
>>> d = D()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class D with abstract methods __delitem__, __getitem__, __iter__, __len__, __setitem__

хороший ответ продемонстрирует, как сделать эту работу, в частности, без подклассов dict (концепция, с которой я хорошо знаком).

5 ответов


как бы я реализовал dict с абстрактными базовыми классами?

хороший ответ продемонстрирует, как сделать эту работу, в частности без подклассов dict.

вот сообщение об ошибке: TypeError: Can't instantiate abstract class D with abstract methods __delitem__, __getitem__, __iter__, __len__, __setitem__

оказывается, что нужно реализовать их, чтобы использовать абстрактный базовый класс (ABC),MutableMapping.

реализация

поэтому я реализую отображение, которое работает как дикт в большинстве уважает, который использует ссылку атрибута объекта dict для сопоставления. (Делегирование не совпадает с наследованием, поэтому мы просто делегируем экземпляр __dict__, мы могли бы использовать любое другое специальное отображение, но вы, похоже, не заботитесь об этой части реализации. Имеет смысл сделать это таким образом в Python 2, потому что MutableMapping не имеет __slots__ в Python 2, поэтому вы создаете __dict__ в любом случае. В Python 3 Вы можете полностью избежать диктовки, установив __slots__.)

import collections

class D(collections.MutableMapping):
    '''
    Mapping that works like both a dict and a mutable object, i.e.
    d = D(foo='bar')
    and 
    d.foo returns 'bar'
    '''
    # ``__init__`` method required to create instance from class.
    def __init__(self, *args, **kwargs):
        '''Use the object dict'''
        self.__dict__.update(*args, **kwargs)
    # The next five methods are requirements of the ABC.
    def __setitem__(self, key, value):
        self.__dict__[key] = value
    def __getitem__(self, key):
        return self.__dict__[key]
    def __delitem__(self, key):
        del self.__dict__[key]
    def __iter__(self):
        return iter(self.__dict__)
    def __len__(self):
        return len(self.__dict__)
    # The final two methods aren't required, but nice for demo purposes:
    def __str__(self):
        '''returns simple dict representation of the mapping'''
        return str(self.__dict__)
    def __repr__(self):
        '''echoes class, id, & reproducible representation in the REPL'''
        return '{}, D({})'.format(super(D, self).__repr__(), 
                                  self.__dict__)

демонстрация

и продемонстрировать использование:

>>> d = D((e, i) for i, e in enumerate('abc'))
>>> d
<__main__.D object at 0x7f75eb242e50>, D({'b': 1, 'c': 2, 'a': 0})
>>> d.a
0
>>> d.get('b')
1
>>> d.setdefault('d', []).append(3)
>>> d.foo = 'bar'
>>> print(d)
{'b': 1, 'c': 2, 'a': 0, 'foo': 'bar', 'd': [3]}

и для обеспечения API dict, извлеченный урок заключается в том, что вы всегда можете проверить collections.MutableMapping:

>>> isinstance(d, collections.MutableMapping)
True
>>> isinstance(dict(), collections.MutableMapping)
True

и хотя dict всегда будет экземпляром MutableMapping из-за регистрации при импорте коллекций, обратное не всегда верно:

>>> isinstance(d, dict)
False
>>> isinstance(d, (dict, collections.MutableMapping))
True

после выполнения этого упражнения мне ясно, что использование абстрактного Базовые классы предоставляют пользователям класса только гарантию стандартного API. В этом случае пользователям, предполагающим объект MutableMapping, будет гарантирован стандартный API для Python.

предостережения:

на fromkeys метод конструктора класса не реализован.

>>> dict.fromkeys('abc')
{'b': None, 'c': None, 'a': None}
>>> D.fromkeys('abc')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: type object 'D' has no attribute 'fromkeys'

можно замаскировать встроенные методы dict, такие как get или setdefault

>>> d['get'] = 'baz'
>>> d.get('get')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'str' object is not callable

это довольно просто разоблачить снова:

>>> del d['get']
>>> d.get('get', 'Not there, but working')
'Not there, but working'

но я бы не стал используйте этот код в производстве.


демонстрация без Дикта, Python 3:

>>> class MM(MutableMapping):
...   __delitem__, __getitem__, __iter__, __len__, __setitem__ = (None,) *5
...   __slots__ = ()
...
>>> MM().__dict__
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'MM' object has no attribute '__dict__'

по крайней мере

вам нужно реализовать в своем подклассе все абстрактные методы, которые вы наследуете от MutableMapping

class D(MutableMapping):
    def __delitem__(self):
        '''
         Your Implementation for deleting the Item goes here
        '''
        raise NotImplementedError("del needs to be implemented")
    def __getitem__(self):
        '''
         Your Implementation for subscripting the Item goes here
        '''
        raise NotImplementedError("obj[index] needs to be implemented")
    def __iter__(self):
        '''
         Your Implementation for iterating the dictionary goes here
        '''
        raise NotImplementedError("Iterating the collection needs to be implemented")
    def __len__(self):
        '''
         Your Implementation for determing the size goes here
        '''
        raise NotImplementedError("len(obj) determination needs to be implemented")
    def __setitem__(self):
        '''
         Your Implementation for determing the size goes here
        '''
        raise NotImplementedError("obj[index] = item,  needs to be implemented")


>>> D()
<__main__.D object at 0x0258CD50>

кроме того

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


лучший способ продемонстрировать это без фактического использования dict в любом месте, вероятно, реализовать что-то мертвое простое, очень отличается от dict, и не совсем бесполезно. Как отображение фиксированного размера фиксированного размера bytes для того же фиксированного размера bytes. (Вы можете использовать это, например, для таблицы маршрутизации-она будет намного компактнее, чем dict сопоставление распакованных Ключей с распакованными значениями, хотя, очевидно, за счет скорости и гибкости.)

хэш-таблица просто массив (hash, key, value) ОК. Поскольку весь смысл этого-упаковка данных, мы запихиваем их в struct, то есть, мы можем просто использовать большой bytearray для хранения. Чтобы отметить пустой слот, мы устанавливаем его хэш-значение 0 - что означает, что нам нужно "убежать" от любого реального 0 превращается в 1, что глупо, но проще в код. Мы также будем использовать dumbest possible для простоты.

class FixedHashTable(object):
    hashsize = 8
    def __init__(self, elementsize, size):
        self.elementsize = elementsize
        self.size = size
        self.entrysize = self.hashsize + self.elementsize * 2
        self.format = 'q{}s{}s'.format(self.elementsize, self.elementsize)
        assert struct.calcsize(self.format) == self.entrysize
        self.zero = b'' * self.elementsize
        self.store = bytearray(struct.pack(self.format, 0,
                                           self.zero, self.zero)
                               ) * self.size
    def hash(self, k):
        return hash(k) or 1
    def stash(self, i, h, k, v):
        entry = struct.pack(self.format, h, k, v)
        self.store[i*self.entrysize:(i+1)*self.entrysize] = entry
    def fetch(self, i):
        entry = self.store[i*self.entrysize:(i+1)*self.entrysize]
        return struct.unpack(self.format, entry)
    def probe(self, keyhash):
        i = keyhash % self.size
        while True:
            h, k, v = self.fetch(i)
            yield i, h, k, v
            i = (i + 1) % self.size
            if i == keyhash % self.size:
                break

как говорится в сообщении об ошибке, вам необходимо предоставить реализации для абстрактных методов __delitem__, __getitem__, __iter__, __len__ и __setitem__. Тем не менее, лучшее место для поиска -документы, который скажет вам, что если вы реализуете эти пять методов (плюс любые другие методы, требуемые базовыми классами, но, как вы можете видеть из таблицы, их нет), вы получите все остальные методы бесплатно. Вы можете не получить максимально эффективной реализации всех из них, но мы вернемся к что.

во-первых, давайте разберемся с __len__. Обычно люди ожидают, что это будет O(1), Что означает, что нам нужно отслеживать его независимо, обновляя его по мере необходимости. Итак:

class FixedDict(collections.abc.MutableMapping):
    def __init__(self, elementsize, size):
        self.hashtable = FixedHashTable(elementsize, size)
        self.len = 0

теперь __getitem__ просто зондирует, пока не найдет нужный ключ или не достигнет конца:

    def __getitem__(self, key):
        keyhash = self.hashtable.hash(key)
        for i, h, k, v in self.hashtable.probe(keyhash):
            if h and k == key:
                return v

и __delitem__ делает то же самое, за исключением того, что он опустошает слот, если он найден, и обновляет len.

    def __delitem__(self, key):
        keyhash = self.hashtable.hash(key)
        for i, h, k, v in self.hashtable.probe(keyhash):
            if h and k == key:
                self.hashtable.stash(i, 0, self.hashtable.zero, self.hashtable.zero)
                self.len -= 1
                return
        raise KeyError(key)

__setitem__ немного сложнее-если найдено, мы должны замените значение в гнездо; если нет, мы должны заполнить пустой слот. И здесь мы имеем дело с тем, что хэш-таблица может быть заполнена. И конечно, мы должны позаботиться о len:

    def __setitem__(self, key, value):
        keyhash = self.hashtable.hash(key)
        for i, h, k, v in self.hashtable.probe(keyhash):
            if not h or k == key:
                if not h:
                    self.len += 1
                self.hashtable.stash(i, keyhash, key, value)
                return
        raise ValueError('hash table full')

и __iter__. Так же, как с dict, у нас нет определенного порядка, поэтому мы можем просто перебирать слоты хэш-таблицы и давать все непустые:

def __iter__(self):
    return (k for (h, k, v) in self.hashtable.fetch(i)
            for i in range(self.hashtable.size) if h)

пока мы на нем, мы могли бы также написать __repr__. Обратите внимание, что мы можем использовать факт, что мы получаем items бесплатно:

def __repr__(self):
    return '{}({})'.format(type(self).__name__, dict(self.items()))

однако обратите внимание, что по умолчанию items просто создает ItemsView(self), и если вы отслеживаете это через Источник, вы увидите, что он повторяет self и ищет каждое значение. Вы, очевидно, можете сделать лучше, если производительность имеет значение:

def items(self):
    pairs = ((k, v) for (h, k, v) in self.hashtable.fetch(i)
             for i in range(self.hashtable.size) if h)
    return collections.abc.ItemsView._from_iterable(pairs)

и аналогично для values, и, возможно, другие методы.


вся идея абстрактного базового класса заключается в том, что у него есть некоторые члены (чистые виртуальные члены в терминах C++), которые ваш код должен предоставить - в C++ это чистые виртуальные члены и другие виртуальные члены, которые вы мая переопределить.

Python отличается от C++ тем, что все члены всех классов являются виртуальными и могут быть переопределены (и вы можете добавлять члены ко всем классам и экземплярам), но абстрактные базовые классы имеют некоторые необходимые отсутствующие классы. equivent в C++ чисто виртуальные.

получив это, вам просто нужно предоставить отсутствующие члены, чтобы иметь возможность создать экземпляр вашего производного класса.

для примера того, что вы пытаетесь сделать, см. принятый ответ здесь но вместо того, чтобы использовать dict в классе, вам придется предоставить методы, которые он предоставляет самостоятельно.


С MutableMapping в качестве базового класса, вы должны создать этот метод в свой класс: __delitem__, __getitem__, __iter__, __len__, __setitem__.

чтобы создать пользовательский класс dict, вы можете получить его из dict:

>>> class D(dict):
...     pass
... 
>>> d = D()
>>> d
{}