5

Consider this simple class:

class A:
    def method(self): pass
    def __eq__(self, other): return True

Now if I create two instances of the class and compare their 'method' attributes, I get different results in Python 3.7 and 3.8:

meth1 = A().method
meth2 = A().method
print(meth1 == meth2)  # True in 3.7, False in 3.8

What's going on here? Why are the methods equal in 3.7 but not in 3.8? And what does this have to do with __eq__?

Chris_Rands
  • 30,797
  • 12
  • 66
  • 100
planetp
  • 10,603
  • 14
  • 62
  • 124
  • 1
    It has nothing to do with `A`'s `__eq__` method. You are comparing two function objects. The fact that `==` returns True/False in different versions is just a Python implementation-specific thing of `function.__eq__`. – Yevhen Kuzmovych Nov 12 '19 at 16:36
  • 2
    @YevhenKuzmovych what do you mean? i think the OP's understanding is correct and the behaviour puzzling – Chris_Rands Nov 12 '19 at 16:40
  • @Chris_Rands I mean that `__eq__` method in the `A` class does not influence that. It's the function class' `__eq__` method (`A.method.__eq__`) that has been changed somehow. – Yevhen Kuzmovych Nov 12 '19 at 16:43
  • @YevhenKuzmovych if you remove the `__eq__` method you will get `False` in all python versions – Chris_Rands Nov 12 '19 at 16:45
  • `print(meth1 == meth2)` is not comparing thre instances though, it's comparing the (horribly-named) `method` instance methods on the instances. – Tom Dalton Nov 12 '19 at 16:49
  • @Chris_Rands Even though it is true, it is not `__eq__`'s business. Python never ensures that the `__eq__` method in the instance can be used to test the instance's methods. If it can, then it is just a bug or an implementation detail. – Sraw Nov 12 '19 at 16:52
  • @Chris_Rands Aha. Didn't see that coming :). That's interesting then. It actually calls `A`'s `__eq__`. WAT? – Yevhen Kuzmovych Nov 12 '19 at 16:52
  • My guess is there's an optimisation/similar that says 'if testing equality of 2 instance methods, first check if they're methods of equal/the-same instances'. – Tom Dalton Nov 12 '19 at 16:54
  • Incidentally, it's not clear to me what the expected/desired behaviour of instance-mathod-comparion should be generally - I can see arguments for True and False here. – Tom Dalton Nov 12 '19 at 16:55
  • @YevhenKuzmovych btw i agree that defining `A.__eq__` should not impact `A.method.__eq__` but apparently it does in this case. It's not actually just using `A.__eq__` though because other comparisons still return `False` e.g. `meth1 == 1` – Chris_Rands Nov 12 '19 at 16:55
  • @Chris_Rands looks like a bug to me. – Yevhen Kuzmovych Nov 12 '19 at 16:56
  • 2
    @YevhenKuzmovych a bug that was fixed for python 3.8, but i can't find anything relevant in the changelog – Chris_Rands Nov 12 '19 at 17:03
  • `A.__eq__` was never (immediately) relevant. Because `A.method` is a descriptor, the result of `A().method` is not a `function` object, but the `method` object returned by `A.method.__get__(x, A)` (where `x` is the instance of `A` on which the attribute lookup occurs). – chepner Nov 12 '19 at 17:32

1 Answers1

5

What happened is this commit based on issue 16171610 (and this discussion on python-dev).

With these (selected) comments:

It seems unlogical to me that whether or not the instance methods of two different instances are equal or not depends on the equality of the instance.

followed by

All in all I think that this part was an accident and never designed;

and

I think it makes little sense that the equality test for the instance methods takes the equality of the instances into account. Imho, this behaviour is inconsistent with the principle of no surprises. The correct behaviour (again imho of course) is that instance methods only compare equal to the same instance method of the same instance, where 'same instance' is based on 'is' not on '=='.

and

This change can be considered as a bugfix, but since it can break the user code (unlikely), it may be safer to merge it only in 3.8 and expose as a new feature.

So this seems to be considered a bugfix/feature because bound methods should only be equal if they are bound on the same instance, not if the instances are considered equal. In Python <= 3.7 the bound method equality calls the equivalent of instance1 == instance2 (thus calling your __eq__) of the instance while in Python 3.8 it checks if instance1 is instance2.


The corresponding changelog item can be found in section "Python 3.8.0 alpha 1" - it's a long list of items, so I included a copy here:

  • bpo-1617161: The hash of BuiltinMethodType instances (methods of built-in classes) now depends on the hash of the identity of __self__ instead of its value. The hash and equality of ModuleType and MethodWrapperType instances (methods of user-defined classes and some methods of built-in classes like str.__add__) now depend on the hash and equality of the identity of __self__ instead of its value. MethodWrapperType instances no longer support ordering.
MSeifert
  • 118,681
  • 27
  • 271
  • 293
  • 1
    That discussion on python-dev was really interesting, thanks for this excellent and enlightening answer :-) – Tom Dalton Nov 12 '19 at 17:37