Python是否评估前向引用的类型提示?

时间:2019-03-24 02:28:59

标签: python type-hinting pep

我正在查看Forward References上的PEP 484部分,并注意到以下声明:

  

...该定义可以表示为字符串文字,稍后再解决。

这让我想知道,“以后”什么时候出现?解释器以后不会尝试将其解析为文字,那么怎么办?仅仅是编写了第三方工具可以做到这一点吗?

演示解释器结果的小例子:

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并不会在运行时真正对它们进行任何操作来提高性能,但是内省性使用允许严格 >第三方应用程序,例如linter,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不这么做,那为什么我要用字符串文字作为类型提示而不是自记录代码?第一种形式给用户,用户或第三方工具有什么价值?

1 个答案:

答案 0 :(得分:2)

考虑以下代码:

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尚未被评估,因此解释器抛出异常。


有三个解决方案。第一种是使您的某些类型提示字符串有效地“向前声明”它们:

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检查等中使用。)