【问题标题】:How to distribute a module with both a pure Python and Cython version如何使用纯 Python 和 Cython 版本分发模块
【发布时间】:2016-08-31 03:54:18
【问题描述】:

我有一个纯 Python 模块,我想使用 Cython 重写一些子模块。然后我想将新的 Cython 子模块添加到原始 Python 模块中,并使其仅作为选项可用,这意味着对模块进行 cython 化不是强制性的(在这种情况下,应该使用“旧”纯 Python 模块)。

这是一个例子:

my_module
    - __init__.py
    - a.py
    - b.py
    - setup.py

其中a.py 包含import b

我想在 Cython 中写 b.py。想法是添加一个包含.pyx 文件的文件夹,例如:

my_module
    - __init_.py
    - a.py
    - b.py
    - setup.py
    cython
        -b.pyx

setup.py 将包含编译b.pyx 和安装模块的方向。但是,我希望如果有人运行 python setup.py install 则安装纯 Python 代码,而如果添加选项则编译并安装 Cython 代码。

知道怎么做吗?

另外,应该如何修改文件a.py 以导入正确的模块?

【问题讨论】:

    标签: python module cython


    【解决方案1】:

    我不确定你的setup.py 要求(我不知道你为什么需要那个),但至于运行时导入问题,我写了一个装饰器来做到这一点:

    from __future__ import print_function
    from importlib import import_module
    from functools import wraps
    import inspect
    import sys
    
    MAKE_NOISE = False
    
    def external(f):
        """ Decorator that looks for an external version of
            the decorated function -- if one is found and
            imported, it replaces the decorated function
            in-place (and thus transparently, to would-be
            users of the code). """
        f.__external__ = 0 # Mark func as non-native
    
        function_name = hasattr(f, 'func_name') and f.func_name or f.__name__
        module_name = inspect.getmodule(f).__name__
    
        # Always return the straight decoratee func,
        # whenever something goes awry. 
        if not function_name or not module_name:
            MAKE_NOISE and print("Bad function or module name (respectively, %s and %s)" % (
                function_name, module_name), file=sys.stderr)
            return f
    
        # This function is `pylire.process.external()`.
        # It is used to decorate functions in `pylire.process.*`,
        # each of which possibly has a native (Cython) accelerated
        # version waiting to be imported in `pylire.process.ext.*`
        # … for example: if in `pylire/process/my_module.py` you did this:
        # 
        #   @external
        #   def my_function(*args, **kwargs):
        #       """ The slow, pure-Python implementation """
        #       pass
        # 
        # … and you had a Cython version of `my_function()` set up
        # in `pylire/process/ext/my_module.pyx` – you would get the fast
        # function version, automatically at runtime, without changing code.
        #
        # TL,DR: you'll want to change the `pylire.process.ext` string (below)
        # to match whatever your packages' structure looks like.
        module_file_name = module_name.split('.')[-1]
        module_name = "pylire.process.ext.%s" % module_file_name
    
        # Import the 'ext' version of process
        try:
            module = import_module(module_name)
        except ImportError:
            MAKE_NOISE and print("Error importing module (%s)" % (
                module_name,), file=sys.stderr)
            return f
        MAKE_NOISE and print("Using ext module: %s" % (
            module_name,), file=sys.stderr)
    
        # Get the external function with a name that
        # matches that of the decoratee.
        try:
            ext_function = getattr(module, function_name)
        except AttributeError:
            # no matching function in the ext module
            MAKE_NOISE and print("Ext function not found with name (%s)" % (
                function_name,), file=sys.stderr)
            return f
        except TypeError:
            # function_name was probably shit
            MAKE_NOISE and print("Bad name given for ext_function lookup (%s)" % (
                function_name,), file=sys.stderr)
            return f
    
        # Try to set telltale/convenience attributes
        # on the new external function -- this doesn't
        # always work, for more heavily encythoned
        # and cdef'd function examples.
        try:
            setattr(ext_function, '__external__', 1)
            setattr(ext_function, 'orig', f)
        except AttributeError:
            MAKE_NOISE and print("Bailing, failed setting ext_function attributes (%s)" % (
                function_name,), file=sys.stderr)
            return ext_function
        return wraps(f)(ext_function)
    

    ... 这让您可以将函数装饰为 @external - 并且它们会在运行时自动替换为您提供的 Cython 优化版本。

    如果您想将此想法扩展到替换整个 Cythonized 类,则可以直接在元类的 __new__ 方法中使用相同的逻辑(例如优化模块中的机会主义查找和替换)。

    【讨论】:

    • 我接受了你的回答,因为它看起来不错! :-) 虽然我还没有机会尝试。我使用了一个更简单但在概念上有些相似的解决方案。你可以在我的回答中找到。
    • 很高兴你喜欢它,谢谢! – 我刚刚对其进行了编辑,为 Python 2.x 用户添加了 from __future__ 行。
    【解决方案2】:

    我的解决方案是这样设置模块:

    my_module
        - __init_.py
        - a.py
        - b.py
        - setup.py
        cython_my_module
            - __init_.py
            - b.pyx
    

    setup.py 将包含与此类似的内容:

    from distutils.core import setup
    from Cython.Build import cythonize
    
    import numpy
    
    setup(
        name='My_module',
        ext_modules=cythonize(["cython_my_module/b.pyx",]),
        include_dirs=[numpy.get_include()],
    ) 
    

    并且文件a.py 将在标题中包含以下行:

    try:
        import cython_my_module.b
    except ImportError:
        import b
    

    它的工作方式非常简单:如果你什么都不做(即如果你不编译 cython 文件)然后模块 a.py 导入模块 b.py;但是,如果您运行python setup.py build_ext --inplace,那么编译后的cython文件将出现在cython_my_module中,并且下次运行a.py时它会自动导入cython模块b.pyx(实际上它会导入编译后的库b.so) .

    到目前为止,它似乎工作并且几乎不需要任何努力。希望对您有所帮助。

    fish2000 解决方案似乎更通用,但我还没有尝试过。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2014-07-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-12-30
      • 2012-06-16
      相关资源
      最近更新 更多