Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Comparison operators called in reverse order for subclasses with no override. #66251

Closed
mdickinson opened this issue Jul 23, 2014 · 10 comments
Closed
Labels
docs Documentation in the Doc dir

Comments

@mdickinson
Copy link
Member

BPO 22052
Nosy @mdickinson, @bitdancer, @vadmium, @serhiy-storchaka

Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.

Show more details

GitHub fields:

assignee = None
closed_at = <Date 2020-05-31.13:19:14.369>
created_at = <Date 2014-07-23.21:03:30.071>
labels = ['docs']
title = 'Comparison operators called in reverse order for subclasses with no override.'
updated_at = <Date 2020-05-31.13:19:14.368>
user = 'https://github.com/mdickinson'

bugs.python.org fields:

activity = <Date 2020-05-31.13:19:14.368>
actor = 'serhiy.storchaka'
assignee = 'docs@python'
closed = True
closed_date = <Date 2020-05-31.13:19:14.369>
closer = 'serhiy.storchaka'
components = ['Documentation']
creation = <Date 2014-07-23.21:03:30.071>
creator = 'mark.dickinson'
dependencies = []
files = []
hgrepos = []
issue_num = 22052
keywords = []
message_count = 10.0
messages = ['223778', '223789', '223812', '223838', '233836', '248163', '251397', '251410', '251411', '370440']
nosy_count = 5.0
nosy_names = ['mark.dickinson', 'r.david.murray', 'docs@python', 'martin.panter', 'serhiy.storchaka']
pr_nums = []
priority = 'normal'
resolution = 'fixed'
stage = 'resolved'
status = 'closed'
superseder = None
type = None
url = 'https://bugs.python.org/issue22052'
versions = ['Python 2.7']

@mdickinson
Copy link
Member Author

As reported in a StackOverflow question [1]: the order in which the special comparison methods are called seems to be contradictory to the docs [2]. In the following snippet, __eq__ is called with reversed operands first:

>>> class A:
...     def __eq__(self, other):
...         print(type(self), type(other))
...         return True
... 
>>> class B(A):
...     pass
... 
>>> A() == B()
<class '__main__.B'> <class '__main__.A'>
True

However, the docs note that:

"""If the right operand’s type is a subclass of the left operand’s type and that subclass provides the reflected method for the operation, this method will be called before the left operand’s non-reflected method. This behavior allows subclasses to override their ancestors’ operations."""

... which suggests that this reversal should only happen when the subclass B *overrides* A's definition of __eq__ (and indeed that's the usual behaviour for arithmetic operations like __add__).

Looking more closely, that statement in the docs is in the 'numeric-types' section, so it's not clear that its rules should apply to the comparison operators. But either way, some doc clarification could be useful.

[1] http://stackoverflow.com/q/24919375/270986
[2] https://docs.python.org/3.5/reference/datamodel.html#emulating-numeric-types

@mdickinson mdickinson added the docs Documentation in the Doc dir label Jul 23, 2014
@bitdancer
Copy link
Member

"the subclass provides" doesn't actually imply anything about overriding, I think. For __eq__, though, that means that *every* class "provides" it. Indeed, I've always thought of the rule as "the subclass goes first" with no qualification, but that's only true for __eq__.

It looks like the "subclass goes first" note is missing from the __eq__ section.

But see bpo-21408. I find it hard to reason about this algorithm, so I could be completely wrong :)

@mdickinson
Copy link
Member Author

"the subclass provides" doesn't actually imply anything about overriding, I think.

Yes, that was the thrust of one of the SO answers. Unfortunately, that explanation doesn't work for arithmetic operators, though: there an explicit override is necessary. Here's another example, partly to get away from the extra complication of __eq__ being its own inverse. After:

    class A(object):
        def __lt__(self, other): return True
        def __gt__(self, other): return False
        def __add__(self, other): return 1729
        def __radd__(self, other): return 42

    class B(A): pass

we get:

    >>> A() + B()
    1729
    >>> A() < B()
    False

So the addition is calling the usual __add__ method first (the special exception in the docs doesn't apply: while B *is* a subclass of A, it doesn't *override* A's __radd__ method). But the comparison is (surprisingly) calling the __gt__ method first.

So we've got two different rules being followed: one for arithmetic operators, and a different one for comparisons.

This isn't a big deal behaviour-wise: I'm certainly not advocating a behaviour change here. But it would be nice to document it.

@bitdancer
Copy link
Member

Ah yes. I remember there being a discussion somewhere about the differences between comparison operator inverses and the arithmetic 'r' methods, but I can't find it at the moment. I *thought* there was a full discussion of the logic involved in these cases, but I can't find that either. We need one somewhere that we can crosslink to if it doesn't already exist.

@vadmium
Copy link
Member

vadmium commented Jan 11, 2015

I have included some rules about the priority for calling reflected operator methods in my patch to bpo-4395

@vadmium
Copy link
Member

vadmium commented Aug 7, 2015

My patch was committed for Python 3.4+. The priority of the comparator methods is now documented at the end of <https://docs.python.org/dev/reference/datamodel.html#richcmpfuncs\>. Perhaps all that is left to do here is to apply similar changes to the Python 2 documentation.

@vadmium
Copy link
Member

vadmium commented Sep 23, 2015

Does anyone know enough about Python 2 to propose a fix? I don’t know enough about object classes versus “instance” classes, and potential interference of the __cmp__() method. In Python 2 the order seems to depend on the class type:

(<main.A instance at 0x7f730d37f5f0>, <main.B instance at 0x7f730d37f518>)
(<main.B object at 0x7f730d37dc10>, <main.A object at 0x7f730d37d110>)

Or perhaps we should just close this now and forget about Python 2 ;)

@mdickinson
Copy link
Member Author

For Python 2, I think the most we should do is document the behaviour somewhere; changing it in a bugfix release seems both unnecessary and potentially risky.

@mdickinson
Copy link
Member Author

the most we should do is document the behaviour somewhere

And indeed, perhaps this issue counts as sufficient documentation...

@serhiy-storchaka
Copy link
Member

Python 2.7 is no longer supported.

@ezio-melotti ezio-melotti transferred this issue from another repository Apr 10, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
docs Documentation in the Doc dir
Projects
None yet
Development

No branches or pull requests

4 participants