Cython:为什么需要将 NumPy 数组类型转换为对象?

Bra*_*mon 5 python numpy cython

我在Pandas 源代码中看到过几次这样的事情:

def nancorr(ndarray[float64_t, ndim=2] mat, bint cov=0, minp=None):
    # ...
    N, K = (<object> mat).shape
Run Code Online (Sandbox Code Playgroud)

这意味着ndarray调用的 NumPymat类型转换为 Python 对象。*

经过进一步检查,似乎使用了这个,因为如果不是,则会出现编译错误。我的问题是:为什么首先需要这种类型转换

这里有一些例子。 这个答案只是表明元组打包在 Cython 中不像在 Python 中那样工作——但它似乎不是元组解包问题。(无论如何,这是一个很好的答案,我并不是要挑剔它。)

使用以下脚本,shape.pyx. 它将在编译时失败,并显示“无法将 'npy_intp *' 转换为 Python 对象”。

from cython cimport Py_ssize_t
import numpy as np
from numpy cimport ndarray, float64_t
cimport numpy as cnp
cnp.import_array()

def test_castobj(ndarray[float64_t, ndim=2] arr):

    cdef:
        Py_ssize_t b1, b2

    # Tuple unpacking - this will fail at compile
    b1, b2 = arr.shape
    return b1, b2
Run Code Online (Sandbox Code Playgroud)

但同样,问题本身似乎不是元组解包。这将失败并出现相同的错误。

def test_castobj(ndarray[float64_t, ndim=2] arr):

    cdef:
        # Py_ssize_t b1, b2
        ndarray[float64_t, ndim=2] zeros

    zeros = np.zeros(arr.shape, dtype=np.float64)
    return zeros
Run Code Online (Sandbox Code Playgroud)

看起来,这里没有发生元组解包。元组是第一个参数np.zeros

def test_castobj(ndarray[float64_t, ndim=2] arr):
    """This works"""
    cdef:
        Py_ssize_t b1, b2
        ndarray[float64_t, ndim=2] zeros

    b1, b2 = (<object> arr).shape
    zeros = np.zeros((<object> arr).shape, dtype=np.float64)
    return b1, b2, zeros
Run Code Online (Sandbox Code Playgroud)

这也有效(也许是最令人困惑的):

def test_castobj(object[float64_t, ndim=2] arr):
    cdef:
        tuple shape = arr.shape
        ndarray[float64_t, ndim=2] zeros
    zeros = np.zeros(shape, dtype=np.float64)
    return zeros
Run Code Online (Sandbox Code Playgroud)

例子:

>>> from shape import test_castobj
>>> arr = np.arange(6, dtype=np.float64).reshape(2, 3)

>>> test_castobj(arr)
(2, 3, array([[0., 0., 0.],
        [0., 0., 0.]]))
Run Code Online (Sandbox Code Playgroud)

*也许这与arr成为内存视图有关?但那是黑暗中的一击。


另一个例子是在 Cython文档中

cpdef int sum3d(int[:, :, :] arr) nogil:
    cdef size_t i, j, k
    cdef int total = 0
    I = arr.shape[0]
    J = arr.shape[1]
    K = arr.shape[2]
Run Code Online (Sandbox Code Playgroud)

在这种情况下,简单的索引arr.shape[i]可以防止错误,我觉得很奇怪。

这也有效:

def test_castobj(object[float64_t, ndim=2] arr):
    cdef ndarray[float64_t, ndim=2] zeros
    zeros = np.zeros(arr.shape, dtype=np.float64)
    return zeros
Run Code Online (Sandbox Code Playgroud)

ead*_*ead 1

你是对的,它与 Cython 下的元组拆包无关。

原因是,这cnp.ndarray不是一个通常的 numpy 数组(这意味着具有 Python 已知接口的 numpy 数组),而是numpy 的 C 实现的Cython 包装器PyArrayObject(在 Python 中称为np.array):

ctypedef class numpy.ndarray [object PyArrayObject]:
    cdef __cythonbufferdefaults__ = {"mode": "strided"}

    cdef:
        # Only taking a few of the most commonly used and stable fields.
        # One should use PyArray_* macros instead to access the C fields.
        char *data
        int ndim "nd"
        npy_intp *shape "dimensions"
        npy_intp *strides
        dtype descr
        PyObject* base
Run Code Online (Sandbox Code Playgroud)

shape实际上映射到底层 C 结构的dimensions-fieldnpy_intp *shape "dimensions"而不是简单地)。npy_intp *dimensions这是一个技巧,所以可以写

mat.shape[0]
Run Code Online (Sandbox Code Playgroud)

它的外观(以及某种程度上的感觉)就像shape调用了 numpy 的 python-property 一样。但实际上,我们采取了直接通向底层 C 结构的捷径。

顺便说一句,调用 python- 的shape成本相当高:必须创建一个元组并用 中的值填充dimensions,然后访问第 0 个元素。另一方面,Cython 的做法要便宜得多 - 只需访问正确的元素即可。

但是,如果您还想访问数组的 python 属性,则必须将其转换为普通的 python 对象(即忘记这是 a ndarray),然后shape通过通常的 Python 机制解析为元组属性调用。

所以基本上,即使这很方便,您也不希望像 pandas 代码中那样以紧密循环的方式访问 numpy 数组的维度,而是为了性能而执行更详细的变体:

...
N=mat.shape[0]
K=mat.shape[1]
...
Run Code Online (Sandbox Code Playgroud)

为什么你可以object[cnp.float64_t]在函数签名中编写或类似的内容让我觉得很奇怪 - 然后参数显然被解释为一个简单的对象。也许这只是一个错误。