Commit 31084ba5 authored by Antoine Pitrou's avatar Antoine Pitrou

Issue #23632: Memoryviews now allow tuple indexing (including for multi-dimensional memoryviews).

parent 9eb57c5f
...@@ -3282,10 +3282,8 @@ copying. ...@@ -3282,10 +3282,8 @@ copying.
the view. The :class:`~memoryview.itemsize` attribute will give you the the view. The :class:`~memoryview.itemsize` attribute will give you the
number of bytes in a single element. number of bytes in a single element.
A :class:`memoryview` supports slicing to expose its data. If A :class:`memoryview` supports slicing and indexing to expose its data.
:class:`~memoryview.format` is one of the native format specifiers One-dimensional slicing will result in a subview::
from the :mod:`struct` module, indexing will return a single element
with the correct type. Full slicing will result in a subview::
>>> v = memoryview(b'abcefg') >>> v = memoryview(b'abcefg')
>>> v[1] >>> v[1]
...@@ -3297,25 +3295,29 @@ copying. ...@@ -3297,25 +3295,29 @@ copying.
>>> bytes(v[1:4]) >>> bytes(v[1:4])
b'bce' b'bce'
Other native formats:: If :class:`~memoryview.format` is one of the native format specifiers
from the :mod:`struct` module, indexing with an integer or a tuple of
integers is also supported and returns a single *element* with
the correct type. One-dimensional memoryviews can be indexed
with an integer or a one-integer tuple. Multi-dimensional memoryviews
can be indexed with tuples of exactly *ndim* integers where *ndim* is
the number of dimensions. Zero-dimensional memoryviews can be indexed
with the empty tuple.
Here is an example with a non-byte format::
>>> import array >>> import array
>>> a = array.array('l', [-11111111, 22222222, -33333333, 44444444]) >>> a = array.array('l', [-11111111, 22222222, -33333333, 44444444])
>>> a[0] >>> m = memoryview(a)
>>> m[0]
-11111111 -11111111
>>> a[-1] >>> m[-1]
44444444 44444444
>>> a[2:3].tolist() >>> m[::2].tolist()
[-33333333]
>>> a[::2].tolist()
[-11111111, -33333333] [-11111111, -33333333]
>>> a[::-1].tolist()
[44444444, -33333333, 22222222, -11111111]
.. versionadded:: 3.3
If the underlying object is writable, the memoryview supports slice If the underlying object is writable, the memoryview supports
assignment. Resizing is not allowed:: one-dimensional slice assignment. Resizing is not allowed::
>>> data = bytearray(b'abcefg') >>> data = bytearray(b'abcefg')
>>> v = memoryview(data) >>> v = memoryview(data)
...@@ -3348,12 +3350,16 @@ copying. ...@@ -3348,12 +3350,16 @@ copying.
True True
.. versionchanged:: 3.3 .. versionchanged:: 3.3
One-dimensional memoryviews can now be sliced.
One-dimensional memoryviews with formats 'B', 'b' or 'c' are now hashable. One-dimensional memoryviews with formats 'B', 'b' or 'c' are now hashable.
.. versionchanged:: 3.4 .. versionchanged:: 3.4
memoryview is now registered automatically with memoryview is now registered automatically with
:class:`collections.abc.Sequence` :class:`collections.abc.Sequence`
.. versionchanged:: 3.5
memoryviews can now be indexed with tuple of integers.
:class:`memoryview` has several methods: :class:`memoryview` has several methods:
.. method:: __eq__(exporter) .. method:: __eq__(exporter)
......
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
# memoryview tests is now in this module. # memoryview tests is now in this module.
# #
import contextlib
import unittest import unittest
from test import support from test import support
from itertools import permutations, product from itertools import permutations, product
...@@ -2825,6 +2826,13 @@ class TestBufferProtocol(unittest.TestCase): ...@@ -2825,6 +2826,13 @@ class TestBufferProtocol(unittest.TestCase):
m = memoryview(ex) m = memoryview(ex)
self.assertRaises(TypeError, eval, "9.0 in m", locals()) self.assertRaises(TypeError, eval, "9.0 in m", locals())
@contextlib.contextmanager
def assert_out_of_bounds_error(self, dim):
with self.assertRaises(IndexError) as cm:
yield
self.assertEqual(str(cm.exception),
"index out of bounds on dimension %d" % (dim,))
def test_memoryview_index(self): def test_memoryview_index(self):
# ndim = 0 # ndim = 0
...@@ -2851,12 +2859,31 @@ class TestBufferProtocol(unittest.TestCase): ...@@ -2851,12 +2859,31 @@ class TestBufferProtocol(unittest.TestCase):
self.assertRaises(IndexError, m.__getitem__, -8) self.assertRaises(IndexError, m.__getitem__, -8)
self.assertRaises(IndexError, m.__getitem__, 8) self.assertRaises(IndexError, m.__getitem__, 8)
# Not implemented: multidimensional sub-views # multi-dimensional
ex = ndarray(list(range(12)), shape=[3,4], flags=ND_WRITABLE) ex = ndarray(list(range(12)), shape=[3,4], flags=ND_WRITABLE)
m = memoryview(ex) m = memoryview(ex)
self.assertRaises(NotImplementedError, m.__getitem__, 0) self.assertEqual(m[0, 0], 0)
self.assertRaises(NotImplementedError, m.__setitem__, 0, 9) self.assertEqual(m[2, 0], 8)
self.assertEqual(m[2, 3], 11)
self.assertEqual(m[-1, -1], 11)
self.assertEqual(m[-3, -4], 0)
# out of bounds
for index in (3, -4):
with self.assert_out_of_bounds_error(dim=1):
m[index, 0]
for index in (4, -5):
with self.assert_out_of_bounds_error(dim=2):
m[0, index]
self.assertRaises(IndexError, m.__getitem__, (2**64, 0))
self.assertRaises(IndexError, m.__getitem__, (0, 2**64))
self.assertRaises(TypeError, m.__getitem__, (0, 0, 0))
self.assertRaises(TypeError, m.__getitem__, (0.0, 0.0))
# Not implemented: multidimensional sub-views
self.assertRaises(NotImplementedError, m.__getitem__, ())
self.assertRaises(NotImplementedError, m.__getitem__, 0) self.assertRaises(NotImplementedError, m.__getitem__, 0)
def test_memoryview_assign(self): def test_memoryview_assign(self):
...@@ -2945,10 +2972,27 @@ class TestBufferProtocol(unittest.TestCase): ...@@ -2945,10 +2972,27 @@ class TestBufferProtocol(unittest.TestCase):
m = memoryview(ex) m = memoryview(ex)
self.assertRaises(NotImplementedError, m.__setitem__, 0, 1) self.assertRaises(NotImplementedError, m.__setitem__, 0, 1)
# Not implemented: multidimensional sub-views # multi-dimensional
ex = ndarray(list(range(12)), shape=[3,4], flags=ND_WRITABLE) ex = ndarray(list(range(12)), shape=[3,4], flags=ND_WRITABLE)
m = memoryview(ex) m = memoryview(ex)
m[0,1] = 42
self.assertEqual(ex[0][1], 42)
m[-1,-1] = 43
self.assertEqual(ex[2][3], 43)
# errors
for index in (3, -4):
with self.assert_out_of_bounds_error(dim=1):
m[index, 0] = 0
for index in (4, -5):
with self.assert_out_of_bounds_error(dim=2):
m[0, index] = 0
self.assertRaises(IndexError, m.__setitem__, (2**64, 0), 0)
self.assertRaises(IndexError, m.__setitem__, (0, 2**64), 0)
self.assertRaises(TypeError, m.__setitem__, (0, 0, 0), 0)
self.assertRaises(TypeError, m.__setitem__, (0.0, 0.0), 0)
# Not implemented: multidimensional sub-views
self.assertRaises(NotImplementedError, m.__setitem__, 0, [2, 3]) self.assertRaises(NotImplementedError, m.__setitem__, 0, [2, 3])
def test_memoryview_slice(self): def test_memoryview_slice(self):
...@@ -2961,8 +3005,8 @@ class TestBufferProtocol(unittest.TestCase): ...@@ -2961,8 +3005,8 @@ class TestBufferProtocol(unittest.TestCase):
self.assertRaises(ValueError, m.__setitem__, slice(0,2,0), self.assertRaises(ValueError, m.__setitem__, slice(0,2,0),
bytearray([1,2])) bytearray([1,2]))
# invalid slice key # 0-dim slicing (identity function)
self.assertRaises(TypeError, m.__getitem__, ()) self.assertRaises(NotImplementedError, m.__getitem__, ())
# multidimensional slices # multidimensional slices
ex = ndarray(list(range(12)), shape=[12], flags=ND_WRITABLE) ex = ndarray(list(range(12)), shape=[12], flags=ND_WRITABLE)
......
...@@ -10,6 +10,9 @@ Release date: 2015-03-28 ...@@ -10,6 +10,9 @@ Release date: 2015-03-28
Core and Builtins Core and Builtins
----------------- -----------------
- Issue #23632: Memoryviews now allow tuple indexing (including for
multi-dimensional memoryviews).
- Issue #23192: Fixed generator lambdas. Patch by Bruno Cauet. - Issue #23192: Fixed generator lambdas. Patch by Bruno Cauet.
- Issue #23629: Fix the default __sizeof__ implementation for variable-sized - Issue #23629: Fix the default __sizeof__ implementation for variable-sized
......
...@@ -192,10 +192,10 @@ PyTypeObject _PyManagedBuffer_Type = { ...@@ -192,10 +192,10 @@ PyTypeObject _PyManagedBuffer_Type = {
#define VIEW_ADDR(mv) (&((PyMemoryViewObject *)mv)->view) #define VIEW_ADDR(mv) (&((PyMemoryViewObject *)mv)->view)
/* Check for the presence of suboffsets in the first dimension. */ /* Check for the presence of suboffsets in the first dimension. */
#define HAVE_PTR(suboffsets) (suboffsets && suboffsets[0] >= 0) #define HAVE_PTR(suboffsets, dim) (suboffsets && suboffsets[dim] >= 0)
/* Adjust ptr if suboffsets are present. */ /* Adjust ptr if suboffsets are present. */
#define ADJUST_PTR(ptr, suboffsets) \ #define ADJUST_PTR(ptr, suboffsets, dim) \
(HAVE_PTR(suboffsets) ? *((char**)ptr) + suboffsets[0] : ptr) (HAVE_PTR(suboffsets, dim) ? *((char**)ptr) + suboffsets[dim] : ptr)
/* Memoryview buffer properties */ /* Memoryview buffer properties */
#define MV_C_CONTIGUOUS(flags) (flags&(_Py_MEMORYVIEW_SCALAR|_Py_MEMORYVIEW_C)) #define MV_C_CONTIGUOUS(flags) (flags&(_Py_MEMORYVIEW_SCALAR|_Py_MEMORYVIEW_C))
...@@ -332,11 +332,11 @@ copy_base(const Py_ssize_t *shape, Py_ssize_t itemsize, ...@@ -332,11 +332,11 @@ copy_base(const Py_ssize_t *shape, Py_ssize_t itemsize,
char *p; char *p;
Py_ssize_t i; Py_ssize_t i;
for (i=0, p=mem; i < shape[0]; p+=itemsize, sptr+=sstrides[0], i++) { for (i=0, p=mem; i < shape[0]; p+=itemsize, sptr+=sstrides[0], i++) {
char *xsptr = ADJUST_PTR(sptr, ssuboffsets); char *xsptr = ADJUST_PTR(sptr, ssuboffsets, 0);
memcpy(p, xsptr, itemsize); memcpy(p, xsptr, itemsize);
} }
for (i=0, p=mem; i < shape[0]; p+=itemsize, dptr+=dstrides[0], i++) { for (i=0, p=mem; i < shape[0]; p+=itemsize, dptr+=dstrides[0], i++) {
char *xdptr = ADJUST_PTR(dptr, dsuboffsets); char *xdptr = ADJUST_PTR(dptr, dsuboffsets, 0);
memcpy(xdptr, p, itemsize); memcpy(xdptr, p, itemsize);
} }
} }
...@@ -364,8 +364,8 @@ copy_rec(const Py_ssize_t *shape, Py_ssize_t ndim, Py_ssize_t itemsize, ...@@ -364,8 +364,8 @@ copy_rec(const Py_ssize_t *shape, Py_ssize_t ndim, Py_ssize_t itemsize,
} }
for (i = 0; i < shape[0]; dptr+=dstrides[0], sptr+=sstrides[0], i++) { for (i = 0; i < shape[0]; dptr+=dstrides[0], sptr+=sstrides[0], i++) {
char *xdptr = ADJUST_PTR(dptr, dsuboffsets); char *xdptr = ADJUST_PTR(dptr, dsuboffsets, 0);
char *xsptr = ADJUST_PTR(sptr, ssuboffsets); char *xsptr = ADJUST_PTR(sptr, ssuboffsets, 0);
copy_rec(shape+1, ndim-1, itemsize, copy_rec(shape+1, ndim-1, itemsize,
xdptr, dstrides+1, dsuboffsets ? dsuboffsets+1 : NULL, xdptr, dstrides+1, dsuboffsets ? dsuboffsets+1 : NULL,
...@@ -2057,7 +2057,7 @@ tolist_base(const char *ptr, const Py_ssize_t *shape, ...@@ -2057,7 +2057,7 @@ tolist_base(const char *ptr, const Py_ssize_t *shape,
return NULL; return NULL;
for (i = 0; i < shape[0]; ptr+=strides[0], i++) { for (i = 0; i < shape[0]; ptr+=strides[0], i++) {
const char *xptr = ADJUST_PTR(ptr, suboffsets); const char *xptr = ADJUST_PTR(ptr, suboffsets, 0);
item = unpack_single(xptr, fmt); item = unpack_single(xptr, fmt);
if (item == NULL) { if (item == NULL) {
Py_DECREF(lst); Py_DECREF(lst);
...@@ -2091,7 +2091,7 @@ tolist_rec(const char *ptr, Py_ssize_t ndim, const Py_ssize_t *shape, ...@@ -2091,7 +2091,7 @@ tolist_rec(const char *ptr, Py_ssize_t ndim, const Py_ssize_t *shape,
return NULL; return NULL;
for (i = 0; i < shape[0]; ptr+=strides[0], i++) { for (i = 0; i < shape[0]; ptr+=strides[0], i++) {
const char *xptr = ADJUST_PTR(ptr, suboffsets); const char *xptr = ADJUST_PTR(ptr, suboffsets, 0);
item = tolist_rec(xptr, ndim-1, shape+1, item = tolist_rec(xptr, ndim-1, shape+1,
strides+1, suboffsets ? suboffsets+1 : NULL, strides+1, suboffsets ? suboffsets+1 : NULL,
fmt); fmt);
...@@ -2171,33 +2171,66 @@ memory_repr(PyMemoryViewObject *self) ...@@ -2171,33 +2171,66 @@ memory_repr(PyMemoryViewObject *self)
/* Indexing and slicing */ /* Indexing and slicing */
/**************************************************************************/ /**************************************************************************/
/* Get the pointer to the item at index. */
static char * static char *
ptr_from_index(Py_buffer *view, Py_ssize_t index) lookup_dimension(Py_buffer *view, char *ptr, int dim, Py_ssize_t index)
{ {
char *ptr; Py_ssize_t nitems; /* items in the given dimension */
Py_ssize_t nitems; /* items in the first dimension */
assert(view->shape); assert(view->shape);
assert(view->strides); assert(view->strides);
nitems = view->shape[0]; nitems = view->shape[dim];
if (index < 0) { if (index < 0) {
index += nitems; index += nitems;
} }
if (index < 0 || index >= nitems) { if (index < 0 || index >= nitems) {
PyErr_SetString(PyExc_IndexError, "index out of bounds"); PyErr_Format(PyExc_IndexError,
"index out of bounds on dimension %d", dim + 1);
return NULL; return NULL;
} }
ptr = (char *)view->buf; ptr += view->strides[dim] * index;
ptr += view->strides[0] * index;
ptr = ADJUST_PTR(ptr, view->suboffsets); ptr = ADJUST_PTR(ptr, view->suboffsets, dim);
return ptr; return ptr;
} }
/* Get the pointer to the item at index. */
static char *
ptr_from_index(Py_buffer *view, Py_ssize_t index)
{
char *ptr = (char *)view->buf;
return lookup_dimension(view, ptr, 0, index);
}
/* Get the pointer to the item at tuple. */
static char *
ptr_from_tuple(Py_buffer *view, PyObject *tup)
{
char *ptr = (char *)view->buf;
Py_ssize_t dim, nindices = PyTuple_GET_SIZE(tup);
if (nindices > view->ndim) {
PyErr_Format(PyExc_TypeError,
"cannot index %zd-dimension view with %zd-element tuple",
view->ndim, nindices);
return NULL;
}
for (dim = 0; dim < nindices; dim++) {
Py_ssize_t index;
index = PyNumber_AsSsize_t(PyTuple_GET_ITEM(tup, dim),
PyExc_IndexError);
if (index == -1 && PyErr_Occurred())
return NULL;
ptr = lookup_dimension(view, ptr, dim, index);
if (ptr == NULL)
return NULL;
}
return ptr;
}
/* Return the item at index. In a one-dimensional view, this is an object /* Return the item at index. In a one-dimensional view, this is an object
with the type specified by view->format. Otherwise, the item is a sub-view. with the type specified by view->format. Otherwise, the item is a sub-view.
The function is used in memory_subscript() and memory_as_sequence. */ The function is used in memory_subscript() and memory_as_sequence. */
...@@ -2229,6 +2262,32 @@ memory_item(PyMemoryViewObject *self, Py_ssize_t index) ...@@ -2229,6 +2262,32 @@ memory_item(PyMemoryViewObject *self, Py_ssize_t index)
return NULL; return NULL;
} }
/* Return the item at position *key* (a tuple of indices). */
static PyObject *
memory_item_multi(PyMemoryViewObject *self, PyObject *tup)
{
Py_buffer *view = &(self->view);
const char *fmt;
Py_ssize_t nindices = PyTuple_GET_SIZE(tup);
char *ptr;
CHECK_RELEASED(self);
fmt = adjust_fmt(view);
if (fmt == NULL)
return NULL;
if (nindices < view->ndim) {
PyErr_SetString(PyExc_NotImplementedError,
"sub-views are not implemented");
return NULL;
}
ptr = ptr_from_tuple(view, tup);
if (ptr == NULL)
return NULL;
return unpack_single(ptr, fmt);
}
Py_LOCAL_INLINE(int) Py_LOCAL_INLINE(int)
init_slice(Py_buffer *base, PyObject *key, int dim) init_slice(Py_buffer *base, PyObject *key, int dim)
{ {
...@@ -2277,6 +2336,22 @@ is_multislice(PyObject *key) ...@@ -2277,6 +2336,22 @@ is_multislice(PyObject *key)
return 1; return 1;
} }
static Py_ssize_t
is_multiindex(PyObject *key)
{
Py_ssize_t size, i;
if (!PyTuple_Check(key))
return 0;
size = PyTuple_GET_SIZE(key);
for (i = 0; i < size; i++) {
PyObject *x = PyTuple_GET_ITEM(key, i);
if (!PyIndex_Check(x))
return 0;
}
return 1;
}
/* mv[obj] returns an object holding the data for one element if obj /* mv[obj] returns an object holding the data for one element if obj
fully indexes the memoryview or another memoryview object if it fully indexes the memoryview or another memoryview object if it
does not. does not.
...@@ -2332,6 +2407,9 @@ memory_subscript(PyMemoryViewObject *self, PyObject *key) ...@@ -2332,6 +2407,9 @@ memory_subscript(PyMemoryViewObject *self, PyObject *key)
return (PyObject *)sliced; return (PyObject *)sliced;
} }
else if (is_multiindex(key)) {
return memory_item_multi(self, key);
}
else if (is_multislice(key)) { else if (is_multislice(key)) {
PyErr_SetString(PyExc_NotImplementedError, PyErr_SetString(PyExc_NotImplementedError,
"multi-dimensional slicing is not implemented"); "multi-dimensional slicing is not implemented");
...@@ -2376,14 +2454,15 @@ memory_ass_sub(PyMemoryViewObject *self, PyObject *key, PyObject *value) ...@@ -2376,14 +2454,15 @@ memory_ass_sub(PyMemoryViewObject *self, PyObject *key, PyObject *value)
return -1; return -1;
} }
} }
if (view->ndim != 1) {
PyErr_SetString(PyExc_NotImplementedError,
"memoryview assignments are currently restricted to ndim = 1");
return -1;
}
if (PyIndex_Check(key)) { if (PyIndex_Check(key)) {
Py_ssize_t index = PyNumber_AsSsize_t(key, PyExc_IndexError); Py_ssize_t index;
if (1 < view->ndim) {
PyErr_SetString(PyExc_NotImplementedError,
"sub-views are not implemented");
return -1;
}
index = PyNumber_AsSsize_t(key, PyExc_IndexError);
if (index == -1 && PyErr_Occurred()) if (index == -1 && PyErr_Occurred())
return -1; return -1;
ptr = ptr_from_index(view, index); ptr = ptr_from_index(view, index);
...@@ -2418,7 +2497,19 @@ memory_ass_sub(PyMemoryViewObject *self, PyObject *key, PyObject *value) ...@@ -2418,7 +2497,19 @@ memory_ass_sub(PyMemoryViewObject *self, PyObject *key, PyObject *value)
PyBuffer_Release(&src); PyBuffer_Release(&src);
return ret; return ret;
} }
else if (PySlice_Check(key) || is_multislice(key)) { if (is_multiindex(key)) {
char *ptr;
if (PyTuple_GET_SIZE(key) < view->ndim) {
PyErr_SetString(PyExc_NotImplementedError,
"sub-views are not implemented");
return -1;
}
ptr = ptr_from_tuple(view, key);
if (ptr == NULL)
return -1;
return pack_single(ptr, value, fmt);
}
if (PySlice_Check(key) || is_multislice(key)) {
/* Call memory_subscript() to produce a sliced lvalue, then copy /* Call memory_subscript() to produce a sliced lvalue, then copy
rvalue into lvalue. This is already implemented in _testbuffer.c. */ rvalue into lvalue. This is already implemented in _testbuffer.c. */
PyErr_SetString(PyExc_NotImplementedError, PyErr_SetString(PyExc_NotImplementedError,
...@@ -2591,8 +2682,8 @@ cmp_base(const char *p, const char *q, const Py_ssize_t *shape, ...@@ -2591,8 +2682,8 @@ cmp_base(const char *p, const char *q, const Py_ssize_t *shape,
int equal; int equal;
for (i = 0; i < shape[0]; p+=pstrides[0], q+=qstrides[0], i++) { for (i = 0; i < shape[0]; p+=pstrides[0], q+=qstrides[0], i++) {
const char *xp = ADJUST_PTR(p, psuboffsets); const char *xp = ADJUST_PTR(p, psuboffsets, 0);
const char *xq = ADJUST_PTR(q, qsuboffsets); const char *xq = ADJUST_PTR(q, qsuboffsets, 0);
equal = unpack_cmp(xp, xq, fmt, unpack_p, unpack_q); equal = unpack_cmp(xp, xq, fmt, unpack_p, unpack_q);
if (equal <= 0) if (equal <= 0)
return equal; return equal;
...@@ -2626,8 +2717,8 @@ cmp_rec(const char *p, const char *q, ...@@ -2626,8 +2717,8 @@ cmp_rec(const char *p, const char *q,
} }
for (i = 0; i < shape[0]; p+=pstrides[0], q+=qstrides[0], i++) { for (i = 0; i < shape[0]; p+=pstrides[0], q+=qstrides[0], i++) {
const char *xp = ADJUST_PTR(p, psuboffsets); const char *xp = ADJUST_PTR(p, psuboffsets, 0);
const char *xq = ADJUST_PTR(q, qsuboffsets); const char *xq = ADJUST_PTR(q, qsuboffsets, 0);
equal = cmp_rec(xp, xq, ndim-1, shape+1, equal = cmp_rec(xp, xq, ndim-1, shape+1,
pstrides+1, psuboffsets ? psuboffsets+1 : NULL, pstrides+1, psuboffsets ? psuboffsets+1 : NULL,
qstrides+1, qsuboffsets ? qsuboffsets+1 : NULL, qstrides+1, qsuboffsets ? qsuboffsets+1 : NULL,
......
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