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)返回IO[bytes]或IO[Text],具体取决于第二个参数是r还是rb等。subprocess.check_output(...)返回字节或文本,具体取决于universal_newlines关键字参数是否设置为True。
这种模式在许多流行的第三方库中也相当常见。例如,这里是 pandas 和 numpy 的两个示例
pandas.concat(...)将返回Series或DataFrame,具体取决于axis参数是设置为 0 还是 1。numpy.unique将返回单个数组或包含两到四个数组的元组,具体取决于三个布尔标志值。
类型问题跟踪器包含一些 其他示例和讨论。
目前无法表达这些函数的类型签名:PEP 484 不包含任何机制来编写返回类型根据传入值而变化的签名。请注意,即使我们将这些 API 重新设计为接受枚举,此问题仍然存在:MyEnum.FOO 和 MyEnum.BAR 都被认为是 MyEnum 类型。
目前,类型检查器通过为重要的内置函数和标准库函数添加临时扩展来解决此限制。例如,mypy 捆绑了一个插件,它试图为 open(...) 推断更精确的类型。虽然这种方法适用于标准库函数,但它通常不可持续:期望第三方库作者为 N 个不同的类型检查器维护插件是不合理的。
我们提议添加 字面量类型 来解决这些空白。
核心语义
本节概述了字面量类型的基线行为。
核心行为
字面量类型表示变量具有特定且具体的值。例如,如果我们将变量 foo 定义为 Literal[3] 类型,我们声明 foo 必须精确等于 3,而不是任何其他值。
给定作为类型 T 成员的某个值 v,类型 Literal[v] 应被视为 T 的子类型。例如,Literal[3] 是 int 的子类型。
父类型中的所有方法都将直接被字面量类型继承。因此,如果我们的变量 foo 的类型是 Literal[3],那么执行 foo + 5 等操作是安全的,因为 foo 继承了 int 的 __add__ 方法。foo + 5 的结果类型是 int。
这种“继承”行为与我们 处理 NewType 的方式相同。
两个字面量的等效性
当以下两个条件都为真时,两个类型 Literal[v1] 和 Literal[v2] 等效
type(v1) == type(v2)v1 == v2
例如,Literal[20] 和 Literal[0x14] 是等效的。但是,Literal[0] 和 Literal[False] 不等效,尽管在运行时 0 == False 评估为“true”: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")]。- 理由:字面量类型旨在作为 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]是非法的。这包括 TypeVars:如果T是 TypeVar,则不允许使用Literal[T]。TypeVars 只能在类型上变化,而不能在值上变化。
为了简化,以下内容被暂时禁止。我们可以在本 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]"。
类型推断
本节描述了一些关于类型推断和字面量的规则,以及一些示例。
向后兼容性
当类型检查器添加对字面量的支持时,重要的是它们以最大限度地向后兼容的方式进行。类型检查器应确保以前进行类型检查的代码在添加字面量支持后仍能继续进行,并尽最大努力。
这在执行类型推断时尤为重要。例如,给定语句 x = "blue",x 的推断类型应该是 str 还是 Literal["blue"]?
一个天真的策略是总是假定表达式旨在成为字面量类型。因此,在上面的例子中,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
另一种 确实 在所有情况下都保持兼容性的策略是,除非明确注明,否则总是假定表达式 不是 字面量类型。使用此策略的类型检查器将始终推断出上面第一个示例中 x 的类型是 str。
这不是唯一可行的策略:类型检查器应该随意尝试更复杂的推断技术。本 PEP 不强制规定任何特定策略;它只强调向后兼容的重要性。
在字面量上下文中使用非字面量
字面量类型遵循现有的子类型规则,没有额外的特殊处理。例如,以下程序是类型安全的
def expects_str(x: str) -> None: ...
var: Literal["foo"] = "foo"
# Legal: Literal["foo"] is a subtype of str
expects_str(var)
这也意味着非字面量表达式通常不应自动转换为字面量。例如
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 支持同时接受字面量 和 原始类型——也许是出于遗留目的——他们应该实现一个回退重载。请参阅 与重载的交互。
与其他类型和功能的交互
本节讨论字面量类型如何与其他现有类型交互。
结构化数据的智能索引
字面量可用于“智能索引”结构化类型,如元组、具名元组和类。(注意:这不是一个详尽的列表)。
例如,当使用对应于有效索引的整数键索引元组时,类型检查器应推断出正确的值类型
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 的交互。
与重载的交互
字面量类型和重载不需要以特殊方式交互:现有规则运行良好。
然而,类型检查器必须支持的一个重要用例是当用户不使用字面量类型时能够使用 回退。例如,考虑 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] 这样的类型,例如与泛型一起使用。
这意味着使用字面量类型参数化泛型函数或类是合法的
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]]'
同样,构建涉及字面量类型的带值限制或边界的 TypeVar 也是合法的
T = TypeVar('T', Literal["a"], Literal["b"], Literal["c"])
S = TypeVar('S', bound=Literal["foo"])
……尽管尚不清楚何时构建一个具有字面量上限的 TypeVar 会有用。例如,上述示例中的 S TypeVar 基本上毫无意义:我们可以通过使用 S = Literal["foo"] 来获得等效行为。
注意: 字面量类型和泛型故意只以非常基本和有限的方式交互。特别是,想要对包含大量数字或 numpy 风格操作的代码进行类型检查的库,几乎肯定会发现本 PEP 中提议的字面量类型不足以满足其需求。
我们考虑了几种不同的解决方案,但最终决定将整数泛型的问题推迟到以后。有关更多详细信息,请参阅 被拒绝或超出范围的想法。
与枚举和穷尽性检查的交互
当处理具有有限数量变体的字面量类型(如枚举)时,类型检查器应该能够执行穷尽性检查。例如,类型检查器应该能够推断出最终的 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)由于预期的实现复杂性尚未实现这一点。
一旦引入字面量类型,其中一些复杂性将得到缓解:我们可以将枚举视为大致等同于其值的联合,而不是完全特殊处理枚举,并利用类型检查器可能已经实现的关于联合、穷尽性、类型窄化、可达性等的任何现有逻辑。
因此,在这里,Status 枚举可以被视为大致等效于 Literal[Status.SUCCESS, Status.INVALID_DATA, Status.FATAL_ERROR],并相应地窄化 s 的类型。
与窄化的交互
类型检查器可以选择对枚举和非枚举字面量类型执行超出上述部分描述的额外分析。
例如,基于包含或相等性检查等进行窄化可能很有用
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[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 限定符用作声明变量 实际上是字面量 的简写。
如果本 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 类型检查器目前已实现本规范中描述行为的很大一部分,但枚举字面量和上面描述的一些更复杂的窄化交互除外。
致谢
感谢 Mark Mendoza、Ran Benita、Rebecca Chen 和 typing-sig 的其他成员对本 PEP 的评论。
特别感谢 mypy 和 typing 问题跟踪器的各位参与者,他们为本 PEP 提供了许多动机和理由。
版权
本文档已置于公共领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0586.rst