[Tutor] Operator overloading surprise [Type coersion, part Deux!]

Danny Yoo dyoo@decrem.com
Mon, 18 Mar 2002 04:26:45 -0800 (PST)


[Warning: I really go off hunting obscure stuff in this post.  Please skip
if you're beginning to learn Python; this is really not useful at all.  
*grin*]



On Mon, 18 Mar 2002, Danny Yoo wrote:

> But to fix this problem, we can add one more method to tell Python to do
> something special on coersion:
> 
> ###
> class Complex(complex):
>     def __mul__(self,other):
>         other = Complex(other)
>         t = complex.__mul__(self,other)
>         return Complex(t.real,t.imag)
>     __rmul__ = __mul__
>     
>     def __coerce__(self, other):
>         return (self, Complex(other))
> 
>     def __add__(self, other):
>         return Complex(self.real.__add__(other.real),
>                        self.imag.__add__(other.imag))
> 
>     __radd__ = __add__
> ###


This problem seemed so interesting that I can't sleep until I look at it
just a little more.  *grin*


There does appear to be a __coerce__ function defined for the standard
complex numbers.  Here's a small experiment I tried:

###
>>> complex(3,4).__coerce__(3)
((3+4j), (3+0j))
>>> c = complex(3, 4)
>>> c is complex(3, 4)
0
>>> c.__coerce__(4)[0] is c
1
###

If we look at this closely, we can deduce that the base __coerce__ method
of complex numbers might look like:

###
def __coerce__(self, other):
    return self, complex(other)
###


... but perhaps not!

###
>>> c1, c2 = complex(3, 4), Complex(3, 4)
>>> d1, d2 = c1.__coerce__(c2)
>>> c1 is d1
1
>>> c2 is d2
1
>>> complex(c2) is c2
0
###


That is, c2 looked sufficiently enough like a complex that the coersion
didn't do anything to it.  There is some wackiness involved here; I get
the distinct feeling that complex.__coerce__ is either more complicated or
more mysterious than I thought.  Let's look at the source code:

/*** in complexobject.c **/
	else if (PyComplex_Check(*pw)) {
		Py_INCREF(*pv);
		Py_INCREF(*pw);
		return 0;
	}
/*** end ***/

Ah.  If 'other' looks anything like a complex (if it's of either 'complex'
type or a subtype of 'complex', coersion won't touch it.  That was tricky!  
The documentation needs to be amended to explain this situation; it could
cause some confusion.



I've been staring at rule two:

"""
2.
    If y is a class instance:
2a.
    If y has a __coerce__() method: replace y and x with the 2-tuple 
returned by y.__coerce__(x); skip to step 3 if the coercion returns None.
2b.
    If neither x nor y is a class instance after coercion, go to step 3.
2b.
If y has a method __rop__(), return y.__rop__(x); otherwise, restore x and 
y to their value before step 2a.
"""


and I think it has a bug.  Besides the obvious cut-and-paste typo that
repeats '2b' twice in the list numbering, I believe there's a missing
step, which I'll call "2b'".

"""
2b.
    If neither x nor y is a class instance after coercion, go to step 3.

2b'.  If x has a method __op__(), return x.__op__(y).

2b''.
If y has a method __rop__(), return y.__rop__(x); otherwise, restore x and 
y to their value before step 2a.
"""


At least, that's how things appear to work from the interpreter.  Here are
three example classes I'm using to test these ideas:

###
class ComplexA(complex):
    def __coerce__(self, other):
        return (self, other)

    def __add__(self, other):
        return Complex(self.real.__add__(other.real),
                       self.imag.__add__(other.imag))
    __radd__ = __add__


class ComplexB(ComplexA):
    def __coerce__(self, other):
        return (self, complex(other))

class ComplexC(ComplexA):
    def __coerce__(self, other):
        return (self, ComplexC(other))
###


Let's see what happens:

###
>>> type(7 + ComplexA(3, 4))
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
TypeError: unsupported operand types for +: 'int' and 'ComplexA'
>>> type(7 + ComplexB(3, 4))
<type 'complex'>
>>> type(7 + ComplexC(3, 4))
<class '__main__.Complex'>
###

The situation with ComplexB appears to be what you've been running into.  
Yet, with the rules as they were written, things didn't make sense.  The 
development docs at:

   http://python.sourceforge.net/devel-docs/ref/numeric-types.html

also appear not to correct the problem.  However, if we introduce an
additional rule 2b' to that list of coersion rules, the predictions fit
more closely with reality.


In any case, this is definitely a bug in the documentation.  Arthur, bring
it up on comp.lang.python and Sourceforge again.  Someone should really
look at your example, since it does seem serious... if not a little
obscure.  *grin*

Thanks for bring this up; it was a fun problem.  Good luck to you!