You need to enable JavaScript to run this app. 如何使用同一份setup.py实现Python包的cythonized与非cythonized开发安装
优惠活动
大模型
产品
解决方案
定价
更多

如何使用同一份setup.py实现Python包的cythonized与非cythonized开发安装

单份setup.py兼容Python源码部署与Cython编译部署实现方案

现有一套纯Python3编写的包,需要在不同场景下分别以Cython编译形式或原始脚本形式部署,所有源码均为纯Python3代码,要求尽可能使用同一份setup.py实现所有场景的需求。

用例类型命令就地安装包含.py模块包含Cython编译.so模块
1. 开发场景pip3 install -e .是是无
2. 未优化安装pip3 install .否是无
3. Cython编译安装pip3 install . --install-option="--cythonize"否否(仅保留__init__.py)是
4. 构建二进制wheelpython3 setup.py bdist_wheel否否(仅保留__init__.py)是

已实现仅包含Cython编译.so共享库、不包含原始.py模块的二进制wheel包构建逻辑(覆盖用例4),通过自定义build_py类实现。现在需要补充逻辑覆盖剩余3个用例。

遇到的核心问题

  1. 用例1、2不需要执行Cython编译,不应指定ext_modules参数:
    • 调用setup()前很难直接检测用户请求的操作类型,不确定是否通过重写install和develop类实现更合理
    • 若采用重写命令类的方案,是否可以合法清空ext_modules,如何避免cythonize()被提前求值
  2. 用例2场景下执行pip3 install .会构建egg包,且egg包中意外包含.so文件,不确定是否是cythonize()全局执行导致,如何避免该问题
  3. 当前方案会同时打包不需要的源码和Cython编译模块,如何实现Cython编译场景下仅安装__init__.py、不安装其余源码模块

解决方案

核心思路

将Cython编译逻辑改为条件触发,仅当用户执行bdist_wheel命令,或install命令携带--cythonize参数时,才执行Cython编译逻辑,其余场景均走纯Python源码安装逻辑:

  • 提前解析命令行参数判断当前执行的命令类型,避免cythonize()被不必要的场景提前执行
  • 动态决定ext_modules参数和cmdclass的注册逻辑,不需要编译的场景直接使用默认的setuptools逻辑
  • Cython编译场景下保留原有的build_py过滤逻辑,自动过滤掉非__init__.py的源码文件

完整实现代码

import fnmatch
import sys
from setuptools import setup, find_packages, Extension
from setuptools.command.build_py import build_py as build_py_orig

try:
    from Cython.Build import cythonize
except ImportError:
    cythonize = None

# 基础配置项
PACKAGE_NAME = "spam"
extensions = [
    Extension(f'{PACKAGE_NAME}.*', [f'{PACKAGE_NAME}/**/*.py'],
              extra_compile_args=["-O3", "-Wall"]),
]
cython_excludes = [f'{PACKAGE_NAME}/**/__init__.py']

# 检测是否需要执行Cython编译
need_cythonize = False
if "bdist_wheel" in sys.argv:
    need_cythonize = True
if "install" in sys.argv and "--cythonize" in sys.argv:
    need_cythonize = True
    # 移除自定义参数,避免setuptools识别为无效参数
    sys.argv.remove("--cythonize")

# Cython编译场景下的源码过滤逻辑
def not_cythonized(tup):
    (package, module, filepath) = tup
    return any(
        fnmatch.fnmatchcase(filepath, pat=pattern) for pattern in cython_excludes
    ) or not any(
        fnmatch.fnmatchcase(filepath, pat=pattern)
        for ext in extensions
        for pattern in ext.sources
    )

class build_py_cythonized(build_py_orig):
    def find_modules(self):
        modules = super().find_modules()
        return list(filter(not_cythonized, modules))

    def find_package_modules(self, package, package_dir):
        modules = super().find_package_modules(package, package_dir)
        return list(filter(not_cythonized, modules))

# 动态配置setup参数
ext_modules = []
cmdclass = {}
if need_cythonize and cythonize is not None:
    ext_modules = cythonize(
        extensions,
        exclude=cython_excludes,
        compiler_directives={
            "language_level": 3,
            "always_allow_keywords": True,
        },
        build_dir="build",
    )
    cmdclass["build_py"] = build_py_cythonized

setup(
    name=PACKAGE_NAME,
    packages=find_packages(),
    ext_modules=ext_modules,
    cmdclass=cmdclass,
    include_package_data=True,
    install_requires=[
        # 填写你的依赖项
    ]
)

问题对应说明

  1. ext_modules处理逻辑:
    • 无需重写install和develop类,通过提前解析sys.argv即可准确判断用户执行的命令类型
    • 不需要Cython编译的场景ext_modules直接设为空列表,是setuptools支持的合法配置;cythonize()仅在需要编译的场景才会被执行,不会提前求值
  2. 普通安装场景egg包包含.so的问题:
    • 普通安装场景不会触发cythonize()执行,不会生成任何.so文件,自然不会被打包到egg包中
    • 若需要完全避免egg构建,可在pip安装时添加参数pip install . --wheel,强制走wheel构建流程
  3. 源码过滤问题:
    • Cython编译场景下会自动注册自定义的build_py_cythonized类,自动过滤掉所有非__init__.py的源码模块,仅保留必要的入口文件

内容的提问来源于stack exchange,提问作者TheDiveO

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.20 18:06:03