14

我尝试使用抽象基类 MutableMapping 在 Python 中实现映射,但在实例化时出现错误。我将如何使用Abstract Base Classesdict以尽可能多的方式制作这个字典的工作版本,以尽可能多地模拟内置类?

>>> 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我非常熟悉的概念)。

4

5 回答 5

28

我将如何使用抽象基类实现 dict?

一个好的答案将演示如何使这项工作,特别是没有子类化 dict。

这是错误消息:TypeError: Can't instantiate abstract class D with abstract methods __delitem__, __getitem__, __iter__, __len__, __setitem__

事实证明,必须实现它们才能使用抽象基类 (ABC) MutableMapping,.

执行

所以我实现了一个在大多数方面都像字典一样工作的映射,它使用对象的属性引用字典进行映射。(委托与继承不同,所以我们只委托给实例__dict__,我们可以使用任何其他临时映射,但您似乎并不关心实现的那部分。这样做是有意义的这种方式在 Python 2 中,因为 MutableMapping__slots__在 Python 2 中没有,所以你正在创建一种__dict__方式。在 Python 3 中,你可以通过设置完全避免 dicts __slots__。)

from collections.abc import MutableMapping

class D(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]}

为了确保 dict API,吸取的教训是您可以随时检查collections.abc.MutableMapping

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

虽然由于在集合导入上注册,dict 总是会成为 MutableMapping 的实例,但反过来并不总是正确的:

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

完成这个练习后,我很清楚,使用抽象基类只为该类的用户提供标准 API 的保证。在这种情况下,假设一个 MutableMapping 对象的用户将被保证使用 Python 的标准 API。

注意事项:

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 方法,例如getsetdefault

>>> 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__'
于 2014-01-26T20:35:33.050 回答
4

在不实际使用dictanywhere 的情况下演示这一点的最佳方法可能是实现一些非常简单、与 非常不同dict且并非完全无用的东西。就像从固定大小bytes到相同固定大小的固定大小映射bytesdict(例如,您可以将它用于路由表——它比将解包键映射到解包值要紧凑得多,尽管显然以速度和灵活性为代价。)

哈希表只是一个(hash, key, value)元组数组。由于这样做的重点是打包数据,因此我们将它们塞进 astruct中,这意味着我们可以只使用 bigbytearray进行存储。为了将插槽标记为空,我们将其哈希值设置为0— 这意味着我们需要0通过将其转换为 a来“转义”任何实数1,这很愚蠢,但更易于编码。为简单起见,我们还将使用最愚蠢的probe算法。

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'\0' * 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__。但是,更好的地方是docs,它将告诉您,如果您实现这五个方法(加上基类所需的任何其他方法,但从表中可以看出没有),您将得到所有其他方法都是免费的。您可能无法获得所有这些最有效的实现,但我们会回到这一点。

首先,让我们处理__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__。就像 a 一样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)

同样 for values,可能还有其他方法。

于 2014-01-28T00:50:25.517 回答
2

至少

您需要在子类中实现从 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、红黑),以及一种构建字典的方法

于 2014-01-26T08:56:45.197 回答
-1

作为MutableMapping基类,你应该在你的类中自己创建这些方法:__delitem__, __getitem__, __iter__, __len__, __setitem__.

要创建自定义 dict 类,您可以从 dict 派生它:

>>> class D(dict):
...     pass
... 
>>> d = D()
>>> d
{}
于 2014-01-26T08:08:54.980 回答
-1

抽象基类的整个想法是它有一些成员(C++ 术语中的纯虚拟成员),您的代码必须提供这些成员 - 在 C++ 中,这些是纯虚拟成员和您可以覆盖的其他虚拟成员。

Python 与 C++ 的不同之处在于所有类的所有成员都是虚拟的并且可以被覆盖(并且您可以将成员添加到所有类和实例)但是抽象基类有一些必需的缺失类,这些类相当于 C++ 纯虚拟。

解决这个问题后,您只需要提供缺失的成员即可创建派生类的实例。

对于您正在尝试做的事情的示例,请在此处查看接受的答案,而不是在类中使用 dict,您必须提供它自己提供的方法。

于 2014-01-26T08:16:06.133 回答