python基础(十九):模块

一、模块介绍

在Python中,一个py文件就是一个模块,文件名为xxx.py模块名则是xxx,导入模块可以引用模块中已经写好的功能。

将程序模块化会使得程序的组织结构清晰,维护起来更加方便。比起直接开发一个完整的程序,单独开发一个小的模块也会更加简单,并且程序中的模块与电脑中的零部件稍微不同的是:程序中的模块可以被重复使用。所以总结下来,使用模块既保证了代码的重用性,又增强了程序的结构性和可维护性。另外除了自定义模块外,我们还可以导入使用内置或第三方模块提供的现成功能,这种“拿来主义”极大地提高了程序员的开发效率。

二、模块的使用

1、import语句

(1)基本用法

有如下示范文件

#文件名:foo.py
x=1
def get():
    print(x)
def change():
    global x
    x=0

要想在另外一个py文件中引用foo.py中的功能,需要使用import foo,首次导入模块会做三件事:

  • 执行源文件代码
  • 产生一个新的名称空间用于存放源文件执行过程中产生的名字
  • 在当前执行文件所在的名称空间中得到一个名字foo,该名字指向新创建的模块名称空间,若要引用模块名称空间中的名字,需要加上该前缀,如下
#本程序文件名为test.py
import foo #导入模块foo
a=foo.x #引用模块foo中变量x的内存地址赋值给当前名称空间中的名字a
foo.get() #调用模块foo的get函数
foo.change() #调用模块foo中的change函数

import foo 此代码执行后的内存图解:
在这里插入图片描述加上foo.作为前缀就相当于指名道姓地说明要引用foo名称空间中的名字,所以肯定不会与当前执行文件所在名称空间中的名字相冲突,并且若当前执行文件的名称空间中存在x,执行foo.get()或foo.change()操作的都是源文件中的全局变量x。

需要强调一点是,一个程序文件中第一次导入模块已经将其加载到内存空间了,此次导入之后的重复导入会直接引用内存中已存在的模块,不会重复执行文件,通过import sys,打印sys.modules的值可以看到内存中已经加载的模块名。

提示:

#1、在Python中模块也属于第一类对象,可以进行赋值、以数据形式传递以及作为容器类型的元素等操作。
#2、模块名应该遵循小写形式,标准库从python2过渡到python3做出了很多这类调整,比如ConfigParser、Queue、SocketServer全更新为纯小写形式。
(2)三种模块
# 三种模块的导入顺序(约定俗成)
1. python内置模块
2. 第三方模块
3. 程序员自定义模块

#例如
import sys
import time

import 第三方模块1
import 第三方模块2

import 自定义模块1
import 自定义模块2

​ 当然,我们也可以在函数内导入模块,对比在文件开头导入模块属于全局作用域,在函数内导入的模块则属于局部的作用域。

2、from … import … 语句

(1)基本用法

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

from foo import x,get,change #将模块foo中的x和get导入到当前名称空间
a=x #直接使用模块foo中的x赋值给a
get() #直接执行foo中的get函数
change() #即便是当前有重名的x,修改的仍然是源文件中的x

from foo import x,get,change此代码执行后的内存图解:
在这里插入图片描述

#文件名:foo.py
x=1
def get():
    print(x)
def change():
    global x
    x=0

#文件名:test.py
from foo import x,get,change
print(x) # 执行结果:1
change() #改变x值为0
print(x) # 执行结果:1
from foo import x
print(x) # 执行结果:0

#模块中的x值改变,并不影响test.py中的x指向1的内存地址,这两个x不是一个x了。除非重新导入模块,才会改变x的值。

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

(2)from 模块名 import *
#foo.py
__all__=['x','get'] #该列表中所有的元素必须是字符串类型,每个元素对应foo.py中的一个名字。在列表中填入,from foo import * 允许导入的此文件的名字,默认列表中全部名字都有!
x=1
def get():
    print(x)
def change():
    global x
    x=0
class Foo:
    def func(self):
       print('from the func')

#test.py文件
from foo import * #默认是把foo中所有的名字都导入到当前执行文件的名称空间中,在当前位置直接可以使用这些名字,但是你可以通过在模块中改变__all__变量中列表的值,达到控制*号导入部分名字,而不是全部名字

a=x	#可用
get() #可用
change() #不可用

3、其他导入语法(as别名)

我们还可以在当前位置为导入的模块起一个别名

import foo as f #为导入的模块foo在当前位置起别名f,以后再使用时就用这个别名f
f.x
f.get()

还可以为导入的一个名字起别名

from foo import get as get_x
get_x()

别名的优点:通常在被导入的名字过长时采用起别名的方式来1、精简代码,另外为被导入的名字2、起别名可以很好地避免与当前名字发生冲突,还有很重要的一点就是:3、可以保持调用方式的一致性,例如我们有两个模块json和pickle同时实现了load方法,作用是从一个打开的文件中解析出结构化的数据,但解析的格式不同,可以用下述代码有选择性地加载不同的模块,第3个优点的代码解释:

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) #最终调用的方式是一致的

4、循环导入问题

循环导入问题指的是在一个模块加载/导入的过程中导入另外一个模块,而在另外一个模块中又返回来导入第一个模块中的名字,由于第一个模块尚未加载完毕,所以引用失败、抛出异常,
我们以下述文件为例,来详细分析循环/嵌套导入出现异常的原因以及解决的方案

m1.py

print('正在导入m1')
from m2 import y

x='m1'

m2.py

print('正在导入m2')
from m1 import x

y='m2'

run.py

import m1
(1)测试一(采用执行run.py文件)
#1、执行run.py会抛出异常
正在导入m1
正在导入m2
Traceback (most recent call last):
  File "/Users/linhaifeng/PycharmProjects/pro01/1 aaaa练习目录/aa.py", line 1, in <module>
    import m1
  File "/Users/linhaifeng/PycharmProjects/pro01/1 aaaa练习目录/m1.py", line 2, in <module>
    from m2 import y
  File "/Users/linhaifeng/PycharmProjects/pro01/1 aaaa练习目录/m2.py", line 2, in <module>
    from m1 import x
ImportError: cannot import name 'x'

#2、分析
先执行run.py--->执行import m1,开始导入m1并运行其内部代码--->打印内容"正在导入m1"
--->执行from m2 import y 开始导入m2并运行其内部代码--->打印内容“正在导入m2”--->执行from m1 import x,由于m1已经被导入过了,所以不会重新导入,所以直接去m1中拿x,然而x此时并没有存在于m1中,所以报错
(2)测试二(采用执行两个模块文件中其中之一)
#1、执行文件不等于导入文件,比如执行m1.py不等于导入了m1
直接执行m1.py抛出异常
正在导入m1
正在导入m2
正在导入m1
Traceback (most recent call last):
  File "/Users/linhaifeng/PycharmProjects/pro01/1 aaaa练习目录/m1.py", line 2, in <module>
    from m2 import y
  File "/Users/linhaifeng/PycharmProjects/pro01/1 aaaa练习目录/m2.py", line 2, in <module>
    from m1 import x
  File "/Users/linhaifeng/PycharmProjects/pro01/1 aaaa练习目录/m1.py", line 2, in <module>
    from m2 import y
ImportError: cannot import name 'y'

#2、分析
执行m1.py,打印“正在导入m1”,执行from m2 import y ,导入m2进而执行m2.py内部代码--->打印"正在导入m2",执行from m1 import x,此时m1是第一次被导入,执行m1.py并不等于导入了m1,于是开始导入m1并执行其内部代码--->打印"正在导入m1",执行from m1 import y,由于m1已经被导入过了,所以无需继续导入而直接问m2要y,然而y此时并没有存在于m2中所以报错

总的来说:无论是测试一还是测试二,报错原因都一样,就是两个模块相互导,因为都想要对方导入模块之后的名字,因此两个模块文件都卡在了import语句哪里,永远运行不到后面的变量赋值,两个测试唯一的不同:在于直接导入模块文件而报错和执行模块文件而报错的区别

(3)循环导入报错的解决方案
# 方案一:导入语句放到最后,保证在导入时,所有名字都已经加载过
# 文件: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()

# 注意:函数解决方案,也有局限性,因为模块在函数中导入,那么模块便只能局部使用。

循环导入问题大多数情况是因为程序设计失误导致,上述解决方案也只是在烂设计之上的无奈之举,在我们的程序中应该尽量避免出现循环/嵌套导入,如果多个模块确实都需要共享某些数据,可以将共享的数据集中存放到某一个地方,然后进行导入

5、搜索模块的路径与优先级

模块其实分为四个通用类别,分别是:

1.使用纯Python代码编写的py文件

2.包含一系列模块的包

3.使用C编写并链接到Python解释器中的内置模块

4.使用C或C++编译的扩展模块
(1)模块查询及sys.path的介绍

在导入一个模块时,如果该模块已加载到内存中,则直接引用,否则会优先查找内置模块,然后按照从左到右的顺序依次检索sys.path中定义的路径,直到找模块对应的文件为止,否则抛出异常。sys.path也被称为模块的搜索路径,它是一个列表类型

>>> import sys
>>> sys.path
['', '/Library/Frameworks/Python.framework/Versions/3.7/lib/python37.zip', '/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7', '/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/lib-dynload', '/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages']

列表中的每个元素其实都可以当作一个目录来看:在列表中会发现有.zip或.egg结尾的文件,二者是不同形式的压缩文件,事实上Python确实支持从一个压缩文件中导入模块,我们也只需要把它们都当成目录去看即可

(2)sys.path的使用

sys.path中的第一个路径通常为空,代表执行文件所在的路径(执行文件执行时,None被赋值成执行文件所在的路径),所以在被导入模块与执行文件在同一目录下时肯定是可以正常导入的,而针对被导入的模块与执行文件在不同路径下的情况,为了确保模块对应的源文件仍可以被找到,需要将源文件foo.py所在的路径添加到sys.path中,假设foo.py所在的路径为/pythoner/projects/

import sys
sys.path.append(r'/pythoner/projects/') #也可以使用sys.path.insert(……)

import foo #无论foo.py在何处,我们都可以导入它了

6、区分py文件的两种用途

一个Python文件有两种用途,一种被当主程序/脚本执行,另一种被当模块导入,为了区别同一个文件的不同用途,每个py文件都内置了__name__变量,该变量在py文件被当做脚本执行时赋值为“__main__”,在py文件被当做模块导入时赋值为模块名

#foo.py
x=1
def get():
    print(x)
def change():
    global x
    x=0

if __name__ == '__main__':
    # foo.py被当做脚本执行时运行的代码,一般写测试模块功能的代码
	change()
	get()  
else:
    # foo.py被当做模块导入时运行的代码,一般这里什么都不用干
    pass

通常我们会在if的子代码块中编写针对模块功能的测试代码,这样foo.py在被当做脚本运行时,就会执行测试代码,而被当做模块导入时则不用执行测试代码。

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() #在被当做脚本执行时,执行此处的代码
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章