8000 MAINT: Make the refactor suggested in prepare_index by eric-wieser · Pull Request #8278 · numpy/numpy · GitHub
[go: up one dir, main page]

Skip to content
Merged
Changes from 8 commits
Commits
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
279 changes: 188 additions & 91 deletions num 8000 py/core/src/multiarray/mapping.c
52F4
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,187 @@ PyArray_MapIterSwapAxes(PyArrayMapIterObject *mit, PyArrayObject **ret, int getm
*ret = (PyArrayObject *)new;
}

NPY_NO_EXPORT NPY_INLINE void
multi_DECREF(PyObject **objects, npy_intp n)
Copy link
Member

Choose a reason for hiding this comment

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

First wasn't sure I like this, but it seems harmless :).

{
npy_intp i;
for (i = 0; i < n; i++) {
Py_DECREF(objects[i]);
}
}

/**
* Turn an index argument into a c-array of `PyObject *`s, one for each index.
*
* When a scalar is passed, this is written directly to the buffer. When a
* tuple is passed, the tuple elements are unpacked into the buffer.
*
* When some o 10000 ther sequence is passed, this implements the following section
* from the advanced indexing docs to decide whether to unpack or just write
* one element:
*
* > In order to remain backward compatible with a common usage in Numeric,
* > basic slicing is also initiated if the selection object is any non-ndarray
* > sequence (such as a list) containing slice objects, the Ellipsis object,
* > or the newaxis object, but not for integer arrays or other embedded
* > sequences.
*
* The rationale for only unpacking `2*NPY_MAXDIMS` items is the assumption
* that the longest possible index that is allowed to produce a result is
* `(0,)*np.MAXDIMS + (None,)*np.MAXDIMS`. This assumption turns out to be
* wrong (we could add one more item, an Ellipsis), but we keep it for
* compatibility.
Copy link
Member

Choose a reason for hiding this comment

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

You know, the 2* is pretty arbitrary, so you can increment it by one if you like, I just set it as a "high enough" value and yeah, forgot that in principle you can go one higher and still get a valid index.

Copy link
Member Author

Choose a reason for hiding this comment

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

Actually, there is no limit to the number of valid indices. You can index with True or False as many times as you like, and the dimensionality will only ever increase by one

Copy link
Member Author

Choose a reason for hiding this comment

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

(although in practice, indexing with more than 32 causes problems elsewhere)

Copy link
Member

Choose a reason for hiding this comment

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

Sorry, just thought I would start on this again, don't have much time now so might forget again though, if I do and you want to come back to this, please don't hesitate to ping.

Copy link
Member

Choose a reason for hiding this comment

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

@eric-wieser no, maxdims*2+1 is actually maximu, if you do None/True you add one so you end up with at least that many dims ;).

Copy link
Member Author
@eric-wieser eric-wieser Jul 15, 2017
edited
Loading

Choose a reason for hiding this comment

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

>>> a = np.arange(6).reshape(2, 3)
>>> a[True]
array([[[0, 1, 2],
        [3, 4, 5]]])
>>> a[(True,)*32]
array([[[0, 1, 2],
        [3, 4, 5]]])
>>> a[(True,)*33]
ValueError: Cannot construct an iterator with more than 32 operands (33 were requested)

Copy link
Member

Choose a reason for hiding this comment

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

OK, comment seems fine to me, could make it "is based ... longest reasonable index" or so.

Copy link
Member Author

Choose a reason for hiding this comment

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

Updated

*
* @param index The index object, which may or may not be a tuple. This is a
* borrowed reference.
* @param result An empty buffer of 2*NPY_MAXDIMS PyObject* to write each
* index component to. The references written are new.
*
* @returns The number of items in `result`, or -1 if an error occured.
* The entries in `result` at and beyond this index should be
* assumed to contain garbage, even if they were initialized
* to NULL, so are not safe to Py_XDECREF.
*/
NPY_NO_EXPORT npy_intp
unpack_indices(PyObject *index, PyObject *result[2*NPY_MAXDIMS])
{
npy_intp n, i;
npy_bool commit_to_unpack;

Copy link
Member Author

Choose a reason for hiding this comment

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

As instructed here

/* Fast route for passing a tuple */
if (PyTuple_CheckExact(index)) {
n = PyTuple_GET_SIZE(index);
if (n > NPY_MAXDIMS * 2) {
PyErr_SetString(PyExc_IndexError,
"too many indices for array");
return -1;
}
for (i = 0; i < n; i++) {
result[i] = PyTuple_GET_ITEM(index, i);
Py_INCREF(result[i]);
}
return n;
}

/* Obvious single-entry cases */
if (0
Copy link
Member

Choose a reason for hiding this comment

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

OK, with those #if formatting it without the 0 is ugly I suppose

Copy link
Member Author
@eric-wieser eric-wieser Jul 16, 2017

Choose a reason for hiding this comment

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

Should optimize out anyway. Could be if (0 /* to make macros below easier */

Copy link
Member

Choose a reason for hiding this comment

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

Nah, its fine, obvious enough, just tried to style nitpick and it didn't work too well ;)

#if !defined(NPY_PY3K)
|| PyInt_CheckExact(index)
#else
|| PyLong_CheckExact(index)
#endif
|| index == Py_None
Copy link
Member

Choose a reason for hiding this comment

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

brackets?

|| PySlice_Check(index)
|| PyArray_Check(index)
|| !PySequence_Check(index)) {

Py_INCREF(index);
result[0] = index;
return 1;
}

/* Passing a tuple subclass - needs to handle errors */
if (PyTuple_Check(index)) {
Copy link
Member Author
@eric-wieser eric-wieser Apr 9, 2017

Choose a reason for hiding this comment

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

The code here is subtly different to what we had before.

Calling PySequence_Tuple(index) invokes __iter__, whereas this invokes __getitem__.

So tuple subclasses that implement those methods inconsistently now behave differently. For instance:

class Plus1Tuple(tuple):
    def __getitem__(self, i):
        return super().__getitem__(i) + 1
    # oops, we forgot `__iter__`, and inherited `tuple.__iter__`, which
    # does not fall back on __getitem__

gives:

  • Before: a[PlusOneTuple([1, 2, 3])]a[1, 2, 3] (!)
  • After: a[PlusOneTuple([1, 2, 3])]a[2, 3, 4]

Copy link
Member

Choose a reason for hiding this comment

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

Can't you just remove this whole block and replace it with commit_to_unpack=1?

Copy link
Member

Choose a reason for hiding this comment

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

OK, plus a check for too many indices.

Copy link
Member

Choose a reason for hiding this comment

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

I am fine with the changed behaviour I think, a tuple subclass should really be OK even with just using PyTuple_GET_ITEM to be honest, otherwise it should be considered badly broken.

Copy link
Member Author
@eric-wieser eric-wieser Jul 16, 2017

Choose a reason for hiding this comment

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

Perhaps we should just bite the bullet here and call tuple(tuplesubclass), since efficiency isn't important for this weird case

Copy link
Member

Choose a reason for hiding this comment

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

Yea, I suppose we might just as well put whatever was there before, it won't make the code any uglier and speed really does not matter. But I no need to add a test or so (or if, put a comment that it is fine to break it). This is too strange to really worry about.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ok, changed to call tuple(tuplesubclass), which makes our life a lot easier

n = PySequence_Size(index);
if (n < 0) {
return -1;
}
if (n > NPY_MAXDIMS * 2) {
PyErr_SetString(PyExc_IndexError,
"too many indices for array");
return -1;
}
for (i = 0; i < n; i++) {
result[i] = PySequence_GetItem(index, i);
if (result[i] == NULL) {
multi_DECREF(result, i);
return -1;
}
}
return n;
}

/*
* At this point, we're left with a non-tuple, non-array, sequence:
* typically, a list. We use some somewhat-arbirary heuristics from here
* onwards to decided whether to treat that list as a single index, or a
* list of indices. It might be worth deprecating this behaviour (gh-4434).
*
* Sequences < NPY_MAXDIMS with any slice objects
* or newaxis, Ellipsis or other arrays or sequences
* embedded, are considered equivalent to an indexing
* tuple. (`a[[[1,2], [3,4]]] == a[[1,2], [3,4]]`)
*/

/* if len fails, treat like a scalar */
n = PySequence_Size(index);
if (n < 0) {
PyErr_Clear();
Py_INCREF(index);
result[0] = index;
return 1;
}

/*
* For some reason, anything that's long but not too long is turned into
Copy link
Contributor

Choose a reason for hiding this comment

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

This comment confuses me: is it totally unclear why this is done at all? I'd guess that here the point is that it is known that unpacking will fail (well, modulo, the factor of 2), but that one should not preclude a very long list of, e.g., integers. If that is not the case, what would fail if one removed this? Should it be deprecated?

Copy link
Member Author

Choose a reason for hiding this comment

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

is it totally unclear why this is done at all?

It's totally unclear to me why the 2 is missing. As a result, x[[None] * 20] and x[(None,) * 20] mean the same thing, yet x[[None] * 40] and x[(None,) * 40] mean different things (yet neither error).

Of course, someone might be relying on x[[None] * 40] meaning x[[None] * 40],], so it's too late to fix it.

The rationale behind the 2*NPY_MAXDIMS limit elsewhere is that the result is limited to this many dimensions - at best, you can use an int to remove every existing dimension, and then None to add them back again - so the longest valid index is assumed to be (0,)*NPY_MAXDIMS + (None,)*NPY_MAXDIMS.

That's not really true either, as it ought to be legal to add an Ellipsis in there too...

Should it be deprecated?

Arguably everything from this point down should be deprecated, as in #4434

Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm, a bit of a mess. But I think for someone reading the code later, it may be useful to explicitly mention your last point, i.e., that everything below here should arguably be deprecated (and refer to 4434).

Copy link
Member Author
@eric-wieser eric-wieser Apr 9, 2017

Choose a reason for hiding this comment

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

Also done (but higher up than this line)

Copy link
Member

Choose a reason for hiding this comment

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

x[[None] * 40] should error? But yes, there are some awful example such as using lists of lists as index. I would prefer the things like "for some reason" to be replaced with things like "As described previously, for backwards compatibility" in general, it is after all the implementation of the comment just a bit further up (All of this comes down to that Numeric compat thing! Except of course the 2*N which I did because I thought "why not allow a bit longer indices just in case someone is crazy, no harm").

Copy link
Member Author

Choose a reason for hiding this comment

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

I'll improve that wording

* a single index. The *2 is missing here for backward-compatibility.
*/
if (n >= NPY_MAXDIMS) {
Py_INCREF(index);
result[0] = index;
return 1;
}

/*
* Some other type of short sequence - assume we should unpack it like a
* tuple, and then decide whether that was actually necessary.
*/
commit_to_unpack = 0;
for (i = 0; i < n; i++) {
PyObject *tmp_obj = result[i] = PySequence_GetItem(index, i);

if (commit_to_unpack) {
/* propagate errors */
if (tmp_obj == NULL) {
multi_DECREF(result, i);
return -1;
}
}
else {
/*
* if getitem fails (unusual) before we've committed, then stop
* unpacking
*/
if (tmp_obj == NULL) {
PyErr_Clear();
break;
}

/* decide if we should treat this sequence like a tuple */
if (PyArray_Check(tmp_obj)
|| PySequence_Check(tmp_obj)
|| PySlice_Check(tmp_obj)
|| tmp_obj == Py_Ellipsis
|| tmp_obj == Py_None) {
Copy link
Member

Choose a reason for hiding this comment

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

Again, I think we usually put brackets, but no big deal

Copy link
Member Author

Choose a reason for hiding this comment

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

Don't agree - we use brackets to make precedence of || and && obvious, but a quick grep shows it faily uncommon to use them to aid reading precedence of ||, && and ==, >=, ...

Copy link
Member

Choose a reason for hiding this comment

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

Ok, frankly don't care much, its not *a++ or something...

commit_to_unpack = 1;
}
}
}

/* unpacking was the right thing to do, and we already did it */
if (commit_to_unpack) {
return n;
}
/* got to the end, never found an indication that we should have unpacked */
else {
/* we partially filled result, so empty it first */
multi_DECREF(result, i);

Py_INCREF(index);
result[0] = index;
return 1;
}
}

/**
* Prepare an npy_index_object from the python slicing object.
Expand Down Expand Up @@ -174,89 +355,17 @@ prepare_index(PyArrayObject *self, PyObject *index,
int i;
npy_intp n;

npy_bool make_tuple = 0;
PyObject *obj = NULL;
PyArrayObject *arr;

int index_type = 0;
int ellipsis_pos = -1;

/*
* The index might be a multi-dimensional index, but not yet a tuple
* this makes it a tuple in that case.
*
* TODO: Refactor into its own function.
*/
if (!PyTuple_CheckExact(index)
/* Next three are just to avoid slow checks */
#if !defined(NPY_PY3K)
&& (!PyInt_CheckExact(index))
#else
&& (!PyLong_CheckExact(index))
#endif
&& (index != Py_None)
&& (!PySlice_Check(index))
&& (!PyArray_Check(index))
&& (PySequence_Check(index))) {
/*
* Sequences < NPY_MAXDIMS with any slice objects
* or newaxis, Ellipsis or other arrays or sequences
* embedded, are considered equivalent to an indexing
* tuple. (`a[[[1,2], [3,4]]] == a[[1,2], [3,4]]`)
*/

if (PyTuple_Check(index)) {
/* If it is already a tuple, make it an exact tuple anyway */
n = 0;
make_tuple = 1;
}
else {
n = PySequence_Size(index);
}
if (n < 0 || n >= NPY_MAXDIMS) {
n = 0;
}
for (i = 0; i < n; i++) {
PyObject *tmp_obj = PySequence_GetItem(index, i);
/* if getitem fails (unusual) treat this as a single index */
if (tmp_obj == NULL) {
PyErr_Clear();
make_tuple = 0;
break;
}
if (PyArray_Check(tmp_obj) || PySequence_Check(tmp_obj)
|| PySlice_Check(tmp_obj) || tmp_obj == Py_Ellipsis
|| tmp_obj == Py_None) {
make_tuple = 1;
Py_DECREF(tmp_obj);
break;
}
Py_DECREF(tmp_obj);
}

if (make_tuple) {
/* We want to interpret it as a tuple, so make it one */
index = PySequence_Tuple(index);
if (index == NULL) {
return -1;
}
}
}
PyObject *raw_indices[NPY_MAXDIMS*2];

/* If the index is not a tuple, handle it the same as (index,) */
if (!PyTuple_CheckExact(index)) {
obj = index;
index_ndim = 1;
}
else {
n = PyTuple_GET_SIZE(index);
if (n > NPY_MAXDIMS * 2) {
PyErr_SetString(PyExc_IndexError,
"too many indices for array");
goto fail;
}
index_ndim = (int)n;
obj = NULL;
index_ndim = unpack_indices(index, raw_indices);
if (index_ndim == -1) {
return -1;
}

/*
Expand All @@ -275,14 +384,7 @@ prepare_index(PyArrayObject *self, PyObject *index,
goto failed_building_indices;
}

/* Check for single index. obj is already set then. */
if ((curr_idx != 0) || (obj == NULL)) {
obj = PyTuple_GET_ITEM(index, get_idx++);
}
else {
/* only one loop */
get_idx += 1;
}
obj = raw_indices[get_idx++];

/**** Try the cascade of possible indices ****/

Expand Down Expand Up @@ -686,20 +788,15 @@ prepare_index(PyArrayObject *self, PyObject *index,
*ndim = new_ndim + fancy_ndim;
*out_fancy_ndim = fancy_ndim;

if (make_tuple) {
Py_DECREF(index);
}
multi_DECREF(raw_indices, index_ndim);

return index_type;

failed_building_indices:
for (i=0; i < curr_idx; i++) {
Py_XDECREF(indices[i].object);
}
fail:
if (make_tuple) {
Py_DECREF(index);
}
multi_DECREF(raw_indices, index_ndim);
return -1;
}

Expand Down
0