在 Python 集中"in"签入到底是什么?



我有一个简单的自定义对象,表示自定义标签,用户可以附加到另一个对象。

我想
  • 将标签存储在一组中,因为我想避免重复,并且因为顺序无关紧要。
  • 每个标记都包含值"名称"和"说明"。稍后,我可能会添加另一个变量,但标签的关键标识符是"name"。
  • 我想通过tag.name == other.name或字符串tag == 'whatever'来检查标签是否等于其他标签。
  • 我希望用户能够编辑标签,包括重命名它们。

我已经像这样定义了对象,一切都按预期工作:

class Tag:
def __init__(self, name, description=""):
self.name = name
self.description = description
def __str__(self):
return self.name
def __repr__(self):
return self.name
def __eq__(self, other):
if isinstance(other, Tag):
return self.name == other.name
else:
return self.name == other
def __hash__(self):
return hash(self.name)

当我尝试更改标签名称时,问题出现了:

blue_tag = Tag("blue") 
tags = {blue_tag}
blue_tag in tags  # returns True as expected
"blue" in tags  # returns True as expected
blue_tag.name = "navy"
"navy" in tags # returns False. Why?

我不明白为什么。当我print(tags)时,标签已正确重命名.bluetag 对象的 id 也相同,名称的哈希值也相同。

在任何地方,包括 Python 官方文档,我都只找到了基本信息,in检查容器中是否存在项并定义自定义容器,我需要定义自定义方法,例如__contains__但我不想创建自定义集方法。

我发现的最接近的东西是关于SO的一个问题:

自定义类对象和"in"集运算符

但它仍然没有解决问题。

问题是,在更改标签name属性时,您会在上面的类中更改其哈希值: 并且对象的哈希值在添加到集合或作为字典作为键后不得更改。

问题是,如果两个对象"相等",它们必须具有相同的哈希值 - 由于您希望您的标签按名称进行比较,这意味着它们根本不能更改其名称:如果一个对象与另一个对象比较相等,则它们的哈希值也必须相等:即您不能简单地将另一个不可变属性添加到您的类中并基于该属性而不是名字。

在这种情况下,我看到的解决方法是在Tag类上有一个特殊的"add_to_set"方法;然后它会跟踪它所属的集合,并将name转换为属性实例,以便每当更改name时,它都会从它所属的所有集合中删除并重新添加Tag本身。新重新插入的标记将相应地运行。

在并行代码中正确工作需要做更多的工作:因为在重命名期间可以在另一个线程中使用集合 - 但如果这不是问题,那么需要的是:

class Tag:
def __init__(self, name, description=""):
self.sets = []
self.name = name
self.description = description
...  # other methods as in your code 
def __hash__(self):
return hash(self.name)
def add_to_set(self, set_):
self.sets.append(set_)
set_.add(self)
def remove_from_set(self, set_):
self.sets.remove(set_)
set_.remove(self)
@property
def name(self):
return self._name
@name.setter
def name(self, value):
# WARNING: this is as thread unsafe as it gets! Do not use this class
# in multi-threaded code. (async is ok)

try:
for set_ in self.sets:
set_.remove(self)
self._name = value
finally:
for set_ in self.sets:
set_.add(self)

现在:

In [17]: a = Tag("blue")
In [18]: b = set()
In [19]: a.add_to_set(b)
In [20]: a in b
Out[20]: True
In [21]: b
Out[21]: {blue}
In [22]: a.name = "mauve"
In [23]: b
Out[23]: {mauve}
In [24]: a in b
Out[24]: True

可以专门化一个set类,该类也会自动为您调用add_to_setremove_from_set方法,但这很可能就足够了。

最新更新