【问题标题】:Type hinting, union with forward references类型提示,与前向引用联合
【发布时间】:2016-06-17 01:14:02
【问题描述】:

现在可以在 python 3 中使用类型提示了。

在我的小脚本中,我希望使用类型提示。但是,特定变量可以有两种类型。 np.ndarray(表示位置)或CelestialBody(从中获取位置)。 该函数位于CelestialOrbit 类中。

现在 CelestialBody 对象没有在类之前定义,所以我使用前向引用,如 pep 所述

from math import pi
import numpy as np
import math as m
from numpy import cos, sin, sqrt, power, square, arctan2, arccos, arcsin, arcsinh, radians, degrees
from scipy.optimize import *
import scipy as sp
import celestial_body as CB
import typing 

#....

def get_total_max_distance(self, ancestor_body: "CB.CelestialBody", eps=3*np.finfo(float).eps):
    if self.parent == ancestor_body:
        return self.apoapsis_distance
    orbit_list = list(self.create_tree_branch(ancestor_body))
    orbit_list.reverse()
    return orbit_list[0]._get_total_max_distance(ancestor_body.getGlobalPositionAtTime(),
                                                 ancestor_body.getGlobalPositionAtTime(),
                                                 orbit_list[1:], eps)

这很好用。 Pycharm 理解类型并且看起来是正确的?现在我想改变它,让它明白它可以同时采用 CB.CelestialBodynp.ndarray 类型。 (已经声明了第二个)。我尝试根据pep使用联合:

def get_total_max_distance(self, ancestor_body: typing.Union["CB.CelestialBody",np.ndarray], eps=3*np.finfo(float).eps):

但是这会失败并显示以下备注:“AttributeError: 'module' object has no attribute 'CelestialBody'”

完整的追溯:

Traceback (most recent call last):
  File "C:/Users/Paul/PycharmProjects/KSP_helper/main.py", line 5, in <module>
    from celestial_body import *
  File "C:\Users\Paul\PycharmProjects\KSP_helper\celestial_body.py", line 7, in <module>
    import celestial_orbit as CO
  File "C:\Users\Paul\PycharmProjects\KSP_helper\celestial_orbit.py", line 123, in <module>
    class CelestialOrbit:
  File "C:\Users\Paul\PycharmProjects\KSP_helper\celestial_orbit.py", line 579, in CelestialOrbit
    def get_total_max_distance(self, ancestor_body: typing.Union["CB.CelestialBody",np.ndarray], eps=3*np.finfo(float).eps):
  File "C:\Python34\lib\site-packages\typing.py", line 537, in __getitem__
    dict(self.__dict__), parameters, _root=True)
  File "C:\Python34\lib\site-packages\typing.py", line 494, in __new__
    for t2 in all_params - {t1} if not isinstance(t2, TypeVar)):
  File "C:\Python34\lib\site-packages\typing.py", line 494, in <genexpr>
    for t2 in all_params - {t1} if not isinstance(t2, TypeVar)):
  File "C:\Python34\lib\site-packages\typing.py", line 185, in __subclasscheck__
    self._eval_type(globalns, localns)
  File "C:\Python34\lib\site-packages\typing.py", line 172, in _eval_type
    eval(self.__forward_code__, globalns, localns),
  File "<string>", line 1, in <module>
AttributeError: 'module' object has no attribute 'CelestialBody'

我该怎么做?


按照 Kevin 的建议,正如预期的那样,错误会更容易一些(因为 celestial_orbit 模块是由 celestial_body 模块导入的,因此当 python 尝试实例化 CelestialOrbit 类时,CelestialBody 类还没有被实例化)。
C:\Python35\python.exe C:/Users/Paul/PycharmProjects/KSP_helper/main.py
Traceback (most recent call last):
  File "C:/Users/Paul/PycharmProjects/KSP_helper/main.py", line 5, in <module>
    from celestial_body import *
  File "C:\Users\Paul\PycharmProjects\KSP_helper\celestial_body.py", line 7, in <module>
    import celestial_orbit as CO
  File "C:\Users\Paul\PycharmProjects\KSP_helper\celestial_orbit.py", line 125, in <module>
    class CelestialOrbit:
  File "C:\Users\Paul\PycharmProjects\KSP_helper\celestial_orbit.py", line 581, in CelestialOrbit
    def get_total_max_distance(self, ancestor_body: typing.Union[CB.CelestialBody, np.ndarray], eps=3*np.finfo(float).eps):
AttributeError: module 'celestial_body' has no attribute 'CelestialBody'

Bakuriu 的建议 - 将 CB.CelestialBody 更改为 celestial_body.CelestialBody 似乎有效。然而,这对我来说非常不合逻辑 - 特别是因为非联合版本与 CB 别名一起使用。

【问题讨论】:

  • CB.CelestialBody 更改为celestial_body.CelestialBody 可以解决问题吗?我最初将此作为答案发布,但检查 PEP PyCharm 实际上应该能够解析CB.CelestialBody。可能是 PyCharm 没有正确实现 PEP,在这种情况下这是一个答案,您应该在 PyCharm 的错误跟踪器中打开一张票。
  • 无论如何,这个错误看起来像一个真正的错误。我的意思是:上面写着module object has no attribute,这意味着PyCharm 确实在评估CB 作为一个模块并寻找CelestialBody。这听起来非常可疑。您能否提供一个最小的完整代码,我们可以使用它来重现错误?只需将这些模块复制到某处并开始删除不必要的代码(例如,方法的所有实现都可以更改为pass 或完全删除),直到找到触发此错误的最小代码。
  • 这种变化(CB.CelesticalBodycelestial_body.CelestialBody)似乎解决了这个问题。但是python.exe报错。从 shell 调用 python main.py 会导致同样的错误。
  • 我想知道如果你这样做"typing.Union[CB.CelestialBody,np.ndarray]"会发生什么。免责声明:我从未使用过类型提示。
  • @Bakuriu:您的建议有效的原因是因为celestial_body 引发了NameError 异常。这实际上并没有使类型定义正确;该名称将从不在其使用的上下文中起作用。如果 PyCharm 接受这一点,那么它就超出了规范。

标签: python python-3.5 type-hinting


【解决方案1】:

注意:此错误最近已修复,将成为 Python 3.5.3 的一部分。与该版本相比,以下答案已过时。


前向引用无法解析,因为您的CB 模块引用存在,但没有CelestialBody 属性,因此引发AttributeError 异常。前向引用解析(由Union 类型间接触发)只允许NameError 异常;据说是因为这是确定名称是否(尚不)可用的规范方法。

但鉴于 PEP 为您提供了一个 example,其中前向引用用于解决两个模块之间的循环依赖关系(因此您希望 AttributeErrors 在过早引用该名称时发生),我实际上,您尝试的方法不起作用,这有点令人惊讶。你几乎肯定发现了一个错误。

发生的情况是Union[...] 类型检查联合中的元素是否是联合中另一种类型的子类,并且正是该检查触发了查找前向引用的尝试。如果'a.A''b.B'(如在循环引用示例中)工作,前向引用检查应接受AttributeError 作为此处处理的有效异常。事实上,任何异常都应该在此时被吞下,因为正如 PEP 所述:

字符串文字应该包含一个有效的 Python 表达式 [...],并且它应该在模块完全加载后正确评估

强调我的。当 Union[..] 对象被创建时,模块还没有完全加载,因此如果允许任何有效的 Python 表达式,代码应该any 异常视为转发的指示参考还没有准备好并忽略它。

解决方法是让您创建将AttributeError 转换为NameError 的函数:

def _CelestialBody_forward_ref():
    try:
        return CB.CelestialBody
    except AttributeError:
        # not yet, raise NameError instead
        raise NameError('CelestialBody')

然后在您的前向引用中使用它:

typing.Union['_CelestialBody_forward_ref()', np.ndarray]

这是可行的,因为前向引用可以是任何有效的 Python 表达式。或者您可以将整个 Union 声明设为字符串;稍后将在所有导入完成后对其进行评估:

def get_total_max_distance(self, ancestor_body: "typing.Union[CB.CelestialBody,np.ndarray]", eps=3*np.finfo(float).eps):

我已经filed this as a bug 参与了 Python 项目。


至于为什么Bakuriu建议将表达式改为celestial_body.CelestialBody;这只“有效”,因为名称 celestial_body 引发了 NameError 异常。该名称将永远在您的代码上下文中起作用,因此前向表达式不符合 PEP(一旦模块被满载)。

如果 PyCharm 无论如何都接受该引用并正确地对函数进行类型检查(例如,它只会让您在编写调用时使用 numpy ndarrayCelestialObject 实例),那么这是由于 PyCharm 超出了此处的规范.其他工具可能不会那么宽容。

换句话说,就typing 而言,您还不如在那里使用frobnar.FlubberdyFlub 作为前向引用,它会以同样的效果抑制这个特定的错误;无效的前向引用。

【讨论】:

  • 感谢您的详细回复。通过更改错误类型来使用解决方法是可行的,但是它也可能会开始混淆 IDE。 (和我自己)。它不是通过类型提示来降低复杂性,而是增加了更多的复杂性。我想我会选择“整个联合字符串”——至少让代码易于阅读。
  • @paul23:是的,解决方法实际上只是一个技术演示,以表明提高NameError 确实有效。
【解决方案2】:

这种行为的原因是CB.CelestialBody 没有抛出NameError,因为全局范围包含名称CB,而是抛出AttributeError,因为模块没有属性CelestialBody

typing.py 中的__subclasscheck__

def __subclasscheck__(self, cls):
    if not self.__forward_evaluated__:
        globalns = self.__forward_frame__.f_globals
        localns = self.__forward_frame__.f_locals
        try:
            self._eval_type(globalns, localns)
        except NameError:
            return False  # Too early.
    return issubclass(cls, self.__forward_value__)

Union.__new__ 尝试在实例化时尽早评估类型。但是,typing 不希望抛出 AttributeError。事实上,在咨询PEP 0484 之后,似乎这是一个实际提议的用于解决循环导入问题的用例;但是Union没有正确处理实际操作。

无论如何,如果您在该模块中实际上不需要celestial_body,除了类型推断,您可能根本不需要导入该模块,或者在文件末尾导入它,例如

def get_total_max_distance(self, ancestor_body: "CB.CelestialBody", eps=3*np.finfo(float).eps):
    ...

import celestial_body as CB

Union 版本没有任何问题 - 那是因为自己的函数注释对Python没有任何意义。 typing 中的类本身确实关心提供给他们的内容。

另一方面,PyCharm 使用独立于 typing 模块的类型推断。

【讨论】:

    猜你喜欢
    • 2016-07-04
    • 1970-01-01
    • 2019-08-09
    • 1970-01-01
    • 1970-01-01
    • 2018-05-26
    • 2022-01-09
    • 2017-07-28
    • 2021-11-11
    相关资源
    最近更新 更多