>>> 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一开始就包含正确的目录。为此有两种办法:将模块放在正确的位置;告诉解释器到哪里去查找。

  1. 将模块放在正确的位置

将模块放在正确的位置很容易,只需找出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 的包,其中包含模块 shapescolors ,需要创建如表所示的文件和目录。

表: 一种简单的包布局

文件/目录

描 述

~/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条语句亦如此。