如何使用等效对象访问集合中的元素?
如果我有一个对象,它和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
我该如何通过 b
和 s
来获取 a
的引用呢?我能想到一种方法,但不确定这样得到的 a
或 b
是否会依赖于具体的实现。编辑:当集合中有多个元素时,这种方法就不管用了;交集的实现方式很自然,像这样 [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 个回答
这里有一个简单的解决方案,我利用了'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()
你的情况听起来适合用字典来处理。可以把对象的某个属性当作键(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)
我在python-list上发现了一个类似的问题:从集合中获取项目。这里有一个聪明的回答,提到了get_equivalent(container, item)(Python食谱)。
这个技巧是为“键”对象构建一个包装对象,然后使用 in
操作符检查这个包装对象是否在集合中。如果这个包装对象的哈希值和键相等,它的 __eq__
方法就可以访问集合中的对象,并保存对它的引用。讨论中一个重要的点是,集合元素的 __eq__
方法必须对未识别的类型返回 NotImplemented
,否则包装对象的 __eq__
方法可能不会被调用。