Python之调包侠—模块
一、模块介绍
在Python中,一个py文件就是一个模块,文件名为xxx.py模块名就是xxx,导入模块可以引用模块中已经写好的功能。如果把开发程序比喻成制造一台电脑,编写模块就像是在制造电脑的零部件,准备好零部件后,剩下的工作就是按照逻辑把它们组装到一起。
将程序模块化会使得程序的组织结构清晰,维护起来更加方便。比起直接开发一个完整的程序,单独开发一个小的模块也会更加简单,并且程序中的模块与电脑中的零部件稍微不同的是:程序中的模块可以被重复使用。
所以总结下来,使用模块既保证了代码的重用性,又增强了程序的结构性和可维护性。另外除了自定义模块外,我们还可以导入使用内置或第三方模块提供的现成功能,这种“拿来主义”极大地提高了程序员的开发效率。
1.1 什么是模块?
一个模块就是一个包含了Python定义和声明的文件,文件名就是模块名字加上.py的后缀。
1.2 为什么要用模块
如果你退出Python解释器然后重新进入,那么你之前定义的函数或者变量都将丢失,因此我们通常将程序写到文件中以便永久保存下来,需要时就通过python test.py方式去执行,此时test.py被称为脚本script。
-
从文件级别组织程序,更方便管理:
随着程序的发展,功能越来越多,为了方便管理,我们通常将程序分成一个个的文件,这样做程序的结构更清晰,方便管理。这时我们不仅仅可以把这些文件当做脚本去执行,还可以把它们当做模块来导入到其他的模块中,实现了功能的重复利用。
-
拿来主义,提升开发效率:
同样的原理,我们也可以下载别人写好的模块然后导入到自己的项目中使用,这种拿来主义,可以极大地提升我们的开发效率。
1.3 模块三种来源
- 内置的(python解释器自带能够直接导入使用);
- 第三方的(别人写好的发布在网上的 需要先下载后使用);
- 自定义的(自己写的);
1.4 模块的四种表现形式
- 使用python编写的代码(.py文件);
- 已被编译为共享库或DLL的C或C++扩展;
- 包好一组模块的包(文件夹);
- 使用C编写并链接到python解释器的内置模块;
![image-20211123152823970](https://img2020.cnblogs.com/blog/2319293/202111/2319293-20211123185836056-1041813226.png)
二、模块的使用
2.1 import语句
有如下示范文件:
文件名:foo.py
x = 1
def get():
print(x)
def change():
global x
x = 0
class Foo:
def func(self):
print('from the func')
要想在另外一个py文件中引用foo.py中的功能,需要使用import foo,首次导入模块会做四件事:
- 执行源文件代码产生该文件的全局名称空间,用于存放源文件执行过程中产生的名字;
- 运行foo.py文件
- 产生foo.py全局名称空间 运行foo文件内代码 将产生的名字全部存档于foo.py名称空间
- 在导入文件名称空间产生一个foo的名字指向foo.py全局名称空间,若要引用模块名称空间中的名字,需要加上该前缀,如下:
import foo # 导入模块foo
a = foo.x # 引用模块foo中变量x的值赋值给当前名称空间中的名字a
foo.get() # 调用模块foo的get函数
foo.change() # 调用模块foo中的change函数
obj = foo.Foo() # 使用模块foo的类Foo来实例化,进一步可以执行obj.func()
加上foo.作为前缀就相当于指名道姓地说明要引用foo名称空间中的名字,所以肯定不会与当前执行文件所在名称空间中的名字相冲突,并且若当前执行文件的名称空间中存在x,执行foo.get()或foo.change()操作的都是源文件中的全局变量x。
![image-20211123155954605](https://img2020.cnblogs.com/blog/2319293/202111/2319293-20211123190009714-1834537931.png)
需要强调的一点是,第一次导入模块已经将其加载到内存空间了,之后的重复导入会直接引用内存中已存在的模块,不会重复执行文件,通过import sys,打印sys.modules的值可以看到内存中已经加载的模块名。
提示 :
- 在Python中模块也属于第一类对象,可以进行赋值、以数据形式传递以及作为容器类型的元素等操作。
- 模块名应该遵循小写形式,标准库从python2过渡到python3做出了很多这类调整,比如ConfigParser、Queue、SocketServer全更新为纯小写形式。
用import语句导入多个模块,可以写多行import语句:
import module1
import module2
...
import moduleN
还可以在一行导入,用逗号分隔开不同的模块:
import module1,module2,...,moduleN
但其实第一种形式更为规范,可读性更强,推荐使用,而且我们导入的模块中可能包含有Python内置的模块、第三方的模块、自定义的模块,为了便于明显地区分它们,我们通常在文件的开头导入模块,并且分类导入,一类模块的导入与另外一类的导入用空行隔开,不同类别的导入顺序如下:
- Python内置模块
- 第三方模块
- 程序员自定义模块
当然,我们也可以在函数内导入模块,对比在文件开头导入模块属于全局作用域,在函数内导入的模块则属于局部作用域。
![image-20211123161436678](https://img2020.cnblogs.com/blog/2319293/202111/2319293-20211123190051983-1341073519.png)
2.2 from…import句式
from…import…与import语句基本一致,唯一不同的是:使用import foo导入模块后,引用模块中的名字都需要加上foo.作为前缀,而使用from foo import x, get, change, Foo则可以在当前执行文件中直接引用模块foo中的名字,如下:
from foo import x, get, change # 将模块foo中的x,get和change导入到当前名称空间
a = x # 直接使用模块foo中的x赋值给a
get() # 直接执行foo中的get函数
change() # 即便是当前有重名的x,修改的仍然是源文件中的x
无需加前缀的好处是使得我们的代码更加简洁,坏处则是容易与当前名称空间中的名字冲突,如果当前名称空间存在相同的名字,则后定义的名字会覆盖之前定义的名字。
另外from语句支持from foo import * 语法,代表将foo中所有的名字都导入到当前位置:
from foo import * """把foo中所有的名字都导入到当前执行文件的名称空间中,在当前位置直接可以使用这些名字"""
a = x
get()
change()
obj = Foo()
如果我们需要引用模块中的名字过多的话,可以采用上述的导入形式来达到节省代码量的效果,但是需要强调的一点是:只能在模块最顶层使用 * 的方式导入,在函数内则非法,并且 * 的方式会带来一种副作用,即我们无法搞清楚究竟从源文件中导入了哪些名字到当前位置,这极有可能与当前位置的名字产生冲突。
模块的编写者可以在自己的文件中定义_ _ all _ _变量用来控制 * 代表的意思:
"""foo.py"""
__all__ = ['x', 'get'] """该列表中所有的元素必须是字符串类型,每个元素对应foo.py中的一个名字"""
x = 1
def get():
print(x)
def change():
global x
x = 0
class Foo:
def func(self):
print('from the func')
这样我们在另外一个文件中使用 * 导入时,就只能导入_ _ all _ _定义的名字了:
from foo import * # 此时的*只代表x和get
x # 可用
get() # 可用
change() # 不可用
Foo() # 不可用
![image-20211123162306643](https://img2020.cnblogs.com/blog/2319293/202111/2319293-20211123190134643-111666991.png)
2.3 其他导入语法(as)
我们还可以在当前位置为导入的模块起一个别名:
import foo as f """为导入的模块foo在当前位置起别名f,以后再使用时就用这个别名f"""
f.x
f.get()
还可以为导入的一个名字起别名:
from foo import get as get_x
get_x()
通常在被导入的名字过长时采用起别名的方式来精简代码,另外为被导入的名字起别名可以很好地避免与当前名字发生冲突,还有很重要的一点就是:可以保持调用方式的一致性,例如我们有两个模块json和pickle同时实现了load方法,作用是从一个打开的文件中解析出结构化的数据,但解析的格式不同,可以用下述代码有选择性地加载不同的模块:
if data_format == 'json':
import json as serialize """如果数据格式是json,那么导入json模块并命名为serialize"""
elif data_format == 'pickle':
import pickle as serialize """如果数据格式是pickle,那么导入pickle模块并命名为serialize"""
data = serialize.load(fn) # 最终调用的方式是一致的
2.4 循环导入问题
![image-20211123163148148](https://img2020.cnblogs.com/blog/2319293/202111/2319293-20211123190205976-1294776501.png)
循环导入问题指的是在一个模块加载/导入的过程中导入另外一个模块,而在另外一个模块中又返回来导入第一个模块中的名字,由于第一个模块尚未加载完毕,所以引用失败、抛出异常,究其根源就是在Python中,同一个模块只会在第一次导入时执行其内部代码,再次导入该模块时,即便是该模块尚未完全加载完毕也不会去重复执行内部代码。
我们以下述文件为例,来详细分析循环/嵌套导入出现异常的原因以及解决的方案
m1.py:
print('正在导入m1')
from m2 import y
x = 'm1'
m2.py:
print('正在导入m2')
from m1 import x
y = 'm2'
run.py:
import m1
测试一
-
执行run.py会抛出异常:
正在导入m1 正在导入m2 Traceback (most recent call last): File "C:/Program Files/PycharmProjects/Python数据结构+算法/模块与包/run.py", line 1, in <module> import m1 File "C:\Program Files\PycharmProjects\Python数据结构+算法\模块与包\m1.py", line 2, in <module> from m2 import y File "C:\Program Files\PycharmProjects\Python数据结构+算法\模块与包\m2.py", line 2, in <module> from m1 import x ImportError: cannot import name 'x' from 'm1'
-
分析:
--->先执行 run.py ;
--->执行 import m1 ,开始导入 m1 并运行其内部代码;
--->打印内容 "正在导入m1" ;
--->执行 from m2 import y ,开始导入 m2 并运行其内部代码;
--->打印内容“正在导入m2”;
--->执行 from m1 import x ,由于 m1 已经被导入过了,所以不会重新导入,所以直接去 m1 中拿 x ,然而 x 此时并没有存在于 m1 中,所以报错。
测试二
-
执行文件不等于导入文件,比如执行m1.py不等于导入了m1:
直接执行m1.py抛出异常:
正在导入m1 正在导入m2 正在导入m1 Traceback (most recent call last): File "C:/Program Files/PycharmProjects/Python数据结构+算法/模块与包/m1.py", line 2, in <module> from m2 import y File "C:\Program Files\PycharmProjects\Python数据结构+算法\模块与包\m2.py", line 2, in <module> from m1 import x File "C:\Program Files\PycharmProjects\Python数据结构+算法\模块与包\m1.py", line 2, in <module> from m2 import y ImportError: cannot import name 'y' from 'm2'
-
分析:
--->执行 m1.py ,打印“正在导入m1”;
--->执行 from m2 import y ,导入 m2 进而执行 m2.py 内部代码;
--->打印 "正在导入m2" ,执行 from m1 import x ,此时 m1 是第一次被导入,执行 m1.py 并不等于导入了 m1 ,于是开始导入 m1 并执行其内部代码;
--->打印 "正在导入m1" ,执行 from m2 import y ,由于m2已经被导入过了,所以无需继续导入而直接问 m2 要 y ,然而 y 此时并没有存在于 m2 中所以报错。
-
解决方案:
方案一:导入语句放到最后,保证在导入时,所有名字都已经加载过
# 文件:m1.py print('正在导入m1') x = 'm1' from m2 import y # 文件:m2.py print('正在导入m2') y = 'm2' from m1 import x # 文件:run.py内容如下,执行该文件,可以正常使用 import m1 print(m1.x) print(m1.y)
方案二:导入语句放到函数中,只有在调用函数时才会执行其内部代码
# 文件:m1.py print('正在导入m1') def f1(): from m2 import y print(x, y) x = 'm1' # 文件:m2.py print('正在导入m2') def f2(): from m1 import x print(x, y) y = 'm2' # 文件:run.py内容如下,执行该文件,可以正常使用 import m1 m1.f1()
注意:
循环导入问题大多数情况是因为程序设计失误导致,上述解决方案也只是在烂设计之上的无奈之举,在我们的程序中应该尽量避免出现循环/嵌套导入,如果多个模块确实都需要共享某些数据,可以将共享的数据集中存放到某一个地方,然后进行导入。
![image-20211123171737650](https://img2020.cnblogs.com/blog/2319293/202111/2319293-20211123190322553-652258187.png)
2.5 搜索模块的路径和优先级
前面介绍过,模块其实分为四个通用类别,分别是:
- 使用纯Python代码编写的py文件;
- 包含一系列模块的包;
- 使用C编写并链接到Python解释器中的内置模块;
- 使用C或C++编译的扩展模块;
在导入一个模块时,如果该模块已加载到内存中,则直接引用,否则会优先查找内置模块,然后按照从左到右的顺序依次检索sys.path中定义的路径,直到找模块对应的文件为止,否则抛出异常。sys.path也被称为模块的搜索路径,它是一个列表类型:
import sys
print(sys.path)
结果为:
['C:\\Program Files\\PycharmProjects\\Python数据结构+算法\\模块与包', 'C:\\Program Files\\PycharmProjects\\Python数据结构+算法', 'C:\\Program Files\\PyCharm\\PyCharm 2020.3.2\\plugins\\python\\helpers\\pycharm_display', 'C:\\Program Files\\Python3.7\\python37.zip', 'C:\\Program Files\\Python3.7\\DLLs', 'C:\\Program Files\\Python3.7\\lib', 'C:\\Program Files\\Python3.7', 'C:\\Users\\Administrator\\AppData\\Roaming\\Python\\Python37\\site-packages', 'C:\\Program Files\\Python3.7\\lib\\site-packages', 'C:\\Program Files\\PyCharm\\PyCharm 2020.3.2\\plugins\\python\\helpers\\pycharm_matplotlib_backend']
列表中的每个元素其实都可以当作一个目录来看:在列表中会发现有.zip或.egg结尾的文件,二者是不同形式的压缩文件,事实上Python确实支持从一个压缩文件中导入模块,我们也只需要把它们都当成目录去看即可。
![image-20211123172908132](https://img2020.cnblogs.com/blog/2319293/202111/2319293-20211123190355453-1604051032.png)
sys.path中的第一个路径代表执行文件所在的路径,所以在被导入模块与执行文件在同一目录下时肯定是可以正常导入的,而针对被导入的模块与执行文件在不同路径下的情况,为了确保模块对应的源文件仍可以被找到,需要将源文件foo.py所在的路径添加到sys.path中,假设foo.py所在的路径为/pythoner/projects/
import sys
sys.path.append(r'/pythoner/projects/') # 也可以使用sys.path.insert(...)
import foo # 无论foo.py在何处,我们都可以导入它了
2.6 区分py文件类型的两种用途
一个Python文件有两种用途,一种被当主程序/脚本执行,另一种被当模块导入,为了区别同一个文件的不同用途,每个py文件都内置了_ _ name _ _变量,该变量在py文件被当做脚本执行时赋值为“ _ _main _ _ ”,在py文件被当做模块导入时赋值为模块名:
作为模块foo.py的开发者,可以在文件末尾基于_ _ name _ _在不同应用场景下值的不同来控制文件执行不同的逻辑:
# foo.py
...
if __name__ == '__main__':
foo.py被当做脚本执行时运行的代码
else:
foo.py被当做模块导入时运行的代码
通常我们会在if的子代码块中编写针对模块功能的测试代码,这样foo.py在被当做脚本运行时,就会执行测试代码,而被当做模块导入时则不用执行测试代码。
2.7 编写一个规范的模块
![img](https://img2018.cnblogs.com/blog/1825659/201910/1825659-20191010170011274-1650337086.jpg)
我们在编写py文件时,需要时刻提醒自己,该文件既是给自己用的,也有可能会被其他人使用,因而代码的可读性与易维护性显得十分重要,为此我们在编写一个模块时最好按照统一的规范去编写,如下:
#!/usr/bin/env python # 通常只在类unix环境有效,作用是可以使用脚本名来执行,而无需直接调用解释器。
"The module is used to..." # 模块的文档描述
import sys # 导入模块
x = 1 """定义全局变量,如果非必须,则最好使用局部变量,这样可以提高代码的易维护性,并且可以节省内存提高性能"""
class Foo: # 定义类,并写好类的注释
'Class Foo is used to...'
pass
def test(): # 定义函数,并写好函数的注释
'Function test is used to…'
pass
if __name__ == '__main__': # 主程序
test() # 在被当做脚本执行时,执行此处的代码