从没有参数的函数返回泛型

Posted

技术标签:

【中文标题】从没有参数的函数返回泛型【英文标题】:Returning Generics from functions without parameters 【发布时间】:2022-01-10 06:34:10 【问题描述】:

我刚刚进入 Python 中更复杂的类型提示内容,特别是 typing.Generic

假设我有一个基类和一个子类:

class Base:
    def base_method(self):
        pass


class Sub(Base):
    def sub_method(self):
        pass

现在我想创建一个类,它的实例变量可以是“Base 或其任何子类”。为此,我必须使用typing.TypeVartyping.Generic

class FullyTypedContainer(Generic[BaseOrSubclass]):
    def __init__(self, p: BaseOrSubclass):
        self._p = p

    @property
    def p(self) -> BaseOrSubclass:
        return self._p

这很好用; p 的类型是通过 BaseOrSubclass “传递”的,因此像 PyLance 这样的语言服务器会看到 FullTypedContainer(Sub()).p 有一个名为 sub_method() 的方法,但 FullyTypedContainer(Base()).p 没有。

那么这个函数的类型提示是什么?

def get_random_list_of_containers():
    a = random.randint(0, 1)
    if a == 0:
        return [FullyTypedContainer(Sub()), FullyTypedContainer(Base())]
    else:
        return [FullyTypedContainer(Base()), FullyTypedContainer(Sub())]

-> typing.List[FullyTypedContainer]:没有指定FullyTypedContainer 的类型,因此get_list_of_containers()[0].p 似乎被视为Any 类型。

-> typing.List[FullyTypedContainer[Base]]:“强制”所有类型为Base,(不是Base 或其子类之一),因此get_list_of_containers()[0].p 被视为从未拥有sub_method() 方法。

-> typing.List[FullyTypedContainer[typing.Union[Base, Sub]]:似乎是最好的选择,但需要我手动维护Base 的每个子类的列表。

【问题讨论】:

你真的需要列表吗?这应该是tuple吗? @juanpa.arrivillaga tuplelist 无关紧要;关键是我想知道如何输入从没有参数的函数返回的Generics 嗯,这是相关的,因为元组可以包含并且可以异构类型,但列表必须是同质的 @juanpa.arrivillaga 我现在看到了,但这实际上与我的问题相反,所以我正在编辑。 我不太清楚您的期望究竟是什么。由于输出完全是随机的,因此类型检查器必须始终假设Base 的最坏情况。即使它是“Base 或子类”,这仍然意味着不能保证比Base 更多的功能。类型检查器无法知道get_list_of_containers()[0].p 绝对不仅仅是Base。那么,你能澄清一下类型检查器在实践中应该从注解中推断出什么吗? 【参考方案1】:

这里有两个问题,这两个问题都与variance of generic types 有关:List 和您的自定义泛型类型FullyTypedContainer 都是不变的。

假设SubBase 的子类型。给定一个泛型类型GenType

如果GenType[Sub]GenType[Base] 的子类型,那么它是协变的。许多容器在直觉上是协变的。 如果GenType[Base]GenType[Sub] 的子类型,那么它是逆变的。这听起来有悖常理,但 Callable 类型实际上与它的参数类型是逆变的。如果我们需要一个带有 Sub 参数的可调用对象,则可以使用带有 Base 参数的可调用对象。 如果以上都不成立,那么GenType不变

Python 的List 类型是不变的,Generic 类型的变化取决于它的TypeVar——默认情况下TypeVars 是不变的。因此,要正确键入注释您的函数,您需要进行两处更改:

使用协变序列类型,例如Sequence。 将BaseOrSubclass 类型变量也设为协变。

在代码中,它看起来像这样:

BaseOrSubclass = TypeVar("BaseOrSubclass", bound=Base, covariant=True)

class FullyTypedContainer(Generic[BaseOrSubclass]):
    def __init__(self, p: BaseOrSubclass): ...

def get_list_of_containers() -> Sequence[FullyTypedContainer[Base]]:
    return [FullyTypedContainer(Sub()), FullyTypedContainer(Base())]

这是关于 mypy-play 的更完整示例:https://mypy-play.net/?mypy=latest&python=3.10&gist=85af8d25521c9d4b8454719685b37fc8


关于ListSequence 的注释:

技术上List 不等同于Sequence

List 始终指代内置 list 的类型。 Sequence 是定义__getitem____len__ 的任何“类列表”类型,以及一些其他方法,如__reverse__count 等。这也包括tuplestrbytes类型等。

【讨论】:

ListMutableSequence 我认为这与协方差无关,或者添加协方差可以解决问题。即使使用Sequence[FullyTypedContainer[Base]] 和协方差,您仍然会丢失每个元素的静态类型(除了Bases) @joel 但是正如你所说,列表/序列是同质的,并且没有办法对每个单独元素的类型进行编码。 Sequence[T] 意味着每个元素要么是 T 类型,要么是 T 的某个子类型,我认为这是 OP 想要的。如果 OP 想要编码“只有 Base 及其子类型 Sub,而没有其他子类型”,那么我同意 List[FullyTypedContainer[Union[Base, Sub]]] 是更准确的类型,但是你仍然需要使 FullyTypedContainer 协变(或使用List[Union[FullyTypedContainer[Base], FullyTypedContainer[Sub]]])。 @ZecongHu 关于List[FullyTypedContainer[Union[Base, Sub]] 的好点子。无论如何,我认为List[FullyTypedContainer[Base]] 更好。也许问题在于,尚不清楚 OP 希望该功能做什么。我的回答是基于他们正在使用get_list_of_containers()[0].p 进行测试的事实。索引后,协方差就无关紧要了 对不起,一旦你[0].p,协方差就无关紧要了【参考方案2】:

列表的类型是同质的:所有元素都具有相同的静态类型。您可以做的最好的事情是List[FullyTypedContainer[Base]]List[FullyTypedContainer[Union[Base, Sub]]。或者使用自定义集合,允许您将特定内容限制为特定类型并使用它而不是列表

【讨论】:

编辑了我的问题以摆脱这个tuple 建议;在我面临的实际情况中,列表内容在脚本时是未知的。

以上是关于从没有参数的函数返回泛型的主要内容,如果未能解决你的问题,请参考以下文章

根据泛型参数返回函数签名

返回类型为协议的泛型函数与参数和返回类型为协议的非泛型函数的区别

如果成功,从没有结果的函数返回错误的惯用方法是啥?

java 泛型函数需要返回Integer 我返回null 为啥报空指针异常

默认泛型可作为参数正常使用,但不能作为函数参数的返回类型使用

是否可以在 TypeScript 中传播独特的泛型类型?