什么是模块
在计算机程序的开发过程中,随着程序代码越写越多,在一个文件里代码就会越来越长,越来越不容易维护。为了编写可维护的代码,我们把很多函数分组,分别放到不同的文件里,这样,每个文件包含的代码就相对较少,很多编程语言都采用这种组织代码的方式。在 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 |
官方链接: