什么是模块
在计算机程序的开发过程中,随着程序代码越写越多,在一个文件里代码就会越来越长,越来越不容易维护。为了编写可维护的代码,我们把很多函数分组,分别放到不同的文件里,这样,每个文件包含的代码就相对较少,很多编程语言都采用这种组织代码的方式。在 Python 中,常规情况下一个模块就是一个包含了 Python 定义和声明的文件,文件名就是模块名字加上 .py 的后缀。
但事实上 import 加载的模块分为四个通用类别:
1、使用 Python 编写的代码(.py 文件)。
2、已被编译为共享库或 DLL 的 C 或 C++ 扩展。
3、包好一组模块的包。
4、使用C编写并链接到 Python 解释器的内置模块。
为什么要使用模块
如果你退出 Python 解释器然后重新进入,之前定义的函数或者变量都将丢失,因此我们通常将程序写到文件中以便永久保存下来,需要时就通过 python test.py 方式去执行,此时 test.py 被称为脚本(script)。
随着程序的开发和补充,功能越来越多,为了方便管理,我们通常将程序分成一个个的文件,这样做可以使程序的结构更清晰,方便管理。这时我们不仅仅可以把这些文件当做脚本去执行,还可以把他们当做模块来导入到其他的模块中,实现了功能的重复利用。我们在编写程序的时候,也经常引用其他模块,包括 Python 内置的模块和来自第三方的模块。
使用模块还可以避免函数名和变量名冲突。相同名字的函数和变量完全可以分别存在不同的模块中,因此,我们自己在编写模块时,不必考虑名字会与其他模块冲突。但是也要注意,尽量不要与内置函数名字冲突。
如何使用模块
示例文件:文件名 hello.py,模块名 hello:
1 | #!/usr/bin/env python3 |
import
模块可以包含可执行的语句和函数的定义,这些语句的目的是初始化模块,它们只在模块名第一次遇到导入 import 语句时才执行。
import 语句是可以在程序中的任意位置使用的,且针对同一个模块能 import 多次,为了防止重复导入,Python 的优化手段是:第一次导入之后就将模块名加载到内存,后续的 import 语句仅是对已经加载到内存中的模块对象增加了一次引用,不会重新执行模块内的语句,如下:
1 | #!/usr/bin/env python3 |
我们可以从 sys.modules 中找到当前已经加载的模块,sys.modules 是一个字典,内部包含模块名与模块对象的映射,该字典决定了导入模块时是否需要重新导入。
每个模块都是一个独立的名称空间,定义在这个模块中的函数,把这个模块的名称空间当作全局名称空间,这样我们在编写自己的模块时,就不用担心我们定义在自己模块中全局变量会在被导入时,与使用者的全局变量冲突。
测试一,money 与 hello.money 不冲突:
1 | #!/usr/bin/env python3 |
测试二,read1 与 hello.read1 不冲突:
1 | #!/usr/bin/env python3 |
测试三,执行 hello.change() 操作的全局变量 money 仍然是 hello.py 中的:
1 | #!/usr/bin/env python3 |
总结:
首次导入模块 hello 时会做三件事:
1、为源文件 hello.py 创建新的名称空间,在 hello 中定义的函数和方法若是使用到了 global 时访问的就是这个命名空间。
2、在新创建的命名空间中执行模块中包含的代码。
3、创建名字 hello 来引用该命名空间。
import … as …
使用 as 可以为模块起别名。
1 | #!/usr/bin/env python3 |
为已经导入的模块起别名的方式对编写可扩展的代码很有用,假设有两个模块 xmlreader.py 和 csvreader.py,它们都定义了函数 read_data(filename) 用来从文件中读取一些数据,但采用不同的输入格式。可以编写代码来选择性地挑选读取模块,例如:
1 | if file_format == 'xml': |
在一行导入多个模块也是可行的:
1 | import sys, os, re |
但从维护性上考虑,不建议使用这种导入方法。
from … import …
对比 import hello,会将源文件的名称空间 hello ‘带到当前名称空间中,使用时必须是 hello. 名字的方式。而 from 语句相当于 import,也会创建新的名称空间,但是将 hello 中的名字直接导入到当前的名称空间中,在当前名称空间中直接使用名字就可以了。
1 | from hello import read1,read2 |
这样在当前位置直接使用 read1 和 read2 就好了,执行时仍然以 hello.py 文件作为全局名称空间。
测试一,导入的函数 read1,执行时仍然回到 hello.py 中寻找全局变量 money :
1 | # test.py |
测试二,导入的函数 read2,执行时需要调用 read1(),仍然回到 hello.py 中找 read1():1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16# test.py
from hello import read2
def read1():
print('==========')
read2()
'''
执行结果:
from the hello.py
hello->read2 calling read1
hello->read1->money 1000
'''
如果当前有重名 read1 或者 read2,那么会有覆盖效果。
测试三,导入的函数 read1,被当前位置定义的 read1 覆盖掉了。
1 | # test.py |
需要特别强调的一点是:Python 中的变量赋值不是一种存储操作,而只是一种绑定关系,如下:
1 | from hello import money, read1 |
支持 as
1 | from hello import read1 as read |
也支持导入多行
1 | from hello import (money, |
from … import *
from hello import * 会把 hello 中所有的不是以下划线(_)开头的名字都导入到当前位置,大部分情况下我们的 Python 程序不应该使用这种导入方式,因为 * 不知道你想要导入什么名字,很有可能会覆盖掉之前已经定义的名字。而且可读性极其差,不过,在交互式编译器中为了节省打字可以这么用。
可以使用 __all__ 来控制 *(多用来发布新版本)。在 hello.py 中新增一行:
1 | __all__ = ['money', 'read1'] |
这样在另外一个文件中用 from hello import * 就只能导入列表中规定的两个名字。
考虑到性能的原因,每个模块只被导入一次,放入字典 sys.modules 中,如果改变了模块的内容就必须重启程序,Python 不支持重新加载或卸载之前导入的模块。
你可能会想到直接从 sys.modules 中删除一个模块不就可以卸载了吗?请注意,当删除了 sys.modules 中的模块对象,仍然可能被其他程序的组件所引用,因而不会被清除。
特别是对于我们引用了这个模块中的一个类,用这个类产生了很多对象,因而这些对象都有关于这个模块的引用。如果只是交互测试一个模块,使用 importlib.reload(), 比如 import importlib; importlib.reload(modulename),这只能用于测试环境。
把模块当做脚本执行
我们可以通过模块的全局变量 __name__来查看模块名:
- 当做脚本运行时,
__name__等于'__main__'。 - 当做模块导入时,
__name__等于 模块名。
作用:用来控制 .py 文件在不同的应用场景下执行不同的逻辑:
1 | #!/usr/bin/env python3 |
if __name__ == "__main__": 的意思是:当 .py 文件被直接运行时,if __name__ == '__main__' 之下的代码块将被运行;当 .py 文件以模块形式被导入时,if __name__ == '__main__' 之下的代码块不被运行。
模块搜索路径
Python 解释器在启动时会自动加载一些模块,可以使用 sys.modules查看。在第一次导入某个模块时(比如 hello),会先检查该模块是否已经被加载到内存中(当前执行文件的名称空间对应的内存),如果有则直接引用。如果没有,解释器则会查找同名的内建模块,如果还没有找到就从 sys.path 给出的目录列表中依次寻找 hello.py 文件。
所以总结模块的查找顺序是:内存中已经加载的模块 => 内置模块 => sys.path 路径中包含的模块。
需要特别注意的是:自定义的模块名不应该与系统内置模块重名。
sys.path 变量是一个字符串列表,用于确定解释器的模块搜索路径。该变量被初始化为从环境变量 PYTHONPATH 获取的默认路径,或者如果 PYTHONPATH 未设置,则从内置默认路径初始化。在初始化后,Python 程序可以修改 sys.path,路径放到前面的优先于标准库被加载。
1 | import sys |
搜索时按照 sys.path 中从左到右的顺序查找,位于前的优先被查找,sys.path 中还可能包含 .zip 归档文件和 .egg 文件,Python 会把 .zip 归档文件当成一个目录去处理。
至于 .egg 文件是由 setuptools 创建的包,这是按照第三方 Pthon 和扩展时使用的一种常见格式,·egg 文件实际上只是添加了额外元数据(如版本号,依赖项等)的 .zip 文件。
需要强调的一点是:只能从 .zip 文件中导入 .py,.pyc 等文件。使用 C 编写的共享库和扩展块无法直接从 .zip 文件中加载(此时 setuptools 等打包系统有时能提供一种规避方法),且从 .zip 中加载文件不会创建 .pyc 或者 .pyo 文件,因此一定要事先创建他们,来避免加载模块时性能下降。
编译 Python 文件
为了加速模块载入(提高的是加载速度而程序非运行速度),Python 在 __pycache__ 目录里缓存了每个模块的编译后版本,名称为 module.version.pyc ,通常会包含 Python 的版本号。例如,在CPython3.7 版本下,hello.py 模块会被缓存成 __pycache__/hello.cpython-37.pyc。这种命名规范保证了编译后的结果多版本共存。
Python检查源文件的修改时间与编译的版本进行对比,如果过期就需要重新编译。这是完全自动的过程。此外,编译的模块与平台无关,所以相同的库可以在不同的架构的系统之间共享,即 pyc 是一种跨平台的字节码,类似于 JAVA 、.NET,是由 Python 虚拟机来执行的,但是 pyc 的内容跟 Python 的版本相关,不同的版本编译后的 pyc 文件不同,2.5 编译的 pyc 文件不能到 3.5 上执行,并且 pyc 文件是可以反编译的,因而它的出现仅仅是用来提升模块的加载速度的。
Python 在两种情况下不会检查缓存。首先,对于从命令行直接载入的模块,它从来都是重新编译并且不存储编译结果;其次,如果源文件不存在,那么缓存的结果也不会被使用,它不会检查缓存。为了支持无源文件(仅编译)发行版本, 编译模块必须是在源目录下,并且绝对不能有源模块。
给专业人士的一些小建议:
1、模块名区分大小写,foo.py 与 FOO.py 代表的是两个模块。
2、你可以使用 -O 或者 -OO 转换 Python 命令来减少编译模块的大小。-O 开关去除(assert)断言语句,-OO 开关同时去除(assert)断言语句和 __doc__ 字符串。由于一些程序可能依赖于assert语句或文档字符串,你应该在在确认需要的情况下使用这些选项。
3、一个从 .pyc 文件读出的程序并不会比它从 .py 读出时运行的更快,.pyc 文件唯一快的地方在于载入速度。
4、只有使用 import 语句时才将文件自动编译为 .pyc 文件,在命令行或标准输入中指定运行脚本则不会生成这类文件,因而我们可以使用 compieall 模块为一个目录中的所有模块创建 .pyc 文件。
标准模块
python提供了一个标准模块库,一些模块被内置到解释器中,这些提供了不属于语言核心部分的操作的访问,但它们是内置的,无论是为了效率还是提供对操作系统原语的访问。这些模块集合是依赖于底层平台的配置项,如winreg模块只能用于windows系统。特别需要注意的是,sys模块内建在每一个python解释器
Python 附带了一个标准模块库,一些模块内置于解释器中,它们提供对不属于语言核心但仍然内置的操作的访问,以提高效率或提供对系统调用等操作系统原语的访问。这些模块的集合是一个配置选项,它也取决于底层平台。例如 winreg 模块只在 Windows 操作系统上提供。一个特别值得注意的模块 sys 它被内嵌到每一个 Python 解释器中。变量 sys.ps1 和 sys.ps2 定义用作主要和辅助提示的字符串:
1 | import sys |
这两个变量只有在编译器是交互模式下才被定义。
dir() 函数
内置函数 dir() 用于查找模块定义的名称。 它返回一个排序过的字符串列表:
1 | import hello |
如果没有参数 dir() 会列出你当前定义的名称:
1 | import hello |
注意:它列出所有类型的名称:变量,模块,函数,等等。
dir() 不会列出内置函数和变量的名称。如果你想要这些,它们的定义是在标准模块 builtins 中:
1 | import builtins |
官方链接: