Python `setuptools` / `distutils`:构建复杂 Python 包与 C 扩展

好的,咱们今天来聊聊 Python 打包这事儿,特别是那些带 C 扩展的复杂包。别担心,我会尽量用大白话,争取让大家听明白,搞懂怎么用 setuptoolsdistutils 把你的代码打包成一个能让别人轻松安装的宝贝。

开场白:为什么要打包?

想象一下,你辛辛苦苦写了一个 Python 库,里面用 C 优化了一些关键部分,性能嗷嗷叫。现在你想分享给你的小伙伴或者发布到 PyPI 上,让全世界的人都能用。难道你要把你的代码一股脑地扔给他们,然后说:“自己编译去吧!”?

这显然不靠谱。

打包就是为了解决这个问题。它可以把你的代码、依赖、编译好的 C 扩展等等,都打包成一个标准格式的文件(比如 .whl 或者 .tar.gz),然后别人只需要用 pip install 一下,就能轻松安装你的库,不用操心编译、依赖这些乱七八糟的事情。

distutilssetuptools:一对好基友

distutils 是 Python 官方提供的打包工具,历史悠久,地位崇高。但是,它功能比较简单,只能满足一些基本的打包需求。

setuptools 是一个第三方库,它在 distutils 的基础上进行了扩展,提供了更多更强大的功能,比如:

  • 依赖管理:自动下载和安装依赖包
  • 插件机制:允许其他库扩展打包过程
  • 自动发现包:自动找到你的 Python 模块和包
  • 支持 C 扩展:方便地编译和打包 C 扩展

现在基本上大家都用 setuptools,因为它更方便、更强大。所以,咱们今天主要讲 setuptools,但也会简单提一下 distutils,毕竟它们是好基友嘛。

setup.py:打包的灵魂

setup.py 是你打包项目的核心文件。它告诉 setuptools 你的项目是什么、有哪些文件、需要哪些依赖等等。

一个最简单的 setup.py 可能是这样的:

from setuptools import setup

setup(
    name='my_package',
    version='0.1.0',
    packages=['my_package'],
)

这个 setup.py 定义了一个名为 my_package 的包,版本号是 0.1.0,并且包含一个名为 my_package 的 Python 包。

setup() 函数:配置你的包

setup() 函数是 setup.py 的核心。它接受很多参数,用于配置你的包。下面是一些常用的参数:

  • name: 包的名字。
  • version: 包的版本号。
  • packages: 一个包含所有 Python 包的列表。
  • py_modules: 一个包含所有 Python 模块的列表(单个 .py 文件)。
  • install_requires: 一个包含所有依赖包的列表。
  • author: 作者的名字。
  • author_email: 作者的邮箱。
  • description: 包的简短描述。
  • long_description: 包的详细描述(通常从 README 文件读取)。
  • url: 包的网站地址。
  • classifiers: 一系列分类器,用于描述你的包的特性(比如编程语言、操作系统、许可证等等)。
  • ext_modules: 一个包含所有 C 扩展的列表(后面会详细讲)。
  • entry_points: 定义命令行工具或者其他入口点。

一个更完整的例子

from setuptools import setup, find_packages

with open("README.md", "r", encoding="utf-8") as fh:
    long_description = fh.read()

setup(
    name="my_package",
    version="0.1.0",
    author="Your Name",
    author_email="[email protected]",
    description="A short description of your package",
    long_description=long_description,
    long_description_content_type="text/markdown",
    url="https://github.com/yourusername/my_package",
    packages=find_packages(),
    classifiers=[
        "Programming Language :: Python :: 3",
        "License :: OSI Approved :: MIT License",
        "Operating System :: OS Independent",
    ],
    python_requires='>=3.6',
    install_requires=[
        "requests",
        "numpy",
    ],
)

这个例子使用了 find_packages() 函数自动查找项目中的所有 Python 包,并从 README.md 文件读取详细描述。它还指定了依赖包 requestsnumpy,以及一些分类器。

C 扩展:让你的 Python 飞起来

C 扩展是用 C 或者 C++ 编写的 Python 模块。它们可以用来优化 Python 代码的性能,或者访问一些 Python 无法直接访问的底层资源。

要构建 C 扩展,你需要使用 setuptools 提供的 Extension 类。

from setuptools import setup, Extension

module1 = Extension('my_package.my_module',
                    sources=['my_package/my_module.c'])

setup(
    name='my_package',
    version='0.1.0',
    packages=['my_package'],
    ext_modules=[module1],
)

这个例子定义了一个名为 my_package.my_module 的 C 扩展,它的源代码是 my_package/my_module.c

Extension 类:配置 C 扩展

Extension 类接受很多参数,用于配置 C 扩展。下面是一些常用的参数:

  • name: 扩展的名字(必须包含包名)。
  • sources: 一个包含所有 C/C++ 源代码文件的列表。
  • include_dirs: 一个包含所有头文件目录的列表。
  • define_macros: 一个包含所有宏定义的列表(用于条件编译)。
  • library_dirs: 一个包含所有库文件目录的列表。
  • libraries: 一个包含所有需要链接的库的列表。
  • extra_compile_args: 一个包含所有额外的编译选项的列表。
  • extra_link_args: 一个包含所有额外的链接选项的列表。

一个更复杂的 C 扩展例子

假设你的 C 扩展依赖于一个外部库 libfoo,并且需要使用一些特定的编译选项。

from setuptools import setup, Extension

module1 = Extension('my_package.my_module',
                    sources=['my_package/my_module.c'],
                    include_dirs=['/usr/local/include'],
                    libraries=['foo'],
                    library_dirs=['/usr/local/lib'],
                    extra_compile_args=['-O3', '-Wall'],
                    extra_link_args=['-pthread'])

setup(
    name='my_package',
    version='0.1.0',
    packages=['my_package'],
    ext_modules=[module1],
)

这个例子指定了头文件目录 /usr/local/include,库文件目录 /usr/local/lib,需要链接的库 libfoo,以及一些额外的编译和链接选项。

MANIFEST.in:包含额外文件

有时候,你的包除了 Python 代码和 C 扩展之外,还包含一些其他文件,比如配置文件、数据文件、文档等等。你需要使用 MANIFEST.in 文件告诉 setuptools 这些文件也需要包含在包里。

MANIFEST.in 文件是一个简单的文本文件,每行指定一个文件或者目录。

include my_package/data.txt
recursive-include my_package/docs *
global-exclude *.pyc

这个例子包含了 my_package/data.txt 文件,递归包含了 my_package/docs 目录下的所有文件,并且排除了所有 .pyc 文件。

构建你的包

有了 setup.pyMANIFEST.in,你就可以构建你的包了。打开终端,进入你的项目目录,然后运行以下命令:

python setup.py sdist bdist_wheel

这个命令会生成两个文件:

  • sdist: 源代码包(通常是一个 .tar.gz 文件)。
  • bdist_wheel: wheel 包(通常是一个 .whl 文件)。

wheel 包是一种二进制包,它已经包含了编译好的 C 扩展,所以安装速度更快。

安装你的包

你可以使用 pip 安装你的包:

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

或者,如果你想从源代码安装,可以这样做:

pip install dist/my_package-0.1.0.tar.gz

pyproject.toml:新的打包标准

pyproject.toml 是一个新的打包配置文件,它正在逐渐取代 setup.py。它使用 TOML 格式,更加清晰易懂。

一个简单的 pyproject.toml 文件可能是这样的:

[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "my_package"
version = "0.1.0"
authors = [
    { name = "Your Name", email = "[email protected]" },
]
description = "A short description of your package"
readme = "README.md"
requires-python = ">=3.6"
classifiers = [
    "Programming Language :: Python :: 3",
    "License :: OSI Approved :: MIT License",
    "Operating System :: OS Independent",
]
dependencies = [
    "requests",
    "numpy",
]

[project.urls]
"Homepage" = "https://github.com/yourusername/my_package"
"Bug Tracker" = "https://github.com/yourusername/my_package/issues"

要使用 pyproject.toml,你需要安装 build 包:

pip install build

然后,你可以使用 build 命令构建你的包:

python -m build

总结:打包,让你的代码飞得更高

打包是 Python 开发中非常重要的一环。它可以让你轻松地分享你的代码,让别人也能享受到你的劳动成果。

  • setuptools 是一个强大的打包工具,它提供了很多功能,可以满足各种各样的打包需求。
  • setup.py 是打包的核心文件,它告诉 setuptools 你的项目是什么、有哪些文件、需要哪些依赖等等。
  • C 扩展可以用来优化 Python 代码的性能,或者访问一些 Python 无法直接访问的底层资源。
  • MANIFEST.in 可以用来包含额外文件。
  • pyproject.toml 是一个新的打包标准,它正在逐渐取代 setup.py

好了,今天的讲座就到这里。希望大家都能掌握 Python 打包的技巧,让你的代码飞得更高!

一些提示和技巧

  • 尽量使用 find_packages() 函数自动查找 Python 包,避免手动列出所有包。
  • 使用 install_requires 指定依赖包,让 pip 自动下载和安装依赖。
  • 使用 classifiers 描述你的包的特性,方便用户搜索和发现你的包。
  • 编写详细的文档,让用户更容易理解和使用你的包。
  • 使用版本控制系统(比如 Git)管理你的代码,方便协作和维护。
  • 发布你的包到 PyPI 上,让全世界的人都能使用你的代码。

常见问题

  • Q: 我应该使用 distutils 还是 setuptools

    A: 除非你有特殊的需求,否则应该使用 setuptools。它更强大、更方便。

  • Q: 我的 C 扩展编译失败了,怎么办?

    A: 检查你的编译选项、头文件目录、库文件目录等等是否正确。确保你的系统已经安装了所有必要的依赖。

  • Q: 我的包安装后无法运行,怎么办?

    A: 检查你的 entry_points 是否正确配置。确保你的脚本或者模块在 PATH 环境变量中。

  • Q: 如何发布我的包到 PyPI 上?

    A: 首先,你需要注册一个 PyPI 账号。然后,你需要安装 twine 包。最后,你可以使用 twine upload 命令上传你的包。

表格:setup() 函数常用参数总结

参数名 描述
name 包的名字。
version 包的版本号。
packages 一个包含所有 Python 包的列表。
py_modules 一个包含所有 Python 模块的列表(单个 .py 文件)。
install_requires 一个包含所有依赖包的列表。
author 作者的名字。
author_email 作者的邮箱。
description 包的简短描述。
long_description 包的详细描述(通常从 README 文件读取)。
url 包的网站地址。
classifiers 一系列分类器,用于描述你的包的特性(比如编程语言、操作系统、许可证等等)。
ext_modules 一个包含所有 C 扩展的列表。
entry_points 定义命令行工具或者其他入口点。
python_requires 指定包所需要的 Python 版本,例如 '>=3.6' 表示需要 Python 3.6 或更高版本。
package_data 用于指定包中需要包含的非 Python 文件,例如 {'my_package': ['*.txt', '*.dat']} 表示 my_package 包中所有 .txt.dat 文件。
exclude_package_data 用于指定包中需要排除的非 Python 文件,例如 {'my_package': ['*.pyc']} 表示 my_package 包中排除所有 .pyc 文件。
data_files 用于指定需要安装到特定位置的数据文件,例如 [('share/my_package', ['data/config.ini'])] 表示将 data/config.ini 文件安装到 share/my_package 目录下。
keywords 一个包含与包相关的关键词的列表,方便用户搜索。
license 包的许可证类型,例如 'MIT''GPL-3.0'

希望这篇文章对你有所帮助! 祝你打包愉快!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注