Repository navigation
ENH: Nditer as context manager - #9998
Conversation
a68c7ef to
12d6caf
Compare
|
Is it possible to create a simple custom context manager for It may also be easier do this in python instead of trying to modify (incidentally, |
|
IHMO just reimplement nested for python 3 and return that: |
|
Rare case where Python 2 has a better API. |
There was a problem hiding this comment.
Is this related to np.nditer? (edit: I see, separate commit)
There was a problem hiding this comment.
Sorry if this is a naive question, I'm just coming back to this:
Why is this line needed? Doesn't the context manager exit take care of any needed writebacks? Furthermore, let's say that within the context manager the user creates a view of the array they got from nditer, and writes to the view (Thus calling Writeback) and then the context manager exits, doesn't that double-call the writeback code?
This PR segfaults on the following for me:
a = np.arange(24, dtype='f8').reshape(2, 3, 4).T
with np.nditer(a, [],[['readwrite', 'updateifcopy']],casting='same_kind',op_dtypes=[np.dtype('f4')]) as i:
i.operands[0][...] = 3
i.operands[0][...] = 14which I suspect is related to my comment, but I haven't checked carefully.
There was a problem hiding this comment.
good catch. Added your test and fixed in c6a1a2c6d09. The context manager does indeed clean up, but this function can possibly allocate a new ndarray with writebackifcopy semantics that need to be cleaned up. The fix was to call the resolve function iff a new view is created
There was a problem hiding this comment.
Or even just a char for each flag - we don't care about efficiency here.
Perhaps
char has_writebackifcopy;
enum {
CONTEXT_BEFORE,
CONTEXT_INSIDE,
CONTEXT_AFTER
} context_state;
There was a problem hiding this comment.
Would be better as npyiter_enter since this also sets flags.
There was a problem hiding this comment.
Why does this clear ITR_HAS_WRITEBACKIFCOPY?
There was a problem hiding this comment.
IIUC, PyArray_ResolveWritebackIfCopy is called, so there's no need to worry about writing back.
There was a problem hiding this comment.
So what does the following do ?
it = np.nditer(...)
with it:
next(it)
with it:
# do stuff
It seems to me that we'll forget that we were supposed to be in a context manager the second time around
There was a problem hiding this comment.
Obviously that should be fixed by moving the constructor logic into __enter__.
There was a problem hiding this comment.
IMO calling __enter__ the second time should raise an exception since writebackifcopy resolution would have already been triggered. Is it common to try to re-enter a context manager? In my limited experience I have not seen code like that
There was a problem hiding this comment.
Raising an exception sounds reasonable to me - as long as it doesn't fail silently
There was a problem hiding this comment.
now raises an error, tested in 9a45b3d055
There was a problem hiding this comment.
Braces and/or indentation needed here
There was a problem hiding this comment.
Why not declare f as NewNpyArrayIterObject like you do in npy_iter_self?
There was a problem hiding this comment.
Why is this desirable? How do operands interact with WRITEBACK_IF_COPY? Do they contain the temporary, or the target of the writeback?
There was a problem hiding this comment.
IIUC, you must call PyArray_ResolveWritebackIfCopy only once. I think operands contain the temporary copy, they operate on it, and then when the context has exited, the results are written back. Obviously they cannot be written back if the context has already exited.
There was a problem hiding this comment.
Not only that, but once they have been written back, the temporary scratch buffer is removed. Now the whole reason for the writeback semantics have been undone, for instance the operand dtype conflicts with the underlying base array's dtype. Reading from the operand will most likely retrieve the wrong value, or writing to it will corrupt the data.
There was a problem hiding this comment.
Would be ideal to recreate the scratch buffer in every __enter__. But fixing the deprecation is a higher priority IMHO.
There was a problem hiding this comment.
So to be clear, operands contains the temporary scratch buffer, not the original array?
There was a problem hiding this comment.
I think operands contain the temporary copy
There was a problem hiding this comment.
operands are ndarrays that have the original array as a base attribute. Since they have the WRITEBACKIFCOPY flag, they hold another scratch buffer until PyArray_ResolveWritebackIfCopy is called
There was a problem hiding this comment.
del it might be clearer here
There was a problem hiding this comment.
That's one possibility. I think people were looking for a one-line solution and Python 3 may require a two-line solution.
There was a problem hiding this comment.
I don't have a good enough understanding of nested_iters to know what makes sense here. Is it documented anywhere? Do the nditers it returns depend on each other? If so, isn't it problematic to construct the inner iter before calling __enter__ on the outer one? (which is exactly the reason that python 3 removed contextlib.nested)
There was a problem hiding this comment.
It is part of the public API, but it isn't documented.
There was a problem hiding this comment.
It would be nice if it was at least internally documented. Right now, I have very little idea of what it does.
There was a problem hiding this comment.
I would prefer to resolve the nested_iter issue in a separate pull request. I have filed issue #10009 a a reminder to do so. AFAICT the c-api function NpyIter_NestedIters is not part of the public API, so we could fix this on the python level only
There was a problem hiding this comment.
heh, in the end it is enough to do what @eric-wieser suggested in the first comment, simple and clear. I left the issue open since the function is not documented
There was a problem hiding this comment.
Unfortunately, AFAICT there are circumstances where nditer can use UPDATEIFCOPY even if "updateifcopy" isn't passed, like if you have a writable array that overlaps with another array.
Suggested text:
Under certain conditions, nditer must be used in a context manager
-----------------------------------------------
When using an nditer with the `"writeonly"` or `"readwrite"` flags, there are some
circumstances where nditer doesn't actually give you a view onto the writable
array. Instead, it gives you a copy, and if you make changes to the copy, nditer later
writes those changes back into your actual array. Currently, this writeback depends
on when the array objects are garbage collected, which makes this API error-prone
on CPython and entirely broken on PyPy. Therefore, `nditer` can now be used
as a context manager (`with np.nditer(...) as it: ...`), and in the future this will
be required whenever using `nditer` with writable arrays.There was a problem hiding this comment.
Note also that I'm suggesting here that we actually make the criterion for issuing deprecation warnings (now) and raising an error (later) be no context manager + writable arrays, since that's so much easier to explain to users than the actual edge cases. (And in particular if you have writable arrays then it's easy to get into a situation where in testing you never hit the UPDATEIFCOPY path, but then in production...)
There was a problem hiding this comment.
I tried to couch the wording in a way that did not limit it to the single flag. Requiring any writable nditer to be used inside a context manager pretty much means any nditer must be used that way. Is a change that deep acceptable/desired?
There was a problem hiding this comment.
The problem is that anyone who uses writable nditer might need this depending on details of the input arrays memory layout (in particular whether they overlap). I think forcing everyone to change at once with a nice warning message is better than forcing people to change piecemeal as they accidentally hit the edge case one by one.
Fortunately almost no-one uses nditer :-)
There was a problem hiding this comment.
I went through all the hits for nditer on searchcode.com, and only found ~15 projects, of which only ~5 use writeonly/readwrite. I'm sure there are others not in their corpus, but we're still only talking about probably ~dozens of pieces of code worldwide that need updating.
There was a problem hiding this comment.
If array_dealloc sees WRITEBACKIFCOPY then that's just a bug, right? Shouldn't we issue a RuntimeWarning or something? (Really it ought to raise an exception, but since you can't do that from array_dealloc a noisy warning is a reasonable alternative.)
There was a problem hiding this comment.
fixed in 7e5c2a7e6, warning message adjusted to indicate the root cause could be nditer
There was a problem hiding this comment.
Isn't this just our PyUString_FromString macro?
There was a problem hiding this comment.
I'm assuming view can be NULL here from the XDECREF below. Is this save if view is NULL?
There was a problem hiding this comment.
PyArray_DiscardWritebackIfCopy(arr) does check if arr is NULL
There was a problem hiding this comment.
This second condition is always true, right, because there's no way to be inside the context already here?
There was a problem hiding this comment.
gaah, this is a bug. I need to check the operands and set iter->has_writebackifcopy appropriately
There was a problem hiding this comment.
Nit: elsewhere we write this 2017-11-10 or 2017-Nov-10
There was a problem hiding this comment.
Should this call Resolve even if an error occurs? (args doesn't contain Py_Nones)
Or would it be better to Discard?
There was a problem hiding this comment.
correct, I did not properly handle the error condition. TODO
4299e5e to
7eb8aed
Compare
|
One overall comment - it's very rare to have something in python that is required to be a context manager - for instance, the Perhaps we should add
|
|
I have no strong opinion on |
1e152b4 to
38f7a34
Compare
38f7a34 to
c26d273
Compare
|
All right. I'm going to do one last pass through the code soon, and then I am aiming to merge sometime in the next few days. |
| if (op_flags[iop] & NPY_ITER_READWRITE) { | ||
| self->needs_context_manager = 1; | ||
| } | ||
|
|
There was a problem hiding this comment.
also, all lines related to needs_context_manager can be removed now.
|
All right, I did my pass. Just a few nits, and removal of After you fix those last few, I will merge. |
94730b0 to
fa9a741
Compare
|
All right! Here goes.... Thank you so much for bearing with all the reviews, I think it is quite a nice PR now. |
| should be incremented). Whenever you add an item to one of the dict, the API | ||
| needs to be updated. | ||
| needs to be updated in both setup_common.py and by adding an appropriate | ||
| entry to cversion.txt (generate the hash via "python cversions.py". |
There was a problem hiding this comment.
What is "best practice" when fixing a merged PR, a new PR or modifying this one again?
There was a problem hiding this comment.
You can't modify an existing PR once it's merged.
What I normally do is keep working in the same branch, but open a new PR
| """ | ||
| close() | ||
|
|
||
| Resolve all writeback semantics in writeable operands. |
There was a problem hiding this comment.
Perhaps reference the with statement here.
| ============= | ||
|
|
||
| ``NpyIter_Close`` has been added and should be called before | ||
| ``NpyIter_Dealloc`` to resolve possible writeback-enabled arrays. |
There was a problem hiding this comment.
Should this be NpyIter_Deallocate?
| i.operands[0][:] = 2 | ||
| assert_equal(au, [2]*6) | ||
|
|
||
| i = None # should not raise a DeprecationWarning |
There was a problem hiding this comment.
nit: PEP8 wants two spaces before the #. Also, del i might be clearer.
| /* TODO: enable this once a solution for UPDATEIFCOPY | ||
| * and nditer are resolved, also pending the fix for GH7054 | ||
| */ | ||
| /* 2017-Nov-10 1.14 */ |
There was a problem hiding this comment.
I think this needs an additional comment for the non-pypy deprecation
There was a problem hiding this comment.
What would you suggest? Leaving the "GH7054" issue reference as a hint?
There was a problem hiding this comment.
/* 2017-11-10 1.14 (only in pypy) */
/* 2018-04-21 1.15 (all python implementations) */
| /* Flag indicating iteration started/stopped */ | ||
| char started, finished; | ||
| /* iter must used as a context manager if writebackifcopy semantics used */ | ||
| char managed; |
There was a problem hiding this comment.
The comment above this line is no longer correct, right?
| PyErr_SetString(PyExc_ValueError, "cannot reuse iterator after exit"); | ||
| return NULL; | ||
| } | ||
| self->managed = CONTEXT_INSIDE; |
There was a problem hiding this comment.
Should probably also check that you're not re-entering the iterator
There was a problem hiding this comment.
Either that, or allow re-entrant withs, but only cleanup on the outermost one.
There was a problem hiding this comment.
Reentrant is OK (test added), multiple cleanups are harmless. Once the cleanup is finished, we set self->managed = CONTEXT_EXITED so reentering a closed nditer fails (there is a test for that in test_nditer.py, line 2847
There was a problem hiding this comment.
I guess the question is, is this the behavior we expect?
with it:
with it:
do_stuff_with_it() # ok
do_stuff_with(it) # error: already closed
It does seem to match how contextlib.closing works, so I suppose is consistent.
| * Returns 0 on success, -1 on failure | ||
| */ | ||
| NPY_NO_EXPORT int | ||
| NpyIter_Close(NpyIter *iter) |
There was a problem hiding this comment.
Is it safe to call this multiple times?
There was a problem hiding this comment.
the first call will resolve all writebacks, so if there was a writeback scratch buffer now the iterator operand's base attribute will be None and the operand's data will be independent from the original writeable array data. Subsequent calls will no do anything
|
|
||
| if (self->managed == CONTEXT_EXITED) { | ||
| PyErr_SetString(PyExc_ValueError, | ||
| "Iterator is closed"); |
There was a problem hiding this comment.
Shouldn't this happen after .close() is called, not just after __exit__ is called?
DOC: cleanup documentation, continuation of nditer PR #9998
Resolves issue #9714 by enabling use of nditer as a context manager. Code like this:
now becomes
No more need to do the
it = Noneassignment to write the data back to the original array. Tests were adjusted. In addition, DeprecationWarning will be raised if a nditer should be using this code pattern and is not, or if the nditeritis used outside the context manager.This also allows completing the confusing part of pull request #9639, and now any call to
PyArray_SetUpdateIfCopyBasewill emit a DeprecationWarning.I am not really happy with the way this affects
nested_iters, since it returns a tuple there is no convienient way to handle the context manager there, that is why the test tweak is in a seperate commit.