【问题标题】:Pass c function as argument to python function in cython将c函数作为参数传递给cython中的python函数
【发布时间】:2019-08-13 12:34:16
【问题描述】:

我有以下有效的代码:

%%cython 

cdef int add(int  a, int b):
    return a+b

cdef int mult(int  a, int b):
    return a*b

ctypedef int (*function_type)(int  a, int b)

cdef int lambda_c(function_type func, int c, int d):
    return func(c,d)

print(lambda_c(add, 5, 6))
print(lambda_c(mult, 5, 6))

所以我有 lambda_c 函数,它将 c 函数作为参数,我无法更改它(因为它是另一个团队支持的 c 库的包装器)。

我想做的是写一个包装器:

cdef class PyClass:
    def py_wrap(func, e, f):
        return lambda_c(func, e, f)

print(PyClass().py_wrap(add, 5, 5))
print(PyClass().py_wrap(mult, 6, 6))

但这会引发错误:

无法将 Python 对象转换为“function_type”

我也尝试过强制转换 func(return lambda_c(<function_type>func, e, f)) 但出现错误:

Python 对象不能转换为原始类型的指针

其背后的想法如下:任何用户都可以在 cython 中编写自己的函数,对其进行编译,然后导入并将其函数传递给 PyClass().py_wrap 方法。

是否甚至可以导入纯 c 函数并将其作为参数传递给 cython?

我也看到了Pass cython functions via python interface,但与那里的解决方案不同,我无法更改lambda_c 函数并将其变成一个类。此外,lambda_c 只接受特定类型的函数(在我们的示例中为function_type

【问题讨论】:

  • 您不能将指针作为参数传递给 def 函数,只能传递给 cdef。
  • @ead 是否可以将指针传递给PyClass 类的cdef py_wrap(func, e, f) 方法?

标签: cython


【解决方案1】:

如果您想将任意可调用的 Python 作为 C 函数指针传递,则它不起作用 - 在标准 C 中无法执行此操作(因此 Cython 无法生成编码)。有一个涉及ctypes 的非常hacky 的解决方法(它可以生成运行时代码,如果需要我会找到链接。但是我真的不推荐它。

如果您对您的用户在 Cython 中编写 cdef 函数感到高兴(问题暗示您是这样),那么您可以在 my answer to your question yesterday 的基础上进行构建。

  1. 编写一个合适的包装类(您只需要更改函数指针类型) - 这会在您编写的 您的 .pxd 和 .pyx 文件之间拆分。

  2. 让您的用户 cimport 使用它,然后使用它将他们的 cdef 类公开给 Python:

    from your_module cimport FuncWrapper
    
    cdef int add_c_implementation(int  a, int b):
        return a+b
    
    # `add` accessible from Python
    add = WrapperFunc.make_from_ptr(add_c_implementation)
    
  3. PyClass 更改为以FuncWrapper 作为参数:

    # this is in your_module.pyx
    
    cdef class PyClass:
        def py_wrap(FuncWrapper func, e, f):
            return lambda_c(func.func, e, f)
    
  4. 然后您的用户可以使用他们从 Python 编译的函数:

    from your_module import PyClass
    from users_module import add
    
    PyClass().py_wrap(add,e,f)
    

实际上,所有这些都是使用一个小的 Python 包装器来允许您传递 Python 通常无法处理的类型。您在使用这些包装的函数指针可以做什么方面非常有限(例如,它们必须在 Cython 中设置),但它确实提供了选择和传递它们的句柄。

【讨论】:

  • 非常感谢!还有一个细节,据我了解,我可以编译主包,而不是从已安装的 FuncWrapper 中的 cimport 并构建一个新的。我试过这个,但是在尝试 cimport wrap 时得到'wrap.pxd' not found。我在 setuptools 中添加了 packages=['wrap'] 到 setup 函数,但它没有帮助
  • 我认为主要是wrap.pxd 在 Python 路径上。作为概念证明,尝试在与wrap 相同的目录中构建您的其他包,它应该可以工作。如果您使用的是不同的目录,那么 cimport 需要反映这一点。不幸的是,这总是有点难以解决,我不太了解最佳实践
【解决方案2】:

我不确定你是否可以更改函数指针类型

ctypedef int (*function_type)(int  a, int b)

ctypedef int (*function_type)(int a, int b, void *func_d)

但这通常是回调函数在C中实现的方式。void *参数func_d函数包含用户提供的任何形式的数据。如果答案是肯定的,那么您可以有以下解决方案。

首先,在 Cython 中创建以下定义文件,以向其他 Cython 用户显示您的 C API:

# binary_op.pxd
ctypedef int (*func_t)(int a, int b, void *func_d) except? -1

cdef int func(int a, int b, void *func_d) except? -1

cdef class BinaryOp:
  cpdef int eval(self, int a, int b) except? -1

cdef class Add(BinaryOp):
  cpdef int eval(self, int a, int b) except? -1

cdef class Multiply(BinaryOp):
  cpdef int eval(self, int a, int b) except? -1

这基本上允许任何 Cython 用户 cimport 这些定义直接进入他们的 Cython 代码并绕过任何与 Python 相关的函数调用。然后,您在以下pyx 文件中实现该模块:

# binary_op.pyx
cdef int func(int a, int b, void *func_d) except? -1:
  return (<BinaryOp>func_d).eval(a, b)

cdef class BinaryOp:
  cpdef int eval(self, int a, int b) except? -1:
    raise NotImplementedError()

cdef class Add(BinaryOp):
  cpdef int eval(self, int a, int b) except? -1:
    return a + b

cdef class Multiply(BinaryOp):
  cpdef int eval(self, int a, int b) except? -1:
    return a * b

def call_me(BinaryOp oper not None, c, d):
  return func(c, d, <void *>oper)

如您所见,BinaryOp 充当基类,它为未正确实现eval 的用户引发NotImplementedErrorcpdef 函数可以被 Cython 和 Python 用户覆盖,如果从 Cython 调用它们,则涉及高效的 C 调用机制。否则,从 Python 调用时会产生很小的开销(当然,这些函数在标量上工作,因此开销可能不会那么小)。

那么,Python 用户可能拥有以下应用程序代码:

# app_1.py
import pyximport
pyximport.install()

from binary_op import BinaryOp, Add, Multiply, call_me

print(call_me(Add(), 5, 6))
print(call_me(Multiply(), 5, 6))

class LinearOper(BinaryOp):
  def __init__(self, p1, p2):
    self.p1 = p1
    self.p2 = p2
  def eval(self, a, b):
    return self.p1 * a + self.p2 * b

print(call_me(LinearOper(3, 4), 5, 6))

如您所见,它们不仅可以从高效的 Cython(具体)类(即 AddMultiply)创建对象,还可以基于 BinaryOp 实现自己的类(希望通过提供实现eval)。当你运行python app_1.py 时,你会看到(编译后):

11
30
39

然后,您的 Cython 用户可以实现他们喜欢的功能,如下所示:

# sub.pyx
from binary_op cimport BinaryOp

cdef class Sub(BinaryOp):
  cpdef int eval(self, int a, int b) except? -1:
    return a - b

当然,任何使用sub.pyx 的应用程序代码都可以使用这两个库,如下所示:

import pyximport
pyximport.install()

from sub import Sub
from binary_op import call_me

print(call_me(Sub(), 5, 6))

当您运行python app_2.py 时,您会得到预期的结果:-1

编辑。顺便说一句,如果您被允许拥有上述function_type 签名(即,具有void * 参数作为第三个参数的签名),您can 实际上将任意 Python 可调用对象作为 C 指针传递。为此,您需要进行以下更改:

# binary_op.pyx
cdef int func(int a, int b, void *func_d) except? -1:
  return (<object>func_d)(a, b)


def call_me(oper not None, c, d):
  return func(c, d, <void *>oper)

但是请注意,Python 现在需要确定 object oper 是哪个。在前一个解决方案中,我们将 oper 限制为有效的 BinaryOp 对象。另请注意,__call__ 和类似的特殊功能只能声明为def,这会限制您的用例。不过,通过这些最后的更改,我们可以毫无问题地运行以下代码:

print(call_me(lambda x, y: x - y, 5, 6))

【讨论】:

    【解决方案3】:

    感谢@ead,我稍微修改了代码,结果令我满意:

    cdef class PyClass:
        cdef void py_wrap(self, function_type func, e, f):
            print(lambda_c(func, e, f))
    
    PyClass().py_wrap(mult, 5, 5)
    

    出于我的目的,可以使用 void 函数,但我不知道如何使它与方法一起工作,该方法应该返回一些值。对此案例的任何想法都会很有用

    UPD: cdef 方法在 python 中是不可见的,所以看起来没有办法让事情正常工作

    【讨论】:

    • 可以将函数指针暴露给 python 的一种方法是将函数指针包装在 cdef 类中。另一种选择可能是使用 python 的胶囊 api 来包装函数指针。您可能想看看 scipy 的 LowLevelCallable 以获得一些灵感。
    猜你喜欢
    • 1970-01-01
    • 2015-11-22
    • 1970-01-01
    • 1970-01-01
    • 2015-06-09
    • 2013-01-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多