视频链接:

不如语冰:

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.除了直接在路径里寻找之外,还经常使用的是相对路径导入,这个后面会详细介绍分析。

找到模块文件之后,就是将模块的代码整合到运行的代码里,具体如下:

1. 找到模块文件后,将其编译为字节码(如果必要),然后执行模块中的代码(注意:模块顶层的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:动态修复包上下文(高级技巧)

# submodule.py

顶部添加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__)

### 注意事项:

1. **循环导入**:两个模块相互导入可能导致问题,应尽量避免。可以通过重构代码(如将导入语句放在函数内部或使用延迟导入)来解决。

2. **重复导入**:多次导入同一个模块,实际上只会执行一次(由于`sys.modules`缓存)。但可以多次使用`import`语句,不过通常只导入一次。

3. **模块执行**:导入模块时,模块顶层的代码(包括print语句、函数定义等)会被执行。因此,通常将可执行代码封装在`if __name__ == '__main__':`中,避免在导入时执行

4. **命名冲突**:使用`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 导入机制底层原理

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐