如何在 Python 包中包含附加文件

PythonBeginner
立即练习

介绍

Python 包是组织和分发代码的强大方式。虽然 Python 脚本(.py 文件)构成了包的核心,但你通常需要包含其他文件,例如配置文件、数据文件、模板或文档。本教程将指导你创建一个包含这些附加资源的 Python 包,使你的包更通用、更有用。

在本实验结束时,你将创建一个包含附加文件的完整 Python 包,并学习如何从你的代码中访问这些文件。

创建一个基本的 Python 包结构

让我们从创建一个基本的 Python 包结构开始。一个包本质上是一个包含 Python 模块和一个特殊的 __init__.py 文件的目录,该文件告诉 Python 应该将此目录视为一个包。

创建包目录结构

首先,让我们为我们的包创建必要的目录:

mkdir -p ~/project/mypackage/data

此命令创建一个名为 mypackage 的目录,其中包含一个子目录 data,用于存储我们的附加文件。

现在,让我们导航到我们的项目目录:

cd ~/project

创建基本的包文件

每个 Python 包都需要在其根目录中有一个 __init__.py 文件。让我们创建这个文件:

touch mypackage/__init__.py

这个空文件告诉 Python mypackage 目录是一个包。

接下来,让我们在我们的包中创建一个简单的 Python 模块:

echo 'def greet():
    print("Hello from mypackage!")' > mypackage/greeting.py

将数据文件添加到包中

现在,让我们将一个数据文件添加到我们的包中。这可以是一个配置文件、一个 CSV 文件或你的包需要的任何其他类型的文件:

echo 'This is sample data for our package.' > mypackage/data/sample.txt

我们还创建一个配置文件:

echo '[config]
debug = true
log_level = INFO' > mypackage/config.ini

验证你的包结构

你可以使用以下命令检查你的包的结构:

find mypackage -type f | sort

你应该看到类似如下的输出:

mypackage/__init__.py
mypackage/config.ini
mypackage/data/sample.txt
mypackage/greeting.py

这是一个基本的 Python 包结构,其中包含一些额外的非 Python 文件。在接下来的步骤中,我们将学习如何在分发包时包含这些文件,以及如何从你的代码中访问它们。

为你的包创建安装脚本

为了在你的 Python 包中正确地包含附加文件,你需要创建一个 setup.py 文件。Python 的打包工具使用此文件来构建和安装你的包。

理解 setup.py

setup.py 文件包含有关你的包的元数据,例如它的名称、版本、作者和依赖项。它还指定了在分发包时应该包含哪些文件。

让我们在你的项目的根目录中创建一个基本的 setup.py 文件:

cd ~/project

现在,使用以下内容创建 setup.py 文件:

cat > setup.py << 'EOF'
from setuptools import setup, find_packages

setup(
    name="mypackage",
    version="0.1",
    packages=find_packages(),
    
    ## Include data files
    package_data={
        "mypackage": ["config.ini", "data/*.txt"],
    },
    
    ## Metadata
    author="Your Name",
    author_email="your.email@example.com",
    description="A simple Python package with additional files",
)
EOF

理解 Package Data 配置

package_data 参数是包含包中附加文件的关键。它接受一个字典,其中:

  • 键是包名(或 "" 代表所有包)
  • 值是相对于包目录的文件模式列表

在我们的示例中,我们包含:

  • 位于我们包根目录的 config.ini 文件
  • data 目录中的所有 .txt 文件

文件模式支持通配符,例如 *,以匹配具有相似名称或扩展名的多个文件。

测试你的安装配置

让我们创建一个虚拟环境来测试我们的包:

python3 -m venv ~/project/venv
source ~/project/venv/bin/activate

现在,让我们以开发模式安装我们的包:

cd ~/project
pip install -e .

-e 标志代表“可编辑”模式,这意味着你可以编辑你的包代码,而无需每次都重新安装它。

你应该看到输出,表明你的包已成功安装:

Successfully installed mypackage-0.1

让我们验证我们的包安装:

python -c "import mypackage.greeting; mypackage.greeting.greet()"

这应该输出:

Hello from mypackage!

你现在已经成功创建了一个 Python 包,该包具有一个包含附加文件的安装脚本。在下一步中,我们将学习如何从你的 Python 代码中访问这些文件。

访问你的包中的附加文件

现在我们已经在包中包含了附加文件,我们需要学习如何从我们的 Python 代码中访问它们。有几种方法可以做到这一点,但最可靠的方法是使用来自 setuptools 包的 pkg_resources 模块。

创建一个模块来访问附加文件

让我们在我们的包中创建一个新模块,演示如何访问附加文件:

cd ~/project

mypackage 目录中创建一个名为 fileaccess.py 的新文件:

cat > mypackage/fileaccess.py << 'EOF'
import os
import pkg_resources

def get_config_path():
    """返回 config.ini 文件的路径。"""
    return pkg_resources.resource_filename('mypackage', 'config.ini')

def read_config():
    """读取并返回 config.ini 文件的内容。"""
    config_path = get_config_path()
    with open(config_path, 'r') as f:
        return f.read()

def get_sample_data_path():
    """返回 sample.txt 文件的路径。"""
    return pkg_resources.resource_filename('mypackage', 'data/sample.txt')

def read_sample_data():
    """读取并返回 sample.txt 文件的内容。"""
    data_path = get_sample_data_path()
    with open(data_path, 'r') as f:
        return f.read()

def list_package_data():
    """列出包含在包数据中的所有文件。"""
    ## 获取包目录
    package_dir = os.path.dirname(pkg_resources.resource_filename('mypackage', '__init__.py'))
    
    ## 列出主包目录中的文件
    main_files = [f for f in os.listdir(package_dir) 
                  if os.path.isfile(os.path.join(package_dir, f))]
    
    ## 列出数据目录中的文件
    data_dir = os.path.join(package_dir, 'data')
    data_files = [f'data/{f}' for f in os.listdir(data_dir) 
                 if os.path.isfile(os.path.join(data_dir, f))]
    
    return main_files + data_files
EOF

更新 init.py 文件

让我们更新 __init__.py 文件以公开我们的新函数:

cat > mypackage/__init__.py << 'EOF'
from mypackage.greeting import greet
from mypackage.fileaccess import (
    get_config_path,
    read_config,
    get_sample_data_path,
    read_sample_data,
    list_package_data
)

__all__ = [
    'greet',
    'get_config_path',
    'read_config',
    'get_sample_data_path',
    'read_sample_data',
    'list_package_data'
]
EOF

测试文件访问函数

让我们创建一个脚本来测试我们的文件访问函数:

cat > ~/project/test_package.py << 'EOF'
import mypackage

## 测试问候函数
print("测试问候函数:")
mypackage.greet()
print()

## 测试配置文件访问
print("配置文件路径:")
print(mypackage.get_config_path())
print("\n配置文件内容:")
print(mypackage.read_config())
print()

## 测试数据文件访问
print("样本数据文件路径:")
print(mypackage.get_sample_data_path())
print("\n样本数据文件内容:")
print(mypackage.read_sample_data())
print()

## 列出所有包数据
print("所有包数据文件:")
for file in mypackage.list_package_data():
    print(f"- {file}")
EOF

现在运行测试脚本:

cd ~/project
python test_package.py

你应该看到类似如下的输出:

测试问候函数:
Hello from mypackage!

配置文件路径:
/home/labex/project/mypackage/config.ini

配置文件内容:
[config]
debug = true
log_level = INFO

样本数据文件路径:
/home/labex/project/mypackage/data/sample.txt

样本数据文件内容:
This is sample data for our package.

所有包数据文件:
- __init__.py
- config.ini
- fileaccess.py
- greeting.py
- data/sample.txt

理解 pkg_resources

pkg_resources 模块提供了一种访问已安装包内资源的方法。resource_filename 函数返回包内文件的路径,无论包安装在哪里。

这种方法确保你的代码可以访问附加文件,无论:

  • 在开发期间从源目录运行
  • 安装在虚拟环境中
  • 系统范围安装
  • 分发并在另一台机器上安装

这使得你的包更具可移植性和可靠性,因为它不依赖于硬编码路径或相对路径,这些路径可能会根据包的使用方式而改变。

构建和分发你的包

现在我们已经创建了一个带有附加文件的 Python 包,并确认我们可以访问它们,让我们学习如何构建和分发这个包。

更新安装脚本

在构建包之前,让我们更新我们的 setup.py 文件以包含更多元数据和需求:

cd ~/project
cat > setup.py << 'EOF'
from setuptools import setup, find_packages

setup(
    name="mypackage",
    version="0.1.0",
    packages=find_packages(),
    
    ## 包含数据文件
    package_data={
        "mypackage": ["config.ini", "data/*.txt"],
    },
    
    ## 依赖项
    install_requires=[
        "setuptools",
    ],
    
    ## 元数据
    author="Your Name",
    author_email="your.email@example.com",
    description="一个带有附加文件的简单 Python 包",
    keywords="sample, package, data",
    url="https://example.com/mypackage",
    classifiers=[
        "Development Status :: 3 - Alpha",
        "Intended Audience :: Developers",
        "Programming Language :: Python :: 3",
        "Programming Language :: Python :: 3.8",
        "Programming Language :: Python :: 3.9",
        "Programming Language :: Python :: 3.10",
    ],
    python_requires=">=3.6",
)
EOF

构建源分发和 Wheel 分发

Python 包可以以多种格式分发,但最常见的是:

  1. 源分发 (sdist):包含源代码和附加文件的 tarball
  2. Wheel 分发 (bdist_wheel):一个预构建的包,无需构建即可安装

让我们创建这两种类型的分发:

## 确保我们有最新的构建工具
pip install --upgrade setuptools wheel

## 构建分发
python setup.py sdist bdist_wheel

你应该看到输出,表明分发已创建,并且新文件应该出现在 dist 目录中。

让我们检查 dist 目录的内容:

ls -l dist

你应该至少看到两个文件:

  • 一个 .tar.gz 文件(源分发)
  • 一个 .whl 文件(wheel 分发)

从分发文件安装包

现在,让我们测试从其中一个分发文件安装包。首先,让我们卸载我们的开发版本:

pip uninstall -y mypackage

现在,让我们安装 wheel 分发:

pip install dist/mypackage-0.1.0-py3-none-any.whl

你应该看到输出,表明该包已成功安装。

让我们验证该包是否已安装,并且我们仍然可以访问附加文件:

python -c "import mypackage; print(mypackage.read_config())"

这应该输出 config.ini 文件的内容:

[config]
debug = true
log_level = INFO

发布你的包

在实际场景中,你通常会将你的包发布到 Python 包索引 (PyPI),以便其他人可以使用 pip install mypackage 安装它。这包括:

  1. 在 PyPI 上创建一个帐户 (https://pypi.org/)
  2. 使用 twine 等工具上传你的分发:
    pip install twine
    twine upload dist/*

但是,对于这个实验,我们将停留在本地创建分发。你现在拥有一个完整的 Python 包,其中包含附加文件,可以由其他人分发和安装。

你创建的总结

  • 一个带有模块和附加文件的 Python 包
  • 一个安装脚本,将这些文件包含在分发中
  • 从你的代码访问这些文件的函数
  • 准备分发的源分发和 wheel 分发文件

这种结构为将来可能要创建的任何 Python 包提供了坚实的基础。

总结

在这个实验中,你已经学习了如何:

  1. 创建一个基本的 Python 包结构,其中包含额外的非 Python 文件
  2. 配置你的 setup.py 以将这些文件包含在包分发中
  3. 使用 pkg_resources 模块从你的 Python 代码访问附加文件
  4. 为分发构建你的包的源分发和 wheel 分发

你现在已经掌握了创建更全面的 Python 包的知识,这些包不仅包含 Python 代码,还包含配置文件、数据文件、模板和其他资源。这种能力对于开发实际应用至关重要,在实际应用中,Python 代码通常需要与外部文件一起工作。

这个实验的一些关键要点:

  • setup() 中使用 package_data 参数来包含附加文件
  • 使用 pkg_resources.resource_filename() 从你的代码可靠地访问这些文件
  • 构建源分发和 wheel 分发以实现最大的兼容性
  • 保持你的包结构井井有条,以使维护更容易

这些知识在你继续开发更复杂的 Python 应用程序和包时将非常有用。