Welcome to the Treehouse Community

The Treehouse Community is a meeting place for developers, designers, and programmers of all backgrounds and skill levels to get support. Collaborate here on code errors or bugs that you need feedback on, or asking for an extra set of eyes on your latest project. Join thousands of Treehouse students and alumni in the community today. (Note: Only Treehouse students can comment or ask questions, but non-students are welcome to browse our conversations.)

Looking to learn something new?

Treehouse offers a seven day free trial for new students. Get access to thousands of hours of content and a supportive community. Start your free trial today.

Python

Why do comparisons work even though we do not convert "other" to an int?

In this video, we are using magic methods to compare the values of various dice.

For example, consider this method:

def __eq__(self, other):
    return int(self) == other 

This method allows us to check whether or not two die are equal in value.

In the method, we need to convert our object to an int to do the comparison, but there is no such requirement for the other parameter. Yet, somehow, python successfully can do the comparison. The same goes for all the other comparison methods we define in this video.

My question is, how does python manage to calculate a value for other in order to do the comparison successfully? How does it know that it is comparing two ints, or how does it know to convert other to an int before converting? I am sure that this is some "magic" going on behind the scenes, but would someone be able to illuminate for me what is going on?

The video I am referring to this: https://teamtreehouse.com/library/comparing-and-combining-dice

4 Answers

Chris Freeman
MOD
Chris Freeman
Treehouse Moderator 68,154 Points

The comparison operation is reflexive. Given, a == b, Python will automatically try b == a if needed.

If d1 and d2 are instances of D6, the flow would be:

d1 = D6(value= 3)
d2 = D6(value= 6)

d1 == d2  # call d1.__eq__
int(d1) == d2
3 == d2  # reverse
d2 == 3  # call d2.__eq__
int(d2) == 3
6 == 3

This action can be seen in the code

import random


class Die:
    def __init__(self, sides=2, value=0):
        print("running Die.__init__")
        if not sides >= 2:
            raise ValueError("Must have at least 2 sides")
        if not isinstance(sides, int):
            raise ValueError("Sides must be a whole number")
        self.value = value or random.randint(1, sides)

    def __int__(self):
        print("running Die.__int__")
        return self.value


class D6(Die):
    def __init__(self, value=0):
        print("running D6.__init__")
        super().__init__(sides=6, value=value)

    def __eq__(self, other):
        print("running D6.__eq__", self, other)
        return int(self) == other

    def __ne__(self, other):
        print("running D6.__ne__")
        return int(self) == other

    def __gt__(self, other):
        print("running D6.__gt__")
        return int(self) > other

    def __lt__(self, other):
        print("running D6.__le__")
        return int(self) < other

    def __ge__(self, other):
        print("running D6.__ge__")
        return int(self) > other or int(self) == other

    def __le__(self, other):
        print("running D6.__le__")
        return int(self) < other or int(self) == other

    def __add__(self, other):
        print("running D6.__add__", self, other)
        return int(self) + other

    def __radd__(self, other):
        print("running D6.__radd__ with int(self)", self, other)
        return int(self) + other


d1 = D6(value= 3)
print('d1', d1)
d2 = D6(value= 6)
print('d2', d2)

print(d1 == d2)

Output:

running D6.__init__
running Die.__init__
d1 <__main__.D6 object at 0x1163fcb38>
running D6.__init__
running Die.__init__
d2 <__main__.D6 object at 0x11640b320>
running D6.__eq__ <__main__.D6 object at 0x1163fcb38> <__main__.D6 object at 0x11640b320>
running Die.__int__
running D6.__eq__ <__main__.D6 object at 0x11640b320> 3
running Die.__int__
False

Update:

When an object is on the right-side of an operator a reflexive method is tried: __radd__ instead of __add__

For comparison operators, there isn’t an explicit corresponding reflexive method.

From the docs

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, __le__() and __ge__() are each other’s reflection, and __eq__() and __ne__() are their own reflection.

So, comparing 3 > d2 would actually go

3 > d2  # reverse
d2 < 3  # call d2.__lt__
int(d2) < 3
6 < 3
running D6.__le__
running Die.__int__
Steven Parker
Steven Parker
220,925 Points

Golly, that's more complicated than coercion! I expected that it did it the simpler way. :see_no_evil:

Okay, here is what I am understanding from this:

1) First, the object on the left is reduced to a primitive data type 2) The appropriate reflexive method for the object on the right, as defined in its own class code, is called to reduce it to a primitive data type. 3) The two primitive datatypes are compared to give the final result

Is that right?

I see from this that it tries the equation one way, goes as far as it can until it ends up with a primitive value, then flips it and goes through again until it is comparing two primitive values.

That makes sense for an easily reversible comparison, like ==, but what about < or >, where which side of the operator you are on matters?

Great! Thanks for your help, Chris!