以下是我的问题的最小示例:
[test/__init__.py]
from test.test1 import Test1
from test.test2 import Test2
[test/test1.py]
class Test1:
pass
[test/test2.py]
from test import Test1
class Test2:
pass
Run Code Online (Sandbox Code Playgroud)
模块或 __init__.py 的 Mypy 输出:
test/test2.py:1: error: Module 'test' has no attribute 'Test1'
Run Code Online (Sandbox Code Playgroud)
代码本身在 Python 2 和 Python 3 上都运行良好。
这是因为你的代码有一个导入周期——为了让 mypy 解析test/__init__.py,它需要准确地理解Test2是什么。(毕竟,如果您决定Test2稍后在该文件中使用/调用其方法之一怎么办?然后 mypy 需要知道输出是什么)。
因此,它击中了这一要点,有效地暂停,然后跳转到尝试了解test/test2.py正在做什么*。
但是在 中test/test2.py,我们遇到了完全相同的问题——我们看到了导入,并且需要跳回test/__init__.py以了解什么Test1是...但是我们还没有完成对该文件的解析!
这就是 mypy 与 Python 运行时不同的地方,仅供参考——mypy 一次只能解析整个文件,但 Python 运行时实际上会暂停执行以运行test/test2.py。这意味着当您这样做时from test import Test1,test模块有一个部分完整的符号空间,当前恰好仅包含Test1,而不是同时包含Test1和Test2,这就是您的代码在运行时工作的原因。
在这种情况下,修复方法是将导入修改为test/test2.py:
from test.test1 import Test1
Run Code Online (Sandbox Code Playgroud)
这打破了导入周期。
*这实际上并不是 mypy 所做的 - 它实际上所做的是尝试通过首先识别所有强连接组件(SCC) 来解决导入周期 - 每个 SCC 基本上都是一个导入周期。
然后,它应用一些启发式方法来确定 SCC 中文件的处理顺序,但这是一个不完美的过程,无法解决所有导入周期。
例如,就您的情况而言,无论我们是处理test还是test.test2首先,我们都会遇到问题。
-v您可以通过使用该标志(对于详细模式)重新运行 mypy 来查看 mypy 标识的 SCC 。
您可以在此处的源代码中找到有关 mypy 使用的算法的更多详细信息。有关导入循环解析算法的具体细节可以在此处找到。
(我怀疑整个评论有点过时/不完整,仅供参考——还有其他几个与 mypy 的静默导入机制及其增量模式机制相关的问题,但没有真正解释。)
有关 mypy 用于订购 SCC 的确切启发式的信息可以在此处找到。(基本上,我们用于导入某些内容的确切语法可以给出一些有关如何排序 SCC 的提示)。