如果类已经被导入用于类型提示,那么“from __future__ import annotations”的好处是什么?

7
导入 from __future__ import annotations 有什么好处?如果我理解正确的话,我应该停止在运行时不必要地输入导入。
在我的示例中,HelloWorld 仅用于类型。但是使用此代码时,输出始终为:
应该发生这种情况吗? x = World!
当我删除 from hello import HelloWorld 时,PyCharm 中的类型帮助将不再起作用(我可以理解这一点,因为它不知道 HelloWorld 是从哪里来的)。
from __future__ import annotations

from hello import HelloWorld

if __name__ == '__main__':
    def hello(x: str, hello: HelloWorld = None):
        if hello is not None:
            print('hello.data_01 =', hello.data_01)
        print('x =', x)


    hello('World!')

hello.py

from dataclasses import dataclass


@dataclass
class HelloWorld:
    data_01: str
    data_02: str


print("Should this happen?")

所以我的问题是,如果我仍然需要执行 from hello import HelloWorld,那么从 from __future__ import annotations 中我可以得到什么好处?
1个回答

17
from __future__ import annotations的导入具有一个核心优势:它使得使用前向引用更加简洁。例如,考虑下面这个(当前破损的)程序。
# Error! MyClass has not been defined yet in the global scope
def foo(x: MyClass) -> None:
    pass

class MyClass:
    # Error! MyClass is not defined yet, in the class scope
    def return_copy(self) -> MyClass:
        pass

当您在运行时尝试运行此程序时,它实际上会崩溃:您尝试在'MyClass'实际定义之前使用它。为了在此之前修复此问题,您必须使用类型注释语法或将每个'MyClass'包装在字符串中以创建前向引用

def foo(x: "MyClass") -> None:
    pass

class MyClass:
    def return_copy(self) -> "MyClass":
        pass

虽然这样做可以工作,但感觉很不稳定。类型应该是类型:我们不应该需要手动将某些类型转换为字符串,只是为了让类型与Python运行时良好地配合。
我们可以通过包含“from __future__ import annotations”导入来解决这个问题:该行代码在运行时自动将所有类型转换为字符串。这使我们能够编写类似第一个示例的代码而不会崩溃:由于每个类型提示实际上在运行时都是一个字符串,因此我们不再引用尚不存在的内容。
而像mypy或Pycharm这样的类型检查器则不会关心:对它们来说,无论Python本身选择如何表示类型,类型看起来都是相同的。
需要注意的是,这个导入本身并不能让我们避免导入东西,只是使得导入更加清晰。例如,请考虑以下内容:
from expensive_to_import_module import MyType

def blah(x: MyType) -> None: ...

如果expensive_to_import_module在启动时进行了大量逻辑处理,那么导入MyType可能需要花费相当长的时间。一旦程序实际运行,这并不会有太大影响,但它确实会使启动时间变慢。如果您尝试编写短暂的命令行程序,则会感到特别糟糕:添加类型提示的操作有时会使程序在启动时感觉更加缓慢。
我们可以通过将MyType设置为字符串,并将其隐藏在if TYPE_CHECKING保护之后来解决这个问题,例如:
from typing import TYPE_CHECKING

# TYPE_CHECKING is False at runtime, but treated as True by type checkers.
# So the Python interpreters won't do the expensive import, but the type checker
# will still understand where MyType came from!
if TYPE_CHECKING:
    from expensive_to_import_module import MyType

# This is a string reference, so we don't attempt to actually use MyType
# at runtime.
def blah(x: "MyType") -> None: ...

这样做是可行的,但看起来有些笨重。为什么我们需要在最后一个类型周围添加引号呢?annotations 未来导入使得执行此操作的语法更加流畅:

from __future__ import annotations
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from expensive_to_import_module import MyType

# Hooray, no more quotes!
def blah(x: MyType) -> None: ...

根据不同的观点,这可能看起来并不是一个巨大的胜利。但它确实有助于使使用类型提示更加人性化,使它们感觉更加“整合”到Python中,并且(一旦默认启用这些)消除了新手在PEP 484上经常遇到的常见障碍。


感谢您的出色回答。有一个问题,我们知道这种行为何时会成为默认行为,以及何时不再需要“from future import annotations”吗?似乎应该是3.10版本,但我正在运行该版本,仍然需要导入才能摆脱引号。 - Mathieu

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接