Python 3 __cmp__ semantic change?

Steven D'Aprano steve at REMOVE-THIS-cybersource.com.au
Sat Nov 22 20:06:20 EST 2008


On Sat, 22 Nov 2008 09:10:04 +0000, Arnaud Delobelle wrote:

> That's not surprising.  You're measuring the wrong things.  If you read
> what I wrote, you'll see that I'm talking about Fraction.__gt__ being
> slower (as it is defined in terms of Fraction.__eq__ and
> Fraction.__lt__) using when my 'totally_ordered' decorator.
> 
> I haven't done any tests but as Fraction.__gt__ calls *both*
> Fraction.__eq__ and Fraction.__lt__ it is obvious that it is going to be
> roughly twice as slow.


What's obvious to you and what's obvious to the Python VM are not 
necessarily the same thing. I believe you are worrying about the wrong 
thing. (BTW, I think your earlier decorator had a bug, in that it failed 
to define __ne__ but then called "self != other".) My tests suggest that 
relying on __cmp__ is nearly three times *slower* than your decorated 
class, and around four times slower than defining all the rich 
comparisons directly:

$ python comparisons.py
Testing FractionCmp... 37.4376080036
Testing FractionRichCmpDirect... 9.83379387856
Testing FractionRichCmpIndirect... 16.152534008
Testing FractionDecoratored... 13.2626030445

Test code follows. If I've made an error, please let me know.


[comparisons.py]
from __future__ import division

def totally_ordered(cls):
    # Define total ordering in terms of __eq__ and __lt__ only.
    if not hasattr(cls, '__ne__'):
        def ne(self, other):
            return not self == other
        cls.__ne__ = ne
    if not hasattr(cls, '__gt__'):
        def gt(self, other):
            return not (self < other or self == other)
        cls.__gt__ = gt
    if not hasattr(cls, '__ge__'):
        def ge(self, other):
            return not (self < other)
        cls.__ge__ = ge
    if not hasattr(cls, '__le__'):
        def le(self, other):
            return (self < other or self == other)
        cls.__le__ = le
    return cls


class AbstractFraction:
    def __init__(self, num, den=1):
        if self.__class__ is AbstractFraction:
            raise TypeError("abstract base class, do not instantiate")
        assert den > 0, "denomintator must be > 0"
        self.num = num
        self.den = den
    def __float__(self):
        return self.num/self.den

class FractionCmp(AbstractFraction):
    def __cmp__(self, other):
        return cmp(self.num*other.den, self.den*other.num)

class FractionRichCmpDirect(AbstractFraction):
    def __eq__(self, other):
        return (self.num*other.den) == (self.den*other.num)
    def __ne__(self, other):
        return (self.num*other.den) != (self.den*other.num)
    def __lt__(self, other):
        return (self.num*other.den) < (self.den*other.num)
    def __le__(self, other):
        return (self.num*other.den) <= (self.den*other.num)
    def __gt__(self, other):
        return (self.num*other.den) > (self.den*other.num)
    def __ge__(self, other):
        return (self.num*other.den) >= (self.den*other.num)

class FractionRichCmpIndirect(AbstractFraction):
    def __cmp__(self, other):
        return cmp(self.num*other.den, self.den*other.num)
    def __eq__(self, other):
        return self.__cmp__(other) == 0
    def __ne__(self, other):
        return self.__cmp__(other) != 0
    def __lt__(self, other):
        return self.__cmp__(other) < 0
    def __le__(self, other):
        return self.__cmp__(other) <= 0
    def __gt__(self, other):
        return self.__cmp__(other) > 0
    def __ge__(self, other):
        return self.__cmp__(other) >= 0


class FractionDecoratored(AbstractFraction):
    def __eq__(self, other):
        return self.num*other.den == self.den*other.num
    def __lt__(self, other):
        return self.num*other.den < self.den*other.num

FractionDecoratored = totally_ordered(FractionDecoratored)

def test_suite(small, big):
    assert small < big
    assert small <= big
    assert not (small > big)
    assert not (small >= big)
    assert small != big
    assert not (small == big)

from timeit import Timer

test = 'test_suite(p, q)'
setup = '''from __main__ import %s as frac
from __main__ import test_suite
p = frac(1, 2)
q = frac(4, 5)
assert float(p) < float(q)'''

for cls in [FractionCmp, FractionRichCmpDirect, FractionRichCmpIndirect, 
FractionDecoratored]:
    t = Timer(test, setup % cls.__name__)
    print "Testing %s..." % cls.__name__, 
    best = min(t.repeat())
    print best
[end comparisons.py]



-- 
Steven



More information about the Python-list mailing list