模块

一、模块介绍

在python中,一个py文件就是一个模块,文件名为xxx.py,模块名为xxx,导入模块可以引用模块中已经写好的功能。打个比方,如果汽车是一个完整的程序,那模块就像是汽车中的零部件,剩下的就是如何把模块按照逻辑组装成一部完整的汽车了。比起直接编写一个完整的程序,单独开发一个小模块更加简单,而且一旦模块被开发出来之后就可以重复调用;另外,模块化的程序,从结构上看更加清晰,也方便后期的维护。总结一下,使用模块开发程序避免了代码重复开发的问题,同时也使得程序的结构更加清晰易懂,增强了后期的可维护性。除了自定义的模块之外,还可以导入内置或者第三方的模块,这种社区共享的模式可以扩展程序猿的工具圈极高的提高研发效率。

二、模块使用

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")

要想在另外一个a.py文件中引用foo.py中的功能,首先应该导入模块,指令:import foo。
首次导入模块会做三件事:
1、执行源文件代码
2、产生一个新的名称空间(foo文件的名称空间)用于存放源文件执行过程中产生的名字
3、在当前执行文件所在的名称空间(a文件的名称空间)中得到一个变量名foo,该名字指向新创建的模块名称空间(foo文件的名称空间),若要引用模块名称空间中的名字,需要加上前缀,如下:

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的值可以看到内存中已经加载的模块名。
Tips:
1、在python中模块也属于第一类对象,可以进行赋值,以数据形式传递以及作为容器类型的元素等操作
2、模块名应该遵循小写形式

用import语句导入多个模块,可以写多行import语句

import module1
import module2
...
import moduleN

还可以在一行导入,用逗号分隔开不同的模块

import module1,module2,module3,.....,moduleN

推荐使用第一种方式:结构规范、可读性强,而且我们导入的模块中可能包含有python内置的模块、第三方模块、自定义模块。通常我们都在文件的开头导入模块,同时为了便于区分,不同类别的导入顺序如下:

#1、内置模块
#2、第三方模块
#3、自定义模块

除了在文件开头导入模块,我们还可以在函数中导入模块,对比在文件开头导入模块属于全局作用域,在函数内导入的模块则属于局部的作用域

2.2 from…import语句

from…import…与import语句基本一致,唯一不同的是:使用import foo导入模块后,引用模块中的名字都需要加上foo.作为前缀,而使用from foo import x,get,change,Foo则可以在当前执行文件中直接引用名字,如下:

from foo import x,get,change  # 将模块foo中的x、get、change导入到当前名称空间
a = x  # 直接使用模块foo中的x赋值给a
get()  # 直接执行foo中的get函数
change()  # 直接执行foo中的change函数

无需加前缀的好处是代码简洁,坏处是容易与当前名称空间中的名字冲突,如果当前名称空间存在相同的名字,则后定义的名字会覆盖之前定义的名字。还支持:

from foo import *  #
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( )  # 可用
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

// A code block
var foo = 'bar';

run.py

import m1

测试一

#1、执行run.py会抛出异常
正在导入m1
正在导入m2
Traceback (most recent call last):
  File "C:/Users/Administrator/PycharmProjects/Test/180days/常用模块/循环导入/run.py", line 1, in <module>
    import m1
  File "C:\Users\Administrator\PycharmProjects\Test\180days\常用模块\循环导入\m1.py", line 2, in <module>
    from m2 import y
  File "C:\Users\Administrator\PycharmProjects\Test\180days\常用模块\循环导入\m2.py", line 2, in <module>
    from m1 import x
ImportError: cannot import name 'x' from 'm1' 

#2、分析
先执行run.py--->执行import m1,开始导入m1并运行其内部代码--->打印“正在导入m1”
--->执行from m2 import y 开始导入m2并运行其内部代码--->打印“正在导入m2”--->执行from m1 import x
由于m1已经被导入过,所以不会重新导入,再去m1中拿x,然而x此时并没有存在于m1中,所以报错。

测试二

#1、执行文件不等于导入文件,比如执行m1.py不等于导入了m1
直接执行m1.py
正在导入m1
正在导入m2
正在导入m1
Traceback (most recent call last):
  File "C:/Users/Administrator/PycharmProjects/Test/180days/常用模块/循环导入/m1.py", line 2, in <module>
    from m2 import y
  File "C:\Users\Administrator\PycharmProjects\Test\180days\常用模块\循环导入\m2.py", line 2, in <module>
    from m1 import x
  File "C:\Users\Administrator\PycharmProjects\Test\180days\常用模块\循环导入\m1.py", line 2, in <module>
    from m2 import y
ImportError: cannot import name 'y' from 'm2' 

#2、分析
执行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已经被导入过了,所以无法再次导入,然而此时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
#正在导入m2
#m1
#m2
方案二:导入语句放到函数中,只有在调用函数时才会执行其内部代码
# 文件: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 搜索模块的路径与优先级

模块其实分为四个通用类别,分别是:
1、使用纯Python代码编写的py文件
2、包含一系列模块的包
3、使用C编写并链接到Python解释器中的内置模块
4、使用C或者C++编译的扩展模块
在导入一个模块时,如果该模块已经加载到内存中,则直接引用,否则会优先查找内置模块,然后按照从左到右的顺序一次检索sys.path中定义的路径,直到找到模块对应的文件为止,否则抛出异常。sys.path也称为模块的搜索路径,它是一个列表类型。

>>> import sys
>>> sys.path
['', 'C:\\Users\\Administrator\\AppData\\Local\\Programs\\Python\\Python37\\python37.zip', 'C:\\Users\\Administrator\\AppData\\Local\\Programs\\Python\\Python37\\DLLs', 'C:\\Users\\Adm
inistrator\\AppData\\Local\\Programs\\Python\\Python37\\lib', 'C:\\Users\\Administrator\\AppData\\Local\\Programs\\Python\\Python37', 'C:\\Users\\Administrator\\PycharmProjects\\Test\\
venv', 'C:\\Users\\Administrator\\PycharmProjects\\Test\\venv\\lib\\site-packages']
>>>

列表中的每个元素其实都可以当做一个目录来看:在列表中会发现有.zip或者.egg结尾的文件,二者是不同形式的压缩文件,事实上Python确实支持从一个压缩文件中导入模块,我们也只需要把它当做目录去看。
sys.path中的第一个路径通常为空,代表执行文件所在的路径,所以在被导入模块与执行文件在同一目录下肯定可以正常导入,然而针对被导入的模块与执行文件在不同路径下的情况,为了确保模块对应的源文件仍然可以被找到,需要将源文件foo.py所在的路径添加到sys.path中。

import sys
sys.path.append(r"foo.py的地址")

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文件时,需要时刻提醒自己,该文件既是给自己用的,也可能会被其他人使用,因而代码的可读性与易维护性显得十分重要,为此我们在编写一个模块时最好按照统一的规范去编写,如下:

"the module is used to..."  # 模块的文档描述
import sys  # 导入模块
x = 1  # 定义全局变量,如果非必须,则最好使用局部变量,这样可以提高代码的易维护性,并且可以节省内存提高性能
class Foo:  # 定义类,并写好类的注释
    "Class Foo is used to..."
    pass

def test():  # 定义函数,并写好函数的注释
    "Function test is used to..."

if __name__== "__main__":  # 主程序
    test()  # 在被当做脚本执行时,执行此处的代码
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值