0

Here is a minimal reproducible example:

class Attribut:
    def __init__(
        self,
        name: str,
        other_name: str,
    ):
        self.name: str = name
        self.other_name: str = other_name
        
    def __eq__(self, other):
        if isinstance(other, Attribut):
            return self.name == other.name and self.other_name == other.other_name
        else:
            return NotImplemented
    
    def __hash__(self):
        return 0

If I try to do:

a = Attribut("lol", "a")
print(a==4)

I thought I would get NotImplemented, but instead I get False.

EDIT: (following chepner's answer) Comparing one object from one class to an object to another class instead of comparing it to an integer:

class Attribut:
    def __init__(
        self,
        name: str,
        other_name: str,
    ):
        self.name: str = name
        self.other_name: str = other_name
        
    def __eq__(self, other):
        if isinstance(other, Attribut):
            return self.name == other.name and self.other_name == other.other_name
        else:
            return NotImplemented
    
    def __hash__(self):
        return 0

class Attribut2:
    def __init__(
        self,
        name: str,
        other_name: str,
    ):
        self.name: str = name
        self.other_name: str = other_name
        
    def __eq__(self, other):
        if isinstance(other, Attribut2):
            return self.name == other.name and self.other_name == other.other_name
        else:
            return NotImplemented
    
    def __hash__(self):
        return 1

a = Attribut("lol", "a")
b = Attribut2("lol", "b")

print(a==b)

I also get False.

Also, what is the point of overriding __hash__, I cannot find a situation where this is useful?

2 Answers2

2

Overriding hash method allows you to use other methods that would involve sorting a list of your Attribut objects.

To test it create a script in which you would create such a list

a = [Attribut("lol", "a"),  Attribut("lol", "a"),  Attribut("lol", "a")]

and then try to make a set out of it

set(a)

If the method hash is implemented in Attribut class then there would be no problem. If not then the program will return an error TypeError: unhashable type: 'Attribut'

However, hash is only being used in Python hash-table and it's sole purpose is to make a number out of a class object.

Refer to these articles for more information:

Also, there can be more than one object that would have the same hash - the comparison goes deeper and checks each fields of a class in methods that use comparison between two instances (like set). To make it possible there must be implemented the first step - main class must be hashable.

The hash should also be equal for the objects that should be equal. When comparison appears, Python first checks if two objects have the same hash, if not then the comparison results in False, no matter if two objects should be 'equal'.

You can try this code and check this behaviour. Comment/uncomment function body in hash function to be same or different hash:

from random import random

class Attribut:
    def __init__(
        self,
        name: str,
        other_name: str,
    ):
        self.name: str = name
        self.other_name: str = other_name
    
    def __eq__(self, other):
        if isinstance(other, Attribut):
            return self.name == other.name and self.other_name == other.other_name
        else:
            return NotImplemented

    def __hash__(self):
        # Same hash
        # return 0
        # Different hash
        return hash(random())

c = [Attribut("lol", "a"), Attribut("lol", "a")]
print(set(c))
mlokos
  • 359
  • 2
  • 10
2

When a.__eq__(4) returns NotImplemented, you don't get the value back immediately. Instead, Python attempts to call the reflected version of __eq__ (__eq__ itself) with the other argument, namely (4).__eq__(a). It's this call that returns False.

By returning NotImplemented, you are not saying that self and other cannot be compared for equality (for that, raise a ValueError), but rather that Attribut.__eq__ does not know how to do so, but perhaps the other argument's __eq__ method does.


In your two-class example, we have the following:

  1. Attribut.__eq__ returns NotImplemented, so Attribut2.__eq__ (since self and other have different types) is tried.
  2. Attribut2.__eq__ returns NotImplemented, so what gets tried next?

Since calling Attribute.__eq__ again would just put us in an endless cycle, I think that Python falls back to object.__eq__ instead (which can compare any two objects via object identity), but this is not obvious from the descriptions of either NotImplemented or object.__eq__ itself. (This is supported by the fact that if you define both methods as

def __eq__(self, other):
    return NotImplemented

and attempt to evaluate a == a, it evaluates to True.)

The closest thing to documentation for this that I can find is the following sentence from the description of NotImplemented:

(The interpreter will then try the reflected operation, or some other fallback, depending on the operator.)

I suspect that whether or not A.__eq__ is considered the reflection of B.__eq__ depends on the context in which B.__eq__ is called. That is, A.__eq__ is not the reflection of B.__eq__ in the case where B.__eq__ was just called as the reflection of A.__eq__.

chepner
  • 497,756
  • 71
  • 530
  • 681
  • I'm not sure I understand why this doesn't work when defining two classes Attribut and Attribut2 (cf. EDIT I made to the post for a minimal reproducible example) – FluidMechanics Potential Flows Nov 01 '22 at 16:52
  • 1
    I *think* what's happening is that Python keeps track of whether it has already tried the original, and is falling back to `object.__eq__` after both `Attribut.__eq__` and `Attribut2.__eq__` have returned `NotImplented`, to avoid an infinite cycle. I'll look for some documentation to that effect. – chepner Nov 01 '22 at 16:59
  • Hey! Thanks for the help!! I'm not sure I understand what you refer to when talking about `object.__eq__`. Who defined this method? Where is it? – FluidMechanics Potential Flows Nov 01 '22 at 17:36
  • It's defined in the Python implementation itself. Basically, it provides the default definition of `==` if neither operand provides any other definition of `__eq__`. It just falls back to object identity comparison, i.e., `a == b` if and only if `a is b`. – chepner Nov 01 '22 at 17:38
  • Oh, so if both the overrides of this return `NotImplemented`, then it comes back to the default definition of it? – FluidMechanics Potential Flows Nov 01 '22 at 17:39
  • 1
    I think so. I don't see any specific mention in the documentation how the cycle is broken, but that seems the most likely explanation. (Since there are only two classes involved, it's a short enough chain to easily break.) – chepner Nov 01 '22 at 17:46