在 Python 类型中指定 TypeVar 的附加子类边界

Specifying additional subclassed bounds of a TypeVar in Python typing

当结合使用类型提示和 mypy 时,我想定义一个泛型 class,它允许访问仅对某些泛型 classes 可用的属性。我想知道如何在 Python 类型提示中定义它。

例如,让我们举个例子:

class SomeObject:
    pass
class SomeObjectWithAttr:
    def __init__(self, attr):
        self.attr = attr

class AppendListWithAttrs:
    def __init__(self):
        self._list = []
        self._attrs = []
    def append(self, obj, record_attr=False):
        self._list.append(obj)
        if record_attr:
            self._attrs.append(obj.attr)

l = AppendListWithAttrs()
l.append(SomeObject())
l.append(SomeObjectWithAttr())
l.append(SomeObjectWithAttr(), record_attr=True)

我已经尝试对此进行注释,但 mypy 似乎不太高兴。这是我最好的尝试:

from typing import Generic, TypeVar, Protocol, overload, Literal, Union

T = TypeVar("T")

class HasAttr(Protocol):
    attr: str

class AppendListWithAttrs(Generic[T]):
    def __init__(self):
        self._list: list[T] = []
        self._attrs: list[str] = []

    @overload
    def append(self: 'AppendListWithAttrs[HasAttr]', obj: HasAttr, record_attr: Literal[True]): ...
    @overload
    def append(self, obj: T, record_attr: Literal[False]): ...

    def append(self, obj: T, record_attr: bool = False):
        self._list.append(obj)
        if record_attr:
            self._attrs.append(obj.attr)

这也不完全正确,但我正在有效地寻找一种方法来告诉 mypy T 可以是任何东西,但如果它受 HasAttr 限制,它可能会访问这个attr。也许像 subclassing T with HasAttr 这样的东西,但也许我看错了。

你会如何注释?

这个怎么样?它passes MyPy.

from typing import overload, Literal, Union, Any, Protocol, Generic, TypeVar


class GenericObjectProto(Protocol):
    pass


class ObjectWithAttrProto(GenericObjectProto, Protocol):
    attr: str
        
    
T = TypeVar('T', bound=GenericObjectProto)


class AppendListWithAttrs(Generic[T]):
    def __init__(self) -> None:
        self._list: list[T] = []
        self._attrs: list[str] = []
        
    @overload
    def append(self, obj: GenericObjectProto, record_attr: Literal[False] = False) -> None: ...
    
    @overload
    def append(self, obj: ObjectWithAttrProto, record_attr: Literal[True] = ...) -> None: ...
        
    def append(self, obj,  record_attr: bool = False) -> None:
        self._list.append(obj)
        if record_attr:
            self._attrs.append(obj.attr)
            

class SomeObject:
    pass


class SomeObjectWithAttr:
    def __init__(self, attr: str) -> None:
        self.attr = attr


mylist = AppendListWithAttrs[SomeObject]()
mylist.append(SomeObject())
mylist.append(SomeObjectWithAttr('hello'))
mylist.append(SomeObjectWithAttr('hello'), record_attr=True)
reveal_type(mylist._list) # Revealed type is "builtins.list[__main__.SomeObject*]"

工作原理

通过让 ObjectWithAttr 协议继承自 GenericObject 协议,我们可以获得一些特殊的类型提示魔法。通过这样做,我们将 ObjectWithAttr 定义为 GenericObject 结构 子类型。这意味着当我们将 SomeObjectWithAttr 的实例附加到 SomeObject 实例的列表时,MyPy 不会抱怨。由于我们定义的“结构继承”方案,MyPy 理解 SomeObjectWithAttrSomeObject 实例列表完全兼容,即使没有 actual 继承正在进行中。

注意事项

这通过了 MyPy,但前提是您不将它与 --strict 设置一起使用,因为我没有在 .append 的具体实现中注释 obj。 MyPy 似乎能够使用现有注释完美地推断出类型,但如果您在 --strict 设置中按原样 运行 上面的代码,它仍然会抱怨您遗漏了注释。即使有重载,它也不喜欢你用 TUnion 类型注释 obj (这对我来说似乎有点错误),所以如果你是 运行ning MyPy on --strict,你可以在具体实现中用Any注释obj,或者在行尾放一个# type: ignore[no-untyped-def]