Làm cách nào để cấu trúc một gói Python có chứa mã Cython


122

Tôi muốn tạo một gói Python chứa một số mã Cython . Tôi đã có mã Cython hoạt động tốt. Tuy nhiên, bây giờ tôi muốn biết cách tốt nhất để đóng gói nó.

Đối với hầu hết những người chỉ muốn cài đặt gói, tôi muốn bao gồm .ctệp mà Cython tạo và sắp xếp setup.pybiên dịch tệp đó để tạo mô-đun. Sau đó, người dùng không cần cài đặt Cython để cài đặt gói.

Nhưng đối với những người có thể muốn sửa đổi gói, tôi cũng muốn cung cấp các .pyxtệp Cython và bằng cách nào đó cũng cho phép setup.pyxây dựng chúng bằng Cython (vì vậy những người dùng đó sẽ cần cài đặt Cython).

Tôi nên cấu trúc các tệp trong gói như thế nào để đáp ứng cho cả hai trường hợp này?

Các tài liệu Cython đưa ra một hướng dẫn nhỏ . Nhưng nó không nói cách tạo một đĩa đơn setup.pyxử lý cả trường hợp có / không có Cython.


1
Tôi thấy câu hỏi đang nhận được nhiều phiếu bầu hơn bất kỳ câu trả lời nào. Tôi tò mò muốn biết tại sao mọi người có thể thấy câu trả lời không thỏa đáng.
Craig McQueen

4
Tôi đã tìm thấy phần này của tài liệu , phần này đưa ra câu trả lời chính xác.
Will

Câu trả lời:


72

Tôi đã tự mình thực hiện việc này ngay bây giờ, trong một gói Python simplerandom( BitBucket repo - EDIT: now github ) (Tôi không mong đợi đây là một gói phổ biến, nhưng đó là một cơ hội tốt để học Cython).

Phương pháp này dựa trên thực tế là việc xây dựng .pyxtệp với Cython.Distutils.build_ext(ít nhất là với Cython phiên bản 0.14) dường như luôn tạo .ctệp trong cùng thư mục với .pyxtệp nguồn .

Đây là một phiên bản rút gọn setup.pymà tôi hy vọng sẽ hiển thị những điều cần thiết:

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,
    ...
)

Tôi cũng đã chỉnh sửa MANIFEST.inđể đảm bảo điều đó mycythonmodule.cđược bao gồm trong bản phân phối nguồn (bản phân phối nguồn được tạo bằng python setup.py sdist):

...
recursive-include cython *
...

Tôi không cam kết mycythonmodule.ckiểm soát phiên bản 'thân cây' (hoặc 'mặc định' cho Mercurial). Khi tôi thực hiện một bản phát hành, tôi cần phải nhớ thực hiện một bước python setup.py build_extđầu tiên, để đảm bảo rằng bản phát hành đó mycythonmodule.ccó sẵn và cập nhật cho bản phân phối mã nguồn. Tôi cũng tạo một nhánh phát hành và cam kết tệp C vào nhánh. Bằng cách đó, tôi có một bản ghi lịch sử của tệp C đã được phân phối cùng với bản phát hành đó.


Cảm ơn, đây chính xác là những gì tôi cần cho một dự án Pyrex mà tôi đang mở! MANIFEST.in đã làm tôi vấp ngã trong một giây, nhưng tôi chỉ cần một dòng đó. Tôi đang bao gồm tệp C trong kiểm soát nguồn ngoài sự quan tâm, nhưng tôi thấy quan điểm của bạn rằng nó không cần thiết.
chmullig

Tôi đã chỉnh sửa câu trả lời của mình để giải thích cách tệp C không nằm trong đường trục / mặc định, nhưng được thêm vào một nhánh phát hành.
Craig McQueen

1
@CraigMcQueen cảm ơn vì câu trả lời tuyệt vời, nó đã giúp tôi rất nhiều! Tuy nhiên, tôi đang tự hỏi, liệu có hành vi mong muốn sử dụng Cython khi có sẵn không? Đối với tôi, có vẻ như sẽ tốt hơn theo mặc định là sử dụng các tệp c được tạo trước, trừ khi người dùng muốn sử dụng Cython một cách rõ ràng, trong trường hợp đó anh ta có thể đặt biến môi trường hoặc một cái gì đó. Điều đó sẽ làm cho việc cài đặt ổn định / mạnh mẽ hơn, bởi vì người dùng có thể nhận được các kết quả khác nhau dựa trên phiên bản Cython mà anh ta đã cài đặt - thậm chí anh ta có thể không biết rằng mình đã cài đặt nó và nó đang ảnh hưởng đến việc xây dựng gói.
Martinsos

20

Thêm vào câu trả lời của Craig McQueen: xem bên dưới để biết cách ghi đè sdistlệnh để Cython tự động biên dịch các tệp nguồn của bạn trước khi tạo bản phân phối nguồn.

Bằng cách đó, bạn không có nguy cơ vô tình phân phối Ccác nguồn lỗi thời . Nó cũng hữu ích trong trường hợp bạn có quyền kiểm soát hạn chế đối với quá trình phân phối, ví dụ khi tự động tạo các bản phân phối từ tích hợp liên tục, v.v.

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

Chúng tôi thực sự khuyên bạn nên phân phối các tệp .c đã tạo cũng như các nguồn Cython của bạn để người dùng có thể cài đặt mô-đun của bạn mà không cần có sẵn Cython.

Chúng tôi cũng khuyến nghị rằng biên dịch Cython không được bật theo mặc định trong phiên bản bạn phân phối. Ngay cả khi người dùng đã cài đặt Cython, họ có thể không muốn sử dụng nó chỉ để cài đặt mô-đun của bạn. Ngoài ra, phiên bản mà anh ấy có có thể không giống với phiên bản bạn đã sử dụng và có thể không biên dịch các nguồn của bạn một cách chính xác.

Điều này đơn giản có nghĩa là tệp setup.py mà bạn gửi cùng sẽ chỉ là một tệp distutils bình thường trên các tệp .c đã tạo, đối với ví dụ cơ bản mà chúng tôi sẽ có:

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

7

Đơn giản nhất là bao gồm cả hai nhưng chỉ sử dụng c-file? Bao gồm tệp .pyx là tốt, nhưng nó không cần thiết khi bạn đã có tệp .c. Những người muốn biên dịch lại .pyx có thể cài đặt Pyrex và thực hiện theo cách thủ công.

Nếu không, trước tiên bạn cần có lệnh build_ext tùy chỉnh cho các bản phân phối tạo tệp C. Cython đã bao gồm một. http://docs.cython.org/src/userguide/source_files_and_compilation.html

Điều mà tài liệu đó không làm là nói cách tạo điều kiện này, nhưng

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

Nên xử lý nó.


1
Cảm ơn câu trả lời của bạn. Điều đó hợp lý, mặc dù tôi thích nếu setup.pycó thể xây dựng trực tiếp từ .pyxtệp khi Cython được cài đặt. Câu trả lời của tôi cũng đã thực hiện điều đó.
Craig McQueen

Đó là toàn bộ câu trả lời của tôi. Nó chỉ không phải là một setup.py hoàn chỉnh.
Lennart Regebro

4

Bao gồm các tệp .c được tạo (Cython) khá kỳ lạ. Đặc biệt là khi chúng tôi đưa nó vào git. Tôi muốn sử dụng setuptools_cython . Khi không có Cython, nó sẽ tạo một quả trứng có môi trường Cython tích hợp sẵn, sau đó xây dựng mã của bạn bằng quả trứng.

Ví dụ có thể có: https://github.com/douban/greenify/blob/master/setup.py


Cập nhật (2017-01-05):

setuptools 18.0, không cần phải sử dụng setuptools_cython. Đây là một ví dụ để xây dựng dự án Cython từ đầu mà không cần setuptools_cython.


điều này có khắc phục được sự cố Cython không được cài đặt mặc dù bạn chỉ định nó trong setup_requires không?
Kamil Sindi

cũng không thể đưa 'setuptools>=18.0'vào setup_requires thay vì tạo phương thức is_installed?
Kamil Sindi

1
@capitalistpug Trước tiên, bạn cần đảm bảo setuptools>=18.0đã được cài đặt, sau đó bạn chỉ cần đưa 'Cython >= 0.18'vào setup_requiresvà Cython sẽ được cài đặt trong quá trình cài đặt. Nhưng nếu bạn đang sử dụng setuptools <18.0, ngay cả khi bạn sử dụng cython cụ thể trong setup_requires, nó sẽ không được cài đặt, trong trường hợp này, bạn nên cân nhắc sử dụng setuptools_cython.
McKelvin

Cảm ơn @McKelvin, đây có vẻ là một giải pháp tuyệt vời! Có lý do nào tại sao chúng ta nên sử dụng cách tiếp cận khác, với việc dữ liệu mạng các tệp nguồn trước, bên cạnh cách này? Tôi đã thử cách tiếp cận của bạn và nó có vẻ hơi chậm khi cài đặt (mất một phút để cài đặt nhưng xây dựng trong một giây).
Martinsos

1
@Martinsos pip install wheel. Sau đó, nó phải là lý do 1. Vui lòng cài đặt bánh xe trước và thử lại.
McKelvin

2

Đây là một tập lệnh thiết lập mà tôi đã viết giúp dễ dàng bao gồm các thư mục lồng nhau bên trong bản dựng. Người ta cần chạy nó từ thư mục trong một gói.

Cấu trúc givig như thế này:

__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

Biên dịch vui vẻ;)


2

Cách hack đơn giản mà tôi nghĩ ra:

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(…)

Chỉ cần cài đặt Cython nếu nó không thể được nhập. Người ta có thể không nên chia sẻ mã này, nhưng đối với các phụ thuộc của riêng tôi, nó đủ tốt.


2

Tất cả các câu trả lời khác đều dựa vào

  • distutils
  • nhập từ Cython.Build, điều này tạo ra một vấn đề giữa gà và trứng giữa việc yêu cầu mạng qua mạng setup_requiresvà nhập nó.

Một giải pháp hiện đại là sử dụng setuptools thay thế, hãy xem câu trả lời này (việc xử lý tự động các phần mở rộng Cython yêu cầu setuptools 18.0, tức là nó đã có sẵn trong nhiều năm rồi). Một tiêu chuẩn hiện đại setup.pyvới xử lý yêu cầu, điểm đầu vào và mô-đun cython có thể trông như sau:

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'],
        ),
    ],
)

Việc nhập từ Cython.Buildlúc thiết lập gây ra lỗi ImportError cho tôi. Có setuptools để biên dịch pyx là cách tốt nhất để làm điều đó.
Carson Ip

1

Cách dễ nhất mà tôi thấy chỉ sử dụng các công cụ cài đặt thay vì các bản phân phối giới hạn tính năng là

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)

Trên thực tế, với các công cụ thiết lập, không cần nhập thử / bắt rõ ràng Cython.Build, hãy xem câu trả lời của tôi.
bluenote 10

0

Tôi nghĩ rằng tôi đã tìm thấy một cách khá tốt để thực hiện việc này bằng cách cung cấp một build_extlệnh tùy chỉnh . Ý tưởng như sau:

  1. Tôi thêm các tiêu đề numpy bằng cách ghi đè finalize_options()và thực hiện import numpytrong nội dung của hàm, điều này giúp tránh được vấn đề không có sẵn numpy trước khi setup()cài đặt nó.

  2. Nếu cython có sẵn trên hệ thống, nó sẽ kết nối với check_extensions_list()phương thức của lệnh và bằng cách mạng hóa tất cả các mô-đun cython đã lỗi thời, thay thế chúng bằng các phần mở rộng C mà build_extension() phương pháp này sau này có thể xử lý . Chúng tôi cũng chỉ cung cấp phần sau của chức năng trong mô-đun của chúng tôi: điều này có nghĩa là nếu cython không khả dụng nhưng chúng tôi có phần mở rộng C, nó vẫn hoạt động, cho phép bạn thực hiện phân phối nguồn.

Đây là mã:

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())

Điều này cho phép một người chỉ cần viết các setup()đối số mà không cần lo lắng về việc nhập và liệu một người có sẵn cython hay không:

setup(
    # ...
    ext_modules=[Extension("_my_fast_thing", ["src/_my_fast_thing.pyx"])],
    setup_requires=['numpy'],
    cmdclass={'build_ext': BuildExtWithNumpy}
    )
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.