0

I set up a Jupyter Notebook where I'm running a recursive function that clears the output like so, creating an animated output:

from IPython.display import clear_output

active = True
def animate:
    myOutput = ""
    # do stuff
    clear_output(wait=True)
    print(myOutput)
    sleep(0.2)
    if active:
        animate()

and that's working perfectly.

But now I want to add in one more step: A speed toggle. What I'm animating is a debugging visualization of a cursor moving through interpreted code as an interpreter I'm writing parses that code. I tried conditional slow-downs to have more time to read what's going on as the parsing continues, but what I really need is to be able to click a button to toggle the speed between fast and slow. Maybe I'll use a slider, but for now I just want a button for proof of concept.

This sounds simple enough. Note that I'm writing this statefully as a class because I need to read / write the state from within another imported class.

Jupyter block 1:

import ipywidgets as widgets
from IPython.display import display
out = widgets.Output()

class ToggleState():
    def __init__(self):
        self.button = widgets.Button(description="Toggle")
        self.button.on_click(self.toggle)
        display(self.button)
        self.toggleState = False
        print("Toggle State:", self.toggleState)

    def toggle(self, arg): # arg has to be accepted here to meet on_click requirements
        self.toggleState = not self.toggleState
        print("Toggle State:", self.toggleState)

    def read(self):
        return self.toggleState

toggleState = ToggleState()

Then, in Jupyter block 2, note I decided to to do this in a separate block because the clear_output I'm doing with the animate func clears the button if it's in the same block, and therein lies the problem:

active = True
    def animate:
        myOutput = ""
        # do stuff
        clear_output(wait=True)
        print(myOutput)
        if toggleState.read():
            sleep(5)
        else:
            sleep(0.2)
        if active:
            animate()

But the problem with this approach was that two blocks don't actually run at the same time (without using parallel kernels which is way more complexity than I care for) so that button can't keep receiving input in the previous block. Seems obvious now, but I didn't think about it.

How can I clear input in a way that doesn't delete my button too (so I can put the button in the animating block)?

Edit:

I thought I figured the solution, but only part of it:

Using the Output widget:

out = widgets.Output()

and

with out:
    clear_output(wait=True) # clears only the logged output
    # logging code

We can render to two separate stdouts within the same block. This works to an extent, as in the animation renders and the button isn't cleared. But still while the animation loop is running the button seems to be incapable of processing input. So it does seem like a synchronous code / event loop blocking problem. What's the issue here?

Do I need an alternative to sleep that frees up the event loop?

Edit 2:

After searching async code in Python, I learned about asyncio but I'm still struggling. Jupyter already runs the code via asyncio.run(), but the components obviously have to be defined as async for that to matter. I defined animate as async and tried using async sleeps, but the event loops still seems to be locked for the button.

J.Todd
  • 707
  • 1
  • 12
  • 34
  • I don't know asyncio or Jupyter, I would probably try doing something like `threading.Thread(target=animate).start()` to launch a function "in the background". But I'm mostly commenting because of your `animate` function. You should probably just wrap its contents into a `while active:` block and remove the `if active:` at the end. Recursion doesn't make sense here and after a couple of minutes of running the animation it will just get you a `RecursionError: maximum recursion depth exceeded`. – Czaporka Nov 17 '20 at 01:46
  • @Czaporka I see, interesting. JavaScript (my main language) doesnt have that issue and recursion is the common way of running an animation loop in JS land. Sort of makes sense to do recursion because JS exposes a method that allows you to trigger your next frame process when the animation frame (GPU task) has finished rendering. That kind of event based cycle seems impossible with Python if what you say is true. – J.Todd Nov 17 '20 at 10:48
  • Interesting, I would say this sounds like JS is capable of [tail call optimization](https://en.wikipedia.org/wiki/Tail_call) but I hear that [it is currently only supported in Safari/Webkit](https://stackoverflow.com/questions/37224520/are-functions-in-javascript-tail-call-optimized), so I don't know how this might work. If this is indeed due to the tail call optimization magic - Python doesn't support that, so I guess it seems pretty obvious that with all those function calls that never return you're eventually gonna run out of stack. – Czaporka Nov 17 '20 at 12:31
  • @Czaporka thanks for pointing it out, I've been studying assembly lately and now that you mention it it does make sense that you'd run out of stack with recursion without some sort of trick, like the one you linked. – J.Todd Nov 17 '20 at 13:52

0 Answers0