Skip to content

MAINT: move comparison operator special-handling out of ufunc parsing. #11282

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 3 commits into from
Jun 21, 2018

Conversation

mhvk
Copy link
Contributor

@mhvk mhvk commented Jun 8, 2018

The argument parsing for ufuncs contains special logic to enable returning NotImplemented for comparison functions. The ufuncs, however, should not have to know whether they were called via a python comparison operator, so this PR moves the logic to the operators, checking after ufunc failed
whether a NotImplemented should be returned.

Follow-up of #11260, in particular #11260 (comment)

@mhvk mhvk requested review from eric-wieser and mattip June 8, 2018 15:29
* is counted as inflexble. (This repeats work done in the ufunc,
* but OK to waste some time in an unlikely path.)
*/
if (PyArray_GetArrayParamsFromObject(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I tried using the seemingly more logical PyArray_FromAny(other, NULL, 0, 0, 0, NULL) but somehow this returns a failure when other is a python string.

Copy link
Member

Choose a reason for hiding this comment

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

You probably need to PyErr_Fetch above, before you start calling any more functions - most python functions are not willing to run if an exception is actively being thrown

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, yes, that indeed does it!

@eric-wieser
Copy link
Member

This presumably means np.equal("one", 1) no longer returns `NotImplemented?

@mhvk mhvk requested a review from njsmith June 8, 2018 15:50
@mhvk mhvk added component: numpy._core 03 - Maintenance 56 - Needs Release Note. Needs an entry in doc/release/upcoming_changes labels Jun 8, 2018
@mhvk mhvk added this to the 1.16.0 release milestone Jun 8, 2018
@mhvk
Copy link
Contributor Author

mhvk commented Jun 8, 2018

@eric-wieser - yes, indeed, the ufuncs themselves will no longer return NotImplemented - which means this would definitely need a note to that extent. So, not for 1.15!

@eric-wieser
Copy link
Member

Can you add some tests for ufuncs no longer returning NotImplemented?

* silenced error.
*/
NPY_NO_EXPORT int
DEPRECATE_FUTUREWARNING_silence_error(const char *msg) {
Copy link
Member

@eric-wieser eric-wieser Jun 8, 2018

Choose a reason for hiding this comment

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

Maybe better to just add a PyObject *warning_type argument to the original function, and use PyErr_WarnEx(warning_type, msg, 1) in place of DEPRECATE_FUTUREWARNING

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Since I needed the fetch the error anyway, the need for the new function disappeared...

int other_type_num, other_is_flexible;
/*
* Determine whether other has a flexible dtype; here, inconvertible
* is counted as inflexble. (This repeats work done in the ufunc,
Copy link
Member

Choose a reason for hiding this comment

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

typo

return Py_NotImplemented;
}
else if (other_is_flexible) {
/*
Copy link
Member

Choose a reason for hiding this comment

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

weird indent

@mattip
Copy link
Member

mattip commented Jun 8, 2018

It would be nice even at this early stage to document the user-visible changes and change the tests appropriately

@mhvk mhvk force-pushed the array-comparison-not-implemented branch from c49d901 to c8dffb2 Compare June 8, 2018 17:15
@mhvk
Copy link
Contributor Author

mhvk commented Jun 8, 2018

OK, now simpler with PyArray_FromAny - and some tests that were ready to go enabled.

@mattip - agreed about documentation, but there isn't even a file for the 1.16 release notes yet... But the main effect is the one @eric-wieser noted: now the ufuncs themselves will error rather than return NotImplemented (surely a good thing...)

@charris
Copy link
Member

charris commented Jun 8, 2018

Just to emphasize, do not merge before the 1.15 branch.

@mhvk mhvk force-pushed the array-comparison-not-implemented branch from c8dffb2 to 61bd834 Compare June 8, 2018 18:06
@mhvk
Copy link
Contributor Author

mhvk commented Jun 8, 2018

OK, tests now all pass - further review much appreciated!

@mhvk mhvk force-pushed the array-comparison-not-implemented branch from 61bd834 to 72bab23 Compare June 10, 2018 17:16
@mhvk
Copy link
Contributor Author

mhvk commented Jun 10, 2018

Rebased, so now this is just a single commit.

@mhvk mhvk force-pushed the array-comparison-not-implemented branch from 72bab23 to f7029ce Compare June 14, 2018 17:40
@mhvk
Copy link
Contributor Author

mhvk commented Jun 14, 2018

With 1.15 branches, I rebased this and added a release note for 1.16. If everyone is still happy with this, it would be nice to merge it, since I have other changes to the ufunc parsing in the pipeline (#11333 in particular).

@mhvk mhvk removed the 56 - Needs Release Note. Needs an entry in doc/release/upcoming_changes label Jun 15, 2018
@mhvk
Copy link
Contributor Author

mhvk commented Jun 18, 2018

@mattip, @eric-wieser - any further comments on this?

Py_DECREF(array_other);
}
else {
PyErr_Clear(); /* we restore the original error if needed */
Copy link
Member

Choose a reason for hiding this comment

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

Would be clearer if other_is_flexible and ndim_other were assigned in this path, rather than initialized.

The argument parsing for ufuncs contains special logic to enable returning
NotImplemented for comparison functions.  The ufuncs, however, should
not have to know whether they were called via a python comparison operator,
so this PR moves the logic to the operators, checking after ufunc failed
whether a NotImplemented should be returned.
@mhvk mhvk force-pushed the array-comparison-not-implemented branch from f7029ce to 13c42ef Compare June 18, 2018 20:11
@mhvk
Copy link
Contributor Author

mhvk commented Jun 18, 2018

OK, thanks, did all three.

/*
* In future, we should create a correctly shaped
* array of bool. For now, a placeholder error.
*/
Copy link
Member

Choose a reason for hiding this comment

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

I think this should

  • Appear before the DEPRECATE_FUTUREWARNING line
  • say placeholder warning, not placeholder error.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is just that if the deprecation is removed, the chained exception should be replaced (I wanted to have code that would at least work, but didn't feel like creating the array now...). I clarified this.

Copy link
Member

Choose a reason for hiding this comment

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

I'd say that's obvious, in the same way that the return NULL should also be replaced

return Py_NotImplemented;
}
if (result == NULL) {
/*
Copy link
Member

@eric-wieser eric-wieser Jun 18, 2018

Choose a reason for hiding this comment

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

Can you extract the contents of this long if to _failed_compare_workaround(self, other, op), or something similar (documented to expect an exception to already be in flight)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

@mhvk
Copy link
Contributor Author

mhvk commented Jun 18, 2018

OK, pushed an extra commit moving the failure path to its own function.

* ordering, so we emit a warning.
*/
#if !defined(NPY_PY3K)
if (DEPRECATE(
Copy link
Member

Choose a reason for hiding this comment

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

Date and version comment needed

* If other did not have a flexible dtype, the error cannot
* have been caused by a lack of implementation in the ufunc.
*/
if (DEPRECATE(
Copy link
Member

Choose a reason for hiding this comment

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

Date and version comment needed

* array of bool.
*/
if (ndim_other != 0 || PyArray_NDIM(self) != 0) {
if (DEPRECATE_FUTUREWARNING(
Copy link
Member

Choose a reason for hiding this comment

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

Date and version comment needed

"elementwise comparison failed; "
"this will raise an error in the future.") < 0) {
PyArray_ChainExceptionsCause(exc, val, tb);
return NULL;
Copy link
Member

Choose a reason for hiding this comment

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

I think it would be tidy to move this to a fail: label

}
else {
/* LE, LT, GT, or GE with non-flexible other; just pass on error */
PyErr_Restore(exc, val, tb);
Copy link
Member

Choose a reason for hiding this comment

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

This could also be the fail label - PyArray_ChainExceptionsCause is a shorhand for PyErr_Restore if no exception is set.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, this makes a lot more sense.

@mhvk mhvk force-pushed the array-comparison-not-implemented branch from 8574044 to a2b9baa Compare June 19, 2018 00:51
@mhvk
Copy link
Contributor Author

mhvk commented Jun 19, 2018

@mattip, @eric-wieser - all implemented.


/* LE, LT, GT, or GE with non-flexible other; just pass on error */
fail:
PyErr_Restore(exc, val, tb);
Copy link
Member

Choose a reason for hiding this comment

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

This line should be the ChainExceptions, not PyErr_Restore. The former implies the latter, not vice versa

Copy link
Member

Choose a reason for hiding this comment

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

A comment here saying "Reraise the original exception, or chain the new one" might be nice too

Comparison ufuncs will now error rather than return NotImplemented
------------------------------------------------------------------

Previously, comparison ufuncs would return `NotImplemented` if their arguments
Copy link
Member

Choose a reason for hiding this comment

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

"such as np.equal" might be helpful here.

------------------------------------------------------------------

Previously, comparison ufuncs would return `NotImplemented` if their arguments
had structured dtypes, to help the comparison operators deal with those. This
Copy link
Member

Choose a reason for hiding this comment

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

Can you point out that the comparison operators continue to return NotImplemented as before?

* For scalars, returning NotImplemented is correct.
* For arrays, we emit a future deprecation warning.
* If that is removed, the failure jump should be replaced
* by the creation of a correctly shaped array of bool.
Copy link
Member

Choose a reason for hiding this comment

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

Perhaps just "when this warning is removed, it should return a correctly-shaped array of bool.

return Py_NotImplemented;
}

/* LE, LT, GT, or GE with non-flexible other; just pass on error */
Copy link
Member

@eric-wieser eric-wieser Jun 19, 2018

Choose a reason for hiding this comment

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

I'd probably add an else { goto fail; } here, just to have somewhere obvious to tie the comment to. The compiler should optimize it just the same anyway

@mhvk mhvk force-pushed the array-comparison-not-implemented branch from a2b9baa to 533bb37 Compare June 19, 2018 11:17
@mhvk
Copy link
Contributor Author

mhvk commented Jun 19, 2018

@eric-wieser - thanks for the further comments; all done.

Copy link
Member

@eric-wieser eric-wieser left a comment

Choose a reason for hiding this comment

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

Implementation looks good now.

Did you just cut up the original long comment, or did you throw it out and reword it? I haven't yet checked to see if all of the important bits survived.

@mhvk
Copy link
Contributor Author

mhvk commented Jun 20, 2018

Yes, I reworded the long comment, removing parts not relevant for comparison operators. I think it now (even) more clearly states where we want to go.

@mhvk
Copy link
Contributor Author

mhvk commented Jun 21, 2018

As this was approved, I'll go ahead and merge so I can can rebase some of the other PRs (which will be easier to look at without the long distracting stuff in get_ufunc_arguments)

@mhvk mhvk merged commit 9c43b17 into numpy:master Jun 21, 2018
@mhvk mhvk deleted the array-comparison-not-implemented branch June 21, 2018 12:55
}
else {
/* LE, LT, GT, or GE with non-flexible other; just pass on error */
goto fail;
Copy link
Member

Choose a reason for hiding this comment

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

I'm not clear on why this path doesn't return NotImplemented in py3

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The reason is that the comparison is actually implemented, i.e., the error is real and we act just like we would do for __add__ (note that the behaviour was the same before this PR).

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.

4 participants