From b3f432c87ef4215a8159f760d98e4eb438f6e2ee Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Tue, 2 Sep 2025 21:52:00 +0200 Subject: [PATCH 1/5] ENH: Improve performance of numpy scalar __copy__ and __deepcopy__ --- numpy/_core/src/multiarray/scalartypes.c.src | 29 ++++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/numpy/_core/src/multiarray/scalartypes.c.src b/numpy/_core/src/multiarray/scalartypes.c.src index f84c120ad409..3f25f5b51bec 100644 --- a/numpy/_core/src/multiarray/scalartypes.c.src +++ b/numpy/_core/src/multiarray/scalartypes.c.src @@ -2093,12 +2093,11 @@ gentype_wraparray(PyObject *NPY_UNUSED(scalar), PyObject *args) /* * These gentype_* functions do not take keyword arguments. - * The proper flag is METH_VARARGS. + * The proper flag is METH_VARARGS or METH_NOARGS. */ /**begin repeat * - * #name = tolist, item, __deepcopy__, __copy__, - * swapaxes, conj, conjugate, nonzero, + * #name = tolist, item, swapaxes, conj, conjugate, nonzero, * fill, transpose# */ static PyObject * @@ -2108,6 +2107,30 @@ gentype_@name@(PyObject *self, PyObject *args) } /**end repeat**/ +static PyObject * +gentype___copy__(PyObject *self) +{ + // scalars are immutable, so we can return a new reference + // the only expections are scalars with void dtype + if PyObject_IsInstance(obj, (PyObject *)&PyVoidScalarObject)) { + // path via array + return gentype_generic_method(self, NULL, NULL, "__copy__"); + } + return Py_NewRef(self); +} + +static PyObject * +gentype___deepcopy__(PyObject *self) +{ + // scalars are immutable, so we can return a new reference + // the only expections are scalars with void dtype + if PyObject_IsInstance(obj, (PyObject *)&PyVoidScalarObject)) { + // path via array + return gentype_generic_method(self, NULL, NULL, "__deepcopy__"); + } + return Py_NewRef(self); +} + static PyObject * gentype_byteswap(PyObject *self, PyObject *args, PyObject *kwds) { From 6b47b9871a4dbdd6c229f61d3ea553c4aed8303c Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Tue, 2 Sep 2025 22:45:25 +0200 Subject: [PATCH 2/5] correct signature and type check --- numpy/_core/src/multiarray/scalartypes.c.src | 14 ++++++++------ numpy/_core/tests/test_multiarray.py | 7 +++++++ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/numpy/_core/src/multiarray/scalartypes.c.src b/numpy/_core/src/multiarray/scalartypes.c.src index 3f25f5b51bec..6e4bb10ccef1 100644 --- a/numpy/_core/src/multiarray/scalartypes.c.src +++ b/numpy/_core/src/multiarray/scalartypes.c.src @@ -2108,25 +2108,27 @@ gentype_@name@(PyObject *self, PyObject *args) /**end repeat**/ static PyObject * -gentype___copy__(PyObject *self) +gentype___copy__(PyObject *self, PyObject *args) { // scalars are immutable, so we can return a new reference // the only expections are scalars with void dtype - if PyObject_IsInstance(obj, (PyObject *)&PyVoidScalarObject)) { + if (PyObject_IsInstance(self, (PyObject *)&PyVoidArrType_Type)) { // path via array - return gentype_generic_method(self, NULL, NULL, "__copy__"); + return gentype_generic_method(self, args, NULL, "__copy__"); } return Py_NewRef(self); } static PyObject * -gentype___deepcopy__(PyObject *self) +gentype___deepcopy__(PyObject *self, PyObject *args) { + // note: maybe the signature needs to be updated as __deepcopy__ can accept the keyword memo + // scalars are immutable, so we can return a new reference // the only expections are scalars with void dtype - if PyObject_IsInstance(obj, (PyObject *)&PyVoidScalarObject)) { + if (PyObject_IsInstance(self, (PyObject *)&PyVoidArrType_Type)) { // path via array - return gentype_generic_method(self, NULL, NULL, "__deepcopy__"); + return gentype_generic_method(self, args, NULL, "__deepcopy__"); } return Py_NewRef(self); } diff --git a/numpy/_core/tests/test_multiarray.py b/numpy/_core/tests/test_multiarray.py index d5aac78c4a4d..3286a5fbfae0 100644 --- a/numpy/_core/tests/test_multiarray.py +++ b/numpy/_core/tests/test_multiarray.py @@ -2470,6 +2470,13 @@ def test__deepcopy__(self, dtype): with pytest.raises(AssertionError): assert_array_equal(a, b) + def test__deepcopy___void_scalar(self): + # gh-xxxx + v = np.void('Rex', dtype=[('name', 'U10') ]) + w = v.__deepcopy__(None) + v[0]=None + assert w[0] == 'Rex' + def test__deepcopy__catches_failure(self): class MyObj: def __deepcopy__(self, *args, **kwargs): From 3ddd3b7a3b09f13f3a840cad176b0ae9ce129362 Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Tue, 2 Sep 2025 22:48:41 +0200 Subject: [PATCH 3/5] ruff --- numpy/_core/tests/test_multiarray.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/numpy/_core/tests/test_multiarray.py b/numpy/_core/tests/test_multiarray.py index 3286a5fbfae0..f7132fc03971 100644 --- a/numpy/_core/tests/test_multiarray.py +++ b/numpy/_core/tests/test_multiarray.py @@ -2472,9 +2472,9 @@ def test__deepcopy__(self, dtype): def test__deepcopy___void_scalar(self): # gh-xxxx - v = np.void('Rex', dtype=[('name', 'U10') ]) + v = np.void('Rex', dtype=[('name', 'U10')]) w = v.__deepcopy__(None) - v[0]=None + v[0] = None assert w[0] == 'Rex' def test__deepcopy__catches_failure(self): From 6300707b4c781c3c8ae49f7fbea5b7ab5c2f19d0 Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Wed, 3 Sep 2025 14:48:31 +0200 Subject: [PATCH 4/5] convert __copy__ to METH_NOARGS --- numpy/_core/src/multiarray/scalartypes.c.src | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/numpy/_core/src/multiarray/scalartypes.c.src b/numpy/_core/src/multiarray/scalartypes.c.src index 6e4bb10ccef1..a488e576db95 100644 --- a/numpy/_core/src/multiarray/scalartypes.c.src +++ b/numpy/_core/src/multiarray/scalartypes.c.src @@ -2108,13 +2108,13 @@ gentype_@name@(PyObject *self, PyObject *args) /**end repeat**/ static PyObject * -gentype___copy__(PyObject *self, PyObject *args) +gentype___copy__(PyObject *self) { // scalars are immutable, so we can return a new reference // the only expections are scalars with void dtype if (PyObject_IsInstance(self, (PyObject *)&PyVoidArrType_Type)) { // path via array - return gentype_generic_method(self, args, NULL, "__copy__"); + return gentype_generic_method(self, NULL, NULL, "__copy__"); } return Py_NewRef(self); } @@ -2677,7 +2677,7 @@ static PyMethodDef gentype_methods[] = { /* for the copy module */ {"__copy__", (PyCFunction)gentype___copy__, - METH_VARARGS, NULL}, + METH_NOARGS, NULL}, {"__deepcopy__", (PyCFunction)gentype___deepcopy__, METH_VARARGS, NULL}, From cf3531acb408d3110df812667cf0bc7333112901 Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Wed, 3 Sep 2025 14:53:52 +0200 Subject: [PATCH 5/5] handle unexpected number of arguments in __deepcopy__ --- numpy/_core/src/multiarray/scalartypes.c.src | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/numpy/_core/src/multiarray/scalartypes.c.src b/numpy/_core/src/multiarray/scalartypes.c.src index a488e576db95..ad49de0c231e 100644 --- a/numpy/_core/src/multiarray/scalartypes.c.src +++ b/numpy/_core/src/multiarray/scalartypes.c.src @@ -2126,7 +2126,9 @@ gentype___deepcopy__(PyObject *self, PyObject *args) // scalars are immutable, so we can return a new reference // the only expections are scalars with void dtype - if (PyObject_IsInstance(self, (PyObject *)&PyVoidArrType_Type)) { + // if the number of arguments is not 1, we let gentype_generic_method do the + // error handling + if (PyObject_IsInstance(self, (PyObject *)&PyVoidArrType_Type) || (PyTuple_Size(args)!=1)) { // path via array return gentype_generic_method(self, args, NULL, "__deepcopy__"); }