Commit 71064916 authored by Mark Florisson's avatar Mark Florisson

Support memoryview(slice) -> NumPy coercion + NumPy-like attributes

parent dca00ef2
...@@ -11,6 +11,7 @@ cdef extern from "Python.h": ...@@ -11,6 +11,7 @@ cdef extern from "Python.h":
PyBUF_F_CONTIGUOUS, PyBUF_F_CONTIGUOUS,
PyBUF_ANY_CONTIGUOUS PyBUF_ANY_CONTIGUOUS
PyBUF_FORMAT PyBUF_FORMAT
PyBUF_WRITABLE
void Py_INCREF(object) void Py_INCREF(object)
...@@ -33,6 +34,7 @@ cdef class array: ...@@ -33,6 +34,7 @@ cdef class array:
unicode mode unicode mode
bytes _format bytes _format
void (*callback_free_data)(char *data) void (*callback_free_data)(char *data)
cdef object _memview
def __cinit__(array self, tuple shape, Py_ssize_t itemsize, format, def __cinit__(array self, tuple shape, Py_ssize_t itemsize, format,
mode=u"c", bint allocate_buffer=True): mode=u"c", bint allocate_buffer=True):
...@@ -113,6 +115,7 @@ cdef class array: ...@@ -113,6 +115,7 @@ cdef class array:
info.strides = self.strides info.strides = self.strides
info.suboffsets = NULL info.suboffsets = NULL
info.itemsize = self.itemsize info.itemsize = self.itemsize
info.readonly = 0
if flags & PyBUF_FORMAT: if flags & PyBUF_FORMAT:
info.format = self.format info.format = self.format
...@@ -138,13 +141,24 @@ cdef class array: ...@@ -138,13 +141,24 @@ cdef class array:
self.format = NULL self.format = NULL
self.itemsize = 0 self.itemsize = 0
def __getitem__(self, index): property memview:
view = __pyx_memoryview_new(self, PyBUF_ANY_CONTIGUOUS|PyBUF_FORMAT) @cname('__pyx_cython_array_get_memview')
return view[index] def __get__(self):
# Make this a property as 'data' may be set after instantiation
if self._memview is None:
flags = PyBUF_ANY_CONTIGUOUS|PyBUF_FORMAT|PyBUF_WRITABLE
self._memview = __pyx_memoryview_new(self, flags)
return self._memview
def __getattr__(self, attr):
return getattr(self.memview, attr)
def __setitem__(self, index, value): def __getitem__(self, item):
view = __pyx_memoryview_new(self, PyBUF_ANY_CONTIGUOUS|PyBUF_FORMAT) return self.memview[item]
view[index] = value
def __setitem__(self, item, value):
self.memview[item] = value
@cname("__pyx_array_new") @cname("__pyx_array_new")
...@@ -165,6 +179,7 @@ import cython ...@@ -165,6 +179,7 @@ import cython
# from cpython cimport ... # from cpython cimport ...
cdef extern from "Python.h": cdef extern from "Python.h":
int PyIndex_Check(object) int PyIndex_Check(object)
object PyLong_FromVoidPtr(void *)
cdef extern from "pythread.h": cdef extern from "pythread.h":
ctypedef void *PyThread_type_lock ctypedef void *PyThread_type_lock
...@@ -244,6 +259,8 @@ cdef indirect_contiguous = Enum("<contiguous and indirect>") ...@@ -244,6 +259,8 @@ cdef indirect_contiguous = Enum("<contiguous and indirect>")
cdef class memoryview(object): cdef class memoryview(object):
cdef object obj cdef object obj
cdef object _size
cdef object _array_interface
cdef PyThread_type_lock lock cdef PyThread_type_lock lock
cdef int acquisition_count cdef int acquisition_count
cdef Py_buffer view cdef Py_buffer view
...@@ -336,6 +353,7 @@ cdef class memoryview(object): ...@@ -336,6 +353,7 @@ cdef class memoryview(object):
info[0] = self.view info[0] = self.view
info.obj = self info.obj = self
# Some properties that have the same sematics as in NumPy
property T: property T:
@cname('__pyx_memoryview_transpose') @cname('__pyx_memoryview_transpose')
def __get__(self): def __get__(self):
...@@ -343,8 +361,8 @@ cdef class memoryview(object): ...@@ -343,8 +361,8 @@ cdef class memoryview(object):
transpose_memslice(&result.from_slice) transpose_memslice(&result.from_slice)
return result return result
property object: property base:
@cname('__pyx_memoryview__get__object') @cname('__pyx_memoryview__get__base')
def __get__(self): def __get__(self):
return self.obj return self.obj
...@@ -353,23 +371,75 @@ cdef class memoryview(object): ...@@ -353,23 +371,75 @@ cdef class memoryview(object):
def __get__(self): def __get__(self):
return tuple([self.view.shape[i] for i in xrange(self.view.ndim)]) return tuple([self.view.shape[i] for i in xrange(self.view.ndim)])
property strides: property strides:
@cname('__pyx_memoryview_get_strides') @cname('__pyx_memoryview_get_strides')
def __get__(self): def __get__(self):
return tuple([self.view.strides[i] for i in xrange(self.view.ndim)]) return tuple([self.view.strides[i] for i in xrange(self.view.ndim)])
property suboffsets: property suboffsets:
@cname('__pyx_memoryview_get_suboffsets') @cname('__pyx_memoryview_get_suboffsets')
def __get__(self): def __get__(self):
return tuple([self.view.suboffsets[i] for i in xrange(self.view.ndim)]) return tuple([self.view.suboffsets[i] for i in xrange(self.view.ndim)])
property ndim:
@cname('__pyx_memoryview_get_ndim')
def __get__(self):
return self.view.ndim
property itemsize:
@cname('__pyx_memoryview_get_itemsize')
def __get__(self):
return self.view.itemsize
property nbytes:
@cname('__pyx_memoryview_get_nbytes')
def __get__(self):
return self.size * self.view.itemsize
property size:
@cname('__pyx_memoryview_get_size')
def __get__(self):
if self._size is None:
result = 1
for length in self.shape:
result *= length
self._size = result
return self._size
property __array_interface__:
@cname('__pyx_numpy_array_interface')
def __get__(self):
"Interface for NumPy to obtain a ndarray from this object"
# Note: we always request writable buffers, so we set readonly to
# False in the data tuple
if self._array_interface is None:
for suboffset in self.suboffsets:
if suboffset >= 0:
raise ValueError("Cannot convert indirect buffer to numpy object")
self._array_interface = dict(
data = (PyLong_FromVoidPtr(self.view.buf), False),
shape = self.shape,
strides = self.strides,
typestr = "%s%d" % (self.format, self.view.itemsize),
version = 3)
return self._array_interface
def __len__(self):
if self.view.ndim >= 1:
return self.view.shape[0]
return 0
def __repr__(self): def __repr__(self):
return "<MemoryView of %r at 0x%x>" % (self.object.__class__.__name__, id(self)) return "<MemoryView of %r at 0x%x>" % (self.base.__class__.__name__, id(self))
def __str__(self): def __str__(self):
return "<MemoryView of %r object>" % (self.object.__class__.__name__,) return "<MemoryView of %r object>" % (self.base.__class__.__name__,)
@cname('__pyx_memoryview_new') @cname('__pyx_memoryview_new')
...@@ -703,8 +773,8 @@ cdef class _memoryviewslice(memoryview): ...@@ -703,8 +773,8 @@ cdef class _memoryviewslice(memoryview):
else: else:
memoryview.assign_item_from_object(self, itemp, value) memoryview.assign_item_from_object(self, itemp, value)
property object: property base:
@cname('__pyx_memoryviewslice__get__object') @cname('__pyx_memoryviewslice__get__base')
def __get__(self): def __get__(self):
return self.from_object return self.from_object
......
...@@ -154,6 +154,19 @@ These typed slices can be converted to Python objects (`cython.memoryview`), and ...@@ -154,6 +154,19 @@ These typed slices can be converted to Python objects (`cython.memoryview`), and
slicable and transposable in the same way that the slices are. They can also be converted back to typed slicable and transposable in the same way that the slices are. They can also be converted back to typed
slices at any time. slices at any time.
They have the following attributes:
* shape
* strides
* suboffsets
* ndim
* size
* itemsize
* nbytes
And of course the aforementioned ``T`` attribute. These attributes have the same semantics as in NumPy_.
Cython Array Cython Array
============ ============
Whenever a slice is copied (using any of the `copy` or `copy_fortran` methods), you get a new Whenever a slice is copied (using any of the `copy` or `copy_fortran` methods), you get a new
...@@ -186,21 +199,26 @@ You can also cast pointers to arrays:: ...@@ -186,21 +199,26 @@ You can also cast pointers to arrays::
Again, when the array will go out of scope, it will free the data, unless you register a callback like above. Again, when the array will go out of scope, it will free the data, unless you register a callback like above.
Of course, you can also immidiately assign a cython.array to a typed memoryview slice. Of course, you can also immidiately assign a cython.array to a typed memoryview slice.
The arrays are indexable and slicable from Python space just like memoryview objects. If you need to do this The arrays are indexable and slicable from Python space just like memoryview objects, and have the same
a lot, you're better off creating a memoryview object from your array:: attributes as memoryview objects.
Coercion to NumPy
=================
Memoryview (and array) objects can be coerced to a NumPy ndarray, without having to copy the data. You can
e.g. do::
memview = cython.memoryview(my_cython_array, PyBUF_C_CONTIGUOUS) cimport numpy as np
import numpy as np
# OR numpy_array = np.asarray(<np.int32_t[:10, :10]> my_pointer)
cdef int[:, ::1] myslice = my_cython_array Of course, you are not restricted to using NumPy's type (such as ``np.int32_t`` here), you can use any usable type.
memview = myslice
The future The future
========== ==========
In the future some functionality may be added for convenience, like In the future some functionality may be added for convenience, like
1. A numpy-like `.flat` attribute (that allows efficient iteration) 1. A numpy-like `.flat` attribute (that allows efficient iteration)
2. A numpy-like `.reshape` method 2. Indexing with newaxis or None to introduce a new axis
3. A method `to_numpy` which would convert a memoryview object to a NumPy object
4. Indexing with newaxis or None to introduce a new axis .. _NumPy: http://docs.scipy.org/doc/numpy/reference/arrays.ndarray.html#memory-layout
...@@ -144,3 +144,9 @@ def test_array_from_pointer(): ...@@ -144,3 +144,9 @@ def test_array_from_pointer():
cdef int[:, ::1] mslice = <int[:10, :10]> getp() cdef int[:, ::1] mslice = <int[:10, :10]> getp()
print mslice[5, 6] print mslice[5, 6]
print (<int[:12, :10]> getp(12, 10))[5, 6] print (<int[:12, :10]> getp(12, 10))[5, 6]
# There is a reference cycle between the array object to its memoryview
# object that it keeps
del c_arr
import gc
gc.collect()
...@@ -331,24 +331,10 @@ cdef class LongComplexMockBuffer(MockBuffer): ...@@ -331,24 +331,10 @@ cdef class LongComplexMockBuffer(MockBuffer):
cdef get_default_format(self): return b"Zg" cdef get_default_format(self): return b"Zg"
def print_offsets(*args, size=0, newline=True): def print_offsets(*args, size, newline=True):
""" sys.stdout.write(' '.join([str(item // size) for item in args]))
print with a trailing comma does not have the same semantics in python 3. if newline:
Use sys.stdout.write instead. sys.stdout.write('\n')
# python 2
->> print 'foo',; sys.stdout.write('bar\n') # no space between foo and bar
foobar
In python 3 you get trailing spaces from the last ','
"""
for idx, item in enumerate(args):
if idx == len(args) - 1:
sys.stdout.write(str(item // size))
else:
sys.stdout.write('%s ' % (item // size))
if newline: sys.stdout.write('\n')
def print_int_offsets(*args, newline=True): def print_int_offsets(*args, newline=True):
print_offsets(*args, size=sizeof(int), newline=newline) print_offsets(*args, size=sizeof(int), newline=newline)
......
...@@ -8,6 +8,8 @@ Test slicing for memoryviews and memoryviewslices ...@@ -8,6 +8,8 @@ Test slicing for memoryviews and memoryviewslices
cimport numpy as np cimport numpy as np
import numpy as np import numpy as np
include "cythonarrayutil.pxi"
ctypedef np.int32_t dtype_t ctypedef np.int32_t dtype_t
def get_array(): def get_array():
...@@ -163,3 +165,36 @@ def test_transpose(): ...@@ -163,3 +165,36 @@ def test_transpose():
print a[3, 2], a.T[2, 3], a_obj[3, 2], a_obj.T[2, 3], numpy_obj[3, 2], numpy_obj.T[2, 3] print a[3, 2], a.T[2, 3], a_obj[3, 2], a_obj.T[2, 3], numpy_obj[3, 2], numpy_obj.T[2, 3]
def test_coerce_to_numpy():
"""
>>> test_coerce_to_numpy()
34
34
2
"""
cyarray = create_array(shape=(8, 5), mode="c", use_callback=True)
numarray = np.asarray(cyarray)
print cyarray[6, 4]
del cyarray
print numarray[6, 4]
numarray[6, 4] = 2
print numarray[6, 4]
def test_numpy_like_attributes(cyarray):
"""
>>> cyarray = create_array(shape=(8, 5), mode="c", use_callback=True)
>>> test_numpy_like_attributes(cyarray)
>>> test_numpy_like_attributes(cyarray.memview)
"""
numarray = np.asarray(cyarray)
assert cyarray.shape == numarray.shape
assert cyarray.strides == numarray.strides
assert cyarray.ndim == numarray.ndim
assert cyarray.size == numarray.size
assert cyarray.nbytes == numarray.nbytes
cdef int[:, :] mslice = numarray
assert (<object> mslice).base is numarray
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment