告诉基 class 的方法在派生的 class 中使用更多受限类型提示?

Tell methods of base class to use more restricted type hints in a derived class?

场景

假设我有一个通用的 Store class,它实现了各种方法来检索 StoreObjects。为了填充存储,它定义了一个抽象方法 load_object.

然后我创建一个 CarStore。我从 Store 派生并将 load_object 方法覆盖为 return Car 对象。

现在的问题是如何为此添加类型提示。首先是代码:

from typing import Dict
import weakref
import abc


class StoreObject:
    pass


class Car(StoreObject):
    def __init__(self, color: str):
        self.color = color  # type: str


class Store(abc.ABC):
    def __init__(self):
        self._cache = weakref.WeakValueDictionary()  # type: weakref.WeakValueDictionary[int, StoreObject]

    def get(self, index: int) -> StoreObject:
        try:
            return self._cache[index]
        except KeyError:
            obj = self.load_object(index)
            self._cache[index] = obj
            return obj

    @abc.abstractmethod
    def load_object(self, index: int) -> StoreObject:
        raise NotImplementedError


class CarStore(Store):
    def load_object(self, index: int) -> Car:
        if index < 100:
            return Car("red")
        else:
            return Car("blue")


store = CarStore()
car = store.get(10)
print("Your car color is", car.color)

类型检查错误

问题出在以下行:

print("Your car color is", car.color)

此处PyCharm给出以下警告:

Unresolved attribute reference 'color' for class 'StoreObject'

Mypy 给出以下错误:

development/storetyping.py:39: error: "StoreObject" has no attribute "color"

此外 PyCharm 代码完成显然不包括 store.get(10).?name 方法。

问题

如何输入基数 class 以便 PyCharmmypy 可以成功检查此代码?

有没有办法参数化 Store 中的类型,这样在创建 CarStore 时我可以告诉它在注释中使用 Car 而不是 StoreObject

您的类型检查运行正常; get 没有在 CarStore 中被覆盖,所以它上面的注释继续指定它 returns StoreObject。如果要更改注释,则必须在 CarStore 中重新定义 get,例如添加:

def get(self, index: int) -> Car:
    return typing.cast(Car, super().get(index))

确保 import typing 获得对 cast 的访问权限(或使用不合格的 cast 并将其添加到您的 from typing import Dict 导入)。

为避免运行时性能开销,您只能根据 if typing.TYPE_CHECKING: 测试有条件地定义 get(其中 returns True 当静态检查器分析代码时, False 当 运行 它时),因此 get 重载实际上并未在运行时定义。

在更多静态语言中,您可以将 Store 创建为泛型 class,并在从 Store.

继承时使用 Car 作为类型参数

我们实际上可以使用 python 中的 typing 模块来做到这一点。

这是一个最小的例子:

from typing import Generic, TypeVar


T = TypeVar('T')  # this is the generic placeholder for a type

# Store is a generic class with type parameter T
class Store(Generic[T]):
    def get(self) -> T:  # this returns a T
        return self.load_object()

    def load_object(self) -> T:  # this also returns a T
        raise NotImplementedError


class Car:
    def __init__(self, color):
        self.color = color

# Now we inherit from the Store and use Car as the type parameter
class CarStore(Store[Car]):
    def load_object(self):
        return Car('red')


s = CarStore()
c = s.get()
print(c.color)  # Code completion works and no warnings are shown

编辑:

解决 ShadowRanger 的注意事项:如果您希望 Car 和所有产品具有共同的基础 class,您可以使用 TypeVarbound 参数。谢谢 juanpa.arrivillaga 的提示。

所以我们创建一个产品 class 并将 TypeVar 绑定到它。

class Product:
    def get_id(self):
        raise NotImplementedError

T = TypeVar('T', bound=Product)

Mypy 现在会抱怨这个:

class CarStore(Store[Car]):
    def load_object(self):
        return Car('red')

因为 Car 不是 Product。所以让我们也改变一下:

class Car(Product):
    def get_id(self):
        return ...

    def __init__(self, color):
        self.color = color

现在,mypy 很开心。

编辑2:

这是带有更多注释的完整代码,即使 mypy --strict 也很高兴。

from typing import Generic, TypeVar


class Product:
    def get_id(self) -> int:
        raise NotImplementedError


T = TypeVar('T', bound=Product)


class Store(Generic[T]):
    def get(self) -> T:
        return self.load_object()

    def load_object(self) -> T:
        raise NotImplementedError


class Car(Product):
    def get_id(self) -> int:
        return hash(self.color)

    def __init__(self, color: str):
        self.color = color


class CarStore(Store[Car]):
    def load_object(self) -> Car:
        return Car('red')


if __name__ == '__main__':
    s = CarStore()
    c = s.get()
    print(c.color)