PythonLearn

解决循环导入的坑

问题来源

现在有项目结构如下,各模块都有一个与模块名同名但首字母大写的类,比如player.py里有Player

project/
│
├── entity/
│   ├── __init__.py
│   ├── player.py
│   └── ball.py
│
└── main.py
# __init__.py
from entity.player import Player
from entity.ball import Ball

# player.py
from entity.ball import Ball
class Player:
    def play(self, ball: Ball):
        ...

# ball.py
from entity.player import Player
class Ball:
    def hit(self, player: Player):
        ...

这时就会出现循环导入的错误 ImportError: cannot import name 'Player' from partially initialized module 'entity.player' (most likely due to a circular import)

原因

可以先看看代码执行顺序

  1. entity/__init__.pyentity/player.py导入Player
  2. sys.modules中添加了entity.player模块,并执行里面的代码
  3. entity/player.pyentity/ball.py导入Ball
  4. sys.modules中添加了entity.ball模块,并执行里面的代码
  5. entity/ball.pyentity/player.py导入Player
  6. sys.modules已存在entity.player模块,于是直接从该模块中获取Player
  7. [没执行到] entity/ball.py创建Ball

问题在第6步,获取Player时,entity.player模块还在执行from entity.ball import Ball, 而Player目前还没有被定义,导致无法找到,抛出ImportError

检验

为了验证是否真是这样 创建a.py和b.py

# a.py
import sys
print("fff" in sys.modules["a"].__dict__)
fff = 0
print("fff" in sys.modules["a"].__dict__)
# b.py
import a

执行b.py,输出

False
True

解决方法

大部分情况下,我们导入一个类在都是用于类型注释,而实际创建时都是在函数内部
因此,有两种方法

1. 在尾部导入

如果先导入的是player.py,那就确保player.py中先包含Player类,再导入Ball

# player.py
class Player:
    def play(self, ball: 'Ball') -> None:
        ...
from entity.ball import Ball

注意此时在类型注释中,由于Ball类在下面才定义,Ball的注释需要使用引号包裹

2. 假导入

由于typing.TYPE_CHECKING的值固定为False, 所有我们可以在if TYPE_CHECKING下导入只用来类型检查的模块

# player.py
from typing import TYPE_CHECKING
if TYPE_CHECKING:
    from entity.ball import Ball

class Player:
    def play(self, ball: 'Ball') -> None:
        from entity.ball import Ball # 如果要在内部创建Ball,需要引入

注意此时在类型注释中,由于Ball类从未被真正导入,Ball的注释需要使用引号包裹 而如果要在函数内部创建Ball对象,则需要引入Ball