Python 是否评估前向引用的类型提示?
Does Python evaluate type hinting of a forward reference?
我正在查看 Forward References 上的 PEP 484 部分并注意到声明:
...that definition may be expressed as a string literal, to be resolved later.
这让我想知道,"later" 是什么时候,什么时候?解释器稍后不会尝试将其解析为文字,那又如何呢?是否只是编写了第三方工具来执行此操作?
演示解释器结果的小例子:
class A:
def test(self, a: 'A') -> None:
pass
class B:
def test(self, a: A) -> None:
pass
>>> A().test.__annotations__
{'a': 'A', 'return': None}
>>> B().test.__annotations__
{'a': <class '__main__.A'>, 'return': None}
如果我对函数注释和类型提示的理解是正确的,那么 Python 并没有在运行时对它们做 任何事情 来提高性能,而是内省use 允许 strictly 第三方应用程序,例如 linters、IDE 和静态分析工具(例如 mypy
)利用它们的可用性。那么这些工具会尝试解析 'A'
的类型提示,而不是将其作为交给解释器的工作吗?如果是这样,它们是如何完成的?
更新:
通过使用 typing
模块,用户代码可以执行以下操作:
>>> typing.get_type_hints(A().test)
{'a': <class '__main__.A'>, 'return': <class 'NoneType'>}
>>> typing.get_type_hints(B().test)
{'a': <class '__main__.A'>, 'return': <class 'NoneType'>}
但是,我的问题是针对 Python 是否有责任从字符串文字更新函数的 __annotations__
,也就是说在运行时更改:
>>> A().test.__annotations__
{'a': 'A', 'return': None}
到...
>>> A().test.__annotations__
{'a': <class '__main__.A'>, 'return': None}
如果 Python 不这样做,那么我为什么要将字符串文字作为类型提示而不是用于自记录代码?第一种形式对我、用户或第三方工具有什么价值?
考虑以下代码:
class Foo:
def bar(self) -> Foo:
return Foo()
如果你用 Python 尝试 运行 这个程序实际上会在运行时崩溃:当解释器看到 bar
的定义时,Foo
的定义是还没有完成。因此,由于 Foo
尚未添加到全局命名空间,我们还不能将其用作类型提示。
同样,考虑这个程序:
class Foo:
def bar(self) -> Bar:
return Bar()
class Bar:
def foo(self) -> Foo:
return Foo()
这个相互依赖的定义遇到了同样的问题:当我们正在评估 Foo
时,Bar
还没有被评估,所以解释器抛出一个异常。
这个问题有三种解决方法。首先是制作一些类型提示字符串,有效地 "forward declaring" 它们:
class Foo:
def bar(self) -> "Foo":
return Foo()
这满足 Python 解释器的要求,并且不会破坏像 mypy 这样的第三方工具:它们可以在解析类型之前删除引号。主要缺点是这种语法看起来有点丑陋和笨拙。
第二种解决方案是使用类型注释语法:
class Foo:
def bar(self):
# type: () -> Foo
return Foo()
这与第一个解决方案具有相同的优点和缺点:它满足解释器和工具的要求,但看起来很笨拙和丑陋。它还具有额外的好处,它使您的代码向后兼容 Python 2.7.
第三个解决方案仅适用于 Python 3.7+——使用 from __future__ import annotations
指令:
from __future__ import annotations
class Foo:
def bar(self) -> Foo:
return Foo()
这会自动将所有注释表示为字符串。所以我们得到了第一个解决方案的好处,但没有丑陋。
此行为最终将成为 Python 未来版本的默认行为。
事实证明,自动生成所有注释字符串可以带来一些性能改进。构造像 List[Dict[str, int]]
这样的类型可能会非常昂贵:它们只是运行时的正则表达式并且被评估为就好像它们被写成 List.__getitem__(Dict.__getitem__((str, int))
.
计算这个表达式有些昂贵:我们最终执行了两次方法调用,构造了一个元组,并构造了两个对象。当然,这还没有计算 __getitem__
方法本身发生的任何额外工作——而且这些方法中发生的工作最终出于必要而变得非常重要。
(简而言之,他们需要构建特殊对象,以确保像 List[int]
这样的类型不会在运行时以不当方式使用——例如在 isinstance
检查等中。)
我正在查看 Forward References 上的 PEP 484 部分并注意到声明:
...that definition may be expressed as a string literal, to be resolved later.
这让我想知道,"later" 是什么时候,什么时候?解释器稍后不会尝试将其解析为文字,那又如何呢?是否只是编写了第三方工具来执行此操作?
演示解释器结果的小例子:
class A:
def test(self, a: 'A') -> None:
pass
class B:
def test(self, a: A) -> None:
pass
>>> A().test.__annotations__
{'a': 'A', 'return': None}
>>> B().test.__annotations__
{'a': <class '__main__.A'>, 'return': None}
如果我对函数注释和类型提示的理解是正确的,那么 Python 并没有在运行时对它们做 任何事情 来提高性能,而是内省use 允许 strictly 第三方应用程序,例如 linters、IDE 和静态分析工具(例如 mypy
)利用它们的可用性。那么这些工具会尝试解析 'A'
的类型提示,而不是将其作为交给解释器的工作吗?如果是这样,它们是如何完成的?
更新:
通过使用 typing
模块,用户代码可以执行以下操作:
>>> typing.get_type_hints(A().test)
{'a': <class '__main__.A'>, 'return': <class 'NoneType'>}
>>> typing.get_type_hints(B().test)
{'a': <class '__main__.A'>, 'return': <class 'NoneType'>}
但是,我的问题是针对 Python 是否有责任从字符串文字更新函数的 __annotations__
,也就是说在运行时更改:
>>> A().test.__annotations__
{'a': 'A', 'return': None}
到...
>>> A().test.__annotations__
{'a': <class '__main__.A'>, 'return': None}
如果 Python 不这样做,那么我为什么要将字符串文字作为类型提示而不是用于自记录代码?第一种形式对我、用户或第三方工具有什么价值?
考虑以下代码:
class Foo:
def bar(self) -> Foo:
return Foo()
如果你用 Python 尝试 运行 这个程序实际上会在运行时崩溃:当解释器看到 bar
的定义时,Foo
的定义是还没有完成。因此,由于 Foo
尚未添加到全局命名空间,我们还不能将其用作类型提示。
同样,考虑这个程序:
class Foo:
def bar(self) -> Bar:
return Bar()
class Bar:
def foo(self) -> Foo:
return Foo()
这个相互依赖的定义遇到了同样的问题:当我们正在评估 Foo
时,Bar
还没有被评估,所以解释器抛出一个异常。
这个问题有三种解决方法。首先是制作一些类型提示字符串,有效地 "forward declaring" 它们:
class Foo:
def bar(self) -> "Foo":
return Foo()
这满足 Python 解释器的要求,并且不会破坏像 mypy 这样的第三方工具:它们可以在解析类型之前删除引号。主要缺点是这种语法看起来有点丑陋和笨拙。
第二种解决方案是使用类型注释语法:
class Foo:
def bar(self):
# type: () -> Foo
return Foo()
这与第一个解决方案具有相同的优点和缺点:它满足解释器和工具的要求,但看起来很笨拙和丑陋。它还具有额外的好处,它使您的代码向后兼容 Python 2.7.
第三个解决方案仅适用于 Python 3.7+——使用 from __future__ import annotations
指令:
from __future__ import annotations
class Foo:
def bar(self) -> Foo:
return Foo()
这会自动将所有注释表示为字符串。所以我们得到了第一个解决方案的好处,但没有丑陋。
此行为最终将成为 Python 未来版本的默认行为。
事实证明,自动生成所有注释字符串可以带来一些性能改进。构造像 List[Dict[str, int]]
这样的类型可能会非常昂贵:它们只是运行时的正则表达式并且被评估为就好像它们被写成 List.__getitem__(Dict.__getitem__((str, int))
.
计算这个表达式有些昂贵:我们最终执行了两次方法调用,构造了一个元组,并构造了两个对象。当然,这还没有计算 __getitem__
方法本身发生的任何额外工作——而且这些方法中发生的工作最终出于必要而变得非常重要。
(简而言之,他们需要构建特殊对象,以确保像 List[int]
这样的类型不会在运行时以不当方式使用——例如在 isinstance
检查等中。)