这一篇主要记录包、模块和 import 相关的一些概念以及流程,需要一丢丢的Python基础
在 Python 中,一个模块是一个包含 Python 代码的文件,其扩展名为 .py。(Python脚本)
每个模块都可以定义变量、函数、类和其他 Python 对象。
这些对象可以通过导入模块进行访问。要使用模块,可以使用 import
语句将其导入到代码中。
一个包是一个包含多个模块的目录。包可以是任意层次结构,并且可以包含其他包(子包)。
包通常包含一个名为 __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 将不会将目录视为包。平时会导入包、模块,那有没有想过动态的加载包和模块呢?这里用上述的目录结果演示
# 导入包
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名字符串的数组
因为模块和包导入以后会保存在内存里面,所以当我们更新了某个文件未释放解释器时,变更不会生效,当我们碰到需要长时间运行的脚本就需要重启解释器。可以考虑代码中触发重新导入
这是最简单的方法,通过重启解释器,所有的模块都会重新加载
reload
函数from importlib import reload
import module5
# 修改了 module5.py 文件后,使用 reload 函数重新加载模块
reload(module5)
注意:
reload
函数,某些更改可能仍然不会生效,特别是如果它们涉及到全局变量或类的定义。 一些开发环境(如 Jupyter Notebook)或代码编辑器(如 PyCharm)提供了自动重载模块的功能,当你保存文件时,它们可以自动重新执行模块代码。
在一些复杂的场景中,你可以编写自定义逻辑来监控模块文件的更改,并在检测到更改时手动重新加载模块
如果你有一个包结构,你可以将包的路径添加到 sys.path
中,然后使用正常的导入语句
import sys
# 将包的路径添加到 sys.path
sys.path.insert(0, '/path/to/your/package')
# 现在你可以正常导入包下的模块
my_module = import module
注意:如果可能存在多个同名的模块,请以 package.module 的方式或者用from 导入
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')
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)
__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__()
时,你应该尽量避免直接导入顶级模块,因为这可能会导致命名空间的不明确。通常建议在导入嵌套模块时使用它。就是详细一些的意思。
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
是最推荐的方式,因为它提供了更多的控制和安全保障。有些情况需要获取包的子模块,进行选择性的加载
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)
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)
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)
Python 的 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 解释器会执行以下步骤:
sys.modules
字典中,以便之后可以直接引用,避免重复加载当你尝试访问一个模块的属性(如函数、类或变量)时,Python 解释器确保该模块已经被加载到内存中。如果尚未加载,它将加载该模块。
使用 from module import name
语句导入特定元素时,如果模块还没有被加载,Python 解释器会首先加载整个模块
如果使用了像 reload()
这样的内置函数来重新加载模块,或者使用了 importlib.reload()
,Python 解释器会重新执行模块的代码,这通常发生在调试过程中。
某些情况下,模块可能在 Python 解释器启动时被预加载,例如通过 sitecustomize.py
或 usercustomize.py
脚本。
Python 的 import 机制是为了避免重复加载同一个模块,因为模块的加载和初始化可能涉及昂贵的操作。因此,一旦模块被加载,它就会保留在内存中,直到解释器关闭或模块被显式卸载。
内存管理:一旦模块被加载到内存中,它就会占据一定的内存空间,直到 Python 解释器退出或者显式地卸载该模块。
sys.modules
:sys.modules
字典是 Python 解释器中所有已加载模块的缓存。如果模块已经在这个字典中,后续的导入操作将直接使用缓存中的模块对象,而不会重新加载模块。
所以,加载到内存后,变更模块文件是不会生效的。需要重新加载或者重启解释器
from module import *
语句通常用于从某个模块中导入所有的符号(函数、类和变量等),但它的行为并不会预先加载该模块包下的所有模块
执行流程:
module
的模块__all__
列表(如果定义了的话),这个列表指定了哪些名称应该被导出__all__
列表,解释器会导入该模块顶层定义的所有名称(即不在任何类或函数体内的变量、函数和类)注意事项:
1)不会递归地导入子模块。也就是说,如果 module
是一个包,它包含其他子模块,这些子模块不会因为 from module import *
而被自动导入。
2)如果想要导入包下的所有子模块,需要分别对每个子模块使用 import
语句或者使用其他包导入机制,如 from module.submodule import *
3)
使用 from module import *
可能会导致命名空间的污染,因为可能会导入许多不需要的符号,并且可能会覆盖当前命名空间中已经存在的名称,需要斟酌一下
到这里也基本能了解清楚Python的包和模块的概念以及 import 的导入流程了。
其实平时除了导入包、模块也经常会有导入模块中的某个 Class 或者 某个函数的情况,具体问题具体分析吧