From b0825b438237ee6eb7f1340e5615e036267f7a05 Mon Sep 17 00:00:00 2001 From: Stephan Hoyer Date: Thu, 27 Apr 2017 19:45:58 -0700 Subject: [PATCH 1/2] ENH: disable ufuncs if any operand sets __array_ufunc__=None --- numpy/core/src/multiarray/methods.c | 9 +++++-- numpy/core/src/private/ufunc_override.c | 33 +++++++++++++++++++++---- numpy/core/src/umath/override.c | 19 ++++++-------- numpy/core/tests/test_umath.py | 33 ++++++++++++++++++++++++- 4 files changed, 75 insertions(+), 19 deletions(-) diff --git a/numpy/core/src/multiarray/methods.c b/numpy/core/src/multiarray/methods.c index 946dc542f090..1a78b958c84a 100644 --- a/numpy/core/src/multiarray/methods.c +++ b/numpy/core/src/multiarray/methods.c @@ -1012,6 +1012,7 @@ array_ufunc(PyArrayObject *self, PyObject *args, PyObject *kwds) { PyObject *ufunc, *method_name, *normal_args, *ufunc_method; PyObject *result = NULL; + int num_override_args; if (PyTuple_Size(args) < 2) { PyErr_SetString(PyExc_TypeError, @@ -1023,7 +1024,11 @@ array_ufunc(PyArrayObject *self, PyObject *args, PyObject *kwds) return NULL; } /* ndarray cannot handle overrides itself */ - if (PyUFunc_WithOverride(normal_args, kwds, NULL)) { + num_override_args = PyUFunc_WithOverride(normal_args, kwds, NULL); + if (num_override_args == -1) { + return NULL; + } + if (num_override_args) { result = Py_NotImplemented; Py_INCREF(Py_NotImplemented); goto cleanup; @@ -2527,7 +2532,7 @@ NPY_NO_EXPORT PyMethodDef array_methods[] = { /* * While we could put these in `tp_sequence`, its' easier to define them * in terms of PyObject* arguments. - * + * * We must provide these for compatibility with code that calls them * directly. They are already deprecated at a language level in python 2.7, * but are removed outright in python 3. diff --git a/numpy/core/src/private/ufunc_override.c b/numpy/core/src/private/ufunc_override.c index b5cd46b898f5..c150c5021799 100644 --- a/numpy/core/src/private/ufunc_override.c +++ b/numpy/core/src/private/ufunc_override.c @@ -48,12 +48,28 @@ has_non_default_array_ufunc(PyObject *obj) return non_default; } +/* + * Check whether an object sets __array_ufunc__ = None. The __array_func__ + * attribute must already be known to exist. + */ +static int +disables_array_ufunc(PyObject *obj) +{ + PyObject *array_ufunc; + int disables; + + array_ufunc = PyObject_GetAttrString(obj, "__array_ufunc__"); + disables = (array_ufunc == Py_None); + Py_XDECREF(array_ufunc); + return disables; +} + /* * Check whether a set of input and output args have a non-default * `__array_ufunc__` method. Return the number of overrides, setting * corresponding objects in PyObject array with_override (if not NULL) * using borrowed references. - * + * * returns -1 on failure. */ NPY_NO_EXPORT int @@ -65,7 +81,7 @@ PyUFunc_WithOverride(PyObject *args, PyObject *kwds, int nargs; int nout_kwd = 0; int out_kwd_is_tuple = 0; - int noa = 0; /* Number of overriding args.*/ + int num_override_args = 0; PyObject *obj; PyObject *out_kwd_obj = NULL; @@ -117,13 +133,20 @@ PyUFunc_WithOverride(PyObject *args, PyObject *kwds, * any ndarray subclass instances that did not override __array_ufunc__. */ if (has_non_default_array_ufunc(obj)) { + if (disables_array_ufunc(obj)) { + PyErr_Format(PyExc_TypeError, + "operand '%.200s' does not support ufuncs " + "(__array_ufunc__=None)", + obj->ob_type->tp_name); + goto fail; + } if (with_override != NULL) { - with_override[noa] = obj; + with_override[num_override_args] = obj; } - ++noa; + ++num_override_args; } } - return noa; + return num_override_args; fail: return -1; diff --git a/numpy/core/src/umath/override.c b/numpy/core/src/umath/override.c index 1faf2568b4f1..c7490a043769 100644 --- a/numpy/core/src/umath/override.c +++ b/numpy/core/src/umath/override.c @@ -317,7 +317,7 @@ PyUFunc_CheckOverride(PyUFuncObject *ufunc, char *method, int j; int status; - int noa; + int num_override_args; PyObject *with_override[NPY_MAXARGS]; PyObject *obj; @@ -334,9 +334,12 @@ PyUFunc_CheckOverride(PyUFuncObject *ufunc, char *method, /* * Check inputs for overrides */ - noa = PyUFunc_WithOverride(args, kwds, with_override); + num_override_args = PyUFunc_WithOverride(args, kwds, with_override); + if (num_override_args == -1) { + goto fail; + } /* No overrides, bail out.*/ - if (noa == 0) { + if (num_override_args == 0) { *result = NULL; return 0; } @@ -496,7 +499,7 @@ PyUFunc_CheckOverride(PyUFuncObject *ufunc, char *method, *result = NULL; /* Choose an overriding argument */ - for (i = 0; i < noa; i++) { + for (i = 0; i < num_override_args; i++) { obj = with_override[i]; if (obj == NULL) { continue; @@ -506,7 +509,7 @@ PyUFunc_CheckOverride(PyUFuncObject *ufunc, char *method, override_obj = obj; /* Check for sub-types to the right of obj. */ - for (j = i + 1; j < noa; j++) { + for (j = i + 1; j < num_override_args; j++) { other_obj = with_override[j]; if (other_obj != NULL && PyObject_Type(other_obj) != PyObject_Type(obj) && @@ -552,12 +555,6 @@ PyUFunc_CheckOverride(PyUFuncObject *ufunc, char *method, goto fail; } - /* If None, try next one (i.e., as if it returned NotImplemented) */ - if (array_ufunc == Py_None) { - Py_DECREF(array_ufunc); - continue; - } - *result = PyObject_Call(array_ufunc, override_args, normal_kwds); Py_DECREF(array_ufunc); diff --git a/numpy/core/tests/test_umath.py b/numpy/core/tests/test_umath.py index 41108ab5f8d2..efe368775106 100644 --- a/numpy/core/tests/test_umath.py +++ b/numpy/core/tests/test_umath.py @@ -1901,7 +1901,8 @@ def __array_ufunc__(self, *a, **kwargs): def test_ufunc_override_not_implemented(self): class A(object): - __array_ufunc__ = None + def __array_ufunc__(self, *args, **kwargs): + return NotImplemented msg = ("operand type(s) do not implement __array_ufunc__(" ", '__call__', <*>): 'A'") @@ -1914,6 +1915,36 @@ class A(object): with assert_raises_regex(TypeError, fnmatch.translate(msg)): np.add(A(), object(), out=1) + def test_ufunc_override_disabled(self): + + class OptOut(object): + __array_ufunc__ = None + + opt_out = OptOut() + + # ufuncs always raise + msg = "operand 'OptOut' does not support ufuncs" + with assert_raises_regex(TypeError, msg): + np.add(opt_out, 1) + with assert_raises_regex(TypeError, msg): + np.add(1, opt_out) + with assert_raises_regex(TypeError, msg): + np.negative(opt_out) + + # opt-outs still hold even when other arguments have pathological + # __array_ufunc__ implementations + + class GreedyArray(object): + def __array_ufunc__(self, *args, **kwargs): + return self + + greedy = GreedyArray() + assert_(np.negative(greedy) is greedy) + with assert_raises_regex(TypeError, msg): + np.add(greedy, opt_out) + with assert_raises_regex(TypeError, msg): + np.add(greedy, 1, out=opt_out) + def test_gufunc_override(self): # gufunc are just ufunc instances, but follow a different path, # so check __array_ufunc__ overrides them properly. From 777534d5a6b2a8850dd8a6c52c17f94145f0ef03 Mon Sep 17 00:00:00 2001 From: Stephan Hoyer Date: Sat, 29 Apr 2017 21:19:21 -0700 Subject: [PATCH 2/2] DOC: update docs for __array_ufunc__ = None --- doc/source/reference/arrays.classes.rst | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/doc/source/reference/arrays.classes.rst b/doc/source/reference/arrays.classes.rst index 25105001ce68..2719f9239a53 100644 --- a/doc/source/reference/arrays.classes.rst +++ b/doc/source/reference/arrays.classes.rst @@ -81,14 +81,11 @@ NumPy provides several hooks that classes can customize: same may happen with functions such as :func:`~numpy.median`, :func:`~numpy.min`, and :func:`~numpy.argsort`. - Like with some other special methods in python, such as ``__hash__`` and ``__iter__``, it is possible to indicate that your class does *not* - support ufuncs by setting ``__array_ufunc__ = None``. With this, - inside ufuncs, your class will be treated as if it returned - :obj:`NotImplemented` (which will lead to an :exc:`TypeError` - unless another class also provides a :func:`__array_ufunc__` method - which knows what to do with your class). + support ufuncs by setting ``__array_ufunc__ = None``. Ufuncs always raise + :exc:`TypeError` when called on an object that sets + ``__array_ufunc__ = None``. The presence of :func:`__array_ufunc__` also influences how :class:`ndarray` handles binary operations like ``arr + obj`` and ``arr @@ -102,10 +99,9 @@ NumPy provides several hooks that classes can customize: Alternatively, if ``obj.__array_ufunc__`` is set to :obj:`None`, then as a special case, special methods like ``ndarray.__add__`` will notice this - and *unconditionally* return :obj:`NotImplemented`, so that Python will - dispatch to ``obj.__radd__`` instead. This is useful if you want to define - a special object that interacts with arrays via binary operations, but - is not itself an array. For example, a units handling system might have + and *unconditionally* raise :exc:`TypeError`. This is useful if you want to + create objects that interact with arrays via binary operations, but + are not themselves arrays. For example, a units handling system might have an object ``m`` representing the "meters" unit, and want to support the syntax ``arr * m`` to represent that the array has units of "meters", but not want to otherwise interact with arrays via ufuncs or otherwise. This