coady / multimethod

Multiple argument dispatching.
https://coady.github.io/multimethod
Other
277 stars 24 forks source link

`Self` cannot be used with `issubclass` #114

Closed sylvorg closed 6 months ago

sylvorg commented 7 months ago

Hello!

It seems that the Self type hint cannot be used with issubclass; would you happen to have any suggestions on how to create a dispatched function that accepts an instance of the current class?

Thank you kindly for the help!

coady commented 6 months ago

Do you have an example use case? I've only seen Self used as a return type.

sylvorg commented 6 months ago

Something like the following:

def __eq__(self: Self, expr: Self) -> bool:
        return expr() == self()

So basically, I'd like to be able to check if expr is an instance of the current class, and I'd like to avoid having to repeat the name of the class if I can help it! 😅 Not to mention I can't seem to use the class name as a type hint anyway...

sylvorg commented 6 months ago

Would something like types = signature(inspect._findclass(func) if t is Self else t for t in types) in the __setitem__ method in the multimethod class work?

coady commented 6 months ago

There's a lot to consider here. A few thoughts:

  1. Forward references are supported, as a string:

    class Base:
    def __eq__(self, expr: 'Base') -> bool:

    or using a future import:

    from __future__ import annotations
    ...
    def __eq__(self, expr: Base) -> bool:
  2. As for Self, I think there is some ambiguity. Your example looks like the intention is that expr match the defining class. Whereas it could be more strictly interpreted to match the runtime type of self. That's analogous to how it works as a return type. Consider:

    
    class Base:
    @multimethod
    def __eq__(self, expr: Self) -> bool:
        return type(self) is type(expr)

class Subclass(Base): ...

assert Base() == Base() assert Subclass() == Subclass() assert not Base().eq(Subclass()) assert not Subclass().eq(Base()) # should raise DispatchError? because not isinstance(Base(), Subclass)



3. Python assumes that `__eq__` is commutative, and may reverse the args. Meaning it has an ad-hoc binary dispatch already. So I think this is a particularly confusing example to start with, as opposed to just a normal named method.
sylvorg commented 6 months ago
  1. Ah; got it. Thanks!
  2. Sorry, I'm not to good with the terminology and the internals of python; basically, I want to be able to check if one instance of a particular class is the same as another instance of that same class, hence why I'm calling self and expr, where the class has a custom __call__ method that returns a string that may be the same for both instances.
  3. When reversing the args, does that mean that self could be passed in as the second argument and expr could be passed in as the first when using __eq__? So if there was another __eq__ dispatched with expr annotated as a string, that could get self instead?

Again, sorry for the confusion; while I'm not exactly new to python, I'm not very good with the more complicated aspects of the language! 😅

sylvorg commented 6 months ago

Would something like types = signature(inspect._findclass(func) if t is Self else t for t in types) in the __setitem__ method in the multimethod class work?

This new version seems to work for some reason, allowing me to use Self to refer to the current class:

if any(t is Self for t in types):
    types = signature(inspect._findclass(func) if t is Self else t for t in types)

I'm not exactly sure why the above works but types = signature(inspect._findclass(func) if t is Self else t for t in types) causes test_defaults to fail with multimethod.DispatchError: ('func: 0 methods found', (<class 'int'>,), set())...

Again, sorry for not quite understanding your earlier points about this!

coady commented 6 months ago

I recommend putting aside multiple dispatch, and focusing on core Python features: __eq__, isinstance, and type. What you want is probably something like:

class Base:
    def __eq__(self, other):
        return isinstance(other, Base) and self() == other()
sylvorg commented 6 months ago

I guess that's fair... Thanks for the advice, and sorry for the trouble!