-
-
Notifications
You must be signed in to change notification settings - Fork 7.9k
Fix passing iterator as frames to FuncAnimation #13679
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
Conversation
lib/matplotlib/animation.py
Outdated
self._iter_gen = lambda: iter(frames) | ||
# _iter_gen() must return a new iterator on each call to support | ||
# repeat=True. We need to copy since iter() does not ensure this. | ||
self._iter_gen = lambda: iter(copy.copy(frames)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Strictly speaking, copy would only be necessary if frames not (iter(frames) is frames)
, but I don't think the added complexity is worth trying to save a copy. frames
is typically not worse than a list of 1000 elements. Copying that is negligable compared to the fact that we have to do a draw step for every frame when rendering the animation.
6a6e361
to
16efd80
Compare
lib/matplotlib/animation.py
Outdated
@@ -1545,6 +1546,11 @@ def func(frame, *fargs) -> iterable_of_artists | |||
- If an iterable, then simply use the values provided. If the | |||
iterable has a length, it will override the *save_count* kwarg. | |||
|
|||
Note that when using ``repeat=True`` (the default) the iterable must | |||
be usable times (either ``iter(iterable) != iterable`` or the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
*be usable multiple times
16efd80
to
29e4881
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems reasonable. Though based on the complexity of the argument handling, I'm regretting some of the original (now > 10 years old) design.
lib/matplotlib/animation.py
Outdated
@@ -1545,6 +1546,12 @@ def func(frame, *fargs) -> iterable_of_artists | |||
- If an iterable, then simply use the values provided. If the | |||
iterable has a length, it will override the *save_count* kwarg. | |||
|
|||
Note that when using ``repeat=True`` (the default) the iterable must | |||
be usable multiple times (either ``iter(iterable) is not iterable`` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is really inscrutable and very jargon-y. Can you provide examples of what works and doesn't work? I think I'm more knowledgable than the average matplotlib user, and I have no practical idea what you mean here. I assume a list or np.array is OK? What is a "generator expression"? Can you give an example.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What in particular do you find problematic? iter(iterable) is not iterable
or "copyable"?
Technically, we must be able to call:
for elem in iterable:
# do something
mutliple times for repeat=True
. While this works with many common containers like lists and numpy arrays, it's not true for every iterable. Thing is that these non-reusable iterables are all more or less advanced. E.g.
iterable = iter(range(10))
- Generator expressions:
iterable = (x**2 for x in range(10))
- Generators
def my_iter(n): for i in range(n): yield i iterable = my_iter(3)
- I can create my own class that implements the iterator protocol but does not fulfill the above requirement.
There are two ways to try and get around the issue:
- use
iterable2 = iter(iterable)
- use
iterable2 = copy.copy(iterable)
We try both, but depending on the iterable both, either one, or none of these work.
I have no idea how to express this more clearly than what I've written in the docstring. If you do, you're welcome to improve the text.
That said, this is just an additional note on some technical details. It doesn't matter too much if the novice user does not understand this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Something that makes it clear most people don't need to read the rest of the sentence? Something like:
"Note some advanced and custom iterables cannot be re-used or copied, so the default repeat=True
won't work; i.e. a list or numpy array will work, but the moree advanced iterable=iter(range(10))
may not. Its possible to work around this by passing as frames = iter(iterable)
."
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Its possible to work around this by passing as
frames = iter(iterable)
.
Unfortunately, this is not true. There are iterables, where iter(iterable) is iterable
(e.g. iter(range(10))
). If this was possible, I would have used it internally, and could have saved all the hassle of describing that not all iterables can be made to work with repeat=True
.
Note also, that iter(range(10))
is not something you would find in real-life code. Within our discussion and the original bug report #13676, it just serves as an over-simplified example for an arbitrary iterator. As such I wouldn't want to use it in the docs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jklymak I've rephrased the section please check if that's more understandable.
29e4881
to
17fef88
Compare
17fef88
to
0f3d561
Compare
Can you not use if repeat:
def iter_frames(frames=frames):
while True:
this, frames = itertools.tee(frames, 2)
yield from this
self._iter_gen = iter_frames() |
Superseeded be #14068. |
PR Summary
Fixes #13676.
The problem occurs in
FuncAnimation
when usingrepeat=True
(the default) and passing an iterator asframes
. Internally, we use aiter(frames)
for each cycle, however, sincei = iter(iterable); iter(i) is i
, the iterator values are used up after the first animation cycle.Updated:
Fix: Ensure to use a new iterator for each cycle. This can be achieved in two ways:
iter(frames)
does already yield a new iterator (iter(frames) is not frames
)If neither of both is possible error out with a reasonable error message.