Motivation and Problem
In my code, it's a common idiom to have a two classes called, e.g., SomethingMutable and SomethingFrozen; these are both attrs classes decorated with @attrs.define and @attrs.frozen, respectively. Their field definitions are "the same" with minor type differences to account for mutability (list vs. tuple, set vs. frozenset, etc.)
I'd like instances of SomethingMutable and SomethingFrozen to compare equal IFF every the values for the defined fields are all equal.
What doesn't work
The attrs-generated __eq__() always checks instance types are the same before checking that the field values match, meaning that instances of different attrs classes can never compare equal. Consequently, overriding __eq__() and calling super() isn't helpful.
There's no obvious way to leverage the attrs-generated, field-by-field comparisons currently in __eq__() unless both instances are exactly the same type.
Possible approaches
It would be very helpful if there were a way to tell attrs to ignore the classes when comparing, and consider only those fields defined by the left instance's class.
This could take a number of forms, such as:
__eq__() could take an optional keyword argument ignore_type: bool, intended for use via super()
attrs could break __eq__() into two steps: one to check instance types, and one to check field values
classmethod[__attrs_types_equal__(cls, other: type) -> bool]
would replace today's __eq__()'s first step, i.e., roughly: if type(other) is not type(self): return False
__attrs_fields_equal__(self, other: Any) -> bool
would check that every field in self is matched by an attribute of other which has an equal value (or whatever attrs.field(eq=...) says to do)
- for example
- if
class Child(Parent): ... adds at least one field not found in Parent
- and given:
c: Child; p: Parent
- then it would typically be true that
p.__attrs_fields_equal__(c),
- since
p._afe_(c) considers only fields in p, all of which are also present in c (per the LSP/Liskov Substitution Principle),
- ... but typically false that
c.__attrs_fields_equal__(p)
- since
c._afe_(p) considers only fields in c, some of which are absent from p (by assumption)
__eq__() could be replaced by just a few, static lines:
return (
self.__attrs_types_equal__(type(other)) and
self.__attrs_fields_equal__(other)
)
- breaking up
__eq__() might carry a performance penalty
- but I think it should usually be possible to detect cases where a bifurcated implementation is called for, add an override flag to
attrs.define() (etc.) for edge cases, and use a unified implementation (as we do today) where we can
__eq__() could also check whether type(self).__attrs_types_equal__ is the unmodified attrs-provided version (identity comparison); and, if so, use an inlined copy of the same generated code to save the overhead of the method call
- (...and similarly for
__attrs_fields_equal__())
- this should be safe even even where there's monkeypatching
Motivation and Problem
In my code, it's a common idiom to have a two classes called, e.g.,
SomethingMutableandSomethingFrozen; these are bothattrsclasses decorated with@attrs.defineand@attrs.frozen, respectively. Their field definitions are "the same" with minor type differences to account for mutability (listvs.tuple,setvs.frozenset, etc.)I'd like instances of
SomethingMutableandSomethingFrozento compare equal IFF every the values for the defined fields are all equal.What doesn't work
The
attrs-generated__eq__()always checks instance types are the same before checking that the field values match, meaning that instances of differentattrsclasses can never compare equal. Consequently, overriding__eq__()and callingsuper()isn't helpful.There's no obvious way to leverage the
attrs-generated, field-by-field comparisons currently in__eq__()unless both instances are exactly the same type.Possible approaches
It would be very helpful if there were a way to tell
attrsto ignore the classes when comparing, and consider only those fields defined by the left instance's class.This could take a number of forms, such as:
__eq__()could take an optional keyword argumentignore_type: bool, intended for use viasuper()attrscould break__eq__()into two steps: one to check instance types, and one to check field valuesclassmethod[__attrs_types_equal__(cls, other: type) -> bool]would replace today's
__eq__()'s first step, i.e., roughly:if type(other) is not type(self): return False__attrs_fields_equal__(self, other: Any) -> boolwould check that every field in
selfis matched by an attribute ofotherwhich has an equal value (or whateverattrs.field(eq=...)says to do)class Child(Parent): ...adds at least one field not found inParentc: Child; p: Parentp.__attrs_fields_equal__(c),p._afe_(c)considers only fields inp, all of which are also present inc(per the LSP/Liskov Substitution Principle),c.__attrs_fields_equal__(p)c._afe_(p)considers only fields inc, some of which are absent fromp(by assumption)__eq__()could be replaced by just a few, static lines:__eq__()might carry a performance penaltyattrs.define()(etc.) for edge cases, and use a unified implementation (as we do today) where we can__eq__()could also check whethertype(self).__attrs_types_equal__is the unmodifiedattrs-provided version (identity comparison); and, if so, use an inlined copy of the same generated code to save the overhead of the method call__attrs_fields_equal__())