为什么collections.MutableSet没有提供update方法?

5 投票
2 回答
3018 浏览
提问于 2025-04-18 14:55

当你想要创建一个像集合(set)一样工作的类时,可以从 collections.MutableSet 这个类继承。这样,你的新类就会获得一些混合方法,只要你实现它们所需要的方法。(换句话说,集合的一些方法可以通过其他方法来实现。为了避免这种繁琐,collections.MutableSet 及其相关类已经包含了这些实现。)

文档中提到的抽象方法有:

__contains____iter____len__adddiscard

而混合方法包括:

继承自 Set 的方法,还有 clearpopremove__ior____iand____ixor____isub__

(为了明确说明,update 并不属于“继承自 Set 的方法”,Set 的混合方法有:

__le____lt____eq____ne____gt____ge____and____or____sub____xor__isdisjoint

不过,Set 是指不可变集合,自然就没有 update 这个方法。)

为什么 update 不在这些方法中呢? 我觉得这很奇怪,甚至有点不符合直觉,因为 set 包含这个方法,但 collections.Set 却没有。例如,这会导致以下情况:

In [12]: my_set
Out[12]: <ms.MySet at 0x7f947819a5d0>

In [13]: s
Out[13]: set()

In [14]: isinstance(my_set, collections.MutableSet)
Out[14]: True

In [15]: isinstance(s, collections.MutableSet)
Out[15]: True

In [16]: s.update
Out[16]: <function update>

In [17]: my_set.update
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-17-9ed968a9eb18> in <module>()
----> 1 my_set.update

AttributeError: 'MySet' object has no attribute 'update'

更奇怪的是,MutableMapping 反而有一个 update 方法,而 MutableSet 没有。根据我所了解的,源代码 并没有提到任何原因。

2 个回答

3

更新:

Raymond Hettinger本人回应了你提出的错误报告,如下所述,集合抽象基类使用的是运算符,而不是命名的方法。


原始回应:

Raymond Hettinger写了一个关于有序集合的示例,这个示例是基于MutableSet抽象基类的(请看底部的代码块)。但他没有使用更新方法。相反,他使用了|=运算符,这个运算符是更新方法调用的。我不知道你的错误报告是否会得到重视,因为这可能会破坏一些只期望当前实现的已有代码。

不过,你可以写一个抽象基类,来期待你想要包含的方法:

import abc
import collections

class MyMutableSet(collections.MutableSet):
    @abc.abstractmethod
    def update(self, other):
        raise NotImplementedError

MyMutableSet.register(set)

这样就可以正常工作:

>>> isinstance(set('abc'), MyMutableSet)
True

如果我们尝试从我们新的抽象基类(见下面的示例)继承,而不是从MutableSet继承:

>>> s = OrderedSet()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class OrderedSet with abstract methods update

所以我们可以看到,如果我们要求用户从我们定制的抽象基类继承,就可以以这种方式要求update方法。

这也提醒我们,如果你想测试实例,应该小心只期望你正在使用的抽象基类所实现的方法,不要假设你在内置类(在这个例子中是set)中拥有每一个方法。set被注册为MutableSet的子类,而不是反过来。

在抽象基类中实现更新

如果你想在抽象基类中实现更新,因为它期望__ior__

def update(self, other): 
    self |= other

这样做不应该破坏已有的代码。不过,如果你要这样做,最好也实现其他所有的方法。


Raymond的示例,经过我们的调整:

import collections

# class OrderedSet(collections.MutableSet):
class OrderedSet(MyMutableSet):
    def __init__(self, iterable=None):
        self.end = end = [] 
        end += [None, end, end]         # sentinel node for doubly linked list
        self.map = {}                   # key --> [key, prev, next]
        if iterable is not None:
            self |= iterable
    def __len__(self):
        return len(self.map)
    def __contains__(self, key):
        return key in self.map
    def add(self, key):
        if key not in self.map:
            end = self.end
            curr = end[1]
            curr[2] = end[1] = self.map[key] = [key, curr, end]
    def discard(self, key):
        if key in self.map:        
            key, prev, next = self.map.pop(key)
            prev[2] = next
            next[1] = prev
    def __iter__(self):
        end = self.end
        curr = end[2]
        while curr is not end:
            yield curr[0]
            curr = curr[2]
    def __reversed__(self):
        end = self.end
        curr = end[1]
        while curr is not end:
            yield curr[0]
            curr = curr[1]
    def pop(self, last=True):
        if not self:
            raise KeyError('set is empty')
        key = self.end[1][0] if last else self.end[2][0]
        self.discard(key)
        return key
    def __repr__(self):
        if not self:
            return '%s()' % (self.__class__.__name__,)
        return '%s(%r)' % (self.__class__.__name__, list(self))
    def __eq__(self, other):
        if isinstance(other, OrderedSet):
            return len(self) == len(other) and list(self) == list(other)
        return set(self) == set(other)

            
if __name__ == '__main__':
    s = OrderedSet('abracadaba')
    t = OrderedSet('simsalabim')
    print(s | t)
    print(s & t)
    print(s - t)
10

MutableSet的接口是由Guido van Rossum设计的。他在PEP 3119的集合部分中详细说明了他的提议。简单来说,他指出:

“这个类还定义了具体的操作符,用于计算并集、交集、对称差和非对称差,分别是__or__、__and__、__xor__和__sub__。”

...

“这也支持就地修改操作 |=、&=、^=、-=。这些是具体的方法,右侧的操作数可以是任意可迭代对象,除了&=,它的右侧操作数必须是一个容器。这个抽象基类没有提供内置具体集合类型中存在的命名方法,这些方法执行(几乎)相同的操作。”

这里并不是有bug或疏漏,而是关于你是否喜欢Guido的设计的个人看法。

Python之禅对此有一些看法:

  • 应该有一个——最好只有一个——明显的方法来做到这一点。
  • 虽然这个方法一开始可能不明显,除非你是荷兰人。

话虽如此,抽象基类的设计是为了便于扩展。你可以很简单地在你的具体类中添加自己的update()方法,方法是update = Set.__ior__

撰写回答