疯狂Python讲义-李刚编著-第9章模块和包
第9章 模块和包
Python 3 的标准库请参考 https://docs.python.org/3/library/index.html
9.1.模块化编程
9.1.1.导入模块的语法
使用 import 导入模块
import 语句主要有两种用法
1.import 模块名1[as 别名1],模块名2[as 别名2], ...:导入整个模块
2.from 模块名 import 成员名1[as 别名1],成员名2[as 别名2],...:导入模块中指定的成员。
上面两种 import 语句导入的区别主要有三点
1.第一种 import 语句导入整个模块内的所有成员(包括变量、函数、类等);第二种 import 语句只导入模块内的指定成员(除非使用 form 模块名 import *,但通常不推荐使用这种语法)。
2.当使用第一种 import 语句导入模块中的成员时,必须添加模块名或模块别名前缀;当使用第二种 import 语句导入模块中的成员时,无须使用任何前缀,直接使用成员名或成员别名即可。
import sys print(sys.argv[0])
在导入整个模块时可以为模块指定别名
import sys as s print(s.argv[0])
使用导入整个模块的语法也可一次导入多个模块,多个模块之间用逗号隔开
import sys, os print(sys.argv[0]) print(os.sep)
使用 from...import 导入模块成员
from sys import argv print(argv[0])
使用 from...import 导入模块成员时也可同时导入多个成员
from sys import argv, winver print(argv[0]) print(winver)
在使用 from...import 语法时也可一次导入指定模块内的所有成员
from sys import * print(argv[0]) print(winver)
需要说明的是,一般不推荐使用 "from 模块 import *" 这种语法导入指定模块内的所有成员,因为它存在潜在的风险。比如同时导入 module1 和 module2 内的所有成员,假如这两个模块内都有一个 foo() 函数,那么当在程序中执行时,这个 foo() 函数到底是 module1 模块中的还是 module2 模块中的?因此,这种导入指定模块内所有成员的用法是偶有风险的。
如果换成如下两种导入方式,就非常清晰
import module1 import module2 as m2 module1.foo() m2.foo() # 或者 from module1 import foo as foo1 form module2 import foo as foo2 foo1() foo2()
9.1.2.定义模块
模块就是 python 程序!任何 Python 程序都可作为模块导入。
print('这是 module 1')
my_book = '疯狂 Python 讲义'
def say_hi(user):
print('%s,您好,欢迎学习 Python' % user)
class User:
def __init__(self, name):
self.name = name
def walk(self):
print('%s 正慢慢地走路' % self.name)
def __repr__(self):
return 'User[name=%s]' % self.name
使用模块的好处在于:如果将程序需要使用的程序单元(比如 module1.py 定义的 say_hi() 函数、User 类)定义在模块中,后面不管哪个程序只要导入该模块,该程序即可使用该模块所包含的程序单元,这样就可以提供很好的复用——导入模块,使用模块,从而避免每个程序都需要重新定义这些程序单元。
模块文件的文件名就是它的模块名,比如 module1.py 的模块名就是 module1。
9.1.3.为模块编写说明文档
为模块编写说明文档很简单,只要在模块开始处定义一个字符串直接量即可。
''' 这是我们编写的第一个模块,该模块包含以下内容 my_book:字符串变量 say_hi:简单的函数 User:代表用户的类 '''
这段字符串内容将会作为该模块的说明文档,可通过模块的 __doc__ 属性来访问文档。
9.1.4.为模块编写测试代码
当模块编写完成之后,可能还需要为模块编写一些测试代码,用于测试模块中的每一个程序单元是否都能正常运行。
由于模块其实也是一个 Python 程序,因此我们完全可以使用 Python 命令来解释和执行模块程序——只要模块中包含可执行代码。比如使用 python module1.py 命令来运行上面的模块程序,即可看到如下输出结果。
这是 module 1
上面这行输出是因为 module1.py 程序中包含了一条 print 语句。但模块中的变量、函数、类都没有得到测试,因此还应该为这些变量、函数、类提供测试程序。
# ===以下部分是测试代码===
def test_my_book():
print(my_book)
def test_say_hi():
say_hi('孙悟空')
say_hi(User('Charlie'))
def test_User():
u = User('白骨精')
u.walk()
print(u)
注意:对于实际开发的项目,对每个函数、类可能都需要使用更多的测试用例进行测试,这样才能达到各种覆盖效果(比如语句覆盖、条件覆盖等)。
如果只是简单地调用上面的测试程序,则会导致一个问题:当其他程序每次导入该模块时,这三个测试函数都会自动运行,这显然不是我们期望看到的结果,此时希望实现的效果是:如果直接使用 python 命令运行该模块(相当于测试),程序应该执行该模块的测试函数;如果是其他程序导入该模块,程序不应该执行该模块的测试函数。
此时可借助于所有模块内置的__name__变量进行区分,如果直接使用 python 命令来运行一个模块,__name__变量的值为__main__;如果该模块被导入其他程序中,__name__变量的值就是模块名。因此,如果希望测试函数只有在使用 python 命令直接运行时才执行,则可以调用测试函数时增加判断:只有当__name__属性为__main__时才调用测试函数,为模块增加如下代码
# 当__name__为 '__main__' (直接使用 python 运行该模块)时执行如下代码
if __name__ == '__main__':
test_my_book()
test_say_hi()
test_User()
# 输出
这是 module 1
疯狂 Python 讲义
孙悟空,您好,欢迎学习 Python
User[name=Charlie],您好,欢迎学习 Python
白骨精 正慢慢地走路
User[name=白骨精]
9.2.加载模块
在编写一个 Python 模块之后,如果直接用 import 或 form...import 来导入该模块,Python 通常并不能加载该模块。道理很简单:Python 怎么知道到哪里去找这个模块呢?
提示:编程其实并不难,就是用适合的语法告诉计算机,让它帮助完成某个工作。因此计算机能完成的事情,其实都是程序员预先告诉它的。
为了让 Python 能找到我们编写(或第三方提供)的模块,可以用以下两种方式来告诉它。
1.使用环境变量
2.将模块放在默认的模块加载路径下
9.2.1.使用环境变量
Python 将会根据 PYTHONPATH 环境变量的值来确定哪里去加载模块。PYTHONPATH 环境变量的值是多个路径的集合,这样 Python 就会依次搜索 "PYTHONPATH" 环境变量所指定的多个路径,试图从中找到程序想要加载的模块。
1.在 Windows 平台上设置环境变量
2.在 Linux 上设置环境变量
# 设置 PYTHONPATH 环境变量 PYTHONPATH=.:/home/yeeku/python_module # 完成后了 PYTHONPATH 变量值得设置后,在 .bash_profile 文件的最后添加导出 PYTHONPATH 变量的语句。 export PYTHONPATH # 重新登录 Linux 平台,或者执行 source .bash_profile
3.在 Mac OS X 上设置环境变量
同上
在设置好环境变量之后,编写一个程序来导入
import module1 as md
import module1 as md
print(md.my_book)
md.say_hi('Charlie')
user = md.User('孙悟空')
print(user)
user.walk()
# 输出
这是 module 1
疯狂 Python 讲义
Charlie,您好,欢迎学习 Python
User[name=孙悟空]
孙悟空 正慢慢地走路
这里为什么要两次导入 module1 模块呢?其实完全没有必要,此处两次导入只是为了说明一点:Python 很智能。
虽然上面程序两次导入了 module1 模块,但最后运行程序,我们看到输出语句只输出一条 "这是 module 1",这说明第二次导入的 module1 模块并没有起作用,这就是 Python 的 "智能" 之处。
当程序重复导入同一个模块时,Python 只会导入一次。道理很简单,因为这些变量、函数、类等程序单元都只需要定义一次即可,何必导入多次呢?
相反,如果 Python 允许导入多次,反而可能会导致严重的后果。比如程序定义了 foo 和 bar 两个模块,假如 foo 模块导入了 bar 模块,而 bar 模块又导入了 foo 模块——这似乎形成了无限循环导入,但由于 Python 只会导入一次,所以这个无限循环导入的问题完全可以避免。
9.2.2.默认的模块加载路径
如果要安装某些通用性模块,比如复数功能支持的模块、矩阵计算支持的模块、图形界面支持的模块等,这些都属于对 Python 本身进行扩展的模块,这种模块应该直接安装在 Python 内部,以便被所有程序共享,此时就可借助于 Python 默认的模块加载路径。
Python 默认的模块加载路径由 sys.path 变量代表,因此可通过交互式解释器中执行如下命令来查看 Python 默认的模块加载路径

上面代码使用 pprint 模块下的 pprint() 函数代替普通的 print() 函数,这是因为如果要打印的内容很多,使用 pprint 可以显示更友好的打印结果。
上面运行的结果就是 Python 3.x 默认的模块加载路径,这是因为作者将 Python 安装在了 d:\Python 路径下。如果将 Python 安装在其他路径下,上面的运行结果应该略有差异。
上面的运行结果列出的路径都是 Python 默认的模块加载路径,但通常来说,我们应该将 Python 的扩展模块添加在 lib\site-packages 路径下,它专门用于存放 Python 的扩展模块和包。
'''
简单的模块,该模块包含以下内容
my_list:保存列表的变量
print_triangle:打印由星号组成的三角形的函数
'''
my_list = ['Python', 'Kotlin', 'Swift']
def print_triangle(n):
'''打印由星号组成的一个三角形'''
if n <= 0:
raise ValueError('n 必须大于 0')
for i in range(n):
print(' ' * (n - i - 1), end='')
print('*' * (2 * i + 1), end='')
print('')
# ====以下是测试代码====
def test_print_triangle():
print_triangle(3)
print_triangle(4)
print_triangle(7)
if __name__ == '__main__': test_print_triangle()
# 输出
*
***
*****
*
***
*****
*******
*
***
*****
*******
*********
***********
*************
上面模块文件中定义了一个 print_triangle() 函数,把该模块文件拷贝到 lib\site-packages 路径下,就相当于位 Python 扩展了一个 print_shape 模块,这样任何 Python 程序都可使用该模块。
import print_sharp print(print_shape.__doc__) print_shape.print_triangle.__doc__ print_shape.my_list[1] print_shape.print_triangle(4)
9.2.3.导入模块的本质
'一个简单地测试模块:fkmodule'
print("this is fk_module")
name = 'fkit'
def hello():
print("Hello, Python")
接下来,在相同的路径定义如下程序来使用该模块
import fk_module
print('=================')
print(type(fk_module))
print(fk_module)
# 输出
this is fk_module
=================
<class 'module'>
<module 'fk_module' from 'D:\\django\\test\\20200207\\fk_module.py'>
从上面的输出结果来看,当程序导入 fk_module 时,该模块中的输出语句会在 import 时自动执行。该程序中还包含一个与模块同名的变量,该变量的类型是 module。
使用 "import fk_module" 导入模块的本质就是:将 fk_module.py 中的全部代码加载在内存并执行,然后将整个模块内容赋值给与模块同名的变量,该变量的类型是 module,而在该模块中定义的所有程序单元相当于该 module 对象的成员。
from fk_module import name, hello
print('===================')
print(name)
print(hello)
print(fk_module)
# 输出
this is fk_module
Traceback (most recent call last):
===================
fkit
<function hello at 0x03303810>
File "D:/django/test/20200207/1448.py", line 9, in <module>
print(fk_module)
NameError: name 'fk_module' is not defined
从上面的输出结果看,即使使用 from...import 只导入模块中部分成员,该模块中的输出语句也会在 import 时自动执行,这说明 Python 依然会加载并执行模块中的代码。
使用 "from fk_module import name, hello" 导入模块中成员的本质是:将 fk_module.py 中的全部代码加载到内存并执行,然后只导入指定变量、函数等成员单元,并不会将整个模块导入,因此上面程序在输出 fk_module 时将看到错误提示:NameError: name 'fk_module' is not defined
在导入模块后,可以在模块文件所在目录下看到一个名为 "__pycache__" 的文件夹,打开该文件夹,可以看到 Python 为每个模块都生成一个 *.cpython-36.pye 文件,比如 Python 为 fk_module 模块生成一个 fk_module.cpython-36.pyc 文件,该文件其实是 Python 为模块编译生成的字节码,用于提升该模块的运行效率。

9.2.4.模块的__all__变量
在默认情况下,如果使用 "from 模块名 import *" 这样的语句来导入模块,程序会导入该模块中所有不以下划线开头的程序单元。
有时候模块中虽然包含很多成员,但并不希望每个成员都被暴露出来供外界使用,此时可借助于模块 __all__ 变量,将变量的只设置成一个列表,只有该列表中的程序单元才会暴露出来。
# 文件 all_module.py
def hello():
print("Hello, Python")
def world():
print("Python World is funny")
def test():
print('--test--')
__all__ = ['hello', 'world']
# 新建文件调用
from all_module import *
hello()
world()
test() # 会提示找不到 test() 函数
# 输出
Traceback (most recent call last):
Hello, Python
File "D:/django/test/20200207/1511.py", line 7, in <module>
test() # 会提示找不到 test() 函数
Python World is funny
NameError: name 'test' is not defined
从上面的输出结果可以看到,通过 "from all_module import *" 语句确实只能导入 __all__ 变量所列出的全部程序单元,没有列出的 test 就没有被导入进来。
事实上,__all__ 变量的意义在于为模块定义了一个开放的公共接口。通常来说,只有 __all__ 变量列出的程序单元,才是希望该模块被外界使用的程序单元。因此,为模块设置 __all__ 变量还是比较有用的。比如一个实际的大模块可能包含了大量其他程序不需要使用的变量、函数和类,那么通过 __all__ 变量即可把它们自动过滤掉,这还是非常酷的。
如果确实希望程序使用模块内 __all__ 列表之外的程序单元,有两种解决方法
1.第一种是使用 "import 模块名" 来导入模块。在通过这种方式导入模块之后,总可以通过模块名前缀(如果为模块指定了别名,则可以使用模块的别名作为前缀)来调用模块内的成员
2.第二种是使用 "from 模块名 import 程序单元" 来导入指定程序单元。在这种方式下,即使想导入的程序单元没有位于 __all__ 列表中,也依然可以导入。
9.3.使用包
对于一个需要实际应用的模块而言,往往会具有很多程序单元,包括变量、函数和类等,如果将整个模块的所有内容都定义在同一个 Python 源文件中,这个文件将会变得非常庞大,显然并不利于模块化开发。
9.3.1.什么是包
什么是包
1.从物理上看,包就是一个文件夹,在该文件夹下包含了一个 __init__.py 文件,该文件价可用于包含多个模块源文件
2.从逻辑上看,包的本质依然是模块
推论:包的作用是包含多个模块,但包的本质依然是模块,因此包也可用于包含包。典型地,当我们为 Python 包装了 numpy模块之后,可以在 Python 安装目录的 Lib\site-packages 目录下找到一个 numpy 文件夹,它就是前面安装的 numpy 模块(其实是一个包)。
在 numpy 包(也是模块)下既包含了 matlib.py 等模块源文件,也包含了 core 等子包(也是模块)。这正对应:包的本质依然是模块,因此包又可以包含包。
9.3.2.定义包
定义包
1.创建了一个文件夹,该文件夹的名字就是该包的包名
2.在该文件夹内添加一个__init__.py 即可
下面定义一个非常简单的包。先新建一个 first_package 文件夹,然后在该文件夹中添加一个 __init__.py 文件,该文件内容如下:
'''
这是学习包的第一个示例
'''
print('this is first_package')
通过如下程序来使用该包
import first_package
print('=========')
print(first_package.__doc__)
print(type(first_package))
print(first_package)
# 输出
this is first_package
=========
这是学习包的第一个示例
<class 'module'>
<module 'first_package' from 'D:\\django\\test\\20200208\\first_package\\__init__.py'>
从上面的代码可以看出,在导入 first_package 包时,程序执行了该包对应的文件夹下的 __init__.py;从倒数第二行输出可以看到,包的本质就是模块;从最后一行输出可以看到,使用 import first_package 导入包的本质就是加载并执行该包下的 __init__.py 文件,然后将整个文件内容赋值给与包同名的变量,该变量的类型是 module。
与模块类似的是,包被导入之后,会在包目录下生成一个 __pycache__ 文件夹,并在该文件夹内为包生成一个 __init__.cpython-36.pyc 文件。
由于导入包就相当于导入该包下的 __init__.py 文件,因此我们完全可以在 __init__.py 文件中定义变量、函数、类等程序单元,但实际上往往并不会这么做。想一想原因是什么?包的作用是包含多个模块,因此__init__.py 文件的主要作用就是导入该包内的其他模块。
下面再定义一个更加复杂的包,在该包下将会包含多个模块,并使用 __init__.py 文件来加载这些模块。
新建一个 fk_package 包,并在该包下包含三个模块文件
1.print_shape.py
2.billing.py
3.arithmetic_chart.py
fk_package 的文件结构如下
| fk_package | arithmetic_chart.py |
| billing.py | |
| print_shape.py | |
| __init__.py |
arithmetic_chart.py 内容
def print_multiple_chart(n):
'打印乘法口诀表的函数'
for i in range(n):
for j in range(i + 1):
print('%d * %d = %2d' % ((j + 1), (i + 1), (j + 1) * (i + 1)), end=' ')
print('')
billing.py 内容
class Item:
'定义代表商品的 Item 类'
def __init__(self, price):
self.price = price
def __repr__(self):
return 'Item[price=%g]' % self.price
print_shape.py 内容
def print_blank_triangle(n):
'打印一个由星号组成的空心的三角形'
if n < 0:
raise ValueError('n 必须大于 0')
for i in range(n):
print(' ' * (n - i - 1), end='')
print('*', end='')
if i != n - 1:
print(' ' * (2 * i - 1), end='')
else:
print('*' * (2 * i - 1), end='')
if i != 0:
print('*')
else:
print('')
fk_package 包下的 __init__.py 文件暂时为空,不用编写任何内容。
上面三个模块文件都位于 fk_package 包下,总共提供了两个函数和一个类。这意味着:fk_package 包(也是模块)总共包含 arithmetic_chart、billing 和 print_shape 三个模块。在这种情况下,这三个模块就相当于 fk_package 包的成员。
9.3.3.导入包内成员
import fk_package
import fk_package.print_shape
from fk_package import billing
import fk_package.arithmetic_chart
fk_package.print_shape.print_blank_triangle(5)
im = billing.Item(4.5)
fk_package.arithmetic_chart.print_multiple_chart(5)
# 输出
*
* *
* *
* *
*********
1 * 1 = 1
1 * 2 = 2 2 * 2 = 4
1 * 3 = 3 2 * 3 = 6 3 * 3 = 9
1 * 4 = 4 2 * 4 = 8 3 * 4 = 12 4 * 4 = 16
1 * 5 = 5 2 * 5 = 10 3 * 5 = 15 4 * 5 = 20 5 * 5 = 25
上面程序中第一行代码是 "import fk_package",由于导入包的本质只是加载并执行包里的 __init__.py 文件,因此执行这条导入语句之后,程序只能使用 fk_package 目录下的 __init__.py 文件中定义的程序单元。对于本例而言,由于 fk_package\__init__.py 文件内容为空,因此这条导入语句没有任何作用。
第二行代码是 "import fk_package.print_shape",这条导入语句的本质就是加载并执行 fk_package 包下的 print_shape.py 文件,并将其赋值给 fk_package.print_shape 变量。因此执行这条导入语句之后,程序可访问 fk_package\print_shape.py 文件所定义的程序单元,但需要添加 fk_package.print_shape 前缀。
第三行代码是 "from fk_package import billing",这条导入语句的本质是导入 fk_package 包(也是模块)下的 billing 成员(其实是模块)。因此执行这条导入语句之后,程序可使用 fk_package\billing.py 文件定义的程序单元,而且只需要添加 billing 前缀。
第四行代码与第二行代码的导入效果相同。
该程序后面分别测试了 fk_package 包下的 print_shape、billing、arithmetic_chart 这三个模块的功能。运行上面程序,可以看到三个模块的功能完全可以正常显示。
上面程序虽然可以正常运行,但此时存在两个问题
1.为了调用包内模块中的程序单元,需要使用很长的前缀,这实在是太麻烦了。
2.包内 __init__.py 文件的功能完全被忽略了
想一想就知道:包内的__init__.py 文件并不是用来定义程序单元的,而是用于导入该包内模块的成员,这样即可把模块中的成员导入变成包内成员,以后使用起来会更加方便。
# 从当前包中导入 print_shape from . import print_shape # 从.print_shape 中导入所有程序单元到 fk_package 中 from .print_shape import * # 从当前包中导入 billing 模块 from . import billing # 从 .billing 中导入所有程序单元到 fk_package 中 from .billing import * # 从当包中导入 arithmetic_char 模块 from . import arithmetic_chart # 从.arithmetic_chart 中导入所有程序单元到 fk_package 中 from .arithmetic_chart import *
通过测试文件导入包 fk_package,测试包中 __init__.py 文件
# 导入 fk_pacakge 包,实际上就是导入该包下的 __init__.py 文件
import fk_package
# 直接使用 fk_package.前缀即可调用它所包含的模块内的程序单元
fk_package.print_blank_triangle(5)
im = fk_package.Item(4.5)
print(im)
fk_package.print_multiple_chart(5)
# 输出
*
* *
* *
* *
*********
Item[price=4.5]
1 * 1 = 1
1 * 2 = 2 2 * 2 = 4
1 * 3 = 3 2 * 3 = 6 3 * 3 = 9
1 * 4 = 4 2 * 4 = 8 3 * 4 = 12 4 * 4 = 16
1 * 5 = 5 2 * 5 = 10 3 * 5 = 15 4 * 5 = 20 5 * 5 = 25
导入 fk_package 包,导入该包的本质就是导入该包下的 __init__.py 文件。而 __init__.py 文件又执行了导入,它们会把三个模块内的程序单元导入 fk_package 包中,因此程序的下面代码可使用 fk_package 前缀来访问三个模块内的程序单元。
9.4.查看模块内容
9.4.1.模块包含什么
为了查看模块包含什么,可以通过如下两种方式:
1.使用 dir() 函数。
2.使用模块本身提供的 __all__变量。
import string
dir(string)
[e for e in dir(string) if not e.startwith('__')]


注意:并不是所有模块都会提供 __all__ 变量的,在这种情况下,只能使用列表推导式来查看模块中的程序单元。
9.4.2.使用__doc__属性查看文档
使用 help() 函数来查看程序单元的帮助信息。例如,在交互式解释器中输入如下命令来查看 string 模块下 capwords() 函数的作用。

通过上面描述可以看到,capwords() 函数的作用就是将给定的 s 字符串中每个单词的首字母变成大写的。该函数可通过 sep 参数指定分隔符;如果不指定 sep 参数,该字符默认以空白作为分隔符。
测试 string.capwords() 函数的用法。

需要说明的是,使用 help() 函数之所以能查看到程序单元的帮助信息,其实完全是因为该程序单元本身有文档信息,也就是有__doc__属性。换句话说,使用 help() 函数查看的其实就是程序单元的__doc__属性值。

提示:从理论上说,应该为每个程序单元都编写完备而详细的文档信息,这样开发者只要通过 help() 函数即可查看该程序单元的文档信息,完全不需要查看文档。但不得不说的是,有些程序单元的文档信息并不是很详细,此时可能需要借助于 Python 库的参考文档:https://docs.python.org/3/library/index.html
9.4.3.使用__file__属性查看模块的源文件路径
除可以查看模块的帮助信息之外,还可以直接阅读模块的源代码来掌握模块功能,提升 Python 编程能力。
提示:不管学习哪种编程语言,认真阅读那些优秀的框架、库的源代码都是非常好的学习方法。
通过模块的__file__属性即可查看到指定模块的源文件路径。

需要说明的是,并不是所有模块都是使用 Python 语言编写的,有些与底层交互的模块可能是用 C 语言编写的,而且是 C 语言编译之后的效果,因此这种模块可能没有__file__属性。
9.5.小结
posted on 2020-02-07 12:02 herisson_pan 阅读(50) 评论(0) 收藏 举报
浙公网安备 33010602011771号