RecursionError on macOS with tkinter moving objects - python

I am learning with tkinter, and that involves fluid motion of shapes. That's where I am stuck now.
I get inconsistent recursion errors. The code works no problem on Windows 1O, but on my macOS it crashes.
Furthemore, if the time between recursions is ≥ 17 milliseconds, it just stops making errors and runs as intended. I've simplified the code, just to contain the one problem.
from tkinter import *
master = Tk()
canvas = Canvas(bg = "gray", width = 1000, height = 800)
canvas.pack()
cara = canvas.create_line(100,100,900,100, width = 5, fill = "red")
def moveCara():
canvas.move(cara, 0,1)
canvas.after(16, moveCara) # Here is the time setting, change it to 17 and the thing
# does not crash.
canvas.update()
moveCara()
mainloop()
This code causes crash on my MacBook:
RecursionError: maximum recursion depth exceeded during compilation
However, if I change the time in canvas.after() to 17 or greater, everything works. why is that?

The problem is this line of code:
canvas.update()
You should definitely not be calling update inside a function called from an event handler (and running a function via after is considered an event handler). It's completely unnecessary to make your program work and needs to be removed.
What is happening is that your call to after adds some work to be done by mainloop after a given amount of time. When you call update, it performs that work if the given amount of time has elapsed, otherwise it might not do anything at all.
If your computer is slow enough, then by the time you call update it will be time for the next frame of your animation. That causes moveCora to be called again via canvas.update() before the first call returns. And that is why you get a recursion error.
I wasn't able to duplicate the problem on my machine at 16ms, but I was able to reproduce it when I moved the time to 5ms. You must have a machine that takes more than 16ms to move the line and then call after. When I remove the call to canvas.update() I am able to set the value down to 1ms without a problem.

Related

Regarding the window.after() finction in tkinter

import tkinter as tk
window = tk.Tk()
def function_example():
a= 2
My question is what is the difference between these two following code executions, and why are they producing different results?
Scenario 1:
window.after(1000, function_example)
VS.
Scenario 2:
window.after(1000)
function_example()
I thought both should produce the same outcome, but they are producing different outcomes.
Perhaps easier to understand with a slightly different example.
import tkinter as tk
window = tk.Tk()
def function_example( note ):
print( note )
window.after( 5000, function_example, 'Called from after' )
function_example('1st call outside after.')
window.after( 1000 ) # Stops execution of the GUI for 1 second
function_example( '2nd call outside after.' )
window.mainloop()
Results are
1st call outside after. # Prints as the GUI is being created
2nd call outside after. # Prints 1 second after that
Called from after # 5 seconds after the GUI is created
No python code is processed for 1 second after the after method is called without a callback function being specified.
The GUI window opening as the '2nd call outside after.' is printed helps to demonstrate this.
after with a single argument arguments works exactly like time.sleep - it will cause the program to freeze for the given duration. Tkinter is not able to respond to any events during that time, including events that tell it to redraw the window. When the given time period has expired, the function will return and any following code can continue.
after called with two or more arguments will add the second argument on a queue to be run after a delay given as the first parameter. In this case, the call to after returns immediately and your application will continue executing. As soon as possible after the given delay has elapsed, the function will be run.

Create a Tkinter window auto update without using "after()" function

I'm using Tkinter for a small overlay on a screen that got to be update every 1 or 2 second. I search a lot about it and find the after() function that could be execute after the mainloop. But this one doesn't work quitely, the idea is to call a after() function witch contain another after() function and the main function that we want to execute in the loop, like that :
def my_functions():
print('task done')
ws.after(1000, my_functions)
ws.after(1000, my_functions())
But this one got a limit of "function calling itself" of 992, that means my window will refresh her value only for 16 min 32 sec (or 992 sec) then will crash.
I could maybe destroy and recreate all my window and loop before the limit but I doesn't love this solution and I would have to work a lot on it, I would prefer a easyest solution, but I'm searching for 3 days and doesn't find anything good.
With trying to reproduce the following error :
RecursionError: maximum recursion depth exceeded while calling a Python object
I find the solution, so for any other I will write it.
Like you said, you can't use the after() method if you pass an argument like that:
root.after(1000, task(arg))
If you want to pass an argument you have to do like that:
root = Tk()
x = 0
def task(x):
x += 1
print(x)
root.after(2000, task, x) # reschedule event in 2 seconds
root.after(2000, task, x)
root.mainloop()
If you use the first solution, it's only rise the error after the of the maximum recursion, tkinter doesn't find any mistake in the formulation and you can still execute your code.
Which is a bit weird for me, but I just doesn't understand quitely the documentation when I read it. And with re-reading it seems logic now.

Problems with update and update_idletasks

I've been learning python for a month now and run into my first brick wall. I have a large art viewer GUI program and at one point want to put an image on screen with a countdown counter-approx every 5 secs. I thought of a code such as the one below The problem is that this uses update and all my reading says that update is bad (starts a new event loop (?)) and that I should use update_idletasks. when I replace update with update_idletasks in the code below the countdown button is not visible until it reaches single figures, update superficially works fine. But also the q bound key calls the subroutine but has no effect
from tkinter import *
import sys
import time
root = Tk()
def q_key(event):
sys.exit()
frame=Frame(root, padx=100, pady=100, bd=10, relief=FLAT)
frame.pack()
button=Button(frame,relief="flat",bg="grey",fg="white",font="-size 18",text="60")
button.pack()
root.bind("q",q_key)
for x in range(30, -1, -5) :
button.configure(text=str(x))
button.update()
print(x)
button.after(5000)
root.mainloop()
In this case you don't need update nor update_idletasks. You also don't need the loop, because tkinter is already running in a loop: mainloop.
Instead, move the body of the loop to a function, and call the function via after. What happens is that you do whatever work you want to do, and then schedule your function to run again after a delay. Since your function exits, tkinter returns to the event loop and is able to process events as normal. When the delay is up, tkinter calls your function and the whole process starts over again.
It looks something like this:
def show(x):
button.configure(text=x)
if x > 0:
button.after(5000, show, x-5)
show(30)

tkinter window doesn't display - if .event time is 0 inside loop

I was racking my brain a bit trying to figure out why a tkinter window would only appear after I had stopped my script. Turns out, it won't appear if the delay time in my root.after (that is within my infinite fruity loop) was set to 0. Setting it to 1 or higher caused it to work correctly. Is this a bug or am I missing something important about how .after works? I'm running this with Python 2.7 in Anaconda on mac OS.
import time
import Tkinter as tk
import random
root = tk.Tk()
root.title("random numbers")
root.geometry("220x220+5+5")
frame = tk.Frame(root, width=210, height=210)
frame.pack()
luckynumber = tk.IntVar()
label1 = tk.Label(frame, text="random number").pack(side=tk.LEFT)
display1 = tk.Label(frame, textvariable=luckynumber)
display1.pack( side=tk.LEFT )
def askrandy():
randy = random.randrange(0, 100, 1)
luckynumber.set(randy)
def fruityloop():
time.sleep(.5)
askrandy()
root.after(1, fruityloop)
root.after(0, fruityloop)
root.mainloop()
Second question: this code doesn't run very smoothly. Seeing as it's quite simple, I assumed it would be pretty solid. But I find that it takes a couple seconds to get started and moving the window around causes it to stutter as well. Would this work better with my main loop run as a class?
This is normal behavior.
Tkinter maintains a queue of work to be done when it goes idle. This is the "idle" queue.
When you call after, the function you supply is added to this queue. When the main event loop (or a call to after_idle) processes the queue, it looks for items on the queue that should be run based on the current time and the time that the item should be run. All items that are due to be run are run before processing of the queue stops.
If one of those adds an item to the queue with a value of zero it will be run since its time is due. If that item itself adds an item to the queue, then you take one item off of the queue and immediately put one one so the queue will never become empty. If the queue never becomes empty, tkinter isn't able to process other types of events.
The reason that the program seems slow and jerky is because of the call to sleep. When you call sleep, tkinter does exactly that: it sleeps. It cannot process any events, even events that simply refresh the window. If you want askrandy to be called once every half second, you should simply call after with a value of 500, rather than call it with a value of zero and then sleep for half a second.
Whether the main window is a class or not will not affect your program all all. You simply need to stop using sleep, and provide sane values to after. If you are trying to show a simple animation, a value of 30 is about as small as you need to go.
This is how it should looks without sleep(). But I don't know if it can help. It works fast on Linux.
If you run code in IDLE then you may have problem because it uses Tkinter to display windows and runs own mainloop() but Tkinter should run only one mainloop(). You can try directly in console python script.py.
import Tkinter as tk
import random
# --- functions ---
def fruityloop():
randy = random.randrange(0, 100, 1)
luckynumber.set(randy)
# run again after 500ms = 0.5s
root.after(500, fruityloop)
# --- main ---
root = tk.Tk()
root.title("random numbers")
root.geometry("220x220+5+5")
luckynumber = tk.IntVar()
frame = tk.Frame(root, width=210, height=210)
frame.pack()
label = tk.Label(frame, text="random number")
label.pack(side=tk.LEFT)
display = tk.Label(frame, textvariable=luckynumber)
display.pack(side=tk.LEFT)
# run first time
fruityloop()
root.mainloop()

Memory leak when embedding and updating a matplotlib graph in a PyQt GUI

I am trying to embed a matplotlib graph that updates every second into a PyQt GUI main window.
In my program I call an update function every second using threading.Timer via the timer function shown below. I have a problem: my program grows bigger every second - at a rate of about 1k every 4 seconds. My initial thoughts are that the append function (that returns a new array in update_figure) does not delete the old array? Is it possible this is the cause of my problem?
def update_figure(self):
self.yAxis = np.append(self.yAxis, (getCO22()))
self.xAxis = np.append(self.xAxis, self.i)
# print(self.xAxis)
if len(self.yAxis) > 10:
self.yAxis = np.delete(self.yAxis, 0)
if len(self.xAxis) > 10:
self.xAxis = np.delete(self.xAxis, 0)
self.axes.plot(self.xAxis, self.yAxis, scaley=False)
self.axes.grid(True)
self.i = self.i + 1
self.draw()
This is my timer function - this is triggered by the click of a button in my PyQt GUI and then calls itself as you can see:
def timer(self):
getCH4()
getCO2()
getConnectedDevices()
self.dc.update_figure()
t = threading.Timer(1.0, self.timer)
t.start()
EDIT: I cant post my entire code because it requires a lot of .dll includes. So i'll try to explain what this program does.
In my GUI I want to show the my CO2 value over time. My get_co22 function just returns a float value and I'm 100% sure this works fine. With my timer, shown above, I want to keep append a value to a matplotlib graph - the Axes object is available to me as self.axes. I try to plot the last 10 values of the data.
EDIT 2: After some discussion in chat, I tried putting the call to update_figure() in a while loop and using just one thread to call it and was able to make this minimal example http://pastebin.com/RXya6Zah. This changed the structure of the code to call update_figure() to the following:
def task(self):
while True:
ui.dc.update_figure()
time.sleep(1.0)
def timer(self):
t = Timer(1.0, self.task())
t.start()
but now the program crashes after 5 iterations or so.
The problem is definitely not with how you are appending to your numpy array, or truncating it.
The problem here is with your threading model. Integrating calculation loops with a GUI control loop is difficult.
Fundamentally, you need your GUI threading to have control of when your update code is called (spawning a new thread to handle it if necessary) - so that
your code does not block the GUI updating,
the GUI updating does not block your code executing and
you don't spawn loads of threads holding multiple copies of objects (which might be where your memory leak comes from).
In this case, as your main window is controlled by PyQt4, you want to use a QTimer (see a simple example here)
So - alter your timer code to
def task(self):
getCH4()
getCO2()
getConnectedDevices()
self.dc.update_figure()
def timer(self):
self.t = QtCore.QTimer()
self.t.timeout.connect(self.task)
self.t.start(1000)
and this should work. Keeping the reference to the QTimer is essential - hence self.t = QtCore.QTimer() rather than t = QtCore.QTimer(), otherwise the QTimer object will be garbage collected.
Note:
This is a summary of a long thread in chat clarifying the issue and working through several possible solutions. In particular - the OP managed to mock up a simpler runnable example here: http://pastebin.com/RXya6Zah
and the fixed version of the full runnable example is here: http://pastebin.com/gv7Cmapr
The relevant code and explanation is above, but the links might help anyone who wants to replicate / solve the issue. Note that they require PyQt4 to be installed
if you are creating a new figure for every time this is quite common.
matplotlib do not free the figures you create, unless you ask it, somethink like:
pylab.close()
see How can I release memory after creating matplotlib figures

Categories