Python method - __eq__ is not called

I have a collection of objects, and I'm interested in getting a specific object from the collection. After some research, I decided to use the solution provided here: http://code.activestate.com/recipes/499299/

The problem is that it does not work.

I have two classes defined as such:

class Foo(object): def __init__(self, a, b, c): self.a = a self.b = b self.c = c def __key(self): return (self.a, self.b, self.c) def __eq__(self, other): return self.__key() == other.__key() def __hash__(self): return hash(self.__key()) class Bar(Foo): def __init__(self, a, b, c, d, e): self.a = a self.b = b self.c = c self.d = d self.e = e 

Note: the equality of these two classes should be defined only for attributes a, b, c.

The _CaptureEq at http://code.activestate.com/recipes/499299/ also defines its own __eq__ method. The problem is that this method is never called (I think). Consider

 bar_1 = Bar(1,2,3,4,5) bar_2 = Bar(1,2,3,10,11) summary = set((bar_1,)) assert(bar_1 == bar_2) bar_equiv = get_equivalent(summary, bar_2) 

bar_equiv.d should be 4, and bar_equiv .e should be 5, but it is not. As I mentioned, it looks like the __CaptureEq __eq__ method is not called when the bar_2 in summary statement is bar_2 in summary .

Is there a reason why the __CaptureEq __eq__ method is not called? Hope this is not too obscure to the issue.

+4
source share
3 answers

Brandon's answer is informative, but incorrect. There are actually two problems: one with a recipe that relies on _CaptureEq , written as an old-style class (so it won’t work properly if you try it in Python 3 with a hash-based container) and one with your own Foo.__eq__ definition Foo.__eq__ , which will finally state that the two objects are not equal when you need to say "I do not know, ask another object if we are equal."

The recipe issue is trivial to fix: just define __hash__ in the comparison shell class:

 class _CaptureEq: 'Object wrapper that remembers "other" for successful equality tests.' def __init__(self, obj): self.obj = obj self.match = obj # If running on Python 3, this will be a new-style class, and # new-style classes must delegate hash explicitly in order to populate # the underlying special method slot correctly. # On Python 2, it will be an old-style class, so the explicit delegation # isn't needed (__getattr__ will cover it), but it also won't do any harm. def __hash__(self): return hash(self.obj) def __eq__(self, other): result = (self.obj == other) if result: self.match = other return result def __getattr__(self, name): # support anything else needed by __contains__ return getattr(self.obj, name) 

The problem with your own __eq__ definition __eq__ also easy to fix: return NotImplemented when necessary so as not to require a final answer for comparison with unknown objects:

 class Foo(object): def __init__(self, a, b, c): self.a = a self.b = b self.c = c def __key(self): return (self.a, self.b, self.c) def __eq__(self, other): if not isinstance(other, Foo): # Don't recognise "other", so let *it* decide if we're equal return NotImplemented return self.__key() == other.__key() def __hash__(self): return hash(self.__key()) 

With these two fixes, you will find that the Raymond get_equivalent recipe works as it should:

 >>> from capture_eq import * >>> bar_1 = Bar(1,2,3,4,5) >>> bar_2 = Bar(1,2,3,10,11) >>> summary = set((bar_1,)) >>> assert(bar_1 == bar_2) >>> bar_equiv = get_equivalent(summary, bar_2) >>> bar_equiv.d 4 >>> bar_equiv.e 5 

Update: It has been __hash__ that an explicit override of __hash__ is only necessary for Python 3 to handle the case properly.

+6
source

The problem is that set compares two “wrong path” objects for this template to intercept the __eq__() call. The recipe from 2006 was obviously written against containers that, when asked if x present, went through candidate y already present in the container:

 x == y 

and in this case a __eq__() on x can perform special actions during the search. But the set object does the opposite:

 y == x 

for every y in the set. Therefore, this template may simply not be used on this form if your data type is set . You can confirm this by pressing Foo.__eq__() as follows:

 def __eq__(self, other): print '__eq__: I am', self.d, self.e, 'and he is', other.d, other.e return self.__key() == other.__key() 

Then you will see a message like:

 __eq__: I am 4 5 and he is 10 11 

confirming that equality comparison raises the question of equality to an object already in the set - this, alas, is not an object wrapped in a Hettinger _CaptureEq object.

Update:

And I forgot to suggest a way forward: Have you thought about using a dictionary? Since you have an idea about a key, which is a subset of the data inside the object, you may find that separating the idea of ​​the key from the idea of ​​the object itself may make it easier to try to intercept tangled objects. Just write a new function that, taking into account the object and your dictionary, calculates key and looks in the dictionary and returns an object already in the dictionary, if the key is present, adds a new key to the key.

Update 2: Okay, look at this - Nick responds using NotImplemented in one direction to force set to perform the comparison in the other direction. Give the guy a few + 1!

+4
source

There are two problems here. The first is:

 t = _CaptureEq(item) if t in container: return t.match return default 

Does not do what you think. In particular, t will never be in container since _CaptureEq does not define __hash__ . This becomes more obvious in Python 3, as it will point you to this, and not to the default __hash__ . The code for _CaptureEq seems to believe that providing __getattr__ will solve it - it won’t, because searches for Python special methods do not guarantee that all of the same steps as regular attribute searches are performed - this is the same reason for __hash__ (and various others) must be defined in the class and cannot be deactivated per instance. So, the most direct way is to define _CaptureEq.__hash__ like this:

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

But this is still not guaranteed to work, due to a second problem: set search is not guaranteed to check for equality. set are based on hash tables and only perform an equality test if there is more than one element in the hash bucket. You cannot (and do not want) force elements that hash differently into the same bucket, as these are all the details of the set implementation. The easiest way to get around this problem and carefully circumvent the first is to use a list instead:

 summary = [bar_1] assert(bar_1 == bar_2) bar_equiv = get_equivalent(summary, bar_2) assert(bar_equiv is bar_1) 
-2
source

All Articles