'Tkinter's .after() and recursion

To update a widget in time I use the .after() method, usually in the following form:

def update():
    do_something()
    <widget>.after(<delay>, update)

It is my understanding that the widget waits for a certain amount of time and then executes the update() function, at the end of which the widget waits once again before re-executing the function and so on.

This seems to me a lot like recursion. So, the question is: Does .after() actually work by means of recursion or not?

If it does, then there is a limit to the depth of recursion, but the following example should prove that such limit is never reached:

from tkinter import *

counter = 0

def count():
    global counter
    counter += 1
    lbl.config(text=counter)
    root.after(10, count)

root = Tk()
lbl = Label(root, text='0')
lbl.pack()
Button(root, text='Start count', command=count).pack()
root.mainloop()

In my system the limit to the depth of recursion is 1000, but this example goes far beyond that value in a few seconds until I stop it.



Solution 1:[1]

It is my understanding that the widget waits for a certain amount of time and then executes the update() function, at the end of which the widget waits once again before re-executing the function and so on.

The highlighted section is false. after simply places the function on a queue. It doesn't re-execute anything. mainloop simply pops things off of the "after" queue and runs them once.

So, the question is: Does .after() actually work by means of recursion or not?

No. after should have been named add_job_to_queue. It isn't recursion, it simply places a job on a queue.

If it does, then there is a limit to the depth of recursion, but the following example should prove that such limit is never reached:

def count():
    global counter
    counter += 1
    lbl.config(text=counter)
    root.after(10, count)

The reason no limit is reached is, again, because it's not recursion. When you call count by clicking on a button, it does some work and then it adds one item to the "after" queue. The length of the queue is now one.

When the time comes, mainloop will pop that item off of the queue, making the queue have a length of zero. Then, your code adds itself to the queue, making the length one. When the time comes, mainloop will pop that item off the queue, making the queue have a length of zero. Then, ...

Solution 2:[2]

There's no recursion at all in your example, since count() is not called from itself (you're just telling Tk that it needs to call your function after 10ms) but invoked by Tk's main loop ;).

Solution 3:[3]

in my program

keyboard=tk.Tk()
def readsm_s():
   ...
   keyboard.after(30, readsm_s)

readsm_s() is recalled many time, after that there is a error 'maximum recursion depth exceeded while calling a Python object'

find python the default depth of recursion is limited. ? the default is 1000 ?

https://www.codestudyblog.com/cs2112pya/1208015041.html

Solution 4:[4]

Took a look at the python source code, I don't think .after works recursively. I starts new threads using the Tcl library.

def after(self, ms, func=None, *args):
    """Call function once after given time.
    MS specifies the time in milliseconds. FUNC gives the
    function which shall be called. Additional parameters
    are given as parameters to the function call.  Return
    identifier to cancel scheduling with after_cancel."""
    if not func:
        # I'd rather use time.sleep(ms*0.001)
        self.tk.call('after', ms)
    else:
        def callit():
            try:
                func(*args)
            finally:
                try:
                    self.deletecommand(name)
                except TclError:
                    pass
        callit.__name__ = func.__name__
        name = self._register(callit)
        return self.tk.call('after', ms, name)

Modules/_tkinter.c: Registers the function and calls it. The Tk class is builtin class also located in the same file. The API works by calling Tcl library functions.

The function bounded to tk.call is Tkapp_Call:

{"call",                   Tkapp_Call, METH_VARARGS},

The comments for this function explain that this just calls Tcl functions.

/* This is the main entry point for calling a Tcl command.
   It supports three cases, with regard to threading:
   1. Tcl is not threaded: Must have the Tcl lock, then can invoke command in
      the context of the calling thread.
   2. Tcl is threaded, caller of the command is in the interpreter thread:
      Execute the command in the calling thread. Since the Tcl lock will
      not be used, we can merge that with case 1.
   3. Tcl is threaded, caller is in a different thread: Must queue an event to
      the interpreter thread. Allocation of Tcl objects needs to occur in the
      interpreter thread, so we ship the PyObject* args to the target thread,
      and perform processing there. */

Additionally, the arguments are freed when this is called at the end of the function: Tkapp_CallDeallocArgs(objv, objStore, objc);, so if the arguments are recursively used, they would not have been freed after 1 call.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Bryan Oakley
Solution 2 cdonts
Solution 3 lam vu Nguyen
Solution 4 BrockLee