类型化内存视图¶
注意
此页面使用两种不同的语法变体
Cython 特定的
cdef
语法,旨在使类型声明简洁明了,并易于从 C/C++ 的角度阅读。纯 Python 语法,允许在 纯 Python 代码 中进行静态 Cython 类型声明,遵循 PEP-484 类型提示和 PEP 526 变量注释。
要在 Python 语法中使用 C 数据类型,您需要在要编译的 Python 模块中导入特殊的
cython
模块,例如import cython
如果您使用纯 Python 语法,我们强烈建议您使用最新的 Cython 3 版本,因为与 0.29.x 版本相比,这里已经进行了重大改进。
类型化内存视图允许高效地访问内存缓冲区,例如那些位于 NumPy 数组底层的缓冲区,而不会产生任何 Python 开销。内存视图类似于当前的 NumPy 数组缓冲区支持 (np.ndarray[np.float64_t, ndim=2]
),但它们具有更多功能和更简洁的语法。
内存视图比旧的 NumPy 数组缓冲区支持更通用,因为它们可以处理更多种类的数组数据源。例如,它们可以处理 C 数组和 Cython 数组类型 (Cython 数组).
内存视图可以在任何上下文中使用(函数参数、模块级、cdef 类属性等),并且可以从几乎任何通过 PEP 3118 缓冲区接口公开可写缓冲区的对象中获取。
快速入门¶
如果您习惯使用 NumPy,以下示例将帮助您开始使用 Cython 内存视图。
from cython.cimports.cython.view import array as cvarray
import numpy as np
# Memoryview on a NumPy array
narr = np.arange(27, dtype=np.dtype("i")).reshape((3, 3, 3))
narr_view = cython.declare(cython.int[:, :, :], narr)
# Memoryview on a C array
carr = cython.declare(cython.int[3][3][3])
carr_view = cython.declare(cython.int[:, :, :], carr)
# Memoryview on a Cython array
cyarr = cvarray(shape=(3, 3, 3), itemsize=cython.sizeof(cython.int), format="i")
cyarr_view = cython.declare(cython.int[:, :, :], cyarr)
# Show the sum of all the arrays before altering it
print(f"NumPy sum of the NumPy array before assignments: {narr.sum()}")
# We can copy the values from one memoryview into another using a single
# statement, by either indexing with ... or (NumPy-style) with a colon.
carr_view[...] = narr_view
cyarr_view[:] = narr_view
# NumPy-style syntax for assigning a single value to all elements.
narr_view[:, :, :] = 3
# Just to distinguish the arrays
carr_view[0, 0, 0] = 100
cyarr_view[0, 0, 0] = 1000
# Assigning into the memoryview on the NumPy array alters the latter
print(f"NumPy sum of NumPy array after assignments: {narr.sum()}")
# A function using a memoryview does not usually need the GIL
@cython.nogil
@cython.ccall
def sum3d(arr: cython.int[:, :, :]) -> cython.int:
i: cython.size_t
j: cython.size_t
k: cython.size_t
I: cython.size_t
J: cython.size_t
K: cython.size_t
total: cython.int = 0
I = arr.shape[0]
J = arr.shape[1]
K = arr.shape[2]
for i in range(I):
for j in range(J):
for k in range(K):
total += arr[i, j, k]
return total
# A function accepting a memoryview knows how to use a NumPy array,
# a C array, a Cython array...
print(f"Memoryview sum of NumPy array is {sum3d(narr)}")
print(f"Memoryview sum of C array is {sum3d(carr)}")
print(f"Memoryview sum of Cython array is {sum3d(cyarr)}")
# ... and of course, a memoryview.
print(f"Memoryview sum of C memoryview is {sum3d(carr_view)}")
from cython.view cimport array as cvarray
import numpy as np
# Memoryview on a NumPy array
narr = np.arange(27, dtype=np.dtype("i")).reshape((3, 3, 3))
cdef int [:, :, :] narr_view = narr
# Memoryview on a C array
cdef int[3][3][3] carr
cdef int [:, :, :] carr_view = carr
# Memoryview on a Cython array
cyarr = cvarray(shape=(3, 3, 3), itemsize=sizeof(int), format="i")
cdef int [:, :, :] cyarr_view = cyarr
# Show the sum of all the arrays before altering it
print(f"NumPy sum of the NumPy array before assignments: {narr.sum()}")
# We can copy the values from one memoryview into another using a single
# statement, by either indexing with ... or (NumPy-style) with a colon.
carr_view[...] = narr_view
cyarr_view[:] = narr_view
# NumPy-style syntax for assigning a single value to all elements.
narr_view[:, :, :] = 3
# Just to distinguish the arrays
carr_view[0, 0, 0] = 100
cyarr_view[0, 0, 0] = 1000
# Assigning into the memoryview on the NumPy array alters the latter
print(f"NumPy sum of NumPy array after assignments: {narr.sum()}")
# A function using a memoryview does not usually need the GIL
cpdef int sum3d(int[:, :, :] arr) nogil:
cdef size_t i, j, k, I, J, K
cdef int total = 0
I = arr.shape[0]
J = arr.shape[1]
K = arr.shape[2]
for i in range(I):
for j in range(J):
for k in range(K):
total += arr[i, j, k]
return total
# A function accepting a memoryview knows how to use a NumPy array,
# a C array, a Cython array...
print(f"Memoryview sum of NumPy array is {sum3d(narr)}")
print(f"Memoryview sum of C array is {sum3d(carr)}")
print(f"Memoryview sum of Cython array is {sum3d(cyarr)}")
# ... and of course, a memoryview.
print(f"Memoryview sum of C memoryview is {sum3d(carr_view)}")
此代码应给出以下输出
NumPy sum of the NumPy array before assignments: 351
NumPy sum of NumPy array after assignments: 81
Memoryview sum of NumPy array is 81
Memoryview sum of C array is 451
Memoryview sum of Cython array is 1351
Memoryview sum of C memoryview is 451
使用内存视图¶
语法¶
内存视图使用类似于 NumPy 的 Python 切片语法。
要创建一个对一维 int
缓冲区的完整视图
view1D: cython.int[:] = exporting_object
cdef int[:] view1D = exporting_object
一个完整的 3D 视图
view3D: cython.int[:,:,:] = exporting_object
cdef int[:,:,:] view3D = exporting_object
它们也方便地用作函数参数
def process_3d_buffer(view: cython.int[:,:,:]):
...
def process_3d_buffer(int[:,:,:] view not None):
...
Cython not None
声明用于参数会自动拒绝 None
值作为输入,否则将允许。默认情况下允许 None
的原因是它方便地用于返回值参数。另一方面,当使用纯 python 模式时,默认情况下会拒绝 None
值。只有当类型声明为 Optional
时才允许。
import numpy as np
import typing
def process_buffer(input_view: cython.int[:,:],
output_view: typing.Optional[cython.int[:,:]] = None):
if output_view is None:
# Creating a default view, e.g.
output_view = np.empty_like(input_view)
# process 'input_view' into 'output_view'
return output_view
process_buffer(None, None)
import numpy as np
def process_buffer(int[:,:] input_view not None,
int[:,:] output_view=None):
if output_view is None:
# Creating a default view, e.g.
output_view = np.empty_like(input_view)
# process 'input_view' into 'output_view'
return output_view
process_buffer(None, None)
Cython 会自动拒绝不兼容的缓冲区,例如将三维缓冲区传递给需要二维缓冲区的函数将引发 ValueError
。
要在具有自定义类型的 numpy 数组上使用内存视图,您需要声明一个等效的打包结构,以模仿该类型
import numpy as np
CUSTOM_DTYPE = np.dtype([
('x', np.uint8),
('y', np.float32),
])
a = np.zeros(100, dtype=CUSTOM_DTYPE)
cdef packed struct custom_dtype_struct:
# The struct needs to be packed since by default numpy dtypes aren't
# aligned
unsigned char x
float y
def sum(custom_dtype_struct [:] a):
cdef:
unsigned char sum_x = 0
float sum_y = 0.
for i in range(a.shape[0]):
sum_x += a[i].x
sum_y += a[i].y
return sum_x, sum_y
注意
纯 python 模式目前不支持打包结构
索引¶
在 Cython 中,内存视图上的索引访问会自动转换为内存地址。以下代码请求 C int
类型项的二维内存视图,并对其进行索引
buf: cython.int[:,:] = exporting_object
print(buf[1,2])
cdef int[:,:] buf = exporting_object
print(buf[1,2])
负索引也适用,从各自维度的末尾开始计数
print(buf[-1,-2])
以下函数遍历二维数组的每个维度,并将每个项加 1
import numpy as np
def add_one(buf: cython.int[:,:]):
for x in range(buf.shape[0]):
for y in range(buf.shape[1]):
buf[x, y] += 1
# exporting_object must be a Python object
# implementing the buffer interface, e.g. a numpy array.
exporting_object = np.zeros((10, 20), dtype=np.intc)
add_one(exporting_object)
import numpy as np
def add_one(int[:,:] buf):
for x in range(buf.shape[0]):
for y in range(buf.shape[1]):
buf[x, y] += 1
# exporting_object must be a Python object
# implementing the buffer interface, e.g. a numpy array.
exporting_object = np.zeros((10, 20), dtype=np.intc)
add_one(exporting_object)
索引和切片可以在有或没有 GIL 的情况下完成。它基本上与 NumPy 一样。如果为每个维度指定了索引,您将获得基本类型的元素(例如 int
)。否则,您将获得一个新的视图。省略号表示您将为每个未指定的维度获得连续的切片
import numpy as np
def main():
exporting_object = np.arange(0, 15 * 10 * 20, dtype=np.intc).reshape((15, 10, 20))
my_view: cython.int[:, :, :] = exporting_object
# These are all equivalent
my_view[10]
my_view[10, :, :]
my_view[10, ...]
import numpy as np
exporting_object = np.arange(0, 15 * 10 * 20, dtype=np.intc).reshape((15, 10, 20))
cdef int[:, :, :] my_view = exporting_object
# These are all equivalent
my_view[10]
my_view[10, :, :]
my_view[10, ...]
复制¶
内存视图可以在原地复制
import numpy as np
def main():
to_view: cython.int[:, :, :] = np.empty((20, 15, 30), dtype=np.intc)
from_view: cython.int[:, :, :] = np.ones((20, 15, 30), dtype=np.intc)
# copy the elements in from_view to to_view
to_view[...] = from_view
# or
to_view[:] = from_view
# or
to_view[:, :, :] = from_view
import numpy as np
cdef int[:, :, :] to_view, from_view
to_view = np.empty((20, 15, 30), dtype=np.intc)
from_view = np.ones((20, 15, 30), dtype=np.intc)
# copy the elements in from_view to to_view
to_view[...] = from_view
# or
to_view[:] = from_view
# or
to_view[:, :, :] = from_view
它们也可以使用 copy()
和 copy_fortran()
方法复制;请参见 C 和 Fortran 连续复制。
转置¶
在大多数情况下(见下文),内存视图可以像 NumPy 切片一样转置
import numpy as np
def main():
array = np.arange(20, dtype=np.intc).reshape((2, 10))
c_contig: cython.int[:, ::1] = array
f_contig: cython.int[::1, :] = c_contig.T
import numpy as np
array = np.arange(20, dtype=np.intc).reshape((2, 10))
cdef int[:, ::1] c_contig = array
cdef int[::1, :] f_contig = c_contig.T
这将提供一个新的、转置的、对数据的视图。
转置要求内存视图的所有维度都具有直接访问内存布局(即,没有通过指针的间接访问)。有关详细信息,请参见 指定更通用的内存布局。
新轴¶
与 NumPy 一样,可以通过用 None
索引数组来引入新轴
myslice: cython.double[:] = np.linspace(0, 10, num=50)
# 2D array with shape (1, 50)
myslice[None] # or
myslice[None, :]
# 2D array with shape (50, 1)
myslice[:, None]
# 3D array with shape (1, 10, 1)
myslice[None, 10:-20:2, None]
cdef double[:] myslice = np.linspace(0, 10, num=50)
# 2D array with shape (1, 50)
myslice[None] # or
myslice[None, :]
# 2D array with shape (50, 1)
myslice[:, None]
# 3D array with shape (1, 10, 1)
myslice[None, 10:-20:2, None]
可以将新轴索引与所有其他形式的索引和切片混合使用。另请参见 示例。
只读视图¶
注意
纯 Python 模式目前不支持只读视图。
从 Cython 0.28 开始,内存视图项类型可以声明为 const
以支持只读缓冲区作为输入
import numpy as np
cdef const double[:] myslice # const item type => read-only view
a = np.linspace(0, 10, num=50)
a.setflags(write=False)
myslice = a
使用非 const 内存视图与二进制 Python 字符串一起使用会导致运行时错误。可以使用 const
内存视图解决此问题
cdef bint is_y_in(const unsigned char[:] string_view):
cdef int i
for i in range(string_view.shape[0]):
if string_view[i] == b'y':
return True
return False
print(is_y_in(b'hello world')) # False
print(is_y_in(b'hello Cython')) # True
请注意,这并不要求输入缓冲区为只读
a = np.linspace(0, 10, num=50)
myslice = a # read-only view of a writable buffer
可写缓冲区仍然被 const
视图接受,但只读缓冲区不被非 const、可写视图接受
cdef double[:] myslice # a normal read/write memory view
a = np.linspace(0, 10, num=50)
a.setflags(write=False)
myslice = a # ERROR: requesting writable memory view from read-only buffer!
与旧的缓冲区支持的比较¶
您可能更喜欢内存视图而不是旧的语法,因为
语法更简洁
内存视图通常不需要 GIL(请参见 内存视图和 GIL)
内存视图的速度要快得多
例如,这是上面 sum3d
函数的旧语法等效项
cpdef int old_sum3d(object[int, ndim=3, mode='strided'] arr):
cdef int I, J, K, total = 0
I = arr.shape[0]
J = arr.shape[1]
K = arr.shape[2]
for i in range(I):
for j in range(J):
for k in range(K):
total += arr[i, j, k]
return total
请注意,我们不能对函数的缓冲区版本使用 nogil
,就像我们对上面 sum3d
的内存视图版本一样,因为缓冲区对象是 Python 对象。但是,即使我们不对内存视图使用 nogil
,它也快得多。这是导入两个版本后 IPython 会话的输出
In [2]: import numpy as np
In [3]: arr = np.zeros((40, 40, 40), dtype=int)
In [4]: timeit -r15 old_sum3d(arr)
1000 loops, best of 15: 298 us per loop
In [5]: timeit -r15 sum3d(arr)
1000 loops, best of 15: 219 us per loop
Python 缓冲区支持¶
Cython 内存视图支持几乎所有导出 Python 新式缓冲区 接口的对象。这是 PEP 3118 中描述的缓冲区接口。NumPy 数组支持此接口,Cython 数组 也支持此接口。“几乎所有”是因为 Python 缓冲区接口允许数据数组中的元素本身是指针;Cython 内存视图尚不支持此功能。
内存布局¶
缓冲区接口允许对象以多种方式识别底层内存。除了数据元素的指针外,Cython 内存视图支持所有 Python 新类型缓冲区布局。如果内存必须以特定格式用于外部例程或用于代码优化,则了解或指定内存布局可能很有用。
背景¶
概念如下:存在数据访问和数据打包。数据访问意味着直接(无指针)或间接(指针)。数据打包意味着您的数据在内存中可能是连续的或不连续的,并且可以使用步幅来识别每个维度连续索引需要采取的内存跳跃。
NumPy 数组提供了带步幅的直接数据访问的良好模型,因此我们将使用它们来回顾 C 和 Fortran 连续数组以及数据步幅的概念。
关于 C、Fortran 和带步幅的内存布局的简要回顾¶
最简单的数据布局可能是 C 连续数组。这是 NumPy 和 Cython 数组中的默认布局。C 连续意味着数组数据在内存中是连续的(见下文),并且数组第一维中的相邻元素在内存中相距最远,而最后一维中的相邻元素在内存中相距最近。例如,在 NumPy 中
In [2]: arr = np.array([['0', '1', '2'], ['3', '4', '5']], dtype='S1')
这里,arr[0, 0]
和 arr[0, 1]
在内存中相距一个字节,而 arr[0, 0]
和 arr[1, 0]
在内存中相距 3 个字节。这使我们想到了步幅的概念。数组的每个轴都有一个步幅长度,它是从该轴上的一个元素到下一个元素所需的字节数。在上面的情况下,轴 0 和 1 的步幅显然将是
In [3]: arr.strides
Out[4]: (3, 1)
对于一个 3D C 连续数组
In [5]: c_contig = np.arange(24, dtype=np.int8).reshape((2,3,4))
In [6] c_contig.strides
Out[6]: (12, 4, 1)
Fortran 连续数组的内存排序相反,第一个轴上的元素在内存中彼此最靠近
In [7]: f_contig = np.array(c_contig, order='F')
In [8]: np.all(f_contig == c_contig)
Out[8]: True
In [9]: f_contig.strides
Out[9]: (1, 2, 6)
连续数组是指所有元素数据都存储在单个连续内存块中的数组,因此内存块长度是数组元素数量和元素大小(以字节为单位)的乘积。在上面的例子中,内存块长度为 2 * 3 * 4 * 1 字节,其中 1 是 np.int8
的长度。
一个数组可以是连续的,但不是 C 或 Fortran 顺序
In [10]: c_contig.transpose((1, 0, 2)).strides
Out[10]: (4, 12, 1)
对 NumPy 数组进行切片很容易使其不再连续
In [11]: sliced = c_contig[:,1,:]
In [12]: sliced.strides
Out[12]: (12, 1)
In [13]: sliced.flags
Out[13]:
C_CONTIGUOUS : False
F_CONTIGUOUS : False
OWNDATA : False
WRITEABLE : True
ALIGNED : True
UPDATEIFCOPY : False
内存视图布局的默认行为¶
正如您将在 指定更通用的内存布局 中看到的那样,您可以为内存视图的任何维度指定内存布局。对于您未指定布局的任何维度,数据访问被假定为直接,数据打包被假定为步长。例如,对于像这样的内存视图,将是这种假设
my_memoryview: cython.int[:, :, :] = obj
int [:, :, :] my_memoryview = obj
C 和 Fortran 连续内存视图¶
您可以使用定义中的 ::1
步长语法为内存视图指定 C 和 Fortran 连续布局。例如,如果您确定您的内存视图将位于 3D C 连续布局之上,则可以编写
c_contiguous: cython.int[:, :, ::1] = c_contig
cdef int[:, :, ::1] c_contiguous = c_contig
其中 c_contig
可以是 C 连续的 NumPy 数组。第 3 个位置的 ::1
表示此第 3 维中的元素在内存中将相隔一个元素。如果您知道您将拥有一个 3D Fortran 连续数组
f_contiguous: cython.int[::1, :, :] = f_contig
cdef int[::1, :, :] f_contiguous = f_contig
如果您传递一个非连续缓冲区,例如
# This array is C contiguous
c_contig = np.arange(24).reshape((2,3,4))
c_contiguous: cython.int[:, :, ::1] = c_contig
# But this isn't
c_contiguous = np.array(c_contig, order='F')
# This array is C contiguous
c_contig = np.arange(24).reshape((2,3,4))
cdef int[:, :, ::1] c_contiguous = c_contig
# But this isn't
c_contiguous = np.array(c_contig, order='F')
您将在运行时得到一个 ValueError
/Users/mb312/dev_trees/minimal-cython/mincy.pyx in init mincy (mincy.c:17267)()
69
70 # But this isn't
---> 71 c_contiguous = np.array(c_contig, order='F')
72
73 # Show the sum of all the arrays before altering it
/Users/mb312/dev_trees/minimal-cython/stringsource in View.MemoryView.memoryview_cwrapper (mincy.c:9995)()
/Users/mb312/dev_trees/minimal-cython/stringsource in View.MemoryView.memoryview.__cinit__ (mincy.c:6799)()
ValueError: ndarray is not C-contiguous
因此,切片类型规范中的 ::1
指示数据在哪个维度上是连续的。它只能用于指定完整的 C 或 Fortran 连续性。
C 和 Fortran 连续副本¶
可以使用 .copy()
和 .copy_fortran()
方法制作 C 或 Fortran 连续副本
# This view is C contiguous
c_contiguous: cython.int[:, :, ::1] = myview.copy()
# This view is Fortran contiguous
f_contiguous_slice: cython.int[::1, :] = myview.copy_fortran()
# This view is C contiguous
cdef int[:, :, ::1] c_contiguous = myview.copy()
# This view is Fortran contiguous
cdef int[::1, :] f_contiguous_slice = myview.copy_fortran()
指定更通用的内存布局¶
可以使用之前看到的 ::1
切片语法,或使用 cython.view
中的任何常量来指定数据布局。如果在任何维度中都没有指定说明符,则数据访问被假定为直接,数据打包被假定为步长。如果您不知道某个维度将是直接还是间接(因为您可能从某个库中获取了一个具有缓冲区接口的对象),那么您可以指定 generic
标志,在这种情况下,它将在运行时确定。
标志如下
generic
- 步长和直接或间接strided
- 步长和直接(这是默认值)indirect
- 步长和间接contiguous
- 连续和直接indirect_contiguous
- 指针列表是连续的
它们可以像这样使用
from cython.cimports.cython import view
def main():
# direct access in both dimensions, strided in the first dimension, contiguous in the last
a: cython.int[:, ::view.contiguous]
# contiguous list of pointers to contiguous lists of ints
b: cython.int[::view.indirect_contiguous, ::1]
# direct or indirect in the first dimension, direct in the second dimension
# strided in both dimensions
c: cython.int[::view.generic, :]
from cython cimport view
def main():
# direct access in both dimensions, strided in the first dimension, contiguous in the last
cdef int[:, ::view.contiguous] a
# contiguous list of pointers to contiguous lists of ints
cdef int[::view.indirect_contiguous, ::1] b
# direct or indirect in the first dimension, direct in the second dimension
# strided in both dimensions
cdef int[::view.generic, :] c
只有第一个、最后一个或紧随间接维度的维度可以指定为连续的
from cython.cimports.cython import view
def main():
# VALID
a: cython.int[::view.indirect, ::1, :]
b: cython.int[::view.indirect, :, ::1]
c: cython.int[::view.indirect_contiguous, ::1, :]
# INVALID
d: cython.int[::view.contiguous, ::view.indirect, :]
e: cython.int[::1, ::view.indirect, :]
from cython cimport view
def main():
# VALID
cdef int[::view.indirect, ::1, :] a
cdef int[::view.indirect, :, ::1] b
cdef int[::view.indirect_contiguous, ::1, :] c
# INVALID
cdef int[::view.contiguous, ::view.indirect, :] d
cdef int[::1, ::view.indirect, :] e
contiguous
标志和 ::1
指定符之间的区别在于,前者仅指定一个维度的连续性,而后者指定所有后续(Fortran)或先前(C)维度的连续性。
c_contig: cython.int[:, ::1] = ...
# VALID
myslice: cython.int[:, ::view.contiguous] = c_contig[::2]
# INVALID
myslice: cython.int[:, ::1] = c_contig[::2]
cdef int[:, ::1] c_contig = ...
# VALID
cdef int[:, ::view.contiguous] myslice = c_contig[::2]
# INVALID
cdef int[:, ::1] myslice = c_contig[::2]
前一种情况是有效的,因为最后一维保持连续,但第一维不再“跟随”最后一维(这意味着,它已经被步长化,但不再是 C 或 Fortran 连续的),因为它被切片了。
内存视图和 GIL¶
正如您将在 快速入门 部分中看到的那样,内存视图通常不需要 GIL。
@cython.nogil
@cython.ccall
def sum3d(arr: cython.int[:, :, :]) -> cython.int:
...
cpdef int sum3d(int[:, :, :] arr) nogil:
...
特别是,您不需要为内存视图索引、切片或转置使用 GIL。内存视图在以下情况下需要 GIL:复制方法 (C 和 Fortran 连续复制),或当 dtype 为 object 且读取或写入 object 元素时。
内存视图对象和 Cython 数组¶
这些类型化的内存视图可以转换为 Python 内存视图对象 (cython.view.memoryview
)。这些 Python 对象是可索引的、可切片的和可转置的,与原始内存视图相同。它们也可以随时转换回 Cython 空间内存视图。
它们具有以下属性
shape
:每个维度的尺寸,作为元组。
strides
:沿每个维度的步长,以字节为单位。
子偏移量
ndim
:维数。
size
:视图中项目的总数(形状的乘积)。
itemsize
:视图中项目的尺寸,以字节为单位。
nbytes
:等于size
乘以itemsize
。
base
当然还有前面提到的 T
属性 (转置)。这些属性与 NumPy 中的语义相同。例如,要检索原始对象
import numpy
from cython.cimports.numpy import int32_t
def main():
a: int32_t[:] = numpy.arange(10, dtype=numpy.int32)
a = a[::2]
print(a)
print(numpy.asarray(a))
print(a.base)
# this prints:
# <MemoryView of 'ndarray' object>
# [0 2 4 6 8]
# [0 1 2 3 4 5 6 7 8 9]
import numpy
cimport numpy as cnp
cdef cnp.int32_t[:] a = numpy.arange(10, dtype=numpy.int32)
a = a[::2]
print(a)
print(numpy.asarray(a))
print(a.base)
# this prints:
# <MemoryView of 'ndarray' object>
# [0 2 4 6 8]
# [0 1 2 3 4 5 6 7 8 9]
请注意,此示例返回获取视图的原始对象,并且视图在此期间被重新切片。
Cython 数组¶
每当复制 Cython 内存视图(使用任何 copy()
或 copy_fortran()
方法)时,您都会获得新创建的 cython.view.array
对象的新内存视图切片。此数组也可以手动使用,并将自动分配一块数据。它以后可以分配给 C 或 Fortran 连续切片(或步长切片)。它可以像这样使用
from cython.cimports.cython import view
my_array = view.array(shape=(10, 2), itemsize=cython.sizeof(cython.int), format="i")
my_slice: cython.int[:, :] = my_array
from cython cimport view
my_array = view.array(shape=(10, 2), itemsize=sizeof(int), format="i")
cdef int[:, :] my_slice = my_array
它还接受一个可选参数 mode
('c' 或 'fortran')和一个布尔值 allocate_buffer
,它指示是否应该分配缓冲区并在其超出范围时释放。
my_array: view.array = view.array(..., mode="fortran", allocate_buffer=False)
my_array.data = cython.cast(cython.p_char, my_data_pointer)
# define a function that can deallocate the data (if needed)
my_array.callback_free_data = free
cdef view.array my_array = view.array(..., mode="fortran", allocate_buffer=False)
my_array.data = <char *> my_data_pointer
# define a function that can deallocate the data (if needed)
my_array.callback_free_data = free
您还可以将指针转换为数组,或将 C 数组转换为数组
my_array: view.array = cython.cast(cython.int[:10, :2], my_data_pointer)
my_array: view.array = cython.cast(cython.int[:, :], my_c_array)
cdef view.array my_array = <int[:10, :2]> my_data_pointer
cdef view.array my_array = <int[:, :]> my_c_array
当然,您也可以立即将 cython.view.array 分配给类型化的内存视图切片。C 数组可以直接分配给内存视图切片
myslice: cython.int[:, ::1] = my_2d_c_array
cdef int[:, ::1] myslice = my_2d_c_array
数组与内存视图对象一样,可以从 Python 空间进行索引和切片,并具有与内存视图对象相同的属性。
CPython 数组模块¶
cython.view.array
的替代方案是 Python 标准库中的 array
模块。在 Python 3 中,array.array
类型本机支持缓冲区接口,因此内存视图在没有额外设置的情况下就可以在其之上工作。
然而,从 Cython 0.17 开始,可以使用这些数组作为缓冲区提供者,即使在 Python 2 中也是如此。这可以通过显式地 cimport cpython.array
模块来实现,如下所示
def sum_array(view: cython.int[:]):
"""
>>> from array import array
>>> sum_array( array('i', [1,2,3]) )
6
"""
total: cython.int = 0
for i in range(view.shape[0]):
total += view[i]
return total
def sum_array(int[:] view):
"""
>>> from array import array
>>> sum_array( array('i', [1,2,3]) )
6
"""
cdef int total = 0
for i in range(view.shape[0]):
total += view[i]
return total
请注意,cimport 还启用了数组类型的旧缓冲区语法。因此,以下代码也适用
from cython.cimports.cpython import array
def sum_array(arr: array.array[cython.int]): # using old buffer syntax
...
from cpython cimport array
def sum_array(array.array[int] arr): # using old buffer syntax
...
强制转换为 NumPy¶
Memoryview(和数组)对象可以强制转换为 NumPy ndarray,而无需复制数据。例如,可以执行以下操作
from cython.cimports.numpy import int32_t
import numpy as np
numpy_array = np.asarray(cython.cast(int32_t[:10, :10], my_pointer))
cimport numpy as cnp
import numpy as np
numpy_array = np.asarray(<cnp.int32_t[:10, :10]> my_pointer)
当然,您不限于使用 NumPy 的类型(例如,此处为 cnp.int32_t
),可以使用任何可用的类型。
无切片¶
虽然 memoryview 切片不是对象,但可以将其设置为 None
,并且也可以检查其是否为 None
def func(myarray: typing.Optional[cython.double[:]] = None):
print(myarray is None)
def func(double[:] myarray = None):
print(myarray is None)
如果函数需要真正的内存视图作为输入,那么最好在签名中直接拒绝 None
输入
def func(myarray: cython.double[:]):
...
def func(double[:] myarray not None):
...
与扩展类的对象属性不同,memoryview 切片不会初始化为 None
。
通过指针传递来自 C 函数的数据¶
由于在 C 中使用指针无处不在,因此这里我们将快速介绍如何调用其参数包含指针的 C 函数。假设您想使用 NumPy 管理一个数组(分配和释放)(它也可以是 Python 数组,或者任何支持缓冲区接口的数组),但您想使用 C_func_file.c
中实现的外部 C 函数对该数组执行计算
1#include "C_func_file.h"
2
3void multiply_by_10_in_C(double arr[], unsigned int n)
4{
5 unsigned int i;
6 for (i = 0; i < n; i++) {
7 arr[i] *= 10;
8 }
9}
此文件附带一个名为 C_func_file.h
的头文件,其中包含
1#ifndef C_FUNC_FILE_H
2#define C_FUNC_FILE_H
3
4void multiply_by_10_in_C(double arr[], unsigned int n);
5
6#endif
其中 arr
指向数组,而 n
是其大小。
可以在 Cython 文件中以以下方式调用该函数
1cdef extern from "C_func_file.c":
2 # The C file is include directly so that it doesn't need to be compiled separately.
3 pass
4
5cdef extern from "C_func_file.h":
6 void multiply_by_10_in_C(double *, unsigned int)
1import numpy as np
2
3def multiply_by_10(arr): # 'arr' is a one-dimensional numpy array
4
5 if not arr.flags['C_CONTIGUOUS']:
6 arr = np.ascontiguousarray(arr) # Makes a contiguous copy of the numpy array.
7
8 arr_memview: cython.double[::1] = arr
9
10 multiply_by_10_in_C(cython.address(arr_memview[0]), arr_memview.shape[0])
11
12 return arr
13
14
15a = np.ones(5, dtype=np.double)
16print(multiply_by_10(a))
17
18b = np.ones(10, dtype=np.double)
19b = b[::2] # b is not contiguous.
20
21print(multiply_by_10(b)) # but our function still works as expected.
1cdef extern from "C_func_file.c":
2 # C is include here so that it doesn't need to be compiled externally
3 pass
4
5cdef extern from "C_func_file.h":
6 void multiply_by_10_in_C(double *, unsigned int)
7
8import numpy as np
9
10def multiply_by_10(arr): # 'arr' is a one-dimensional numpy array
11
12 if not arr.flags['C_CONTIGUOUS']:
13 arr = np.ascontiguousarray(arr) # Makes a contiguous copy of the numpy array.
14
15 cdef double[::1] arr_memview = arr
16
17 multiply_by_10_in_C(&arr_memview[0], arr_memview.shape[0])
18
19 return arr
20
21
22a = np.ones(5, dtype=np.double)
23print(multiply_by_10(a))
24
25b = np.ones(10, dtype=np.double)
26b = b[::2] # b is not contiguous.
27
28print(multiply_by_10(b)) # but our function still works as expected.
- 需要注意几点
::1
请求 C 连续视图,如果缓冲区不是 C 连续的,则会失败。请参阅 C 和 Fortran 连续内存视图。&arr_memview[0]
和cython.address(arr_memview[0]
可以理解为“内存视图第一个元素的地址”。对于连续数组,这等效于平面内存缓冲区的起始地址。arr_memview.shape[0]
可以用arr_memview.size
、arr.shape[0]
或arr.size
替换。但arr_memview.shape[0]
更有效,因为它不需要任何 Python 交互。multiply_by_10
将在传递的数组是连续的情况下就地执行计算,如果arr
不是连续的,则将返回一个新的 numpy 数组。如果您使用的是 Python 数组而不是 numpy 数组,则无需检查数据是否连续存储,因为这种情况总是如此。请参阅 使用 Python 数组。
通过这种方式,您可以像调用普通 Python 函数一样调用 C 函数,并将所有内存管理和清理工作留给 NumPy 数组和 Python 的对象处理。有关如何编译和调用 C 文件中函数的详细信息,请参阅 使用 C 库。
性能:禁用初始化检查¶
每次访问 memoryview 时,Cython 都会添加一个检查以确保它已初始化。
如果您正在寻找性能,可以通过将 initializedcheck
指令设置为 False
来禁用它们。请参阅:编译器指令,以获取有关此指令的更多信息。