I am attempting to create a generic class Foo[T] which uses isinstance() on an instance of T passed to __init__() and then stores that result on the class so that its methods already know which T this class has without having to redo the isinstance() check every method call.
This is probably better explained with a simplified example. Here’s my current attempt:
from typing import Literal, overload, reveal_type
type FooInt = Foo[int, Literal[True]]
type FooStr = Foo[str, Literal[False]]
class Foo[T: (int, str), IS_INT: bool]:
_is_int: IS_INT
_val: T
@overload
def __init__(self: FooInt, val: int) -> None: ...
@overload
def __init__(self: FooStr, val: str) -> None: ...
def __init__[S: (FooInt, FooStr)](self: S, val) -> None:
if isinstance(val, int):
# Should be FooInt == Foo[int, Literal[True]], since this is the only possible way that __init__(int) can be called.
reveal_type(self) # Line 18
self._is_int = isinstance(val, int) # Line 19
self._val = val
# Example method utilising the cached `_is_int` value.
def bar[S: (FooInt, FooStr)](self: S) -> str:
if self._is_int:
# self._val is an int
return str(self._val + 10)
# self._val is a str
return self._val
Unfortunately, mypy outputs the following:
test.py:18: note: Revealed type is "test.Foo[builtins.int, Literal[True]]"
test.py:18: note: Revealed type is "test.Foo[builtins.str, Literal[False]]"
test.py:19: error: Incompatible types in assignment (expression has type "bool", variable has type "Literal[True]") [assignment]
test.py:19: error: Incompatible types in assignment (expression has type "bool", variable has type "Literal[False]") [assignment]
Is there any way to do this properly with the current type system?
Thanks!
P.S. I’m not sure if this should have went in the Typing category instead, but I’m looking for Help, so put it here.
If self._is_int is only used to change how self._val is handled, I would redesign the class to ensure self._val is always an int after __init__ has ended, and avoid all but a single isinstance check based on that new information.
Does the actual application you have in mind do something more complicated?
class Foo[T: (str, int)]:
_val: T
def __init__(self, val: T) -> None:
self._val = val
def bar(self) -> str:
if isinstance(self._val, int):
return str(self._val + 10)
return self._val
I’m pretty sure that if you time it then you will find that caching the result of isinstance does not make a big performance difference:
In [3]: class Foo[T: (str, int)]:
...: _val: T
...:
...: def __init__(self, val: T) -> None:
...: self._val = val
...: self._is_int = isinstance(val, int)
...:
...: def bar1(self) -> str:
...: if isinstance(self._val, int):
...: return str(self._val + 10)
...: return self._val
...:
...: def bar2(self) -> str:
...: if self._is_int:
...: return str(self._val + 10)
...: return self._val
...:
In [4]: f = Foo(1)
In [5]: %timeit f.bar1()
51.4 ns ± 0.601 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
In [6]: %timeit f.bar2()
49.1 ns ± 0.795 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
Like James I find it a bit difficult to imagine why you would want to do this rather than say converting the string to an int either in the constructor or probably just before calling the constructor.
Thanks. Yes, the actual application uses self._is_int in various different ways, including to call methods (which needs to be done lazily, not in the constructor). The code in the OP was a simplified example.
Thanks for pointing that out! I was trying to be efficient, and it’s still a 5% difference. But that is pretty small in the scheme of things.
Grand. Have you tried making _is_int into one of those TypePredicate functions, and then dispatching a different sub function, specialised for each of the bound/constrained types?
If there really is a dichotomy similar to (str, int), then it feels like it should be two different classes to me.