Skip to content

Latest commit

 

History

History
389 lines (263 loc) · 11.8 KB

File metadata and controls

389 lines (263 loc) · 11.8 KB

模块

在项目开发的过程中,随着程序代码越写越多,在一个文件中的代码就会越来越长,也就会越来越难以维护

为了编写可维护性的代码,我们会把很多函数进行分组,分别放到不同的文件中,这样,每个文件中包含的代码就会相对较少。

一、概念

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

在模块中,当前模块中所有定义的函数或者变量,都属于当前的module,这个模块相对于当前的所有函数和变量 就是一个全局的命名空间,而每个函数又有自己的命名空间(作用域)。

优点

  • 提高了代码的可维护性
  • 提高了代码的复用度 编写代码不必从零开始 当一个模块编写完成 就可以在其他位置进行引用
  • 避免函数名和变量名的冲突

模块分类

  • 内置模块
  • 第三方模块
  • 自定义模块

二、模块的导入方式

2.1、import 语句

# 导入模块
import module

# 用import语句导入多个模块,可以写多行import语句
import module1
import module2
    ...
import moduleN

# 还可以在一行导入,用逗号分隔开不同的模块
import module1,module2,...,moduleN

但其实第一种形式更为规范,可读性更强,推荐使用。

我们导入的模块中可能包有python内置的模块、第三方的模块、自定义的模块,为了便于明显地区分它们,我们通常在文件的开头导入模块,并且分类导入,一类模块的导入与另外一类的导入用空行隔开,不同类别的导入顺序如下:

#1. python内置模块
#2. 第三方模块
#3. 程序员自定义模块

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

提示:

?> 1、在Python中模块也属于第一类对象,可以进行赋值、以数据形式传递以及作为容器类型的元素等操作。

?> 2、一个模块只会被导入一次,之后的重复导入会直接引用内存中已存在的模块,不会重复执行文件

2.2、from...import... 语句

from...import...与import语句基本一致,唯一不同的是:使用import导入模块后,引用模块中的名字都需要加上module.作为前缀,而使用 from module 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文件中导入

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

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

2.3、import...as... / from...import...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、from...import * 语句

from语句支持from foo import *语法,*代表将foo中所有的名字都导入到当前位置

from foo import * #把foo中所有的名字都导入到当前执行文件的名称空间中,在当前位置直接可以使用这些名字

a=x
get()
change()
obj=Foo()

如果我们需要引用模块中的名字过多的话,可以采用上述的导入形式来达到节省代码量的效果。

!> 需要强调的一点是:只能在模块最顶层使用的方式导入,在函数内则非法。

2.5、接口暴露_all_

from...import * 这种方式会带来一种副作用,即我们无法搞清楚究竟从源文件中导入了哪些名字到当前位置,这极有可能与当前位置的名字产生冲突。

模块的编写者可以在自己的文件中定义__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.6 _name_ 属性

一个Python文件有两种用途,一种被当主程序/脚本执行,另一种被当模块导入。

为了区别同一个文件的不同用途,每个py文件都内置了__name__ 变量,该变量在py文件被当做脚本执行时赋值为__main__ ,在py文件被当做模块导入时赋值为模块名

一般作为主程序执行来使用

'''
main.py
'''

def main():
    pass

if __name__ == '__main__':
    main()

三、模块的使用

3.1、使用内置模块

>>> import random
>>> print(random.randrange(0,10))

3.2、使用自定义模块

有如下示范文件

#文件名: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,首次导入模块会做三件事:

1、执行源文件代码

2、产生一个新的名称空间用于存放源文件执行过程中产生的名字

3、在当前执行文件所在的名称空间中得到一个名字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()

3.3 循环导入问题

循环导入问题指的是在一个模块加载/导入的过程中导入另外一个模块,而在另外一个模块中又返回来导入第一个模块中的名字,由于第一个模块尚未加载完毕,所以引用失败、抛出异常,究其根源就是在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

测试一

#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中所以报错

测试二

#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中所以报错

解决方案

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

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

3.4、编写一个规范的模块

我们在编写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() #在被当做脚本执行时,执行此处的代码