第二十四章 包

一、包介绍

1.什么是包?

#官网解释
Packages are a way of structuring Python’s module namespace by using “dotted module names”
包是一种通过使用‘.模块名’来组织python模块名称空间的方式。

#具体的:包就是一个包含有__init__.py文件的文件夹,所以其实我们创建包的目的就是为了用文件夹将文件/模块组织起来,例如:
pool/                #顶级包
├── __init__.py     
├── futures          #子包
│   ├── __init__.py
│   ├── process.py
│   └── thread.py
└── versions.py      #子模块

需要强调的是:

#1. 在python3中,即使包下没有__init__.py文件,import 包仍然不会报错,而在python2中,包下一定要有该文件,否则import 包报错

#2. 创建包的目的不是为了运行,而是被导入使用,记住,包只是模块的一种形式而已,包的本质就是一种模块

2.为何要使用包?

包的本质就是一个文件夹,那么文件夹唯一的功能就是将文件组织起来
随着功能越写越多,我们无法将所以功能都放到一个文件中,于是我们使用模块去组织功能,而随着模块越来越多,我们就需要用文件夹将模块文件组织起来,以此来提高程序的结构性和可维护性

3.测试

1 实验一
    准备:
        执行文件为test.py,内容
        #test.py
        import aaa
        同级目录下创建目录aaa,然后自建空__init__.py(或者干脆建包)

    需求:验证导入包就是在导入包下的__init__.py

    解决:
        先执行看结果
        再在__init__.py添加打印信息后,重新执行

2、实验二
    准备:基于上面的结果

    需求:
        aaa.x
        aaa.y
    解决:在__init__.py中定义名字x和y

3、实验三
    准备:在aaa下建立m1.py和m2.py
        #m1.py
        def f1():
            print('from 1')
        #m2.py
        def f2():
            print('from 2')
    需求:
        aaa.m1 #进而aaa.m1.func1()
        aaa.m2 #进而aaa.m2.func2()

    解决:在__init__.py中定义名字m1和m2,先定义一个普通变量,再引出如何导入模块名,强调:环境变量是以执行文件为准
    

4、实验四
    准备:在aaa下新建包bbb

    需求:
        aaa.bbb

    解决:在aaa的__init__.py内导入名字bbb

5、实验五
    准备:
        在bbb下建立模块m3.py
        #m3.py
        def f3():
            print('from 3')
    需求:
        aaa.bbb.m3 #进而aaa.bbb.m3.f3()

    解决:是bbb下的名字m3,因而要在bbb的__init__.py文件中导入名字m3,from aaa.bbb import m3

6、实验六
    准备:基于上面的结果

    需求:
        aaa.m1()
        aaa.m2()
        aaa.m3()
        进而实现
        aaa.f1()
        aaa.f2()
        aaa.f3()
        先用绝对导入,再用相对导入
        
    解决:在aaa的__init__.py中拿到名字m1、m2、m3
    包内模块直接的相对导入,强调包的本质:包内的模块是用来被导入的,而不是被执行的
    用户无法区分模块是文件还是一个包,我们定义包是为了方便开发者维护

7、实验七
    将包整理当做一个模块,移动到别的目录下,操作sys.path

接下来我们就以包pool为例来介绍包的使用,包内各文件内容如下

# process.py
class ProcessPoolExecutor:
    def __init__(self,max_workers):
        self.max_workers=max_workers

    def submit(self):
        print('ProcessPool submit')

# thread.py
class ThreadPoolExecutor:
    def __init__(self, max_workers):
        self.max_workers = max_workers

    def submit(self):
        print('ThreadPool submit')

# versions.py
def check():
    print('check versions')

# __init__.py文件内容均为空

二、包的使用

1.导入包与__init__.py

包属于模块的一种,因而包以及包内的模块均是用来被导入使用的,而绝非被直接执行,首次导入包(如import pool)同样会做三件事:

1、执行包下的__init__.py文件

2、产生一个新的名称空间用于存放__init__.py执行过程中产生的名字

3、在当前执行文件所在的名称空间中得到一个名字pool,该名字指向__init__.py的名称空间,例如http://pool.xxx和pool.yyy中的xxx和yyy都是来自于pool下的__init__.py,也就是说导入包时并不会导入包下所有的子模块与子包
import pool

pool.versions.check() #抛出异常AttributeError
pool.futures.process.ProcessPoolExecutor(3) #抛出异常AttributeError
pool.versions.check()要求pool下有名字versions,进而pool.versions下有名字check。pool.versions下已经有名字check了,所以问题出在pool下没有名字versions,这就需要在pool下的__init__.py中导入模块versions

强调

#1.关于包相关的导入语句也分为import和from ... import ...两种,但是无论哪种,无论在什么位置,在导入时都必须遵循一个原则:凡是在导入时带点的,点的左边都必须是一个包,否则非法。可以带有一连串的点,如item.subitem.subsubitem,但都必须遵循这个原则。但对于导入后,在使用时就没有这种限制了,点的左边可以是包,模块,函数,类(它们都可以用点的方式调用自己的属性)。

#2、import导入文件时,产生名称空间中的名字来源于文件,import 包,产生的名称空间的名字同样来源于文件,即包下的__init__.py,导入包本质就是在导入该文件

#3、包A和包B下有同名模块也不会冲突,如A.a与B.a来自俩个命名空间

2.绝对导入和相对导入

针对包内的模块之间互相导入,导入的方式有两种

2.1 绝对导入:以顶级包为起始

#pool下的__init__.py
from pool import versions

2.2 相对导入:.代表当前文件所在的目录,..代表当前目录的上一级目录,依此类推

#pool下的__init__.py
from . import versions

同理,针对pool.futures.process.ProcessPoolExecutor(3),则需要

#操作pool下的__init__.py,保证pool.futures
from . import futures #或from pool import futures

#操作futrues下的__init__.py,保证pool.futures.process
from . import process #或from pool.futures import process

在包内使用相对导入还可以跨目录导入模块,比如thread.py中想引用versions.py的名字check

import也能使用绝对导入,导入过程中同样会依次执行包下的__init__.py,只是基于import导入的结果,使用时必须加上该前缀

例1:

import pool.futures #拿到名字pool.futures指向futures下的__init__.py

pool.futures.xxx #要求futures下的__init__.py中必须有名字xxx

例2:

import pool.futures.thread #拿到名字pool.futures.thread指向thread.py

thread_pool=pool.futures.thread.ThreadPoolExecutor(3)
thread_pool.submit()

相对导入只能用from module import symbol的形式,import ..versions语法是不对的,且symbol只能是一个明确的名字

from pool import futures.process #语法错误
from pool.futures import process #语法正确

针对包内部模块之间的相互导入推荐使用相对导入,需要特别强调:

1、相对导入只能在包内部使用,用相对导入不同目录下的模块是非法的

2、无论是import还是from-import,但凡是在导入时带点的,点的左边必须是包,否则语法错误

2.3 绝对导入与相对导入总结

# 绝对导入: 以执行文件的sys.path为起始点开始导入,称之为绝对导入
#        优点: 执行文件与被导入的模块中都可以使用
#        缺点: 所有导入都是以sys.path为起始点,导入麻烦

# 相对导入: 参照当前所在文件的文件夹为起始开始查找,称之为相对导入
#        符号: .代表当前所在文件的文件加,..代表上一级文件夹,...代表上一级的上一级文件夹
#        优点: 导入更加简单
#        缺点: 只能在导入包中的模块时才能使用
      #注意:
        1. 相对导入只能用于包内部模块之间的相互导入,导入者与被导入者都必须存在于一个包内
        2. attempted relative import beyond top-level package # 试图在顶级包之外使用相对导入是错误的,言外之意,必须在顶级包内使用相对导入,每增加一个.代表跳到上一级文件夹,而上一级不应该超出顶级包
        
        
总结包的使用需要牢记三点
1、导包就是在导包下__init__.py文件
2、包内部的导入应该使用相对导入,相对导入也只能在包内部使用,而且...取上一级不能出包
3、
使用语句中的点代表的是访问属性
m.n.x ----> 向m要n,向n要x
而导入语句中的点代表的是路径分隔符
import a.b.c --> a/b/c,文件夹下a下有子文件夹b,文件夹b下有子文件或文件夹c
所以导入语句中点的左边必须是一个包

3.from 包 import *

在使用包时同样支持from pool.futures import * ,毫无疑问代表的是futures下__init__.py中所有的名字,通用是用变量__all__来控制代表的意思

#futures下的__init__.py
__all__=['process','thread']

最后说明一点,包内部的目录结构通常是包的开发者为了方便自己管理和维护代码而创建的,这种目录结构对包的使用者往往是无用的,此时通过操作__init__.py可以“隐藏”包内部的目录结构,降低使用难度,比如想要让使用者直接使用

import pool

pool.check()
pool.ProcessPoolExecutor(3)
pool.ThreadPoolExecutor(3)

需要操作pool下的__init__.py

from .versions import check
from .futures.process import ProcessPoolExecutor
from .futures.thread import ThreadPoolExecutor
posted @ 2022-10-11 10:32  年少纵马且长歌  阅读(20)  评论(0编辑  收藏  举报