How do I check for equality using super-class-level comparison?
from dataclasses import dataclass
@dataclass(order=True)
class Base:
foo: int
@dataclass(order=True)
class X(Base):
x: str
@dataclass(order=True)
class Y(Base):
y: float
x = X(0, "x")
y = Y(1, 1.0)
How do I compare x
and y
on their Base
attributes only?
Base.__eq__(x,y)
seems to do the job, is this the "pythonic" way?
Unlike C++, Java and certain OOP frameworks for C, in which classes and inheritances are implemented essentially as data structures that grow larger as classes are inherited, and bound methods, Python's objects can't be temporarily "cast" as one instance of their superclasses, where a superclass method can be called and just "see" the attributes it is interested in. Instead, the super()
operator resolves to a proxy to the next class in the inheritance chain, but any methods called in this proxy (even __eq__
) will still "see" the "self" argument as an instance of the subclass.
The differences are subtle, actually - in general, the super way will behave the same as casting - but a key difference is that whatever method is called using super()
will get the actual subclass instance as self
, and all class-attributes and class bound identifiers relate to the subclass:
the information about the instance type is on the instance itself, not in the variable slot reserved to receive the function as a parameter! While in static languages, the type information is in the variable declaration (the same if it is declared as a parameter) so, if a different object happens to be passed to a method, (through the use of cast in the caller code), it is treated as if it is the declared parameter, nonetheless. (It is a bit more subtle when inheritance gets into play, like in this case, but that is the idea)
If dataclasses generated __eq__
method would be "dumber", they could just perform an "isinstance" check, and compare the hardcoded attributes where the __eq__
is defined. But upon being called with Base.__eq__(x, y)
, instead, it goes through the operand delegating heuristics suggested when overriding operators: so it declines comparing "itself" with an instance of another (sibling) class. Note that when making this call: Base.__eq__(x, y)
it will behave like x
is from the Base class, respecting the "L" in OOP's "SOLID" -
What it doesn't assume is that it makes sense to use Base's attributes in the comparison to an instance of another subclass.
However, in Python, for pure-Python defined classes, it is possible, certain rules respected, to assign the __class__
attribute itself to an instance. This has the same effect that the cast
operator in C++ and Java: it will preserve the instance attributes as they are, while the instance itself will, for all effects, be an instance of the class assigned to the __class__
slot. The big difference to the cast
operator in those languages is that this change is "permanent": the instance is not just "seem as a member of that class for the duration of this expression", the instance is rather converted into an instance of that class.
So, we can build some code that will take both instances to be compared, find out a common ancestor, convert the operators to that ancestor class, and then just use the ==
operator normally. If this code is built into a __eq__
method of a class designed for this, the final expressions can be more convenient to use. And, of course, we can use the copy
operator to avoid modifying the original instances:
from copy import copy
class SuperComp:
def __init__(self, operand):
self.operand = operand
def __eq__(self, other):
mr1 = type(self.operand).__mro__
mr2 = type(other).__mro__
if not (common:=(set(mr1) & set(mr2))):
# no common ancestors:
return False
# take the "most advanced" common class in the first operand:
for cls in mr1:
if cls in common:
break
# make a copy of both operands and force the class to the common one:
op1 = copy(self.operand)
op2 = copy(other)
op1.__class__ = cls
op2.__class__ = cls
return op1 == op2
Using this, with your dataclasses defined above, it is possible to do:
In [81]: x = X(23, "x")
In [82]: y = Y(23, 1.0)
In [83]: SuperComp(x) == y
Out[83]: True
(As a side note: in this text I've been calling "cast", "copy" and "super" as "operators" - instead of methods, functions or keywords, because semantically that is what they are - but you are little likely finding docs referring to them using this term)