Python包如何避免循环导入,同时复用并暴露typing类型定义?
我开发一个Python包,需要实现两个目标:
- 通过
typing模块提供代码提示 - 导出部分类型变量供外部用户使用,同时包内部也能复用这些类型
核心困扰是这类操作经常引发循环导入问题。
示例代码如下:package/foo.py:
from typing import Union, Any class A: def hello(self, arg: Union[str, dict]) -> Any: pass
package/bar.py:
from typing import Union, Any from . import foo # 无法执行:from package.typing import BarBType class B: def hello(self, arg: Union[foo.A, str, dict]) -> Any: pass
我尝试创建package/typing.py来集中管理类型:
from typing import Union from package import foo from package import bar BarBType = Union[foo.A, str, dict] FooBarType = Union[foo.A, bar.B]
但这样会导致循环导入,而且package.bar也没法复用这个类型定义。
请问,对于需要暴露复杂类型结构、既要内部复用又要对外暴露的Python包,该如何组织文件和类型信息?我倾向于使用内联类型定义(不使用存根文件.pyi),同时希望文档工具能利用这些类型优化最终文档。我曾考虑在使用类型的地方定义类型,再让typing.py导入并暴露,但这似乎不太符合直觉...
方法1:使用类型字符串延迟解析(Python 3.7+)
Python 3.7开始支持字符串形式的类型标注(PEP 484),结合typing.TYPE_CHECKING常量,可以避免直接导入导致的循环依赖。同时在typing.py中统一暴露类型,内部模块也从这里导入。
调整后的代码结构:
package/foo.py:
from typing import Union, Any from .typing import BarBType class A: def hello(self, arg: Union[str, dict]) -> Any: pass
package/bar.py:
from typing import Any from .typing import BarBType class B: def hello(self, arg: BarBType) -> Any: pass
package/typing.py:
from typing import Union, TYPE_CHECKING # 类型检查阶段用字符串延迟导入,避免循环 BarBType = Union["foo.A", str, dict] FooBarType = Union["foo.A", "bar.B"] # 仅在类型检查工具运行时解析实际类型 if TYPE_CHECKING: from . import foo, bar BarBType = Union[foo.A, str, dict] FooBarType = Union[foo.A, bar.B]
TYPE_CHECKING是一个仅在类型检查工具(如mypy)运行时为True的常量,运行时不会触发导入,彻底规避循环问题。类型检查工具能识别字符串对应的类型,运行时也不会报错。
方法2:将基础类与类型分离
如果类型依赖的是基础业务类,可以把这些类放在独立模块(比如package/models.py),让typing.py和业务模块都从这个基础模块导入,从根源上消除循环。
结构调整:
package/models.py:
class A: pass class B: pass
package/typing.py:
from typing import Union from .models import A, B BarBType = Union[A, str, dict] FooBarType = Union[A, B]
package/foo.py:
from typing import Any from .models import A from .typing import BarBType class A: def hello(self, arg: Union[str, dict]) -> Any: pass
package/bar.py:
from typing import Any from .models import B from .typing import BarBType class B: def hello(self, arg: BarBType) -> Any: pass
这种方式适合类结构清晰的场景,完全避免循环导入的可能。
方法3:业务模块定义类型,typing.py统一导出
如果觉得在业务模块定义类型更直观,可以在使用类型的地方先定义,再在typing.py中导入并统一对外暴露。内部模块用自己定义的类型,外部用户从typing.py获取。
示例:
package/bar.py:
from typing import Union, Any from . import foo # 内部先定义类型 BarBType = Union[foo.A, str, dict] class B: def hello(self, arg: BarBType) -> Any: pass
package/typing.py:
from typing import Union from . import foo, bar # 导入业务模块的类型并重新导出 BarBType = bar.BarBType FooBarType = Union[foo.A, bar.B]
package/foo.py若需使用BarBType,可直接从bar导入:
from typing import Any from .bar import BarBType class A: def hello(self, arg: BarBType) -> Any: pass
如果出现双向依赖,可结合方法1的字符串标注来兼容。
内容的提问来源于stack exchange,提问作者nickpapior

