如何使用等效对象访问集合中的元素?

26 投票
3 回答
10556 浏览
提问于 2025-04-17 08:54

如果我有一个对象,它和Python集合中的某个元素相等,但不是同一个对象,有没有什么好的办法可以获取集合中那个对象的引用?这个问题的背景是,我想用集合来识别和共享重复的数据。

举个例子(Python 2.7):

>>> a = "This is a string"
>>> b = "This is a string"
>>> a is b
False
>>> a == b
True
>>> s = set((a,))
>>> b in s
True

我该如何通过 bs 来获取 a 的引用呢?我能想到一种方法,但不确定这样得到的 ab 是否会依赖于具体的实现。编辑:当集合中有多个元素时,这种方法就不管用了;交集的实现方式很自然,像这样 [x for x in smaller_set if x in larger_set]

>>> for x in set((b,)).intersection(s): c = x
...
>>> c is a
True

也许一个好的解决办法是使用一个字典,把每个键映射到它自己,而不是使用集合。

3 个回答

1

这里有一个简单的解决方案,我利用了'eq'和'contains'这两个方法的特性。代码中的注释(希望)能自我解释,但整体来说还是挺简单的。

如例子所示,这个方法适用于集合、字典和列表,理论上也可以用于任何实现了'contains'的对象。

import typing as _ts
from typing import Any


class Getter:
    __slots__ = "key", "value"

    def __init__(self, key, value=None):
        self.key = key
        self.value = value

    def __repr__(self):
        return "{}({}, {})".format(
            type(self).__name__,
            repr(self.key), repr(self.value),
        )

    def __hash__(self):
        return hash(self.key)

    def __eq__(self, other):
        self.value = other
        return self.key == other


RAISES = object()


def getkey(keyed: _ts.Container, key: Any, default: Any = RAISES):
    getter = Getter(key)
    if getter in keyed:
        # providing '__contains__' is implemented to call
        #  the '__eq__' method (which in any sane case it
        #  should be), this results in our special
        #  'Getter.__eq__' method being called with the
        #  element we're trying to get as the 'other' argument
        return getter.value
    if default is RAISES:
        raise KeyError(key)
    return default


if __name__ == '__main__':
    # testing

    class T(int):
        def __repr__(self):
            return "T({})".format(int.__repr__(self))

    def main():
        # works for both builtin set and dict
        hm1 = {T(1), T(2), T(3)}
        hm2 = {T(1): 1, T(2): 2, T(3): 3}
        print(getkey(hm1, 2))
        print(getkey(hm2, 2))
        # should print "T(2)"

        # even works for list
        lst = [T(1), T(2), T(3)]
        print(getkey(lst, 3))
        # should print "T(3)"

        # in theory could work for any type that
        #  implements '__contains__' by calling '__eq__'

    main()
3

你的情况听起来适合用字典来处理。可以把对象的某个属性当作键(key),而把你想要的对象本身当作值(value)。

如果你的需求比较简单,并且可以进行线性搜索,那你可以直接用最简单的方法,这样做也没什么不好:

def get_equal(in_set, in_element):
   for element in in_set:
       if element == in_element:
           return element
   return None 

如果你需要的正是你所问的(我能想到一些这样的用例),那么可以考虑创建一个自定义的字典类,这个类里面有一个集合(set)作为成员。你需要实现一些代理方法来操作这个集合,并且在字典和集合的方法中,保持字典和集合的内容同步。这样做可能会花费一些时间来正确实现,但其实相对简单,并且能达到O(1)的时间复杂度。

如果复制所有数据的引用不是问题(这虽然是线性的,但可能比上面提到的简单搜索要差),你可以使用以下表达式:

(data - (data - {key})).pop()

像这样:

In [40]: class A:
    ...:     def __init__(self, id, extra):
    ...:         self.id = id
    ...:         self.extra = extra
    ...:     def __eq__(self, other):
    ...:         return self.id == other.id
    ...:     def __hash__(self):
    ...:         return hash(self.id)
    ...:     def __repr__(self):
    ...:         return f"({self.id}, {self.extra})"
    ...: 
    ...: 

In [41]: data = set(A(i, "initial") for i in range(10))

In [42]: (data - (data - {A(5, None)})).pop()
Out[42]: (5, initial)
6

我在python-list上发现了一个类似的问题:从集合中获取项目。这里有一个聪明的回答,提到了get_equivalent(container, item)(Python食谱)

这个技巧是为“键”对象构建一个包装对象,然后使用 in 操作符检查这个包装对象是否在集合中。如果这个包装对象的哈希值和键相等,它的 __eq__ 方法就可以访问集合中的对象,并保存对它的引用。讨论中一个重要的点是,集合元素的 __eq__ 方法必须对未识别的类型返回 NotImplemented,否则包装对象的 __eq__ 方法可能不会被调用。

撰写回答