如何使用纯Python和Cython版本分发模块

时间:2016-08-31 03:54:19

标签: python module cython

我有一个纯Python模块,我想用Cython重写一些子模块。然后我想将新的Cython子模块添加到原始的Python模块中,并使它们仅作为一个选项使用,这意味着cythoning模块不是强制性的(在这种情况下,旧的纯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以导入正确的模块?

2 个答案:

答案 0 :(得分:2)

我不确定你的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__方法中使用相同的逻辑会很简单(例如,优化模块中的机会主义查找和替换)

答案 1 :(得分:1)

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

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解决方案似乎更通用,但我还没有尝试过。