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

好的,各位观众老爷,欢迎来到今天的“Python 包裹大法:从入门到入土,C 扩展也不怕!” 讲座。我是你们的老朋友,包治百病,哦不,包罗万象的 Python 包裹师傅。

今天咱们要聊的是 Python 的 setuptoolsdistutils,这两个家伙可是 Python 包裹界的扛把子,专门负责把你的 Python 代码、C 扩展、数据文件等等,打包成一个方便快捷、人见人爱的包裹,供大家下载安装。

我知道,一开始看到 setuptoolsdistutils,很多人都会一脸懵逼:“这俩啥玩意儿?有啥区别?我该用哪个?” 别慌,听我慢慢道来。

distutils:老牌劲旅,但已显疲态

distutils 其实是 Python 的标准库自带的,相当于 Python 的“亲儿子”。它历史悠久,资格老,但是功能相对简单,很多时候不太够用。就像你家里的老式自行车,能骑,但是爬坡有点费劲,功能也比较单一。

setuptools:功能强大,社区支持广泛

setuptools 则是社区开发的,相当于 Python 的“干儿子”。它功能更强大,提供了很多高级特性,比如:

  • 依赖管理: 可以自动下载和安装你的包所依赖的其他包。
  • 入口点 (Entry Points): 可以让你定义的函数或类直接通过命令行调用。
  • 插件机制: 可以方便地扩展 setuptools 的功能。

setuptools 就像一辆性能卓越的山地车,爬坡轻松,功能丰富,还能改装升级。

那么,我该用哪个呢?

答案很简单:永远选择 setuptools

distutils 已经被 setuptools 取代,它不会再更新,而且 setuptools 已经包含了 distutils 的所有功能,并且提供了更多更强大的特性。就像你不会再用老式自行车去参加山地车比赛一样,别再纠结 distutils 了,拥抱 setuptools 吧!

好了,废话不多说,咱们开始实战!

1. 创建一个简单的 Python 包

首先,咱们创建一个最简单的 Python 包,只有一个 .py 文件。

my_package/
├── my_module.py
└── setup.py

my_module.py 的内容:

def greet(name):
  """
  向指定的人打招呼。
  """
  return f"Hello, {name}!"

setup.py 的内容:

from setuptools import setup, find_packages

setup(
    name='my_package',  # 包的名字
    version='0.1.0',  # 包的版本
    description='A simple Python package',  # 包的描述
    author='Your Name',  # 作者
    author_email='[email protected]',  # 作者邮箱
    packages=find_packages(),  # 自动查找包下面的所有模块
    # install_requires=['requests'],  # 依赖的其他包,这里先不加
)

setup.py 文件详解:

  • name: 包的名字,也就是你以后 pip install 的时候用的名字。
  • version: 包的版本号,遵循语义化版本规范 (Semantic Versioning)。
  • description: 包的简单描述,方便别人了解你的包是干嘛的。
  • author: 作者的名字。
  • author_email: 作者的邮箱。
  • packages: 这个是关键!find_packages() 函数会自动查找当前目录下所有包含 __init__.py 文件的目录,并将它们作为包包含进来。
  • install_requires: 列出你的包所依赖的其他包,setuptools 会自动下载和安装这些依赖。

2. 打包你的代码

打开你的终端,进入 my_package 目录,然后运行以下命令:

python setup.py sdist bdist_wheel

这条命令会生成两个压缩包:

  • sdist:源码包 (source distribution),包含你的 Python 代码、setup.py 文件和其他必要的文件。
  • bdist_wheel:编译后的包 (built distribution),是一种二进制格式的包,安装速度更快。

这两个压缩包都会放在 dist 目录下。

3. 安装你的包

你可以使用 pip 安装你刚刚打包好的包:

pip install dist/my_package-0.1.0-py3-none-any.whl  # 安装 wheel 包
# 或者
pip install dist/my_package-0.1.0.tar.gz  # 安装源码包

安装完成后,你就可以在你的 Python 代码中使用 my_module 了:

import my_module

print(my_module.greet("World"))  # 输出:Hello, World!

4. 添加依赖

假设你的包依赖 requests 库,你需要在 setup.py 文件中添加 install_requires 参数:

from setuptools import setup, find_packages

setup(
    name='my_package',
    version='0.1.0',
    description='A simple Python package',
    author='Your Name',
    author_email='[email protected]',
    packages=find_packages(),
    install_requires=['requests'],  # 添加 requests 依赖
)

然后重新打包你的代码,安装后,setuptools 会自动安装 requests 库。

5. 添加数据文件

有时候,你的包需要包含一些数据文件,比如配置文件、图像文件等等。你可以使用 package_data 参数来指定这些文件:

from setuptools import setup, find_packages

setup(
    name='my_package',
    version='0.1.0',
    description='A simple Python package',
    author='Your Name',
    author_email='[email protected]',
    packages=find_packages(),
    install_requires=['requests'],
    package_data={
        'my_package': ['data/*.txt'],  # 包含 my_package/data 目录下的所有 .txt 文件
    },
)

注意: package_data 是一个字典,key 是包名,value 是一个包含文件模式的列表。文件模式可以使用通配符,比如 *.txt 表示所有 .txt 文件。

6. C 扩展:进阶玩法

现在,咱们来点高级的:如何打包包含 C 扩展的 Python 包?

首先,你需要一个 C 语言源文件,比如 my_module.c

#include <Python.h>

static PyObject* greet(PyObject* self, PyObject* args) {
  const char* name;
  if (!PyArg_ParseTuple(args, "s", &name)) {
    return NULL;
  }

  char greeting[256];
  snprintf(greeting, sizeof(greeting), "Hello from C, %s!", name);

  return PyUnicode_FromString(greeting);
}

static PyMethodDef MyModuleMethods[] = {
  {"greet",  greet, METH_VARARGS, "Greet someone from C."},
  {NULL, NULL, 0, NULL}        /* Sentinel */
};

static struct PyModuleDef mymodule = {
    PyModuleDef_HEAD_INIT,
    "my_module",   /* name of module */
    NULL, /* module documentation, may be NULL */
    -1,       /* size of per-interpreter state of the module,
                 or -1 if the module keeps state in global variables. */
    MyModuleMethods
};

PyMODINIT_FUNC
PyInit_my_module(void)
{
    return PyModule_Create(&mymodule);
}

这个 C 代码定义了一个 greet 函数,它接受一个字符串参数,并返回一个包含问候语的字符串。

然后,你需要修改 setup.py 文件,告诉 setuptools 如何编译这个 C 扩展:

from setuptools import setup, Extension

setup(
    name='my_package',
    version='0.1.0',
    description='A simple Python package with a C extension',
    author='Your Name',
    author_email='[email protected]',
    packages=['my_package'],
    ext_modules=[
        Extension(
            'my_package.my_module',  # 扩展模块的名字
            ['my_package/my_module.c'],  # C 语言源文件
        ),
    ],
)

ext_modules 参数:

  • ext_modules 是一个列表,包含 Extension 对象。
  • Extension 对象指定了 C 扩展模块的名字和源文件。
  • 'my_package.my_module' 表示扩展模块的名字,必须与 Python 包的结构对应。
  • ['my_package/my_module.c'] 表示 C 语言源文件的路径。

注意: 你需要安装 C 编译器才能编译 C 扩展。在 Linux 上,你可以使用 gcc;在 Windows 上,你需要安装 Visual Studio。

重新打包你的代码,安装后,你就可以在你的 Python 代码中使用 C 扩展了:

import my_package.my_module

print(my_package.my_module.greet("World"))  # 输出:Hello from C, World!

7. Entry Points:让你的代码更易用

entry_pointssetuptools 的一个非常强大的特性,它可以让你定义的函数或类直接通过命令行调用。

假设你想要创建一个命令行工具,可以用来向指定的人打招呼。你可以在 setup.py 文件中添加 entry_points 参数:

from setuptools import setup, find_packages

setup(
    name='my_package',
    version='0.1.0',
    description='A simple Python package with a command-line interface',
    author='Your Name',
    author_email='[email protected]',
    packages=find_packages(),
    entry_points={
        'console_scripts': [
            'greet = my_module:greet',  # greet 命令对应 my_module.py 中的 greet 函数
        ],
    },
)

entry_points 参数:

  • entry_points 是一个字典,key 是入口点的类型,value 是一个包含入口点定义的列表。
  • 'console_scripts' 表示命令行脚本入口点。
  • 'greet = my_module:greet' 表示 greet 命令对应 my_module.py 中的 greet 函数。

重新打包你的代码,安装后,你就可以在命令行中使用 greet 命令了:

greet World  # 输出:Hello, World!

8. 一些技巧和注意事项

  • 使用 .gitignore 文件: 在你的包的根目录下创建一个 .gitignore 文件,排除一些不需要打包的文件,比如 .pyc 文件、__pycache__ 目录等等。
  • 使用 MANIFEST.in 文件: 如果你需要包含一些 find_packages() 无法自动找到的文件,比如数据文件、文档等等,可以使用 MANIFEST.in 文件来指定这些文件。
  • 使用 requirements.txt 文件: 你可以使用 pip freeze > requirements.txt 命令将你的项目的依赖列表导出到一个 requirements.txt 文件中,然后在 setup.py 文件中使用 install_requires 参数来读取这个文件。
  • 发布你的包到 PyPI: 你可以将你的包发布到 PyPI (Python Package Index),让全世界的人都可以下载和使用你的包。你需要注册一个 PyPI 账号,然后使用 twine 工具上传你的包。

表格总结:distutils vs setuptools

特性 distutils setuptools
标准库
依赖管理
入口点 (Entry Points)
插件机制
功能 简单 强大
社区支持 较少 广泛
是否推荐使用

最后,送给大家一句忠告:

打包虽好,可不要贪杯哦! 合理规划你的包结构,编写清晰的 setup.py 文件,才能让你的代码更好地服务于人民群众!

今天的讲座就到这里,感谢大家的观看! 如果有什么问题,欢迎在评论区留言,我会尽力解答。 下次再见!

发表回复

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