A. Lin
A. Lin

Reputation: 31

Monkey patching __eq__ in Python

Having some trouble understanding why I'm able to re-define (monkey patch) __eq__ outside of a class, but not change its definition through __init__ or in a method:

class SpecialInteger:
    def __init__(self,x):
        self.x = x
        self.__eq__ = self.equals_normal
    def equals_normal(self,other):
        return self.x == other.x
    def equals_special(self,other):
        return self.x != other.x
    def switch_to_normal(self):
        self.__eq__ = self.equals_normal
    def switch_to_special(self):
        self.__eq__ = self.equals_special

a = SpecialInteger(3)
b = SpecialInteger(3)

print(a == b)  # false

a.switch_to_normal()
print(a == b)  # false

SpecialInteger.__eq__ = SpecialInteger.equals_normal
print(a == b)  # true

SpecialInteger.__eq__ = SpecialInteger.equals_special
print(a == b)  # false

Am I just using self incorrectly or is there some other reason it works like this?

Upvotes: 3

Views: 834

Answers (4)

Samantha Atkins
Samantha Atkins

Reputation: 678

All method definitions are defined at class level (literally the name is a key in a dict belonging to the class). This is also true of anything else you put at class level. Which is why for instance a variable assignment outside a method in a class produces a class variable.

Upvotes: 1

Alex Huszagh
Alex Huszagh

Reputation: 14634

Just to add on to an excellent existing answer, but this doesn't work because you are modifying the class instance, and not the class.

In order to get the behavior you desire, you can modify the class during __init__, however, this is woefully inadequate (since it modifies the class, and therefore all instances of the class), and you are better off making those changes visible at the class scope.

For example, the following are equivalent:

class SpecialInteger1:
    def __init__(self,x):
        self.x = x
        self.__class__.__eq__ = self.equals_normal
    ...

class SpecialInteger2:
    def __init__(self,x):
        self.x = x
    def equals_normal(self,other):
        return self.x == other.x
    def __eq__(self, other):
        return self.equals_normal(other)

You should prefer case SpecialInteger2 in all examples, since it is more explicit about what it does.

However, none of this actually solves the issue you are trying to solve: how can I create a specialized equality comparison at the instance level that I can toggle? The answer is through the use of an enum (in Python 3):

from enum import Enum

class Equality(Enum):
    NORMAL = 1
    SPECIAL = 2

class SpecialInteger:
    def __init__(self, x, eq = Equality.NORMAL):
        self.x = x
        self.eq = eq
    def equals_normal(self, other):
        return self.x == other.x
    def equals_special(self, other):
        return self.x != other.x
    def __eq__(self, other):
        return self.__comp[self.eq](self, other)
    # Define a dictionary for O(1) access 
    # to call the right method.
    __comp = {
        Equality.NORMAL: equals_normal,
        Equality.SPECIAL: equals_special
    }

Let's walk through this quickly, since there are 3 parts:

  1. An instance member variable of eq, which can be modified dynamically.
  2. An implementation of __eq__ that selects the correct equality function based on the value of self.eq.
  3. A namespace-mangled dictionary (a class/member variable that starts with __, in this case, self.__comp) that allows efficient lookup of the desired equality method.

The dictionary can easily be done-away with, especially for cases where you only wish to support 1-5 different possible comparisons, and replaced with idiomatic if/then statements, however, if you ever wish to support many more comparison options (say, 300), a dictionary will be much more efficient O(1) than if/then comparisons (linear search, O(n)).

If you wish to do this with setters (like in the original example), and actually hide the member functions from the user, you can also do this by directly storing the function as a variable.

Upvotes: 1

SCB
SCB

Reputation: 6149

The easiest way to keep the same functionality would be to just refer to some other variable from __eq__. It could be some reference variable, or a saved method.

class SpecialInteger:
    def __init__(self,x):
        self.x = x
        self._equal_method = self.equals_normal

    # ...

    def switch_to_normal(self):
        self._equal_method = self.equals_normal

    def switch_to_special(self):
        self._equal_method = self.equals_special

    def __eq__(self, other):
        return self._equal_method(other)

Upvotes: 0

MrName
MrName

Reputation: 2529

To do it inside the class, you would simply define the __eq__ method inside of your class.

class SpecialInteger:
    def __init__(self,x):
        self.x = x

    def __eq__(self, other):
        # do stuff, call whatever other methods you want

EDIT: I see what you are asking, you wish to override the method (which is a "magic" method) at the instance level. I don't believe this is possible in the base construct of the language, per this discussion.

The reason your monkey patch works in that example is because it is being passed on the Class level, as opposed to the instance level, whereas self is referring to the instance.

Upvotes: 3

Related Questions