在Python中使用typeguard装饰器:@typechecked,同时避免循环导入?

a.t*_*.t. 4 python circular-dependency type-hinting typeguards

语境

为了防止在使用类型提示时在 Python 中循环导入,可以使用以下构造:

# controllers.py
from __future__ import annotations
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from models import Book


class BookController:
    def __init__(self, book: "Book") -> None:
        self.book = book
Run Code Online (Sandbox Code Playgroud)

if TYPE_CHECKING:在类型检查期间执行,而不是在代码执行期间执行。

问题

当应用主动函数参数类型验证(基于参数的类型提示)时,typeguard会抛出错误:

名称错误:名称“Supported_experiment_settings”未定义

多维我

# models.py
from controllers import BookController

from typeguard import typechecked

class Book:
    
    @typechecked
    def get_controller(self, some_bookcontroller:BookController):
        return some_bookcontroller

some_book=Book()
BookController("somestring")
Run Code Online (Sandbox Code Playgroud)

和:

# controllers.py
from __future__ import annotations
from typing import TYPE_CHECKING
from typeguard import typechecked
#from models import Book

if TYPE_CHECKING:
    from models import Book

class BookController:
    
    @typechecked
    def __init__(self, book: Book) -> None:
        self.book = book
Run Code Online (Sandbox Code Playgroud)

请注意,#from models import Book已被注释掉。现在如果有人运行:

python models.py
Run Code Online (Sandbox Code Playgroud)

它抛出错误:

文件“/home/name/Documents/eg/models.py”,第 13 行,在 BookController(“somestring”) ... NameError:名称“Book”未定义。您指的是: 'bool'? 因为类型检查def __init__(self, book: Book) -> None:不知道 Book 类是什么。

微量元素II

然后,如果有人@typechecked禁用controllers.py

# controllers.py
from __future__ import annotations
from typing import TYPE_CHECKING
from typeguard import typechecked

if TYPE_CHECKING:
    from models import Book

class BookController:
    
    #@typechecked
    def __init__(self, book: Book) -> None:
        self.book = book
Run Code Online (Sandbox Code Playgroud)

有用。(但没有类型检查)。

多维韦三世

然后,如果重新启用类型检查,包括书籍的导入,(带有from models import Book),例如:

python models.py
Run Code Online (Sandbox Code Playgroud)

它抛出循环导入错误:

Traceback (most recent call last):
  File "/home/name/Documents/eg/models.py", line 2, in <module>
    from controllers import BookController
  File "/home/name/Documents/eg/controllers.py", line 5, in <module>
    from models import Book
  File "/home/name/Documents/eg/models.py", line 2, in <module>
    from controllers import BookController
ImportError: cannot import name 'BookController' from partially initialized module 'controllers' (most likely due to a circular import) (/home/name/Documents/eg/controllers.py)
Run Code Online (Sandbox Code Playgroud)

问题

如何避免这种循环导入,同时仍然允许@typechecked装饰器验证/访问Book导入?

是否有等效的TYPE_CHECKING布尔值typeguard

Sha*_*ger 6

您的问题是,通过使用 denamespacing import form ( from x import y) ,导入模块的内容无法延迟解析,因此一侧或另一侧将在另一个模块完成导入之前(因此在它定义了姓名)。

这里的典型解决方案是使用命名空间导入(import x)并限定您的使用(x.y),这样,假设不需要名称来定义您的模块,那么当调用其中的函数时,它们可以将解析推迟到需要时圆形方面不是问题。

检查源代码后,typechecked它是懒惰的,并推迟解析注释直到调用有问题的函数为止,因此应该很容易解决此问题,将代码更改为(内联注释指示重大更改):

# models.py
from __future__ import annotations  # Ensure annotations evaluated lazily here as well,
                                    # so it works regardless of which module in the cycle
                                    # is imported first
import controllers  # Namespaced import

from typeguard import typechecked

class Book:
    @typechecked
    def get_controller(self, some_bookcontroller: controllers.BookController):  # Namespace qualified annotation
        return some_bookcontroller
Run Code Online (Sandbox Code Playgroud)

和:

# controllers.py
from __future__ import annotations
from typeguard import typechecked

import models  # Namespaced import

class BookController:
    @typechecked
    def __init__(self, book: models.Book) -> None:  # Namespace qualified annotation
        self.book = book
Run Code Online (Sandbox Code Playgroud)

代码的models.py作用是:

some_book=Book()
BookController("somestring")
Run Code Online (Sandbox Code Playgroud)

应该从models具有循环依赖关系的两个模块中导入这些名称的其他模块,以使代码完全健壮。这是因为,在顶层,根据首先导入的循环定义模块(问题发生在此处models.py),controllers即使该名称是命名空间限定的(因为它将在完成定义之前尝试使用它models,并且当 importcontrollers暂停以等待models完成定义时,它尚未定义其其余内容,因此在 import in 上运行的代码models无法解析名称来自controllers)。

models如果它是在从和导入的其他模块中controllers,则这两个导入都将在运行后续代码时解析(假设它们module3本身都不导入这个假设),因此它可以以任何方式工作(使用命名空间或去命名空间导入)。


如果您好奇 Python 中的循环导入是如何工作的,可以查看在 Python 中使用相互或循环(循环)导入时会发生什么?,但简短的版本是,第一次在整个程序中导入模块时,Python 会将一个空模块对象放入缓存中sys.modules,然后运行模块内的代码以填充缓存的模块对象。第二导入模块时,包括由循环导入原始模块的模块导入该模块时,Python 仅绑定缓存的模块对象,不执行任何其他操作,即使缓存的模块对象尚未填充。

这意味着,当堆栈顶部( module B)正在进行的导入尝试导入堆栈上已经在其下方进行的模块( module A)时, moduleB从缓存中获取未完全初始化的模块(因为A已经在导入的过程)。如果定义依赖的内容依赖于在尝试导入之前未定义的B任何组件,则它不会存在于缓存(并且大部分为空)模块中。但是,只要对in的所有此类引用仅限于在定义期间未调用的函数,或者在转换为基于字符串的注释的注释中使用,并且在导入过程中没有任何尝试解析它们,那么这就可以正常工作。完成定义而不尝试使用任何元素AABAABfrom __future__ import annotationsBA,并且当A的定义恢复时,B存在一个完整的模块供其加载。

当第二个导入的循环中的模块(B在本例中)尝试使用第一个导入的模块( )的组件时,会出现问题,而该组件直到完成导入A后才定义(通常在 中很早,因此几乎没有定义任何内容))。需要立即解决,所以它会崩溃。,并使用ABAAfrom A import spamA.spamimport AA.spam在顶层使用(就像您controllers.BookController在原始代码中所做的那样)也会破坏事情。

你的用例甚至更糟糕,因为事实上,python models.py有秘密的三个模块:

  1. __main__(这是赋予顶级脚本的假名,因此代码是models.py,但它与用于导入/缓存目的的模块不同models),它导入...
  2. controllers,进而导入...
  3. models,然后循环导入controllers

这让事情变得更难看,因为尽管你认为你models先导入了,但你实际上是controllers在 before 之前导入的models,并且 in 中的代码models.py被执行了两次,一次针对脚本本身 as __main__,一次针对 import as models。有两个不相关的Book类(可能会导致类型检查问题),并且 中 的顶级代码models.py执行了两次(创建 的两个实例BookController,以及 的两个不相关但相同的定义各一个实例Book)。

解决办法是:

  1. 确保循环导入的双方都使用纯命名空间导入 ( /import A ) import B,并且既不尝试访问顶层的任何组件(包括当类位于顶层时在类定义级别),也不尝试调用任何组件来自顶层的方法/函数/构造函数将间接访问另一个组件。从技术上讲,你可以在他们两个都不那么小心的情况下创建一个有效的用例,但它会非常脆弱;无论哪个模块对另一个模块有急切的依赖,都必须首先导入,实际上,您不想假设一个模块总是导入(您可以在包含 的包结构中强制它__init__.py,但这会破坏隐式命名空间包,并且从维护者的角度来看它仍然很脆弱,即使从 API 消费者的角度来看它是安全的),所以最好在双方都谨慎行事。
  2. 切勿在循环导入场景中涉及将作为脚本(使用python modulename.py或)调用的模块。python -mmodulename理想情况下,永远不要编写在程序的同一运行中既用作脚本又用作导入模块的模块(要么它是主脚本,没有其他人导入它,要么它作为模块导入,而其他脚本作为主脚本)。如果一个模块既作为顶级脚本运行在其他地方导入,就会发生奇怪的事情。