[Python-ideas] Binary arithmetic does not always call subclasses first

Steven D'Aprano steve at pearwood.info
Sun Apr 23 22:54:14 EDT 2017


On Sun, Apr 23, 2017 at 06:23:12PM -0700, Stephan Hoyer wrote:
> I recently filed this as a bug, and was asked to repost to python-dev or
> python-ideas for greater visibility:
> http://bugs.python.org/issue30140
> 
> Without further ado, here is my original report:
[...]

> The reference documentation on binary arithmetic [3] states:
> 
> > Note: 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.
> 
> However, this isn't actually done if the right operand merely inherits from
> the left operand's type. In practice, CPython requires that the right
> operand defines a different method before it defers to it. Note that the
> behavior is different for comparisons, which defer to subclasses regardless
> of whether they implement a new method [4].

I think that's a bug. At the very least, comparisons and operators 
should behave the same.

I think that the comparison behaviour is correct: the subclass 
reflected method should be called, even if the method isn't explicitly 
over-ridden.


> I think this behavior is a mistake and should be corrected. It is just as
> useful to write generic binary arithmetic methods that are well defined on
> subclasses as generic comparison operations.

Agreed.


[...]
> Here is a simple example, of a well-behaved type that implements addition
> by wrapping its value and that returns NotImplemented when the other
> operand has the wrong type:

I often write classes like your example, and it is humbling to realise 
that they are buggy! I must admit I misread the docs and didn't notice 
the "and overrides" language, and clearly I didn't test my classes 
sufficiently or else I would have discovered this myself :-(


> A does not defer to subclass B:
> 
> >>> A(1) + B(1)
> A(2)
> 
> But it does defer to subclass C, which defines new methods (literally
> copied/pasted) for __add__/__radd__:
> 
> >>> A(1) + C(1)
> C(2)

To me, that's the deciding point. We have two ways to go, and one of 
them encourages the copy-and-paste anti-pattern, the other doesn't.

I think that's a Bad Thing and we should treat the comparison behaviour 
as correct, and the other operators are buggy. An explicit over-ride 
shouldn't be necessary.


> Mark Dickinson's response:
>
> The "Coercion rules" section[1] of the Python 2.7 docs is a bit more
> explicit about the intent:
> 
> """
> Exception to the previous item: if the left operand is an instance of a
> built-in type or a new-style class, and the right operand is an instance of
> a proper subclass of that type or class and overrides the base’s __rop__()
> method, the right operand’s __rop__() method is tried before the left
> operand’s __op__() method.
> """
> 
> so the check for an override was clearly intentional, rather than an
> implementation convenience or accident. (It's also clearly intentional in
> the source and comments.)

The next paragraph tells us:

    This is done so that a subclass can completely override binary 
    operators. Otherwise, the left operand’s __op__() method would 
    always accept the right operand: when an instance of a given class 
    is expected, an instance of a subclass of that class is always 
    acceptable.


but as far as I can tell, the comparison behaviour equally accomplishes 
that, without an explicit over-ride.


> The 3.x docs don't have the "and overrides"
> language; I haven't figured out why and when that language changed.
> 
> [1]
> https://docs.python.org/release/2.7.6/reference/datamodel.html#coercion-rules




More information about the Python-ideas mailing list