Re: class semantics

Mark Lutz (mlutz@KaPRE.COM)
Fri, 3 Feb 1995 16:15:31 +0700

While I'm making trouble...

There seems to be no way to overload comparison operators,
except for "<instance> <op> <instance>" cases. I can't
compare an instance and a true number, the way I can mix
instances and numbers for add, subtract, etc., even if I've
defined a __cmp__ method:

x = Thing(9) # make some instances
y = Thing(11)

x < y, cmp(x,y)... # ok; calls __coerce__, __cmp__
x < 12, cmp(x,12)... # no way to overload these

Defining __cmp__, __rcmp__, and/or __coerce__ doesn't help.
I attached a program that demonstrates the problem below.
A more detailed description of how this might be fixed appears
before that.

Again, this is mostly a 'wish', and is somewhat specific
to what I'm trying to do [make a C++ object wrapper act like
a native Python object]; not sure how big a need there is for
this. And again, I could force users to call coerce() or
int() on possibly-wrapped-objects, before using them in
comparisons.

But it's another place where instances can't really be used
like other kinds of objects, even if they've got the relevant
overload methods. '__cmp__' should probably work like '__add__'.
Have I missed something? Already fixed in 1.2 (didn't see it
in 1.1.1)?

Mark L.

-------------WARNING: overly-detailed description follows!---------------

Here's what I saw (attached code):

- The first scheme, using __coerce__ to convert *UP* before __cmp__
is called, only works for "x > y" cases. It fails on the "x > 12"
case: neither __coerce__ nor __cmp__ get called.

- The alternative scheme, using __cmp__/__rcmp__ to convert *DOWN*
never works right: neither __cmp__ nor __rcmp__ get called for
"x > 12" cases, and __rcmp__ isn't called for "x > y" cases
(since it's really a "12 > y" case after __cmp__!).

In the failing cases, we fall back on type-name comparison (and all
"int" are > all "instance"). It looks like this starts in function
cmpobject(), file object.c:

if ((tp = v->ob_type) != w->ob_type) {
if (tp->tp_as_number != NULL &&
w->ob_type->tp_as_number != NULL) {
if (coerce(&v, &w) != 0) {
err_clear();
}
else {
int cmp = (*v->ob_type->tp_compare)(v, w);
... return cmp
}
}
return strcmp(tp->tp_name, w->ob_type->tp_name);
}

For mixed instance/number cases, both Inttype and Instancetype
have a 'tp_as_number' slot, so we go to coerce(). But neither
type defines a 'nb_coerce' slot, so we don't call '__coerce__',
and coerce() [bltinmodule.c] fails. And since coerce() fails,
we don't run the 'tp_compare' (which would trigger '__cmp__').
When 2 instances are compared, we jump past this block, and call
the instance 'tp_compare' directly...

Not sure how to fix this yet, but here's some ideas. The
interpreter applies special methods in mixed ways-- some at
an outer-level (in ceval.c), and some by the same process
used for built-in types.

1) It might work to add a BINOP("__cmp__", "__rcmp__") call to
cmp_outcome() in ceval.c, as done for add(), or(), etc.
We'd never get to cmpobject() in this case. Probably need
a BINOP in cmp_member() too.

2) Adding a 'nb_coerce' slot to 'instance_as_number', which
points to code that runs the __coerce__/__rcoerce__ BINOP,
would make the coerce() call work, and we'd get to the
instance's 'tp_compare' (instance_compare).

3) Adding logic similar to ceval's BINOP at the start of
coerce() would have the same effect as idea (2). It would
just be moved from builtin_coerce() to coerce().

The BINOP solution (1) seems most plausible, but I may be
missing something about the interpreter's internals.
This would support both overloading schemes (converting
up or down) at the 'top-level' (for operators). But
cmpobject() and coerce() both get called in a lot more
places-- ideas (2) or (3) would have to be used to make
these work if we want them to...

It's late on Friday, and I haven't had a chance to test
or debug any of this. Sorry-- hope you got the idea.


----TEST CODE----------------------------------------------------------------

from types import *

class Thing1:
def __init__(self, value):
self.v = value
def __repr__(self):
return '<Thing1=' + `self.v` + '>'
def __cmp__(self, other):
print 'Thing1.cmp', self, other
return cmp(self.v, other.v)
def __coerce__(self, other):
print 'Thing1.coerce', self, other
if type(other) == IntType:
return self, Thing(other)
elif type(other) == type(self) and other.__class__ == self.__class__:
return self, other

class Thing2:
def __init__(self, value):
self.v = value
def __repr__(self):
return '<Thing2=' + `self.v` + '>'
def __cmp__(self, other):
print 'Thing2.cmp', self, other
return cmp(self.v, other)
def __rcmp__(self, other):
print 'Thing2.rcmp', self, other
return cmp(other, self.v)

def test():
x, y = Thing1(8), Thing1(9)
print (x < y, x > y, cmp(x,y)) # ok; calls __coerce__ and __cmp__
print (x < 7, 7 < x, cmp(x,7)) # oops: type-name comparison

x, y = Thing2(8), Thing2(9)
print (x < y, x > y, cmp(x,y)) # oops: calls __cmp__, but not __rcmp__
print (x < 7, 7 < x, cmp(x,7)) # oops: "instance" < "int"...

test()

OUTPUT (Python 1.1):
>>> import testcmp
Thing1.coerce <Thing1=8> <Thing1=9>
Thing1.cmp <Thing1=8> <Thing1=9>
Thing1.coerce <Thing1=8> <Thing1=9>
Thing1.cmp <Thing1=8> <Thing1=9>
Thing1.coerce <Thing1=8> <Thing1=9>
Thing1.cmp <Thing1=8> <Thing1=9>
(1, 0, -1) <- x < y: right (8 < 9)
(1, 0, -1) <- x < 7: wrong (8 > 7)
Thing2.cmp <Thing2=8> <Thing2=9>
Thing2.cmp <Thing2=8> <Thing2=9>
Thing2.cmp <Thing2=8> <Thing2=9>
(0, 1, 1) <- x < y: wrong (8 < 9)
(1, 0, -1) <- x < 7: wrong (8 > 7)

-----------------------------------------------------------------------------