模块和包

1. 内置模块和标准库

  • 内置模块 Python内置模块是用c语言编写并直接内嵌在Python解释器中的模块。内置模块在 Python 解释器构建时就已经编译成机器码,不需要在运行时再进行编译。尽管内置模块已经预编译成机器码并嵌入在 Python 解释器中,使用 import 语句来导入这些模块仍然是必要的。import 语句不仅负责加载模块,还确保模块的命名空间隔离、内存管理、初始化和代码可读性,从而使得模块管理更加高效和灵活。sys.builtin_module_names 属性列出了所有的内置模块。

  • 标准库 标准库是随 Python 安装包提供的一大堆模块和包的集合,包含了大量的功能模块。标准库中的模块一般是以 Python 源代码形式存在的,需要在使用时编译成字节码。importlib.util.find_spec 可以帮助查找模块的规范,并从中获取模块的路径。标准库模块通常位于 Python 安装目录的 lib 目录下。

2. 模块和包的定义

2.1 模块

典型的,一个 .py 后缀文件即是 Python 的一个模块。在模块的内部,可以通过全局变量 name 来获得模块名。模块可以包含可执行的语句,这些语句会在模块 初始化 的时候执行 —— 当所在模块被 import 导入时它们有且只会执行一次。

2.2 包

根据目前 PEP420 的提案,目前的 Python 实际上是有两种包的存在:正规包(regular Package) 以及 命名空间包(Namespace package)。 要注意的是,Python 的 package 实际上都是特殊的 module :可以通过导入 package 之后查看 globals() 可知;实际上,任何带有 path 属性的对象都会被 Python 视作 package 。

  • 正规包 在 Python 3.2 之前就已经存在了的,通常是以包含一个__init__.py文件的目录形式展现。当package被导入时,这个__init__.py文件会被隐式地执行。

  • 命名空间包 根据 PEP420 的定义,命名空间包是由多个 portion 组成的 —— portion 类似于父包下的子包,但它们物理位置上不一定相邻,而且它们可能表现为 .zip 中的文件、网络上的文件等等。命名空间包不需要 init.py 文件,只要它本身或者子包(也就是 portion)被导入时,Python 就会给顶级的部分创建为命名空间包 —— 因此,命名空间包不一定直接对应到文件系统中的对象,它可以是一个 虚拟 的 module 。

3. 模块导入

模块中的 Python 代码可以通过 import(导入)操作访问另一个模块内的代码。import 语句时调起导入机制的常用方式,但不是唯一方式。importlib.import_module() 以及内置的 import() 函数都可以调起导入机制。 import 语句实际上结合了两个操作:

  • 搜索操作:根据指定的命名查找模块

  • 绑定操作:将搜索的结果绑定到当前作用域对应的命名上 import 的 search 操作实际上是带参调用 import() 函数,而函数的返回值会用在 import 语句的绑定操作上。 直接调用 import() 只会执行模块查找,以及如果找到的话就创建模块。这会有一定的副作用,比如导入父包和更新各式各样的缓存(包括 sys.modules),而且绑定操作只有 import 语句才会做得到。 当一个模块被首次导入时,Python 会搜索该模块,如果找到就创建一个 module 对象并初始化;如果位找到则抛出 ModuleNotFoundError 异常。至于如何找到这些模块,Python 定义了多种的 搜索策略 (search strategy),而这些策略可以通过 importlib 等提供的各类 hook 来修改和扩展。 根据 Python 3.3 的 changlog 可知目前导入系统已完全实现了 PEP302 的提案,所有的导入机制都会通过 sys.meta_path 暴露出来,不会再有任何隐式的导入机制。

  1. 单独使用 import 时,import 后面只能接模块或包 (常规包或命名空间包)

  2. 使用 from xxx import xxx 时,from 后只能接模块或包,而此时 import 后可以接任何变量 (模块、包或模块中具体的方法)

3.2 模块搜索路径

当我们要导入一个模块(比如foo)时,搜索路径如下:

  • 内存中已经加载的模块:Python 会首先检查该模块是否已经存在于 sys.modules 中

  • 内置模块:如果模块在内存中不存在,Python 接下来会检查它是否是一个内置模块(例如 sys 或 math)

  • sys.path 中的目录:最后,Python 会遍历 sys.path 中的目录,按顺序查找该模块。sys.path 是一个包含目录路径的列表,默认包含以下内容:

    • 当前工作目录

    • PYTHONPATH 环境变量中指定的目录

    • 安装 Python 时默认的标准库目录

    • .pth文件中定义的路径

    • 第三方库路径 如果当前目录包含有和标准库同名的模块,会直接使用当前目录的模块而不是标准模块。 如果你在运行 Python 脚本的过程中使用 os.environ 动态设置 PYTHONPATH,这些变化不会影响当前 Python 进程的 sys.path,因为 sys.path 是在 Python 启动时从环境变量 PYTHONPATH 初始化的。 如果不想修改 sys.path 的同时又想扩展搜索路径,可以使用 .pth 文件。首先该文件内容很简单,只需要补充你要导入的库的路径(绝对路径),一行一个;然后将该文件放到 特定的位置 ,Python 在加载模块时,就会读取 .pth 文件中的路径。 那么这个所谓的 特定位置 是哪里呢?我们可以通过 site 模块的 getsitepackages 方法得到:

import site
# 不同的平台返回的结果不同,其结果是一个路径列表
site.getsitepackages()

3.3 模块的导入过程

python的import是在程序运行期间执行的,并非像其他很多语言一样在编译期间执行。也就是说,import可以出现在任何地方,只有执行到这个import行时,才会执行导入操作。且在import某个模块之前,无法访问这个模块的属性。 示例:

# a.py
variable = 42

def print_variable():
    print(f"Value of variable in a: {variable}")
# b.py
import a

def modify_a_variable():
    a.variable = 100
# main.py
import a
import b

# Initial value of variable in a
a.print_variable()  # Output: Value of variable in a: 42

# Modify the variable in a using b
b.modify_a_variable()

# Value of variable in a after modification
a.print_variable()  # Output: Value of variable in a: 100

以b.py为例说明: import 导入模块时,搜索到模块文件a.py后

  1. 首先在内存中为每个待导入的模块构建module类的实例:模块对象,这个模块对象目前是空对象,这个对象的名称为全局变量a,因为a时全局变量,所以当前程序文件b.py中不能重新对全局变量b进行赋值,否则会使导入的模块b丢失。

  2. 构造空模块实例后,将编译、执行模块文件a.py,并按照一定的规则将一些结果放入到这个模块的对象中

  • 模块第一次被导入的时候,会进行编译,并生成.pyc字节码文件,然后python执行这个pyc文件。当模块被再次导入时,如果检查到pyc文件的存在,且和源代码文件的上一次修改时间戳mtime完全对应(也就是说,编译后源代码没有进行过修改),则直接装载这个pyc文件并执行,不会再进行额外的编译过程。当然,如果修改过源代码,将会重新编译得到新的pyc文件。

  • 在 Python 中,.pyc 文件是编译后的字节码文件,通常在首次导入某个模块时自动生成。这些文件存储在 pycache 目录中,以便加快后续的模块加载速度。然而并非所有的 .py 文件都会生成对应的 .pyc 文件。以下是一些情况和示例,说明为什么某些 .py 文件不会生成 .pyc 文件。

    • 直接执行的脚本 会将内存中的编译结果在执行完成后直接丢弃

    • 使用 PYTHONDONTWRITEBYTECODE 环境变量 如果设置了环境变量 PYTHONDONTWRITEBYTECODE,Python 将不会生成 .pyc 文件。

    • 使用 -B 选项 在运行 Python 脚本时使用 -B 选项来禁止生成 .pyc 文件

  • 执行模块文件(已完成编译)的时候,按照一般的执行流程执行:一行一行地、以代码块为单元执行。一般地,模块文件中只用来声明变量、函数等属性,以便提供给导入它的模块使用,而不应该有其他任何操作性的行为,比如print()操作不应该出现在模块文件中,但这并非强制。 总之,执行完模块文件后,这个模块文件将有一个自己的全局名称空间,在此模块文件中定义的变量、函数等属性,都会记录在此名称空间中。 最后,模块的这些属性都会保存到模块对象中。由于这个模块对象赋值给了模块变量b,所以通过变量b可以访问到这个对象中的属性(比如变量、函数等),也就是模块文件内定义的全局属性。

3.4 重载模块

无论是import还是from,都只导入一次模块,但可以使用reload进行重现装载。 reload()是imp模块中的一个函数,所以要使用imp.reload()之前,必须先导入imp。

from imp import reload
reload(b)

reload()会重新执行模块文件,但不会在内存中建立新的模块对象,所以原有模块对象中的属性可能会被修改。

3.5 模块导入细节

import导入时,模块对象中的属性有自己的名称空间,然后将整个模块对象赋值给模块变量。

  • 命名空间 命名空间(namespace)是一个从名称到对象的映射。例如,当你定义一个变量 x 时,Python 会在当前的命名空间中创建一个从名称 x 到该变量值的映射。

  • 模块的命名空间 每个模块在导入时都会创建一个新的命名空间。这个命名空间包含模块内定义的所有函数、类、变量等。由于每个模块都有自己的命名空间,因此相同的标识符可以在不同的模块中出现,而不会相互影响。 模块的独立命名空间有助于代码的模块化和避免命名冲突。你可以在不同的模块中使用相同的变量名、函数名或类名,而不必担心它们会互相覆盖或冲突。 可以通过模块名来访问模块的命名空间中的标识符。 即使动态导入模块,它们仍然会拥有自己的命名空间。 from导入模块时,会先执行完模块文件,然后将指定的部分属性重新赋值给当前程序文件的同名全局变量。 构造模块对象后,将这个模块对象对应的名称空间中的属性x、y和f重新赋值给a.py中的变量x、y和f,然后丢弃整个模块对象以及整个名称空间。换句话说,b不再是一个有效的模块变量(所以和import不一样),来自b的x,y,z,f和g也都被丢弃。 另一方面,由于模块对象一直保留在内存中,下次继续导入时,将直接使用该模块对象。对于import和from,是直接使用该已存在的模块对象,对于reload,是覆盖此模块对象。 内置函数dir可用于列出某模块中定义了哪些属性(全局名称空间),每个属性都对应一个对象,既然是对象,那么它们都会有自己的属性。 总的来说,获取对象M中一个自定义的属性age,有以下几种方法:

M.age
M.__dict__['age']
sys.modules['M'].age
getattr(M,'age')

4. 包

4.1 导入格式

导入模块时除了使用模块名进行导入,还可以使用目录名进行导入。例如,在sys.path路径下,有一个dir1/dir2/mod.py模块,那么在任意位置处都可以使用下面这种方式导入这个模块。

import dir1.dir2.mod
from dir1.dir2.mod import XXX

顶级目录dir1必须位于sys.path列出的路径搜索列表下 在 Python 3.3 之前,init.py 文件是定义包所必须的。任何目录只要包含一个 init.py 文件,Python 就会将该目录视为一个包。这个文件可以是空的,但必须存在。 从 Python 3.3 开始,引入了命名空间包(namespace package)的概念。命名空间包不需要 init.py 文件,这意味着一个目录可以没有 init.py 文件,但仍然可以被视为一个包。 命名空间包允许多个目录作为单个逻辑包的一部分。这种机制特别适合大型项目或多个分布式包,它们可能由不同的团队或模块维护。命名空间包有以下几种实现方式:

  • 使用 pkgutil 模块:在 init.py 中使用 pkgutil.extend_path。

  • 使用 pkg_resources 模块:在 init.py 中使用 pkg_resources.declare_namespace。

  • 纯命名空间包:完全不需要 init.py 文件。这是 Python 3.3 引入的特性。 包也是模块,所以能使用模块的地方就能使用包,包和模块的区别在于它们的组织形式不一样,模块可能位于包内。

4.2 关于__init__.py

空包,或者说是空模块,并不意味着它们对应的模块对象是空的,因为模块是对象,只要是对象就会有属性。 之所以称为空包,是因为它们现在仅提供了包的组织功能,而且它们是目录,而不像py文件一样,是实实在在的可以编写模块代码的地方。换句话说,包现在是目录文件,而不是真正的模块文件。 为了让包”真正的”成为模块,需要在每个包所代表的目录下加入一个__init__.py文件,它表示让这个目录格式的模块(也就是包)像py文件一样可以写模块代码,只不过这些模块代码是写入__init__.py中的。当然,模块文件中允许没有任何内容,所以__init__.py文件也可以是空文件,它仅表示让包成为真正的模块文件。 每次导入包的时候,如果有__init__.py文件,将会自动执行这个文件中的代码,就像模块文件一样,事实上它就是让目录代表的包变成模块的,甚至可以说它就是包所对应的模块文件(见下面示例),所以也可以认为__init__.py是包的初始化文件。在python3.3之前,这个文件必须存在,否则就会报错,因为它不认为目录是有效的模块。 有一项__all__是应该在__init__.py文件中定义的,它是一个列表,用来控制from package import 使用导入哪些模块文件。这里的*并非像想象中那样会导入包中的所有模块文件,而是只导出__all__列表中指定的模块文件。

4.2 关于__path__.py

严格地说,只有当某个模块设置了__path__属性时,才算是包,否则只算是模块。这是包的绝对严格定义。 path__属性是一个路径列表(可迭代对象即可,但通常用列表),和sys.path类似,该列表中定义了该包的初始化模块文件__init.py的路径。 只要导入的是一个包(无论是名称空间包还是普通包),首先就会设置该属性,默认导入目录时该属性会初始化当前目录,然后去该属性列出的路径下搜索__init__.py文件对包进行初始化。默认情况下由于__init__.py文件后执行,在此文件中可以继续定义或修改__path__属性,使得python会去找其它路径下的__init__.py对模块进行初始化。

5. 导入示例

import和from导入时有多种语法可用,这两个语句的导入方式和导入普通模块的方式是一样的:import导入时需要使用前缀名称去引用,from导入时是赋值到当前程序的同名全局变量中。 假设现在有如下目录结构,且d:\pypath位于sys.path列表中:

$ tree -f d:\pypath
d:\pypath
└── dir1
    ├── __init__.py
    └── dir2
        ├── __init__.py
        └── mod.py

只导入包:

import dir1             # 导入包dir1
import dir1.dir2        # 导入包dir1.dir2
from dir1 import dir2   # 导入包dir1.dir2

导入某个模块:

import dir1.dir2.mod
from dir1.dir2 import mod

如果dir2/init.py中设置了__all__,则下面的导入语句会导入已设置的模块:

from dir1.dir2 import *

导入模块中的属性,比如变量x:

from dir1.dir2.mod import x

6. 相对导入

如果允许,不要使用相对路径导入,很容易出错,特别是对新手而言。使用绝对路径导入,并将包放在sys.path的某个路径下就可以。

7.关于__name__

py文件分为两类,一类是用于执行的程序文件,一类是用于导入的模块文件。当直接用python xxx.py执行的是程序文件,通过from/import导入的py是模块文件。 __name__属性用来区分py文件是程序文件还是模块文件。

  • 当文件是程序文件的时候,该属性被设置为__main__

  • 当文件是模块文件的时候,该属性被设置为自身模块名 换句话说,__main__表示的是当前执行程序文件的默认模块名,程序都需要一个入口,入口程序所在的包就是main包,在main包中导入其他包来组织整个程序。python也是如此,只不过它是隐式自动设置的。 直接在模块文件中通过if name == “main”来判断,然后写属于执行程序的代码,如果直接用python执行这个文件,说明这个文件是程序文件,于是会执行属于if代码块的代码,如果是被导入,则是模块文件,if代码块中的代码不会被执行。这是python中非常方便的单元测试方式。