站内链接:

模块,包,文件

关系

  1. 模块

自我包含并且有组织的代码片段

package 是一个有层次目录结构, 包含子包和模块的 Python 应用程序执行环境, 一种特殊的模块. 任何包含__path__(包所在的路径)属性的对象都可以称为 package: django.__path__

他们之间的关系如下:

  • file—物理组织
  • module—逻辑组织
  • package—执行环境与目录结构(__init__.py)

绝对和相对导入

  • 全限定名: 模块的绝对路径含义, 确保 python 虚拟机在寻找某一个 module 时, 根据__package__ + . + __name__来定位.

  • load: python interpreter 对于 import 和 running 使用两种不同的加载方式进行区分, 其会阻止后者进行相对导入工作. 换言之, 如果允许后者能够进行相对导入工作, 那么 PYTHONPATH,sys.path其中没有那么大的必要, 这样会影响代码的整体组织结构.

  • path: 对于foo.bar.baz路径, 导入逻辑或者步骤: foo --> foo.bar --> foo.bar.baz

python 的导入不同于 java 的 import, 后者仅仅是告知 Java 编译器需要特定的包, 这是大部分人的理解, 但是 python 的的首次 import 会执行导入模块的所有顶层代码, 甚至可以做所有running运行时才能够做到的事情, 关于importrunning的说明和区别见下文说明.

包相关术语

模块(module)是 Python 中代码可重用性的基本单元, 模块分为三个类型: 纯 python 模块, 扩展模块, 包

  • pure python module: 用 Python 编写并包含在单个.py或.pyc文件中的模块
  • extension module: 用 python 实现的低级语言编写的模块, 比如c/c++ for python, java for python, 其通常包含在单个可动态加载的预编译文件中, 例如 so 文件
  • package: 包含其他模块的模块

另外, 还有一个root package的概念, 表示包层次结构的, 这并非真正的包, 因为其不包含__init__.py.

模块导入方法

导入流程

路径导入流程:

  • 每一次 import 的路径搜索流程: sys.modules-->current directory module-->sys.path, 其中sys.modules包含了 python 开始运行其所有已经导入的模块
  • 每一次 import one package 的过程实际就是执行__init__.py 的过程
  • 每一次 execute init 的过程: 如果没有在__init__.py 中导入的话, 不会默认加载 module_name 目录下的其他模块.

模块导入是一个延迟导入, 按需导入的过程, 所以从性能上讲, 没必要将所有的子模块都在__init__.py 中导入, 除非尽可能的低耦合. 对于字符串描述的模块路径, 使用 importlib 导入: importlib.import_module('math'), 相反, 对于相对导入, 该语句必定是在 package 中: importlib.import_module('.b', package__)

导入术语

  1. 已导入模块缓存
  • sys.modules存放当前导入的所有模块的缓存信息, 每一次 finder 开始搜索模块时都会先搜索该缓存, 如果发现则直接返回.
  • sys.modules是一个字典, 其是可写的, 其中reload(), importlib.reload()会重新执行该缓存中已有的某个模块.
  1. Finder and Loader

finder 的动作在搜索sys.module之后, finder 根据所知的所有策略, 找到指定的模块(定位内置模块), 判断是否冻结模块, 并获取模块的导入路径, 模块相关元信息, 返回 loader.

  • 其中导入路径可以为: 文件系统路径, 文件系统压缩文件, URLs 识别的资源.
  • 查找器有多个, 扩展自定义的 finder 可以扩展模块的搜索范围和广度, 见 3.4 节说明.

loader: 根据 finder 返回的信息进行加载操作并执行–模块执行

1
2
3
4
5
6
7
8
# 1 load module(存放-设置)
- 是否在sys.module中存在
- 将 moduleA 放入模块缓存sys.modules中, 以避免模块代码的自我导入(间接或者直接)
- 设置moduleA的属性
- 涉及函数importlib.abc.Loader.load_module(), 该方法在3.4中被直接一步到位替换为exec_module()

# 2 execute module(运行)
- 执行模块本身代码
  1. 导入钩子程序

扩展 finder 的基础就是: 导入钩子程序, 其分类如下

  • 元钩子程序
  • 导入路径钩子程序

其中元钩子程序是一个内建模块, 其覆盖sys.path, 我们可以使用sys.meta_path完成元钩子注册. 如果希望导入路径钩子程序, 则需要使用sys.path 完成扩展注册.

导入和运行时

导入流程

导入一个包import pa时会执行的代码流程如下:

  • 1 执行 pa 模块中的所有顶层代码

  • 2 执行 pa 模块中的导入的其他模块的顶层代码, 其中 a 和 b 步骤是按照代码顺序往下执行的

  • 3 在执行顶层代码时: 若碰到类定义则执行类的定义体, 若碰到函数则编译函数, 若碰到装饰器则执行装饰器函数定义体

  • 4 执行类定义体的时候

    • a. 若执行嵌套类, 则会执行嵌套类的定义体, 之后规则就会重复 4, 一直递归到不存在嵌套类
    • b. 若类被装饰, 则会在执行完类定义体之后再执行装饰器函数定义体
  • 5 执行装饰器函数定义体, 若是嵌套函数则仅仅编译函数

上面所有情况的实例代码bambooimport.py如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
print('执行顺序1: 顶层代码print')


def deco_bamboo(cls):
""" 装饰器 """
print('执行顺序: 装饰器函数定义体print')

def inner(self):
print('inner')

cls.method_inner = inner
return cls


class BambooClass:
print('执行顺序2: 类定义体')

def __init__(self):
self.name = 'bamboo'

class BambooInnerClass:
print('执行顺序3: 内嵌类定义体')

def show(self):
print(f'show:{self.name}')

则执行import bambooimport时的输出如下:

1
2
3
4
In [1]: import bambooimport
执行顺序1: 顶层代码print
执行顺序2: 类定义体
执行顺序3: 内嵌类定义体

那么, 运行时流程同导入流程有什么区别呢? 运行时会真正执行函数定义中的代码, 对于入口 main 还会执行__main__中的代码, 这些代码在导入时是不会执行的.

模块名

对于上面的代码, 在顶层代码中增加一行print(f"此时 __name__ 变量的值是:{repr(__name__)}"), 则导入和运行时他们的输出是不同的

1
2
3
4
# 1. 运行时
此时 __name__ 变量的值是:'__main__'
# 2. 导入时, 此时__name__值变为模块名
此时 __name__ 变量的值是:'bambooimport'

另外, 因为模块缓存的存储, 重复import bambooimport并没有输出, 而python bambooimport.py会一直重复输出.

参考: