Skip to content

Commit

Permalink
DEP: deprecate scalar conversions for arrays with ndim > 0 (#10615)
Browse files Browse the repository at this point in the history
This PR reflects some of the progress achieved in issue #10404 and is used to asses the impact of the changes. 

With the changes in this PR, `float(numpy.array([1.0])` now gives a warning; likewise some other things:
```python
import numpy

a = numpy.random.rand(10, 1)
a[0] = numpy.array([1.0])       # okay
a[0] = numpy.array(1.0)         # okay
a[0] = 1.0                      # okay

b = numpy.random.rand(10)
b[0] = numpy.array([1.0])       # ValueError: setting an array element with a sequence.
b[0, ...] = numpy.array([1.0])  # okay
b[0] = numpy.array(1.0)         # okay
b[0] = 1.0                      # okay
```
This aligns the behavior of numpy arrays with that of lists:
```python
float([3.14]) 
```
```
TypeError: float() argument must be a string or a number, not 'list'
```
```python
import numpy as np

a = np.random.rand(5)
a[0] = [3.14]
```
```
ValueError: setting an array element with a sequence.
```

Fixes #10404.
  • Loading branch information
nschloe committed Apr 20, 2023
1 parent 64e692f commit 1acac89
Show file tree
Hide file tree
Showing 8 changed files with 83 additions and 16 deletions.
14 changes: 14 additions & 0 deletions doc/release/upcoming_changes/10615.deprecation.rst
@@ -0,0 +1,14 @@
Only ndim-0 arrays are treated as scalars
-----------------------------------------
NumPy used to treat all arrays of size 1 (e.g., ``np.array([3.14])``) as scalars.
In the future, this will be limited to arrays of ndim 0 (e.g., ``np.array(3.14)``).
The following expressions will report a deprecation warning:

.. code-block:: python
a = np.array([3.14])
float(a) # better: a[0] to get the numpy.float or a.item()
b = np.array([[3.14]])
c = numpy.random.rand(10)
c[0] = b # better: c[0] = b[0, 0]
2 changes: 1 addition & 1 deletion numpy/core/function_base.py
Expand Up @@ -168,7 +168,7 @@ def linspace(start, stop, num=50, endpoint=True, retstep=False, dtype=None,
y += start

if endpoint and num > 1:
y[-1] = stop
y[-1, ...] = stop

if axis != 0:
y = _nx.moveaxis(y, 0, axis)
Expand Down
29 changes: 29 additions & 0 deletions numpy/core/src/multiarray/common.c
Expand Up @@ -441,3 +441,32 @@ new_array_for_sum(PyArrayObject *ap1, PyArrayObject *ap2, PyArrayObject* out,
}
}

NPY_NO_EXPORT int
check_is_convertible_to_scalar(PyArrayObject *v)
{
if (PyArray_NDIM(v) == 0) {
return 0;
}

/* Remove this if-else block when the deprecation expires */
if (PyArray_SIZE(v) == 1) {
/* Numpy 1.25.0, 2023-01-02 */
if (DEPRECATE(
"Conversion of an array with ndim > 0 to a scalar "
"is deprecated, and will error in future. "
"Ensure you extract a single element from your array "
"before performing this operation. "
"(Deprecated NumPy 1.25.)") < 0) {
return -1;
}
return 0;
} else {
PyErr_SetString(PyExc_TypeError,
"only length-1 arrays can be converted to Python scalars");
return -1;
}

PyErr_SetString(PyExc_TypeError,
"only 0-dimensional arrays can be converted to Python scalars");
return -1;
}
7 changes: 7 additions & 0 deletions numpy/core/src/multiarray/common.h
Expand Up @@ -332,6 +332,13 @@ PyArray_TupleFromItems(int n, PyObject *const *items, int make_null_none)
return tuple;
}

/*
* Returns 0 if the array has rank 0, -1 otherwise. Prints a deprecation
* warning for arrays of _size_ 1.
*/
NPY_NO_EXPORT int
check_is_convertible_to_scalar(PyArrayObject *v);


#include "ucsnarrow.h"

Expand Down
4 changes: 1 addition & 3 deletions numpy/core/src/multiarray/methods.c
Expand Up @@ -2804,9 +2804,7 @@ array_complex(PyArrayObject *self, PyObject *NPY_UNUSED(args))
PyArray_Descr *dtype;
PyObject *c;

if (PyArray_SIZE(self) != 1) {
PyErr_SetString(PyExc_TypeError,
"only length-1 arrays can be converted to Python scalars");
if (check_is_convertible_to_scalar(self) < 0) {
return NULL;
}

Expand Down
6 changes: 2 additions & 4 deletions numpy/core/src/multiarray/number.c
Expand Up @@ -869,13 +869,11 @@ array_scalar_forward(PyArrayObject *v,
PyObject *(*builtin_func)(PyObject *),
const char *where)
{
PyObject *scalar;
if (PyArray_SIZE(v) != 1) {
PyErr_SetString(PyExc_TypeError, "only size-1 arrays can be"\
" converted to Python scalars");
if (check_is_convertible_to_scalar(v) < 0) {
return NULL;
}

PyObject *scalar;
scalar = PyArray_GETITEM(v, PyArray_DATA(v));
if (scalar == NULL) {
return NULL;
Expand Down
12 changes: 12 additions & 0 deletions numpy/core/tests/test_deprecations.py
Expand Up @@ -822,6 +822,18 @@ def test_deprecated_raised(self, dtype):
assert isinstance(e.__cause__, DeprecationWarning)


class TestScalarConversion(_DeprecationTestCase):
# 2023-01-02, 1.25.0
def test_float_conversion(self):
self.assert_deprecated(float, args=(np.array([3.14]),))

def test_behaviour(self):
b = np.array([[3.14]])
c = np.zeros(5)
with pytest.warns(DeprecationWarning):
c[0] = b


class TestPyIntConversion(_DeprecationTestCase):
message = r".*stop allowing conversion of out-of-bound.*"

Expand Down
25 changes: 17 additions & 8 deletions numpy/core/tests/test_multiarray.py
Expand Up @@ -3644,9 +3644,13 @@ def test__complex__(self):
msg = 'dtype: {0}'.format(dt)
ap = complex(a)
assert_equal(ap, a, msg)
bp = complex(b)

with assert_warns(DeprecationWarning):
bp = complex(b)
assert_equal(bp, b, msg)
cp = complex(c)

with assert_warns(DeprecationWarning):
cp = complex(c)
assert_equal(cp, c, msg)

def test__complex__should_not_work(self):
Expand All @@ -3669,7 +3673,8 @@ def test__complex__should_not_work(self):
assert_raises(TypeError, complex, d)

e = np.array(['1+1j'], 'U')
assert_raises(TypeError, complex, e)
with assert_warns(DeprecationWarning):
assert_raises(TypeError, complex, e)

class TestCequenceMethods:
def test_array_contains(self):
Expand Down Expand Up @@ -8756,8 +8761,10 @@ def test_to_int_scalar(self):
int_funcs = (int, lambda x: x.__int__())
for int_func in int_funcs:
assert_equal(int_func(np.array(0)), 0)
assert_equal(int_func(np.array([1])), 1)
assert_equal(int_func(np.array([[42]])), 42)
with assert_warns(DeprecationWarning):
assert_equal(int_func(np.array([1])), 1)
with assert_warns(DeprecationWarning):
assert_equal(int_func(np.array([[42]])), 42)
assert_raises(TypeError, int_func, np.array([1, 2]))

# gh-9972
Expand All @@ -8772,7 +8779,8 @@ class HasTrunc:
def __trunc__(self):
return 3
assert_equal(3, int_func(np.array(HasTrunc())))
assert_equal(3, int_func(np.array([HasTrunc()])))
with assert_warns(DeprecationWarning):
assert_equal(3, int_func(np.array([HasTrunc()])))
else:
pass

Expand All @@ -8781,8 +8789,9 @@ def __int__(self):
raise NotImplementedError
assert_raises(NotImplementedError,
int_func, np.array(NotConvertible()))
assert_raises(NotImplementedError,
int_func, np.array([NotConvertible()]))
with assert_warns(DeprecationWarning):
assert_raises(NotImplementedError,
int_func, np.array([NotConvertible()]))


class TestWhere:
Expand Down

0 comments on commit 1acac89

Please sign in to comment.