一文读懂!Python 模块与包,代码复用和管理的秘密武器

在 Python 编程中,模块和包是组织代码、提高代码复用性和可维护性的重要工具。本文将依据 Python 官方文档,详细介绍模块与包的相关知识,助力你快速掌握这一关键编程概念。

一、模块基础

(一)模块是什么

模块实际上就是包含 Python 定义和语句的文件,其文件名遵循 “模块名.py” 的格式。模块能够将程序拆分成多个便于管理的文件,在不同程序中,我们无需重复编写相同函数,直接调用模块中的函数即可。例如,我们可以创建一个名为fibo.py的模块,用于实现斐波那契数列相关功能。

(二)模块的导入

  1. 基本导入方式:使用import语句导入模块,如import fibo。这种方式不会将模块内定义的函数直接添加到当前命名空间,而是把模块名添加进去,之后通过模块名.函数名的形式来调用函数,像fibo.fib(1000)
  2. 导入特定函数from fibo import fib,fib2语句可以将指定的函数直接导入到当前命名空间,如此便可以直接使用函数名调用,无需加上模块名前缀,即fib(500)
  3. 导入所有非下划线开头的名称from fibo import *能够导入模块中所有不以下划线开头的名称。不过,这种方式可能会覆盖已定义的名称,导致代码可读性变差,所以在生产代码中一般不推荐使用,但在交互式会话中为减少输入量可以使用。
  4. 重命名导入:使用as关键字可以对导入的模块或函数进行重命名。比如import fibo as fib,这样导入后使用fib代替fibo来访问模块内的函数;from fibo import fib as fibonacci则是将fib函数重命名为fibonacci

二、模块进阶

(一)以脚本方式执行模块

当我们以python fibo.py <arguments>这种方式运行模块时,模块中的__name__会被赋值为"__main__"。基于这个特性,我们可以在模块末尾添加如下代码:

if __name__ == "__main__":
    import sys
    fib(int(sys.argv[1]))

这样一来,该模块既可以当作脚本直接运行,接收命令行参数执行相应功能;也能够被其他模块导入使用,且在被导入时,上述解析命令行参数的代码不会运行。这一特性常用于为模块提供便捷的用户接口或进行测试。

(二)模块搜索路径

Python 解释器在导入模块时,会按照特定顺序进行搜索。首先搜索内置模块,这些模块的名称存储在sys.builtin_module_names中。若未找到,则会在sys.path所包含的目录列表中查找对应的.py文件。sys.path的初始化包含以下几个部分:

  1. 被命令行直接运行的脚本所在的目录(若未指定文件,则为当前目录)。需要注意的是,在支持符号链接的文件系统中,是符号链接最终指向的目录被添加到搜索路径,而非符号链接所在目录。
  2. PYTHONPATH环境变量指定的目录列表(其语法与PATH环境变量类似)。
  3. 依赖于安装的默认值,通常包含site-packages目录,由site模块进行处理。

此外,Python 程序在运行过程中可以修改sys.path。由于脚本所在目录优先于标准库路径被搜索,如果脚本目录中有与标准库同名的文件,默认会加载脚本目录中的文件,这可能会引发错误,除非是有意为之。

(三)“已编译的” Python 文件

为了提高模块的加载速度,Python 会将模块的编译版本缓存到__pycache__目录中,文件命名格式为module.version.pyc,其中version用于编码编译文件格式,一般是 Python 的版本号,例如__pycache__/spam.cpython-33.pyc 。Python 会自动对比编译版与源码的修改日期,判断编译版是否过期,进而决定是否重新编译,而且编译模块与平台无关,可在不同架构的系统间共享。

不过,在两种情况下 Python 不会检查缓存:一是从命令行直接载入的模块,每次都会重新编译且不储存编译结果;二是没有源模块时,不会检查缓存。若要分发隐藏源代码的库,可以将编译后的模块放在源目录而非缓存目录,并且源目录不能包含同名的未编译源模块。另外,在 Python 命令中使用-O-OO开关,可以减小编译模块的大小,-O会去除断言语句,-OO不仅去除断言语句还会去除__doc__字符串,但使用这两个选项时需谨慎,因为有些程序可能依赖这些内容。“优化过的” 模块会带有opt -标签,文件通常会更小。compileall模块可以为一个目录下的所有模块创建.pyc文件。

三、标准模块

Python 自带了一个标准模块库,在 Python 库参考中有详细描述。部分模块内嵌到解释器中,为一些操作提供接口,这些接口有的是为了提高效率,有的是用于与操作系统底层操作交互,例如winreg模块仅在 Windows 系统上可用。

sys模块是一个特别重要的内嵌模块,其中sys.ps1sys.ps2变量分别定义了主提示符和辅助提示符,不过这两个变量仅在解释器处于交互模式时才会定义。sys.path是一个字符串列表,用于确定解释器的模块搜索路径,它会以环境变量PYTHONPATH提取的默认路径进行初始化,如果未设置PYTHONPATH,则使用内置的默认路径,并且可以通过标准列表操作对其进行修改,如sys.path.append('/ufs/guido/lib/python')

四、dir()函数

dir()是一个内置函数,主要用于查找模块定义的名称,返回的结果是一个经过排序的字符串列表。当不带参数调用dir()时,它会列出当前已定义的所有名称,包括变量、模块、函数等。需要注意的是,dir()不会列出内置函数和变量的名称,这些内容定义在标准模块builtins中,可以通过dir(builtins)来查看。例如:

import fibo, sys
print(dir(fibo))  
print(dir(sys))  
a = [1, 2, 3, 4, 5]
import fibo
fib = fibo.fib
print(dir())  
import builtins
print(dir(builtins))  

五、包

(一)包的定义与架构

包是一种通过 “带点号模块名” 来构建 Python 模块命名空间的方式,使用包可以有效避免模块名冲突,就像模块能避免全局变量名冲突一样。例如,A.B表示A包中的B子模块。

以设计一个处理声音文件和数据的包为例,其架构可能如下:

sound/                          # 最高层级的包
      __init__.py               # 初始化sound包
      formats/                  # 用于文件格式转换的子包
              __init__.py
              wavread.py
              wavwrite.py
              aiffread.py
              aiffwrite.py
              auread.py
              auwrite.py
              …
      effects/                  # 用于音效的子包
              __init__.py
              echo.py
              surround.py
              reverse.py
              …
      filters/                  # 用于过滤器的子包
              __init__.py
              equalizer.py
              vocoder.py
              karaoke.py
              …

在这个架构中,每个包或子包目录下都必须有__init__.py文件(在使用 namespace package 这种相对高级的特性时除外),该文件可以为空,也可以包含包的初始化代码或设置__all__变量。

(二)包的导入

  1. 导入子模块的多种方式
  2. 可以导入整个子模块,如import sound.effects.echo,之后需要通过全名sound.effects.echo.echofilter(input, output, delay = 0.7, atten = 4)来引用其中的函数。
  3. 使用from sound.effects import echo,这种方式也会加载echo子模块,并且在调用函数时无需加上包前缀,即echo.echofilter(input, output, delay = 0.7, atten = 4)
  4. 还能直接导入所需的函数或变量,如from sound.effects.echo import echofilter,这样echofilter()函数就可以直接使用,即echofilter(input, output, delay = 0.7, atten = 4)
  5. 从包中导入*的情况:当使用from sound.effects import *时,并不会导入包中的所有子模块。如果包的__init__.py文件中定义了__all__列表,那么运行该语句时会导入列表中的模块名;若未定义__all__,则只会确保包被导入(可能会运行__init__.py中的初始化代码),然后导入包中已定义的名称,包括__init__.py中定义的名称以及之前已显式加载的子模块。例如,若sound/effects/__init__.py中包含__all__ = ["echo", "surround", "reverse"],那么from sound.effects import *会导入这三个子模块;若在__init__.py中定义了与子模块同名的函数,如reverse函数,可能会导致子模块被遮挡而无法导入。
  6. 相对导入:在由多个子包构成的包中,模块既可以使用绝对导入引用同级包的子模块,如from sound.effects import echo;也可以使用相对导入,通过前导点号表示相对位置,如from. import echo表示从当前包导入echo模块,from.. import formats表示从上级包导入formats子包,from..filters import equalizer表示从上级包的filters子包导入equalizer模块。但需要注意的是,主模块的导入语句必须始终使用绝对导入,因为主模块名永远是"__main__"
  7. 多目录中的包:包有一个特殊属性__path__,在执行包内代码前,它会被初始化为包含__init__.py文件目录名称的字符串序列。这个变量可以被修改,修改后会影响后续对模块和包中包含的子包的搜索,虽然该功能不常用,但可用于扩展包中的模块集。

掌握模块与包的使用,能让你在 Python 编程中更好地组织和管理代码,提高开发效率和代码质量。希望本文能帮助你对 Python 模块与包有更深入的理解和运用。

作者:tekin

物联沃分享整理
物联沃-IOTWORD物联网 » 一文读懂!Python 模块与包,代码复用和管理的秘密武器

发表回复