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

MAINT: Make the refactor suggested in prepare_index #8278

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 11 commits into from
Jul 18, 2017
Merged
Changes from 1 commit
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
Prev Previous commit
Next Next commit
MAINT: Improve comments, simplify handling of tuple subclasses
  • Loading branch information
eric-wieser committed Jul 16, 2017
commit 68bad6a47f7090a0680ef3b5b48a76623de91df3
143 changes: 78 additions & 65 deletions numpy/core/src/multiarray/mapping.c
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,39 @@ multi_DECREF(PyObject **objects, npy_intp n)
}
}

/**
* Unpack a tuple into an array of new references. Returns the number of objects
* unpacked.
*
* Useful if a tuple is being iterated over multiple times, or for a code path
* that doesn't always want the overhead of allocating a tuple.
*/
NPY_NO_EXPORT NPY_INLINE npy_intp
unpack_tuple(PyTupleObject *index, PyObject **result, npy_intp result_n)
{
npy_intp n, i;
n = PyTuple_GET_SIZE(index);
if (n > result_n) {
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;
}

/* Unpack a single scalar index, taking a new reference to match unpack_tuple */
NPY_NO_EXPORT NPY_INLINE npy_intp
unpack_scalar(PyObject *index, PyObject **result, npy_intp result_n)
{
Py_INCREF(index);
result[0] = index;
return 1;
}

/**
* Turn an index argument into a c-array of `PyObject *`s, one for each index.
*
Expand All @@ -164,45 +197,34 @@ multi_DECREF(PyObject **objects, npy_intp n)
* > 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.
* It might be worth deprecating this behaviour (gh-4434), in which case the
* entire function should become a simple check of PyTuple_Check.
*
* @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.
* @param index The index object, which may or may not be a tuple. This is
* a borrowed reference.
* @param result An empty buffer of PyObject* to write each index component
* to. The references written are new.
* @param result_n The length of the result buffer
*
* @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.
* @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. Use multi_DECREF to
* dispose of them.
*/
NPY_NO_EXPORT npy_intp
unpack_indices(PyObject *index, PyObject *result[2*NPY_MAXDIMS])
unpack_indices(PyObject *index, PyObject **result, npy_intp result_n)
{
npy_intp n, i;
npy_bool commit_to_unpack;

/* 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;
return unpack_tuple((PyTupleObject *)index, result, result_n);
}

/* Obvious single-entry cases */
if (0
if (0 /* to aid macros below */
#if !defined(NPY_PY3K)
|| PyInt_CheckExact(index)
#else
Expand All @@ -213,63 +235,51 @@ unpack_indices(PyObject *index, PyObject *result[2*NPY_MAXDIMS])
|| PyArray_Check(index)
|| !PySequence_Check(index)) {

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

/* Passing a tuple subclass - needs to handle errors */
/*
* Passing a tuple subclass - coerce to the base type. This incurs an
* allocation, but doesn't need to be a fast path anyway
*/
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) {
PyTupleObject *tup = PySequence_Tuple(index);
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 you miss the Py_DECREF for tup here, could have done a recursive call as well (first thought you did) instead of refactoring it out.

Copy link
Member Author

Choose a reason for hiding this comment

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

Thought refactoring it out was more transparent, but yes, I could have.

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed

Copy link
Member

Choose a reason for hiding this comment

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

Also apparently requires a cast because PySequence_Tuple probably returns a PyObject.

if (tup == NULL) {
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;
return unpack_tuple(tup, result, result_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
* typically, a list. We use some somewhat-arbitrary 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]]`)
* list of indices.
*/

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

/*
* For some reason, anything that's long but not too long is turned into
* a single index. The *2 is missing here for backward-compatibility.
* Backwards compatibility only takes effect for short sequences - otherwise
* we treat it like any other scalar.
*
* 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 (n >= NPY_MAXDIMS) {
Py_INCREF(index);
result[0] = index;
return 1;
return unpack_scalar(index, result, result_n);
}

/* In case we change result_n elsewhere */
assert(n <= result_n);

/*
* Some other type of short sequence - assume we should unpack it like a
* tuple, and then decide whether that was actually necessary.
Expand Down Expand Up @@ -314,10 +324,7 @@ unpack_indices(PyObject *index, PyObject *result[2*NPY_MAXDIMS])
else {
/* we partially filled result, so empty it first */
multi_DECREF(result, i);

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

Expand Down Expand Up @@ -361,9 +368,15 @@ prepare_index(PyArrayObject *self, PyObject *index,
int index_type = 0;
int ellipsis_pos = -1;

/*
* The choice of only unpacking `2*NPY_MAXDIMS` items is historic.
* The longest "reasonable" index that produces a result of <= 32 dimensions
* is `(0,)*np.MAXDIMS + (None,)*np.MAXDIMS`. Longer indices can exist, but
* are uncommon.
*/
PyObject *raw_indices[NPY_MAXDIMS*2];

index_ndim = unpack_indices(index, raw_indices);
index_ndim = unpack_indices(index, raw_indices, NPY_MAXDIMS*2);
if (index_ndim == -1) {
return -1;
}
Expand Down
0