Thanks to visit codestin.com
Credit goes to github.com

Skip to content

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

Closed

Conversation

timhoffm
Copy link
Member

@timhoffm timhoffm commented Mar 16, 2019

PR Summary

Fixes #13676.

The problem occurs in FuncAnimation when using repeat=True (the default) and passing an iterator as frames. Internally, we use a iter(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:

  • either iter(frames) does already yield a new iterator (iter(frames) is not frames)
  • or we copy frames.

If neither of both is possible error out with a reasonable error message.

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))
Copy link
Member Author

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.

@timhoffm timhoffm force-pushed the fix-funcanimation-frames-iter branch 2 times, most recently from 6a6e361 to 16efd80 Compare March 17, 2019 14:49
@@ -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
Copy link

Choose a reason for hiding this comment

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

*be usable multiple times

@timhoffm timhoffm force-pushed the fix-funcanimation-frames-iter branch from 16efd80 to 29e4881 Compare March 17, 2019 15:48
Copy link
Contributor

@dopplershift dopplershift left a 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.

@tacaswell tacaswell added this to the v3.2.0 milestone Mar 19, 2019
@@ -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``
Copy link
Member

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.

Copy link
Member Author

@timhoffm timhoffm Mar 23, 2019

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.

Copy link
Member

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)."

Copy link
Member Author

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.

Copy link
Member Author

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.

@timhoffm timhoffm force-pushed the fix-funcanimation-frames-iter branch from 29e4881 to 17fef88 Compare April 2, 2019 06:36
@timhoffm timhoffm force-pushed the fix-funcanimation-frames-iter branch from 17fef88 to 0f3d561 Compare April 2, 2019 06:38
@anntzer
Copy link
Contributor

anntzer commented Apr 2, 2019

Can you not use itertools.tee to perform the "copy" every time? (untested)

if repeat:
    def iter_frames(frames=frames):
        while True:
            this, frames = itertools.tee(frames, 2)
                yield from this
    self._iter_gen = iter_frames()

@timhoffm
Copy link
Member Author

Superseeded be #14068.

@timhoffm timhoffm closed this Apr 28, 2019
@timhoffm timhoffm deleted the fix-funcanimation-frames-iter branch June 10, 2022 21:15
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.

FuncAnimation with generator causes crash on StopIteration
6 participants