从Python集中删除已更改的对象

时间:2016-08-03 14:17:13

标签: python python-3.x

鉴于此计划:

class Obj:
    def __init__(self, a, b):
        self.a = a
        self.b = b

    def __hash__(self):
        return hash((self.a, self.b))

class Collection:
    def __init__(self):
        self.objs = set()

    def add(self, obj):
        self.objs.add(obj)

    def find(self, a, b):
        objs = []

        for obj in self.objs:
            if obj.b == b and obj.a == a:
                objs.append(obj)
        return objs

    def remove(self, a, b):
        for obj in self.find(a, b):
            print('removing', obj)
            self.objs.remove(obj)

o1 = Obj('a1', 'b1')
o2 = Obj('a2', 'b2')
o3 = Obj('a3', 'b3')
o4 = Obj('a4', 'b4')
o5 = Obj('a5', 'b5')

objs = Collection()
for o in (o1, o2, o3, o4, o5):
    objs.add(o)

objs.remove('a1', 'b1')
o2.a = 'a1'
o2.b = 'b1'
objs.remove('a1', 'b1')
o3.a = 'a1'
o3.b = 'b1'
objs.remove('a1', 'b1')
o4.a = 'a1'
o4.b = 'b1'
objs.remove('a1', 'b1')
o5.a = 'a1'
o5.b = 'b1'

如果我使用Python 3.4.2运行几次,有时它会成功,有时它会在删除2或3个对象后抛出KeyError:

$ python3 py_set_obj_remove_test.py
removing <__main__.Obj object at 0x7f3648035828>
removing <__main__.Obj object at 0x7f3648035860>
removing <__main__.Obj object at 0x7f3648035898>
removing <__main__.Obj object at 0x7f36480358d0>

$ python3 py_set_obj_remove_test.py
removing <__main__.Obj object at 0x7f156170b828>
removing <__main__.Obj object at 0x7f156170b860>
Traceback (most recent call last):
  File "py_set_obj_remove_test.py", line 42, in <module>
    objs.remove('a1', 'b1')
  File "py_set_obj_remove_test.py", line 27, in remove
    self.objs.remove(obj)
KeyError: <__main__.Obj object at 0x7f156170b860>

这是Python中的错误吗?或者关于套件实现的一些我不知道的事情?

有趣的是,它似乎总是在Python 2.7.9的第二次objs.remove()调用中失败。

2 个答案:

答案 0 :(得分:5)

这不是Python中的错误,您的代码违反了集合原则:哈希值不得更改。通过改变对象属性,散列更改和集合无法再可靠地定位集合中的对象。

来自__hash__ method documentation

  

如果一个类定义了可变对象并实现了__eq__()方法,那么它不应该实现__hash__(),因为hashable集合的实现要求键的哈希值是不可变的(如果对象的哈希值发生变化) ,它将在错误的哈希桶中。)

自定义Python类定义一个默认的__eq__方法,当两个操作数引用同一个对象(obj1 is obj2为真)时返回True。

有时在Python 3中起作用是字符串hash randomisation的属性。因为字符串的哈希值在Python解释器运行之间发生变化,并且因为使用了散列的模数与散列表的大小,所以可以最终得到正确的散列槽,纯粹是事故,然后==平等测试仍然是真的,因为你没有实现自定义__eq__方法。

Python 2也有哈希随机化,但默认情况下它被禁用,但你可以让你的测试通过&#39;无论如何,通过仔细挑选“正确的”&#39; ab属性的值。

相反,您可以通过将哈希基于实例的id()来使代码工作;这使得哈希值不会改变,并且会与默认的__eq__实现匹配:

def __hash__(self):
    return hash(id(self))

您也可以删除您的__hash__实现以获得相同的效果,因为默认实现基本上完成上述操作(将id()值旋转4位以避开记忆对齐模式)。同样,来自__hash__文档:

  

默认情况下,用户定义的类具有__eq__()__hash__()方法;与它们相比,所有对象都比较不相等(除了自己),x.__hash__()返回一个适当的值,x == y同时暗示x is yhash(x) == hash(y)

或者,实现一个__eq__方法,该方法基于实例属性的相等性,并且不要改变属性

答案 1 :(得分:3)

在将添加到集合之后,您正在更改对象(即更改对象的哈希)

当调用remove时,它无法在集合中找到该哈希值,因为它在计算后更改(当对象最初添加到集合中时)。