在介绍一些标准库模块前,先来说说如何探索模块,这是一种很有用的技能。 因为在使用 Python 语言解决实际问题时会遇到很多很有用的模块,如果能快速而轻松地理解它们,编程工作将有趣得多。
要探索模块,最直接的方式是使用Python解释器进行研究。为此,首先需要将模块导入。
假设听说有一个名为 copy
的标准模块。
import copy
这个模块是做什么用的呢?它都包含些什么呢?
要查明模块包含哪些东西,可使用函数 dir()
,它列出对象的所有属性。
如果将 dir(copy)
的结果打印出来,将是一个很长的名称列表。在这些名称中,有几个以下划线打头。
根据约定,这意味着它们并非供外部使用。有鉴于此,接下来使用一个简单的列表推导将这些名称过滤掉。
[n for n in dir(copy) if not n.startswith('_')]
['Error', 'copy', 'deepcopy', 'dispatch_table', 'error']
结果包含 dir(copy)
返回的不以下划线打头的名称,这比完整清单要好懂些。
[n for n in dir(copy) if n.startswith('_')]
['__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 中看到哪些内容,然而可直接咨询
这个模块来获得正确的答案。在 dir(copy)
返回的完整清单中,包含名称 __all__
。
这个变量包含一个列表,它与前面使用列表推导创建的列表类似,但是在模块内部设置的。下面来看看这个列表包含的内容:
copy.__all__
['Error', 'copy', 'deepcopy']
前面的猜测不算太离谱,只是多了几个并非供用户使用的名称。
这个 __all__
列表是怎么来的呢?为何要提供它?
第一个问题很容易回答:它是在模块 copy
中像下面这样设置的:
__all__ = ["Error", "copy", "deepcopy"]
为何要提供它呢?旨在定义模块的公有接口。具体地说,它告诉解释器从这个模块导入所有 的名称意味着什么。因此,如果使用如下代码:
from copy import *
将只能得到变量 __all__
中列出的4个函数。要导入 PyStringMap
,
必须显式地:导入 copy
并使用 copy.PyStringMap
;
或者使用 from copy import PyStringMap
。
编写模块时,像这样设置 __all__
也很有用。
因为模块可能包含大量其他程序不需要的变量、函数和类,比较周全的做法是将它们过滤掉。
如果不设置 __all__
, 则在以 import *
方式导入时会导入所有不以下划线打头的全局名称。
前面一直在巧妙地利用所熟悉的各种Python函数和特殊属性来探索模块 copy
。
对这种探索来说,交互式解释器是一个强大的工具,因为使用它来探测模块时,
探测的深度仅受限于对Python 语言的掌握程度。
有一个标准函数可提供通常需要的所有信息,它就是 help()
。
下面来尝试使用它获取有关函数 copy
的信息:
import 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__
字符串是什么呢?可能还记得,第6章提到了文档字符串。
文档字符串就是在函数开头编写的字符串,用于对函数进行说明,而函数的属性 __doc__
可能包含这个字符串。
从前面的帮助信息可知,模块也可能有文档字符串(它们位于模块的开头)
,而类也可能如此(位于类的开头)。
实际上,前面的帮助信息是从函数 copy
的文档字符串中提取的:
print(copy.copy.__doc__)
Shallow copy operation on arbitrary Python objects. See the module's __doc__ string for more info.
相比于直接查看文档字符串,使用 help
的优点是可获取更多的信息,如函数的特征标(即它
接受的参数)。请尝试对模块 copy
本身调用 help
,看看将显示哪些信息。这将打印大量的信息,
包括对 copy
和 deepcopy
之间差别的详细讨论(大致而言,deepcopy(x)
创建 x
的属性的副本并依此 类推;
而 copy(x)
只复制 x
,并将副本的属性关联到 x
的属性值)。
显然,文档是有关模块信息的自然来源。之所以到现在才讨论文档,是因为查看模块本身 要快得多。
例如可能想知道 range
的参数是什么?在这种情况下,与其在Python图书或标准 Python文档中查找对 range
的描述,
不如直接检查这个函数。
print(range.__doc__)
range(stop) -> range object range(start, stop[, step]) -> range object Return an object that produces a sequence of integers from start (inclusive) to stop (exclusive) by step. range(i, j) produces i, i+1, i+2, ..., j-1. start defaults to 0, and stop is omitted! range(4) produces 0, 1, 2, 3. These are exactly the valid indices for a list of 4 elements. When step is given, it specifies the increment (or decrement).
这样就获得了函数 range
的准确描述。另外,由于通常是在编程时想了解函数的功能,
而此时Python解释器很可能正在运行,因此获取这些信息只需几秒钟。
在大多数情况下,前面讨论的探索技巧都够用了。但要真正理解Python语言, 可能需要了解 一些不阅读源代码就无法了解的事情。事实上,要学习Python, 阅读源代码是除动手编写代码外的最佳方式。
实际阅读源代码应该不成问题,但源代码在哪里呢?假设要阅读标准模块 copy
的代码,
可以在什么地方找到呢?一种办法是像解释器那样通过 sys.path
来查找,
但更快捷的方式是查看模块的特性 file
。
print(copy.__file__)
/opt/conda/lib/python3.12/copy.py
找到了!可在代码编辑器(如IDLE)中打开文件 copy.py
,并开始研究其工作原理。
如果列出的文件名以 .pyc
结尾,可打开以 .py
结尾的相应文件。
警告:在文本编辑器中打开标准库文件时,存在不小心修改它的风险。这可能会破坏文件。 因此关闭文件时,千万不要保存可能对其所做的修改。
请注意,有些模块的源代码完全无法读懂。它们可能是解释器的组成部分(如模块 sys
),
还可能是使用C语言编写的。
在前面的示例中,修改了 sys.path
。sys.path
包含一个目录列表,解释器将在这些目录中查找模块。
然而,通常不想这样做。最理想的情况是,sys.path
一开始就包含正确的目录。为此有两种办法:
将模块放在正确的位置;告诉解释器到哪里去查找。
将模块放在正确的位置很容易,只需找出Python解释器到哪里去查找模块,再将文件放在这个地方即可。 在使用的计算机中,如果Python解释器是管理员安装的,而有没有管理员权限, 就可能无法将模块保存到Python使用的目录中。在这种情况下,需要采用随后要介绍的另一种解决方案: 告诉解释器去哪里查找。
可在模块 sys
的变量 path
中找到目录列表(即搜索路径)。
import sys, pprint
pprint.pprint(sys.path)
['/opt/conda/lib/python312.zip', '/opt/conda/lib/python3.12', '/opt/conda/lib/python3.12/lib-dynload', '', '/opt/conda/lib/python3.12/site-packages']
这里的要点是,每个字符串都表示一个位置,如果要让解释器能够找到模块,
可将其放在其中任何一个位置中。虽然放在这里显示 的任何一个位置中都可行,
但目录 site-packages
是最佳的选择,因为它就是用来放置模块的。计算机中查看 sys.path
,
找到目录 site-packages
,并将模块保存到这里,
但要使用另一个名称,如 another_hello.py
。然后,尝试像下面这样做:
import another_hello
another_hello.hello()
Hello, world!
只要模块位于类似于 site-packages
这样的地方,所有的程序就都能够导入它。
- 不希望Python解释器的目录中充斥着编写的模块。
- 没有必要的权限,无法将文件保存到Python解释器的目录中。
- 想将模块放在其他地方。
最重要的是,如果将模块放在其他地方,就必须告诉解释器到哪里去查找。前面说过,
要告诉解释器到哪里去查找模块,办法之一是直接修改 sys.path
,但这种做法不常见。
标准做法是将 模块所在的目录包含在环境变量 PYTHONPATH
中。
环境变量 PYTHONPATH
的内容随操作系统而异,但它基本上类似于 sys.path
,也是一个目录列表。
环境变量并不是Python解释器的一部分,而是操作系统的一部分。大致而言,它们类似于Python变量,
但是在Python解释器外面设置的。如果使用的是 bash shell
,
就可使用如下命令将 ~/python
附加到环境变量 PYTHONPATH
末尾:
export PYTHONPATH=$PYTHONPATH:~/python
如果要对所有启动的 shell
都执行这个命令,可将其添加到主目录中的 .bashrc
文件中。
除使用环境变量 PYTHONPATH
外,还可使用路径配置文件。
这些文件的扩展名为 .pth
,位于一 些特殊目录中,包含要添加到 sys.path
中的目录。
为组织模块,可将其编组为包(package)。包其实就是另一种模块,但有趣的是它们可包含其他模块。
模块存储在扩展名为 .py
的文件中,而包则是一个目录。
要被Python视为包,目录必须包含文件 init.py
。
如果像普通模块一样导入包,文件 init.py
的内容就将是包的内容。
例如,如果 有一个名为 constants
的包,而文件 constants/init.py
包含语句 PI = 3.14
,就可以像下面这样做:
import constants
print(constants.PI)
要将模块加入包中,只需将模块文件放在包目录中即可。
还可以在包中嵌套其他包。例如,要创建一个名为 drawing
的包,
其中包含模块 shapes
和 colors
,需要创建如表所示的文件和目录。
下表是一种简单的包布局。
文件/目录 | 描 述 |
---|---|
~/python/ |
PYTHONPATH 中的目录 |
~/python/drawing/ |
包目录(包 drawing ) |
~/python/drawing/ init .py |
包代码(模块 drawing ) |
~/python/drawing/colors.py |
模块 colors |
~/python/drawing/shapes.py |
模块 shapes |
完成这些准备工作后,下面的语句都是合法的:
import drawing # 导入drawing包
import drawing.colors # 导入drawing包中的模块colors
from drawing import shapes # 导入模块shapes
执行第1条语句后,便可使用目录drawing中文件 init.py
的内容,但不能使用模块 shapes
。
请注意,这些语句只是示例,并不用像这里做的那样,先导入包再导入其中的模块。
换而言之,完全可以只使用第2条语句,第3条语句亦如此。