Reputation: 31
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
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
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:
eq
, which can be modified dynamically.__eq__
that selects the correct equality function based on the value of self.eq
.__
, 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
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
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