更正抽象 class 实现的属性类型

Correct attribute types of abstract class implementation

抱歉标题令人困惑,但我仍在寻找描述我的案例的好方法。基本上,我想继承一个以另一个抽象 class 作为参数的抽象 class,但是我怎样才能使其实现的属性正确识别其参数类型?

是不是很迷惑?这就是我的意思:

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

我的问题:如何让first_paramsecond_param分别显示为intlist[float]类型?我没有检查 mypy 但 the Visual Studio Code indicates that.

一次尝试:

如果我使用下面的代码,类型是正确的,但我不认为这是 pythonic 方式,它就像抽象 class Params 没有任何目的完全没有。

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

如何在 class 级别指定变量:

class SolverX(Solver):
    """A subclass of Solver which solves the problem X."""
    params: ParamsX

    def __init__(self, params: ParamsX) -> None:
        """Initialize with the ParamsX object."""
        super().__init__(params)

非常感谢 TeamSpen210 给我这个答案:

What you need is a Generic, not necessarily an ABC.

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