Python之调包侠—模块
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
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解释器的内置模块;
⼆、模块的使⽤
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。
需要强调的⼀点是,第⼀次导⼊模块已经将其加载到内存空间了,之后的重复导⼊会直接引⽤内存中已存在的模块,不会重复执⾏⽂件,通过import sys,打印sys.modules的值可以看到内存中已经加载的模块名。
提⽰:
在Python中模块也属于第⼀类对象,可以进⾏赋值、以数据形式传递以及作为容器类型的元素等操作。
模块名应该遵循⼩写形式,标准库从python2过渡到python3做出了很多这类调整,⽐如ConfigParser、Queue、SocketServer全更新为纯⼩写形式。
⽤import语句导⼊多个模块,可以写多⾏import语句:
import module1
import module2
...
import moduleN
还可以在⼀⾏导⼊,⽤逗号分隔开不同的模块:
import module1,module2,...,moduleN
但其实第⼀种形式更为规范,可读性更强,推荐使⽤,⽽且我们导⼊的模块中可能包含有Python内置的模块、第三⽅的模块、⾃定义的模块,为了便于明显地区分它们,我们通常在⽂件的开头导⼊模块,并且分类导⼊,⼀类模块的导⼊与另外⼀类的导⼊⽤空⾏隔开,不同类别的导⼊顺序如下:
Python内置模块
第三⽅模块
程序员⾃定义模块
当然,我们也可以在函数内导⼊模块,对⽐在⽂件开头导⼊模块属于全局作⽤域,在函数内导⼊的模块则属于局部作⽤域。
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() # 不可⽤
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 循环导⼊问题
循环导⼊问题指的是在⼀个模块加载/导⼊的过程中导⼊另外⼀个模块,⽽在另外⼀个模块中⼜返回来导⼊第⼀个模块中的名字,由于第⼀个模块尚未加载完毕,所以引⽤失败、抛出异常,究其根源就是在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()
注意:
循环导⼊问题⼤多数情况是因为程序设计失误导致,上述解决⽅案也只是在烂设计之上的⽆奈之举,在我们的程序中应该尽量避免出现循环/嵌套导⼊,如果多个模块确实都需要共享某些数据,可以将共
享的数据集中存放到某⼀个地⽅,然后进⾏导⼊。
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 列表中的每个元素其实都可以当作⼀个⽬录来看:在列表中会发现有.zip或.egg结尾的⽂件,⼆者是不同形式的压缩⽂件,事实上Python确实⽀持从⼀个压缩⽂件中导⼊模块,我们也只需要把它们都当
成⽬录去看即可。
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 编写⼀个规范的模块
我们在编写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() # 在被当做脚本执⾏时,执⾏此处的代码。