From 1a009e0d061df490fa91df8684768ed3217704db Mon Sep 17 00:00:00 2001 From: "Joseph R. Fox-Rabinovitz" Date: Mon, 3 Jan 2022 23:27:04 -0600 Subject: ENH: Support for changing dtype in non-C-contiguous views Expires deprecated F-contiguous behavior. Simplifies C code of dtype set descriptor. Adds tests that verify which error condition is triggered. Introduces extra long exception message that upsets linter. --- numpy/core/_add_newdocs.py | 42 +++++++++++++++-------- numpy/core/src/multiarray/getset.c | 34 ++++++------------- numpy/core/tests/test_deprecations.py | 14 -------- numpy/core/tests/test_multiarray.py | 63 +++++++++++++++++++++++++++++++++++ numpy/lib/stride_tricks.py | 1 + 5 files changed, 102 insertions(+), 52 deletions(-) (limited to 'numpy') diff --git a/numpy/core/_add_newdocs.py b/numpy/core/_add_newdocs.py index 759801ccc..523da75b3 100644 --- a/numpy/core/_add_newdocs.py +++ b/numpy/core/_add_newdocs.py @@ -4450,14 +4450,13 @@ add_newdoc('numpy.core.multiarray', 'ndarray', ('view', memory. For ``a.view(some_dtype)``, if ``some_dtype`` has a different number of - bytes per entry than the previous dtype (for example, converting a - regular array to a structured array), then the behavior of the view - cannot be predicted just from the superficial appearance of ``a`` (shown - by ``print(a)``). It also depends on exactly how ``a`` is stored in - memory. Therefore if ``a`` is C-ordered versus fortran-ordered, versus - defined as a slice or transpose, etc., the view may give different - results. + bytes per entry than the previous dtype (for example, converting a regular + array to a structured array), then the last axis of ``a`` must be + contiguous. This axis will be resized in the result. + .. versionchanged:: 1.23.0 + Only the last axis needs to be contiguous. Previously, the entire array + had to be C-contiguous. Examples -------- @@ -4502,19 +4501,34 @@ add_newdoc('numpy.core.multiarray', 'ndarray', ('view', Views that change the dtype size (bytes per entry) should normally be avoided on arrays defined by slices, transposes, fortran-ordering, etc.: - >>> x = np.array([[1,2,3],[4,5,6]], dtype=np.int16) - >>> y = x[:, 0:2] + >>> x = np.array([[1, 2, 3], [4, 5, 6]], dtype=np.int16) + >>> y = x[:, ::2] >>> y - array([[1, 2], - [4, 5]], dtype=int16) + array([[1, 3], + [4, 6]], dtype=int16) >>> y.view(dtype=[('width', np.int16), ('length', np.int16)]) Traceback (most recent call last): ... - ValueError: To change to a dtype of a different size, the array must be C-contiguous + ValueError: To change to a dtype of a different size, the last axis must be contiguous >>> z = y.copy() >>> z.view(dtype=[('width', np.int16), ('length', np.int16)]) - array([[(1, 2)], - [(4, 5)]], dtype=[('width', '>> x = np.arange(2 * 3 * 4, dtype=np.int8).reshape(2, 3, 4) + >>> x.transpose(1, 0, 2).view(np.int16) + array([[[ 256, 770], + [3340, 3854]], + + [[1284, 1798], + [4368, 4882]], + + [[2312, 2826], + [5396, 5910]]], dtype=int16) + """)) diff --git a/numpy/core/src/multiarray/getset.c b/numpy/core/src/multiarray/getset.c index ce21e948e..ac6465acd 100644 --- a/numpy/core/src/multiarray/getset.c +++ b/numpy/core/src/multiarray/getset.c @@ -496,9 +496,6 @@ array_descr_set(PyArrayObject *self, PyObject *arg, void *NPY_UNUSED(ignored)) /* Changing the size of the dtype results in a shape change */ if (newtype->elsize != PyArray_DESCR(self)->elsize) { - int axis; - npy_intp newdim; - /* forbidden cases */ if (PyArray_NDIM(self) == 0) { PyErr_SetString(PyExc_ValueError, @@ -513,31 +510,20 @@ array_descr_set(PyArrayObject *self, PyObject *arg, void *NPY_UNUSED(ignored)) goto fail; } - /* determine which axis to resize */ - if (PyArray_IS_C_CONTIGUOUS(self)) { - axis = PyArray_NDIM(self) - 1; - } - else if (PyArray_IS_F_CONTIGUOUS(self)) { - /* 2015-11-27 1.11.0, gh-6747 */ - if (DEPRECATE( - "Changing the shape of an F-contiguous array by " - "descriptor assignment is deprecated. To maintain the " - "Fortran contiguity of a multidimensional Fortran " - "array, use 'a.T.view(...).T' instead") < 0) { - goto fail; - } - axis = 0; - } - else { - /* Don't mention the deprecated F-contiguous support */ + /* resize on last axis only */ + int axis = PyArray_NDIM(self) - 1; + if (PyArray_DIMS(self)[axis] != 1 && + PyArray_STRIDES(self)[axis] != PyArray_DESCR(self)->elsize) { PyErr_SetString(PyExc_ValueError, - "To change to a dtype of a different size, the array must " - "be C-contiguous"); + "To change to a dtype of a different size, the last axis " + "must be contiguous"); goto fail; } + npy_intp newdim; + if (newtype->elsize < PyArray_DESCR(self)->elsize) { - /* if it is compatible, increase the size of the relevant axis */ + /* if it is compatible, increase the size of the last axis */ if (newtype->elsize == 0 || PyArray_DESCR(self)->elsize % newtype->elsize != 0) { PyErr_SetString(PyExc_ValueError, @@ -549,7 +535,7 @@ array_descr_set(PyArrayObject *self, PyObject *arg, void *NPY_UNUSED(ignored)) PyArray_DIMS(self)[axis] *= newdim; PyArray_STRIDES(self)[axis] = newtype->elsize; } - else if (newtype->elsize > PyArray_DESCR(self)->elsize) { + else /* newtype->elsize > PyArray_DESCR(self)->elsize */ { /* if it is compatible, decrease the size of the relevant axis */ newdim = PyArray_DIMS(self)[axis] * PyArray_DESCR(self)->elsize; if ((newdim % newtype->elsize) != 0) { diff --git a/numpy/core/tests/test_deprecations.py b/numpy/core/tests/test_deprecations.py index d148c89f5..76486f755 100644 --- a/numpy/core/tests/test_deprecations.py +++ b/numpy/core/tests/test_deprecations.py @@ -257,20 +257,6 @@ class TestDatetime64Timezone(_DeprecationTestCase): self.assert_deprecated(np.datetime64, args=(dt,)) -class TestNonCContiguousViewDeprecation(_DeprecationTestCase): - """View of non-C-contiguous arrays deprecated in 1.11.0. - - The deprecation will not be raised for arrays that are both C and F - contiguous, as C contiguous is dominant. There are more such arrays - with relaxed stride checking than without so the deprecation is not - as visible with relaxed stride checking in force. - """ - - def test_fortran_contiguous(self): - self.assert_deprecated(np.ones((2,2)).T.view, args=(complex,)) - self.assert_deprecated(np.ones((2,2)).T.view, args=(np.int8,)) - - class TestArrayDataAttributeAssignmentDeprecation(_DeprecationTestCase): """Assigning the 'data' attribute of an ndarray is unsafe as pointed out in gh-7093. Eventually, such assignment should NOT be allowed, but diff --git a/numpy/core/tests/test_multiarray.py b/numpy/core/tests/test_multiarray.py index 0c611abb5..2529705d5 100644 --- a/numpy/core/tests/test_multiarray.py +++ b/numpy/core/tests/test_multiarray.py @@ -9191,3 +9191,66 @@ def test_getfield(): pytest.raises(ValueError, a.getfield, 'uint8', -1) pytest.raises(ValueError, a.getfield, 'uint8', 16) pytest.raises(ValueError, a.getfield, 'uint64', 0) + + +class TestViewDtype: + """ + Verify that making a view of a non-contiguous array works as expected. + """ + def test_smaller_dtype_multiple(self): + # x is non-contiguous + x = np.arange(10, dtype='