Python学习笔记九2:模块


  至此,你掌握了Python语言的大部分基础知识。Python不仅语言核心非常强大,还提供了其他工具以供使用。标准安装包含一组称为标准库(standard library)的模块,你见过其中的一些(如math和cmath),但还有其他很多。本章简要介绍模块的工作原理以及如何探索模块以获悉其提供的功能,然后概述标准库,重点是几个很有用的模块。

一、模块

  你已知道如何创建和执行程序(或脚本),还知道如何使用import将函数从外部模块导入到程序中。

>>> import math
>>> math.sin(0)		#0.0

  下面来看看如何编写自己的模块。

1. 模块就是程序

  任何Python程序都可作为模块导入。假设你编写了代码清单10-1所示的程序,并将其保存在文件hello.py中,这个文件的名(不包括扩展名.py)将成为模块的名称。

# hello.py
print("Hello, world!")

  文件的存储位置也很重要,将在下一节详细介绍。这里假设这个文件存储在目录C:\python(Windows)~/python(UNIX/macOS)中。
要告诉解释器去哪里查找这个模块,可执行如下命令(以Windows目录为例):

>>> import sys
>>> sys.path.append('C:/python')

提示:在UNIX中,不能直接将字符串'~/python'附加到sys.path末尾,而必须使用完整的路径(如'/home/yourusername/python')。如果你要自动创建完整的路径,可使用sys.path. expanduser('~/python')
  这告诉解释器,除了通常将查找的位置外,还应到目录C:\python中去查找这个模块。这样做后,就可以导入这个模块了(它存储在文件C:\python\hello.py中)。

>>> import hello	#Hello, world!

注意:当你导入模块时,可能发现其所在目录中除源代码文件外,还新建了一个名为__pycache__的子目录(在较旧的Python版本中,是扩展名为.pyc的文件)。这个目录包含处理后的文件,Python能够更高效地处理它们。以后再导入这个模块时,如果.py文件未发生变化,Python将导入处理后的文件,否则将重新生成处理后的文件。删除目录__pycache__不会有任何害处,因为必要时会重新创建它。
如你所见,导入这个模块时,执行了其中的代码。但如果再次导入它,什么事情都不会发生。

>>> import hello
>>>

  这次为何没有执行代码呢?因为模块并不是用来执行操作(如打印文本)的,而是用于定义变量、函数、类等。鉴于定义只需做一次,因此导入模块多次和导入一次的效果相同。

(1)为何只导入一次

  在大多数情况下,只导入一次是重要的优化,且在下述特殊情况下显得尤为重要:两个
模块彼此导入对方。
  在很多情况下,你可能编写两个这样的模块:需要彼此访问对方的函数和类才能正确地发挥作用。例如,你可能创建了两个模块clientdbbilling,分别包含客户数据库和记账系统的代码。客户数据库可能包含对记账系统的调用(如每月自动向客户发送账单),而记账系统可能需要访问客户数据库的功能才能正确地完成记账。
  在这里,如果每个模块都可导入多次,就会出现问题。模块clientdb导入billing,而billing又导入clientdb,结果可想而知:最终将形成无穷的导入循环(还记得无穷递归吗)。然而,由于第二次导入时什么都不会发生,这种循环被打破。
  如果一定要重新加载模块,可使用模块importlib中的函数reload,它接受一个参数(要重新加载的模块),并返回重新加载的模块。如果在程序运行时修改了模块,并希望这种修改反映到程序中,这将很有用。要重新加载前述简单的模块hello(它只包含一条print语句),可像下面这样做:

>>> import importlib
>>> hello = importlib.reload(hello)
	#Hello, world!

  这里假设hello已导入(一次)。通过将函数reload的结果赋给hello,用重新加载的版本替换了以前的版本。由于打印出了问候语,说明这里确实导入了这个模块。
  通过实例化模块bar中的类Foo创建对象x后,如果重新加载模块bar,并不会重新创建x指向的对象,即x依然是(来自旧版bar的)旧版Foo的对象。要让x指向基于重新加载的模块中的Foo创建的对象,需要重新创建它。

2. 模块是用来下定义的

  模块在首次被导入程序时执行。这看似有点用,但用处不大。让模块值得被创建的原因在于它们像类一样,有自己的作用域。这意味着在模块中定义的类和函数以及对其进行赋值的变量都将成为模块的属性。这看似复杂,但实际上非常简单。

(1) 在模块中定义函数

  假设你编写了一个类似于代码清单10-2所示的模块,并将其存储在文件hello2.py中。另外,假设你将这个文件放在了Python解释器能够找到的地方(可像前一节介绍的那样使用sys.path,也可使用10.1.3节介绍的传统方式)。
  提示:像处理模块那样,让程序(这意味着将被执行,而不是用作模块)可用后,可使用Python解释器开关-m来执行它。如果随其他模块一起安装了文件progname.py(请注意扩展名),即导入了progname,命令python -m progname args将使用命令行参数args来执行程序progname
  代码清单10-2 只包含一个函数的简单模块

# hello2.py
def hello():
	print("Hello, world!")

  现在可以像下面这样导入它:

>>> import hello2

  这将执行这个模块,也就是在这个模块的作用域内定义函数hello,因此可像下面这样访问这个函数:

>>> hello2.hello()
	#Hello, world!

  在模块的全局作用域内定义的名称都可像上面这样访问。为何要这样做呢?为何不在主程序中定义一切呢?主要是为了重用代码。

(2)在模块中添加测试代码

  模块用于定义函数和类等,但在有些情况下(实际上是经常),添加一些测试代码来检查情况是否符合预期很有用。例如,如果要确认函数hello管用,你可能将模块hello2重写为代码清单10-3所示的模块hello3。
  代码清单10-3 一个简单的模块,其中的测试代码有问题

# hello3.py
def hello():
	print("Hello, world!")
# 一个测试:
hello()

  这看似合理:如果将这个模块作为普通程序运行,将发现它运行正常。然而,如果在另一个程序中将其作为模块导入,以便能够使用函数hello,也将执行测试代码,就像本章的第一个hello模块一样。

>>> import hello3
	#Hello, world!		#这不是你想要的结果。
>>> hello3.hello()
	#Hello, world!

  要避免这种行为,关键是检查模块是作为程序运行还是被导入另一个程序。为此,需要使用变量__name__

>>> __name__
	#'__main__'
>>> hello3.__name__
	#'hello3'

如你所见,在主程序中(包括解释器的交互式提示符),变量__name__的值是'__main__',而在导入的模块中,这个变量被设置为该模块的名称。因此,要让模块中测试代码的行为更合理,可将其放在一条if语句中,如代码清单10-4所示。
  代码清单10-4 一个包含有条件地执行的测试代码的模块

# hello4.py
def hello():
	print("Hello, world!")
def test():
	hello()
if __name__ == '__main__': test()

  如果将这个模块作为程序运行,将执行函数hello;如果导入它,其行为将像普通模块一样。

>>> import hello4
>>> hello4.hello()
	#Hello, world!

  如你所见,我将测试代码放在了函数test中。原本可以将这些代码直接放在if语句中,但通过将其放在一个独立的测试函数中,可在程序中导入模块并对其进行测试。

>>> hello4.test()
	#Hello, world!

  注意:如果要编写更详尽的测试代码,将其放在一个独立的程序中可能是个不错的主意。有关如何编写测试的详细信息,请参阅第16章。

3. 让模块可用

  在前面的示例中,我修改了sys.pathsys.path包含一个目录(表示为字符串)列表,解释器将在这些目录中查找模块。然而,通常你不想这样做。最理想的情况是,sys.path一开始就包含正确的目录(你的模块所在的目录)。为此有两种办法:将模块放在正确的位置;告诉解释器到哪里去查找。接下来的两节将分别讨论这两种解决方案。如果要让别人能够轻松地使用你的模块,那就是另外一码事了

(1) 将模块放在正确的位置

  将模块放在正确的位置很容易,只需找出Python解释器到哪里去查找模块,再将文件放在这个地方即可。在你使用的计算机中,如果Python解释器是管理员安装的,而你有没有管理员权限,就可能无法将模块保存到Python使用的目录中。在这种情况下,需要采用随后要介绍的另一种解决方案:告诉解释器去哪里查找。
你可能还记得,可在模块sys的变量path中找到目录列表(即搜索路径)。

>>> import sys, pprint
>>> pprint.pprint(sys.path)
['C:\\Python35\\Lib\\idlelib',
'C:\\Python35',
'C:\\Python35\\DLLs',
'C:\\Python35\\lib',
'C:\\Python35\\lib\\plat-win',
'C:\\Python35\\lib\\lib-tk',
'C:\\Python35\\lib\\site-packages']

  提示:如果要打印的数据结构太大,一行容纳不下,可使用模块pprint中的函数pprint(而不是普通print语句)。pprint是个卓越的打印函数,能够更妥善地打印输出。
  这里的要点是,每个字符串都表示一个位置,如果要让解释器能够找到模块,可将其放在其中任何一个位置中。虽然放在这里显示的任何一个位置中都可行,但目录site-packages是最佳的选择,因为它就是用来放置模块的。请在你的计算机中查看sys.path,找到目录site-packages,并将代码清单10-4所示的模块保存到这里,但要使用另一个名称,如another_hello.py。然后,尝试像下面这样做:

>>> import another_hello
>>> another_hello.hello()
	#Hello, world!

  只要模块位于类似于site-packages这样的地方,所有的程序就都能够导入它。

(2) 告诉解释器到哪里去查找

将模块放在正确的位置可能不是合适的解决方案,其中的原因很多。

  • 不希望Python解释器的目录中充斥着你编写的模块。
  • 没有必要的权限,无法将文件保存到Python解释器的目录中。
  • 想将模块放在其他地方。

  最重要的是,如果将模块放在其他地方,就必须告诉解释器到哪里去查找。标准做法是将模块所在的目录包含在环境变量PYTHONPATH中。
环境变量PYTHONPATH的内容随操作系统而异(参见旁注“环境变量”),但它基本上类似于sys.path,也是一个目录列表。
  环境变量并不是Python解释器的一部分,而是操作系统的一部分。大致而言,它们类似于Python变量,但是在Python解释器外面设置的。如果你使用的是bash shell(在大多数类UNIX系统、macOS和较新的Windows版本中都有),就可使用如下命令将~/python附加到环境变量PYTHONPATH末尾:
export PYTHONPATH=$PYTHONPATH:~/python
  如果要对所有启动的shell都执行这个命令,可将其添加到主目录中的.bashrc文件中。关于如何以其他方式编辑环境变量,请参阅操作系统文档。
  除使用环境变量PYTHONPATH外,还可使用路径配置文件。这些文件的扩展名为.pth,位于一些特殊目录中,包含要添加到sys.path中的目录。有关这方面的详细信息,请参阅有关模块site的标准库文档。

4. 包

  为组织模块,可将其编组为包(package)。包其实就是另一种模块,但有趣的是它们可包含其他模块。模块存储在扩展名为.py的文件中,而包则是一个目录。要被Python视为包,目录必须包含文件__init__.py。如果像普通模块一样导入包,文件__init__.py的内容就将是包的内容。
  例如,如果有一个名为constants的包,而文件constants/__init__.py包含语句PI = 3.14,就可以像下面这样做:

import constants
print(constants.PI)

  要将模块加入包中,只需将模块文件放在包目录中即可。你还可以在包中嵌套其他包。例如,要创建一个名为drawing的包,其中包含模块shapescolors,需要创建如表10-1所示的文件和目录(UNIX路径名)。

表10-1 一种简单的包布局
文件/目录描 述
~/python/PYTHONPATH中的目录
~/python/drawing/包目录(包drawing)
~/python/drawing/init.py包代码(模块drawing)
~/python/drawing/colors.py模块colors
~/python/drawing/shapes.py模块shapes

  完成这些准备工作后,下面的语句都是合法的:

import drawing # (1) 导入drawing包
import drawing.colors # (2) 导入drawing包中的模块colors
from drawing import shapes # (3) 导入模块shapes

  执行第1条语句后,便可使用目录drawing中文件__init__.py的内容,但不能使用模块shapescolors的内容。执行第2条语句后,便可使用模块colors,但只能通过全限定名drawing.colors来使用。执行第3条语句后,便可使用简化名(即shapes)来使用模块shapes。请注意,这些语句只是示例,并不用像这里做的那样,先导入包再导入其中的模块。换而言之,完全可以只使用第2条语句,第3条语句亦如此。

二、探索模块

  介绍一些标准库模块前,先来说说如何探索模块。这是一种很有用的技能,因为在你的Python程序员职业生涯中,将遇到很多很有用的模块,而这里无法一一介绍。当前的标准库很大,足以编写专著来论述(市面上也确实有这样的专著),而且还在不断增大。每个新Python版本都新增了模块,通常还会对一些既有模块进行细微的修改和改进。另外,你在网上肯定会找到一些很有用的模块。如果能快速而轻松地理解它们,编程工作将有趣得多。

1. 模块包含什么

  要探索模块,最直接的方式是使用Python解释器进行研究。为此,首先需要将模块导入。假设你听说有一个名为copy的标准模块。

>>> import copy

  没有引发异常,说明确实有这样的模块。但这个模块是做什么用的呢?它都包含些什么呢?

(1) 使用dir

  函数dir它列出对象的所有属性(对于模块,它列出所有的函数、类、变量等)。如果将dir(copy)的结果打印出来,将是一个很长的名称列表。在这些名称中,有几个以下划线打头。根据约定,这意味着它们并非供外部使用。有鉴于此,我们使用一个简单的列表推导将这些名称过滤掉(如果你忘记了列表推导的工作原理,请参阅5.6节)。

>>> dir(copy)
	#['Error', '__all__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', '_copy_dispatch', '_copy_immutable', '_deepcopy_atomic', '_deepcopy_dict', '_deepcopy_dispatch', '_deepcopy_list', '_deepcopy_method', '_deepcopy_tuple', '_keep_alive', '_reconstruct', 'copy', 'deepcopy', 'dispatch_table', 'error']

>>> [n for n in dir(copy) if not n.startswith('_')]
	#['Error', 'copy', 'deepcopy', 'dispatch_table', 'error']

  结果包含dir(copy)返回的不以下划线打头的名称,这比完整清单要好懂些。

(2) 变量__all__

  在前一节中,我使用简单的列表推导来猜测可在模块copy中看到哪些内容,然而可直接咨询这个模块来获得正确的答案。你可能注意到了,在dir(copy)返回的完整清单中,包含名称__all__。这个变量包含一个列表,它与前面使用列表推导创建的列表类似,但是在模块内部设置的。下面来看看这个列表包含的内容:

>>> copy.__all__
	#['Error', 'copy', 'deepcopy']

  前面的猜测不算太离谱,只是多了几个并非供用户使用的名称。这个__all__列表是在模块copy中像下面这样设置的(这些代码是直接从copy.py复制而来的)

__all__ = ["Error", "copy", "deepcopy"]

  为何要提供它呢?旨在定义模块的公有接口。具体地说,它告诉解释器从这个模块导入所有的名称意味着什么。因此,如果你使用如下代码:

from copy import *

  将只能得到变量__all__中列出的4个函数。要导入dispatch_table,必须显式地:导入copy并使用copy. dispatch_table;或者使用from copy import dispatch_table
  编写模块时,像这样设置__all__也很有用。因为模块可能包含大量其他程序不需要的变量、函数和类,比较周全的做法是将它们过滤掉。如果不设置__all__,则会在以import *方式导入时,导入所有不以下划线打头的全局名称。

2. 使用help 获取帮助

  前面一直在巧妙地利用你熟悉的各种Python函数和特殊属性来探索模块copy。对这种探索来说,交互式解释器是一个强大的工具,因为使用它来探测模块时,探测的深度仅受限于你对Python语言的掌握程度。然而,有一个标准函数可提供你通常需要的所有信息,它就是help。下面来尝试使用它获取有关函数copy的信息:

>>> help(copy.copy)
Help on function copy in module copy:
copy(x)
    Shallow copy operation on arbitrary Python objects.
    See the module's __doc__ string for more info.

  上述帮助信息指出,函数copy只接受一个参数x,且执行的是浅复制。在帮助信息中,还提到了模块的__doc__字符串。文档字符串就是在函数开头编写的字符串,用于对函数进行说明,而函数的属性__doc__可能包含这个字符串。从前面的帮助信息可知,模块也可能有文档字符串(它们位于模块的开头),而类也可能如此(位于类的开头)。
  实际上,前面的帮助信息是从函数copy的文档字符串中提取的:

>>> print(copy.copy.__doc__)
Shallow copy operation on arbitrary Python objects.
    See the module's __doc__ string for more info.

  相比于直接查看文档字符串,使用help的优点是可获取更多的信息,如函数的特征标(即它接受的参数)。

3. 文档

  显然,文档是有关模块信息的自然来源。我之所以到现在才讨论文档,是因为查看模块本身要快得多。例如,你可能想知道range的参数是什么?在这种情况下,与其在Python图书或标准Python文档中查找对range的描述,不如直接检查这个函数。

>>> print(range.__doc__)

  由于通常是在编程时想了解函数的功能,而此时Python解释器很可能正在运行,因此获取这些信息只需几秒钟。
  然而,并非每个模块和函数都有详尽的文档字符串(虽然应该如此),且有时需要有关工作
原理的更详尽描述。从网上下载的大多数模块都有配套文档。就学习Python编程而言,最有用的文档是“Python库参考手册”,它描述了标准库中的所有模块。

4. 使用源代码

  在大多数情况下,前面讨论的探索技巧都够用了。但要真正理解Python语言,可能需要了解一些不阅读源代码就无法了解的事情。事实上,要学习Python,阅读源代码是除动手编写代码外的最佳方式。
实际阅读源代码应该不成问题,但源代码在哪里呢?快捷的方式是查看模块的特性__file__

>>> print(copy.__file__)
C:\Python35\lib\copy.py

  找到了!你可在代码编辑器(如IDLE)中打开文件copy.py,并开始研究其工作原理。如果列出的文件名以.pyc结尾,可打开以.py结尾的相应文件。
  警告:在文本编辑器中打开标准库文件时,存在不小心修改它的风险。这可能会破坏文件。因此关闭文件时,千万不要保存你可能对其所做的修改。
  请注意,有些模块的源代码你完全无法读懂。它们可能是解释器的组成部分(如模块sys),还可能是使用C语言编写的①。(有关如何使用C语言扩展Python的详细信息,请参阅第17章。)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值