8000 ENH: Implement concatenate dtype and casting keyword arguments by seberg · Pull Request #16134 · numpy/numpy · GitHub
[go: up one dir, main page]

Skip to content

ENH: Implement concatenate dtype and casting keyword arguments #16134

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Sep 3, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions doc/release/upcoming_changes/16134.compatibility.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Same kind casting in concatenate with ``axis=None``
---------------------------------------------------
When `~numpy.concatenate` is called with `axis=None`,
the flattened arrays were cast with ``unsafe``. Any other axis
choice uses "same kind". That different default
has been deprecated and "same kind" casting will be used
instead. The new ``casting`` keyword argument
can be used to retain the old behaviour.
6 changes: 6 additions & 0 deletions doc/release/upcoming_changes/16134.improvement.rst
Original file line number Diff line number Diff line change
@@ -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.
14 changes: 12 additions & 2 deletions numpy/core/multiarray.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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.20.0

casting : {'no', 'equiv', 'safe', 'same_kind', 'unsafe'}, optional
Controls what kind of data casting may occur. Defaults to 'same_kind'.

..versionadded:: 1.20.0

Returns
-------
Expand Down
6 changes: 5 additions & 1 deletion numpy/core/src/multiarray/ctors.c
Original file line number Diff line number Diff line change
Expand Up @@ -1578,6 +1578,7 @@ PyArray_CheckFromAny(PyObject *op, PyArray_Descr *descr, int min_depth,
return obj;
}


/*NUMPY_API
* steals reference to newtype --- acc. NULL
*/
Expand Down Expand Up @@ -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)
{
Expand Down
133 changes: 115 additions & 18 deletions numpy/core/src/multiarray/multiarraymodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down Expand Up @@ -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");
Expand All @@ -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);
8000 }

/*
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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");
Expand All @@ -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;
Expand Down Expand Up @@ -593,10 +612,37 @@ PyArray_ConcatenateFlattenedArrays(int narrays, PyArrayObject **arrays,
return NULL;
}

int give_deprecation_warning = 1; /* To give warning for just one input array. */
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.20, 2020-09-03 */
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. "
"In the future this will be a TypeError.") < 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) {
Expand All @@ -614,8 +660,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;
Expand All @@ -625,6 +684,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);
Expand All @@ -651,10 +716,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) {
Expand Down Expand Up @@ -686,7 +754,16 @@ PyArray_ConcatenateInto(PyObject *op, int axis, PyArrayObject *ret)
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
Expand Down Expand Up @@ -2259,11 +2336,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&O$O&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) {
Expand All @@ -2272,10 +2365,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 *
Expand Down
21 changes: 21 additions & 0 deletions numpy/core/tests/test_deprecations.py
Original file line number Diff line number Diff line change
Expand Up @@ -707,3 +707,24 @@ def test_deprecated(self):
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):
# NumPy 1.20, 2020-09-03
message = "concatenate with `axis=None` will use same-kind casting"

def test_deprecated(self):
self.assert_deprecated(np.concatenate,
args=(([0.], [1.]),),
kwargs=dict(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")

39 changes: 26 additions & 13 deletions numpy/core/tests/test_shape_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,19 +342,32 @@ def test_bad_out_shape(self):
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():
Expand Down
0