diff options
-rw-r--r-- | doc/release/upcoming_changes/16232.deprecation.rst | 6 | ||||
-rw-r--r-- | numpy/core/src/umath/ufunc_object.c | 72 | ||||
-rw-r--r-- | numpy/core/tests/test_deprecations.py | 15 | ||||
-rw-r--r-- | numpy/core/tests/test_umath.py | 36 | ||||
-rw-r--r-- | numpy/matrixlib/tests/test_masked_matrix.py | 4 |
5 files changed, 109 insertions, 24 deletions
diff --git a/doc/release/upcoming_changes/16232.deprecation.rst b/doc/release/upcoming_changes/16232.deprecation.rst new file mode 100644 index 000000000..d1ac7f044 --- /dev/null +++ b/doc/release/upcoming_changes/16232.deprecation.rst @@ -0,0 +1,6 @@ +``outer`` and ``ufunc.outer`` deprecated for matrix +--------------------------------------------------- +``np.matrix`` use with `~numpy.outer` or generic ufunc outer +calls such as ``numpy.add.outer``. Previously, matrix was +converted to an array here. This will not be done in the future +requiring a manual conversion to arrays. diff --git a/numpy/core/src/umath/ufunc_object.c b/numpy/core/src/umath/ufunc_object.c index b35f377d7..8f841c6fa 100644 --- a/numpy/core/src/umath/ufunc_object.c +++ b/numpy/core/src/umath/ufunc_object.c @@ -5388,13 +5388,11 @@ ufunc_traverse(PyUFuncObject *self, visitproc visit, void *arg) static PyObject * ufunc_outer(PyUFuncObject *ufunc, PyObject *args, PyObject *kwds) { - int i; int errval; PyObject *override = NULL; PyObject *ret; PyArrayObject *ap1 = NULL, *ap2 = NULL, *ap_new = NULL; PyObject *new_args, *tmp; - PyObject *shape1, *shape2, *newshape; static PyObject *_numpy_matrix; @@ -5435,7 +5433,19 @@ ufunc_outer(PyUFuncObject *ufunc, PyObject *args, PyObject *kwds) "matrix", &_numpy_matrix); + const char *matrix_deprecation_msg = ( + "%s.outer() was passed a numpy matrix as %s argument. " + "Special handling of matrix is deprecated and will result in an " + "error in most cases. Please convert the matrix to a NumPy " + "array to retain the old behaviour. You can use `matrix.A` " + "to achieve this."); + if (PyObject_IsInstance(tmp, _numpy_matrix)) { + /* DEPRECATED 2020-05-13, NumPy 1.20 */ + if (PyErr_WarnFormat(PyExc_DeprecationWarning, 1, + matrix_deprecation_msg, ufunc->name, "first") < 0) { + return NULL; + } ap1 = (PyArrayObject *) PyArray_FromObject(tmp, NPY_NOTYPE, 0, 0); } else { @@ -5450,6 +5460,11 @@ ufunc_outer(PyUFuncObject *ufunc, PyObject *args, PyObject *kwds) return NULL; } if (PyObject_IsInstance(tmp, _numpy_matrix)) { + /* DEPRECATED 2020-05-13, NumPy 1.20 */ + if (PyErr_WarnFormat(PyExc_DeprecationWarning, 1, + matrix_deprecation_msg, ufunc->name, "second") < 0) { + return NULL; + } ap2 = (PyArrayObject *) PyArray_FromObject(tmp, NPY_NOTYPE, 0, 0); } else { @@ -5460,34 +5475,45 @@ ufunc_outer(PyUFuncObject *ufunc, PyObject *args, PyObject *kwds) Py_DECREF(ap1); return NULL; } - /* Construct new shape tuple */ - shape1 = PyTuple_New(PyArray_NDIM(ap1)); - if (shape1 == NULL) { - goto fail; - } - for (i = 0; i < PyArray_NDIM(ap1); i++) { - PyTuple_SET_ITEM(shape1, i, - PyLong_FromLongLong((npy_longlong)PyArray_DIMS(ap1)[i])); - } - shape2 = PyTuple_New(PyArray_NDIM(ap2)); - for (i = 0; i < PyArray_NDIM(ap2); i++) { - PyTuple_SET_ITEM(shape2, i, PyInt_FromLong((long) 1)); + /* Construct new shape from ap1 and ap2 and then reshape */ + PyArray_Dims newdims; + npy_intp newshape[NPY_MAXDIMS]; + newdims.len = PyArray_NDIM(ap1) + PyArray_NDIM(ap2); + newdims.ptr = newshape; + + if (newdims.len > NPY_MAXDIMS) { + PyErr_Format(PyExc_ValueError, + "maximum supported dimension for an ndarray is %d, but " + "`%s.outer()` result would have %d.", + NPY_MAXDIMS, ufunc->name, newdims.len); + return NPY_FAIL; } - if (shape2 == NULL) { - Py_DECREF(shape1); + if (newdims.ptr == NULL) { goto fail; } - newshape = PyNumber_Add(shape1, shape2); - Py_DECREF(shape1); - Py_DECREF(shape2); - if (newshape == NULL) { - goto fail; + memcpy(newshape, PyArray_DIMS(ap1), PyArray_NDIM(ap1) * sizeof(npy_intp)); + for (int i = PyArray_NDIM(ap1); i < newdims.len; i++) { + newshape[i] = 1; } - ap_new = (PyArrayObject *)PyArray_Reshape(ap1, newshape); - Py_DECREF(newshape); + + ap_new = (PyArrayObject *)PyArray_Newshape(ap1, &newdims, NPY_CORDER); if (ap_new == NULL) { goto fail; } + if (PyArray_NDIM(ap_new) != newdims.len || + !PyArray_CompareLists(PyArray_DIMS(ap_new), newshape, newdims.len)) { + PyErr_Format(PyExc_TypeError, + "%s.outer() called with ndarray-subclass of type '%s' " + "which modified its shape after a reshape. `outer()` relies " + "on reshaping the inputs and is for example not supported for " + "the 'np.matrix' class (the usage of matrix is generally " + "discouraged). " + "To work around this issue, please convert the inputs to " + "numpy arrays.", + ufunc->name, Py_TYPE(ap_new)->tp_name); + goto fail; + } + new_args = Py_BuildValue("(OO)", ap_new, ap2); Py_DECREF(ap1); Py_DECREF(ap2); diff --git a/numpy/core/tests/test_deprecations.py b/numpy/core/tests/test_deprecations.py index 68502adda..431c9bb49 100644 --- a/numpy/core/tests/test_deprecations.py +++ b/numpy/core/tests/test_deprecations.py @@ -678,3 +678,18 @@ class TestDeprecatedGlobals(_DeprecationTestCase): # from np.compat self.assert_deprecated(lambda: np.long) self.assert_deprecated(lambda: np.unicode) + + +class TestMatrixInOuter(_DeprecationTestCase): + # 2020-05-13 NumPy 1.20.0 + message = (r"add.outer\(\) was passed a numpy matrix as " + r"(first|second) argument.") + + def test_deprecated(self): + arr = np.array([1, 2, 3]) + m = np.array([1, 2, 3]).view(np.matrix) + self.assert_deprecated(np.add.outer, args=(m, m), num=2) + self.assert_deprecated(np.add.outer, args=(arr, m)) + self.assert_deprecated(np.add.outer, args=(m, arr)) + self.assert_not_deprecated(np.add.outer, args=(arr, arr)) + diff --git a/numpy/core/tests/test_umath.py b/numpy/core/tests/test_umath.py index b4a129502..ae72687ca 100644 --- a/numpy/core/tests/test_umath.py +++ b/numpy/core/tests/test_umath.py @@ -3283,3 +3283,39 @@ def test_outer_subclass_preserve(arr): class foo(np.ndarray): pass actual = np.multiply.outer(arr.view(foo), arr.view(foo)) assert actual.__class__.__name__ == 'foo' + +def test_outer_bad_subclass(): + class BadArr1(np.ndarray): + def __array_finalize__(self, obj): + # The outer call reshapes to 3 dims, try to do a bad reshape. + if self.ndim == 3: + self.shape = self.shape + (1,) + + def __array_prepare__(self, obj, context=None): + return obj + + class BadArr2(np.ndarray): + def __array_finalize__(self, obj): + if isinstance(obj, BadArr2): + # outer inserts 1-sized dims. In that case disturb them. + if self.shape[-1] == 1: + self.shape = self.shape[::-1] + + def __array_prepare__(self, obj, context=None): + return obj + + for cls in [BadArr1, BadArr2]: + arr = np.ones((2, 3)).view(cls) + with assert_raises(TypeError) as a: + # The first array gets reshaped (not the second one) + np.add.outer(arr, [1, 2]) + + # This actually works, since we only see the reshaping error: + arr = np.ones((2, 3)).view(cls) + assert type(np.add.outer([1, 2], arr)) is cls + +def test_outer_exceeds_maxdims(): + deep = np.ones((1,) * 17) + with assert_raises(ValueError): + np.add.outer(deep, deep) + diff --git a/numpy/matrixlib/tests/test_masked_matrix.py b/numpy/matrixlib/tests/test_masked_matrix.py index 45424ecf0..95d3f44b6 100644 --- a/numpy/matrixlib/tests/test_masked_matrix.py +++ b/numpy/matrixlib/tests/test_masked_matrix.py @@ -1,4 +1,5 @@ import numpy as np +from numpy.testing import assert_warns from numpy.ma.testutils import (assert_, assert_equal, assert_raises, assert_array_equal) from numpy.ma.core import (masked_array, masked_values, masked, allequal, @@ -198,7 +199,8 @@ class TestSubclassing: # Result should work assert_equal(add(mx, x), mx+x) assert_(isinstance(add(mx, mx)._data, np.matrix)) - assert_(isinstance(add.outer(mx, mx), MMatrix)) + with assert_warns(DeprecationWarning): + assert_(isinstance(add.outer(mx, mx), MMatrix)) assert_(isinstance(hypot(mx, mx), MMatrix)) assert_(isinstance(hypot(mx, x), MMatrix)) |