PEP 586 – 字面量类型
- 作者:
- Michael Lee <michael.lee.0x2a at gmail.com>,Ivan Levkivskyi <levkivskyi at gmail.com>,Jukka Lehtosalo <jukka.lehtosalo at iki.fi>
- BDFL 代表:
- Guido van Rossum <guido at python.org>
- 讨论列表:
- Typing-SIG 列表
- 状态:
- 最终
- 类型:
- 标准跟踪
- 主题:
- 类型提示
- 创建:
- 2019 年 3 月 14 日
- Python 版本:
- 3.8
- 历史记录:
- 2019 年 3 月 14 日
- 决议:
- Typing-SIG 消息
摘要
此 PEP 提出向 PEP 484 生态系统添加 *字面量类型*。字面量类型指示某个表达式确实具有特定值。例如,以下函数将仅接受确实值为“4”的表达式
from typing import Literal
def accepts_only_four(x: Literal[4]) -> None:
pass
accepts_only_four(4) # OK
accepts_only_four(19) # Rejected
动机和基本原理
Python 拥有许多 API,它们根据提供的某些参数的值返回不同的类型。例如
open(filename, mode)
根据第二个参数是否类似于r
或rb
返回IO[bytes]
或IO[Text]
。subprocess.check_output(...)
根据universal_newlines
关键字参数是否设置为True
返回字节或文本。
此模式在许多流行的第三方库中也相当常见。例如,以下是来自 pandas 和 numpy 的两个示例
pandas.concat(...)
将根据axis
参数是否设置为 0 或 1 返回Series
或DataFrame
。numpy.unique
将根据三个布尔标志值返回单个数组或包含两个到四个数组的元组。
类型问题跟踪器包含一些 其他示例和讨论。
目前无法表达这些函数的类型签名:PEP 484 不包含任何机制来编写返回值类型根据传入值而变化的签名。请注意,即使我们重新设计这些 API 以改为接受枚举,此问题仍然存在:MyEnum.FOO
和 MyEnum.BAR
都被视为类型 MyEnum
。
目前,类型检查器通过为重要的内置函数和标准库函数添加临时扩展来解决此限制。例如,mypy 带有一个插件,该插件尝试为 open(...)
推断更精确的类型。虽然此方法适用于标准库函数,但它通常是不可持续的:不能期望第三方库作者为 N 个不同的类型检查器维护插件。
我们建议添加 *字面量类型* 来解决这些差距。
核心语义
本节概述字面量类型的基本行为。
核心行为
字面量类型指示变量具有特定且具体的值。例如,如果我们将某个变量 foo
定义为类型 Literal[3]
,我们声明 foo
必须完全等于 3
,而不是其他值。
给定某个值为 v
且是类型 T
的成员,类型 Literal[v]
将被视为 T
的子类型。例如,Literal[3]
是 int
的子类型。
父类型的全部方法都将被字面量类型直接继承。因此,如果我们有一些类型为 Literal[3]
的变量 foo
,则执行诸如 foo + 5
之类的事情是安全的,因为 foo
继承了 int 的 __add__
方法。 foo + 5
的结果类型为 int
。
此“继承”行为与我们 处理 NewTypes 的方式相同。
两个字面量的等价性
当以下两个条件都为真时,两种类型 Literal[v1]
和 Literal[v2]
等价
type(v1) == type(v2)
v1 == v2
例如,Literal[20]
和 Literal[0x14]
等价。但是,Literal[0]
和 Literal[False]
*不等价*,尽管 0 == False
在运行时计算结果为“真”:0
的类型为 int
,而 False
的类型为 bool
。
简化字面量联合
字面量使用一个或多个值进行参数化。当字面量使用多个值进行参数化时,它被视为与这些类型的并集完全等价。也就是说,Literal[v1, v2, v3]
等价于 Union[Literal[v1], Literal[v2], Literal[v3]]
。
此快捷方式有助于使为接受许多不同字面量的函数编写签名更符合人体工程学——例如,像 open(...)
这样的函数
# Note: this is a simplification of the true type signature.
_PathType = Union[str, bytes, int]
@overload
def open(path: _PathType,
mode: Literal["r", "w", "a", "x", "r+", "w+", "a+", "x+"],
) -> IO[Text]: ...
@overload
def open(path: _PathType,
mode: Literal["rb", "wb", "ab", "xb", "r+b", "w+b", "a+b", "x+b"],
) -> IO[bytes]: ...
# Fallback overload for when the user isn't using literal types
@overload
def open(path: _PathType, mode: str) -> IO[Any]: ...
提供的这些值不必都是同一类型的成员。例如,Literal[42, "foo", True]
是合法类型。
但是,Literal **必须**使用至少一个类型进行参数化。像 Literal[]
或 Literal
这样的类型是非法的。
合法和非法的参数化
本节描述了构成合法 Literal[...]
类型的确切内容:哪些值可用作参数,哪些值不可用。
简而言之,Literal[...]
类型可以使用一个或多个字面量表达式进行参数化,仅此而已。
类型检查时 Literal
的合法参数
Literal
可以使用字面量整数、字节和 Unicode 字符串、布尔值、枚举值和 None
进行参数化。因此,例如,以下所有内容都是合法的
Literal[26]
Literal[0x1A] # Exactly equivalent to Literal[26]
Literal[-4]
Literal["hello world"]
Literal[b"hello world"]
Literal[u"hello world"]
Literal[True]
Literal[Color.RED] # Assuming Color is some enum
Literal[None]
**注意:**由于类型 None
仅包含一个值,因此类型 None
和 Literal[None]
完全等价。类型检查器可以将 Literal[None]
简化为 None
。
Literal
也可以使用其他字面量类型或指向其他字面量类型的类型别名进行参数化。例如,以下内容是合法的
ReadOnlyMode = Literal["r", "r+"]
WriteAndTruncateMode = Literal["w", "w+", "wt", "w+t"]
WriteNoTruncateMode = Literal["r+", "r+t"]
AppendMode = Literal["a", "a+", "at", "a+t"]
AllModes = Literal[ReadOnlyMode, WriteAndTruncateMode,
WriteNoTruncateMode, AppendMode]
此功能再次旨在帮助使使用和重用字面量类型更符合人体工程学。
**注意:**作为上述规则的结果,类型检查器也应支持如下所示的类型
Literal[Literal[Literal[1, 2, 3], "foo"], 5, None]
这应该与以下类型完全等价
Literal[1, 2, 3, "foo", 5, None]
…以及以下类型
Optional[Literal[1, 2, 3, "foo", 5]]
**注意:**像 Literal["foo"]
这样的字符串字面量类型应与常规字符串字面量在运行时一样,成为字节或 Unicode 的子类型。
例如,在 Python 3 中,类型 Literal["foo"]
等价于 Literal[u"foo"]
,因为在 Python 3 中 "foo"
等价于 u"foo"
。
类似地,在 Python 2 中,类型 Literal["foo"]
等价于 Literal[b"foo"]
——除非文件包含 from __future__ import unicode_literals
导入,在这种情况下,它将等价于 Literal[u"foo"]
。
类型检查时 Literal
的非法参数
以下参数在设计上被有意禁止
- 例如
Literal[3 + 4]
或Literal["foo".replace("o", "b")]
这样的任意表达式。- 理由:Literal 类型旨在成为 PEP 484 类型系统的一个最小扩展,并且要求类型检查器解释类型内可能存在的表达式会增加过多的复杂性。另请参阅 被拒绝或超出范围的想法。
- 因此,像
Literal[4 + 3j]
和Literal[-4 + 2j]
这样的复数也被禁止。为了保持一致性,像Literal[4j]
这样只包含单个复数的字面量也被禁止。 - 此规则的唯一例外是整数的一元
-
(减号):像Literal[-5]
这样的类型是允许的。
- 包含有效字面量类型的元组,例如
Literal[(1, "foo", "bar")]
。用户始终可以将其表示为Tuple[Literal[1], Literal["foo"], Literal["bar"]]
。此外,元组很容易与Literal[1, 2, 3]
快捷方式混淆。 - 可变字面量数据结构,如字典字面量、列表字面量或集合字面量:字面量始终隐式最终且不可变。因此,
Literal[{"a": "b", "c": "d"}]
是非法的。 - 任何其他类型:例如,
Literal[Path]
或Literal[some_object_instance]
是非法的。这包括类型变量:如果T
是一个类型变量,则不允许使用Literal[T]
。类型变量只能在类型上变化,不能在值上变化。
出于简单性考虑,以下内容被暂时禁止。我们可以在此 PEP 的未来扩展中考虑允许它们。
- 浮点数:例如
Literal[3.14]
。以干净的方式表示无限大或 NaN 的字面量很棘手;现实世界中的 API 不太可能根据浮点参数改变其行为。 - Any:例如
Literal[Any]
。Any
是一个类型,而Literal[...]
旨在仅包含值。Literal[Any]
的实际语义含义也不清楚。
运行时参数
尽管在类型检查时 Literal[...]
可能包含的参数集非常小,但 typing.Literal
的实际实现不会在运行时执行任何检查。例如
def my_function(x: Literal[1 + 2]) -> int:
return x * 3
x: Literal = 3
y: Literal[my_function] = my_function
类型检查器应该拒绝此程序:根据此规范,所有三个 Literal
的用法都是无效的。但是,Python 本身应该在没有错误的情况下执行此程序。
这部分是为了帮助我们在将来想要扩展 Literal
可用范围时保留灵活性,部分原因是在运行时无法一开始就检测所有非法参数。例如,在运行时无法区分 Literal[1 + 2]
和 Literal[3]
。
字面量、枚举和前向引用
一个潜在的歧义是在字面量字符串和对字面量枚举成员的前向引用之间。例如,假设我们有类型 Literal["Color.RED"]
。此字面量类型包含字面量字符串还是对某个 Color.RED
枚举成员的前向引用?
在这种情况下,我们始终假设用户想要构造一个字面量字符串。如果用户想要前向引用,则必须将整个字面量类型包装在字符串中,例如 "Literal[Color.RED]"
。
类型推断
本节描述了关于类型推断和字面量的一些规则,以及一些示例。
向后兼容性
当类型检查器添加对 Literal 的支持时,务必以最大限度地提高向后兼容性的方式进行。类型检查器应确保在最佳努力的基础上,添加对 Literal 的支持后,以前可以进行类型检查的代码继续进行类型检查。
这在执行类型推断时尤其重要。例如,给定语句 x = "blue"
,x
的推断类型应该是 str
还是 Literal["blue"]
?
一种简单的策略是始终假设表达式旨在成为 Literal 类型。因此,在上面的示例中,x
将始终具有 Literal["blue"]
的推断类型。这种简单的策略几乎肯定过于激进——它会导致以下程序在以前没有失败的情况下开始失败
# If a type checker infers 'var' has type Literal[3]
# and my_list has type List[Literal[3]]...
var = 3
my_list = [var]
# ...this call would be a type-error.
my_list.append(4)
此策略失败的另一个示例是在设置对象中的字段时
class MyObject:
def __init__(self) -> None:
# If a type checker infers MyObject.field has type Literal[3]...
self.field = 3
m = MyObject()
# ...this assignment would no longer type check
m.field = 4
另一种确实在每种情况下都保持兼容性的策略是始终假设表达式不是 Literal 类型,除非它们被显式注释。使用此策略的类型检查器将在上述第一个示例中始终推断 x
的类型为 str
。
这不是唯一可行的策略:类型检查器可以自由地尝试更复杂的推断技术。此 PEP 并不强制执行任何特定策略;它只强调向后兼容性的重要性。
在字面量上下文中使用非字面量
Literal 类型遵循关于子类型的现有规则,没有额外的特殊情况。例如,以下程序是类型安全的
def expects_str(x: str) -> None: ...
var: Literal["foo"] = "foo"
# Legal: Literal["foo"] is a subtype of str
expects_str(var)
这也意味着通常情况下,非 Literal 表达式不应自动转换为 Literal。例如
def expects_literal(x: Literal["foo"]) -> None: ...
def runner(my_str: str) -> None:
# ILLEGAL: str is not a subclass of Literal["foo"]
expects_literal(my_str)
注意:如果用户希望其 API 支持接受字面量和原始类型(可能是出于遗留目的),则应实现一个回退重载。请参阅 与重载的交互。
与其他类型和功能的交互
本节讨论 Literal 类型如何与其他现有类型交互。
结构化数据的智能索引
字面量可用于“智能索引”结构化类型,如元组、NamedTuple 和类。(注意:这不是详尽的列表)。
例如,当使用与有效索引对应的 int 键索引到元组中时,类型检查器应该推断出正确的类型值
a: Literal[0] = 0
b: Literal[5] = 5
some_tuple: Tuple[int, str, List[bool]] = (3, "abc", [True, False])
reveal_type(some_tuple[a]) # Revealed type is 'int'
some_tuple[b] # Error: 5 is not a valid index into the tuple
我们期望在使用诸如 getattr 之类的函数时有类似的行为
class Test:
def __init__(self, param: int) -> None:
self.myfield = param
def mymethod(self, val: int) -> str: ...
a: Literal["myfield"] = "myfield"
b: Literal["mymethod"] = "mymethod"
c: Literal["blah"] = "blah"
t = Test()
reveal_type(getattr(t, a)) # Revealed type is 'int'
reveal_type(getattr(t, b)) # Revealed type is 'Callable[[int], str]'
getattr(t, c) # Error: No attribute named 'blah' in Test
注意:请参阅 与 Final 的交互,了解有关如何以更紧凑的方式表达上述变量声明的建议。
与重载的交互
Literal 类型和重载不需要以特殊方式交互:现有的规则可以正常工作。
但是,类型检查器必须注意支持的一个重要用例是当用户不使用字面量类型时能够使用回退。例如,考虑 open
_PathType = Union[str, bytes, int]
@overload
def open(path: _PathType,
mode: Literal["r", "w", "a", "x", "r+", "w+", "a+", "x+"],
) -> IO[Text]: ...
@overload
def open(path: _PathType,
mode: Literal["rb", "wb", "ab", "xb", "r+b", "w+b", "a+b", "x+b"],
) -> IO[bytes]: ...
# Fallback overload for when the user isn't using literal types
@overload
def open(path: _PathType, mode: str) -> IO[Any]: ...
如果我们将 open
的签名更改为仅使用前两个重载,我们将破坏任何不传入字面量字符串表达式的代码。例如,以下代码将被破坏
mode: str = pick_file_mode(...)
with open(path, mode) as f:
# f should continue to be of type IO[Any] here
更广泛地说:我们建议向 typeshed 添加一项策略,该策略规定,每当我们将字面量类型添加到某个现有 API 时,我们还始终包含一个回退重载以保持向后兼容性。
与泛型的交互
像 Literal[3]
这样的类型旨在仅是 int
的普通旧子类。这意味着您可以在可以使用普通类型(例如泛型)的任何地方使用像 Literal[3]
这样的类型。
这意味着使用 Literal 类型参数化泛型函数或类是合法的
A = TypeVar('A', bound=int)
B = TypeVar('B', bound=int)
C = TypeVar('C', bound=int)
# A simplified definition for Matrix[row, column]
class Matrix(Generic[A, B]):
def __add__(self, other: Matrix[A, B]) -> Matrix[A, B]: ...
def __matmul__(self, other: Matrix[B, C]) -> Matrix[A, C]: ...
def transpose(self) -> Matrix[B, A]: ...
foo: Matrix[Literal[2], Literal[3]] = Matrix(...)
bar: Matrix[Literal[3], Literal[7]] = Matrix(...)
baz = foo @ bar
reveal_type(baz) # Revealed type is 'Matrix[Literal[2], Literal[7]]'
同样,使用涉及 Literal 类型的值限制或边界的构造 TypeVar 也是合法的
T = TypeVar('T', Literal["a"], Literal["b"], Literal["c"])
S = TypeVar('S', bound=Literal["foo"])
……尽管目前尚不清楚何时使用 Literal 上限构造 TypeVar 会有用。例如,上面示例中的 S
TypeVar 本质上毫无意义:我们可以通过使用 S = Literal["foo"]
来获得等效的行为。
注意:Literal 类型和泛型有意仅以非常基本和有限的方式交互。特别是,希望类型检查包含大量数值或 numpy 风格操作的代码的库几乎肯定可能会发现此 PEP 中提出的 Literal 类型不足以满足其需求。
我们考虑了几个不同的修复方案,但最终决定将整数泛型的难题推迟到以后解决。有关更多详细信息,请参阅 被拒绝或超出范围的想法。
与枚举和穷举检查的交互
当使用具有封闭数量变体的 Literal 类型(例如枚举)时,类型检查器应该能够执行穷尽性检查。例如,类型检查器应该能够推断出最终的 else
语句必须是 str
类型,因为 Status
枚举的所有三个值都已用尽
class Status(Enum):
SUCCESS = 0
INVALID_DATA = 1
FATAL_ERROR = 2
def parse_status(s: Union[str, Status]) -> None:
if s is Status.SUCCESS:
print("Success!")
elif s is Status.INVALID_DATA:
print("The given data is invalid because...")
elif s is Status.FATAL_ERROR:
print("Unexpected fatal error...")
else:
# 's' must be of type 'str' since all other options are exhausted
print("Got custom status: " + s)
上面描述的交互不是新的:它已经在 PEP 484 中进行了编码。但是,许多类型检查器(例如 mypy)尚未实现此功能,因为预期实现工作会很复杂。
一旦引入了 Literal 类型,其中一些复杂性将得到缓解:与其完全特殊处理枚举,我们可以将其视为大致等同于其值的联合,并利用类型检查器可能已经实现的关于联合、穷尽性、类型缩小、可达性等任何现有逻辑。
因此,Status
枚举可以被视为大致等价于 Literal[Status.SUCCESS, Status.INVALID_DATA, Status.FATAL_ERROR]
,并且 s
的类型相应地被缩小。
与收窄的交互
类型检查器可以选择对枚举和非枚举 Literal 类型执行超出上述部分描述的额外分析。
例如,基于包含或相等性检查执行缩小可能很有用。
def parse_status(status: str) -> None:
if status in ("MALFORMED", "ABORTED"):
# Type checker could narrow 'status' to type
# Literal["MALFORMED", "ABORTED"] here.
return expects_bad_status(status)
# Similarly, type checker could narrow 'status' to Literal["PENDING"]
if status == "PENDING":
expects_pending_status(status)
考虑涉及 Literal 布尔值的表达式执行缩小也可能很有用。例如,我们可以组合 Literal[True]
、Literal[False]
和重载来构建“自定义类型守卫”。
@overload
def is_int_like(x: Union[int, List[int]]) -> Literal[True]: ...
@overload
def is_int_like(x: object) -> bool: ...
def is_int_like(x): ...
vector: List[int] = [1, 2, 3]
if is_int_like(vector):
vector.append(3)
else:
vector.append("bad") # This branch is inferred to be unreachable
scalar: Union[int, str]
if is_int_like(scalar):
scalar += 3 # Type checks: type of 'scalar' is narrowed to 'int'
else:
scalar += "foo" # Type checks: type of 'scalar' is narrowed to 'str'
与 Final 的交互
PEP 591 提案建议向类型系统添加“Final”限定符。此限定符可用于声明某些变量或属性不能重新赋值。
foo: Final = 3
foo = 4 # Error: 'foo' is declared to be Final
请注意,在上面的示例中,我们知道 foo
将始终完全等于 3
。类型检查器可以使用此信息推断 foo
在任何期望 Literal[3]
的上下文中都是有效的。
def expects_three(x: Literal[3]) -> None: ...
expects_three(foo) # Type checks, since 'foo' is Final and equal to 3
Final
限定符作为声明变量为有效 Literal 的简写。
如果此 PEP 和 PEP 591 均被接受,则类型检查器应支持此快捷方式。具体来说,给定以下形式的变量或属性赋值 var: Final = value
,其中 value
是 Literal[...]
的有效参数,类型检查器应该理解 var
可用于任何期望 Literal[value]
的上下文中。
类型检查器没有义务理解 Final 的任何其他用法。例如,以下程序是否进行类型检查未作规定。
# Note: The assignment does not exactly match the form 'var: Final = value'.
bar1: Final[int] = 3
expects_three(bar1) # May or may not be accepted by type checkers
# Note: "Literal[1 + 2]" is not a legal type.
bar2: Final = 1 + 2
expects_three(bar2) # May or may not be accepted by type checkers
被拒绝或超出范围的想法
本节概述了一些明确超出范围的潜在功能。
真正的依赖类型/整数泛型
本提案实质上是在描述将一个非常简化的依赖类型系统添加到 PEP 484 生态系统中。一个明显的扩展是实现一个完整的依赖类型系统,允许用户以任意方式根据其值来预测类型。这将允许我们编写如下签名
# A vector has length 'n', containing elements of type 'T'
class Vector(Generic[N, T]): ...
# The type checker will statically verify our function genuinely does
# construct a vector that is equal in length to "len(vec1) + len(vec2)"
# and will throw an error if it does not.
def concat(vec1: Vector[A, T], vec2: Vector[B, T]) -> Vector[A + B, T]:
# ...snip...
至少,添加某种形式的整数泛型将很有用。
尽管这样的类型系统肯定有用,但它超出了此 PEP 的范围:与当前提案相比,它需要更多大量的实现工作、讨论和研究才能完成。
我们完全有可能将来会重新审视这个主题:我们很可能需要某种形式的依赖类型以及其他扩展(如可变参数泛型)来支持像 numpy 这样的流行库。
此 PEP 应被视为实现此目标的垫脚石,而不是试图提供全面的解决方案。
添加更简洁的语法
对本 PEP 的一个反对意见是,必须显式地编写 Literal[...]
感觉冗长。例如,而不是编写
def foobar(arg1: Literal[1], arg2: Literal[True]) -> None:
pass
…最好改为编写
def foobar(arg1: 1, arg2: True) -> None:
pass
不幸的是,这些缩写根本无法在运行时与现有的 typing
实现一起使用。例如,以下代码段在使用 Python 3.7 运行时会崩溃。
from typing import Tuple
# Supposed to accept tuple containing the literals 1 and 2
def foo(x: Tuple[1, 2]) -> None:
pass
运行此代码会产生以下异常。
TypeError: Tuple[t0, t1, ...]: each t must be a type. Got 1.
我们不希望用户不得不记住何时可以省略 Literal
,因此我们要求 Literal
始终存在。
更广泛地说,我们认为彻底修改 Python 中类型的语法超出了此 PEP 的范围:最好在单独的 PEP 中进行讨论,而不是将其附加到此 PEP 上。因此,此 PEP 故意没有尝试创新 Python 的类型语法。
回溯移植 Literal
类型
一旦此 PEP 被接受,则需要将 Literal
类型向后移植到捆绑了旧版本 typing
模块的 Python 版本。我们计划通过将 Literal
添加到 typing_extensions
第三方模块来实现此目的,该模块包含各种其他向后移植的类型。
实现
mypy 类型检查器目前已实现了本规范中描述的大部分行为,除了枚举 Literal 和上面描述的一些更复杂的缩小交互之外。
致谢
感谢 Mark Mendoza、Ran Benita、Rebecca Chen 和 typing-sig 的其他成员对本 PEP 的评论。
另外感谢 mypy 和 typing 问题跟踪器中的各种参与者,他们帮助提供了本 PEP 背后的许多动机和推理。
版权
本文档已置于公共领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0586.rst