避免循环导入需延迟导入、提取公共模块或用字符串类型注解;__init__.py应仅定义接口和轻量导出,禁放耗时初始化;pyproject.toml需分场景管理依赖;动态导入适用于插件式或条件性功能。

模块拆分时如何避免循环导入
循环导入是模块拆分中最常踩的坑,现象通常是 ImportError: cannot import name 'X' from partially initialized module 'Y'。根本原因是两个模块在初始化过程中互相依赖对方尚未执行完的顶层代码。
解决思路不是“谁先导入谁”,而是切断顶层依赖链:
- 把跨模块调用移到函数/方法内部(延迟导入),比如在
def load_config():里才import config_loader - 提取公共逻辑到第三个模块(如
core/utils.py),让原模块只依赖它,不互赖 - 用字符串形式的类型注解(
from __future__ import annotations)替代运行时需要的类引用,尤其在__init__.py或数据类中
__init__.py 中该放什么、不该放什么
__init__.py 不是“自动执行脚本”,它的核心作用是定义包的公共接口和控制导入行为。
该放的:
立即学习“Python免费学习笔记(深入)”;
-
__all__ = ['Client', 'connect']—— 明确from package import *暴露哪些符号 - 轻量级别名导出,如
from .client import Client→from .client import Client as APIClient - 包级常量或版本号,如
__version__ = "0.2.1"
不该放的:
- 耗时初始化(如连接数据库、加载大模型权重)—— 这会让任何导入都变慢,且无法按需触发
- 条件导入逻辑(如
if sys.platform == 'win32': import winreg)—— 应移入具体使用它的模块中 - 对子模块的“全量导入”(
from . import *)—— 破坏封装,隐藏真实依赖,还可能引发命名冲突
用 pyproject.toml 精确控制可选依赖与开发依赖
很多人把所有依赖写进 requirements.txt,结果部署时装了一堆用不到的包(比如 pytest 跑在生产环境)。现代 Python 项目应直接用 pyproject.toml 的 [project.optional-dependencies] 和 [build-system] 区分场景。
实操建议:
- 基础依赖写进
[project.dependencies],确保pip install mypkg能跑通主流程 - 测试依赖归到
[project.optional-dependencies.test],安装时用pip install "mypkg[test]" - 文档构建依赖单独设为
[project.optional-dependencies.doc],避免 sphinx 污染 CI 构建环境 - 开发工具(如
ruff,mypy)不要放进dependencies,而应放在[project.optional-dependencies.dev]或独立的dev-requirements.in
注意:如果用了 setuptools,要确认 [build-system.requires] 里声明了 setuptools>=61.0,否则旧版 pip 可能忽略 optional-dependencies。
动态导入 vs 静态导入:什么时候该用 importlib.import_module()
静态导入(import xxx)在模块加载时就解析,适合稳定、必需的依赖;动态导入在运行时才触发,适合插件式、条件性或可选功能。
典型适用场景:
- 用户可配置后端(如
"redis"/"memory"),通过importlib.import_module(f"mypkg.cache.{backend}")加载对应实现 - 兼容多个第三方库版本(如支持
requests>=2.28和httpx),在首次调用时才尝试导入并缓存结果 - 避免启动时加载重量级模块(如
torch),仅当用户调用.train()方法时才导入
注意点:
- 动态导入失败会抛
ModuleNotFoundError,必须显式捕获并提供清晰错误提示(比如 “请安装 torch:pip install mypkg[torch]”) - 不要在循环里反复调用
importlib.import_module(),应缓存返回的模块对象 -
__import__()已过时,一律用importlib.import_module()
模块拆分不是越细越好,关键看边界是否符合单一职责;依赖控制也不是靠删包,而是让每个导入都有明确动机和可控时机。最容易被忽略的是:把“想当然”的导入(比如为了方便在 __init__.py 里聚合所有类)当成设计,而不是问题。










