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 仅保证该名称不会重新绑定到另一个值,但不会使该值不可变。不可变的 ABC 和容器可以与 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