ฉันจะจัดโครงสร้างแพ็คเกจ Python ที่มีรหัส Cython ได้อย่างไร


122

ฉันต้องการสร้างแพ็คเกจ Python ที่มีรหัสCython ฉันมีรหัส Cython ที่ใช้งานได้ดี อย่างไรก็ตามตอนนี้ฉันต้องการทราบวิธีที่ดีที่สุดในการจัดแพคเกจ

สำหรับคนส่วนใหญ่ที่ต้องการติดตั้งแพ็กเกจฉันต้องการรวม.cไฟล์ที่ Cython สร้างและจัดเตรียมsetup.pyเพื่อรวบรวมเพื่อสร้างโมดูล จากนั้นผู้ใช้ไม่จำเป็นต้องติดตั้ง Cython เพื่อติดตั้งแพ็คเกจ

แต่สำหรับผู้ที่อาจต้องการแก้ไขแพ็คเกจฉันก็ต้องการที่จะให้.pyxไฟล์Cython และยังอนุญาตให้setup.pyสร้างโดยใช้ Cython (ดังนั้นผู้ใช้เหล่านั้นจะต้องติดตั้ง Cython)

ฉันจะจัดโครงสร้างไฟล์ในแพ็กเกจเพื่อรองรับทั้งสองสถานการณ์นี้ได้อย่างไร

เอกสาร Cython ให้คำแนะนำเล็ก แต่ไม่ได้บอกว่าจะสร้างซิงเกิ้ลsetup.pyที่จัดการทั้งเคสที่มี / ไม่มี Cython ได้อย่างไร


1
ฉันเห็นว่าคำถามได้รับคะแนนโหวตมากกว่าคำตอบใด ๆ ฉันอยากรู้ว่าทำไมผู้คนถึงพบคำตอบที่ไม่น่าพอใจ
Craig McQueen

4
ฉันพบส่วนนี้ของเอกสารซึ่งให้คำตอบอย่างตรงประเด็น
จะ

คำตอบ:


72

ฉันทำสิ่งนี้ด้วยตัวเองแล้วในแพ็คเกจ Python simplerandom( BitBucket repo - แก้ไข: ตอนนี้github ) (ฉันไม่คาดหวังว่านี่จะเป็นแพ็คเกจยอดนิยม แต่เป็นโอกาสดีที่จะได้เรียนรู้ Cython)

วิธีนี้อาศัยข้อเท็จจริงที่ว่าการสร้าง.pyxไฟล์ด้วยCython.Distutils.build_ext(อย่างน้อยที่สุดด้วย Cython เวอร์ชัน 0.14) ดูเหมือนว่าจะสร้าง.cไฟล์ในไดเร็กทอรีเดียวกับซอร์ส.pyxไฟล์เสมอ

นี่คือเวอร์ชันย่อsetup.pyที่ฉันหวังว่าจะแสดงข้อมูลสำคัญ:

from distutils.core import setup
from distutils.extension import Extension

try:
    from Cython.Distutils import build_ext
except ImportError:
    use_cython = False
else:
    use_cython = True

cmdclass = {}
ext_modules = []

if use_cython:
    ext_modules += [
        Extension("mypackage.mycythonmodule", ["cython/mycythonmodule.pyx"]),
    ]
    cmdclass.update({'build_ext': build_ext})
else:
    ext_modules += [
        Extension("mypackage.mycythonmodule", ["cython/mycythonmodule.c"]),
    ]

setup(
    name='mypackage',
    ...
    cmdclass=cmdclass,
    ext_modules=ext_modules,
    ...
)

ฉันยังแก้ไขMANIFEST.inเพื่อให้แน่ใจว่าmycythonmodule.cรวมอยู่ในการแจกจ่ายซอร์ส (การแจกจ่ายซอร์สที่สร้างขึ้นด้วยpython setup.py sdist):

...
recursive-include cython *
...

ฉันไม่ผูกมัดmycythonmodule.cกับการควบคุมเวอร์ชัน 'trunk' (หรือ 'default' สำหรับ Mercurial) เมื่อฉันสร้างรุ่นฉันต้องจำไว้ว่าต้องทำpython setup.py build_extก่อนเพื่อให้แน่ใจว่าmycythonmodule.cมีอยู่และเป็นปัจจุบันสำหรับการแจกจ่ายซอร์สโค้ด ฉันยังสร้างสาขารีลีสและส่งไฟล์ C ไปยังสาขา ด้วยวิธีนี้ฉันมีบันทึกประวัติของไฟล์ C ที่แจกจ่ายพร้อมกับรุ่นนั้น


ขอบคุณนี่คือสิ่งที่ฉันต้องการสำหรับโครงการ Pyrex ที่ฉันกำลังเปิด! MANIFEST.in ทำให้ฉันสะดุดเป็นวินาที แต่ฉันต้องการแค่บรรทัดเดียว ฉันรวมไฟล์ C ไว้ในการควบคุมแหล่งที่มาโดยไม่สนใจ แต่ฉันเห็นประเด็นของคุณว่ามันไม่จำเป็น
chmullig

ฉันได้แก้ไขคำตอบของฉันเพื่ออธิบายว่าไฟล์ C ไม่อยู่ใน trunk / default แต่ถูกเพิ่มไปยัง release branch
Craig McQueen

1
@CraigMcQueen ขอบคุณสำหรับคำตอบที่ดีมันช่วยฉันได้มาก! อย่างไรก็ตามฉันสงสัยว่าพฤติกรรมที่ต้องการใช้ Cython เมื่อพร้อมใช้งานหรือไม่? สำหรับฉันแล้วดูเหมือนว่าจะเป็นการดีกว่าที่จะใช้ไฟล์ c ที่สร้างไว้ล่วงหน้าโดยค่าเริ่มต้นเว้นแต่ผู้ใช้ต้องการใช้ Cython อย่างชัดเจนซึ่งในกรณีนี้เขาสามารถตั้งค่าตัวแปรสภาพแวดล้อมหรือบางอย่างได้ นั่นจะทำให้การติดตั้งมีเสถียรภาพ / มีประสิทธิภาพมากขึ้นเนื่องจากผู้ใช้อาจได้รับผลลัพธ์ที่แตกต่างกันขึ้นอยู่กับเวอร์ชันของ Cython ที่เขาติดตั้ง - เขาอาจไม่รู้ด้วยซ้ำว่าได้ติดตั้งและส่งผลกระทบต่อการสร้างแพ็คเกจ
Martinsos

20

การเพิ่มคำตอบของ Craig McQueen: ดูด้านล่างสำหรับวิธีการแทนที่sdistคำสั่งเพื่อให้ Cython รวบรวมไฟล์ต้นฉบับของคุณโดยอัตโนมัติก่อนสร้างการแจกจ่ายซอร์ส

ด้วยวิธีนี้คุณจะไม่เสี่ยงต่อการแจกจ่ายCแหล่งข้อมูลที่ล้าสมัยโดยไม่ได้ตั้งใจ นอกจากนี้ยังช่วยในกรณีที่คุณควบคุมกระบวนการแจกจ่ายได้อย่าง จำกัด เช่นเมื่อสร้างการแจกแจงโดยอัตโนมัติจากการรวมแบบต่อเนื่องเป็นต้น

from distutils.command.sdist import sdist as _sdist

...

class sdist(_sdist):
    def run(self):
        # Make sure the compiled Cython files in the distribution are up-to-date
        from Cython.Build import cythonize
        cythonize(['cython/mycythonmodule.pyx'])
        _sdist.run(self)
cmdclass['sdist'] = sdist

19

http://docs.cython.org/en/latest/src/userguide/source_files_and_compilation.html#distributing-cython-modules

ขอแนะนำอย่างยิ่งให้คุณแจกจ่ายไฟล์. c ที่สร้างขึ้นรวมทั้งซอร์ส Cython ของคุณเพื่อให้ผู้ใช้สามารถติดตั้งโมดูลของคุณได้โดยไม่จำเป็นต้องมี Cython

ขอแนะนำว่าอย่าเปิดใช้งานการคอมไพล์ Cython โดยค่าเริ่มต้นในเวอร์ชันที่คุณแจกจ่าย แม้ว่าผู้ใช้จะติดตั้ง Cython แล้ว แต่เขาอาจไม่ต้องการใช้เพียงเพื่อติดตั้งโมดูลของคุณ นอกจากนี้เวอร์ชันที่เขามีอาจไม่ใช่เวอร์ชันเดียวกับที่คุณใช้และอาจรวบรวมแหล่งข้อมูลของคุณไม่ถูกต้อง

นั่นหมายความว่าไฟล์ setup.py ที่คุณส่งมาจะเป็นเพียงไฟล์ distutils ปกติในไฟล์. c ที่สร้างขึ้นสำหรับตัวอย่างพื้นฐานที่เราจะมีแทน:

from distutils.core import setup
from distutils.extension import Extension
 
setup(
    ext_modules = [Extension("example", ["example.c"])]
)

7

ง่ายที่สุดคือรวมทั้งสองอย่าง แต่ใช้ c-file? การรวมไฟล์. pyx เป็นสิ่งที่ดี แต่ก็ไม่จำเป็นเมื่อคุณมีไฟล์. c แล้ว ผู้ที่ต้องการคอมไพล์. pyx ใหม่สามารถติดตั้ง Pyrex และดำเนินการด้วยตนเอง

มิฉะนั้นคุณต้องมีคำสั่ง build_ext แบบกำหนดเองสำหรับ distutils ที่สร้างไฟล์ C ก่อน Cython มีอยู่แล้วhttp://docs.cython.org/src/userguide/source_files_and_compilation.html

สิ่งที่เอกสารนั้นไม่ได้ทำคือบอกว่าจะทำให้เงื่อนไขนี้เป็นอย่างไร แต่

try:
     from Cython.distutils import build_ext
except ImportError:
     from distutils.command import build_ext

ควรจัดการกับมัน


1
ขอบคุณสำหรับคำตอบ. มันสมเหตุสมผลแม้ว่าฉันจะชอบถ้าsetup.pyสามารถสร้างได้โดยตรงจาก.pyxไฟล์เมื่อติดตั้ง Cython คำตอบของฉันได้นำไปใช้เช่นกัน
Craig McQueen

นั่นคือจุดรวมของคำตอบของฉัน มันเป็นเพียงการ setup.py ที่ไม่สมบูรณ์
Lennart Regebro

4

การรวม (Cython) ไฟล์. c ที่สร้างขึ้นนั้นค่อนข้างแปลก โดยเฉพาะอย่างยิ่งเมื่อเรารวมสิ่งนั้นไว้ในคอมไพล์ ฉันต้องการใช้setuptools_cython setuptools_cythonเมื่อ Cython ไม่พร้อมใช้งานมันจะสร้างไข่ที่มีสภาพแวดล้อม Cython ในตัวจากนั้นสร้างรหัสของคุณโดยใช้ไข่

ตัวอย่างที่เป็นไปได้: https://github.com/douban/greenify/blob/master/setup.py


Update (2017/01/05):

เนื่องจากมีความจำเป็นที่จะใช้ไม่ได้setuptools 18.0 นี่เป็นตัวอย่างในการสร้างโครงการ Cython จากรอยขีดข่วนได้โดยไม่ต้องsetuptools_cythonsetuptools_cython


วิธีนี้แก้ไขปัญหา Cython ไม่ได้รับการติดตั้งแม้ว่าคุณจะระบุไว้ใน setup_requires หรือไม่
Kamil Sindi

ยังเป็นไปไม่ได้ที่จะใส่'setuptools>=18.0'setup_requires แทนการสร้างเมธอดis_installed?
Kamil Sindi

1
@capitalistpug แรกที่คุณต้องให้แน่ใจว่าsetuptools>=18.0มีการติดตั้งแล้วคุณจะต้องใส่'Cython >= 0.18'ในsetup_requiresและ Cython จะถูกติดตั้งในระหว่างการติดตั้งความคืบหน้า แต่ถ้าคุณกำลังใช้ setuptools <18.0 แม้คุณ Cython เฉพาะใน setup_requires setuptools_cythonก็จะไม่ได้รับการติดตั้งในกรณีนี้คุณควรพิจารณาการใช้
McKelvin

ขอบคุณ @McKelvin นี่ดูเหมือนจะเป็นทางออกที่ดี! มีเหตุผลใดบ้างที่เราควรใช้วิธีการอื่นโดยทำการ cythonizing ไฟล์ซอร์สล่วงหน้าถัดจากนี้? ฉันลองใช้แนวทางของคุณแล้วและดูเหมือนว่าจะค่อนข้างช้าเมื่อติดตั้ง (ใช้เวลาในการติดตั้งหนึ่งนาที แต่สร้างในหนึ่งวินาที)
Martinsos

1
@ มาร์ตินโซpip install wheel. ดังนั้นจึงต้องมีเหตุผล 1. โปรดติดตั้งล้อก่อนแล้วลองอีกครั้ง
McKelvin

2

นี่คือสคริปต์การตั้งค่าที่ฉันเขียนขึ้นซึ่งทำให้ง่ายต่อการรวมไดเร็กทอรีที่ซ้อนกันภายในบิลด์ จำเป็นต้องเรียกใช้จากโฟลเดอร์ภายในแพ็คเกจ

โครงสร้าง Givig ดังนี้:

__init__.py
setup.py
test.py
subdir/
      __init__.py
      anothertest.py

setup.py

from setuptools import setup, Extension
from Cython.Distutils import build_ext
# from os import path
ext_names = (
    'test',
    'subdir.anothertest',       
) 

cmdclass = {'build_ext': build_ext}
# for modules in main dir      
ext_modules = [
    Extension(
        ext,
        [ext + ".py"],            
    ) 
    for ext in ext_names if ext.find('.') < 0] 
# for modules in subdir ONLY ONE LEVEL DOWN!! 
# modify it if you need more !!!
ext_modules += [
    Extension(
        ext,
        ["/".join(ext.split('.')) + ".py"],     
    )
    for ext in ext_names if ext.find('.') > 0]

setup(
    name='name',
    ext_modules=ext_modules,
    cmdclass=cmdclass,
    packages=["base", "base.subdir"],
)
#  Build --------------------------
#  python setup.py build_ext --inplace

มีความสุขในการรวบรวม;)


2

แฮ็คง่ายๆที่ฉันคิดขึ้น:

from distutils.core import setup

try:
    from Cython.Build import cythonize
except ImportError:
    from pip import pip

    pip.main(['install', 'cython'])

    from Cython.Build import cythonize


setup(…)

เพียงติดตั้ง Cython หากไม่สามารถนำเข้าได้ ไม่ควรแชร์รหัสนี้ แต่สำหรับการอ้างอิงของฉันเองก็ดีพอแล้ว


2

คำตอบอื่น ๆ ทั้งหมดขึ้นอยู่กับ

  • distutils
  • การนำเข้าจากCython.Buildซึ่งทำให้เกิดปัญหาไก่และไข่ระหว่างการต้องใช้ cython ผ่านsetup_requiresและการนำเข้า

วิธีแก้ปัญหาที่ทันสมัยคือการใช้ setuptools แทนดูคำตอบนี้ (การจัดการส่วนขยาย Cython โดยอัตโนมัติต้องใช้ setuptools 18.0 กล่าวคือมีให้ใช้งานมาหลายปีแล้ว) มาตรฐานที่ทันสมัยsetup.pyพร้อมการจัดการข้อกำหนดจุดเริ่มต้นและโมดูล cython อาจมีลักษณะดังนี้:

from setuptools import setup, Extension

with open('requirements.txt') as f:
    requirements = f.read().splitlines()

setup(
    name='MyPackage',
    install_requires=requirements,
    setup_requires=[
        'setuptools>=18.0',  # automatically handles Cython extensions
        'cython>=0.28.4',
    ],
    entry_points={
        'console_scripts': [
            'mymain = mypackage.main:main',
        ],
    },
    ext_modules=[
        Extension(
            'mypackage.my_cython_module',
            sources=['mypackage/my_cython_module.pyx'],
        ),
    ],
)

การนำเข้าจากCython.Buildเวลาตั้งค่าทำให้เกิด ImportError สำหรับฉัน การมี setuptools เพื่อรวบรวม pyx เป็นวิธีที่ดีที่สุด
Carson Ip

1

วิธีที่ง่ายที่สุดที่ฉันพบโดยใช้ setuptools เท่านั้นแทนที่จะใช้คุณสมบัติ จำกัด คือ

from setuptools import setup
from setuptools.extension import Extension
try:
    from Cython.Build import cythonize
except ImportError:
    use_cython = False
else:
    use_cython = True

ext_modules = []
if use_cython:
    ext_modules += cythonize('package/cython_module.pyx')
else:
    ext_modules += [Extension('package.cython_module',
                              ['package/cython_modules.c'])]

setup(name='package_name', ext_modules=ext_modules)

ในความเป็นจริงด้วย setuptools ไม่จำเป็นต้องมีการนำเข้าแบบ Explicit try / catched Cython.Buildดูคำตอบของฉัน
bluenote10

0

ฉันคิดว่าฉันพบวิธีที่ดีในการทำเช่นนี้โดยระบุbuild_extคำสั่งที่กำหนดเอง แนวคิดดังต่อไปนี้:

  1. ฉันเพิ่มส่วนหัวที่เป็นตัวเลขโดยการลบล้างfinalize_options()และดำเนินการimport numpyในเนื้อหาของฟังก์ชันซึ่งช่วยหลีกเลี่ยงปัญหาของการไม่สามารถใช้งานได้ก่อนที่จะsetup()ติดตั้ง

  2. หาก cython มีอยู่ในระบบระบบจะเชื่อมต่อกับcheck_extensions_list()เมธอดของคำสั่งและโดย cythonizes โมดูล cython ที่ล้าสมัยทั้งหมดแทนที่ด้วยส่วนขยาย C ที่สามารถจัดการได้ในภายหลังโดยbuild_extension() วิธีการ เราเพิ่งจัดเตรียมฟังก์ชันส่วนหลังในโมดูลของเราด้วยเช่นกันนั่นหมายความว่าถ้า cython ไม่พร้อมใช้งาน แต่เรามีส่วนขยาย C อยู่แสดงว่ามันยังใช้งานได้ซึ่งช่วยให้คุณทำการแจกแจงซอร์สได้

นี่คือรหัส:

import re, sys, os.path
from distutils import dep_util, log
from setuptools.command.build_ext import build_ext

try:
    import Cython.Build
    HAVE_CYTHON = True
except ImportError:
    HAVE_CYTHON = False

class BuildExtWithNumpy(build_ext):
    def check_cython(self, ext):
        c_sources = []
        for fname in ext.sources:
            cname, matches = re.subn(r"(?i)\.pyx$", ".c", fname, 1)
            c_sources.append(cname)
            if matches and dep_util.newer(fname, cname):
                if HAVE_CYTHON:
                    return ext
                raise RuntimeError("Cython and C module unavailable")
        ext.sources = c_sources
        return ext

    def check_extensions_list(self, extensions):
        extensions = [self.check_cython(ext) for ext in extensions]
        return build_ext.check_extensions_list(self, extensions)

    def finalize_options(self):
        import numpy as np
        build_ext.finalize_options(self)
        self.include_dirs.append(np.get_include())

สิ่งนี้ช่วยให้สามารถเขียนsetup()อาร์กิวเมนต์ได้โดยไม่ต้องกังวลเกี่ยวกับการนำเข้าและว่ามี cython หรือไม่:

setup(
    # ...
    ext_modules=[Extension("_my_fast_thing", ["src/_my_fast_thing.pyx"])],
    setup_requires=['numpy'],
    cmdclass={'build_ext': BuildExtWithNumpy}
    )
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.