• 首页

  • 分类&标签

  • 归档

  • 手册

  • 项目池

  • 友链

  • 关于
伯 乐 讲 堂
伯 乐 讲 堂

查看「系列思维导图」

侠客 · Mr.潘

获取中...

08
30
小Python 起步

第 10 节 小Python 之模块与包

发表于 2020-08-30 • Python • 被 395 人看爆

如果说函数、类是对代码块的封装、重用,那么模块和包则是更高层级的封装、重用。

1. 认识模块和包

在实际项目中往往包含很多功能模块,如果我们把所有的功能都写到一个文件中显然不是一个很好的方案,这个文件可能会有几万甚至十几万行代码,显得代码很臃肿,为了方便代码阅读和维护。你可以想象如果要修改某个功能是一种什么样的场景,更不要提跟别人协作在一个文件写代码的问题了,我们可以将项目拆分为多个模块或者多个包,不同的模块或包是对不同功能的封装,这样分工合作开发效率也高,而且就算出现BUG也方便定位。

模块首先是一个含有源代码的文件,在Python里以.py结尾,文件里可以有函数的定义、变量的定义或者对象(类的实例化)的定义等等内容。如果一个项目的代码量较大,函数较多,最好把一个文件分为多个文件来管理,这样总程序脉络清晰易于维护和团队分工协作,这就是Python里存在模块的意义所在。模块名就是文件名(不含.py),例如假设有一个模块:bljt.py那么模块名为bljt。

但当一个项目的模块文件不断的增多,为了更好地管理项目,通常将功能相近相关的模块放在同一个目录下,这就是包,故包从物理结构上看对应于一个目录,一个特殊要求,包目录下必有一个空的__init__.py文件,这是包区分普通目录的标签或者说是标志。包下可以又有包称为子包,子包也是一个目录,子包的目录下也得有一个空的__init__.py文件。这样就构成了分级或者说分层的项目管理体系。

2. 模块和包的来源

2.1 内置标准模块

内置标准模块(又称标准库),Python 带有一个标准模块库,并发布有独立的文档,名为标准库参考。有一些模块内置于解释器之中,这些操作的访问接口不是语言内核的一部分,但是已经内置于解释器了。这既是为了提高效率,也是为了给系统调用等操作系统原生访问提供接口。这类模块集合是一个依赖于底层平台的配置选项。

Python 标准库非常庞大,所提供的组件涉及范围十分广泛,。这个库包含了多个内置模块 (以 C 编写),Python 程序员必须依靠它们来实现系统级功能,例如文件 I/O,此外还有大量以 Python 编写的模块,提供了日常编程中许多问题的标准解决方案。其中有些模块经过专门设计,通过将特定平台功能抽象化为平台中立的 API 来鼓励和加强 Python 程序的可移植性。

Windows 版本的 Python 安装程序通常包含整个标准库,往往还包含许多额外组件。对于类 Unix 操作系统,Python 通常会分成一系列的软件包,因此可能需要使用操作系统所提供的包管理工具来获取部分或全部可选组件。执行help('modules')查看所有Python所有自带模块列表。

2.2 第三方模块

除了内建的模块外,Python还有大量的第三方模块。基本上,所有的第三方模块都会在PyPI - the Python Package Index上注册,只要找到对应的模块名字,即可用easy_install或者pip安装。

2.3 自定义模块

除了前面两种模块,我们也可以自己写模块来供自己调用,具体实现什么功能有自己决定,在后面的模块调用中会有详细讲解,值得注意的是:模块名字不能和内置模块名字一样,会造成冲突。

3. 模块的使用

3.1 模块的引入

以Python3的内建模块datetime为例,讲解一下模块的基本使用。在新程序里使用datetime模块可以有两种方式:

方式一:引入模块,而模块里的函数的使用需要用点运算的方式来来使用。

# import 是引入模块关键字,datetime 是模块文件名,不带 .py 后缀
import datetime

# 通过 模块名.xxx 方式来调用模块内的成员(函数、变量、类)
birthday = datetime.date(2019, 12, 26)
print(birthday)

方式二:直接引入模块内的成员,使用from ... import ...方式。

# 从模块 datetime 中直接引入成员 xxx
from datetime import date,time

# 只能使用 import 后列出的成员,而模块 datetime 里的其他成员无法在本程序里使用
birthday = date(2019, 12, 26)
print(birthday)

方式二:直接引入模块内的所有成员,使用from ... import *方式。

# 从模块 datetime 中引入所有成员
from datetime import *

# datetime 模块里的所有成员在本程序里均可使用
birthday = date(2019, 12, 26)
print(birthday)

PS:使用from ... import的方式会将模块内的所有成员全部导入,非常容易与其它模块内成员或本程序中成员命名冲突,请慎用!

3.2 入口文件和模块文件

python源代码文件按照功能可以分为两种类型:

  1. 用于执行的可执行程序文件是入口文件
  2. 通过import/from方式导入的py文件是模块文件

模块的全局变量__name__属性用来区分该python文件是入口文件还是模块文件:

  • 当文件是入口文件的时候,该属性被设置为__main__
  • 当文件是模块文件的时候(也就是被导入时),该属性被设置为自身模块名

对于python来说,__name__会根据其所属文件类型,隐式自动设置该属性值:可以在模块文件中通过if __name__ == "__main__"来判断区分属于执行程序的代码,如果直接用python执行这个文件,说明这个文件是入口文件,于是会执行属于if代码块的代码,如果是被导入,则是模块文件,if代码块中的代码不会被执行。

显然,这是python中非常方便的单元测试方式。

例如,写一个模块文件,里面包含一个函数,用来求两个数的和:

def sum(a, b):
    return a + b

# 测试代码
if __name__ == "__main__":
    print(sum(10, 100))

3.3 模块的搜索路径

在导入模块的时候,python会做一系列的模块文件路径搜索操作:被导入的模块在哪里?只有找到它才能读取、运行(装载)该模块。

在任何一个python程序启动时,都会将模块的搜索路径收集到sys模块的path属性中(sys.path)。当python需要搜索模块文件在何处时,首先搜索内置模块,如果不是内置模块,则搜索sys.path中的路径列表,搜索时会从该属性列出的路径中按照从前向后的顺序进行搜索,并且只要找到就立即停止搜索该模块文件(也就是说不会后搜索的同名模块覆盖先搜索的同名模块)。

>>> import sys
>>> sys.path
['', 'D:\\Python37\\python37.zip', 'D:\\Python37\\DLLs', 'D:\\Python37\\lib', 'D:\\Python37', 'C:\\Users\\pansf\\AppData\\Roaming\\Python\\Python37\\site-packages', 'D:\\Python37\\lib\\site-packages']

python模块的搜索路径包括几个方面,按照如下顺序搜索:

  • 程序文件所在目录,即'',这个目录是最先搜索的,且是python自动搜索的,无需对此进行任何设置。
  • 环境变量PYTHONPATH所设置的路径(如果定义了该环境变量,则从左向右的顺序搜索),这个变量中可以自定义一系列的模块搜索路径列表,这样可以跨目录搜索。但默认情况下这个环境变量是未设置的。
  • 标准库路径
    • 在Linux下,标准库的路径一般是在/usr/lib/pythonXXX/下(XXX表示python版本号),此目录下有些分了子目录。
    • Windows下根据python安装位置的不同,标准库的路径不同。如果以默认路径方式安装的python,则标准库路径为D:\\Python37及其分类的子目录。
  • *.pth文件中定义的路径,可以将自定义的搜索路径放进一个.pth文件中,每行一个搜索路径。这是一种替换PYTHONPATH的友好方式,因为不同操作系统设置环境变量的方式不一样,而以文件的方式记录是所有操作系统都通用的。

PS:.pth文件不是放在sys.path中任意一个目录,而是sys.path中属于 site.getusersitepackages()和site.getsitepackages()的某一个目录中。

>>> import site
>>> site.getusersitepackages()
'C:\\Users\\pansf\\AppData\\Roaming\\Python\\Python37\\site-packages'
>>> site.getsitepackages()
['D:\\Python37', 'D:\\Python37\\lib\\site-packages']

从上可以得知,.pth 文件就可以放到以上三个目录中的任意位置,也可以放置多个.pth文件,支持绝对目和相对目录。

3.4 动态修改搜索路径

除了上面环境变量和.pth文件,还可以直接修改sys.path或者site.getsitepackages()的结果。

例如,在import导入sys模块之后,可以修改sys.path,向这个列表中添加其它搜索路径,这样之后导入其它模块的时候,也会搜索该路径。

>>> import sys
>>> sys.path.append('d:\\Modules')
>>> sys.path
['', 'D:\\Python37\\python37.zip', 'D:\\Python37\\DLLs', 'D:\\Python37\\lib', 'D:\\Python37', 'C:\\Users\\pansf\\AppData\\Roaming\\Python\\Python37\\site-packages', 'D:\\Python37\\lib\\site-packages', 'd:\\Modules']

**PS:**sys.path的最后一项将是新添加的路径。

3.5 模块被导入之后

  1. 首先在内存中为每个待导入的模块构建module类的实例:模块对象。这个模块对象目前是空对象,这个对象的名称为全局变量。
  2. 构造空模块实例后,将编译、执行模块文件,并按照一定的规则将一些结果放进这个模块对象中。

模块第一次被导入的时候,会进行编译,并生成.pyc字节码文件,然后python执行这个pyc文件。当模块被再次导入时,如果检查到pyc文件的存在,且和源代码文件的上一次修改时间戳mtime完全对应(也就是说,编译后源代码没有进行过修改),则直接装载这个pyc文件并执行,不会再进行额外的编译过程。当然,如果修改过源代码,将会重新编译得到新的pyc文件。

注意,并非所有的py文件都会生成编译得到的pyc文件,对于那些只执行一次的程序文件,会将内存中的编译结果在执行完成后直接丢弃(多数时候如此,但仍有例外,比如使用compileall模块可以强制编译成pyc文件),但模块会将内存中的编译结果持久化到pyc文件中。另外,运行字节码pyc文件并不会比直接运行py文件更快,执行它也一样是一行行地解释、执行,唯一快的地方在于导入装载的时候无需重新编译而已。

执行模块文件(已完成编译)的时候,按照一般的执行流程执行:一行一行地、以代码块为单元执行。一般地,模块文件中只用来声明变量、函数等属性,以便提供给导入它的模块使用,而不应该有其他任何操作性的行为,比如print()操作不应该出现在模块文件中,但这并非强制。

总之,执行完模块文件后,这个模块文件将有一个自己的全局名称空间,在此模块文件中定义的变量、函数等属性,都会记录在此名称空间中。

最后,模块的这些属性都会保存到模块对象中。由于这个模块对象赋值给了一个模块变量,所以通过这个模块变量可以访问到这个对象中的属性(比如变量、函数等),也就是模块文件内定义的全局属性。

3.6 使用别名

import导入时,可以使用as关键字指定一个别名作为模块对象的变量,例如:

import datetime as dt

这时候模块对象将赋值给变量dt,而不是datetime,datetime此时不再是模块对象变量,而仅仅只是模块名。使用别名并不会影响性能,因为它仅仅只是一个赋值过程,只不过是从原来的赋值对象变量datetime变为变量dt而已。

3.7 重载模块

无论时import还是from,都只导入一次模块,但使用reload()可以强制重新装载模块。reload()是imp模块中的一个函数,所以要使用imp.reload()之前,必须先导入imp。

from imp import reload

reload(datetime)

reload()是一个函数,它的参数是一个已经成功被导入过的模块变量(如果使用了别名,则应该使用别名作为reload的参数),也就是说该模块必须在内存中已经有自己的模块对象。

reload()会重新执行模块文件,并将执行得到的属性完全覆盖到原有的模块对象中。也就是说,reload()会重新执行模块文件,但不会在内存中建立新的模块对象,所以原有模块对象中的属性可能会被修改。有时候reload()很有用,可以让程序无需重启就执行新的代码。

4. 包的使用

此处,再次申明一下包的含义。当你的模块文件越来越多,就需要对模块文件进行划分,比如把负责跟数据库交互的都放一个文件夹,把与页面交互相关的放一个文件夹,为了避免模块名冲突,Python又引入了按目录来组织模块的方法,称为包(Package),包是模块的集合,比模块又高一级的封装。通俗来说,在里面一个文件夹可以管理多个模块文件,这个文件夹就被称为包。

没有比包更高级别的封装,但是包可以嵌套包,就像文件目录一样,如:

91.png

最顶层的selenium包封装了webdriver子包,webdriver包又封装了common等子包,common又有自己的子包和一系列模块。通过包的层层嵌套,我们能够划分出一个又一个的命名空间。

4.1 包的引入

先创建场景目录结构如下:92.png

导入模块时除了使用模块名进行导入,还可以使用目录名进行导入。例如,在sys.path路径下,有一个dir1/dir2/mod.py模块,那么在任意位置处都可以使用下面这种方式导入这个模块。

import dir1.dir2.mod
from dir1.dir2 import mod

PS1:在python3.3版本及更高版本是可以导入成功的,但是在python3.3之前的版本将失败,因为缺少__init__.py文件,稍后会解释该文件
PS1:顶级目录dir1的父级目录必须位于sys.path列出的路径搜索列表下

如果输出dir1和dir2,将会看到它们的是模块对象,且是名称空间。

>>> import dir1.dir2.mod
>>> dir1
<module 'dir1' (namespace)>

>>> dir1.dir2
<module 'dir1.dir2' (namespace)>

>>> dir1.dir2.mod
<module 'dir1.dir2.mod' from 'd:\\moudles\\dir1\\dir2\\mod.py'>

这种***模块+名称空间***的形式就是包(严格地说是包的一种形式),也就是说dir1是包,dir2也是包,这种方式是包的导入形式。包主要用来组织它里面的模块。

从上面的结果也可以看出,包也是模块,所以能使用模块的地方就能使用包。例如下面的代码,可以像导入模块一样直接导入包dir2,包和模块的区别在于它们的组织形式不一样,模块可能位于包内,仅此而已。

4.2 _init_.py文件

上面的dir1和dir1.dir2目前是空包,或者说是空模块(再一次强调,包就是模块)。但并不意味着它们对应的模块对象是空的,因为模块是对象,只要是对象就会有属性。例如,dir1包有如下属性:

>>> dir(dir1)
['__doc__', '__loader__', '__name__', '__package__', '__path__', '__spec__', 'dir2']

之所以称为空包,是因为它们现在仅提供了包的组织功能,而且它们是目录,而不像py文件一样,是实实在在的可以编写模块代码的地方。换句话说,包现在是目录文件,而不是真正的模块文件。

为了让包"真正的"成为模块,需要在每个包所代表的目录下加入一个__init__.py文件,它表示让这个目录格式的模块(也就是包)像py文件一样可以写模块代码,只不过这些模块代码是写入__init__.py中的。当然,模块文件中允许没有任何内容,所以__init__.py文件也可以是空文件,它仅表示让包成为真正的模块文件。

每次导入包的时候,如果有__init__.py文件,将会自动执行这个文件中的代码,就像模块文件一样,事实上它就是让目录代表的包变成模块的,甚至可以说它就是包所对应的模块文件(见下面示例),所以也可以认为__init__.py是包的初始化文件。在python3.3之前,这个文件必须存在,否则就会报错,因为它不认为目录是有效的模块。

现在,在dir1和dir2下分别创建空文件__init__.py:93.png

再去执行导入操作,并输出包dir1和dir2:

>>> import dir1.dir2.mod
>>> dir1
<module 'dir1' from 'd:\\moudles\\dir1\\__init__.py'>

>>> dir1.dir2
<module 'dir1.dir2' from 'd:\\moudles\\dir1\\dir2\\__init__.py'>

>>> dir1.dir2.mod
<module 'dir1.dir2.mod' from 'd:\\moudles\\dir1\\dir2\\mod.py'>

从输出结果中不难看出,包dir1和dir1.dir2是模块,且它们的模块文件是各自目录下的__init__.py。

实际上,包分为两种:名称空间模块、普通模块。名称空间包是没有__init__.py文件的,普通包是有__init__.py文件的。无论是哪种,它都是模块。

4.3 _init_.py的内容

上面提到**_init_.py可以被当作包所对应的模块文件**,这个文件中应该写入什么代码?答案是可以写入任何代码,我们只需把它当作一个模块对待就可以。不过,包既然是用来组织模块的,真正的功能性属性应该尽量写入到它所组织的模块文件中(也就是示例中的mod.py)。

但有一项__all__是应该在__init__.py文件中定义的,它是一个列表,用来控制from package import *指定其中的*导入哪些模块文件。这里的*并非像想象中那样会导入包中的所有模块文件,而是只导入__all__列表中指定的模块文件。

例如,在dir1.dir2包下有mod1.py、mod2.py、mod3.py和mod4.py,如果在dir2\\__init__.py文件中写入:

__all__ = ["mod1", "mod2", "mod3"]

再次执行:

from dir1.dir2 import *		# 不会导入mod4,而是只导入mod1, mod2, mod3

如果不设置__all__,则from dir1.dir2 import *不会导入该包下的任何模块,但会导入dir1和dir1.dir2。

5. 导入路径选择

5.1 绝对路径导入

无论是绝对导入还是相对导入,都需要一个参照物,不然「绝对」与「相对」的概念就无从谈起。绝对导入的参照物是项目的根文件夹。绝对导入要求我们使用从项目的根文件夹到要导入模块的完整路径。

假设我们有如下目录结构:94.png

根文件夹为project,入口程序是main.py,需要导入run.py模块,则可使用如下绝对路径导入方式:

# 前置条件:保证 project 在 sys.path 中存在
from project.mypackage.packageA import run

绝对路径要求我们必须从最顶层的文件夹开始,为每个包或每个模块提供出完整详细的导入路径。在 Python 3.x 中,绝对导入是默认的导入形式,也是 PEP 8 推荐的导入形式,它相当直观,可以很明确的知道要导入的包或模块在哪里。此外使用绝对导入的模块 (.py 文件) 其路径位置发生了改变,绝对导入依旧生效。

5.2 相对路径导入

**申明:**如果允许,不要使用相对路径导入,很容易出错,特别是对新手而言。使用绝对路径导入,并将包放在sys.path的某个路径下就可以。

依旧使用上述的下目录结构。

相对导入语法:

from . import module
from .. import module

场景一描述:main.py作为入口程序,需要导入模块a_module.py,a_module.py需要导入模块c_module.py和b_module.py,则代码如下:

运行方式:python main.py

# main.py 的内容
# main.py 作为入口程序,其所在目录(包)下所有模块都可使用相对方式成功导入
from mypackage.packageA import a_moudle


# a_module.py 的内容
# . :代表a_module.py所在的当前目录,即 packageA
# .. :代表a_module.py所在的上级目录,即 mypackage
from .packageC import c_module
from ..packageB import b_module

print("unnder packageA --> a_module")


# c_module.py 的内容
print("unnder packageC --> c_module")


# b_module.py 的内容
print("unnder packageB --> B_module")

场景二描述:run.py作为入口程序,需要导入模块c_module.py和b_module.py,则代码如下:

运行方式:python run.py

# run.py 的内容
from packageC import c_module
from ..packageB import b_module		# ValueError: attempted relative import beyond top-level package


# c_module.py 的内容
print("unnder packageC --> c_module")


# b_module.py 的内容
print("unnder packageB --> B_module")

packageB明明在mypackage下,在路径相对上,packageB确实是run.py的../packageB,但执行python run.py为什么会报错?这是因为文件系统路径并不真的代表包的相对路径,当在packageA/run.py中使用..packageB,python并不知道包mypackage的存在,因为没有将它导入,没有声明为模块变量,同样,也不知道packageB的存在,仅仅只是根据语句知道了packageB的存在。但因为使用了相对路径,不会搜索sys.path,所以它的相对路径边界只在run.py所在目录下。解决方法是显式导入它们的父包,让python记录它的存在,只有这样才能使用.、..这种路径:python -m mypackage.packageA.run

场景三描述:run.py作为入口程序,需要导入模块a_module.py和c_module.py,则代码如下:

运行方式:python run.py

# run.py 的内容
from . import a_module			# ImportError: cannot import name 'a_module' from '__main__' (packageA\run.py)
from .packageC import c_module	# ModuleNotFoundError: No module named '__main__.packageC'; '__main__' is not a package


# a_module.py 的内容
print("unnder packageC --> a_module")


# c_module.py 的内容
print("unnder packageB --> c_module")

之所以出现上述错误,是因为当py文件作为入口程序执行时,它所在的模块名为__main__,即__name__为__main__,但它并非一个包,而是一个模块文件,对它来说没有任何相对路径可言。解决方法是显式导入它们的父包,让python记录它的存在,只有这样才能使用.、..这种路径:python -m packageA.run

6. 模块引入规范

在编写 import 语句时,无论你使用使用相对导入还是绝对导入,都应该遵循 PEP 8 中提及的建议,这会让代码看起来更加优雅

  1. 导入语句应写在文件的顶部,但在模块 (.py 文件) 注释或说明文字之后
  2. 导入语句要根据导入内容的不同进行分类,一般将其分为 3 类。第一类导入 Python 内置模块;第二类导入相关的第三方库模块;第三类导入程序本地的模块 (即当前应用的模块)
  3. 导入不同类别模块时,需要使用空行分开

示例标准如下:

"""这是PEP 8建议的导入模块标准"""
# 内置模块
import os
import time

# 第三方模块
import selenium

# 本地模块
from project.package import module
标题:第 10 节 小Python 之模块与包
作者:侠客 · Mr.潘

读后有收获可以支付宝请作者喝咖啡,读后有疑问请加在下微信(pansfy)讨论:

第 11 节 小Python 之异常处理
第 9 节 小Python 的对象下
侠客 · Mr.潘

侠客 · Mr.潘

未来的你,会感谢今天仍正在奋斗的你

Github QQ Email RSS
看爆 Top10
  • 助力项目池 3,538次看爆
  • 第 4 节 yum 版的 LAMP 环境部署 2,535次看爆
  • 你的生产力工具集成就高效人士 1,880次看爆
  • 第 5 节 yum 版的 LNMP 环境部署 1,676次看爆
  • 第 3 节 企业级系统环境之上云篇 1,387次看爆
  • 服务端的架构的演进之路 1,285次看爆
  • 第 1 节 Docker 实践 1,190次看爆
  • 镜像仓库一文打尽 982次看爆
  • Docker本地私有镜像仓库Harbor搭建及配置 980次看爆
  • 第 6 节 源码版的LAMP环境部署 945次看爆

Copyright © 2023 侠客 · Mr.潘 · 苏ICP备19067937号

Proudly published with Halo · Theme by fyang · 站点地图