20.3. pkgutil — 扩展包工具

未匹配的标注

目的:为特定的包添加模块搜索路径,并使用包中包含的资源。

pkgutil 模块包括用于更改 Python 包的导入规则的函数以及从包中的分发文件加载非代码资源的函数。

包导入路径

extend_path() 函数用于修改搜索路径并更改从包中导入子模块的方式,以便可以组合多个不同的目录,就像它们是同一个目录一样。 这可用于将开发版本的包覆盖到已安装版本的包,或将特定于平台的模块和共享模块组合到单个包的命名空间中。

调用 extend_path() 的最常用方法是将以下两行代码添加到包中的 __init__.py 。

import pkgutil
__path__ = pkgutil.extend_path(__path__, __name__)

extend_path() 扫描 sys.path 路径以查找包含为和第二个参数给出的包命名相同的子目录的目录。 目录列表与作为第一个参数传递的路径值组合在一起,并作为单个列表返回,适用于包导入路径。

名为 demopkg 的示例包包括两个文件, __ init __.pyshared.pydemopkg1 中的 __init __.py 文件包含 print 语句,用于显示修改前后的搜索路径,以突出显示差异。

demopkg1/init.py

import pkgutil
import pprint

print('demopkg1.__path__ before:')
pprint.pprint(__path__)
print()

__path__ = pkgutil.extend_path(__path__, __name__)

print('demopkg1.__path__ after:')
pprint.pprint(__path__)
print()

extension 目录中带有 demopkg 的附加功能,包含另外三个源文件。 每个目录级别都有一个 __init __.py 和一个 not_shared.py

$ find extension -name '*.py'

extension/__init__.py
extension/demopkg1/__init__.py
extension/demopkg1/not_shared.py

这个简单的测试程序导入了 demopkg1 包。

pkgutil_extend_path.py

import demopkg1
print('demopkg1           :', demopkg1.__file__)

try:
    import demopkg1.shared
except Exception as err:
    print('demopkg1.shared    : Not found ({})'.format(err))
else:
    print('demopkg1.shared    :', demopkg1.shared.__file__)

try:
    import demopkg1.not_shared
except Exception as err:
    print('demopkg1.not_shared: Not found ({})'.format(err))
else:
    print('demopkg1.not_shared:', demopkg1.not_shared.__file__)

当测试程序在命令行直接运行时, not_shared 模块没有找到。

注意

这些示例中的完整文件系统路径已缩短,以强调更改的部分。

$ python3 pkgutil_extend_path.py

demopkg1.__path__ before:
['.../demopkg1']

demopkg1.__path__ after:
['.../demopkg1']

demopkg1           : .../demopkg1/__init__.py
demopkg1.shared    : .../demopkg1/shared.py
demopkg1.not_shared: Not found (No module named 'demopkg1.not_sh
ared')

但是,如果将 extension 目录添加到 PYTHONPATH 并再次运行程序,则会产生不同的结果。

$ PYTHONPATH=extension python3 pkgutil_extend_path.py

demopkg1.__path__ before:
['.../demopkg1']

demopkg1.__path__ after:
['.../demopkg1',
 '.../extension/demopkg1']

demopkg1           : .../demopkg1/__init__.py
demopkg1.shared    : .../demopkg1/shared.py
demopkg1.not_shared: .../extension/demopkg1/not_shared.py

extension 目录中的 demopkg1 版本已添加到搜索路径中,因此在那里找到 not_shared 模块。

以这种方式扩展路径对于将特定于平台的软件包版本与通用软件包相结合非常有用,尤其是在特定于平台的版本包含 C 扩展模块的情况下。

包的开发版本

在开发项目增强功能时,通常需要测试对于已安装软件包的更改。 用开发版本替换已安装的副本可能是个坏主意,因为它不一定正确,系统上的其他工具可能依赖于已安装的软件包。

可以使用 virtualenvvenv 在开发环境中配置完全独立的软件包副本。但是对于小的修改,设置具有所有依赖的虚拟环境的开销可能过大。

另一种选择是使用 pkgutil 来修改属于正在开发的包的模块搜索路径。 但是,在这种情况下,必须反转路径,以便开发版本覆盖已安装的版本。

给定一个包含 __init __.pyoverloaded.py 的包 demopkg2 ,其中正在开发的函数位于 demopkg2/overloaded.py 中。 已安装的版本包含

demopkg2/overloaded.py


def func():
    print('This is the installed version of func().')

demopkg2/__init__.py 包含

demopkg2/init.py

import pkgutil

__path__ = pkgutil.extend_path(__path__, __name__)
__path__.reverse()

reverse() 用于确保在默认位置之前扫描由 pkgutil 添加到搜索路径的任何目录以进行导入。

这个程序导入了 demopkg2.overloaded 然后调用了 func()

pkgutil_devel.py

import demopkg2
print('demopkg2           :', demopkg2.__file__)

import demopkg2.overloaded
print('demopkg2.overloaded:', demopkg2.overloaded.__file__)

print()
demopkg2.overloaded.func()

在没有任何特殊路径处理的情况下运行它会从已安装的 func() 版本生成输出。

$ python3 pkgutil_devel.py

demopkg2           : .../demopkg2/__init__.py
demopkg2.overloaded: .../demopkg2/overloaded.py

This is the installed version of func().

包含的开发目录

$ find develop/demopkg2 -name '*.py'

develop/demopkg2/__init__.py
develop/demopkg2/overloaded.py

以及一个修改版的 overloaded

develop/demopkg2/overloaded.py


def func():
    print('This is the development version of func().')

将在 develop 目录在搜索路径中的情况下运行测试程序时加载。

$ PYTHONPATH=develop python3 pkgutil_devel.py

demopkg2           : .../demopkg2/__init__.py
demopkg2.overloaded: .../develop/demopkg2/overloaded.py

This is the development version of func().

使用 PKG 文件管理路径

第一个例子说明了如何使用 PYTHONPATH 中包含的额外目录扩展搜索路径。 也可以使用包含目录名的 * .pkg 文件添加搜索路径。 PKG 文件类似于 site 模块使用的 PTH 文件。 它们可以包含目录名称,每行一个,以添加到包的搜索路径中。

从第一个示例构建应用程序的特定于平台的部分的另一种方法是为每个操作系统使用单独的目录,并包含一个 .pkg 文件来扩展搜索路径。

此示例使用相同的 demopkg1 文件,还包括以下文件。

$ find os_* -type f

os_one/demopkg1/__init__.py
os_one/demopkg1/not_shared.py
os_one/demopkg1.pkg
os_two/demopkg1/__init__.py
os_two/demopkg1/not_shared.py
os_two/demopkg1.pkg

PKG 文件名为 demopkg1.pkg 以匹配正在扩展的包。 它们都包含一行。

demopkg

此示例程序显示正在导入的模块的版本。

pkgutil_os_specific.py

import demopkg1
print('demopkg1:', demopkg1.__file__)

import demopkg1.shared
print('demopkg1.shared:', demopkg1.shared.__file__)

import demopkg1.not_shared
print('demopkg1.not_shared:', demopkg1.not_shared.__file__)

可以使用简单的包装器脚本在两个包之间切换。

with_os.sh

#!/bin/sh

export PYTHONPATH=os_${1}
echo "PYTHONPATH=$PYTHONPATH"
echo

python3 pkgutil_os_specific.py

当使用 "one""two" 作为参数运行时,路径会被调整。

$ ./with_os.sh one

PYTHONPATH=os_one

demopkg1.__path__ before:
['.../demopkg1']

demopkg1.__path__ after:
['.../demopkg1',
 '.../os_one/demopkg1',
 'demopkg']

demopkg1: .../demopkg1/__init__.py
demopkg1.shared: .../demopkg1/shared.py
demopkg1.not_shared: .../os_one/demopkg1/not_shared.py

$ ./with_os.sh two

PYTHONPATH=os_two

demopkg1.__path__ before:
['.../demopkg1']

demopkg1.__path__ after:
['.../demopkg1',
 '.../os_two/demopkg1',
 'demopkg']

demopkg1: .../demopkg1/__init__.py
demopkg1.shared: .../demopkg1/shared.py
demopkg1.not_shared: .../os_two/demopkg1/not_shared.py

PKG 文件可以出现在普通搜索路径中的任何位置,因此当前工作目录中的单个 PKG 文件也可用于包含开发树。

嵌套包

对于嵌套包,只需要修改顶级包的路径。 例如,使用此目录结构

$ find nested -name '*.py'

nested/__init__.py
nested/second/__init__.py
nested/second/deep.py
nested/shallow.py

nested/__init__.py 包含

nested/init.py

import pkgutil

__path__ = pkgutil.extend_path(__path__, __name__)
__path__.reverse()

以及开发树如下所示

$ find develop/nested -name '*.py'

develop/nested/__init__.py
develop/nested/second/__init__.py
develop/nested/second/deep.py
develop/nested/shallow.py

shallowdeep 模块都包含一个简单的函数来打印一条消息,指示它们是否来自已安装或开发版本。

该测试程序会运行新的包。

pkgutil_nested.py

import nested

import nested.shallow
print('nested.shallow:', nested.shallow.__file__)
nested.shallow.func()

print()
import nested.second.deep
print('nested.second.deep:', nested.second.deep.__file__)
nested.second.deep.func()

在没有任何路径操作的情况下运行 pkgutil_nested.py 时,将使用两个模块的已安装版本。

$ python3 pkgutil_nested.py

nested.shallow: .../nested/shallow.py
This func() comes from the installed version of nested.shallow

nested.second.deep: .../nested/second/deep.py
This func() comes from the installed version of nested.second.de
ep

develop 目录添加到路径时,两个函数的开发版本都会覆盖已安装的版本。

$ PYTHONPATH=develop python3 pkgutil_nested.py

nested.shallow: .../develop/nested/shallow.py
This func() comes from the development version of nested.shallow

nested.second.deep: .../develop/nested/second/deep.py
This func() comes from the development version of nested.second.
deep

包数据

除了代码之外, Python 包还可以包含数据文件,例如模板、默认配置文件、图像以及包中代码使用的其他支持文件。 get_data() 函数以格式无关的方式访问文件中的数据,因此无论软件包是作为 EGG 冻结二进制文件的一部分,还是文件系统上的常规文件分发都无关紧要。

使用包含 templates 目录的包 pkgwithdata

$ find pkgwithdata -type f

pkgwithdata/__init__.py
pkgwithdata/templates/base.html

文件 pkgwithdata/templates/base.html 包含一个简单的HTML模板。

pkgwithdata/templates/base.html

<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
<html> <head>
<title>PyMOTW Template</title>
</head>

<body>
<h1>Example Template</h1>

<p>This is a sample data file.</p>

</body>
</html>

该程序使用 get_data() 来检索模板内容并将其打印出来。

pkgutil_get_data.py

import pkgutil

template = pkgutil.get_data('pkgwithdata', 'templates/base.html')
print(template.decode('utf-8'))

get_data() 的参数是包的点名,以及相对于包顶部的文件名。 返回值是一个字节序列,因此在打印之前进行 UTF-8 解码。

$ python3 pkgutil_get_data.py

<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
<html> <head>
<title>PyMOTW Template</title>
</head>

<body>
<h1>Example Template</h1>

<p>This is a sample data file.</p>

</body>
</html>

get_data() 是与分发格式无关的,因为它使用 PEP 302 中定义的导入钩子来访问包内容。 可以使用任何提供钩子的加载器,包括 zipfile 中的 ZIP 压缩包导入器。

pkgutil_get_data_zip.py

import pkgutil
import zipfile
import sys

# 使用当前目录中的代码创建 ZIP 文件
# 和使用未出现在本地文件系统上的名称的模板。
with zipfile.PyZipFile('pkgwithdatainzip.zip', mode='w') as zf:
    zf.writepy('.')
    zf.write('pkgwithdata/templates/base.html',
             'pkgwithdata/templates/fromzip.html',
             )

# 将ZIP文件添加到导入路径。
sys.path.insert(0, 'pkgwithdatainzip.zip')

# 导入 pkgwithdata 以显示它来自 ZIP 压缩包。
import pkgwithdata
print('Loading pkgwithdata from', pkgwithdata.__file__)

# 打印模板主体
print('\nTemplate:')
data = pkgutil.get_data('pkgwithdata', 'templates/fromzip.html')
print(data.decode('utf-8'))

这个例子使用 PyZipFile.writepy() 创建一个 ZIP 压缩包,其中包含 pkgwithdata 包的副本,包括模板文件的重命名版本。 然后在使用 pkgutil 加载模板并打印之前,将 ZIP 压缩包添加到导入路径。 有关使用 writepy() 的更多详细信息,请参阅 zipfile 的讨论。

$ python3 pkgutil_get_data_zip.py

Loading pkgwithdata from
pkgwithdatainzip.zip/pkgwithdata/__init__.pyc

Template:
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
<html> <head>
<title>PyMOTW Template</title>
</head>

<body>
<h1>Example Template</h1>

<p>This is a sample data file.</p>

</body>
</html>

另请参阅

本文章首发在 LearnKu.com 网站上。

本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://learnku.com/docs/pymotw/pkgutil-...

译文地址:https://learnku.com/docs/pymotw/pkgutil-...

上一篇 下一篇
讨论数量: 0
发起讨论 只看当前版本


暂无话题~