正确做法是将可选依赖探测逻辑置于模块顶层,用 importlib.util.find_spec 或 try/except 预检并设全局开关,避免类内重复导入;依赖应通过构造函数注入,配合 extras_require 声明和完整测试覆盖。

模块顶层单次探测,别在方法里反复 try/except
类内部方法里写 try: import xx 是最常见也最危险的做法——它会让同一模块多次导入时重复执行 try 块,可能触发副作用(比如日志刷屏、初始化逻辑错乱),更关键的是:IDE 和类型检查器(如 mypy)完全看不到这个模块,补全失效、类型提示消失,后续调用 xx.func() 时才爆 NameError,堆栈还找不到源头。
正确做法是把探测逻辑收在类定义**外部**、模块顶层,只执行一次:
- 用
importlib.util.find_spec("xxx")预检(轻量,不执行模块代码,适合启动快、怕副作用的场景) - 或用
try/except ModuleNotFoundError实际导入并设标记变量(适合需要立刻用模块功能的场景) - 显式将模块变量设为
None,避免未定义引用和类型混淆
用 globals() 注册统一开关,别靠局部判断
别在每个方法里写 if "torch" in sys.modules: 或 hasattr(sys.modules, "torch") ——这既慢又不可靠,还容易因重命名、作用域隔离出错。真正稳的方式是模块级注册一个布尔开关和模块引用:
try:
import torch
TORCH_AVAILABLE = True
torch_module = torch
except ModuleNotFoundError:
TORCH_AVAILABLE = False
torch_module = None
这样后续所有代码只需查 TORCH_AVAILABLE,逻辑清晰、IDE 可识别、测试也容易 mock。注意别在 except 里写 torch = None,否则类型检查器会误判 torch 类型为 Any。
类构造时传入可选依赖实例,而非自己导入
如果类的功能强依赖某个可选模块(比如用 orjson 做序列化),与其让它自己去探测导入,不如把依赖“注入”进来:
- 构造函数接收已导入的模块实例(
json_encoder=None) - 默认值为
None,由调用方决定是否传入 - 内部只做
if json_encoder is not None:判断,不碰导入逻辑
这样解耦了依赖探测和业务逻辑,单元测试时直接传个 mock 就行,也不用担心类内部导入失败导致整个类无法实例化。
发布时声明 extras_require,别让使用者猜
光在代码里处理可选依赖不够,用户安装时根本不知道要额外装啥。必须在 setup.py 或 pyproject.toml 中声明:
[project.optional-dependencies] torch = ["torch>=2.0"] rich = ["rich>=13.0"]
然后文档里明确写清:pip install mypkg[torch] 才能启用 GPU 加速,pip install mypkg[rich] 才有彩色日志。否则用户遇到 ModuleNotFoundError 时,第一反应不是缺模块,而是“这库是不是坏了”。
最容易被忽略的一点:探测逻辑本身也要测试覆盖——你得验证当模块不存在时,降级路径确实走通,而不是静默失败或抛出意外异常。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











