Skip to content

Commit 2da02ea

Browse files
authored
Merge pull request numpy#26292 from mtsokol/reshape-shape-keyword
API: Add ``shape`` and ``copy`` arguments to ``numpy.reshape``
2 parents f8392ce + 4a264d1 commit 2da02ea

File tree

11 files changed

+174
-51
lines changed

11 files changed

+174
-51
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* `numpy.reshape` and `numpy.ndarray.reshape` now support ``shape`` and ``copy`` arguments.

doc/source/user/absolute_beginners.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -425,7 +425,7 @@ this array to an array with three rows and two columns::
425425

426426
With ``np.reshape``, you can specify a few optional parameters::
427427

428-
>>> np.reshape(a, newshape=(1, 6), order='C')
428+
>>> np.reshape(a, shape=(1, 6), order='C')
429429
array([[0, 1, 2, 3, 4, 5]])
430430

431431
``a`` is the array to be reshaped.

numpy/__init__.pyi

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1713,11 +1713,19 @@ class ndarray(_ArrayOrScalarCommon, Generic[_ShapeType, _DType_co]):
17131713

17141714
@overload
17151715
def reshape(
1716-
self, shape: _ShapeLike, /, *, order: _OrderACF = ...
1716+
self,
1717+
shape: _ShapeLike,
1718+
/,
1719+
*,
1720+
order: _OrderACF = ...,
1721+
copy: None | bool = ...,
17171722
) -> ndarray[Any, _DType_co]: ...
17181723
@overload
17191724
def reshape(
1720-
self, *shape: SupportsIndex, order: _OrderACF = ...
1725+
self,
1726+
*shape: SupportsIndex,
1727+
order: _OrderACF = ...,
1728+
copy: None | bool = ...,
17211729
) -> ndarray[Any, _DType_co]: ...
17221730

17231731
@overload

numpy/_core/_add_newdocs.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3824,7 +3824,7 @@
38243824

38253825
add_newdoc('numpy._core.multiarray', 'ndarray', ('reshape',
38263826
"""
3827-
a.reshape(shape, /, *, order='C')
3827+
a.reshape(shape, /, *, order='C', copy=None)
38283828
38293829
Returns an array containing the same data with a new shape.
38303830

numpy/_core/fromnumeric.py

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -206,28 +206,32 @@ def take(a, indices, axis=None, out=None, mode='raise'):
206206
return _wrapfunc(a, 'take', indices, axis=axis, out=out, mode=mode)
207207

208208

209-
def _reshape_dispatcher(a, newshape, order=None):
209+
def _reshape_dispatcher(a, /, shape=None, *, newshape=None, order=None,
210+
copy=None):
210211
return (a,)
211212

212213

213-
# not deprecated --- copy if necessary, view otherwise
214214
@array_function_dispatch(_reshape_dispatcher)
215-
def reshape(a, newshape, order='C'):
215+
def reshape(a, /, shape=None, *, newshape=None, order='C', copy=None):
216216
"""
217217
Gives a new shape to an array without changing its data.
218218
219219
Parameters
220220
----------
221221
a : array_like
222222
Array to be reshaped.
223-
newshape : int or tuple of ints
223+
shape : int or tuple of ints
224224
The new shape should be compatible with the original shape. If
225225
an integer, then the result will be a 1-D array of that length.
226226
One shape dimension can be -1. In this case, the value is
227227
inferred from the length of the array and remaining dimensions.
228+
newshape : int or tuple of ints
229+
.. deprecated:: 2.1
230+
Replaced by ``shape`` argument. Retained for backward
231+
compatibility.
228232
order : {'C', 'F', 'A'}, optional
229-
Read the elements of `a` using this index order, and place the
230-
elements into the reshaped array using this index order. 'C'
233+
Read the elements of ``a`` using this index order, and place the
234+
elements into the reshaped array using this index order. 'C'
231235
means to read / write the elements using C-like index order,
232236
with the last axis index changing fastest, back to the first
233237
axis index changing slowest. 'F' means to read / write the
@@ -236,8 +240,12 @@ def reshape(a, newshape, order='C'):
236240
the 'C' and 'F' options take no account of the memory layout of
237241
the underlying array, and only refer to the order of indexing.
238242
'A' means to read / write the elements in Fortran-like index
239-
order if `a` is Fortran *contiguous* in memory, C-like order
243+
order if ``a`` is Fortran *contiguous* in memory, C-like order
240244
otherwise.
245+
copy : bool, optional
246+
If ``True``, then the array data is copied. If ``None``, a copy will
247+
only be made if it's required by ``order``. For ``False`` it raises
248+
a ``ValueError`` if a copy cannot be avoided. Default: ``None``.
241249
242250
Returns
243251
-------
@@ -255,9 +263,9 @@ def reshape(a, newshape, order='C'):
255263
It is not always possible to change the shape of an array without copying
256264
the data.
257265
258-
The `order` keyword gives the index ordering both for *fetching* the values
259-
from `a`, and then *placing* the values into the output array.
260-
For example, let's say you have an array:
266+
The ``order`` keyword gives the index ordering both for *fetching*
267+
the values from ``a``, and then *placing* the values into the output
268+
array. For example, let's say you have an array:
261269
262270
>>> a = np.arange(6).reshape((3, 2))
263271
>>> a
@@ -296,7 +304,26 @@ def reshape(a, newshape, order='C'):
296304
[3, 4],
297305
[5, 6]])
298306
"""
299-
return _wrapfunc(a, 'reshape', newshape, order=order)
307+
if newshape is None and shape is None:
308+
raise TypeError(
309+
"reshape() missing 1 required positional argument: 'shape'")
310+
if newshape is not None:
311+
if shape is not None:
312+
raise TypeError(
313+
"You cannot specify 'newshape' and 'shape' arguments "
314+
"at the same time.")
315+
# Deprecated in NumPy 2.1, 2024-04-18
316+
warnings.warn(
317+
"`newshape` keyword argument is deprecated, "
318+
"use `shape=...` or pass shape positionally instead. "
319+
"(deprecated in NumPy 2.1)",
320+
DeprecationWarning,
321+
stacklevel=2,
322+
)
323+
shape = newshape
324+
if copy is not None:
325+
return _wrapfunc(a, 'reshape', shape, order=order, copy=copy)
326+
return _wrapfunc(a, 'reshape', shape, order=order)
300327

301328

302329
def _choose_dispatcher(a, choices, out=None, mode=None):

numpy/_core/fromnumeric.pyi

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,12 +94,14 @@ def reshape(
9494
a: _ArrayLike[_SCT],
9595
newshape: _ShapeLike,
9696
order: _OrderACF = ...,
97+
copy: None | bool = ...,
9798
) -> NDArray[_SCT]: ...
9899
@overload
99100
def reshape(
100101
a: ArrayLike,
101102
newshape: _ShapeLike,
102103
order: _OrderACF = ...,
104+
copy: None | bool = ...,
103105
) -> NDArray[Any]: ...
104106

105107
@overload

numpy/_core/src/multiarray/methods.c

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -181,14 +181,16 @@ array_put(PyArrayObject *self, PyObject *args, PyObject *kwds)
181181
static PyObject *
182182
array_reshape(PyArrayObject *self, PyObject *args, PyObject *kwds)
183183
{
184-
static char *keywords[] = {"order", NULL};
184+
static char *keywords[] = {"order", "copy", NULL};
185185
PyArray_Dims newshape;
186186
PyObject *ret;
187187
NPY_ORDER order = NPY_CORDER;
188+
NPY_COPYMODE copy = NPY_COPY_IF_NEEDED;
188189
Py_ssize_t n = PyTuple_Size(args);
189190

190-
if (!NpyArg_ParseKeywords(kwds, "|O&", keywords,
191-
PyArray_OrderConverter, &order)) {
191+
if (!NpyArg_ParseKeywords(kwds, "|$O&O&", keywords,
192+
PyArray_OrderConverter, &order,
193+
PyArray_CopyConverter, &copy)) {
192194
return NULL;
193195
}
194196

@@ -210,7 +212,7 @@ array_reshape(PyArrayObject *self, PyObject *args, PyObject *kwds)
210212
goto fail;
211213
}
212214
}
213-
ret = PyArray_Newshape(self, &newshape, order);
215+
ret = _reshape_with_copy_arg(self, &newshape, order, copy);
214216
npy_free_cache_dim_obj(newshape);
215217
return ret;
216218

numpy/_core/src/multiarray/shape.c

Lines changed: 56 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,14 @@ PyArray_Resize(PyArrayObject *self, PyArray_Dims *newshape, int refcheck,
201201
NPY_NO_EXPORT PyObject *
202202
PyArray_Newshape(PyArrayObject *self, PyArray_Dims *newdims,
203203
NPY_ORDER order)
204+
{
205+
return _reshape_with_copy_arg(self, newdims, order, NPY_COPY_IF_NEEDED);
206+
}
207+
208+
209+
NPY_NO_EXPORT PyObject *
210+
_reshape_with_copy_arg(PyArrayObject *array, PyArray_Dims *newdims,
211+
NPY_ORDER order, NPY_COPYMODE copy)
204212
{
205213
npy_intp i;
206214
npy_intp *dimensions = newdims->ptr;
@@ -212,64 +220,82 @@ PyArray_Newshape(PyArrayObject *self, PyArray_Dims *newdims,
212220
int flags;
213221

214222
if (order == NPY_ANYORDER) {
215-
order = PyArray_ISFORTRAN(self) ? NPY_FORTRANORDER : NPY_CORDER;
223+
order = PyArray_ISFORTRAN(array) ? NPY_FORTRANORDER : NPY_CORDER;
216224
}
217225
else if (order == NPY_KEEPORDER) {
218226
PyErr_SetString(PyExc_ValueError,
219227
"order 'K' is not permitted for reshaping");
220228
return NULL;
221229
}
222230
/* Quick check to make sure anything actually needs to be done */
223-
if (ndim == PyArray_NDIM(self)) {
231+
if (ndim == PyArray_NDIM(array) && copy != NPY_COPY_ALWAYS) {
224232
same = NPY_TRUE;
225233
i = 0;
226234
while (same && i < ndim) {
227-
if (PyArray_DIM(self,i) != dimensions[i]) {
235+
if (PyArray_DIM(array, i) != dimensions[i]) {
228236
same=NPY_FALSE;
229237
}
230238
i++;
231239
}
232240
if (same) {
233-
return PyArray_View(self, NULL, NULL);
241+
return PyArray_View(array, NULL, NULL);
234242
}
235243
}
236244

237245
/*
238246
* fix any -1 dimensions and check new-dimensions against old size
239247
*/
240-
if (_fix_unknown_dimension(newdims, self) < 0) {
248+
if (_fix_unknown_dimension(newdims, array) < 0) {
241249
return NULL;
242250
}
243251
/*
244-
* sometimes we have to create a new copy of the array
245-
* in order to get the right orientation and
246-
* because we can't just reuse the buffer with the
247-
* data in the order it is in.
252+
* Memory order doesn't depend on a copy/no-copy context.
253+
* 'order' argument is always honored.
248254
*/
249-
Py_INCREF(self);
250-
if (((order == NPY_CORDER && !PyArray_IS_C_CONTIGUOUS(self)) ||
251-
(order == NPY_FORTRANORDER && !PyArray_IS_F_CONTIGUOUS(self)))) {
252-
int success = 0;
253-
success = _attempt_nocopy_reshape(self, ndim, dimensions,
254-
newstrides, order);
255-
if (success) {
256-
/* no need to copy the array after all */
257-
strides = newstrides;
255+
if (copy == NPY_COPY_ALWAYS) {
256+
PyObject *newcopy = PyArray_NewCopy(array, order);
257+
if (newcopy == NULL) {
258+
return NULL;
258259
}
259-
else {
260-
PyObject *newcopy;
261-
newcopy = PyArray_NewCopy(self, order);
262-
Py_DECREF(self);
263-
if (newcopy == NULL) {
260+
array = (PyArrayObject *)newcopy;
261+
}
262+
else {
263+
/*
264+
* sometimes we have to create a new copy of the array
265+
* in order to get the right orientation and
266+
* because we can't just reuse the buffer with the
267+
* data in the order it is in.
268+
*/
269+
Py_INCREF(array);
270+
if (((order == NPY_CORDER && !PyArray_IS_C_CONTIGUOUS(array)) ||
271+
(order == NPY_FORTRANORDER && !PyArray_IS_F_CONTIGUOUS(array)))) {
272+
int success = 0;
273+
success = _attempt_nocopy_reshape(array, ndim, dimensions,
274+
newstrides, order);
275+
if (success) {
276+
/* no need to copy the array after all */
277+
strides = newstrides;
278+
}
279+
else if (copy == NPY_COPY_NEVER) {
280+
PyErr_SetString(PyExc_ValueError,
281+
"Unable to avoid creating a copy while reshaping.");
282+
Py_DECREF(array);
264283
return NULL;
265284
}
266-
self = (PyArrayObject *)newcopy;
285+
else {
286+
PyObject *newcopy = PyArray_NewCopy(array, order);
287+
Py_DECREF(array);
288+
if (newcopy == NULL) {
289+
return NULL;
290+
}
291+
array = (PyArrayObject *)newcopy;
292+
}
267293
}
268294
}
269295
/* We always have to interpret the contiguous buffer correctly */
270296

271297
/* Make sure the flags argument is set. */
272-
flags = PyArray_FLAGS(self);
298+
flags = PyArray_FLAGS(array);
273299
if (ndim > 1) {
274300
if (order == NPY_FORTRANORDER) {
275301
flags &= ~NPY_ARRAY_C_CONTIGUOUS;
@@ -281,18 +307,17 @@ PyArray_Newshape(PyArrayObject *self, PyArray_Dims *newdims,
281307
}
282308
}
283309

284-
Py_INCREF(PyArray_DESCR(self));
310+
Py_INCREF(PyArray_DESCR(array));
285311
ret = (PyArrayObject *)PyArray_NewFromDescr_int(
286-
Py_TYPE(self), PyArray_DESCR(self),
287-
ndim, dimensions, strides, PyArray_DATA(self),
288-
flags, (PyObject *)self, (PyObject *)self,
312+
Py_TYPE(array), PyArray_DESCR(array),
313+
ndim, dimensions, strides, PyArray_DATA(array),
314+
flags, (PyObject *)array, (PyObject *)array,
289315
_NPY_ARRAY_ENSURE_DTYPE_IDENTITY);
290-
Py_DECREF(self);
316+
Py_DECREF(array);
291317
return (PyObject *)ret;
292318
}
293319

294320

295-
296321
/* For backward compatibility -- Not recommended */
297322

298323
/*NUMPY_API

numpy/_core/src/multiarray/shape.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
#ifndef NUMPY_CORE_SRC_MULTIARRAY_SHAPE_H_
22
#define NUMPY_CORE_SRC_MULTIARRAY_SHAPE_H_
33

4+
#include "conversion_utils.h"
5+
46
/*
57
* Creates a sorted stride perm matching the KEEPORDER behavior
68
* of the NpyIter object. Because this operates based on multiple
@@ -27,4 +29,8 @@ PyArray_SqueezeSelected(PyArrayObject *self, npy_bool *axis_flags);
2729
NPY_NO_EXPORT PyObject *
2830
PyArray_MatrixTranspose(PyArrayObject *ap);
2931

32+
NPY_NO_EXPORT PyObject *
33+
_reshape_with_copy_arg(PyArrayObject *array, PyArray_Dims *newdims,
34+
NPY_ORDER order, NPY_COPYMODE copy);
35+
3036
#endif /* NUMPY_CORE_SRC_MULTIARRAY_SHAPE_H_ */

0 commit comments

Comments
 (0)