Python3 - 模块与包

1 简介

一个 .py 文件就为一个模块。多个 .py 文件放在同一个目录下,此外,此目录下还有一个 __init__.py 文件(这个文件可以为空,也可以写入一些初始化对象或其他操作的语句,但是必须要有,这样解释器才会将这个目录看作一个包,而非普通目录),这个目录就是一个包。多个.py文件可能会产生其中某个函数的名字冲突,但是引入了包之后,就可以通过报名来引用这些函数,从而避免冲突。

需要注意一点的是,包是一种特殊的模块。

2 几个特殊变量

通常,一个模块内,一个函数内,一个类内,有三个特殊变量,分别为__name__,__doc__,__author__。 其中,只有模块内才有__author__。而且,模块内的第一个字符串会自动赋值给这个模块的__doc__。函数定义内的第一个字符串,为这个函数的__doc__。类定义内的第一个字符串为这个类的__doc__。

其引用方式具体为:

  • 对于一个模块,它的这三个特变量的引用方式是: __name____doc____author__

  • 对于一个函数,它的这三个特殊变量的引用方式是: 函数名.__name__函数名.__doc__

  • 对于一个类,它的这三个特殊变量的引用方式是: 类名.__name__类名.__doc__

3 模块的搜索

首先,先根据 sys.modules 进行查找。它是一个字典,保存的是程序到目前为止加载的模块名字,以及模块对象(可以从中获取模块路径)。

如果查找不到,再根据 sys.path 进行查找。它返回的是一个 list,每个元素是各个目录的路径。

1
2
3
4
import sys

# ['/Users/cuckootan/Project/Test', '/usr/local/Cellar/python3/3.6.1/Frameworks/Python.framework/Versions/3.6/lib/python36.zip', '/usr/local/Cellar/python3/3.6.1/Frameworks/Python.framework/Versions/3.6/lib/python3.6', '/usr/local/Cellar/python3/3.6.1/Frameworks/Python.framework/Versions/3.6/lib/python3.6/lib-dynload', '/usr/local/lib/python3.6/site-packages']
print(sys.path)

当执行一个 .py 文件时,解释器会自动将其定义为 main 模块,同时将 执行的 .py 文件所在目录的路径 添加为 sys.path[0]

之后在搜索模块时,最先从这个 .py 文件所在的目录开始,查看这个目录下是否有所要 import 的模块文件。然后依次搜索后面的路径,直至搜索到所要 import 的模块文件或者所有的路径都搜索结束为止。

注意:第一个是执行的 .py 文件所在目录的路径,并不是工作目录的路径。工作目录是指运行脚本时所在的目录(也即是执行 python3 xxx 命令时所在的目录,或者执行 python3 进入交互式环境时所在的目录)。

sys.path 里的路径用以搜索模块,而工作目录路径用以打开文件等。

例如:假设现在正在路径为 PATH 的目录下,执行 python3 test/main.py。那么 sys.path 中的第一个路径为 PATH/test,工作目录的路径为 PATH

4 -m 参数

python3 -m 模块 参数列表

该参数用于指定要执行的模块。解释器会先从 sys.modules 查找,如果没找到,则接着从 sys.path 中查找该模块文件。由于此时的 sys.path[0] 是一个空字符串,表示执行命令时的工作目录所在路径,因此会先从工作目录开始查找。找到后,将该模块的名字定义为 main,并执行该模块。

类似于 java 中执行类文件。

常用方式如下:

  • 安装某个包: python3 -m pip install xxx
  • 查看某个文件的执行时间: python3 -m timeit xxx.py
  • 查看某个文件的汇编指令: python3 -m dis xxx.py

5 模块导入的两种语法比较

  • import 模块名 或者 import 模块名.子模块名。 如果在 sys.modules 中没有找到想要 import 的模块,那么会根据 sys.path 中的路径列表进行查找,如果所要 import 的第一个模块对应文件所在目录的路径在 sys.path 中,则搜索成功。然后将执行紧跟在 import 的模块,包括子模块(对于包,则是执行对应的 __init__.py 文件)。但它只将第一个模块名(该模块内定义的一个符号,即 __name__)纳入到当前文件的全局符号表中,因此在使用该模块里或子模块的其他某个具体的符号时,需要用: 模块名.该模块作用域中的具体符号 或者 模块名.子模块名.该子模块作用域中的具体的符号
  • from 模块名 import 该模块定义的某个具体的符号 或者 from 模块名.子模块名 import 该子模块定义的某个具体的符号

    如果在 sys.modules 中没有找到想要 import 的模块,那么会根据 sys.path 中的路径列表进行查找,如果所要查找的第一个模块对应文件所在目录的路径在 sys.path 中,则搜索成功。然后执行紧跟在后面的模块,包括子模块(对于包,则是执行对应的 __init__.py 文件)。并且,它将 import 后跟的这个具体的符号纳入到当前文件的全局符号表中,因此在使用该模块的时候,只需要直接引用这个符号即可。

官方的推荐写法是 from xxx import xxx。但是该写法在有的时候会导致冲突,比如不同的包里有相同名字的模块,而对两者均使用这个语句的时候,此时就会造成模块名冲突等问题。

6 根据相对路径导入模块

从当前模块文件所在目录 import: from . import xxx

从当前模块文件所在目录的父目录 import: from .. import xxx

从与当前模块文件处于同一目录下的模块 A 中 import: from .A import xxx

从当前模块所在目录的父目录中的 A 模块中 import: from ..A import xxx

使用该方式导入模块的前提是 . 或者 .. 对应的模块已经被加载。

常在某个包中的 init.py 中用相对路径的方式 import 该包中的模块,然后通过 sys.modules 修改模块名。这样做的好处在于对于该包中的所有模块,如果要 import 包中的其他模块,不管是在测试该模块时还是在外部调用该模块时,都只需统一地写 import xxx 即可。例如:

1
2
3
4
5
6
7
8
.
├── Pack1
│   ├── __init__.py
│   └── test1.py
└── Pack2
├── __init__.py
├── modu2.py
└── test2.py

其中,test2.py 依赖于 modu2.py。Pack2 中的 __init__.py 文件内容为:

1
2
3
4
5
import sys

from . import modu2
# modu2 被导入后的名字为 Pack2.modu2,这里为该模块添加一个新的名字 modu2,使得无论是测试 test2.py 还是在 test1 中 import test2,都只需要在 test.py 中直接 import modu2 即可。
sys.modules["modu2"] = modu2

另外,常在 init.py 中用 from .xxx import xxx 语句将所要 import 的类等提升到该包的顶层作用域中,之后外部就可以直接从该包中 import 这个类等。

7 模块导入时as的用法

假设模块按照包的方式进行组织。

由前面的内容可知,若用 import 模块名 导入一个模块,在使用这个模块里的具体的变量或函数或类时,模块路径的边界一定要为整个模块的路径。如:

1
2
3
4
import package1.package2.....packagen.module

# 以整个模块路径为模块搜索的边界,使用这个模块里的函数func()(或变量,或类)
package1.package2.....packagen.module.func()

若用 import 模块名 导入一个模块,如果模块的路径太长,可用as方法缩短。

1
2
3
4
import package1.package2.....packagen.module as nickname_module

# 此时,就以nickname_module为模块搜索的边界,使用这个模块里的函数func()(或变量,或类)。
nickname_module.func()

8 模块按照包进行组织时的导入方法

若模块按照包进行组织,则有四种不同的导入方法。一个是import内建模块或者安装的第三方包里的模块。还有一个是针对.py文件import与其在同一目录下的某个包里的某个模块,或者与其在同一目录下的某个模块。另一个则是在 .py 中用sys.path.insert()或sys.path.append()的方法,将搜索路径添加到sys.path中。这种方法对所要import的模块位置不加限制。还有一种方法是设置环境变量,将搜索路径添加到sys.path中。这种方法与在 .py 文件中添加搜索路径到sys.path的区别在于,这个方法是永久添加,对之后的所有.py文件都有效。而在.py文件中添加搜索路径到sys.path是临时性添加,一旦.py文件执行结束,所添加的路径就会删除。因而,一般不使用这种添加环境变量的方法。最为常用的是第一种和第三种。

8.1 import内建模块或安装的第三方包里的模块

  • 对于内建模块,直接

    1
    2
    3
    import 模块名

    模块名.函数名或类名

  • 对于安装的第三方包里的模块,根据相关文档里的描述进行import。

8.2 .py文件所在目录是要引用模块的当前目录或所在目录的祖先目录

  • 对于存放在与.py文件同一目录下的模块,直接

    1
    2
    3
    import 模块名

    模块名.函数名或类名

  • 若.py文件所在目录是要引用模块所在目录的祖先目录

    1
    2
    3
    import 包名1.包名2.....包名n.具体的模块名

    包名1.包名2.....包名n.具体的模块名.函数名或类名

8.3 在.py文件中添加模块所在目录的路径至sys.path中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import sys
# 所要添加路径的目录,最好是所有需要import的模块的一个祖先目录,以防同名的模块在使用时冲突
# 表示将dir_path插入到path列表中的第0个位置。也可以使用sys.path.append(dir_path),表示查到path列表尾部
sys.path.insert(0, dir_path)

# 若模块就在dir_path这个目录内
import 模块名

模块名.函数名或类名

# 若模块在dir_path这个目录的某一个子目录内。包名1为dir_path下的一个子目录。
import 包名1.包名2.....包名n.模块名

包名1.包名2.....包名n.模块名.函数名或类名

9 模块按照父子层次进行组织及其导入方法

模块按照父子层次进行组织,也就是以模块,子模块1,子模块2 ...... (每个子模块又是这样的形式)的形式,对所有模块进行组织的,这不同于以package的方式进行组织。

需要用到sys.modules组织模块。sys.modules实质上是一个字典,它保存了python解释器自启动以来的所有模块名,以及对应的模块对象(还可以从其中看到模块的绝对地址)。

要组织这种父子层次结构的模块,必须在parent module里import它的一个submodule,然后将这个submodule预先添加到sys.modules中。这样做,一方面是为了防止ImportError,另一方面是为了直接方便的对submodule独立重复的使用。

考虑我们有3个module,分别是a, b, c,前一个是后一个parent。可以这么实现。 对于a.py:

1
2
3
4
import b  
import sys
sys.modules['a.b'] = b
sys.modules['a.b.c'] = b.c

对于b.py:

1
2
3
4
# b.py  
import c
import sys
sys.modules['b.c'] = c

组织好之后,就可以直接根据这种父子组织的方式导入模块了。如下所示: 假如要在一个文件里导入模块b,则为:

1
2
3
4
5
# 只需import父模块即可
import a

# 以父子的方式引用模块中的函数func1(或其他变量,或类)。这里,由于import的是其父模块a,因而a就成为了搜索模块b的边界。
a.b.func1()

假如要在一个文件里导入模块c,可以通过import其父模块的方式,也可通过import其祖先模块(这里即为模块a)的方式。

  • 通过import其父模块的方式:

    1
    2
    3
    4
    5
    # import其父模块
    import b

    # 以父子的方式引用模块中的函数func2(或其他变量,或类)。这里,由于import的是其父模块b,因而b就成为了搜索模块c的边界。
    b.c.func2()

  • 通过import它的其他祖先模块的方式:

    1
    2
    3
    4
    5
    # import它的其他祖先模块
    import a

    # 以父子的方式引用模块中的函数func2(或其他变量,或类)。这里,由于import的是它的其他祖先模块a,因而a就成为了搜索模块c的边界。
    a.b.c.func2()

通过父子层次组织模块,简单,但是需要手动将submodule加入到sys.modules。而且。 > 有一点尤为需要注意。用父子层次进行组织的模块,只能用添加模块所在目录或者其祖先模块所在目录的路径到sys.path的方法才能成功对模块进行导入。即使是.py文件与要导入的模块所在目录的某个祖先目录处于同一个目录下,若要用父子层次进行组织,也必须是添加路径到sys.path的方法才可以,而不能用直接import模块的方法。因为后者只是针对按照包对模块进行组织时的导入模块的方法。


Reference