mypy如何使用typing.TYPE_CHECKING解决循环导入注解问题?

How does mypy use typing.TYPE_CHECKING to resolve the circular import annotation problem?

我的包结构如下:

/prog
-- /ui
---- /menus
------ __init__.py
------ main_menu.py
------ file_menu.py
-- __init__.py
__init__.py
prog.py

这些是我的 import/classes 声明:

prog.py:

from prog.ui.menus import MainMenu

/prog/ui/menus/__init__.py:

from prog.ui.menus.file_menu import FileMenu
from prog.ui.menus.main_menu import MainMenu

main_menu.py:

import tkinter as tk
from prog.ui.menus import FileMenu

class MainMenu(tk.Menu):
    
    def __init__(self, master: tk.Tk, **kwargs):
        super().__init__(master, **kwargs)
        self.add_cascade(label='File', menu=FileMenu(self, tearoff=False))

    [...]

file_menu.py:

import tkinter as tk
from prog.ui.menus import MainMenu

class FileMenu(tk.Menu):

    def __init__(self, master: MainMenu, **kwargs):
        super().__init__(master, **kwargs)
        self.add_command(label='Settings')

    [...]

这会导致序列中的循环导入问题:

prog.py -> __init__.py -> main_menu.py -> file_menu.py -> main_menu.py -> [...]

根据多次搜索,建议将导入更新为:

file_menu.py

import tkinter as tk
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from prog.ui.menus import MainMenu

class FileMenu(tk.Menu):

    def __init__(self, master: 'MainMenu', **kwargs):
        super().__init__(master, **kwargs)
        self.add_command(label='Settings')

    [...]

我已经阅读了 TYPE_CHECKING docs and the mypy docs 的用法,但我不了解如何使用此条件解决循环。是的,在运行时它可以工作,因为它的计算结果为 False 所以这是一个“操作分辨率”,但是它在类型检查期间如何不重新出现:

The TYPE_CHECKING constant defined by the typing module is False at runtime but True while type checking.

我对 mypy 了解不多,因此我看不出一旦条件评估为 True 问题就不会再次出现。 “运行时”和“类型检查”之间有什么不同? “类型检查”的过程是否意味着代码没有被执行?

备注:

Does the process of "type checking" mean code is not executed?

是的,没错。类型检查器从不执行您的代码:相反,它 分析 它。类型检查器的实现方式与编译器的实现方式几乎相同,只是减去 "generate bytecode/assembly/machine code" 步骤。

这意味着您的类型检查器比 Python 解释器在 运行 时间内有更多的策略可用于解决导入循环(或任何类型的循环),因为它不需要尝试盲目导入模块。

例如,mypy 所做的基本上是逐个模块分析您的代码,跟踪正在定义的每个新 class/new 类型。在此过程中,如果 mypy 发现类型提示使用尚未定义的类型,请将其替换为占位符类型。

检查完所有模块后,检查是否仍有任何占位符类型浮动。如果是这样,请尝试使用我们目前收集的类型定义重新分析代码,并尽可能替换所有占位符。我们冲洗并重复,直到没有更多的占位符或我们迭代了太多次。

在那之后,mypy 假定所有剩余的占位符都是无效类型并报告错误。


相比之下,Python 解释器没有像这样重复重新分析模块的奢侈。它需要 运行 它看到的每个模块,反复重新 运行 模块可能会打破一些用户 code/user 的期望。

同样,Python 解释器没有能够交换我们分析模块的顺序的奢侈。相比之下,mypy 理论上可以以任意顺序分析您的模块,而忽略导入的内容——唯一的问题是它的效率非常低,因为我们需要大量迭代才能达到定点。

(因此,mypy 使用您的导入作为 建议 来决定以何种顺序分析模块。例如,如果模块 A 直接导入模块 B,我们可能要分析B 先。但是如果 A 在 if TYPE_CHECKING 之后导入 B,那么放宽顺序可能会帮助我们打破循环。)