模块和包

五十岚2020年6月9日
大约 9 分钟

导入模块、包和构建项目目录

模块和包

1. 模块

用一砣代码实现了某个功能的代码集合

类似于 函数式编程面向过程编程

  • 函数式编程: 也叫 无副作用 编程,它不会改变外部变量
  • 面向过程:函数的堆砌,不断调用函数来完成一个功能,提供了代码的 重用性代码间的耦合

模块分为三种

1.1 模块的调用

使用 import module 的形式,来引入模块

  • 引入标准库

    import os, sys
    import re
    
  • 引入自定义模块

    创建一个 test.py 文件

    import test
    
    # 通过如上形式,即可以引入自定义的模块,解释器通过搜索路径找到 test.py
    
  • 引入第三方模块

    下载第三方库 nmcli

    $ pip install nmcli
    

    直接引入

    import nmcli
    

使用 from module import ... 的形式,来引入模块中所需的对象

from os import environ			# 引入系统环境 变量

from test import add 			# 引入自定义 test 模块中 add 方法

from asyncio import Queue		# 引入异步标准库的 队列类
  • 若需要重命名,通常用于同名模块、方法、类等

    from fibo import fib as fibonacci
    
    # 将 fibo 模块下的 fib方法 重命名为 fibonacci
    

1.2 脚本执行模块

假设有 fibo.py 文件

"""-- fibo.py --"""

def fib(n):
	a, b = 0, 1
    while a < n:
        print(a, end=' ')
        a, b = b, a+b
    print()

可以使用如下方式运行 Python 模块

$ python fibo.py <arguments>

# 如 python fibo.py 50

会直接视为将代码添加到模块末尾 if __name__ == '__main__':

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

通常用于快捷测试,也是程序执行的入口

if __name__ == '__main__':
  • __name__ 的特性是在本模块值为 '__main__',而在其他模块声明后,则是其他模块名
  • 通常用来模块内测试代码功能,使功能模块的功能部分,和逻辑执行分离

1.3 模块搜索路径

标准库: Python 自带的标准模块,内嵌到编译器中,提供操作系统等的基本调用接口,如 sys

对于非内嵌库(自定义),当模块导入后(如: import fibo

  1. 解释器首先搜索具有 fibo 名称的内置模块,sys.builtin_module_names
  2. 若没找到,则会在 sys.path 给出的目录列表中搜索
import sys

sys.path

## 输出结果
['C:\\ProgramData\\Miniconda3\\Scripts\\ipython.exe',
 'c:\\programdata\\miniconda3\\python39.zip',
 'c:\\programdata\\miniconda3\\DLLs',
 'c:\\programdata\\miniconda3\\lib',
 'c:\\programdata\\miniconda3',
 '',
 'c:\\programdata\\miniconda3\\lib\\site-packages',
 'c:\\programdata\\miniconda3\\lib\\site-packages\\win32',
 'c:\\programdata\\miniconda3\\lib\\site-packages\\win32\\lib',
 'c:\\programdata\\miniconda3\\lib\\site-packages\\Pythonwin',
 'c:\\programdata\\miniconda3\\lib\\site-packages\\IPython\\extensions',
 'C:\\Users\\Igarashi\\.ipython']

若自定义模块无法找到(所在目录不在系统环境变量),可以修改 path 告诉编译器目录位置

sys.path.append(abs_path)
dir()

用于查找某个模块下定义的所有名称(变量、模块、函数等),无参时列出当前定义的名称

import sys, builtins

dir(sys)
## 输出结果
['__breakpointhook__', '__displayhook__', '__doc__', '__excepthook__', '__interactivehook__', '__loader__', '__name__', '__package__', '__spec__', '__stderr__', '__stdin__', '__stdout__',
 ...
 'stderr', 'stdin', 'stdout', 'thread_info', 'unraisablehook', 'version', 'version_info', 'warnoptions', 'winver']

dir(builtins)	# 查看内置函数和变量名

模块的查找顺序: 内存中加载 > 内置模块 > sys.path 路径中包含的模块

注意

Pycharm 中,可能会出现正确执行假象,IDE 将路径自动添加到了父包,但其实该路径并不在 sys.path

利用 __file__ 相对路径找绝对路径
import os
import sys

os.path.abspath(__file__)		# 把当前文件的相对路径变为绝对路径

os.path.dirname(__file__)				# 找到当前路径所在的文件夹

# 上 上 级 父路径 加入到环境变量
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))

# 同上,经常出现在配置文件中
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

1.4 Python "编译"

为了快速加载模块,Python 把模块的编译版缓存在文件名为 module.version.pyc__pycache__ 目录中, 对编译文件格式进行编码

  • version 一般为 Python 的版本号

    • 如,CPython3.3 发行版,spam.py 的编译版本缓存为 __pycache__/spam.cpython-33.pyc
    • 使用这种命名惯例,可让不同 Python 发行版,及不同版本的已编译模块,共存
  • Python 会对比编译版本与源码的 修改日期,查看它是否已过期,是否要重新编译,此过程完全自动化

  • 编译模块与平台无关,可在不同架构系统之间共享相同的支持库

提示

Python 在两种情况下不检查缓存

  1. 从命令行直接载入模块,只重新编译,不存储编译结果
  2. 没有源模块,就不会检查缓存
    • 为了支持无源文件(仅编译)发行版本, 编译模块必须在源目录下,并且绝不能有源模块

编译库 文档open in new window


2. 包

解释型语言的 并不是编译成低级语言(像 Java 编译为 .class 字节码文件)而后打包的意思,而是用 .module 构造模块命名空间的方法,是利用包按目录的形式,更加方便模块化和管理模块间的依赖

2.1 __init__.py

导入包时,Python 会搜索 sys.path 里的目录,查找包的子目录,会只将含有 __init__.py 文件的目录当成包

最简情况的 __init__.py 是一个空文件,其也可以执行包的初始化代码,或设置 __all__ 变量

此时从包中导入单个模块

import sound.effects.echo

但通常不用全名形式写,而是

from sound.effects import echo				# 导入包

from sound.effects.echo import echofilter	# 从包中导入函数或变量

当一个包被 import 时,首先加载它的 __init__.py 文件,故也可在 __init__.py 文件下进行初始化

如,在 effects 文件下的 __init__.py 引入同级目录 format 文件下 wavread.py 的所有

""" __init__.py """

from sound.formats.wavread import *

此时 from.sound.effects import * 同时会包含 formats 下的 wavread 模块

2.2 __all__ 变量

如果包的 __init__.py 代码定义了列表 __all__,运行 from package import * 时,会从 __all__ 列表中导入

  • 如作者在 sound/effects/__init__.py 下指定

    __all__ = ["echo", "surround"]
    

    意味执行 from sound.effects import * 时,会导入 echosurround 这两个子模块

简单来说就是 不想引入文件中的所有模块 或是 模块中的所有变量 而进行 限制 导入

2.3 __path__ 变量

包支持一个更特殊的属性 __path__open in new window ,在 __init__.py 文件的代码被执行前,该属性会被初始化为自身所在的目录的列表

  • 默认情况下只有一个元素,就是当前包的路径

  • 修改 __path__,就会改到此包内的搜索路径

  • 不常用,但可用于扩展包中的模块集


3. 调用解释器

Python 解释器(Linux 环境)通常安装在 /usr/local/bin/python3.10 路径下,将 /usr/local/bin 加入系统变量,即可键入 python3 启动,Windows 同理

命令行 文档open in new window

3.1 调用模块

$ python -m fibo

这种形式即可调用 module 运行指定的 package

# 若需运行某项目的 main.py 入口命令如下
$ python -m etutorservice -c etc/default.yml -r
  • -m 即执行 etutorservice 项目下的 main.py 文件,此外,还可以运行 zip 的压缩文件
  • -c 配置文件的路径,用 argparse参数解析器)来获取执行脚本需要的参数
  • -r 默认 True ,用于初始化的判断

注意

python main.pypython -m main.py 区别

  1. 前者是直接运行,后者是把模块当脚本来启动,此时 __name__ 为脚本名称,而非 __main__
  2. 影响 sys.path 中的环境变量,-m 方式默认缺少当下目录路径,直接启动会将当前路径加入环境变量中

4. 目录规范

假设项目名为 Foo,最方便快捷目录结构如下足够

Foo/
|-- bin/
|   |-- foo
|
|-- src/
|   |-- tests/
|   |   |-- __init__.py
|   |   |-- test_main.py
|   |
|   |-- __init__.py
|   |-- main.py
|
|-- docs/
|   |-- conf.py
|   |-- abc.rst
|
|-- setup.py
|-- requirements.txt
|-- README
  • bin/ 存放项目的一些 可执行文件,起名 script/ 之类的也行

  • src/ 存放项目的所有 源代码

    • 源代码中的所有模块、包都应该放在此目录,不要置于顶层目录
    • 子目录 tests/ 用来存放 单元测试 代码
    • 程序的入口通常命名为 main.py
  • docs/ 存放一些文档

  • setup.py 安装、部署、打包的脚本,项目必须,为了 快速部署 & run 起来

  • requirements.txt 存放软件依赖的外部 Python 包列表,类似前端的 package.json

    • 即可通过如下命令将所有的 Python 包依赖安装好

      $ pip install -r requirements.txt
      
  • README.md 项目说明文件,通常使用 MarkDown 形式

    1. 软件定位,软件的基本功能
    2. 运行代码的方法: 安装环境、启动命令等
    3. 简要的使用说明
    4. 代码目录结构说明,更详细点可以说明软件的基本原理
    5. 常见问题说明

    除外,如 LICENSE.txtChangeLog.txt 等,主要用来项目开源,写开源目录怎么组织,自行查阅

4.1 关于配置文件

很多项目对配置文件的使用做法是
  1. 配置文件写在一个或多个 Python 文件中,比如此处的 conf.py
  2. 项目中哪个模块用到这个配置文件就直接通过 import conf 形式在代码引入

如上做法问题

  1. 单元测试变得困难因为模块内部依赖了外部配置

  2. 配置文件作为用户控制程序的接口,应当 可由用户自由指定 该文件的路径

  3. 程序组件 可复用性太差,为这种贯穿所有模块的代码硬编码方式,使得大部分模块都依赖 conf.py 这个文件

配置使用更好的方式:
  1. 模块的配置都可 灵活配置 ,不受外部配置文件的影响
  2. 程序的配置都 灵活控制,如 NginxMySQL 这种软件的配置文件,可用户自由指定
  3. 故不应代码中 import conf 来使用配置文件

故如上目录结构中,没有将 conf.py 放在源码目录下,而是放在 docs/ 目录下,给出的一个配置样例,而不是写死程序中

可通过给 main.py 设置启动参数指定配置路径的方式来让程序读取配置内容

$ python -m program -c doc/settings.py -r
  • conf.py 可换个类似名称,如 settings.py
  • 当然,也可用其他格式来编写配置文件,如 settings.ymldefault.yml

上次编辑于: 2022/9/20 06:19:59