视频链接:
不如语冰:
https://space.bilibili.com/70431433?spm_id_from=333.1007.0.0
文章代码链接:
GitHub - zyf-ngu/Qmatter: 跟着问题学-AI大模型入门系列笔记和代码 · GitHub
模块 ( 类和 函数) 的导入
实际的项目中不可能一份代码文件实现所有的功能,因此需要借助外在的力量,这份力量主要有2类,一种是系统库集成的,像numpy处理数据,另一种是自己项目写的函数和类,但是放到不同的位置。
当前的代码怎么使用这些外在的力量(类和函数)呢?
方法就是import导入,将其它外在的类和函数导入到当前运行的代码文件里,下面详细学习一下。
基本原理
首先明确几个概念,项目中的文件夹对应Python的包/库package,文件对应模块module,模块的名称就是Python代码文件取消扩展名.py。
包package和模块module的导入核心就是找到模块并完成导入,问题来了,如何找到模块呢?找不到模块也是经常导入报错的原因,答案就是在设置的路径里寻找。具体来说,寻找导入模块的步骤如下:
1.在sys.modules缓存中查找是否已经导入过该模块,如果存在则直接使用。这一步通常不进行人为的操作;
2.如果不在缓存中,则搜索模块文件,这部分是需要重点关注的,排查导入报错问题也是需要先看这块。搜索路径由sys.path定义,一般包括当前目录、环境变量PYTHONPATH指定的目录以及Python安装目录(这个一般是conda或pip安装指定的目录)。可以手动添加路径。
3.除了直接在路径里寻找之外,还经常使用的是相对路径导入,这个后面会详细介绍分析。
找到模块文件之后,就是将模块的代码整合到运行的代码里,具体如下:
- 找到模块文件后,将其编译为字节码(如果必要),然后执行模块中的代码(注意:模块顶层的import代码在导入时会被执行,下面的代码看是否有if name==main)。
2.将模块对象添加到`sys.modules`缓存中,并创建到当前命名空间的引用(根据导入方式)。
导入分类
从导入内容和数量来看,
可以直接导入整个模块,
import module_name;
使用的模块中的内容 :
module_name.function_name或 module_name.Class_name
可以导入模块的部分函数 from module_name import function_0 , function_1 ,此时直接使用函数名或类名,无需模块名前缀
也可以 导入模块的全部函数
from module_name import *
因为有的模块和函数名字比较长,或者要导入的函数的名称可能与程序中现有的名称冲突,可以在导入的时候使用as起个别名
import module_name as new_name
从导入方式来看,
分为相对导入和绝对导入,其区别为是否写出模块的绝对路径。
项目根目录与顶级包
下面我们重点区分一下项目根目录和顶级包这个概念,为学习后面的相对导入和绝对导入做好准备。
项目根目录(project_root,项目名)本身不是一个Python包,因为它没有__init__.py,但当我们将项目根目录添加到sys.path后,就可以直接导入其下面的子文件夹作为包package,找不到主目录时,可以手动设置:
# 设置项目根目录
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(file))))
# 在main文件里 添加项目根目录到 sys.path
sys.path.append(os.path.dirname(os.path.abspath(file)))
注意一般情况下项目名称不是顶级包,下面的子文件夹才是顶级包。
项目根目录 vs 顶级包 关键区别
|-------------|--------------------|-------------------------|
| 特性 | 项目根目录 | 顶级包 |
| 定义 | 包含整个项目的文件夹 | 项目中包含 init.py 的顶级目录 |
| Python 识别 | 不在 sys.path 中时不被识别 | Python 将其视为可导入的包 |
| init.py | 不需要 | 必须存在 |
| 导入方式 | 不可直接导入 | 可直接导入 |
| 典型位置 | 包含多个包的父目录 | 项目根目录下的一级子文件夹 |
绝对导入就是从顶级包开始,每个子包都写上
import package1.package2... .module1
相对导入就是确定要导入的模块和当前运行模块的相对位置关系,相对导入本质上也是转换成绝对导入,其连接点就是当前运行模块所归属的__package__属性。根据这个属性,Python会得到当前运行模块所属的包,然后根据前面有几个点(.)来推导出要导入模块的父包
from . import module → from my_package import module
以下面的实例详细解释一下点号含义和相对导入的使用方法:
project_root/ <-- 项目根目录
├── main.py
├── core/ <-- 一级子包
│ ├── init.py
│ ├── utils.py
│ └── services/ <-- 二级子包
│ ├── init.py
│ ├── api.py
│ └── auth.py
└── lib/ <-- 一级子包(同级目录)
├── init.py
├── network.py
└── storage/
├── init.py
└── cloud.py
点号含义 :
. 表示当前文件所在的目录(包),.. 表示上一级目录(父包),... 表示祖父包(但不推荐使用),例如
在 core/services/api.py 中:. = core.services,.. = core
在 lib/network.py 中:. = lib,.. = lib
在 core/utils.py 中:. = core,.. = 项目根目录(无父包)
以上面目录结构为例,相对导入可以导入当前包下的模块,子包下的模块(子包.module),具体到代码里:
假设我们在 core/services/api.py 中:
✅ 正确用法
from . import auth # 导入同级的 auth.py
from .. import utils # 导入父级的 utils.py
❌ 错误用法
from ... import main # 错误!超过两级父目录
from ..lib import network # 错误!不能跨兄弟目录
也就是说,相对导入一般最多只导入父包下的模块,和父包同级的,父包上级的模块都不能导入。
相对导入的问题
因为相对导入也是要转换为绝对导入,而且是依赖__package__属性的,就会出现一个相对导入最常出现的问题,找不到包。
相对导入只能在package里面的module使用,并且使用了相对导入的模块必须由其它模块调用而不能直接运行。
当直接运行包内的模块文件时:
python my_package/submodule.py
会发生下面的问题:
Python 将该文件视为顶级脚本 (类似于main.py) 而非包的一部分,package 被设置为 None,不知道其属于哪个package,则相对导入无法确定父包位置,也就找不到其它模块,导入失败并抛出 ImportError
解决方案有下面几种:
方案 1:使用外部入口点(推荐),比如在main.py主文件里调用运行
# 项目根目录 /main.py
import sys
import os
# 导入包内的模块
from my_package import submodule
if name == "main":
submodule.run()
运行:python main.py
方案 2:使用 -m 参数(模块运行模式)
# 在 项目根目录 运行
python -m my_package.submodule
注意是module,不是.py,也就是去掉.py的模块名。
这样 Python 会:识别完整的包路径 my_package.submodule并正确设置 package = "my_package"启用相对导入
方案 3:动态修复包上下文(高级技巧)
顶部添加if name == "main" and package is None:
# 手动设置包上下文
import os
import sys
# 计算包路径 (my_package.submodule → my_package)
file_path = os.path.abspath(file)
base_dir = os.path.dirname(os.path.dirname(file_path))
package_name = os.path.basename(base_dir)
# 添加到 sys.path
if base_dir not in sys.path:
sys.path.insert(0, base_dir)
# 设置包上下文
package = package_name
# 重新导入当前模块(确保相对导入生效)
import importlib
importlib.import_module(name)
注意事项:
-
**循环导入**:两个模块相互导入可能导致问题,应尽量避免。可以通过重构代码(如将导入语句放在函数内部或使用延迟导入)来解决。
-
**重复导入**:多次导入同一个模块,实际上只会执行一次(由于`sys.modules`缓存)。但可以多次使用`import`语句,不过通常只导入一次。
-
**模块执行**:导入模块时,模块顶层的代码(包括print语句、函数定义等)会被执行。因此,通常将可执行代码封装在`if name == 'main':`中,避免在导入时执行。
-
**命名冲突**:使用`from ... import *`可能导致当前命名空间中已有的名字被覆盖。推荐使用导入整个模块并起别名的方式,或者明确导入需要的名字。
动态导入
在代码的实际运行中,我们会遇到根据参数导入特定的模块,这时可以使用动态导入模块。importlib.import_module提供了这种动态导入的能力。import_module函数内部调用了`import()`函数,它返回导入的模块对象,我们可以通过该对象访问模块中的内容。
importlib.import_module(package名或者模块名)
def get_loader(loader_name: str, file_path: str, loader_kwargs: Dict = None):
'''
根据loader_name和文件路径或内容返回文档加载器。
'''
loader_kwargs = loader_kwargs or {}
try:
if loader_name in "RapidOCRPDFLoader", "RapidOCRLoader":
document_loaders_module = importlib.import_module('document_loaders')
else:
document_loaders_module = importlib.import_module('langchain.document_loaders')
DocumentLoader = getattr(document_loaders_module, loader_name)
except Exception as e:
msg = f"为文件{file_path}查找加载器{loader_name}时出错:{e}"
logger.error(f'{e.class.name}: {msg}',
exc_info=e if log_verbose else None)
document_loaders_module = importlib.import_module('langchain.document_loaders')
DocumentLoader = getattr(document_loaders_module, "UnstructuredFileLoader")
关键要点:
动态导入虽然强大,但应谨慎使用。在大多数场景下,静态导入仍然是首选,因为它更易于理解和调试。动态导入最适合需要运行时灵活性的特定场景。
优先使用 importlib.import_module() 而非 import()
始终验证动态导入的输入来源
合理利用缓存避免重复导入开销
为动态导入提供清晰的错误处理
理解 Python 导入机制底层原理