I’m trying to define a generic base class that can instantiate wrapped instances of itself like so
from typing import Callable, TypeVar, Generic
T = TypeVar("T")
U = TypeVar("U")
class From(Generic[T]):
def __init__(self, val: T):
self.val = val
@classmethod
def unit(cls, val):
return cls(val)
def bind(self, func: Callable[[T], U]):
return self.unit(func(self.val))
Basically, when calling bind
on a From
instance with a function that changes the type, e.g.
def to_string(i: int) -> str:
return str(i)
From(1).bind(to_string) # here, self.val = "1"
The type checker will complain because cls
in From.unit
is already parametrized by T
(hence passing U
to From[T]
raises that U
is not assignable to T
)
You can overcome this problem by returning a parametrized instance of From
in the unit
method like so:
class From(Generic[T]):
...
@classmethod
def unit(cls, val: U) -> "From[U]":
return From[U](val)
which works like a charm.
However, the problem occurs when I let another class inherit from From
. I lose generality:
class Just(From[T]):
pass
Just(1).bind(to_string) # equals From("1"), instead of Just("1")
Not accepting the solution that you should define a new unit
method with the same form and class name on every inheriting instance, is there some way to re-parametrize the type of the cls
variable in the original unit
method?
I know you can use cls.__orig_bases__
to find (Generic[~T])
if you print it inside the unit
method.
So I thought maybe you can do something like setattr(cls, "__orig_bases__", (Generic[U],))
But this seems senseless and in case is not picked up by the type checker.
Ideally there should be some method on self
that allows you to access the unparametrized base class, so you could self.<get_baseclass>[U](<val>)
.
Right now my solution is adding # type: ignore
in unit
like so:
@classmethod
def unit(cls, val: U) -> "From[U]":
"""Return a new instance of the same encapsulating class, wrapping `val`"""
return cls(val) # type: ignore
But this is not ideal.
1
Use Self
as the return type for both unit
and bind
.
class From(Generic[T]):
def __init__(self, val: T):
self.val = val
@classmethod
def unit(cls, val) -> Self: # returns an *instance* of cls
return cls(val)
def bind(self, func: Callable[[T], U]) -> Self:
return self.unit(func(self.val))
Now reveal_type(Just(1).bind(to_string))
returns Just[int]
as desired.