1. 写在前面

在 Python 项目开发中,或多或少都会遇到循环 importcircular import 的问题。除了在更高层代码结构优化外,本文通过示例代码将向大家介绍避免循环导入的技巧。需要的朋友可以参考借鉴。:)

公众号: 滑翔的纸飞机

2. 避免循环 Import 的6种方法

当两个或多个模块相互依赖时,可能会发生循环导入。如果每个模块在完全加载之前尝试导入另一个模块,则可能会发生这种情况,从而导致不可预知的行为和运行时错误。若要避免这些问题,必须仔细考虑代码中的 import 语句,并使用下节中介绍的方法之一来中断导入循环。下面是一个错误语句的示例。以下任意一种方法基于该错误示例代码进行验证。

  • 示例代码:

main.py:

from father import Father

def main():
    print('Circular Import Example')
    Father('Jpzhang', 35).get_son()
    
if __name__ == '__main__':
    main()

father.py:

from son import Son

class Father:

    def __init__(self, name: str, age: int):
        self.name = name
        self.age = age

    def get_info(self):
        print(f'Name:{self.name},Age:{self.age}')

    def get_son():
        Son(name='Jinhao', age='5').info()

son.py:

class Son:

    def __init__(self, name: str, age: int):
        self.name = name
        self.age = age

    def get_info(self):
        print(f'Son Name:{self.name},Age:{self.age}')

    def get_father():
        Father(name='Jpzhang', age='50').info()
  • 抛出错误:
ImportError: cannot import name 'Father' from partially initialized module 'father' (most likely due to a circular import) 

2.1 在函数中导入模块

避免循环导入的一种方法是将模块导入函数内部,而不是在模块的顶层导入。这允许仅在需要时导入模块,而不是在首次导入模块时导入模块。例如:

father.py:

class Father:

    def __init__(self, name: str, age: int):
        self.name = name
        self.age = age

    def get_info(self):
        print(f'Name:{self.name},Age:{self.age}')

    def get_son(self):
        from son import Son
        Son(name='Jinhao', age='5').get_info()

运行main.py: 输出:Son Name:Jinhao,Age:5 ,程序正常运行。

2.2 使用 IMPORT AS

避免循环导入的另一种方法是使用“import as”语法。这允许你使用不同的名称导入模块,然后可以使用该名称在代码中引用该模块。例如:

示例修改:

son.py:

# import as 语法
import father as myfather

class Son:

    def __init__(self, name: str, age: int):
        self.name = name
        self.age = age

    def get_info(self):
        print(f'Son Name:{self.name},Age:{self.age}')

    def get_father(self):
        myfather.Father(name='Jpzhang', age='50').info()

father.py

# import as 语法
import son as myson

class Father:

    def __init__(self, name: str, age: int):
        self.name = name
        self.age = age

    def get_info(self):
        print(f'Name:{self.name},Age:{self.age}')

    def get_son(self):
        myson.Son(name='Jinhao', age='5').get_info()

运行main.py: 输出:Son Name:Jinhao,Age:5 ,程序正常运行。

2.3 将导入移动到模块的末尾

第三个选项是在定义所有其他代码后将 import 语句移动到模块的末尾。这可确保模块在由另一个模块导入之前已完成定义。例如:

father.py

class Father:

    def __init__(self, name: str, age: int):
        self.name = name
        self.age = age

    def get_info(self):
        print(f'Name:{self.name},Age:{self.age}')

    def get_son(self):
        Son(name='Jinhao', age='5').get_info()


from son import Son

运行main.py: 输出:Son Name:Jinhao,Age:5 ,程序正常运行。

2.4 使用库IMPORTLIB

转义循环导入的另一个选择是使用 importlib 库,它允许你在运行时动态导入模块。 如果你在运行时之前不确定需要导入哪个模块,这会很有用。 例如:

father.py

import importlib

class Father:

    def __init__(self, name: str, age: int):
        self.name = name
        self.age = age

    def get_info(self):
        print(f'Name:{self.name},Age:{self.age}')

    def get_son(self):
        module = importlib.import_module("son")
        module.Son(name='Jinhao', age='5').get_info()

运行main.py: 输出:Son Name:Jinhao,Age:5 ,程序正常运行。

2.5 typing.TYPE_CHECKING解决循环导入

TYPE_CHECKING 常量对于避免循环导入非常有用。可以使这些导入成为有条件的并避免在运行时循环导入。

son.py

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from father import Father


class Son:

    def __init__(self, name: str, age: int):
        self.name = name
        self.age = age

    def get_info(self):
        print(f'Son Name:{self.name},Age:{self.age}')

    def get_father(self):
        Father(name='Jpzhang', age='50').info()

运行main.py: 输出:Son Name:Jinhao,Age:5 ,程序正常运行。

关于typing.TYPE_CHECKING,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/61545580

2.6 创建一个通用文件,从该文件导入

另一种方法是创建一个两个模块都可以导入的通用文件。这有助于打破导入循环,避免循环导入。这种方式主要通过更高层代码结构优化来避免。

3. 最后

现在,你可以尝试使用这些方法中的任何一种,而不用浪费几个小时去弄清循环导入的含义,很快就能完成。

感谢您花时间阅读文章

关注公众号不迷路:)