2016-08-03 62 views
1

鑑於此程序:已經從一個Python改變刪除對象設置

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後拋出一個KeyError異常或3個對象:

$ 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()調用失敗。

回答

5

這不是Python中的錯誤,你的代碼違反了套的原則:散列值一定不能改變。通過變異你的對象屬性,哈希變化和集合不再可靠地定位集合中的對象。

__hash__ method documentation

如果一個類定義了可變對象,並實現了__eq__()方法,它不應該實行__hash__(),因爲哈希的集合的實現需要一個關鍵的哈希值是不可變的(如果對象的散列值發生變化,它將位於錯誤的散列桶中)。

定製Python類定義默認__eq__方法當兩個操作數引用同一對象(obj1 is obj2爲真),則返回true。

有時在Python 3中工作是字符串的hash randomisation屬性。由於字符串的散列值在Python解釋器運行之間發生變化,並且由於散列值與散列表大小相關,所以無論如何都會以正確的散列槽結尾,純粹是偶然的,然後是==由於您沒有實施自定義__eq__方法,所以平等測試仍然是真實的。

Python 2也具有散列隨機化功能,但默認情況下它是禁用的,但您可以通過仔細選擇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__方法的實例的屬性平等立足平等,不發生變異屬性

+0

這句話很有意思。爲什麼執行'__eq__'(用於比較,即不改變對象)影響'__hash__'的實現? – DeepSpace

+1

@DeepSpace:比較equal *的兩個對象必須*產生相同的散列。在這種情況下,默認的'__eq__'實現對同一對象的兩個引用返回true,所以即使更改這兩個屬性也不會改變相等測試結果。 –

+1

@DeepSpace:散列用於在散列表中選取一個空位(在其中有限數量的空位中,因此使用散列模表大小),然後測試相等性以查看是否同一個對象已經位於該空位。如果你的平等測試仍然傳遞兩個具有不同散列值的對象,你將打破這個測試;你可以找到一個實際上並不存在的值。 –

3

您正在更改對象(即更改對象的散列)它們被添加到集合中。

當調用remove時,它無法在集合中找到該哈希,因爲它在計算後發生更改(最初將對象添加到集合中時)。

相關問題