Repository: numpy/numpy

Issue:
"max() got an unexpected keyword argument 'out'" in fromnumeric.py line 2256
I will just explain this bug by example:
`

```
from scipy import sparse

e = sparse.eye(3)

amax(abs(e-e.H))
```

`

This generates an error: "max() got an unexpected keyword argument 'out'", and the offending code is line 2256 in fromnumeric.py.



Discussion:
Unless this is a regression (i.e., it used to work in old versions of numpy but broke in newer versions -- in which case, please let us know!), then I think this is either a bug in `scipy` or else a documentation bug. Either way the workaround is to write it like `abs(e - e.H).max()` so that you're calling the sparse matrix `.max` method instead of the numpy `amax` function -- numpy doesn't know anything about sparse matrix objects from scipy.

@pv: thoughts on whether scipy should fix this? It looks like `np.amax(foo)` does the dubious thing of calling `foo.max(axis=axis, out=out)` whenever `hasattr(foo, "max")`. (Also relevant discussion: https://github.com/numpy/numpy/pull/7325#issuecomment-199147582)

@orswan, @njsmith : I am currently squashing `fromnumeric.py` incompatibilities in fact in <a href="https://github.com/scipy/scipy/pull/6002">#6002</a> in `scipy`, so I will address this issue on the `scipy` end at the very least.  Also, in `scipy`, the usage of `numpy` functions on `scipy.sparse` matrices is discouraged not only because of these incompatibilities but also because `numpy` often does not cast them properly into native `ndarray`, leading to incorrect results.

There are several methods that have a similar structure to `max` where they have the `try-except` block if the object is not `ndarray`.  I suspected those might be problematic when I was working on #7325, but I was not sure what the best way was to handle this issue.  Can I just call `_wrap_func` if the object is not `ndarray`?

So this is a regression?

@seberg : I don't know about regression, but it is a compatibility issue at the very least, very similar to the ones I addressed in #7325.

Ok, good, then no regression, just wanted to make sure.


Resolving patch:
diff --git a/numpy/doc/subclassing.py b/numpy/doc/subclassing.py
--- a/numpy/doc/subclassing.py
+++ b/numpy/doc/subclassing.py
@@ -555,6 +555,53 @@ def __array_wrap__(self, arr, context=None):
 how this can work, have a look at the ``memmap`` class in
 ``numpy.core``.
 
+Subclassing and Downstream Compatibility
+----------------------------------------
+
+When sub-classing ``ndarray`` or creating duck-types that mimic the ``ndarray``
+interface, it is your responsibility to decide how aligned your APIs will be
+with those of numpy. For convenience, many numpy functions that have a corresponding
+``ndarray`` method (e.g., ``sum``, ``mean``, ``take``, ``reshape``) work by checking
+if the first argument to a function has a method of the same name. If it exists, the
+method is called instead of coercing the arguments to a numpy array.
+
+For example, if you want your sub-class or duck-type to be compatible with
+numpy's ``sum`` function, the method signature for this object's ``sum`` method
+should be the following:
+
+.. testcode::
+
+    def sum(self, axis=None, dtype=None, out=None, keepdims=False):
+    ...
+
+This is the exact same method signature for ``np.sum``, so now if a user calls
+``np.sum`` on this object, numpy will call the object's own ``sum`` method and
+pass in these arguments enumerated above in the signature, and no errors will
+be raised because the signatures are completely compatible with each other.
+
+If, however, you decide to deviate from this signature and do something like this:
+
+.. testcode::
+
+   def sum(self, axis=None, dtype=None):
+   ...
+
+This object is no longer compatible with ``np.sum`` because if you call ``np.sum``,
+it will pass in unexpected arguments ``out`` and ``keepdims``, causing a TypeError
+to be raised.
+
+If you wish to maintain compatibility with numpy and its subsequent versions (which
+might add new keyword arguments) but do not want to surface all of numpy's arguments,
+your function's signature should accept ``**kwargs``. For example:
+
+.. testcode::
+
+   def sum(self, axis=None, dtype=None, **unused_kwargs):
+   ...
+
+This object is now compatible with ``np.sum`` again because any extraneous arguments
+(i.e. keywords that are not ``axis`` or ``dtype``) will be hidden away in the
+``**unused_kwargs`` parameter.
 
 """
 from __future__ import division, absolute_import, print_function


Test patch: