summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSebastian Berg <sebastian@sipsolutions.net>2020-04-30 19:50:44 -0500
committerSebastian Berg <sebastian@sipsolutions.net>2020-09-02 20:11:21 -0500
commitcfd553dad7c1f315b7508eb56ac4527afc444ebb (patch)
tree77f50e201acf4ecebc66b008a35726a835cccf94
parente3c84a44b68966ab887a3623a0ff57169e508deb (diff)
downloadnumpy-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.rst8
-rw-r--r--doc/release/upcoming_changes/16134.improvement.rst6
-rw-r--r--numpy/core/multiarray.py14
-rw-r--r--numpy/core/src/multiarray/ctors.c6
-rw-r--r--numpy/core/src/multiarray/multiarraymodule.c132
-rw-r--r--numpy/core/tests/test_deprecations.py20
-rw-r--r--numpy/core/tests/test_shape_base.py39
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():