part9-1 Python 模块和包(模块导入语法,模块定义,__doc__属性,环境变量设置,模块加载路径,模块导入的本质,模块__all__变量)


Python 语言被广泛用于各行各业,很大程度上利益于它的模块化系统。在 Python 标准安装时包含了一组自带模块,这些模块称为“标准库”。Python3 标准库参考 https://docs.python.org/3/library/index.html

实际开工中可以根据需要为 Python 增加扩展库。Python 在各行各业有丰富的扩展库,这些扩展库组成了“生态圈”。

一、 模块化编程

一个完整的 Python 程序通常需要借助第三方类库,并且源代码也不可能放在一个源文件中。这些需要以模块方式来组织项目的源代码。

1、 导入模块的语法
使用 import 语句导入模块,主要有两种用法:
(1)、import 模块名1[as 别名1], 模块名2[as 别名2], ...;导入整个模块
(2)、from 模块名 import 成员名1[as 别名1], 成员名2[as 别名2], ...;导入模块中指定成员。

两种导入模块的语法区别有三点:
(1)、第一种语法导入整个模块内的所有成员(包括变量、函数、类等);
(2)、第二种语法只导入模块内的指定成员(除非使用 from 模块名 import *,不建议用这种语法)。
(3)、用第一种语法导入模块时,在后面的代码中要使用该模块的成员时,必须在成员名前添加模块名或模块别名前缀;使用第二种语法导入模块中的成员时,不使用任何前缀,直接使用成员名或成员别名即可。

下面语法使用简单语法导入指定的整个模块,不使用别名:
import sys              # 导入整个 sys 模块
# 要使用 sys 模块名作为前缀来访问模块中的成员
print(sys.argv[0])      # 输出是源程序的文件名
sys 模块下的 argv 变量用于获取运行 Python 程序的命令行参数,其中 argv[0] 获取的是该 Python 程序的文件名。

在导入整个模块时也可以为模块指定别名,此时可通过别名访问模块的成员:
import sys as s     # 导入整个 sys 模块,并指定别名为 s
# 使用 s 别名作为前缀访问模块中的成员
print(s.argv[0])
导入整个模块的语法可以一次导入多个模块,多个模块间用逗号隔开,示例如下:
import sys, os      # 同时导入 sys、os 两个模块
# 使用模块名作为前缀访问模块中对应的成员
print(sys.argv[0])
print(os.sep)
os 模块的 sep 变量代表系统平台上的路径分隔符。当然,在导入多个模块时也可为模块指定别名,示例如下:
import sys as s, os as o        # 为 sys 指定别名 s,为 os 指定别名 o
# 使用模块别名作为前缀访问模块中的成员
print(s.argv[0])
print(o.sep)
使用 from ... import 导入模块的指定成员简单语法是:
from sys import argv        # 导入 sys 模块内的 argv 成员
print(argv[0])              # 导入成员后,直接使用成员名调用
当然,使用 from...import 语法导入成员时,也可为成员指定别名访问,无需使用前缀,示例如下:
from sys import argv as v       # 为导入的成员使用别名
print(v[0])                     # 直接使用成员的别名访问即可
使用 from ... import 导入模块时可以导入同一个模块的多个成员,并且还可为多个成员指定别名,示例如下:
from sys import argv as v, winver as wv     # 使用 as 关键字,为不同的成员指定别名
print(v[0])
print(wv)       # 记录 Python 的版本号
其中 sys 模块内的 winver 成员记录的是 Python 的版本号。

使用 from ... import 语法还可一次导入指定模块内的所有成员,示例如下:
from sys import *   # 导入 sys 模块内的所有成
# 导入所有成员后,直接使用成员名即可访问成员
print(argv[0])
print(winver)
不推荐使用 “from 模块 import *” 的语法导入模块内的所有成员,是因为这样做存在潜在的风险。假设导入的两个模块都有相同的成员 foo() 函数时,那么在后面调用 foo() 函数时,就不知道是在调用哪个模块的 foo() 函数,所以这种导入模块的方法有风险。可以将导入模块的方式改为下面形式:
import module1
import module2 as m2
# 访问模块成员时,加上模块名或别名为前缀,可以很清楚的知道在调用哪个成员
module1.foo()
m2.foo()

# 也可使用 from ... import 语法导入,并指定别名的方式也是可行的
from module2 import foo as foo1
from module2 import foo as foo2
# 直接通过别名访问成员,也可进行更好的区分
foo1()
foo2()

2、 定义模块
模块就是 Python 程序,任何 Python 程序都可作为模块导入。使用模块的好处在于:可将某些全局变量、类、函数等成员放在一个单独的文件(或模块)中,后面不管在哪个代码文件中要使用这些成员,只需要在当前的代码文件中导入包含需要的成员的文件即可,这样可以很好的复用。导入模块,使用模块,避免每个代码文件都需要定义这些重复的成员。

模块文件的文件名就是对应的模块名,比如 sys.py 的模块名就是 sys 一样。

3、 为模块编写说明文档
在定义函数、类的时候应该为函数、类写说明文档,同样也应该为模块写说明文档。模块的说明文档应包含该模块的用途,以及包含的功能等。

模块的说明文档定义在模块的开始处,将说明信息包含在一个三引号内。例如下面这样:
"""
这是 michael 编写的第一个模块,包含的内容有:
N:计数器
foo():一个简单的函数
User:代表用户的类
"""
这样的一段在模块文件开始处的字符串内容就是模块的说明文档,可通过模块的 __doc__ 属性访问说明文档。例如要访问 sys 模块的
说明,可使用 sys.__doc__ 语句即可。

4、 为模块编写测试代码
测试代码用于测试模块中的每一个程序单元是否都能正常运行。由于模块就是一个 Python 文件,所以可以在命令行下像执行脚本文件一样执行 Python 源代码文件,在命令行使用的是 python 命令来解释和执行模块文件,只要模块文件中包含可执行代码。

直接在命令行下执行模块文件对模块进行测试时,对于不是可执行代码的,比如模块中的变量、函数、类这些成员得不到相应的测试,所以还需要为这些成员提供测试程序。在实际开发项目时,对每个函数、类都需要使用更多的测试用例进行测试,以达到各种覆盖效果。

测试代码要达到的效果是:如果直接使用 python 命令运行该模块(相当于测试),程序应该执行该模块的测试函数;如果是在其他代码文件中导入该模块,则不应该执行该模块的测试函数。为达到这个目的,所有模块有内置的 __name__ 变量进行区分,如果直接使用python 命令运行一个模块,则 __name__ 变量的值为 __main__;如果该模块被导入其他文件中,__name__ 变量的值就是模块名。所以希望测试函数在使用 python 命令直接运行时才执行,可在调用测试函数时增加判断:只有当 __name__ 属性值为 __main__ 时才调用测试函数。可在模块中添加下面这种测试代码即可:
if __name__ == '__main__':
    main()      # 模块的测试代码细节在 main() 函数中
可将本模块的测试代码细节封装在一个函数或类中,在 “if __name__ == '__main__':” 语句后面调用相应的函数或类即可完成模块的测试。此时,在其他源代码文件中导入该模块时,测试代码也不会执行。

二、加载模块

编程就是用合适的语法告诉计算机,让它帮助完成某个工作,因此计算能完成的事情,是程序员预先告诉它的。

直接使用 import 或 from ... import 语法导入模块时,Python 需要知道去哪里找到这个模块。为了让 Python 能找到自定义模块(或第三方模块),可用下面两种方式告诉它:
(1)、使用环境变量。
(2)、将模块放在默认的模块加载路径下。

1、 使用环境变量
Python 根据 PYTHONPATH 环境变量的值来确定到哪里去加载模块。该变量的值是多个路径的集合,会依次搜索 PYTHONPATH 环境变量所指定的多个路径,试图从中找到程序要加载的模块。

(1)、 Windows 平台上设置环境变量
右击 “计算机” 图标选择 “属性” 菜单,系统显示“控制面板\所有控制面板项\系统” 窗口,单击该窗口左边栏中的 “高级系统设置”,“出现系统属性” 对话框,单击该对话框中的“高级”选项中的“环境变量”按钮,此时可看到 “环境变量” 对话框,通过该对话框可添加或修改环境变量。

在“环境变量”对话框中,上面是 “用户变量” 部分用于设置当前用户的环境变量,下面是“系统变量” 部分用于设置整个系统的环境变量。系统变量对所有用户有效,在搜索路径时,系统变量的路径排在用户变量路径之前。

单击用户变量中的 “新建” 按钮后,在 “变量名” 文本框中输入 PYTHONPATH,“变量值” 文本框中输入 .;d:\python_module,点击确定后就创建了一个环境变量名是 PYTHONPATH,路径是 .;d:\python_module 的环境变量。变量值中的路径以英文分号为分隔符,第一条路径为一个点(.),表示的是当前路径,即当运行 Python 时,Python 总能从当前路径加载模块;第二条路径是 d:\python_module,表示运行 Python 时,可以从 d:\python_module 中加载模块。

设置成功后,可将 Python 源代码文件放在与当前运行 Python 程序相同的路径中或放在 d:\python_module 路径下,这些源代码文件就能被成功加载。

(2)、 在 Linux 上设置环境变量
在 Linux 命令行界面下,进入到当前用户的 home 目录下,可执行 ls -a 命令列出当前目录下的所有文件,包括隐藏文件。Linux 平台的环境变量是通过 .bash_profile 文件来设置的,用 vim 打开该文件并添加 PYTHONPATH 环境变量,添加下面一行:
PYTHONPATH=.:/home/michael/python_module
Linux 平台上的路径分隔符是冒号(:),上面这一行设置了两个路径:点(.)是当前路径;另一条是当前用户目录下的 python_module 目录下。设置完 PYTHONPATH 变量值后,还需要在 .bash_profile 文件的最后面添加导出 PYTHONPATH 变量的语句:
export PYTHONPATH
保存并退出 .bash_profile 文件,并执行命令:
source .bash_profile
让 .bash_profile 文件中设置的 PYTHONPATH 变量值生效。设置完环境变量后,只要将自定义的模块(Python 源代码文件)放在 /home/michael/python_module 目录下,该模块就可以被成功加载。

当在代码中重复导入同一个模块时,Python 只会导入一次。因为这些变量、函数、类等程序单元都只需要定义一次即可,不必导入多次。

2、 默认的模块加载路径
在安装第三方模块时,应直接安装在 Python 内部,这样可被所有程序共享,此时需要借助于 Python 默认的模块加载路径。Python 默认的模块加载路径由 sys.path 变量代表,在交互式解释器下执行下面命令可查看 Python 默认的模块加载路径。
import sys, pprint
pprint.pprint(sys.path)         # 输出省略
这里使用 pprint 模块下的 pprint() 函数代替普通的 print() 函数,可以对要打印的内容有更友好的显式。通常应将扩展模块添加在 lib\site-packages 路径下,它专用于存放 Python 的扩展模块和包。

示例,将下面的代码保存为 print_shape.py 文件,并将该文件放在 lib\site-packages 目录下。
 1 """
 2 一个简单的测试模块,该模块包含以下内容
 3 my_list:列表变量
 4 print_triangle:打印由星号组成的三角形函数
 5 """
 6 my_list = ['python', 'java', 'javascript']
 7 def print_triangle(n):
 8     """打印由星号组成的三角形"""
 9     if n <= 0:
10         raise ValueError("n 必须大于 0")
11     for i in range(n):
12         print(" " * (n - i - 1), end=" ")
13         print("*" * (i * 2 + 1), end=" ")
14         print()
15 
16 # 下面是测试代码
17 def test_print_triangle():
18     print_triangle(5)
19     print_triangle(3)
20     print_triangle(8)
21 
22 if __name__ == '__main__':
23     test_print_triangle()
将上面代码保存为 print.shape.py 文件,并将该文件放在 lib\site-packages 目录下后,就相当于为 Python 扩展了一个 print_shape 模块,现在在任何 Python 程序中都可以使用该模块,在 Python 交互式解释器中可以进行测试,示例如下:
import pring_shape as ps
print(ps.__doc__)           # 输出模块的描述信息
ps.print_triangle.__doc__   # 查看模块中 print_triangle 函数的描述信息
ps.my_list[2]               # 测试模块中 my_list 变量
ps.print_triangle(5)        # 测试模块中 print_triangle() 函数,该函数的输出如下:
     *
    ***
   *****
  *******
 *********
通过上面的一些测试表明该模块完全可以正常运行。

2、模块导入的本质
在当前的代码文件中导入某一个模块时(比如前面的 print_shape 模块),执行当前的源代码文件,import 导入的模块文件也会自动执行,当导入的模块中含有可执行代码,此时模块中的可执行代码就获得执行的机会,所以在模块中尽量不写可执行的代码。此外,在当前的执行程序中还包含一个与模块同名的变量,变量的类型是 module(可通过 print(type(print_shape)) 查看)。

因此,当使用 “import print_shape” 导入模块的本质就是:将 print_shape 中的全部代码加载到内存并执行,然后将整个模块内容赋值给与模块同名的变量,该变量的类型是 module ,在模块中定义的所有程序单元都相当于该 module 对象的成员。

当使用 from ... import 语句只导入模块中部分成员(比如 from print_shape import my_list),该模块中的可执行代码也会在 import 时自动执行,这说明了 Python 依然会加载并执行模块中的代码。所以,使用 from ... import 导入模块中的成员的本质是:将模块中的全部代码加载到内存并执行,然后只导入指定变量、函数等成员单元,并不会将整个模块导入,此时在使用 print(type(print.shape)) 语句时,会得到错误提示 NameError: name 'print_shape' is not defined。

在导入模块后,可在模块文件所在目录下看到一个名为 “__pycache__” 的文件夹,在该文件夹下可看到 Python 为每个模块都生成一个 *.cpython-36.pyc 文件,比如 Python 为 print_shape 模块生成一个 print_shape.cpython-36.pyc 文件,该文件是Python 为模块编译生成的字节码,用于提升该模块的运行效率。其中的 36 表示 Python 的版本号。

4、 模块的 __all__ 变量
在使用 “from 模块名 import *” 语句导入模块时,会在当前程序中导入该模块中所有不以下划线开头的程序单元。除此之外,还可以在模块中定义 __all__ 变量,该变量的值是一个列表,只有在该列表中的程序单元才会被暴露出来(允许被导入)。示例如下,在 print_shape 模块中定义 __all__ 变量,代码如下:
 1 _name = 'michael'       # 测试以下划线开头的变量能否被导入
 2 my_list = ['python', 'java', 'javascript']
 3 print('hello world')
 4 def print_triangle(n):
 5     """打印由星号组成的三角形"""
 6     if n <= 0:
 7         raise ValueError("n 必须大于 0")
 8     for i in range(n):
 9         print(" " * (n - i - 1), end=" ")
10         print("*" * (i * 2 + 1), end=" ")
11         print()
12 
13 # 下面是测试代码
14 def test_print_triangle():
15     print_triangle(5)
16     print_triangle(3)
17     print_triangle(8)
18 
19 # 定义 __all__ 变量,默认只导入 my_list 和 print_triangle 两个成员,注意列表的元素是字符串形式
20 __all__ = ['my_list', 'print_triangle']
21 
22 if __name__ == '__main__':
23     test_print_triangle()
在这个 print_shape.py 模块中,定义了一个以下划线开头的变量_name、一条可执行语句 print、一个列表变量 my_list和两个函数,现在在当前模块所在的目录下的另一个源代码文件中导入这个模块,代码如下所示:
from print_shape import *
# 把 print_shape.py 模块中的 __all__ 变量这一行注释掉后可测试下面这行代码
# print(_name)        # 提示:NameError: name '_name' is not defined
print(my_list)
print_triangle(3)
test_print_triangle()       # 提示函数未定义

运行代码,输出结果如下所示:
hello world
['python', 'java', 'javascript']
   *
  ***
 *****
Traceback (most recent call last):
  File "cp9_exercise.py", line 15, in <module>
    test_print_triangle()
NameError: name 'test_print_triangle' is not defined
从输出结果可知,使用 “from print_shape import * ” 导入了 print_shape 模块下的程序单元,但由于该模块有 __all__ 变量,因此该语句只导入 __all__ 变量所列出的程序单元。另外,模块中以下划线开头的成员同样可以放在 __all__ 变量中,该下划线开头的成员同样可以被导入。要注意的是,__all__ 变量列表的元素只能是字符串,模块中的成员只能以字符串的形式出现在 __all__ 变量的列表中

__all__ 变量为模块定义了一个开放的公共接口。在大型模块中可能有大量不需要使用的变量、函数和类等,可通过设置 __all__ 变量来把它们过滤掉。如果仍然希望使用模块内 __all__ 列表之外的程序单元,有两种方法可用:
(1)、第一种是使用 “import 模块名” 导入模块。用过种方式导入模块,总可以使用模块名前缀来调用模块内的成员。
(2)、第二种是使用 “from 模块名 import 程序单元” 来导入指定程序单元,即使要导入的程序单元没有在 __all__ 列表中,也可以导入。
posted @ 2019-11-08 10:18  远方那一抹云  阅读(355)  评论(0编辑  收藏  举报