diff options
author | Sebastian Berg <sebastian@sipsolutions.net> | 2020-04-30 19:50:44 -0500 |
---|---|---|
committer | Sebastian Berg <sebastian@sipsolutions.net> | 2020-09-02 20:11:21 -0500 |
commit | cfd553dad7c1f315b7508eb56ac4527afc444ebb (patch) | |
tree | 77f50e201acf4ecebc66b008a35726a835cccf94 | |
parent | e3c84a44b68966ab887a3623a0ff57169e508deb (diff) | |
download | numpy-cfd553dad7c1f315b7508eb56ac4527afc444ebb.tar.gz |
ENH: Implement concatenate dtype and casting keyword arguments
Unfortunately, the casting was not consistent and sometimes used
force casting (axis=None) while normally same kind casting was used.
This thus deprecates the `force_casting` corner case, so that
casting has to be provided in the future.
-rw-r--r-- | doc/release/upcoming_changes/16134.compatibility.rst | 8 | ||||
-rw-r--r-- | doc/release/upcoming_changes/16134.improvement.rst | 6 | ||||
-rw-r--r-- | numpy/core/multiarray.py | 14 | ||||
-rw-r--r-- | numpy/core/src/multiarray/ctors.c | 6 | ||||
-rw-r--r-- | numpy/core/src/multiarray/multiarraymodule.c | 132 | ||||
-rw-r--r-- | numpy/core/tests/test_deprecations.py | 20 | ||||
-rw-r--r-- | numpy/core/tests/test_shape_base.py | 39 |
7 files changed, 191 insertions, 34 deletions
diff --git a/doc/release/upcoming_changes/16134.compatibility.rst b/doc/release/upcoming_changes/16134.compatibility.rst new file mode 100644 index 000000000..4270ea271 --- /dev/null +++ b/doc/release/upcoming_changes/16134.compatibility.rst @@ -0,0 +1,8 @@ +Same kind casting in concatenate with ``axis=None`` +--------------------------------------------------- +Unlike most `~numpy.concatenate` calls, when no axis +was given (the ravelled arrays being concatenated) +previously "unsafe" casting was used. +This is now deprecated and "same kind" casting will be +used by default. The new ``casting`` keyword argument +can be used to retain the old behaviour. diff --git a/doc/release/upcoming_changes/16134.improvement.rst b/doc/release/upcoming_changes/16134.improvement.rst new file mode 100644 index 000000000..0699f44bd --- /dev/null +++ b/doc/release/upcoming_changes/16134.improvement.rst @@ -0,0 +1,6 @@ +Concatenate supports providing an output dtype +---------------------------------------------- +Support was added to `~numpy.concatenate` to provide +an output ``dtype`` and ``casting`` using keyword +arguments. The ``dtype`` argument cannot be provided +in conjunction with the ``out`` one. diff --git a/numpy/core/multiarray.py b/numpy/core/multiarray.py index 10325050d..540c0568f 100644 --- a/numpy/core/multiarray.py +++ b/numpy/core/multiarray.py @@ -141,9 +141,9 @@ def empty_like(prototype, dtype=None, order=None, subok=None, shape=None): @array_function_from_c_func_and_dispatcher(_multiarray_umath.concatenate) -def concatenate(arrays, axis=None, out=None): +def concatenate(arrays, axis=None, out=None, dtype=None, casting=None): """ - concatenate((a1, a2, ...), axis=0, out=None) + concatenate((a1, a2, ...), axis=0, out=None, dtype=None, casting="same_kind") Join a sequence of arrays along an existing axis. @@ -159,6 +159,16 @@ def concatenate(arrays, axis=None, out=None): If provided, the destination to place the result. The shape must be correct, matching that of what concatenate would have returned if no out argument were specified. + dtype : str or dtype + If provided, the destination array will have this dtype. Cannot be + provided together with `out`. + + ..versionadded:: 1.19.0 + + casting : {'no', 'equiv', 'safe', 'same_kind', 'unsafe'}, optional + Controls what kind of data casting may occur. Defaults to 'same_kind'. + + ..versionadded:: 1.19.0 Returns ------- diff --git a/numpy/core/src/multiarray/ctors.c b/numpy/core/src/multiarray/ctors.c index 7534c0717..ef446cd90 100644 --- a/numpy/core/src/multiarray/ctors.c +++ b/numpy/core/src/multiarray/ctors.c @@ -1578,6 +1578,7 @@ PyArray_CheckFromAny(PyObject *op, PyArray_Descr *descr, int min_depth, return obj; } + /*NUMPY_API * steals reference to newtype --- acc. NULL */ @@ -2252,7 +2253,10 @@ PyArray_EnsureAnyArray(PyObject *op) return PyArray_EnsureArray(op); } -/* TODO: Put the order parameter in PyArray_CopyAnyInto and remove this */ +/* + * Private implementation of PyArray_CopyAnyInto with an additional order + * parameter. + */ NPY_NO_EXPORT int PyArray_CopyAsFlat(PyArrayObject *dst, PyArrayObject *src, NPY_ORDER order) { diff --git a/numpy/core/src/multiarray/multiarraymodule.c b/numpy/core/src/multiarray/multiarraymodule.c index 8d5cbf3fa..a19e75a10 100644 --- a/numpy/core/src/multiarray/multiarraymodule.c +++ b/numpy/core/src/multiarray/multiarraymodule.c @@ -362,7 +362,8 @@ PyArray_GetSubType(int narrays, PyArrayObject **arrays) { */ NPY_NO_EXPORT PyArrayObject * PyArray_ConcatenateArrays(int narrays, PyArrayObject **arrays, int axis, - PyArrayObject* ret) + PyArrayObject* ret, PyArray_Descr *dtype, + NPY_CASTING casting) { int iarrays, idim, ndim; npy_intp shape[NPY_MAXDIMS]; @@ -426,6 +427,7 @@ PyArray_ConcatenateArrays(int narrays, PyArrayObject **arrays, int axis, } if (ret != NULL) { + assert(dtype == NULL); if (PyArray_NDIM(ret) != ndim) { PyErr_SetString(PyExc_ValueError, "Output array has wrong dimensionality"); @@ -445,10 +447,16 @@ PyArray_ConcatenateArrays(int narrays, PyArrayObject **arrays, int axis, /* Get the priority subtype for the array */ PyTypeObject *subtype = PyArray_GetSubType(narrays, arrays); - /* Get the resulting dtype from combining all the arrays */ - PyArray_Descr *dtype = PyArray_ResultType(narrays, arrays, 0, NULL); if (dtype == NULL) { - return NULL; + /* Get the resulting dtype from combining all the arrays */ + dtype = (PyArray_Descr *)PyArray_ResultType( + narrays, arrays, 0, NULL); + if (dtype == NULL) { + return NULL; + } + } + else { + Py_INCREF(dtype); } /* @@ -494,7 +502,7 @@ PyArray_ConcatenateArrays(int narrays, PyArrayObject **arrays, int axis, /* Copy the data for this array */ if (PyArray_AssignArray((PyArrayObject *)sliding_view, arrays[iarrays], - NULL, NPY_SAME_KIND_CASTING) < 0) { + NULL, casting) < 0) { Py_DECREF(sliding_view); Py_DECREF(ret); return NULL; @@ -514,7 +522,9 @@ PyArray_ConcatenateArrays(int narrays, PyArrayObject **arrays, int axis, */ NPY_NO_EXPORT PyArrayObject * PyArray_ConcatenateFlattenedArrays(int narrays, PyArrayObject **arrays, - NPY_ORDER order, PyArrayObject *ret) + NPY_ORDER order, PyArrayObject *ret, + PyArray_Descr *dtype, NPY_CASTING casting, + npy_bool casting_not_passed) { int iarrays; npy_intp shape = 0; @@ -541,7 +551,10 @@ PyArray_ConcatenateFlattenedArrays(int narrays, PyArrayObject **arrays, } } + int out_passed = 0; if (ret != NULL) { + assert(dtype == NULL); + out_passed = 1; if (PyArray_NDIM(ret) != 1) { PyErr_SetString(PyExc_ValueError, "Output array must be 1D"); @@ -560,10 +573,16 @@ PyArray_ConcatenateFlattenedArrays(int narrays, PyArrayObject **arrays, /* Get the priority subtype for the array */ PyTypeObject *subtype = PyArray_GetSubType(narrays, arrays); - /* Get the resulting dtype from combining all the arrays */ - PyArray_Descr *dtype = PyArray_ResultType(narrays, arrays, 0, NULL); if (dtype == NULL) { - return NULL; + /* Get the resulting dtype from combining all the arrays */ + dtype = (PyArray_Descr *)PyArray_ResultType( + narrays, arrays, 0, NULL); + if (dtype == NULL) { + return NULL; + } + } + else { + Py_INCREF(dtype); } stride = dtype->elsize; @@ -593,10 +612,36 @@ PyArray_ConcatenateFlattenedArrays(int narrays, PyArrayObject **arrays, return NULL; } + int give_deprecation_warning = 1; /* To give warning only once. */ for (iarrays = 0; iarrays < narrays; ++iarrays) { /* Adjust the window dimensions for this array */ sliding_view->dimensions[0] = PyArray_SIZE(arrays[iarrays]); + if (!PyArray_CanCastArrayTo( + arrays[iarrays], PyArray_DESCR(ret), casting)) { + /* This should be an error, but was previously allowed here. */ + if (casting_not_passed && out_passed) { + /* NumPy 1.19, 2020-04-30 */ + if (give_deprecation_warning && DEPRECATE( + "concatenate with `axis=None` will use same-kind " + "casting by default in the future. Please use " + "`casting='unsafe'` to retain the old behaviour.") < 0) { + Py_DECREF(sliding_view); + Py_DECREF(ret); + return NULL; + } + give_deprecation_warning = 0; + } + else { + npy_set_invalid_cast_error( + PyArray_DESCR(arrays[iarrays]), PyArray_DESCR(ret), + casting, PyArray_NDIM(arrays[iarrays]) == 0); + Py_DECREF(sliding_view); + Py_DECREF(ret); + return NULL; + } + } + /* Copy the data for this array */ if (PyArray_CopyAsFlat((PyArrayObject *)sliding_view, arrays[iarrays], order) < 0) { @@ -614,8 +659,21 @@ PyArray_ConcatenateFlattenedArrays(int narrays, PyArrayObject **arrays, return ret; } + +/** + * Implementation for np.concatenate + * + * @param op Sequence of arrays to concatenate + * @param axis Axis to concatenate along + * @param ret output array to fill + * @param dtype Forced output array dtype (cannot be combined with ret) + * @param casting Casting mode used + * @param casting_not_passed Deprecation helper + */ NPY_NO_EXPORT PyObject * -PyArray_ConcatenateInto(PyObject *op, int axis, PyArrayObject *ret) +PyArray_ConcatenateInto(PyObject *op, + int axis, PyArrayObject *ret, PyArray_Descr *dtype, + NPY_CASTING casting, npy_bool casting_not_passed) { int iarrays, narrays; PyArrayObject **arrays; @@ -625,6 +683,12 @@ PyArray_ConcatenateInto(PyObject *op, int axis, PyArrayObject *ret) "The first input argument needs to be a sequence"); return NULL; } + if (ret != NULL && dtype != NULL) { + PyErr_SetString(PyExc_TypeError, + "concatenate() only takes `out` or `dtype` as an " + "argument, but both were provided."); + return NULL; + } /* Convert the input list into arrays */ narrays = PySequence_Size(op); @@ -651,10 +715,13 @@ PyArray_ConcatenateInto(PyObject *op, int axis, PyArrayObject *ret) } if (axis >= NPY_MAXDIMS) { - ret = PyArray_ConcatenateFlattenedArrays(narrays, arrays, NPY_CORDER, ret); + ret = PyArray_ConcatenateFlattenedArrays( + narrays, arrays, NPY_CORDER, ret, dtype, + casting, casting_not_passed); } else { - ret = PyArray_ConcatenateArrays(narrays, arrays, axis, ret); + ret = PyArray_ConcatenateArrays( + narrays, arrays, axis, ret, dtype, casting); } for (iarrays = 0; iarrays < narrays; ++iarrays) { @@ -686,7 +753,16 @@ fail: NPY_NO_EXPORT PyObject * PyArray_Concatenate(PyObject *op, int axis) { - return PyArray_ConcatenateInto(op, axis, NULL); + /* retain legacy behaviour for casting */ + NPY_CASTING casting; + if (axis >= NPY_MAXDIMS) { + casting = NPY_UNSAFE_CASTING; + } + else { + casting = NPY_SAME_KIND_CASTING; + } + return PyArray_ConcatenateInto( + op, axis, NULL, NULL, casting, 0); } static int @@ -2259,11 +2335,27 @@ array_concatenate(PyObject *NPY_UNUSED(dummy), PyObject *args, PyObject *kwds) { PyObject *a0; PyObject *out = NULL; + PyArray_Descr *dtype = NULL; + NPY_CASTING casting = NPY_SAME_KIND_CASTING; + PyObject *casting_obj = NULL; + PyObject *res; int axis = 0; - static char *kwlist[] = {"seq", "axis", "out", NULL}; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|O&O:concatenate", kwlist, - &a0, PyArray_AxisConverter, &axis, &out)) { + static char *kwlist[] = {"seq", "axis", "out", "dtype", "casting", NULL}; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|O&OO&O:concatenate", kwlist, + &a0, PyArray_AxisConverter, &axis, &out, + PyArray_DescrConverter2, &dtype, &casting_obj)) { + return NULL; + } + int casting_not_passed = 0; + if (casting_obj == NULL) { + /* + * Casting was not passed in, needed for deprecation only. + * This should be simplified once the deprecation is finished. + */ + casting_not_passed = 1; + } + else if (!PyArray_CastingConverter(casting_obj, &casting)) { + Py_XDECREF(dtype); return NULL; } if (out != NULL) { @@ -2272,10 +2364,14 @@ array_concatenate(PyObject *NPY_UNUSED(dummy), PyObject *args, PyObject *kwds) } else if (!PyArray_Check(out)) { PyErr_SetString(PyExc_TypeError, "'out' must be an array"); + Py_XDECREF(dtype); return NULL; } } - return PyArray_ConcatenateInto(a0, axis, (PyArrayObject *)out); + res = PyArray_ConcatenateInto(a0, axis, (PyArrayObject *)out, dtype, + casting, casting_not_passed); + Py_XDECREF(dtype); + return res; } static PyObject * diff --git a/numpy/core/tests/test_deprecations.py b/numpy/core/tests/test_deprecations.py index 4ecfb0919..1b2738e83 100644 --- a/numpy/core/tests/test_deprecations.py +++ b/numpy/core/tests/test_deprecations.py @@ -707,3 +707,23 @@ class TestRaggedArray(_DeprecationTestCase): self.assert_deprecated(lambda: np.array([arr, [0]], dtype=np.float64)) self.assert_deprecated(lambda: np.array([[0], arr], dtype=np.float64)) + +class FlatteningConcatenateUnsafeCast(_DeprecationTestCase): + message = "concatenate with `axis=None` will use same-kind casting" + + def test_deprecated(self): + self.assert_deprecated(np.concatenate, + args=(([0.], [1.]),), + kwargs={'axis': None, 'out': np.empty(2, dtype=np.int64)}) + + def test_not_deprecated(self): + self.assert_not_deprecated(np.concatenate, + args=(([0.], [1.]),), + kwargs={'axis': None, 'out': np.empty(2, dtype=np.int64), + 'casting': "unsafe"}) + + with assert_raises(TypeError): + # Tests should notice if the deprecation warning is given first... + np.concatenate(([0.], [1.]), out=np.empty(2, dtype=np.int64), + casting="same_kind") + diff --git a/numpy/core/tests/test_shape_base.py b/numpy/core/tests/test_shape_base.py index 94a916193..4e56ace90 100644 --- a/numpy/core/tests/test_shape_base.py +++ b/numpy/core/tests/test_shape_base.py @@ -342,19 +342,32 @@ class TestConcatenate: assert_raises(ValueError, concatenate, (a, b), out=np.empty((1,4))) concatenate((a, b), out=np.empty(4)) - def test_out_dtype(self): - out = np.empty(4, np.float32) - res = concatenate((array([1, 2]), array([3, 4])), out=out) - assert_(out is res) - - out = np.empty(4, np.complex64) - res = concatenate((array([0.1, 0.2]), array([0.3, 0.4])), out=out) - assert_(out is res) - - # invalid cast - out = np.empty(4, np.int32) - assert_raises(TypeError, concatenate, - (array([0.1, 0.2]), array([0.3, 0.4])), out=out) + @pytest.mark.parametrize("axis", [None, 0]) + @pytest.mark.parametrize("out_dtype", ["c8", "f4", "f8", ">f8", "i8"]) + @pytest.mark.parametrize("casting", + ['no', 'equiv', 'safe', 'same_kind', 'unsafe']) + def test_out_and_dtype(self, axis, out_dtype, casting): + # Compare usage of `out=out` with `dtype=out.dtype` + out = np.empty(4, dtype=out_dtype) + to_concat = (array([1.1, 2.2]), array([3.3, 4.4])) + + if not np.can_cast(to_concat[0], out_dtype, casting=casting): + with assert_raises(TypeError): + concatenate(to_concat, out=out, axis=axis, casting=casting) + with assert_raises(TypeError): + concatenate(to_concat, dtype=out.dtype, + axis=axis, casting=casting) + else: + res_out = concatenate(to_concat, out=out, + axis=axis, casting=casting) + res_dtype = concatenate(to_concat, dtype=out.dtype, + axis=axis, casting=casting) + assert res_out is out + assert_array_equal(out, res_dtype) + assert res_dtype.dtype == out_dtype + + with assert_raises(TypeError): + concatenate(to_concat, out=out, dtype=out_dtype, axis=axis) def test_stack(): |