<div dir="auto">After much discussion (and the addition of three new co-authors!), I’m pleased to present a significantly revision of NumPy Enhancement Proposal 18: A dispatch mechanism for NumPy's high level array functions:</div><div dir="auto"><a href="http://www.numpy.org/neps/nep-0018-array-function-protocol.html">http://www.numpy.org/neps/nep-0018-array-function-protocol.html</a></div><div dir="auto"><br></div><div dir="auto">The full text is also included below.</div><div dir="auto"><br></div><div dir="auto">Best,</div><div dir="auto">Stephan</div><div dir="auto"><br></div><div dir="auto">===========================================================</div><div dir="auto">A dispatch mechanism for NumPy's high level array functions</div><div dir="auto">===========================================================</div><div dir="auto"><br></div><div dir="auto">:Author: Stephan Hoyer <<a href="mailto:shoyer@google.com">shoyer@google.com</a>></div><div dir="auto">:Author: Matthew Rocklin <<a href="mailto:mrocklin@gmail.com">mrocklin@gmail.com</a>></div><div dir="auto">:Author: Marten van Kerkwijk <<a href="mailto:mhvk@astro.utoronto.ca">mhvk@astro.utoronto.ca</a>></div><div dir="auto">:Author: Hameer Abbasi <<a href="mailto:hameerabbasi@yahoo.com">hameerabbasi@yahoo.com</a>></div><div dir="auto">:Author: Eric Wieser <<a href="mailto:wieser.eric@gmail.com">wieser.eric@gmail.com</a>></div><div dir="auto">:Status: Draft</div><div dir="auto">:Type: Standards Track</div><div dir="auto">:Created: 2018-05-29</div><div dir="auto"><br></div><div dir="auto">Abstact</div><div dir="auto">-------</div><div dir="auto"><br></div><div dir="auto">We propose the ``__array_function__`` protocol, to allow arguments of NumPy</div><div dir="auto">functions to define how that function operates on them. This will allow</div><div dir="auto">using NumPy as a high level API for efficient multi-dimensional array</div><div dir="auto">operations, even with array implementations that differ greatly from</div><div dir="auto">``numpy.ndarray``.</div><div dir="auto"><br></div><div dir="auto">Detailed description</div><div dir="auto">--------------------</div><div dir="auto"><br></div><div dir="auto">NumPy's high level ndarray API has been implemented several times</div><div dir="auto">outside of NumPy itself for different architectures, such as for GPU</div><div dir="auto">arrays (CuPy), Sparse arrays (scipy.sparse, pydata/sparse) and parallel</div><div dir="auto">arrays (Dask array) as well as various NumPy-like implementations in the</div><div dir="auto">deep learning frameworks, like TensorFlow and PyTorch.</div><div dir="auto"><br></div><div dir="auto">Similarly there are many projects that build on top of the NumPy API</div><div dir="auto">for labeled and indexed arrays (XArray), automatic differentiation</div><div dir="auto">(Autograd, Tangent), masked arrays (<a href="http://numpy.ma">numpy.ma</a>), physical units (astropy.units,</div><div dir="auto">pint, unyt), etc. that add additional functionality on top of the NumPy API.</div><div dir="auto">Most of these project also implement a close variation of NumPy's level high</div><div dir="auto">API.</div><div dir="auto"><br></div><div dir="auto">We would like to be able to use these libraries together, for example we</div><div dir="auto">would like to be able to place a CuPy array within XArray, or perform</div><div dir="auto">automatic differentiation on Dask array code. This would be easier to</div><div dir="auto">accomplish if code written for NumPy ndarrays could also be used by</div><div dir="auto">other NumPy-like projects.</div><div dir="auto"><br></div><div dir="auto">For example, we would like for the following code example to work</div><div dir="auto">equally well with any NumPy-like array object:</div><div dir="auto"><br></div><div dir="auto">.. code:: python</div><div dir="auto"><br></div><div dir="auto">    def f(x):</div><div dir="auto">        y = np.tensordot(x, x.T)</div><div dir="auto">        return np.mean(np.exp(y))</div><div dir="auto"><br></div><div dir="auto">Some of this is possible today with various protocol mechanisms within</div><div dir="auto">NumPy.</div><div dir="auto"><br></div><div dir="auto">-  The ``np.exp`` function checks the ``__array_ufunc__`` protocol</div><div dir="auto">-  The ``.T`` method works using Python's method dispatch</div><div dir="auto">-  The ``np.mean`` function explicitly checks for a ``.mean`` method on</div><div dir="auto">   the argument</div><div dir="auto"><br></div><div dir="auto">However other functions, like ``np.tensordot`` do not dispatch, and</div><div dir="auto">instead are likely to coerce to a NumPy array (using the ``__array__``)</div><div dir="auto">protocol, or err outright. To achieve enough coverage of the NumPy API</div><div dir="auto">to support downstream projects like XArray and autograd we want to</div><div dir="auto">support *almost all* functions within NumPy, which calls for a more</div><div dir="auto">reaching protocol than just ``__array_ufunc__``. We would like a</div><div dir="auto">protocol that allows arguments of a NumPy function to take control and</div><div dir="auto">divert execution to another function (for example a GPU or parallel</div><div dir="auto">implementation) in a way that is safe and consistent across projects.</div><div dir="auto"><br></div><div dir="auto">Implementation</div><div dir="auto">--------------</div><div dir="auto"><br></div><div dir="auto">We propose adding support for a new protocol in NumPy,</div><div dir="auto">``__array_function__``.</div><div dir="auto"><br></div><div dir="auto">This protocol is intended to be a catch-all for NumPy functionality that</div><div dir="auto">is not covered by the ``__array_ufunc__`` protocol for universal functions</div><div dir="auto">(like ``np.exp``). The semantics are very similar to ``__array_ufunc__``, except</div><div dir="auto">the operation is specified by an arbitrary callable object rather than a ufunc</div><div dir="auto">instance and method.</div><div dir="auto"><br></div><div dir="auto">A prototype implementation can be found in</div><div dir="auto">`this notebook <<a href="https://nbviewer.jupyter.org/gist/shoyer/1f0a308a06cd96df20879a1ddb8f0006">https://nbviewer.jupyter.org/gist/shoyer/1f0a308a06cd96df20879a1ddb8f0006</a>>`_.</div><div dir="auto"><br></div><div dir="auto">The interface</div><div dir="auto">~~~~~~~~~~~~~</div><div dir="auto"><br></div><div dir="auto">We propose the following signature for implementations of</div><div dir="auto">``__array_function__``:</div><div dir="auto"><br></div><div dir="auto">.. code-block:: python</div><div dir="auto"><br></div><div dir="auto">    def __array_function__(self, func, types, args, kwargs)</div><div dir="auto"><br></div><div dir="auto">-  ``func`` is an arbitrary callable exposed by NumPy's public API,</div><div dir="auto">   which was called in the form ``func(*args, **kwargs)``.</div><div dir="auto">-  ``types`` is a ``frozenset`` of unique argument types from the original NumPy</div><div dir="auto">   function call that implement ``__array_function__``.</div><div dir="auto">-  The tuple ``args`` and dict ``kwargs`` are directly passed on from the</div><div dir="auto">   original call.</div><div dir="auto"><br></div><div dir="auto">Unlike ``__array_ufunc__``, there are no high-level guarantees about the</div><div dir="auto">type of ``func``, or about which of ``args`` and ``kwargs`` may contain objects</div><div dir="auto">implementing the array API.</div><div dir="auto"><br></div><div dir="auto">As a convenience for ``__array_function__`` implementors, ``types`` provides all</div><div dir="auto">argument types with an ``'__array_function__'`` attribute. This</div><div dir="auto">allows downstream implementations to quickly determine if they are likely able</div><div dir="auto">to support the operation. A ``frozenset`` is used to ensure that</div><div dir="auto">``__array_function__`` implementations cannot rely on the iteration order of</div><div dir="auto">``types``, which would facilitate violating the well-defined "Type casting</div><div dir="auto">hierarchy" described in</div><div dir="auto">`NEP-13 <<a href="https://www.numpy.org/neps/nep-0013-ufunc-overrides.html">https://www.numpy.org/neps/nep-0013-ufunc-overrides.html</a>>`_.</div><div dir="auto"><br></div><div dir="auto">Example for a project implementing the NumPy API</div><div dir="auto">~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~</div><div dir="auto"><br></div><div dir="auto">Most implementations of ``__array_function__`` will start with two</div><div dir="auto">checks:</div><div dir="auto"><br></div><div dir="auto">1.  Is the given function something that we know how to overload?</div><div dir="auto">2.  Are all arguments of a type that we know how to handle?</div><div dir="auto"><br></div><div dir="auto">If these conditions hold, ``__array_function__`` should return</div><div dir="auto">the result from calling its implementation for ``func(*args, **kwargs)``.</div><div dir="auto">Otherwise, it should return the sentinel value ``NotImplemented``, indicating</div><div dir="auto">that the function is not implemented by these types. This is preferable to</div><div dir="auto">raising ``TypeError`` directly, because it gives *other* arguments the</div><div dir="auto">opportunity to define the operations.</div><div dir="auto"><br></div><div dir="auto">There are no general requirements on the return value from</div><div dir="auto">``__array_function__``, although most sensible implementations should probably</div><div dir="auto">return array(s) with the same type as one of the function's arguments.</div><div dir="auto">If/when Python gains</div><div dir="auto">`typing support for protocols <<a href="https://www.python.org/dev/peps/pep-0544/">https://www.python.org/dev/peps/pep-0544/</a>>`_</div><div dir="auto">and NumPy adds static type annotations, the ``@overload`` implementation</div><div dir="auto">for ``SupportsArrayFunction`` will indicate a return type of ``Any``.</div><div dir="auto"><br></div><div dir="auto">It may also be convenient to define a custom decorators (``implements`` below)</div><div dir="auto">for registering ``__array_function__`` implementations.</div><div dir="auto"><br></div><div dir="auto">.. code:: python</div><div dir="auto"><br></div><div dir="auto">    HANDLED_FUNCTIONS = {}</div><div dir="auto"><br></div><div dir="auto">    class MyArray:</div><div dir="auto">        def __array_function__(self, func, types, args, kwargs):</div><div dir="auto">            if func not in HANDLED_FUNCTIONS:</div><div dir="auto">                return NotImplemented</div><div dir="auto">            # Note: this allows subclasses that don't override</div><div dir="auto">            # __array_function__ to handle MyArray objects</div><div dir="auto">            if not all(issubclass(t, MyArray) for t in types):</div><div dir="auto">                return NotImplemented</div><div dir="auto">            return HANDLED_FUNCTIONS[func](*args, **kwargs)</div><div dir="auto"><br></div><div dir="auto">    def implements(numpy_function):</div><div dir="auto">        """Register an __array_function__ implementation for MyArray objects."""</div><div dir="auto">        def decorator(func):</div><div dir="auto">            HANDLED_FUNCTIONS[numpy_function] = func</div><div dir="auto">            return func</div><div dir="auto">        return decorator</div><div dir="auto"><br></div><div dir="auto">    @implements(np.concatenate)</div><div dir="auto">    def concatenate(arrays, axis=0, out=None):</div><div dir="auto">        ...  # implementation of concatenate for MyArray objects</div><div dir="auto"><br></div><div dir="auto">    @implements(np.broadcast_to)</div><div dir="auto">    def broadcast_to(array, shape):</div><div dir="auto">        ...  # implementation of broadcast_to for MyArray objects</div><div dir="auto"><br></div><div dir="auto">Note that it is not required for ``__array_function__`` implementations to</div><div dir="auto">include *all* of the corresponding NumPy function's optional arguments</div><div dir="auto">(e.g., ``broadcast_to`` above omits the irrelevant ``subok`` argument).</div><div dir="auto">Optional arguments are only passed in to ``__array_function__`` if they</div><div dir="auto">were explicitly used in the NumPy function call.</div><div dir="auto"><br></div><div dir="auto">Necessary changes within the NumPy codebase itself</div><div dir="auto">~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~</div><div dir="auto"><br></div><div dir="auto">This will require two changes within the NumPy codebase:</div><div dir="auto"><br></div><div dir="auto">1. A function to inspect available inputs, look for the</div><div dir="auto">   ``__array_function__`` attribute on those inputs, and call those</div><div dir="auto">   methods appropriately until one succeeds.  This needs to be fast in the</div><div dir="auto">   common all-NumPy case, and have acceptable performance (no worse than</div><div dir="auto">   linear time) even if the number of overloaded inputs is large (e.g.,</div><div dir="auto">   as might be the case for `np.concatenate`).</div><div dir="auto"><br></div><div dir="auto">   This is one additional function of moderate complexity.</div><div dir="auto">2. Calling this function within all relevant NumPy functions.</div><div dir="auto"><br></div><div dir="auto">   This affects many parts of the NumPy codebase, although with very low</div><div dir="auto">   complexity.</div><div dir="auto"><br></div><div dir="auto">Finding and calling the right ``__array_function__``</div><div dir="auto">^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^</div><div dir="auto"><br></div><div dir="auto">Given a NumPy function, ``*args`` and ``**kwargs`` inputs, we need to</div><div dir="auto">search through ``*args`` and ``**kwargs`` for all appropriate inputs</div><div dir="auto">that might have the ``__array_function__`` attribute. Then we need to</div><div dir="auto">select among those possible methods and execute the right one.</div><div dir="auto">Negotiating between several possible implementations can be complex.</div><div dir="auto"><br></div><div dir="auto">Finding arguments</div><div dir="auto">'''''''''''''''''</div><div dir="auto"><br></div><div dir="auto">Valid arguments may be directly in the ``*args`` and ``**kwargs``, such</div><div dir="auto">as in the case for ``np.tensordot(left, right, out=out)``, or they may</div><div dir="auto">be nested within lists or dictionaries, such as in the case of</div><div dir="auto">``np.concatenate([x, y, z])``. This can be problematic for two reasons:</div><div dir="auto"><br></div><div dir="auto">1. Some functions are given long lists of values, and traversing them</div><div dir="auto">   might be prohibitively expensive.</div><div dir="auto">2. Some functions may have arguments that we don't want to inspect, even</div><div dir="auto">   if they have the ``__array_function__`` method.</div><div dir="auto"><br></div><div dir="auto">To resolve these issues, NumPy functions should explicitly indicate which</div><div dir="auto">of their arguments may be overloaded, and how these arguments should be</div><div dir="auto">checked. As a rule, this should include all arguments documented as either</div><div dir="auto">``array_like`` or ``ndarray``.</div><div dir="auto"><br></div><div dir="auto">We propose to do so by writing "dispatcher" functions for each overloaded</div><div dir="auto">NumPy function:</div><div dir="auto"><br></div><div dir="auto">- These functions will be called with the exact same arguments that were passed</div><div dir="auto">  into the NumPy function (i.e., ``dispatcher(*args, **kwargs)``), and should</div><div dir="auto">  return an iterable of arguments to check for overrides.</div><div dir="auto">- Dispatcher functions are required to share the exact same positional,</div><div dir="auto">  optional and keyword-only arguments as their corresponding NumPy functions.</div><div dir="auto">  Otherwise, valid invocations of a NumPy function could result in an error when</div><div dir="auto">  calling its dispatcher.</div><div dir="auto">- Because default *values* for keyword arguments do not have</div><div dir="auto">  ``__array_function__`` attributes, by convention we set all default argument</div><div dir="auto">  values to ``None``. This reduces the likelihood of signatures falling out</div><div dir="auto">  of sync, and minimizes extraneous information in the dispatcher.</div><div dir="auto">  The only exception should be cases where the argument value in some way</div><div dir="auto">  effects dispatching, which should be rare.</div><div dir="auto"><br></div><div dir="auto">An example of the dispatcher for ``np.concatenate`` may be instructive:</div><div dir="auto"><br></div><div dir="auto">.. code:: python</div><div dir="auto"><br></div><div dir="auto">    def _concatenate_dispatcher(arrays, axis=None, out=None):</div><div dir="auto">        for array in arrays:</div><div dir="auto">            yield array</div><div dir="auto">        if out is not None:</div><div dir="auto">            yield out</div><div dir="auto"><br></div><div dir="auto">The concatenate dispatcher is written as generator function, which allows it</div><div dir="auto">to potentially include the value of the optional ``out`` argument without</div><div dir="auto">needing to create a new sequence with the (potentially long) list of objects</div><div dir="auto">to be concatenated.</div><div dir="auto"><br></div><div dir="auto">Trying ``__array_function__`` methods until the right one works</div><div dir="auto">'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''</div><div dir="auto"><br></div><div dir="auto">Many arguments may implement the ``__array_function__`` protocol. Some</div><div dir="auto">of these may decide that, given the available inputs, they are unable to</div><div dir="auto">determine the correct result. How do we call the right one? If several</div><div dir="auto">are valid then which has precedence?</div><div dir="auto"><br></div><div dir="auto">For the most part, the rules for dispatch with ``__array_function__``</div><div dir="auto">match those for ``__array_ufunc__`` (see</div><div dir="auto">`NEP-13 <<a href="https://www.numpy.org/neps/nep-0013-ufunc-overrides.html">https://www.numpy.org/neps/nep-0013-ufunc-overrides.html</a>>`_).</div><div dir="auto">In particular:</div><div dir="auto"><br></div><div dir="auto">-  NumPy will gather implementations of ``__array_function__`` from all</div><div dir="auto">   specified inputs and call them in order: subclasses before</div><div dir="auto">   superclasses, and otherwise left to right. Note that in some edge cases</div><div dir="auto">   involving subclasses, this differs slightly from the</div><div dir="auto">   `current behavior <<a href="https://bugs.python.org/issue30140">https://bugs.python.org/issue30140</a>>`_ of Python.</div><div dir="auto">-  Implementations of ``__array_function__`` indicate that they can</div><div dir="auto">   handle the operation by returning any value other than</div><div dir="auto">   ``NotImplemented``.</div><div dir="auto">-  If all ``__array_function__`` methods return ``NotImplemented``,</div><div dir="auto">   NumPy will raise ``TypeError``.</div><div dir="auto"><br></div><div dir="auto">One deviation from the current behavior of ``__array_ufunc__`` is that NumPy</div><div dir="auto">will only call ``__array_function__`` on the *first* argument of each unique</div><div dir="auto">type. This matches Python's</div><div dir="auto">`rule for calling reflected methods <<a href="https://docs.python.org/3/reference/datamodel.html#object.__ror__">https://docs.python.org/3/reference/datamodel.html#object.__ror__</a>>`_,</div><div dir="auto">and this ensures that checking overloads has acceptable performance even when</div><div dir="auto">there are a large number of overloaded arguments. To avoid long-term divergence</div><div dir="auto">between these two dispatch protocols, we should</div><div dir="auto">`also update <<a href="https://github.com/numpy/numpy/issues/11306">https://github.com/numpy/numpy/issues/11306</a>>`_</div><div dir="auto">``__array_ufunc__`` to match this behavior.</div><div dir="auto"><br></div><div dir="auto">Special handling of ``numpy.ndarray``</div><div dir="auto">'''''''''''''''''''''''''''''''''''''</div><div dir="auto"><br></div><div dir="auto">The use cases for subclasses with ``__array_function__`` are the same as those</div><div dir="auto">with ``__array_ufunc__``, so ``numpy.ndarray`` should also define a</div><div dir="auto">``__array_function__`` method mirroring ``ndarray.__array_ufunc__``:</div><div dir="auto"><br></div><div dir="auto">.. code:: python</div><div dir="auto"><br></div><div dir="auto">    def __array_function__(self, func, types, args, kwargs):</div><div dir="auto">        # Cannot handle items that have __array_function__ other than our own.</div><div dir="auto">        for t in types:</div><div dir="auto">            if (hasattr(t, '__array_function__') and</div><div dir="auto">                    t.__array_function__ is not ndarray.__array_function__):</div><div dir="auto">                return NotImplemented</div><div dir="auto"><br></div><div dir="auto">        # Arguments contain no overrides, so we can safely call the</div><div dir="auto">        # overloaded function again.</div><div dir="auto">        return func(*args, **kwargs)</div><div dir="auto"><br></div><div dir="auto">To avoid infinite recursion, the dispatch rules for ``__array_function__`` need</div><div dir="auto">also the same special case they have for ``__array_ufunc__``: any arguments with</div><div dir="auto">an ``__array_function__`` method that is identical to</div><div dir="auto">``numpy.ndarray.__array_function__`` are not be called as</div><div dir="auto">``__array_function__`` implementations.</div><div dir="auto"><br></div><div dir="auto">Changes within NumPy functions</div><div dir="auto">^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^</div><div dir="auto"><br></div><div dir="auto">Given a function defining the above behavior, for now call it</div><div dir="auto">``try_array_function_override``, we now need to call that function from</div><div dir="auto">within every relevant NumPy function. This is a pervasive change, but of</div><div dir="auto">fairly simple and innocuous code that should complete quickly and</div><div dir="auto">without effect if no arguments implement the ``__array_function__``</div><div dir="auto">protocol.</div><div dir="auto"><br></div><div dir="auto">In most cases, these functions should written using the</div><div dir="auto">``array_function_dispatch`` decorator, which also associates dispatcher</div><div dir="auto">functions:</div><div dir="auto"><br></div><div dir="auto">.. code:: python</div><div dir="auto"><br></div><div dir="auto">    def array_function_dispatch(dispatcher):</div><div dir="auto">        """Wrap a function for dispatch with the __array_function__ protocol."""</div><div dir="auto">        def decorator(func):</div><div dir="auto">            @functools.wraps(func)</div><div dir="auto">            def new_func(*args, **kwargs):</div><div dir="auto">                relevant_arguments = dispatcher(*args, **kwargs)</div><div dir="auto">                success, value = try_array_function_override(</div><div dir="auto">                    new_func, relevant_arguments, args, kwargs)</div><div dir="auto">                if success:</div><div dir="auto">                    return value</div><div dir="auto">                return func(*args, **kwargs)</div><div dir="auto">            return new_func</div><div dir="auto">        return decorator</div><div dir="auto"><br></div><div dir="auto">    # example usage</div><div dir="auto">    def _broadcast_to_dispatcher(array, shape, subok=None, **ignored_kwargs):</div><div dir="auto">        return (array,)</div><div dir="auto"><br></div><div dir="auto">    @array_function_dispatch(_broadcast_to_dispatcher)</div><div dir="auto">    def broadcast_to(array, shape, subok=False):</div><div dir="auto">        ...  # existing definition of np.broadcast_to</div><div dir="auto"><br></div><div dir="auto">Using a decorator is great! We don't need to change the definitions of</div><div dir="auto">existing NumPy functions, and only need to write a few additional lines</div><div dir="auto">for the dispatcher function. We could even reuse a single dispatcher for</div><div dir="auto">families of functions with the same signature (e.g., ``sum`` and ``prod``).</div><div dir="auto">For such functions, the largest change could be adding a few lines to the</div><div dir="auto">docstring to note which arguments are checked for overloads.</div><div dir="auto"><br></div><div dir="auto">It's particularly worth calling out the decorator's use of</div><div dir="auto">``functools.wraps``:</div><div dir="auto"><br></div><div dir="auto">- This ensures that the wrapped function has the same name and docstring as</div><div dir="auto">  the wrapped NumPy function.</div><div dir="auto">- On Python 3, it also ensures that the decorator function copies the original</div><div dir="auto">  function signature, which is important for introspection based tools such as</div><div dir="auto">  auto-complete. If we care about preserving function signatures on Python 2,</div><div dir="auto">  for the `short while longer <<a href="http://www.numpy.org/neps/nep-0014-dropping-python2.7-proposal.html">http://www.numpy.org/neps/nep-0014-dropping-python2.7-proposal.html</a>>`_</div><div dir="auto">  that NumPy supports Python 2.7, we do could do so by adding a vendored</div><div dir="auto">  dependency on the (single-file, BSD licensed)</div><div dir="auto">  `decorator library <<a href="https://github.com/micheles/decorator">https://github.com/micheles/decorator</a>>`_.</div><div dir="auto">- Finally, it ensures that the wrapped function</div><div dir="auto">  `can be pickled <<a href="http://gael-varoquaux.info/programming/decoration-in-python-done-right-decorating-and-pickling.html">http://gael-varoquaux.info/programming/decoration-in-python-done-right-decorating-and-pickling.html</a>>`_.</div><div dir="auto"><br></div><div dir="auto">In a few cases, it would not make sense to use the ``array_function_dispatch``</div><div dir="auto">decorator directly, but override implementation in terms of</div><div dir="auto">``try_array_function_override`` should still be straightforward.</div><div dir="auto"><br></div><div dir="auto">- Functions written entirely in C (e.g., ``np.concatenate``) can't use</div><div dir="auto">  decorators, but they could still use a C equivalent of</div><div dir="auto">  ``try_array_function_override``. If performance is not a concern, they could</div><div dir="auto">  also be easily wrapped with a small Python wrapper.</div><div dir="auto">- The ``__call__`` method of ``np.vectorize`` can't be decorated with</div><div dir="auto"><p style="margin:0px;font-stretch:normal;font-size:17.4px;line-</div>