1. 写在前面
在 Python 项目开发中,或多或少都会遇到循环 import
即 circular 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. 最后
现在,你可以尝试使用这些方法中的任何一种,而不用浪费几个小时去弄清循环导入的含义,很快就能完成。