PEP 591 – 为类型提示添加 final 限定符
- 作者:
- Michael J. Sullivan <sully at msully.net>, Ivan Levkivskyi <levkivskyi at gmail.com>
- BDFL 委托:
- Guido van Rossum <guido at python.org>
- 讨论至:
- Typing-SIG 邮件列表
- 状态:
- 最终版
- 类型:
- 标准跟踪
- 主题:
- 类型标注
- 创建日期:
- 2019年3月15日
- Python 版本:
- 3.8
- 发布历史:
- 决议:
- Typing-SIG 消息
摘要
本 PEP 提议在 typing 模块中添加一个“final”限定符——以 final 装饰器和 Final 类型注解的形式——用于以下三个相关目的:
- 声明一个方法不应被覆盖
- 声明一个类不应被继承
- 声明一个变量或属性不应被重新赋值
动机
final 装饰器
当前的 typing 模块缺少在类型检查器层面限制继承或覆盖的方法。这是其他面向对象语言(如 Java)中的一个常见特性,对于减少类的潜在行为空间、简化推理很有用。
最终类或方法可能有用的一些情况包括:
- 一个类未被设计为可继承,或者一个方法未被设计为可覆盖。也许它不会按预期工作,或者容易出错。
- 子类化或覆盖会使代码更难理解或维护。例如,您可能希望防止基类和子类之间不必要的紧密耦合。
- 您希望保留将来随意更改类实现的自由,而这些更改可能会破坏子类。
Final 注解
当前的 typing 模块缺少一种方法来指示变量不会被赋值。这在以下几种情况下是一个有用的特性:
- 防止对模块和类级别常量的意外修改,并以可检查的方式将其记录为常量。
- 创建一个只读属性,该属性可能不会被子类覆盖。(
@property可以使属性只读,但不能阻止覆盖) - 允许在通常期望字面值的情况下使用名称(例如,作为
NamedTuple的字段名、传递给isinstance的类型元组,或作为具有Literal类型参数的函数的参数 (PEP 586))。
规范
final 装饰器
typing.final 装饰器用于限制继承和覆盖的使用。
类型检查器应禁止任何用 @final 装饰的类被继承,并禁止任何用 @final 装饰的方法在子类中被覆盖。方法装饰器版本可用于所有实例方法、类方法、静态方法和属性。
例如
from typing import final
@final
class Base:
...
class Derived(Base): # Error: Cannot inherit from final class "Base"
...
和
from typing import final
class Base:
@final
def foo(self) -> None:
...
class Derived(Base):
def foo(self) -> None: # Error: Cannot override final attribute "foo"
# (previously declared in base class "Base")
...
对于重载方法,@final 应该放在实现上(或对于存根文件,放在第一个重载上)
from typing import Any, overload
class Base:
@overload
def method(self) -> None: ...
@overload
def method(self, arg: int) -> int: ...
@final
def method(self, x=None):
...
在非方法函数上使用 @final 是错误的。
Final 注解
typing.Final 类型限定符用于指示变量或属性不应被重新赋值、重新定义或覆盖。
语法
Final 可以以下几种形式之一使用:
- 使用显式类型,语法为
Final[<type>]。例如:ID: Final[float] = 1
- 没有类型注解。例如:
ID: Final = 1
类型检查器应应用其通常的类型推断机制来确定
ID的类型(在此处,很可能是int)。请注意,与泛型类不同,这 不 等同于Final[Any]。 - 在类主体和存根文件中,您可以省略右侧,只写
ID: Final[float]。如果省略右侧,则Final必须有一个显式类型参数。 - 最后,如
self.id: Final = 1(也可以在方括号中可选地带类型)。这 只 允许在__init__方法中使用,以便最终实例属性在实例创建时只赋值一次。
语义和示例
定义最终名称的两个主要规则是:
- 对于给定属性,每个模块或类中 最多只能有一个 最终声明。不能有同名的独立类级别和实例级别常量。
- 对最终名称必须有 恰好一个 赋值。
这意味着类型检查器应阻止在类型检查代码中对最终名称进行进一步赋值。
from typing import Final
RATE: Final = 3000
class Base:
DEFAULT_ID: Final = 0
RATE = 300 # Error: can't assign to final attribute
Base.DEFAULT_ID = 1 # Error: can't override a final attribute
请注意,类型检查器不需要允许循环内的 Final 声明,因为运行时会在后续迭代中看到对同一变量的多次赋值。
此外,类型检查器应阻止最终属性在子类中被覆盖。
from typing import Final
class Window:
BORDER_WIDTH: Final = 2.5
...
class ListView(Window):
BORDER_WIDTH = 3 # Error: can't override a final attribute
在类主体中声明但未初始化的最终属性必须在 __init__ 方法中初始化(存根文件除外)。
class ImmutablePoint:
x: Final[int]
y: Final[int] # Error: final attribute without an initializer
def __init__(self) -> None:
self.x = 1 # Good
类型检查器应将类主体中初始化的最终属性推断为类变量。变量不应同时用 ClassVar 和 Final 注解。
Final 只能作为赋值或变量注解中的最外层类型使用。在任何其他位置使用都是错误的。特别是,Final 不能用于函数参数的注解。
x: List[Final[int]] = [] # Error!
def fun(x: Final[List[int]]) -> None: # Error!
...
请注意,将名称声明为 final 只保证该名称不会被重新绑定到另一个值,但不会使值不可变。不可变抽象基类和容器可以与 Final 结合使用,以防止修改这些值。
x: Final = ['a', 'b']
x.append('c') # OK
y: Final[Sequence[str]] = ['a', 'b']
y.append('x') # Error: "Sequence[str]" has no attribute "append"
z: Final = ('a', 'b') # Also works
类型检查器应将使用字面值初始化的最终名称视为已被字面值替换。例如,以下情况应被允许:
from typing import NamedTuple, Final
X: Final = "x"
Y: Final = "y"
N = NamedTuple("N", [(X, int), (Y, int)])
参考实现
mypy [1] 类型检查器支持 Final 和 final。typing_extensions [2] 模块中提供了运行时组件的参考实现。
已拒绝/推迟的想法
曾考虑将 Const 作为 Final 类型注解的名称。最终选择了 Final,因为这些概念是相关的,并且保持一致性似乎是最好的。
我们曾考虑使用单一名称 Final 而不是同时引入 final,但是 @Final 对我们来说看起来太奇怪了。
与最终类相关的一个特性是 Scala 风格的密封类,其中类只允许由在同一模块中定义的类继承。密封类似乎与模式匹配结合使用时最有用,因此在我们看来,它不值得增加复杂性。这将来可能会重新考虑。
在类上使用 @final 装饰器可以在运行时动态阻止子类化。然而,typing 中的其他任何内容都不进行运行时强制执行,所以 final 也不会。如果需要运行时强制执行和静态检查,可以使用以下惯用法(可能在一个支持模块中):
if typing.TYPE_CHECKING:
from typing import final
else:
from runtime_final import final
参考资料
版权
本文档已置于公共领域。
来源: https://github.com/python/peps/blob/main/peps/pep-0591.rst
最后修改: 2024-06-11 22:12:09 GMT