classmethod __eq__ comparisons and Any wildcards
Hey, I noticed that writing a class with a classmethod `__eq__`, it does not work as expected: ```python class ANY: @classmethod def __eq__(cls, other): return True assert ANY == 2 # succeeds assert ANY == 2 # fails ``` This is, to my understanding, because `ANY == 2` translates into `type(ANY).__eq__(2)`. However, I think it would be useful to allow user implementation of `classmethod` dunder methods that work as expected, or at least put a big disclaimer in the documentation. I am not sure how difficult this would be to do, but I imagine that `type` could check if the given object/class has a classmethod __eq__ and if so use it as a replacement for `type(obj).__eq__` As a sidenote, I think it would actually be kinda cool to have the `typing.Any` behave this way, i.e. always comparing to True. One example where this would be useful is that it would allow comparisons like ```python assert mytuple == (Any, "abc") ``` which would then be equivalent, but imo a lot more elegant than ```python assert len(mytuple) == 2 and mytuple[-1] == "abc" ``` Best regards - Randolf
A typo, it should be: ``` assert ANY() == 2 # succeeds assert ANY == 2 # fails ```
On Fri, Sep 10, 2021 at 12:28 AM Randolf Scholz <randolf.scholz@gmail.com> wrote:
Hey,
I noticed that writing a class with a classmethod `__eq__`, it does not work as expected:
```python class ANY: @classmethod def __eq__(cls, other): return True
assert ANY == 2 # succeeds assert ANY == 2 # fails ```
This is, to my understanding, because `ANY == 2` translates into `type(ANY).__eq__(2)`. However, I think it would be useful to allow user implementation of `classmethod` dunder methods that work as expected, or at least put a big disclaimer in the documentation. I am not sure how difficult this would be to do, but I imagine that `type` could check if the given object/class has a classmethod __eq__ and if so use it as a replacement for `type(obj).__eq__`
Yeah, a classmethod doesn't work like that. What you want is a metaclass, which allows you to change the class of the class itself: class AnyMeta(type): def __eq__(self, other): return True class ANY(metaclass=AnyMeta): pass assert ANY == 2 Although at this point, you probably don't want a metaclass at all, as an instance will suffice: class AnyType: def __eq__(self, other): return True ANY = AnyType() del AnyType # optional assert ANY == 2 Under what circumstances do you actually want the same handler for both the class and its instances? Seems unusual. But if you do, a metaclass will let you do exactly that. ChrisA
Hey Chris, thanks for the response. Right, the metaclass approach should work because then type(ANY) returns AnyMeta. Still, I find it a bit unintuitive that `classmethod` does not work and that one needs to either write two classes to achieve this behaviour, or to create an instance of a class.
Under what circumstances do you actually want the same handler for both the class and its instances? Seems unusual.
Well, I think the main application are non-instantiable classes such as `None`, things like `typing.Any` or the example `ANY` here. Since the only purpose of this class is to equality compare to `True`, it doesn't really make sense to allow instances of this class. Disclaimer: my background is mathematics and not computer science so apologies if I am talking non-sense here....
09.09.21 17:19, Randolf Scholz пише:
As a sidenote, I think it would actually be kinda cool to have the `typing.Any` behave this way, i.e. always comparing to True. One example where this would be useful is that it would allow comparisons like
```python assert mytuple == (Any, "abc") ```
which would then be equivalent, but imo a lot more elegant than
```python assert len(mytuple) == 2 and mytuple[-1] == "abc" ```
Did you try to use unittest.mock.ANY? typing.Any cannot behave this way. First, list[Any] and list[int] are different types. Second, an object which is equal to everything cannot be hashable, but typing.Any is hashable, and should be hashable for using in caching.
As pointed out, you want metaclass for this. A warning, I've overwritten many of the dunders on classes for fun and I actually broke the ability to repr my class in certain situations by overriding __eq__ and/or __hash__ something deep in ipython/python with the repr. I forget the exact situation, I think I implemented __eq__ but not __hash__ and it broke the golden rule of x == y => x is y i,.e. hash(x) == hash(y) Great way to really learn the data model but you will be in for a bit of pain trying to debug these things.
On Tue, Sep 14, 2021 at 11:30 PM <johnmelendowski@gmail.com> wrote:
I think I implemented __eq__ but not __hash__ and it broke the golden rule of x == y => x is y i,.e. hash(x) == hash(y)
Not sure what you're referring to there. The rule regarding hashes is that if x == y, then hash(x) == hash(y). Identity doesn't come into it, other than in the trivial sense that, for most classes, hash(x) == hash(x) (in other words, the hash is stable), and x == x, which complies with the rule. But if x != x (eg with float("nan")), there's no problem. Incidentally, I don't think it's documented, but an object's hash should never change. Otherwise, you can get bizarre behaviours:
class X: ... def __init__(self, value): self.thing = value ... def __eq__(self, other): return self.thing == other.thing ... def __hash__(self): return hash(self.thing) ... stuff = {X(5): "hello", X(7): "world"} stuff[X(5)] 'hello'
Well and good. But if we mutate the key...
list(stuff)[0].thing = 2 for key in stuff: ... print(key, stuff[key]) ... Traceback (most recent call last): File "<stdin>", line 2, in <module> KeyError: <__main__.X object at 0x7f1420409180>
... we can iterate over the keys in the dictionary, use that exact object to subscript the dictionary, and it isn't found. That key/value pair has been placed in the bucket for 5, and won't be found in 2's bucket. If equality can change, __hash__ should be omitted. ChrisA
participants (4)
-
Chris Angelico
-
johnmelendowski@gmail.com
-
Randolf Scholz
-
Serhiy Storchaka