【问题标题】:How would I implement a dict with Abstract Base Classes in Python? [duplicate]如何在 Python 中使用抽象基类实现 dict? [复制]
【发布时间】:2014-02-17 03:00:48
【问题描述】:

我尝试使用抽象基类 MutableMapping 在 Python 中实现映射,但在实例化时出现错误。我将如何使用 Abstract Base Classes 制作这个字典的工作版本,以尽可能多的方式模拟内置的 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 (a concept that I am quite familiar with)。

【问题讨论】:

    标签: python dictionary abc


    【解决方案1】:

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

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

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

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

    实施

    所以我实现了一个在大多数方面都像字典一样工作的映射,它使用对象的属性引用字典进行映射。 (委托与继承不同,所以我们将只委托给实例__dict__,我们可以使用任何其他临时映射,但您似乎并不关心实现的那部分。这是有道理的在 Python 2 中这样做,因为 MutableMapping 在 Python 2 中没有 __slots__,所以无论哪种方式,你都在创建一个 __dict__。在 Python 3 中,你可以通过设置 __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__'
    

    【讨论】:

    • 非常好的答案,谢谢!
    • 请注意,collections.MutableMapping 已折旧,应改为collections.abc.MutableMapping
    • @ReinstateMonica 对我来说很合适,如果您看到其他类似的情况,请告诉我。谢谢。
    【解决方案2】:

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

    哈希表只是(hash, key, value) 元组的数组。由于这一切的重点是打包数据,我们将它们塞进一个struct,这意味着我们可以使用一个大的bytearray 来存储。为了将插槽标记为空,我们将其哈希值设置为0——这意味着我们需要通过将任何真实的0“转义”为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__ 的实现。但是,更好的查看位置是the 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__。就像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,可能还有其他方法。

    【讨论】:

    • 也许 __slots__ 在这里会有一些用处,否则你仍然实例化 __dict__,据我所知。
    • @AaronHall:嗯,是的,但那又怎样?您没有使用它来实现可变映射方法。即使你使用插槽,你仍然有一个__dict__,因为MutableMapping 有一个。
    • 我在看源码,你能告诉我它是怎么做的吗? hg.python.org/cpython/file/1166b3321012/Lib/_collections_abc.py
    • @AaronHall: MutableMapping 有一个__dict__。因此,您的班级也是如此。避免这种情况的唯一方法是使用自定义元类(显然ABCMeta 不会这样做)。所以,即使你使用__slots__,你仍然只是通过定义你的类来创建字典。这是完全不相关的,但与__slots__ 一开始完全不相关的方式相同。谁在乎 dict 对象正在被创建?它们都没有被用于实现MutableMapping 的方法,这是 OP 所要求的。
    • @AaronHall:特别是,您要求的是如何在没有子类化 dict 的情况下模拟带有 ABC 的 dict。这不像您自己的答案,您通过委托给您的__dict__ 来模拟dict(除了没有真正回答您的问题之外,还有一些明显的负面影响);我通过从头开始实现它来模拟dict 而不委托任何东西。
    【解决方案3】:

    至少

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

    【讨论】:

      【解决方案4】:

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

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

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

      【讨论】:

      • 这是标题,我认为我没有误解:“如何在 Python 中使用 Abstract Base Classes 实现 dict?”我绝对不会接受这个答案。
      • @AaronHall 你已经这样做了并且出错了。并且错误说您的班级缺少此方法。阅读一些关于抽象基类的文档
      • 我认为我在子类化 dict 上给出的这个答案证明了我对它的熟悉,出于这个问题的目的,我特别不打算对 dict 进行子类化。 stackoverflow.com/questions/635483/…
      【解决方案5】:

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

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

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

      有关您尝试做的事情的示例,请参阅接受的答案 here,但您必须提供它自己提供的方法,而不是在类中使用 dict。

      【讨论】:

      猜你喜欢
      • 2015-05-24
      • 1970-01-01
      • 1970-01-01
      • 2021-04-30
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-07-11
      • 2016-10-22
      相关资源
      最近更新 更多