Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

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 是一个历史文档:请参阅 @final/@typing.finalFinal/typing.Final 以获取最新的规范和文档。规范的类型规范维护在 类型规范站点;运行时类型行为在 CPython 文档中进行了描述。

×

请参阅 类型规范更新流程,了解如何提出对类型规范的更改。

摘要

此 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

类型检查器应推断在类体中初始化的最终属性为类变量。变量不应同时使用 ClassVarFinal 注解。

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] 类型检查器支持 Finalfinal。运行时组件的参考实现在 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