PEP 586:Literal 实际上如何帮助变量返回类型?

han*_*ans 2 python numpy pep python-typing

语境

我刚刚阅读了PEP 586。在动机中,作者这样说:

numpy.unique 将返回单个数组或包含两个到四个数组的元组,具体取决于三个布尔标志值。

(...)

目前无法表达这些函数的类型签名:PEP 484 不包含任何用于编写签名的机制,其中返回类型根据传入的值而变化。

我们建议添加文字类型来解决这些差距。

但我真的不明白添加Literal类型对此有什么帮助。而且我也不同意这样的说法

PEP 484 不包含任何用于编写签名的机制,其中返回类型根据传入的值而变化。

据我所理解,Union可以在这种情况下使用。

问题

返回类型如何numpy.unique注释Literal

Dan*_*erg 5

Literal解决的问题

您从 PEP 586 中摘取了完全正确的段落。为了再次强调这里的两个关键词,这是关于

返回类型根据传入的值而变化的签名。

这是该类型的应用之一Literal。事实上,这个说法是正确的。

您能否注释一个在之前各种(未进一步定义)情况下返回两种不同类型之一的函数?当然,正如您正确指出的那样,Union可以使用 a 来实现这一点。

您能否注释一个函数,该函数根据传入的不同参数类型(或其组合)返回两种不同类型之一?是的,这就是@overload装饰器的用途。

但是注释一个函数,根据传递给它的参数的返回两种不同类型之一?这在以前是不可能的Literal

为了实现这一点,我们现在Literal与装饰器结合使用@overload。在我们开始讨论之前,请考虑以下示例np.unique


简单的例子

假设我有一个非常愚蠢的函数,它将传递的参数double加倍。float但如果还设置了特殊标志,它可以float再次返回 a 或将其作为 a 返回:str

from typing import Union

def double(
    num: float,
    as_string: bool = False,
) -> Union[float, str]:
    num *= 2
    if as_string:
        return str(num)
    return num
Run Code Online (Sandbox Code Playgroud)

现在,这个注释完全没问题。返回类型捕获两种可能的情况,即返回的floatstr被返回的情况。

但是,假设现在我有另一个只接受a 的函数str

def need_str(x: str) -> None:
    print(x.startswith("2"))
Run Code Online (Sandbox Code Playgroud)

double如果我想将作为参数的输出传递给 ,我该怎么办need_str

output = double(1.1, as_string=True)
need_str(output)
Run Code Online (Sandbox Code Playgroud)

对于严格的类型检查器来说这是一个问题。尽管代码运行良好,因为我们知道,自从我们传递 以来as_string=Trueoutput是一个字符串。静态类型检查器(mypy此处)只能看到第一个函数的返回类型和第二个函数的参数类型,并正确地抱怨:

error: Argument 1 to "need_str" has incompatible type "Union[float, str]"; expected "str"  [arg-type]
Run Code Online (Sandbox Code Playgroud)

它认为这output很可能是一个float. 它不知道double里面在做什么。我们该如何解决这个问题?好吧,在此之前Literal,我能想到的最简单的解决方案就是这样做:

error: Argument 1 to "need_str" has incompatible type "Union[float, str]"; expected "str"  [arg-type]
Run Code Online (Sandbox Code Playgroud)

这是合理的,满足类型检查器的要求并完成工作。

但现在我们有了Literal,我们可以(可以说)更优雅地解决这个问题:

output = double(1.1, as_string=True)
assert isinstance(output, str)
need_str(output)
Run Code Online (Sandbox Code Playgroud)

现在,如果我再次尝试此操作,类型检查器会理解对 的特定调用double,推断返回值的类型str,并认为下一个函数调用是类型安全的:

from typing import Literal, Union, overload

@overload
def double(
    num: float,
    as_string: Literal[False],
) -> float: ...

@overload
def double(
    num: float,
    as_string: Literal[True],
) -> str: ...

def double(
    num: float,
    as_string: bool = False,
) -> Union[float, str]:
    num *= 2
    if as_string:
        return str(num)
    return num
Run Code Online (Sandbox Code Playgroud)

添加reveal_type(output)使得mypy告诉我们Revealed type is "builtins.str"

我希望这能说明本次引入的功能以及它们以前不存在的功能。您还可以用 做其他事情Literal,但这是题外话。


这有什么帮助np.unique

正如您链接的文档所示,np.unique本质上有四种不同的可能返回类型:

  1. 一组相同dtypear
  2. 一个 2 元组,由一个相同的数组dtype组成,ar后跟一个整数数组
  3. 一个 3 元组,由一个相同的数组dtype后跟ar两个整数数组组成
  4. 一个 4 元组,由一个相同的数组dtype后跟ar三个整数数组组成

它是哪种类型(以及值的含义)完全取决于传递给参数、和 的return_indexreturn_inversereturn_counts

  1. 如果所有这些参数都是False(默认)
  2. 如果这些参数之一是True
  3. 如果其中两个参数是True
  4. 如果所有这三个论点都是True

因此,情况类似于上面的简单示例。只是还有很多需要定义@overloads,因为我们有2 3 = 8个参数组合要反映在我们的调用中。

现在,如果我有太多时间并且想编写一个无用的包装器np.unique,我将演示如何Literal使用它来正确注释所有不同的调用变体并满足最严格的类型检查器的要求......

*叹*

一个无用的包装np.unique

output = double(1.1, as_string=True)
need_str(output)
Run Code Online (Sandbox Code Playgroud)

值得注意的是,由于如此广泛的重载,理论上的可能性要大得多。如果碰巧其中一个选项会产生另一个不同 dtype元素的数组,我们仍然可以在这里正确注释这种情况。

还值得一提的是,恕我直言,这太过分了。我认为这不是好的风格。一个函数不应该这么多根本不同的调用签名。这就是有些人所说的“代码味道”......

但至于打字功能,我认为最好拥有它并且不需要它,而不是相反。


希望这可以帮助。