Sorry for the confusing title, but I'm still finding the good way to describe my case. Basically, I want to inherit from an abstract class that takes another abstract class as argument, but how can I make the attributes of its implementation recognized correctly its argument types?
Is it confusing? Here is what I mean:
from abc import ABC
from abc import abstractmethod
from dataclasses import dataclass
# Define abstract classes
class Params(ABC):
"""An abstract class for storing parameters."""
pass
class Solver(ABC):
"""An abstract class to solve a problem with a specific parameter set."""
def __init__(self, params: Params) -> None:
"""Initialize with a Params object."""
self.params = params
@abstractmethod
def solve(self) -> None:
"""An abstract method that must be implemented in the subclasses."""
pass
# Implementation of abstract classes
@dataclass
class ParamsX(Params):
"""A subclass of Params which has attributes first_param and second_param."""
first_param: int
second_param: list[float]
class SolverX(Solver):
"""A subclass of Solver which solve the problem X."""
def __init__(self, params: ParamsX) -> None:
"""Initialize with the ParamsX object."""
super().__init__(params)
def solve(self) -> None:
"""Solve the problem X."""
print(self.params) # The params is recognized as Params
print(self.params.first_param) # The first_param is recognized as Any
print(self.params.second_param) # The second_param is recognized as Any
# There're also ParamsY and SolverY, ParamsZ and SolverZ, etc
My question: How can I make the first_param
and second_param
show respectively as the int
and list[float]
types? I haven't check with mypy
but the Visual Studio Code indicates that.
An attempt:
If I do with the following code, the types are correct but I don't think it's a pythonic way, it will be like the abstract class Params
doesn't have any purpose at all.
class SolverX(Solver):
"""A subclass of Solver which takes a ParamsX object as argument."""
def __init__(self, params: ParamsX) -> None:
super().__init__(params)
self.params: ParamsX # Forced annotation
A big thanks to TeamSpen210 who gave me this answer:
What you need is a
Generic
, not necessarily anABC
.Change
Solver
to be like this:ParamT = TypeVar('ParamT', bound=Params) class Solver(ABC, Generic[ParamT]): def __init__(self, param: ParamT) -> None: self.params = param class SolverX(Solver[ParamsX]): ....
TypeVar
defines a type variable, which you later substitute with a specific type, bound restricts it to only permit subclasses of the specified type. Then having the class "subclass"Generic
means that the variable is scoped to the whole class. So then when you use the subscripted version, it's as if the type variable was replaced by the real class.Note the other way you can use it is specific to a function - you put it in the signature, then it takes on whatever type you use to call the function.
The MyPy docs have a lot more info about how things work, as well as say PEP484: https://mypy.readthedocs.io/en/stable/generics.html
There's also typeshed, the type definitions for the standard library: https://github.com/python/typeshed/tree/master/stdlib