Skip to content

BUG: __array_ufunc__= None -> TypeError #9014

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 30, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 6 additions & 10 deletions doc/source/reference/arrays.classes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
9 changes: 7 additions & 2 deletions numpy/core/src/multiarray/methods.c
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
Expand Down Expand Up @@ -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.
Expand Down
33 changes: 28 additions & 5 deletions numpy/core/src/private/ufunc_override.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
19 changes: 8 additions & 11 deletions numpy/core/src/umath/override.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}
Expand Down Expand Up @@ -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;
Expand All @@ -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) &&
Expand Down Expand Up @@ -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);

Expand Down
33 changes: 32 additions & 1 deletion numpy/core/tests/test_umath.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__("
"<ufunc 'negative'>, '__call__', <*>): 'A'")
Expand All @@ -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.
Expand Down