注意: 虽然 JavaScript 对于本网站不是必需的,但您与内容的互动将受到限制。请开启 JavaScript 以获得完整的体验。

Python 1.5 中内置包支持

Python 1.5 中内置包支持

从 Python 1.5a4 版本开始,包支持已内置到 Python 解释器中。这实现了一个略微简化和修改过的包导入语义版本,该语义由“ni”模块首创。

“包导入”是一种通过使用“带点的模块名”来组织 Python 模块命名空间的方法。例如,模块名 A.B 表示包 A 中的一个名为 B 的子模块。正如使用模块使不同模块的作者不必担心彼此的全局变量名一样,使用带点的模块名使 NumPy 或 PIL 等多模块包的作者不必担心彼此的模块名。

从 Python 1.3 版本开始,包导入由一个标准的 Python 库模块“ni”支持。(这个名字据说是 New Import 的首字母缩写,但实际上指的是电影《巨蟒与圣杯》中“说 Ni 的骑士”,在亚瑟王的骑士带着灌木丛返回后,他们改名为“说 Neeeow ... Wum ... Ping 的骑士”——但那是另一个故事了。)

ni 模块除了对 Python 解析器进行了一些修改(也在 1.3 中引入)以接受“import A.B.C”和“from A.B.C import X”形式的 import 语句之外,全部是用户代码。当 ni 未启用时,使用此语法会导致运行时错误“No such module”。一旦 ni 启用(通过在导入其他模块之前执行“import ni”),ni 的导入钩子将查找正确包的子模块。

新的包支持旨在类似于 ni,但已进行了简化,并且一些功能已更改或删除。

一个例子

假设您想设计一个用于统一处理声音文件和声音数据的包。存在许多不同的声音文件格式(通常通过其扩展名识别,例如 .wav、.aiff、.au),因此您可能需要创建和维护一个不断增长的模块集合,用于在各种文件格式之间进行转换。您还可能希望对声音数据执行许多不同的操作(例如混音、添加回声、应用均衡器功能、创建人工立体声效果),因此您还将编写源源不断的模块来执行这些操作。以下是您的包可能的结构(以分层文件系统的形式表示)

Sound/				Top-level package
      __init__.py		Initialize the sound package
      Utils/			Subpackage for internal use
            __init__.py
            iobuffer.py
	    errors.py
	    ...
      Formats/			Subpackage for file format conversions
              __init__.py
              wavread.py
	      wavwrite.py
	      aiffread.py
	      aiffwrite.py
	      auread.py
	      auwrite.py
	      ...
      Effects/			Subpackage for sound effects
              __init__.py
	      echo.py
	      surround.py
	      reverse.py
	      ...
      Filters/			Subpackage for filters
              __init__.py
              equalizer.py
	      vocoder.py
	      karaoke.py
	      dolby.py
	      ...

包的用户可以从包中导入单个模块,例如

import Sound.Effects.echo
这将加载子模块 Sound.Effects.echo。它必须用其完整名称引用,例如 Sound.Effects.echo.echofilter(input, output, delay=0.7, atten=4)

from Sound.Effects import echo
这也加载了子模块 echo,并使其无需包前缀即可使用,因此可以按以下方式使用:echo.echofilter(input, output, delay=0.7, atten=4)

from Sound.Effects.echo import echofilter
同样,这会加载子模块 echo,但这会使其函数 echofilter 直接可用:echofilter(input, output, delay=0.7, atten=4)

请注意,当使用 from package import item 时,item 可以是包的子模块(或子包),也可以是包中定义的其他名称,如函数、类或变量。import 语句首先测试 item 是否在包中定义;如果没有,它假定它是一个模块并尝试加载它。如果找不到,则会引发 ImportError。

相反,当使用诸如 import item.subitem.subsubitem 这样的语法时,除了最后一个之外的每个 item 都必须是一个包;最后一个 item 可以是一个模块或一个包,但不能是前一个 item 中定义的类、函数或变量。

从包导入 *;__all__ 属性

现在,当用户编写 from Sound.Effects import * 时会发生什么?理想情况下,人们希望这会以某种方式访问文件系统,查找包中存在哪些子模块,并全部导入它们。不幸的是,此操作在 Mac 和 Windows 平台上运行不佳,因为文件系统并不总是具有关于文件名字母大小写的准确信息!在这些平台上,无法保证知道文件 ECHO.PY 应该作为模块 echo、Echo 还是 ECHO 导入。(例如,Windows 95 有一种令人讨厌的做法,即所有文件名都以大写字母开头。)DOS 8+3 文件名限制为长模块名增加了另一个有趣的问题。

唯一的解决方案是让包作者提供包的显式索引。import 语句使用以下约定:如果包的 __init__.py 代码定义了一个名为 __all__ 的列表,则当遇到 from package import * 时,它将被视为应导入的模块名列表。包作者有责任在发布新版本包时保持此列表最新。如果包作者认为从其包导入 * 没有用处,他们也可以决定不支持它。例如,文件 Sounds/Effects/__init__.py 可能包含以下代码

__all__ = ["echo", "surround", "reverse"]
这意味着 from Sound.Effects import * 将导入 Sound 包的三个命名子模块。

如果未定义 __all__,语句 from Sound.Effects import * 不会将包 Sound.Effects 中的所有子模块导入到当前命名空间中;它只确保包 Sound.Effects 已导入(可能运行其初始化代码 __init__.py),然后导入包中定义的任何名称。这包括 __init__.py 定义的任何名称(以及显式加载的子模块)。它还包括先前 import 语句显式加载的包的任何子模块,例如

import Sound.Effects.echo
import Sound.Effects.surround
from Sound.Effects import *
在此示例中,echo 和 surround 模块被导入到当前命名空间中,因为它们在执行 from...import 语句时在 Sound.Effects 包中定义。(这在定义 __all__ 时也适用。)

请注意,通常不鼓励从模块或包导入 * 的做法,因为它经常导致代码难以阅读。但是,在交互式会话中为了节省打字是可以使用它的,并且某些模块旨在只导出遵循特定模式的名称。

请记住,使用 from Package import specific_submodule 没有任何问题!事实上,除非导入模块需要使用来自不同包的同名子模块,否则这会成为推荐的表示法。

包内引用

子模块通常需要相互引用。例如,surround 模块可能会使用 echo 模块。事实上,此类引用非常常见,以至于 import 语句在标准模块搜索路径中查找之前,首先会在包含包中查找。因此,surround 模块只需使用 import echofrom echo import echofilter。如果在当前包(当前模块所属的包)中找不到导入的模块,import 语句将查找具有给定名称的顶层模块。

当包被组织成子包时(如示例中的 Sound 包),没有捷径可以引用兄弟包的子模块——必须使用子包的完整名称。例如,如果模块 Sound.Filters.vocoder 需要使用 Sound.Effects 包中的 echo 模块,它可以使用 from Sound.Effects import echo

(人们可以设计一种表示法来引用父包,类似于 Unix 和 Windows 文件系统中“..”用于引用父目录的用法。事实上,ni 支持使用 __ 表示包含当前模块的包,__.__ 表示父包等等。此功能因其笨拙而被取消;由于大多数包将具有相对较浅的子结构,因此这不是一个很大的损失。)

细节

包也是模块!

警告:对于熟悉 Java 包表示法(类似于 Python 但有所不同)的人来说,以下内容可能会令人困惑。

每当加载包的子模块时,Python 都会确保包本身首先加载,必要时加载其 __init__.py 文件。包也是如此。因此,当执行语句 import Sound.Effects.echo 时,它首先确保 Sound 已加载;然后确保 Sound.Effects 已加载;只有在那之后,它才确保 Sound.Effects.echo 已加载(如果之前未加载则加载)。

一旦加载,包和模块之间的区别就微乎其微了。事实上,两者都由模块对象表示,并且都存储在已加载模块表 sys.modules 中。sys.modules 中的键是模块的完整带点名称(这不总是与 import 语句中使用的名称相同)。这也是 __name__ 变量的内容(它给出模块或包的完整名称)。

__path__ 变量

包和模块之间的一个区别在于 __path__ 变量的存在与否。这只存在于包中。它被初始化为一个包含包目录名(sys.path 上某个目录的子目录)的列表,其中只有一个项目。更改 __path__ 会更改搜索包子模块的目录列表。例如,Sound.Effects 包可能包含特定于平台的子模块。它可以使用以下目录结构

Sound/
      __init__.py
      Effects/			# Generic versions of effects modules
              __init__.py
              echo.py
	      surround.py
	      reverse.py
	      ...
              plat-ix86/	# Intel x86 specific effects modules
	                echo.py
			surround.py
	      plat-PPC/		# PPC specific effects modules
	                echo.py

Effects/__init__.py 文件可以操作其 __path__ 变量,使得适当的特定于平台的子目录位于主 Effects 目录之前,这样某些效果的特定于平台的实现(如果可用)会覆盖通用(可能较慢)的实现。例如

platform = ...			# Figure out which platform applies
dirname = __path__[0]		# Package's main folder
__path__.insert(0, os.path.join(dirname, "plat-" + platform))

如果不希望特定于平台的子模块隐藏具有相同名称的通用模块,则应使用 __path__.append(...) 而不是 __path__.insert(0, ...)

请注意,plat-* 子目录不是 Effects 的子包——文件 Sound/Effects/plat-PPC/echo.py 对应于模块 Sound.Effects.echo。

sys.modules 中的占位符条目

在使用包时,您偶尔会在 sys.modules 中发现虚假条目,例如 sys.modules['Sound.Effects.string'] 的值可能为 None。这是一个“间接”条目,因为 Sound.Effects 包中的某个子模块导入了顶层 string 模块而创建的。它的目的是一个重要的优化:因为 import 语句无法判断需要本地模块还是全局模块,并且因为规则规定本地模块(在同一个包中)会隐藏同名的全局模块,所以 import 语句必须在查找(可能已经导入的)全局模块之前搜索包的搜索路径。由于搜索包的路径是相对昂贵的操作,而导入已经导入的模块应该很便宜(大约一两次字典查找),因此需要进行优化。当同一个全局模块被同一个包的子模块第二次导入时,占位符条目可以避免搜索包的路径。

占位符条目仅为在顶层找到的模块创建;如果根本找不到模块,导入将失败,通常不需要优化。此外,在交互式使用中,用户可以将模块创建为包本地子模块并重试导入;如果已创建占位符条目,则不会找到。如果用户通过创建与包中已使用的全局模块同名的本地子模块来更改包结构,结果通常被称为“混乱”,正确的解决方案是退出解释器并重新开始。

如果我的模块和包同名怎么办?

您可能有一个目录(在 sys.path 上)既有模块 spam.py 又有子目录 spam,其中包含 __init__.py(如果没有 __init__.py,目录将不被识别为包)。在这种情况下,子目录具有优先权,导入 spam 将忽略 spam.py 文件,而是加载包 spam。如果您希望模块 spam.py 具有优先权,则必须将其放置在 sys.path 中更靠前的目录中。

(提示:搜索顺序由函数 imp.get_suffixes() 返回的后缀列表决定。通常,后缀按以下顺序搜索:“.so”、“module.so”、“.py”、“.pyc”。目录没有明确出现在此列表中,但优先于其中的所有条目。)

安装包的建议

为了让 Python 程序使用一个包,该包必须能被 import 语句找到。换句话说,该包必须是 sys.path 上某个目录的子目录。

传统上,确保包在 sys.path 上的最简单方法是将其安装在标准库中,或者让用户通过设置其 $PYTHONPATH shell 环境变量来扩展 sys.path。在实践中,这两种解决方案很快就会导致混乱。

专用目录

在 Python 1.5 中,建立了一个约定,通过赋予系统管理员更多控制权来防止混乱。首先,在默认搜索路径的末尾添加了两个额外的目录(如果安装前缀和 exec_prefix 不同,则为四个)。这些目录相对于安装前缀(默认为 /usr/local)

  • $prefix/lib/python1.5/site-packages
  • $prefix/lib/site-python

site-packages 目录可用于可能依赖 Python 版本的包(例如,包含共享库或使用新功能的包)。site-python 目录用于与 Python 1.4 的向后兼容性,以及与所使用的 Python 版本无关的纯 Python 包或模块。

这些目录的推荐用法是将每个包放置在其自己的子目录中,无论是 site-packages 还是 site-python 目录。子目录应为包名,包名应可作为 Python 标识符。然后,任何 Python 程序都可以通过给出其完整名称来导入包中的模块。例如,示例中使用的 Sound 包可以安装在 $prefix/lib/python1.5/site-packages/Sound 目录中,以启用诸如 import Sound.Effects.echo 的导入语句)。

添加间接层

有些站点希望将他们的包安装在其他地方,但仍然希望所有用户运行的所有 Python 程序都能导入它们。这可以通过两种不同的方式实现

符号链接
如果包是为带点名称导入而构建的,请在其顶级目录的 site-packages 或 site-python 目录中放置一个符号链接。符号链接的名称应为包名;例如,Sound 包可以有一个符号链接 $prefix/lib/python1.5/site-packages/Sound,指向 /usr/home/soundguru/lib/Sound-1.1/src。

路径配置文件
如果包确实需要向 sys.path 添加一个或多个目录(例如,因为它尚未构建为支持带点名称导入),则可以在 site-python 或 site-packages 目录中放置一个名为 package.pth 的“路径配置文件”。此文件中的每一行(注释和空行除外)都被视为包含一个目录名,该目录名将附加到 sys.path。允许使用相对路径名,并相对于包含 .pth 文件的目录进行解释。

.pth 文件按字母顺序读取,区分大小写与本地文件系统相同。这意味着,如果您发现无法抗拒地想要玩弄目录搜索顺序,至少您可以以可预测的方式进行操作。(这并不等同于认可。典型的安装应该没有或很少有 .pth 文件,否则就出问题了;如果您需要玩弄搜索顺序,那么问题非常大。尽管如此,有时确实需要,这就是您必须这样做的方式。)

Mac 和 Windows 平台注意事项

在 Mac 和 Windows 上,约定略有不同。这些平台上包安装的常规目录是 Python 安装目录的根目录(或子目录),该目录特定于已安装的 Python 版本。这也是搜索路径配置文件 (*.pth) 的(唯一)目录。

标准库目录的子目录

由于 sys.path 上任何目录的任何子目录现在都可以隐式地用作包,人们可能会很容易混淆它们是否旨在如此。例如,假设有一个名为 tkinter 的子目录,其中包含模块 Tkinter.py。是应该写 import Tkinter 还是 import tkinter.Tkinter?如果 tkinter 子目录在路径上,两者都将有效,但这会造成不必要的混淆。

我建立了一个简单的命名约定,应该可以消除这种混淆:非包目录的名称中必须包含连字符。特别是,所有特定于平台的子目录(sunos5、win、mac 等)都已重命名为带有“plat-”前缀的名称。特定于尚未转换为包的可选 Python 组件的子目录已重命名为带有“lib-”前缀的名称。dos_8x3 子目录已重命名为 dos-8x3。下表列出了所有重命名的目录

旧名称新名称
tkinterlib-tk
stdwinlib-stdwin
sharedmoduleslib-dynload
dos_8x3dos-8x3
aix3plat-aix3
aix4plat-aix4
freebsd2plat-freebsd2
genericplat-generic
irix5plat-irix5
irix6plat-irix6
linux1plat-linux1
linux2plat-linux2
next3plat-next3
sunos4plat-sunos4
sunos5plat-sunos5
winplat-win
testtest

请注意,test 子目录未重命名。它现在是一个包。要调用它,请使用诸如 import test.autotest 的语句。

其他内容

XXX 我还没有时间写完以下项目的讨论
  • 新的 imp 函数。
  • 未来方向。
  • ihooks 的未来。
  • 未来命名空间重组。
  • 如何处理 ni?禁用它并强制使用 oldni?

    ni 的变化

    ni 的以下功能并未完全复制。除非您当前正在使用 ni 模块并希望迁移到内置包支持,否则请忽略本节。

    删除了 __domain__

    默认情况下,当包 A.B.C 的子模块导入模块 X 时,ni 会按 A.B.C.X、A.B.X、A.X 和 X 的顺序搜索。这是由包中的 __domain__ 变量定义的,该变量可以设置为要搜索的包名列表。内置包支持中取消了此功能。相反,搜索总是先查找 A.B.C.X,然后查找 X。(这是对 Python 其他地方成功使用的命名空间解析的“两范围”方法的逆转。)

    删除了 __

    使用 ni,包可以使用特殊的名称“__”(两个下划线)来使用显式的“相对”模块名。例如,包 A.B.C 中的模块可以通过 __.__.K.module 形式的名称引用包 A.B.K 中定义的模块。此功能因其有限的用途和较差的可读性而被取消。

    __init__ 不兼容的语义

    使用 ni,包内的 __init__.py 文件(如果存在)将作为包的标准子模块导入。内置包支持则将 __init__.py 文件加载到包的命名空间中。这意味着如果包 A 中的 __init__.py 定义了一个名称 x,则无需进一步操作即可将其引用为 A.x。使用 ni,__init__.py 必须包含 __.x = x 形式的赋值才能达到相同的效果。

    此外,新的包支持要求存在 __init__ 模块;在 ni 下,它是可选的。这是 Python 1.5b1 中引入的一项更改;它旨在避免使用常用名称(如“string”)的目录无意中隐藏在模块搜索路径中稍后出现的有效模块。

    希望向后兼容 ni 的包可以测试特殊变量 __ 是否存在,例如

    # Define a function to be visible at the package level
    def f(...): ...
    
    try:
        __
    except NameError:    # new built-in package support
        pass
    else:                # backwards compatibility for ni
        __.f = f