10000 np.array: call __array__ with copy keyword · numpy/numpy@786d2d8 · GitHub
[go: up one dir, main page]

Skip to content

Commit 786d2d8

Browse files
committed
np.array: call __array__ with copy keyword
1 parent 118f8e7 commit 786d2d8

33 files changed

+111
-91
lines changed

doc/source/reference/arrays.classes.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,7 @@ NumPy provides several hooks that classes can customize:
305305
.. note:: For ufuncs, it is hoped to eventually deprecate this method in
306306
favour of :func:`__array_ufunc__`.
307307

308-
.. py:method:: class.__array__([dtype])
308+
.. py:method:: class.__array__(dtype=None, copy=None)
309309
310310
If defined on an object, should return an ``ndarray``.
311311
This method is called by array-coercion functions like np.array()

doc/source/reference/c-api/array.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -515,7 +515,8 @@ From other objects
515515
516516
Return an ndarray object from a Python object that exposes the
517517
:obj:`~numpy.class.__array__` method. The :obj:`~numpy.class.__array__`
518-
method can take 0, or 1 argument ``([dtype])``. ``context`` is unused.
518+
method can take 0, 1 or 2 arguments ``(dtype=None, copy=None)``.
519+
``context`` is unused.
519520
520521
.. c:function:: PyObject* PyArray_ContiguousFromAny( \
521522
PyObject* op, int typenum, int min_depth, int max_depth)

doc/source/user/basics.dispatch.rst

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ example that has rather narrow utility but illustrates the concepts involved.
2222
... self._i = value
2323
... def __repr__(self):
2424
... return f"{self.__class__.__name__}(N={self._N}, value={self._i})"
25-
... def __array__(self, dtype=None):
25+
... def __array__(self, dtype=None, copy=None):
2626
... return self._i * np.eye(self._N, dtype=dtype)
2727

2828
Our custom array can be instantiated like:
@@ -84,7 +84,7 @@ For this example we will only handle the method ``__call__``
8484
... self._i = value
8585
... def __repr__(self):
8686
... return f"{self.__class__.__name__}(N={self._N}, value={self._i})"
87-
... def __array__(self, dtype=None):
87+
... def __array__(self, dtype=None, copy=None):
8888
... return self._i * np.eye(self._N, dtype=dtype)
8989
... def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
9090
... if method == '__call__':
@@ -135,7 +135,7 @@ conveniently by inheriting from the mixin
135135
... self._i = value
136136
... def __repr__(self):
137137
... return f"{self.__class__.__name__}(N={self._N}, value={self._i})"
138-
... def __array__(self, dtype=None):
138+
... def __array__(self, dtype=None, copy=None):
139139
... return self._i * np.eye(self._N, dtype=dtype)
140140
... def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
141< 10000 /code>141
... if method == '__call__':
@@ -173,7 +173,7 @@ functions to our custom variants.
173173
... self._i = value
174174
... def __repr__(self):
175175
... return f"{self.__class__.__name__}(N={self._N}, value={self._i})"
176-
... def __array__(self, dtype=None):
176+
... def __array__(self, dtype=None, copy=None):
177177
... return self._i * np.eye(self._N, dtype=dtype)
178178
... def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
179179
... if method == '__call__':
@@ -306,4 +306,3 @@ Refer to the `dask source code <https://github.com/dask/dask>`_ and
306306
examples of custom array containers.
307307

308308
See also :doc:`NEP 18<neps:nep-0018-array-function-protocol>`.
309-

doc/source/user/basics.interoperability.rst

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,8 @@ relies on the existence of the following attributes or methods:
7575
- ``__array_interface__``: a Python dictionary containing the shape, the
7676
element type, and optionally, the data buffer address and the strides of an
7777
array-like object;
78-
- ``__array__()``: a method returning the NumPy ndarray view of an array-like
79-
object;
78+
- ``__array__()``: a method returning the NumPy ndarray copy or a view of an
79+
array-like object;
8080

8181
The ``__array_interface__`` attribute can be inspected directly:
8282

@@ -125,6 +125,16 @@ new ndarray object. This is not optimal, as coercing arrays into ndarrays may
125125
cause performance problems or create the need for copies and loss of metadata,
126126
as the original object and any attributes/behavior it may have had, is lost.
127127

128+
The signature of the method should be ``__array__(self, dtype=None, copy=None)``.
129+
If a passed ``dtype`` isn't ``None`` and different than the object' 57AE s data type,
130+
a casting should happen to a specified type. If ``copy`` is ``None``, a copy
131+
should be made only if ``dtype`` argument enforces it. For ``copy=True``, a copy
132+
should always be made, where ``copy=False`` should raise an exception if a copy
133+
is needed.
134+
135+
If a class implements the old signature ``__array__(self)``, for ``np.array(a)``
136+
a warning will be raised saying that ``dtype`` and ``copy`` arguments are missing.
137+
128138
To see an example of a custom array implementation including the use of
129139
``__array__()``, see :ref:`basics.dispatch`.
130140

numpy/__init__.pyi

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1464,9 +1464,13 @@ class ndarray(_ArrayOrScalarCommon, Generic[_ShapeType, _DType_co]):
14641464
def __class_getitem__(self, item: Any) -> GenericAlias: ...
14651465

14661466
@overload
1467-
def __array__(self, dtype: None = ..., /) -> ndarray[Any, _DType_co]: ...
1467+
def __array__(
1468+
self, dtype: None = ..., /, *, copy: None | bool = ...
1469+
) -> ndarray[Any, _DType_co]: ...
14681470
@overload
1469-
def __array__(self, dtype: _DType, /) -> ndarray[Any, _DType]: ...
1471+
def __array__(
1472+
self, dtype: _DType, /, *, copy: None | bool = ...
1473+
) -> ndarray[Any, _DType]: ...
14701474

14711475
def __array_ufunc__(
14721476
self,
@@ -3715,9 +3719,9 @@ class poly1d:
37153719
__hash__: ClassVar[None] # type: ignore
37163720

37173721
@overload
3718-
def __array__(self, t: None = ...) -> NDArray[Any]: ...
3722+
def __array__(self, t: None = ..., copy: None | bool = ...) -> NDArray[Any]: ...
37193723
@overload
3720-
def __array__(self, t: _DType) -> ndarray[Any, _DType]: ...
3724+
def __array__(self, t: _DType, copy: None | bool = ...) -> ndarray[Any, _DType]: ...
37213725

37223726
@overload
37233727
def __call__(self, val: _ScalarLike_co) -> Any: ...

numpy/_core/defchararray.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1097,7 +1097,7 @@ class adds the following functionality:
10971097
10981098
copy : bool, optional
10991099
If true (default), then the object is copied. Otherwise, a copy
1100-
will only be made if __array__ returns a copy, if obj is a
1100+
will only be made if ``__array__`` returns a copy, if obj is a
11011101
nested sequence, or if a copy is needed to satisfy any of the other
11021102
requirements (`itemsize`, unicode, `order`, etc.).
11031103

numpy/_core/src/multiarray/ctors.c

Lines changed: 29 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1477,16 +1477,7 @@ _array_from_array_like(PyObject *op,
14771477
}
14781478
}
14791479

1480-
/*
1481-
* If op supplies the __array__ function.
1482-
* The documentation says this should produce a copy, so
1483-
* we skip this method if writeable is true, because the intent
1484-
* of writeable is to modify the operand.
1485-
* XXX: If the implementation is wrong, and/or if actual
1486-
* usage requires this behave differently,
1487-
* this should be changed!
1488-
*/
1489-
if (!writeable && tmp == Py_NotImplemented) {
1480+
if (tmp == Py_NotImplemented) {
14901481
tmp = PyArray_FromArrayAttr_int(op, requested_dtype, never_copy);
14911482
if (tmp == NULL) {
14921483
return NULL;
@@ -2418,9 +2409,8 @@ PyArray_FromInterface(PyObject *origin)
24182409
* @param descr The desired `arr.dtype`, passed into the `__array__` call,
24192410
* as information but is not checked/enforced!
24202411
* @param never_copy Specifies that a copy is not allowed.
2421-
* NOTE: Currently, this means an error is raised instead of calling
2422-
* `op.__array__()`. In the future we could call for example call
2423-
* `op.__array__(never_copy=True)` instead.
2412+
* NOTE: For false it passes `op.__array__(copy=None)`,
2413+
* for true: `op.__array__(copy=False)`.
24242414
* @returns NotImplemented if `__array__` is not defined or a NumPy array
24252415
* (or subclass). On error, return NULL.
24262416
*/
@@ -2438,15 +2428,6 @@ PyArray_FromArrayAttr_int(
24382428
}
24392429
return Py_NotImplemented;
24402430
}
2441-
if (never_copy) {
2442-
/* Currently, we must always assume that `__array__` returns a copy */
2443-
PyErr_SetString(PyExc_ValueError,
2444-
"Unable to avoid copy while converting from an object "
2445-
"implementing the `__array__` protocol. NumPy cannot ensure "
2446-
"that no copy will be made.");
2447-
Py_DECREF(array_meth);
2448-
return NULL;
2449-
}
24502431

24512432
if (PyType_Check(op) && PyObject_HasAttrString(array_meth, "__get__")) {
24522433
/*
@@ -2458,12 +2439,33 @@ PyArray_FromArrayAttr_int(
24582439
Py_DECREF(array_meth);
24592440
return Py_NotImplemented;
24602441
}
2461-
if (descr == NULL) {
2462-
new = PyObject_CallFunction(array_meth, NULL);
2463-
}
2464-
else {
2465-
new = PyObject_CallFunction(array_meth, "O", descr);
2442+
2443+
PyObject *copy = never_copy ? Py_False : Py_None;
2444+
PyObject *kwargs = PyDict_New();
2445+
PyDict_SetItemString(kwargs, "copy", copy);
2446+
PyObject *args = descr != NULL ? PyTuple_Pack(1, descr) : PyTuple_New(0);
2447+
2448+
new = PyObject_Call(array_meth, args, kwargs);
2449+
2450+
if (PyErr_Occurred()) {
2451+
PyObject *type, *value, *traceback;
2452+
PyErr_Fetch(&type, &value, &traceback);
2453+
if (PyUnicode_Check(value) && PyUnicode_CompareWithASCIIString(value,
2454+
"__array__() got an unexpected keyword argument 'copy'") == 0) {
2455+
Py_DECREF(type);
2456+
Py_XDECREF(value);
2457+
Py_XDECREF(traceback);
2458+
if (PyErr_WarnEx(PyExc_UserWarning,
2459+
"__array__ should implement 'dtype' and 'copy' keywords", 1) < 0) {
2460+
return NULL;
2461+
}
2462+
new = PyObject_Call(array_meth, args, NULL);
2463+
} else {
2464+
PyErr_Restore(type, value, traceback);
2465+
return NULL;
2466+
}
24662467
}
2468+
24672469
Py_DECREF(array_meth);
24682470
if (new == NULL) {
24692471
return NULL;

numpy/_core/src/multiarray/iterators.c

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1071,7 +1071,7 @@ static PyMappingMethods iter_as_mapping = {
10711071
* ignored.
10721072
*/
10731073
static PyArrayObject *
1074-
iter_array(PyArrayIterObject *it, PyObject *NPY_UNUSED(op))
1074+
iter_array(PyArrayIterObject *it, PyObject *NPY_UNUSED(args), PyObject *NPY_UNUSED(kwds))
10751075
{
10761076

10771077
PyArrayObject *ret;
@@ -1120,7 +1120,7 @@ static PyMethodDef iter_methods[] = {
11201120
/* to get array */
11211121
{"__array__",
11221122
(PyCFunction)iter_array,
1123-
METH_VARARGS, NULL},
1123+
METH_VARARGS | METH_KEYWORDS, NULL},
11241124
{"copy",
11251125
(PyCFunction)iter_copy,
11261126
METH_VARARGS, NULL},
@@ -1132,7 +1132,7 @@ iter_richcompare(PyArrayIterObject *self, PyObject *other, int cmp_op)
11321132
{
11331133
PyArrayObject *new;
11341134
PyObject *ret;
1135-
new = (PyArrayObject *)iter_array(self, NULL);
1135+
new = (PyArrayObject *)iter_array(self, NULL, NULL);
11361136
if (new == NULL) {
11371137
return NULL;
11381138
}

numpy/_core/src/multiarray/methods.c

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -928,7 +928,7 @@ array_getarray(PyArrayObject *self, PyObject *args, PyObject *kwds)
928928
{
929929
PyArray_Descr *newtype = NULL;
930930
NPY_COPYMODE copy = NPY_COPY_IF_NEEDED;
931-
static char *kwlist[] = {"", "copy", NULL};
931+
static char *kwlist[] = {"dtype", "copy", NULL};
932932
PyObject *ret;
933933

934934
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O&$O&:__array__", kwlist,
@@ -981,6 +981,7 @@ array_getarray(PyArrayObject *self, PyObject *args, PyObject *kwds)
981981
} else { // copy == NPY_COPY_NEVER
982982
PyErr_SetString(PyExc_ValueError,
983983
"Unable to avoid copy while creating an array from given array.");
984+
Py_DECREF(self);
984985
return NULL;
985986
}
986987
}
< B41A /td>

numpy/_core/src/multiarray/multiarraymodule.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1655,7 +1655,7 @@ _array_fromobject_generic(
16551655
if (copy == NPY_COPY_ALWAYS) {
16561656
flags = NPY_ARRAY_ENSURECOPY;
16571657
}
1658-
else if (copy == NPY_COPY_NEVER ) {
1658+
else if (copy == NPY_COPY_NEVER) {
16591659
flags = NPY_ARRAY_ENSURENOCOPY;
16601660
}
16611661
if (order == NPY_CORDER) {

numpy/_core/tests/test_api.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,10 @@ def test_array_array():
8787
assert_equal(bytes(np.array(o).data), bytes(a.data))
8888

8989
# test array
90-
o = type("o", (object,),
91-
dict(__array__=lambda *x: np.array(100.0, dtype=np.float64)))()
90+
def custom__array__(self, dtype=None, copy=None):
91+
return np.array(100.0, dtype=dtype, copy=copy)
92+
93+
o = type("o", (object,), dict(__array__=custom__array__))()
9294
assert_equal(np.array(o, dtype=np.float64), np.array(100.0, np.float64))
9395

9496
# test recursion

numpy/_core/tests/test_array_coercion.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ class ArrayDunder(_SequenceLike):
5353
def __init__(self, a):
5454
self.a = a
5555

56-
def __array__(self, dtype=None):
56+
def __array__(self, dtype=None, copy=None):
5757
return self.a
5858

5959
yield param(ArrayDunder, id="__array__")
@@ -706,7 +706,7 @@ def __array_interface__(self):
706706
def __array_struct__(self):
707707
pass
708708

709-
def __array__(self):
709+
def __array__(self, dtype=None, copy=None):
710710
pass
711711

712712
arr = np.array(ArrayLike)
@@ -832,7 +832,7 @@ class TestSpecialAttributeLookupFailure:
832832

833833
class WeirdArrayLike:
834834
@property
835-
def __array__(self):
835+
def __array__(self, dtype=None, copy=None):
836836
raise RuntimeError("oops!")
837837

838838
class WeirdArrayInterface:

numpy/_core/tests/test_deprecations.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -696,7 +696,7 @@ class TestDeprecatedArrayWrap(_DeprecationTestCase):
696696

697697
def test_deprecated(self):
698698
class Test1:
699-
def __array__(self,):
699+
def __array__(self, dtype=None, copy=None):
700700
return np.arange(4)
701701

702702
def __array_wrap__(self, arr, context=None):

numpy/_core/tests/test_indexing.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -420,7 +420,7 @@ def __index__(self):
420420

421421
class ArrayLike:
422422
# Simple array, should behave like the array
423-
def __array__(self):
423+
def __array__(self, dtype=None, copy=None):
424424
return np.array(0)
425425

426426
a = np.zeros(())

numpy/_core/tests/test_mem_overlap.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -553,7 +553,7 @@ class MyArray2:
553553
def __init__(self, data):
554554
self.data = data
555555

556-
def __array__(self):
556+
def __array__(self, dtype=None, copy=None):
557557
return self.data
558558

559559
for cls in [MyArray, MyArray2]:

numpy/_core/tests/test_multiarray.py

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -890,7 +890,7 @@ class TestCreation:
890890
"""
891891
def test_from_attribute(self):
892892
class x:
893-
def __array__(self, dtype=None):
893+
def __array__(self, dtype=None, copy=None):
894894
pass
895895

896896
assert_raises(ValueError, np.array, x())
@@ -6855,7 +6855,7 @@ def test_huge_vectordot(self, dtype):
68556855
def test_dtype_discovery_fails(self):
68566856
# See gh-14247, error checking was missing for failed dtype discovery
68576857
class BadObject(object):
6858-
def __array__(self):
6858+
def __array__(self, dtype=None, copy=None):
68596859
raise TypeError("just this tiny mint leaf")
68606860

68616861
with pytest.raises(TypeError):
@@ -8432,7 +8432,7 @@ def test___array__(self):
84328432
base_arr = np.arange(10)
84338433

84348434
class ArrayLike:
8435-
def __array__(self):
8435+
def __array__(self, dtype=None, copy=None):
84368436
# __array__ should return a copy, numpy cannot know this
84378437
# however.
84388438
return base_arr
@@ -8447,15 +8447,11 @@ def __array__(self):
84478447
# may be open for change:
84488448
assert res is not base_arr
84498449

8450-
for copy in self.if_needed_vals:
8450+
for copy in self.if_needed_vals + self.false_vals:
84518451
res = np.array(arr, copy=copy)
84528452
assert_array_equal(res, base_arr)
84538453
assert res is base_arr # numpy trusts the ArrayLike
84548454

8455-
for copy in self.false_vals:
8456-
with pytest.raises(ValueError):
8457-
np.array(arr, copy=copy)
8458-
84598455
def test___array__copy_arg(self):
84608456
a = np.ones((10, 10), dtype=int)
84618457

@@ -9709,7 +9705,7 @@ def test_no_loop_gives_all_true_or_false(dt1, dt2):
97099705
operator.gt])
97109706
def test_comparisons_forwards_error(op):
97119707
class NotArray:
9712-
def __array__(self):
9708+
def __array__(self, dtype=None, copy=None):
97139709
raise TypeError("run you fools")
97149710

97159711
with pytest.raises(TypeError, match="run you fools"):

numpy/_core/tests/test_overrides.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -712,7 +712,7 @@ def test_function_like():
712712
assert type(np.mean) is np._core._multiarray_umath._ArrayFunctionDispatcher
713713

714714
class MyClass:
715-
def __array__(self):
715+
def __array__(self, dtype=None, copy=None):
716716
# valid argument to mean:
717717
return np.arange(3)
718718

0 commit comments

Comments
 (0)
0