Skip to content

ENH: Allow ndarray.__array_function__ to dispatch functions that use like= - #27504

Merged
seberg merged 1 commit into
numpy:mainfrom
mhvk:ndarray_array_function_for_like
Nov 21, 2024
Merged

seberg merged 1 commit into
numpy:mainfrom
mhvk:ndarray_array_function_for_like

Conversation

@mhvk

@mhvk mhvk commented Oct 4, 2024 •

Copy link
Copy Markdown
Contributor

This makes ndarray.__array_function__ look for _implementation, but dispatch to the function if that is not found.

This is rather crude, as there are no further sanity checks, but I think it is OK to let subclasses deal with possible problems.

A more general question is whether we should not let ndarray.__array_function__ actually return a subclass in this case. I guess this is possible in principle, by actually using the self argument in the method implementation, and passing that on to array_function_method_impl.

EDIT: rebased, so now just on its own Note: built on #27503, so just look at second commit.

fixes #27500

@mhvk
mhvk force-pushed the ndarray_array_function_for_like branch from 91bebea to a1cbd6d Compare October 4, 2024 19:45

@seberg seberg left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks, LGTM. I would like to consider briefly once more not what should happen for subclasses that do implement __array_function__ but rather subclasses that do not.

like= was always designed for those that do, so we didn't pay much attention about what the right thing is for those that do not implement it. In that sense, maybe it would be reasonable if we do actually call self.__array_wrap__() already in __array_function__.
OTOH, that may be good to have a release note for, since it subtly changes behavior (unlikely to be noticed much, though).

Subclasses that do not want the array-wrap behavior, will have to view a base-class array or call the original function themself (although that may or may not wrap depending on the function).

I can go either way, but my gut leans a very tiny bit towards wrapping right now.

@mhvk

mhvk commented Oct 7, 2024

Copy link
Copy Markdown
Contributor Author

@seberg - It took me some wondering but I ended up thinking there is no need for wrapping, at least not inside ndarray.__array_function__. The reason is that for subclasses that do not define __array_function__, we never get here: for like=some_non_override_subclass(), array_implement_c_array_function_creation just returns NotImplemented:

if (is_default_array_function(method)) {
/*
* Return a borrowed reference of Py_NotImplemented to defer back to
* the original function.
*/
Py_DECREF(method);
return Py_NotImplemented;
}

(Or did you mean overriding it there?)

FWIW, in the regular case of dispatching on some array-like argument, dispatcher_vectorcall does the same: it just calls the default implementation if all arguments have the default __array_function__:

/*
* Handle the typical case of no overrides. This is merely an optimization
* if some arguments are ndarray objects, but is also necessary if no
* arguments implement __array_function__ at all (e.g., if they are all
* built-in types).
*/
int any_overrides = 0;
for (int j = 0; j < num_implementing_args; j++) {
if (!is_default_array_function(array_function_methods[j])) {
any_overrides = 1;
break;
}
}
if (!any_overrides) {
/* Directly call the actual implementation. */
result = PyObject_Vectorcall(self->default_impl, args, len_args, kwnames);
goto cleanup;
}

(I don't think you meant to add wrapping for those, but if we wanted that, it would need some checking of the python code, to remove wrapping there...)

p.s. For subclasses that do define __array_function__, we get here if they call super().__array_function__ (which Quantity does to avoid having to know about _implementation). But for Quantity at least, wrapping would not be helpful because __array_wrap__ does not have the right context (e.g., the unit of the scalar one might want to use for np.full); I think it would be similar if, e.g., we were to write an MaskedArray.__array_function__, where the output mask might depend on the actual function and parameters.

@seberg

seberg commented Oct 7, 2024

Copy link
Copy Markdown
Member

Ah, good point. So right now it actually doesn't matter to begin with, although we may want to change that at some point, I suppose. At some point we might not reject the like= part, but maybe the other point is that basically evryone should implement __array_function__ anyway.
(And rather we should clarify that calling _implementation or func() is a way to do that without overriding all functions.)

@seberg seberg left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Sorry for not going back to this earlier. Let's give this a try, thanks!

@seberg seberg changed the title Allow ndarray.__array_function__ to dispatch on NEP 35 functions that use like= ENH: Allow ndarray.__array_function__ to dispatch functions that use like= Nov 21, 2024
@seberg

seberg commented Nov 21, 2024

Copy link
Copy Markdown
Member

Arg, so here is the thing that I guess still makes me pause:

class SomeClass(np.ndarray):
    pass

# This returns a plain array:
np.ones(3, like=np.arange(3).view(SomeClass))
# This returns a subclass (of course):
np.empty_like(np.arange(3).view(SomeClass))

So to me, it seems like it would be a fix to do what you said: Pass on self and call __array_wrap__ additionally here (or whatever it is that we call for empty_like).
Unless that would be problematic for your case?!

Basically, there is the weird thing that for other functions we often (but not always!) preserve subclasses, but for creation functions like= just doesn't, when that seems odd.

OTOH, I am not 100% sure either that it is doing much of a favor to preserve the subclasses here if those subclasses don't bother implementing __array_function__ maybe they are not important enough to get a chance/say... (should play well enough with the returned NumPy array)?

TL;DR: I still feel it seems more right to return subclasses, but I am also fine with putting this in, it doesn't break anything (although it might slightly cement the behavior).

@mhvk

mhvk commented Nov 21, 2024

Copy link
Copy Markdown
Contributor Author

@seberg - it remains a tricky one. On the one hand, I think that the whole __array_ufunc__/__array_function__ system was always meant to replace the __array_wrap__ stuff, so I'd rather not rely on __array_wrap__. On the other hand, returning a subclass by default seems somewhat logical.

But maybe more importantly is that it is tricky, since if we do it for the like= NEP35 cases, then why not for the other functions? But those often do it themselves already. So, it is not clear to me how to implement it correctly.

Thinking concretely for the case of astropy, it would not help the implementations to have a subclass returned, as we will regardless do things like set units, etc., after this call happens. Also, assuming this really should be done generally, not all functions should return subclasses (e.g., for quantities, isclose should still return a regular plain ndarray of bool). Really, in our case the main use of super().__array_function__ is just that it means we can pass on our subclasses to the implementation without having to know where those reside. This saves us from writing wrappers for a large number of functions...

So, overall perhaps best to just take this as a bug fix that allows using the like= functions, in a way that is exactly the same as the NEP 20 ones.

@mhvk

mhvk commented Nov 21, 2024

Copy link
Copy Markdown
Contributor Author

Sorry, one further thought: in principle, I think it would be OK for the actual C code that creates arrays to know how to create a subclass, and then __array_function__ could pass on the type of class. But it seems weird to have code in __array_function__ to have to care about what implementations do - it should just pass things on, not do any wrapping itself.

@seberg

seberg commented Nov 21, 2024

Copy link
Copy Markdown
Member

Yeah, it isn't clear cut. Because I can also say that most subclasses that do not implement __array_function__ (or are lazy about it), should interact gracefully with NumPy arrays.

And if they interact gracefully, then it actually makes sense to just return a plain array!


So maybe, I should just stop worry about it, either way someone who tries to be lazy by not implementing these (falling back), will have to do more than they hope.

And while I may have a slight tendency to the other being the better default... In the end, it is just a default!

So... Let's give this a shot, I don't think anyone else will chime in after all.

@seberg
seberg merged commit da4fa73 into numpy:main Nov 21, 2024
@mhvk
mhvk deleted the ndarray_array_function_for_like branch November 21, 2024 17:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BUG: np.ndarray.__array_function__ cannot be handle NEP 35 creation functions

2 participants