关于字典:如何在Python中使用Abstract Base Classes实现dict?

How would I implement a dict with Abstract Base Classes in Python?

本问题已经有最佳答案,请猛点这里访问。

我试图使用抽象基类mutablemapping在python中实现映射,但在实例化时出错。我该如何制作一个有效版本的字典,以尽可能多的方式来模拟内置的dict类,以明确抽象的基类?

1
2
3
4
5
6
7
>>> 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子类化的情况下(我非常熟悉这个概念)。


How would I implement a dict with Abstract Base Classes?

A good answer will demonstrate how to make this work, specifically
without subclassing dict.

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

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

实施

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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__)

示范

并演示用法:

1
2
3
4
5
6
7
8
9
10
11
>>> 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.MutableMapping

1
2
3
4
>>> isinstance(d, collections.MutableMapping)
True
>>> isinstance(dict(), collections.MutableMapping)
True

虽然由于集合导入上的注册,dict始终是可变映射的实例,但反过来并不总是正确的:

1
2
3
4
>>> isinstance(d, dict)
False
>>> isinstance(d, (dict, collections.MutableMapping))
True

在执行了这个练习之后,我很清楚使用抽象基类只能为类的用户提供标准API的保证。在这种情况下,假设一个可变映射对象的用户将被保证是Python的标准API。

Caveats:

未实现fromkeys类构造函数方法。

1
2
3
4
5
6
>>> 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'

我们可以屏蔽诸如getsetdefault之类的内置dict方法。

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

很容易再次取消屏蔽:

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

但我不会在生产中使用此代码。

没有dict的演示,python 3:

1
2
3
4
5
6
7
8
>>> 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__'

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

哈希表只是(hash, key, value)元组的数组。因为整个过程都是打包数据,所以我们把它们塞进一个struct中,这意味着我们可以使用一个大的bytearray来存储数据。为了将槽标记为空,我们将其散列值设置为0,这意味着我们需要通过将任何实际的0转换为1,从而"转义"任何实际的0,这很愚蠢,但代码更简单。为了简单起见,我们还将使用尽可能小的probe算法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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__的实现。然而,一个更好的地方是文档,它会告诉您,如果实现这五个方法(加上基类所需的任何其他方法,但是从表中可以看到,没有其他方法),您将免费获得所有其他方法。您可能无法获得所有方法中最有效的实现,但我们将回到这一点上。

首先,我们来处理一下__len__。通常人们希望这是O(1),这意味着我们需要独立地跟踪它,根据需要更新它。所以:

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

现在,__getitem__只需探测,直到找到所需的键或到达末端:

1
2
3
4
5
    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

1
2
3
4
5
6
7
8
    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

1
2
3
4
5
6
7
8
9
    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一样,我们没有任何特定的顺序,因此我们可以迭代哈希表槽,并生成所有非空的槽:

1
2
3
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的事实:

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

但是,请注意,默认的items只是创建一个ItemsView(self),如果通过源跟踪它,您会看到它迭代self并查找每个值。如果性能很重要,显然可以做得更好:

1
2
3
4
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和其他方法。


至少

需要在子类中实现从可变映射继承的所有抽象方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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、红黑),以及一种构造字典的方法。


抽象基类的全部思想是它有一些成员,(C++中的纯虚拟成员),你的代码必须提供——C++,这些是纯虚拟成员和其他可以重写的虚拟成员。

Python与C++的不同之处在于,所有类的所有成员都是虚拟的,并且可以被重写,(并且可以向所有类和实例添加成员),但是抽象基类具有一些需要的缺失类,这些类是C++纯虚拟的等价物。

如果不这样做,您只需提供缺少的成员就可以创建派生类的实例。

举一个例子,你试图在这里看到一个被接受的答案,而不是在课堂上使用听写,你必须提供它提供给你自己的方法。


使用MutableMapping作为基类,您应该自己在类中创建这个方法:__delitem__, __getitem__, __iter__, __len__, __setitem__

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

1
2
3
4
5
6
>>> class D(dict):
...     pass
...
>>> d = D()
>>> d
{}