Python 包、模块和 import 笔记

这一篇主要记录包、模块和 import 相关的一些概念以及流程,需要一丢丢的Python基础

Python 包与模块

模块(Module)

在 Python 中,一个模块是一个包含 Python 代码的文件,其扩展名为 .py。(Python脚本)

每个模块都可以定义变量、函数、类和其他 Python 对象。

这些对象可以通过导入模块进行访问。要使用模块,可以使用 import 语句将其导入到代码中。

包(Package)

一个包是一个包含多个模块的目录。包可以是任意层次结构,并且可以包含其他包(子包)。

包通常包含一个名为 __init__.py 的特殊模块文件,该文件用于初始化包并定义其公共接口。

要使用包,可以像导入模块一样使用 import 语句。

目录结构

如下是一个简单的项目目录结构,里面包含两个包以及6个模块

project/
   ├── package1/
   │   ├── __init__.py
   │   ├── module1.py
   │   └── module2.py
   ├── package2/
   │   ├── __init__.py
   │   ├── module3.py
   │   └── module4.py
   ├── module5.py
   └── main.py
  • __init__.py 文件:这个文件是包的初始化文件。它用于定义包的公共接口,并在导入包时执行一些初始化代码,内容可以为空。如果没有 __init__.py 文件,Python 将不会将目录视为包
  • moudle5 不属于任何一个包

模块的导入

平时会导入包、模块,那有没有想过动态的加载包和模块呢?这里用上述的目录结果演示

简单导入

# 导入包
import package1
# 导入包是不能用如下方式调用模块的,模块不是包的属性
# package1.module1
------------------------------------------------
# 导入包中的所有模块
from package1 import *
# 调用
module1
------------------------------------------------
# 导入指定的模块
import package1.module1
# 调用
package1.module1

from package1 import moudle1
# 调用
moudle1
------------------------------------------------
# 调用指定包,定义别名
import package1.module1 as moudle_a
# 调用
moudle_a

注意:因为from package import module 是需要获取package的__all__属性来加载模块的,所以如果报错没有这个模块,建议在__init__.py 中添加__all__这个包含module名字符串的数组

重新导入

因为模块和包导入以后会保存在内存里面,所以当我们更新了某个文件未释放解释器时,变更不会生效,当我们碰到需要长时间运行的脚本就需要重启解释器。可以考虑代码中触发重新导入

1)重启解释器

这是最简单的方法,通过重启解释器,所有的模块都会重新加载

2)使用 reload 函数
from importlib import reload

import module5

# 修改了 module5.py 文件后,使用 reload 函数重新加载模块
reload(module5)

注意:

  •  即使使用了 reload 函数,某些更改可能仍然不会生效,特别是如果它们涉及到全局变量或类的定义。 
  • 因为这些更改可能不会影响已经创建的对象或已经执行的代码
  • 在某些情况下,你可能需要手动重新初始化变量或对象以反映更改。
3)开发环境中的自动重载

一些开发环境(如 Jupyter Notebook)或代码编辑器(如 PyCharm)提供了自动重载模块的功能,当你保存文件时,它们可以自动重新执行模块代码。

4)自定义重载逻辑

在一些复杂的场景中,你可以编写自定义逻辑来监控模块文件的更改,并在检测到更改时手动重新加载模块

动态导入

1)使用路径导入

如果你有一个包结构,你可以将包的路径添加到 sys.path 中,然后使用正常的导入语句

import sys

# 将包的路径添加到 sys.path
sys.path.insert(0, '/path/to/your/package')

# 现在你可以正常导入包下的模块
my_module = import module

注意:如果可能存在多个同名的模块,请以 package.module 的方式或者用from 导入

2)使用 importlib 模块

importlib 是 Python 的一个内置库,它提供了用于导入模块的功能。你可以使用 importlib.util 中的 spec_from_file_location 和 module_from_spec 来动态加载模块。

spec_from_file_location 函数创建一个 ModuleSpec 对象,它包含了指定文件的位置和名称。ModuleSpec 对象用于描述模块的属性,如名称、文件路径等。使用 spec_from_file_location 可以创建一个模块规范,以便稍后使用 module_from_spec 加载它。

module_from_spec 函数根据给定的 ModuleSpec 对象创建和加载一个模块。module_from_spec 会读取模块文件,执行其中的代码,并返回一个已加载的模块对象。对象包含了 模块的所有属性和引用,但是模块代码本身并没有执行。这意味着,如果 模块中有任何初始化代码或全局变量的定义,这些代码将不会执行,全局变量也不会被初始化。使用 module_from_spec 可以动态加载模块,而不必使用 import 语句。

ModuleSpec 对象的 spec.loader.exec_module(module) 是在动态加载模块时执行模块代码的关键步骤。虽然 module_from_spec 返回了一个已加载的模块对象,但该对象在创建时并没有立即执行模块代码。执行 spec.loader.exec_module(module) 可以确保模块代码在加载后立即执行,从而初始化模块中的变量、函数和类

import importlib.util

def import_module_dynamic(module_name, module_path):
    # 创建一个 ModuleSpec 对象,它包含了指定文件的位置和名称
    spec = importlib.util.spec_from_file_location(module_name, module_path)
    # 读取模块文件,执行其中的代码,并返回一个已加载的模块对象
    module = importlib.util.module_from_spec(spec)
    # 模块代码在加载后立即执行,从而初始化模块中的变量、函数和类
    spec.loader.exec_module(module)
    return module

my_module = import_module_dynamic('module1', '/path/to/module1.py')

模块名字符串导入

1)使用 importlib.import_module()

importlib.import_module() 函数是动态导入模块的标准方式

import importlib

# 通过字符串动态导入模块
module_name = "json"
my_module1 = importlib.import_module(module_name)

# 使用模块
data = my_module1.dumps({"key": "value"})

# 导入包内的模块
my_module2 = importlib.import_module(package1.moudle1)
2)使用 __import__()

Python 的内置函数 __import__() 也可以用于动态导入模块,但它的使用稍微复杂一些。

# 使用 __import__ 动态导入模块
module_name = "json"
my_module = __import__(module_name)
 
# 使用模块
data = my_module.dumps({"key": "value"})

嵌套导入

module_name = "package1.module1"
my_module = __import__("package1.module1", fromlist=[""])
 
# 访问 submodule
module1 = getattr(my_module, "module1")

注意:使用 __import__() 时,你应该尽量避免直接导入顶级模块,因为这可能会导致命名空间的不明确。通常建议在导入嵌套模块时使用它。就是详细一些的意思。

3)使用 exec()

虽然不推荐,但也可以使用 exec() 函数执行导入语句。这种方法不安全,因为它执行了字符串形式的代码,可能会引入安全风险。

# 使用 exec() 导入模块(不推荐)
module_name = "json"
exec(f"import {module_name}")
my_module = locals()[module_name]
# f"import {module_name}" 这个是一种字符串的格式化,f 开头的字符串,可以用{}取变量
module_name = "json"
print(f"import {module_name}")
import json

 导入注意事项

  • 动态导入模块时要确保字符串来源是可信的,避免执行恶意代码。
  • 使用 importlib 是最推荐的方式,因为它提供了更多的控制和安全保障。
  • 如果在循环或多次调用中动态导入模块,确保使用缓存机制来避免不必要的重复导入。
  • 导入模块的成本还是比较高的,特别是某些模块内有些耗时的代码

获取包的子模块

有些情况需要获取包的子模块,进行选择性的加载

1)使用 pkgutil 模块

pkgutil 模块提供了一个 walk_packages 函数,它可以遍历包中的所有子模块,可以递归子包

import pkgutil
 
def get_submodules(package):
    submodules = []
    for loader, name, ispkg in pkgutil.walk_packages(package.__path__):
        full_name = package.__name__ + '.' + name
        submodules.append(full_name)
        if ispkg:
            submodules.extend(get_submodules(__import__(full_name, fromlist=[''])))
    return submodules
 
# 假设你想要获取当前包的子模块列表
current_package = __import__(__name__)
submodules_list = get_submodules(current_package)
print(submodules_list)

2)使用 importlib 模块

importlib 模块提供了更底层的导入机制。你可以使用 importlib.util.find_spec 来检查子模块是否存在。也可以递归子包

import importlib.util
 
def get_submodules(package_name):
    submodules = []
    package_path = importlib.util.find_spec(package_name).submodule_search_locations
    for entry in package_path:
        for item in os.listdir(entry):
            if item.endswith('.py') and item != '__init__.py':
                submodule_name = item[:-3]
                submodule_full_name = f"{package_name}.{submodule_name}"
                if importlib.util.find_spec(submodule_full_name) is not None:
                    submodules.append(submodule_full_name)
    return submodules
 
# 获取当前包的子模块列表
package_name = __name__
submodules_list = get_submodules(package_name)
print(submodules_list)

3)使用 os 模块直接检查文件

如果你确信子模块都在文件系统中以 .py 文件的形式存在,你可以直接使用 os 模块来遍历目录。这个不会递归子包,当然这种递归的逻辑写起来也简单,判断子目录是否存在__init__.py文件,然后递归就是了

import os
 
def get_submodules(package_dir):
    submodules = []
    for item in os.listdir(package_dir):
        if item.endswith('.py') and item != '__init__.py':
            submodule_name = item[:-3]
            submodules.append(submodule_name)
    return submodules
 
# 假设你知道当前包的目录路径
package_dir = os.path.dirname(__file__)
submodules_list = get_submodules(package_dir)
print(submodules_list)

获取子模块提醒

  • 这些方法可能不会考虑到所有的情况,例如它们可能不会检测到动态加载的模块或内置模块
  • 如果包的结构复杂,或者使用了特殊的导入机制,这些方法可能需要相应的调整

import 机制

Python 的 import 机制是一种用于将其他模块或包中的代码引入当前模块或包的方法

import 流程

简单来说,就是寻找模块并执行,然后加载到当前模块的 __dict__ 属性中来实现代码的导入,所以我们自己定义的模块的时候,要避免加载过程中执行耗时操作

1、当 Python 解释器遇到 import 语句时,它会在内置模块用户自定义模块中查找指定的模块名

2、如果找到指定的模块,解释器会检查该模块是否已经加载。如果已经加载,跳到步骤 5

3、如果模块未被加载,解释器会查找模块所在的路径。

       Python 解释器会在 sys.path 列表中查找模块路径。

  sys.path 列表包含 Python 解释器的搜索路径,例如当前目录、Python 标准库目录等。

4、解释器在找到模块路径后,会加载该模块。

        加载过程包括读取模块文件执行模块代码创建模块对象等。

        模块对象是一个包含模块名称、模块代码和模块内所有定义的类的对象

5、一旦模块被加载,解释器会将模块对象添加到当前模块的 __dict__ 属性中。

        可以通过模块名访问模块中的定义。

6、如果在 import 语句中使用了 as 关键字为模块指定别名,解释器会将模块对象添加到当前模块的 __dict__ 属性中,并使用指定的别名作为键。

7、如果使用 from module import name 语句导入模块中的特定名称,解释器会查找模块中名为 name 的定义,并将其添加到当前模块的 __dict__ 属性中。

模块加载时机

首次导入模块时

当你第一次使用 import 语句导入一个模块时,Python 解释器会执行以下步骤:

  1. 查找模块文件
  2. 加载模块文件到内存中
  3. 执行模块文件中的代码,初始化模块级变量和函数
  4. 创建一个模块对象,并将其添加到 sys.modules 字典中,以便之后可以直接引用,避免重复加载

尝试访问模块属性时

当你尝试访问一个模块的属性(如函数、类或变量)时,Python 解释器确保该模块已经被加载到内存中。如果尚未加载,它将加载该模块。

导入模块中的元素时

使用 from module import name 语句导入特定元素时,如果模块还没有被加载,Python 解释器会首先加载整个模块

使用内置函数或装饰器时

如果使用了像 reload() 这样的内置函数来重新加载模块,或者使用了 importlib.reload(),Python 解释器会重新执行模块的代码,这通常发生在调试过程中。

初始化阶段

某些情况下,模块可能在 Python 解释器启动时被预加载,例如通过 sitecustomize.pyusercustomize.py 脚本。

import 内存

Python 的 import 机制是为了避免重复加载同一个模块,因为模块的加载和初始化可能涉及昂贵的操作。因此,一旦模块被加载,它就会保留在内存中,直到解释器关闭或模块被显式卸载。

内存管理:一旦模块被加载到内存中,它就会占据一定的内存空间,直到 Python 解释器退出或者显式地卸载该模块。

sys.modulessys.modules 字典是 Python 解释器中所有已加载模块的缓存。如果模块已经在这个字典中,后续的导入操作将直接使用缓存中的模块对象,而不会重新加载模块。

所以,加载到内存后,变更模块文件是不会生效的。需要重新加载或者重启解释器

from moudle import *

from module import * 语句通常用于从某个模块中导入所有的符号(函数、类和变量等),但它的行为并不会预先加载该模块包下的所有模块

执行流程:

  1. 查找名为 module 的模块
  2. 在该模块中找到 __all__ 列表(如果定义了的话),这个列表指定了哪些名称应该被导出
  3. 如果没有 __all__ 列表,解释器会导入该模块顶层定义的所有名称(即不在任何类或函数体内的变量、函数和类)

注意事项:

1)不会递归地导入子模块。也就是说,如果 module 是一个包,它包含其他子模块,这些子模块不会因为 from module import * 而被自动导入。

2)如果想要导入包下的所有子模块,需要分别对每个子模块使用 import 语句或者使用其他包导入机制,如 from module.submodule import *

3)使用 from module import * 可能会导致命名空间的污染,因为可能会导入许多不需要的符号,并且可能会覆盖当前命名空间中已经存在的名称,需要斟酌一下


到这里也基本能了解清楚Python的包和模块的概念以及 import 的导入流程了。

其实平时除了导入包、模块也经常会有导入模块中的某个 Class 或者 某个函数的情况,具体问题具体分析吧

你可能感兴趣的:(知识系列,Python,python,笔记,开发语言)