>>> from env_helper import info; info()
页面更新时间: 2024-01-19 23:28:56
运行环境:
Linux发行版本: Debian GNU/Linux 12 (bookworm)
操作系统内核: Linux-6.1.0-17-amd64-x86_64-with-glibc2.36
Python版本: 3.11.2
10.2. 探索模块与包¶
介绍一些标准库模块前,先来说说如何探索模块,这是一种很有用的技能。 因为在使用 Python 语言解决实际问题时会遇到很多很有用的模块,如果能快速而轻松地理解它们,编程工作将有趣得多。
10.2.1. 模块包含什么¶
要探索模块,最直接的方式是使用Python解释器进行研究。为此,首先需要将模块导入。假设你听说有一个名为
copy
的标准模块。
>>> import copy
这个模块是做什么用的呢?它都包含些什么呢?
使用 dir()
函数¶
要查明模块包含哪些东西,可使用函数 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']
变量__all__
¶
在前一节中,我使用简单的列表推导来猜测可在模块 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 *
方式导入时会导入所有不以下划线打头的全局名称。
10.2.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__
字符串是什么呢?你可能还记得,第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的属性值)。
10.2.3. __doc__
属性¶
显然,文档是有关模块信息的自然来源。我之所以到现在才讨论文档,是因为查看模块本身 要快得多。例如,你可能想知道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解释器很可能正在运行,因此获取这些信息只需几秒钟。
10.2.4. 使用源代码¶
在大多数情况下,前面讨论的探索技巧都够用了。但要真正理解Python语言,可能需要了解 一些不阅读源代码就无法了解的事情。事实上,要学习Python,阅读源代码是除动手编写代码外的最佳方式。
实际阅读源代码应该不成问题,但源代码在哪里呢?假设你要阅读标准模块copy的代码,可以在什么地方找到呢?一种办法是像解释器那样通过sys.path来查找,但更快捷的方式是查看模块的特性file 。
>>> print(copy.__file__)
/usr/lib/python3.11/copy.py
找到了!你可在代码编辑器(如IDLE)中打开文件copy.py,并开始研究其工作原理。如果列出的文件名以.pyc结尾,可打开以.py结尾的相应文件。
**警告** 在文本编辑器中打开标准库文件时,存在不小心修改它的风险。这可能会破坏文件。因 此关闭文件时,千万不要保存你可能对其所做的修改。
请注意,有些模块的源代码你完全无法读懂。它们可能是解释器的组成部分(如模块sys), 还可能是使用C语言编写的。
10.2.5. 让模块可用¶
在前面的示例中,我修改了sys.path。sys.path包含一个目录列表,解释器将在这些目录中查找模块。然而,通常你不想这样做。最理想的情况是,sys.path一开始就包含正确的目录。为此有两种办法:将模块放在正确的位置;告诉解释器到哪里去查找。
将模块放在正确的位置
将模块放在正确的位置很容易,只需找出Python解释器到哪里去查找模块,再将文件放在这个地方即可。在你使用的计算机中,如果Python解释器是管理员安装的,而你有没有管理员权限,就可能无法将模块保存到Python使用的目录中。在这种情况下,需要采用随后要介绍的另一种解决方案:告诉解释器去哪里查找。
你可能还记得,可在模块sys的变量path中找到目录列表(即搜索路径)。
>>> import sys, pprint
>>> pprint.pprint(sys.path)
['/home/bk/book-jubook/python/jubook_python/pt01_basic/ch24_module',
'/usr/lib/python311.zip',
'/usr/lib/python3.11',
'/usr/lib/python3.11/lib-dynload',
'',
'/usr/local/lib/python3.11/dist-packages',
'/usr/lib/python3/dist-packages',
'/usr/lib/python3.11/dist-packages']
这里的要点是,每个字符串都表示一个位置,如果要让解释器能够找到模块,可将其放在其中任何一个位置中。虽然放在这里显示 的任何一个位置中都可行,但目录site-packages是最佳的选择,因为它就是用来放置模块的。请你的计算机中查看sys.path,找到目录site-packages,并将代码清单10-4所示的模块保存到这里,但要使用另一个名称,如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中的目录。
10.2.6. 包¶
为组织模块,可将其编组为包(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条语句亦如此。