8000 ENH: Add `__array_ufunc__` by charris · Pull Request #8247 · numpy/numpy · GitHub
[go: up one dir, main page]

Skip to content

ENH: Add __array_ufunc__ #8247

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 43 commits into from
Apr 27, 2017
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
4fd7e84
ENH: Revert "Temporarily disable __numpy_ufunc__"
charris Nov 6, 2016
fcd11d2
ENH: Rename __numpy_ufunc__ to __array_ufunc__.
charris Nov 9, 2016
c7b25e2
ENH: Remove position arg from __array_ufunc__.
charris Nov 12, 2016
8a9e790
MAINT: Put PyArray_GetAttrString_SuppressException in get_attr_string.h
njsmith Jun 24, 2015
4dd5380
MAINT: dike out a bunch of weird old code implementing scalar power
njsmith Jun 24, 2015
7d9bc2f
BUG/ENH: Switch to simplified __array_ufunc__/binop interaction
njsmith Jun 22, 2015
e4b5163
MAINT: allow __array_ufunc__ = None to force binops to defer.
mhvk Mar 12, 2017
2e6d8c0
MAINT: Split out C code in ufunc_override.h to .c file.
mhvk Mar 15, 2017
d5c5ac1
MAINT: Add NPY_NO_EXPORT modifier to PyUFunc_CheckOverride.
charris Mar 16, 2017
3124e96
MAINT: for __array_ufunc__ pass inputs as *args, ensure out is tuple.
mhvk Mar 14, 2017
6a3ca31
DOC: describe current implementation of __array_ufunc__.
mhvk Mar 14, 2017
79bb733
DOC: Style and sphinx fixes for arrays.classes.rst.
charris Mar 23, 2017
7c3dc5a
TST: test that gufuncs are also overridden by __array_ufunc__.
mhvk Mar 25, 2017
71201d2
DOC: Describe __array_func__ in subclassing
mhvk Mar 15, 2017
3041710
ENH: implement ndarray.__array_ufunc__
mhvk Mar 13, 2017
5fe6fc6
DOC Update NEP to reflect actual implementation.
mhvk Mar 31, 2017
e092823
MAINT: let ndarray.__array_ufunc__ bail if any overrides are in place.
mhvk Apr 2, 2017
1147894
MAINT: Update array_ufunc NEP.
pv Apr 1, 2017
e325a10
DOC: Document behavior of ufuncs with default ndarray.__array_ufunc__
pv Apr 1, 2017
39c2273
DOC: Update ndarray.__array_ufunc__ documentation vs. review comments
pv Apr 2, 2017
6b41d11
DOC: clarify use of super and getattr
mhvk Apr 2, 2017
0ede0e9
DOC: update NEP again.
mhvk Apr 2, 2017
5f9252c
DOC: implement many smaller and bigger changes suggested in review.
mhvk Apr 4, 2017
8cc2f71
BUG,MAINT: ensure out=None is never passed on to __array_ufunc__.
mhvk Apr 4, 2017
856da73
DOC: remove left-over piece discussing binops
mhvk Apr 5, 2017
2b6c7fd
REVERT: remove __array_ufunc__ override for np.dot and ndarray.dot.
mhvk Apr 6, 2017
36e8494
REVERT: remove __array_ufunc__ override for np.matmul.
mhvk Apr 6, 2017
55500b9
MAINT: simplify now that __array_ufunc__ overrides ufuncs only.
mhvk Apr 6, 2017
25e973d
MAINT: split out umath-specific part of ufunc_override.
mhvk Apr 6, 2017
b1fa10a
BUG: ensure subclass of override class doesn't segfault.
mhvk Apr 8, 2017
1de8f5a
DOC: Mention `__array_ufunc__` in the 1.13.0 release notes.
charris Apr 8, 2017
a460015
DOC: ufunc-overrides: sync the discussion vs. current implementation
pv Apr 9, 2017
cd2e42c
DOC: ufunc-overrides: revise hierarchy discussion
pv Apr 9, 2017
ff628f1
BUG: Add back removed elision code.
charris Apr 9, 2017
1fc6e63
DOC,TST: clarify example of ndarray subclass using __array_ufunc__
mhvk Apr 10, 2017
a431743
BUG: Support nout == 0 and at method
eric-wieser Apr 12, 2017
1e460b7
DOC,MAINT: small corrections to NEP following Stephan's comments.
mhvk Apr 12, 2017
02600d3
ENH: Add NDArrayOperatorsMixin mixin class.
shoyer Apr 21, 2017
d3ff023
DOC: clarify recommendations for subclasses, deprecations.
mhvk Apr 21, 2017
b9359f1
MAINT: remove unnecessary checks, wrong code for 'outer', cleanup.
mhvk Apr 21, 2017
256a8ae
BUG: Fix ArrayLike(NDArrayOperatorsMixin) operations with object()
shoyer Apr 23, 2017
3272a86
ENH: Better error message for __array_ufunc__ not implemented
shoyer Apr 24, 2017
32221df
ENH: NDArrayOperatorsMixin calls ufuncs directly, like ndarray
shoyer Apr 27, 2017
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
Prev Previous commit
Next Next commit
DOC: Describe __array_func__ in subclassing
This includes the use of super everywhere, and in the brief
description of __array_ufunc__ in the reference section.
  • Loading branch information
mhvk authored and charris committed Apr 27, 2017
commit 71201d286c38157f25325096286b2bc6089fbf84
34 changes: 25 additions & 9 deletions doc/source/reference/arrays.classes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ NumPy provides several hooks that classes can customize:

.. versionadded:: 1.13

.. note:: The API is `provisional
<https://docs.python.org/3/glossary.html#term-provisional-api>`_,
i.e., we do not yet guarantee backward compatibility.

Any class, ndarray subclass or not, can define this method or set it to
:obj:`None` in order to override the behavior of NumPy's ufuncs. This works
quite similarly to Python's ``__mul__`` and other binary operation routines.
Expand Down Expand Up @@ -70,8 +74,11 @@ NumPy provides several hooks that classes can customize:
raised.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we only check for __array_ufunc__ on input arrays, not output arrays or where= arguments?


.. note:: In addition to ufuncs, :func:`__array_ufunc__` also
overrides the behavior of :func:`numpy.dot` and :func:`numpy.matmul`
even though these are not ufuncs.
overrides the behavior of :func:`numpy.dot` and :func:`numpy.matmul`.
This even though these are not ufuncs, but they can be thought of as
:ref:`generalized universal functions<c-api.generalized-ufuncs>`
(which are overridden). We intend to extend this behaviour to other
relevant functions.

Like with other methods in python, such as ``__hash__`` and
``__iter__``, it is possible to indicate that your class does *not*
Expand All @@ -92,17 +99,26 @@ NumPy provides several hooks that classes can customize:
:class:`ndarray` will unconditionally return :obj:`NotImplemented`,
so that your reverse methods will get called.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The use of the undefined term "forward methods" here is pretty confusing – maybe it should be something like:

The presence of __array_ufunc__ also influences how ndarray handles special binary operations like arr + obj and arr < obj when arr is an ndarray and obj is an instance of a custom class. There are two possibilities. If obj.__array_ufunc__ is present and not None, then ndarray.__add__ and friends will delegate to the ufunc machinery, meaning that arr + obj becomes np.add(arr, obj), and then np.add invokes __array_ufunc__. This is useful if you want to define an object that acts like an array. If your class defines __array_ufunc__, then for consistency it should also define special methods like __add__ and __lt__ that delegate to ufuncs just like ndarray does. [Do we have a helper for this?]

Alternatively, if obj.__array_ufunc__ is set to None, then as a special case, special methods like ndarray.__add__ will notice this and unconditionally return 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 isn't itself an array. 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 can be done by setting __array_ufunc__ = None and defining __mul__ and __rmul__ methods. (Note that this means that writing an __array_ufunc__ that always returns NotImplemented is not quite the same as setting __array_ufunc__ = None: in the former case, arr + obj will raise TypeError, while in the latter case it's possible to define a __radd__ method to prevent this.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should also describe how in-place operators like += are handled.


.. note:: If you subclass :class:`ndarray`, we strongly recommend
that you avoid confusion by neither setting
:func:`__array_ufunc__` to :obj:`None`, which makes no
sense for an array subclass, nor by defining it and also defining
reverse methods, which methods will be called by ``CPython`` in
preference over the :class:`ndarray` forward methods.
.. note:: If you subclass :class:`ndarray`:

- We strongly recommend that you avoid confusion by neither setting
:func:`__array_ufunc__` to :obj:`None`, which makes no sense for
an array subclass, nor by defining it and also defining reverse
methods, which methods will be called by ``CPython`` in
preference over the :class:`ndarray` forward methods.
- :class:`ndarray` defines its own :func:`__array_ufunc__`, which
corresponds to ``getattr(ufunc, method)(*inputs, **kwargs)``. Hence,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems really wrong? ndarray.__array_ufunc__ should return NotImplemented if any of its arguments are unrecognized (which I guess means "define their own __array_ufunc__ or something? it's pretty subtle but it's definitely not the same as just calling the ufunc again).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(If ndarray.__array_ufunc__ is tricky then it might make sense to defer it to a second PR.)

a typical override of :func:`__array_ufunc__` would convert any
instances of one's own class, pass these on to its superclass using
``super().__array_ufunc__(*inputs, **kwargs)``, and finally return
the results after possible back-conversion. This practice ensures
that it is possible to have a hierarchy of subclasses. See
:ref:`Subclassing ndarray <basics.subclassing>` for details.

.. note:: If a class defines the :func:`__array_ufunc__` method,
this disables the :func:`__array_wrap__`,
:func:`__array_prepare__`, :data:`__array_priority__` mechanism
described below (which may eventually be deprecated).
described below for ufuncs (which may eventually be deprecated).

.. py:method:: class.__array_finalize__(obj)

Expand Down
181 changes: 148 additions & 33 deletions numpy/doc/subclassing.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
"""
=============================
"""=============================
Subclassing ndarray in python
=============================

Expand Down Expand Up @@ -220,8 +219,9 @@ class other than the class in which it is defined, the ``__init__``
* For the explicit constructor call, our subclass will need to create a
new ndarray instance of its own class. In practice this means that
we, the authors of the code, will need to make a call to
``ndarray.__new__(MySubClass,...)``, or do view casting of an existing
array (see below)
``ndarray.__new__(MySubClass,...)``, a class-hierarchy prepared call to
``super(MySubClass, cls).__new__(cls, ...)``, or do view casting of an
existing array (see below)
* For view casting and new-from-template, the equivalent of
``ndarray.__new__(MySubClass,...`` is called, at the C level.

Expand All @@ -237,7 +237,7 @@ class other than the class in which it is defined, the ``__init__``
class C(np.ndarray):
def __new__(cls, *args, **kwargs):
print('In __new__ with class %s' % cls)
return np.ndarray.__new__(cls, *args, **kwargs)
return super(C, cls).__new__(cls, *args, **kwargs)

def __init__(self, *args, **kwargs):
# in practice you probably will not need or want an __init__
Expand Down Expand Up @@ -275,7 +275,8 @@ def __array_finalize__(self, obj):

def __array_finalize__(self, obj):

``ndarray.__new__`` passes ``__array_finalize__`` the new object, of our
One sees that the ``super`` call, which goes to
``ndarray.__new__``, passes ``__array_finalize__`` the new object, of our
own class (``self``) as well as the object from which the view has been
taken (``obj``). As you can see from the output above, the ``self`` is
always a newly created instance of our subclass, and the type of ``obj``
Expand Down Expand Up @@ -303,13 +304,14 @@ def __array_finalize__(self, obj):
class InfoArray(np.ndarray):

def __new__(subtype, shape, dtype=float, buffer=None, offset=0,
strides=None, order=None, info=None):
strides=None, order=None, info=None):
# Create the ndarray instance of our type, given the usual
# ndarray input arguments. This will call the standard
# ndarray constructor, but return an object of our type.
# It also triggers a call to InfoArray.__array_finalize__
obj = np.ndarray.__new__(subtype, shape, dtype, buffer, offset, strides,
order)
obj = super(InfoArray, subtype).__new__(subtype, shape, dtype,
buffer, offset, strides,
order)
# set the new 'info' attribute to the value passed
obj.info = info
# Finally, we must return the newly created object:
Expand Down Expand Up @@ -412,15 +414,132 @@ def __array_finalize__(self, obj):
>>> v.info
'information'

.. _array-wrap:
.. _array-ufunc:

``__array_ufunc__`` for ufuncs
------------------------------

.. versionadded:: 1.13

A subclass can override what happens when executing numpy ufuncs on it by
overriding the default ``ndarray.__array_ufunc__`` method. This method is
executed *instead* of the ufunc and should return either the result of the
operation, or :obj:`NotImplemented` if the operation requested is not
implemented.

The signature of ``__array_ufunc__`` is::

def __array_ufunc__(ufunc, method, *inputs, **kwargs):

``__array_wrap__`` for ufuncs
-------------------------------------------------------
- *ufunc* is the ufunc object that was called.
- *method* is a string indicating which Ufunc method was called
(one of ``"__call__"``, ``"reduce"``, ``"reduceat"``,
``"accumulate"``, ``"outer"``, ``"inner"``).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

similar comment here, what about "at" and is "inner" a ufunc method?

- *inputs* is a tuple of the input arguments to the ``ufunc``.
- *kwargs* is a dictionary containing the optional input arguments
of the ufunc. If given, any ``out`` arguments, both positional
and keyword, are passed as a :obj:`tuple` in *kwargs*.

``__array_wrap__`` gets called at the end of numpy ufuncs and other numpy
functions, to allow a subclass to set the type of the return value
and update attributes and metadata. Let's show how this works with an example.
First we make the same subclass as above, but with a different name and
A typical implementation would convert any inputs or ouputs that are
instances of one's own class, pass everything on to a superclass using
``super()``, and finally return the results after possible
back-conversion. An example, taken from the test case
``test_ufunc_override_with_super`` in ``core/tests/test_umath.pu``, is the
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be test_umath.py

following.

.. testcode::

input numpy as np

class A(np.ndarray):
def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
args = []
in_no = []
for i, input_ in enumerate(inputs):
if isinstance(input_, A):
in_no.append(i)
args.append(input_.view(np.ndarray))
else:
args.append(input_)

outputs = kwargs.pop('out', [])
Copy link
Member
@shoyer shoyer Apr 3, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default here is wrong: out=[] implies the ufunc itself has no output arguments, which is different from the ufunc being called without setting out explicitly. (None is probably a safer default value.)

out_no = []
if outputs:
out_args = []
for j, output in enumerate(outputs):
if isinstance(output, A):
out_no.append(j)
out_args.append(output.view(np.ndarray))
else:
out_args.append(output)
kwargs['out'] = tuple(out_args)

info = {key: no for (key, no) in (('inputs', in_no),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic is a little hard to unpack. How about info = {'inputs': in_no, 'outputs': out_no} instead? If you insist on the current behavior, I would even prefer:

info = {}
if in_no:
    info['inputs'] = in_op
...

('outputs', out_no))
if no != []}

results = super(A, self).__array_ufunc__(ufunc, method,
Copy link
Member
@eric-wieser eric-wieser Apr 5, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If out is always a tuple, then maybe the contract of __array_ufunc__ should be to always return one too?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, that might work; so, in that case it would fall to PyUFunc_CheckOverride to normalize it. Should we just unpack a 1-item tuple anyway, even without explicitly insisting that the output is a tuple? (which would mean that if one wanted to return a 1-item tuple, it would have to be packed inside another one)

Copy link
Member
@eric-wieser eric-wieser Apr 5, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Obviously the 1-tuple should still be unpacked at the top level for calls like np.add(a, b), but I think it's a lot easier to work with array_ufunc if the model is (crossed out remarks are our current model):

  • You get a tuple of inputs (*args, len(args) == ufunc.nin)
  • You get a tuple of preallocated outputs (or None when that output is not preallocated) (out, len(out) == ufunc.nout), unless they are all None, and then you don't get anything at all
  • You are expected to produce a tuple of final outputs (results, len(results) == ufunc.nout), unless there is only one output, then you are expected to produce that alone

Crossing out the requirements as I have above is a change that affects two groups of people:

  • People who care about multi-output ufuncs — these people no longer have to special-case multi-result ufuncs
  • People who don't care about multi-output ufuncs — these people have to write a little more code, but are prevented from writing code that crashes on multi-result ufuncs

This change makes it easier to do complex things, and harder to do incorrect things - which sounds like a win to me. It should take more code to handle only a special case, not less

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You get a tuple of preallocated outputs (out, len(out) == ufunc.nout)

Just to be clear, we can pass in None for unknown outputs, but we can't preallocate them (as numpy arrays). That wouldn't work for some override cases (e.g., for lazy arrays).

Copy link
Member
@eric-wieser eric-wieser Apr 5, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shoyer: Right, I forgot to mention that. Updated

*args, **kwargs)
if not isinstance(results, tuple):
if not isinstance(results, np.ndarray):
return results
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is wrong, and fails for duck types. I think the test should actually just be if ufunc.nout == 1

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

results = (results,)

if outputs == []:
outputs = [None] * len(results)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An argument for why normalizing kwargs['out'] to (none)*nout is a good idea...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's handy for one class is unhandy for another....

results = tuple(result.view(A) if output is None else output
for result, output in zip(results, outputs))
if isinstance(results[0], A):
results[0].info = info

return results[0] if len(results) == 1 else results

So, this class does not actually do anything interesting: it just
converts any instances of its own to regular ndarray (otherwise, we'd
get infinite recursion!), and adds an ``info`` dictionary that tells
which inputs and outputs it converted. Hence, e.g.,

>>> a = np.arange(5.).view(A)
>>> b = np.sin(a)
>>> b.info
{'inputs': [0]}
>>> b = np.sin(np.arange(5.), out=(a,))
>>> b.info
{'outputs': [0]}
>>> a = np.arange(5.).view(A)
>>> b = np.ones(1).view(A)
>>> a += b
>>> a.info
{'inputs': [0, 1], 'outputs': [0]}

Note that one might also consider just doing ``getattr(ufunc,
methods)(*inputs, **kwargs)`` instead of the ``super`` call. This would
work (indeed, ``ndarray.__array_ufunc__`` effectively does just that), but
by using ``super`` one can more easily have a class hierarchy. E.g.,
suppose we had another class ``B`` that defined ``__array_ufunc__`` and
then made a subclass ``C`` depending on both, i.e., ``class C(A, B)``
without yet another ``__array_ufunc__`` override. Then any ufunc on an
instance of ``C`` would pass on to ``A.__array_ufunc__``, the ``super``
call in ``A`` would go to ``B.__array_ufunc__``, and the ``super`` call in
``B`` would go to ``ndarray.__array_ufunc__``.

.. _array-wrap:

``__array_wrap__`` for ufuncs and other functions
-------------------------------------------------

Prior to numpy 1.13, the behaviour of ufuncs could be tuned using
``__array_wrap__`` and ``__array_prepare__``. These two allowed one to
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes it sound like these methods were removed in 1.13

change the output type of a ufunc, but, in constrast to
``__array_ufunc__``, did not allow one to make any changes to the inputs.
It is hoped to eventually deprecate these, but ``__array_wrap__`` is also
used by other numpy functions and methods, such as ``squeeze``, so at the
present time is still needed for full functionality.

Conceptually, ``__array_wrap__`` "wraps up the action" in the sense of
allowing a subclass to set the type of the return value and update
attributes and metadata. Let's show how this works with an example. First
we return to the simpler example subclass, but with a different name and
some print statements:

.. testcode::
Expand All @@ -446,7 +565,7 @@ def __array_wrap__(self, out_arr, context=None):
print(' self is %s' % repr(self))
print(' arr is %s' % repr(out_arr))
# then just call the parent
return np.ndarray.__array_wrap__(self, out_arr, context)
return super(MySubClass, self).__array_wrap__(self, out_arr, context)

We run a ufunc on an instance of our new a 4D1F rray:

Expand All @@ -467,13 +586,12 @@ def __array_wrap__(self, out_arr, context=None):
>>> ret.info
'spam'

Note that the ufunc (``np.add``) has called the ``__array_wrap__`` method of the
input with the highest ``__array_priority__`` value, in this case
``MySubClass.__array_wrap__``, with arguments ``self`` as ``obj``, and
``out_arr`` as the (ndarray) result of the addition. In turn, the
default ``__array_wrap__`` (``ndarray.__array_wrap__``) has cast the
result to class ``MySubClass``, and called ``__array_finalize__`` -
hence the copying of the ``info`` attribute. This has all happened at the C level.
Note that the ufunc (``np.add``) has called the ``__array_wrap__`` method
with arguments ``self`` as ``obj``, and ``out_arr`` as the (ndarray) result
of the addition. In turn, the default ``__array_wrap__``
(``ndarray.__array_wrap__``) has cast the result to class ``MySubClass``,
and called ``__array_finalize__`` - hence the copying of the ``info``
attribute. This has all happened at the C level.

But, we could do anything we wanted:

Expand All @@ -494,11 +612,12 @@ def __array_wrap__(self, arr, context=None):
So, by defining a specific ``__array_wrap__`` method for our subclass,
we can tweak the output from ufuncs. The ``__array_wrap__`` method
requires ``self``, then an argument - which is the result of the ufunc -
and an optional parameter *context*. This parameter is returned by some
ufuncs as a 3-element tuple: (name of the ufunc, argument of the ufunc,
domain of the ufunc). ``__array_wrap__`` should return an instance of
its containing class. See the masked array subclass for an
implementation.
and an optional parameter *context*. This parameter is returned by
ufuncs as a 3-element tuple: (name of the ufunc, arguments of the ufunc,
domain of the ufunc), but is not set by other numpy functions. Though,
as seen above, it is possible to do otherwise, ``__array_wrap__`` should
return an instance of its containing class. See the masked array
subclass for an implementation.

In addition to ``__array_wrap__``, which is called on the way out of the
ufunc, there is also an ``__array_prepare__`` method which is called on
Expand All @@ -511,10 +630,6 @@ def __array_wrap__(self, arr, context=None):
Like ``__array_wrap__``, ``__array_prepare__`` must return an ndarray or
subclass thereof or raise an error.

.. note:: As of numpy 1.13, there also is a new, more powerful method to
handle how a subclass deals with ufuncs, ``__array_ufunc__``. For details,
see the reference section.

Extra gotchas - custom ``__del__`` methods and ndarray.base
-----------------------------------------------------------

Expand Down
0