rbaleksandar
rbaleksandar

Reputation: 9701

Why does greater than and unequal operators work even though only less than and equal operator has been overloaded

I'm currently looking into how Python does its operator overloading. So far I find it much more appealing than in C++ especially in term of operators such as * (or similar arithmetic operators) that has to handle an operation that can be applied both right to left (2*x) and left to right (x*2) in some way.

I have the following class as a test:

from math import sqrt

class Vector3:
    def __init__(self, x,y,z):
        self.x = x
        self.y = y
        self.z = z

    def __repr__(self):
        return 'Vector3(x=%d, y=%d, z=%d)' % (self.x, self.y, self.z)

    def __str__(self):
        return '[x: %d, y: %d, z: %d]' % (self.x, self.y, self.z)

    def length(self):
        return sqrt(self.x**2 + self.y**2 + self.z**2)

    def __add__(self, vector):
        return Vector3(self.x + vector.x, self.y + vector.y, self.z + vector.z)

    def __sub__(self, vector):
        return Vector3(self.x - vector.x, self.y - vector.y, self.z - vector.z)

    def __mul__(self, scalar):
        return Vector3(self.x * scalar, self.y * scalar, self.z * scalar)

    __rmul__ = __mul__ # Right multiplication equals left multiplication (if this defers, __rmul__ has to be overwritten and defined manually)

    def __eq__(self, vector):
        return (self.x == vector.x and self.y == vector.y and self.z == vector.z)

    def __lt__(self, vector):
        return self.length() < vector.length()

    @staticmethod
    def compareAndPrint(vector1, vector2):
        if vector1 == vector2: return 'v1 == v2 since len(v1) = %f == %f = len(v2)' % (vector1.length(), vector2.length())
        elif vector1 < vector2: return 'v1 < v2 since len(v1) = %f < %f = len(v2)' % (vector1.length(), vector2.length())
        elif vector1 > vector2: return 'v1 > v2 since len(v1) = %f > %f = len(v2)' % (vector1.length(), vector2.length())

v1 = Vector3(1,2,3)
v2 = Vector3(0,-1,1)
v3 = v1 + v2
v4 = v3 - v1
v5 = v1 * 2
v6 = 2 * v1
print(v1)
print(v2)
print(v3)
print(v4)
print(v5)
print(v6)

print(Vector3.compareAndPrint(v1,v2))
print(Vector3.compareAndPrint(v2,v1))
print(Vector3.compareAndPrint(v1,v1))

I'm simply adding more and more operators to my custom class and observing how those behave. You might have noticed two things (based on my question in the title):

For some reason I get the output I would expect as if I have overloaded >:

[x: 1, y: 2, z: 3]
[x: 0, y: -1, z: 1]
[x: 1, y: 1, z: 4]
[x: 0, y: -1, z: 1]
[x: 2, y: 4, z: 6]
[x: 2, y: 4, z: 6]
v1 > v2 since len(v1) = 3.741657 > 1.414214 = len(v2)
v1 < v2 since len(v1) = 1.414214 < 3.741657 = len(v2)
v1 == v2 since len(v1) = 3.741657 == 3.741657 = len(v2)

Does Python handle this automatically or have I done something that I haven't noticed to make this work? The only thing that comes into my mind is that Python takes the inverse of < and at the same time adds exclusion for the == since the inverse of > is <= and not just <.

The same thing applies to the != (unequal) operator. Here I'm 99% sure that Python inverts the overloaded == operator.

Upvotes: 1

Views: 1025

Answers (2)

user2357112
user2357112

Reputation: 281604

Most binary operators in Python can be overloaded by either operand. There's one method for the left operand to define, like __add__ for addition, and one for the right operand, like __radd__. The only one I recall that can only be overloaded by one operand is in, which the right side must define.

For comparisons, instead of __gt__ and __rgt__ methods, __rgt__ is just __lt__. That means that when you do left_thing > right_thing and left_thing doesn't know what to do, Python tries right_thing < left_thing. Since you've implemented __lt__, this works.

Note that Python will not try anything involving __le__, __ge__, or __eq__ if __gt__ and __lt__ fail.

Upvotes: 2

mgilson
mgilson

Reputation: 310089

This is a bit of the python data model that many seem to not understand. To trace this through the documentation we need to start with binary arithmetic operations (__mul__, __add__, etc.).

We notice that there is a __mul__ and a __rmul__ method. The difference is described in docs under the latter:

These methods are called to implement the binary arithmetic operations (+, -, *, /, %, divmod(), pow(), **, <<, >>, &, ^, |) with reflected (swapped) operands. These functions are only called if the left operand does not support the corresponding operation and the operands are of different types.

Now when we look at the documentation for rich comparison methods:

There are no swapped-argument versions of these methods (to be used when the left argument does not support the operation but the right argument does); rather, __lt__() and __gt__() are each other’s reflection

So, what's happening in your case is that since __gt__ hasn't been overloaded, python actually swaps the order of the arguments and calls __lt__. Pretty neat.


FWIW, if you want to construct a class that is orderable with other instances of the class, the functools.total_ordering decorator can be super helpful. You just supply __lt__ and __eq__ and the decorator supplies the rest.

Upvotes: 2

Related Questions