Posts: 25
Threads: 12
Joined: Mar 2025
i made a module for the first time but its glitchy
i keep getting this error:
Error: Exception has occurred: UnboundLocalError
cannot access local variable 'foreverfuncs' where it is not associated with a value
File "C:\Users\Azdaghost\Documents\Coding Projects\Python\ghostmansion.py", line 111, in forever
foreverfuncs += func
^^^^^^^^^^^^
File "C:\Users\Azdaghost\Documents\Coding Projects\Python\catchgame.py", line 23, in <module>
script.forever(forever)
~~~~~~~~~~~~~~^^^^^^^^^
UnboundLocalError: cannot access local variable 'foreverfuncs' where it is not associated with a value
code:
import tkinter as tk
import time
import math
window = tk.Tk()
window.title('ghostmansion window')
window.geometry('100x100')
foreverfuncs = []
age = -1
mouse_x = 0
mouse_y = 0
def forever():
for element in foreverfuncs:
element
window.after(0, forever)
def forever_module():
global age
age += 1
window.after(1, forever_module)
def find_mouse_pos(event):
global mouse_x, mouse_y
mouse_x, mouse_y = event.x, event.y
window.bind('<Motion>', find_mouse_pos)
class Project():
def __init__(self, size):
self.size = size
self.name = 'ghostmansion window'
def rename(self, name):
self.name = name
def resize(self, size):
self.size = [size[0], size[1]]
window.geometry(f'{self.size[0]}x{self.size[1]}')
def finish(self):
window.mainloop()
class Painting():
def __init__(self, pos, size, dir, name):
self.pos = pos
self.size = size
self.dir = dir
self.inst = tk.Frame(width=size[0], height=size[1])
# MOTION
def get_data(self, type):
if type == 'x':
return self.pos[0]
if type == 'y':
return self.pos[1]
if type == 'xs':
return self.size[0]
if type == 'ys':
return self.size[1]
if type == 'dir':
return self.dir
def print(self):
self.inst.place(x=self.pos[0], y=self.pos[1])
def move_steps(self, steps):
self.pos[0] += steps * math.sin(self.dir)
self.pos[1] += steps * math.cos(self.dir)
def turn(self, angle):
self.dir += angle
def set_pos(self, x, y):
self.pos[0] = x
self.pos[1] = y
def point_in_dir(self, dir):
self.dir = dir
def change_pos(self, x, y):
self.pos[0] += x
self.pos[1] += y
# LOOKS
def resize_pxl(self, xs, ys):
self.size[0] = xs
self.size[1] = ys
def resize_per(self, perc):
self.size[0] *= perc / 100
class Sensing():
def __init__(self, obj):
self.obj = obj
def touching(frame1, frame2):
x1, y1 = frame1.winfo_x(), frame1.winfo_y()
w1, h1 = frame1.winfo_width(), frame1.winfo_height()
x2, y2 = frame2.winfo_x(), frame2.winfo_y()
w2, h2 = frame2.winfo_width(), frame2.winfo_height()
if (x1 < x2 + w2 and x1 + w1 > x2 and y1 < y2 + h2 and y1 + h1 > y2):
return True
return False
def distance_from(self, frame):
s1 = self.obj.pos[0] - frame.pos[0]
s2 = self.obj.pos[1] - frame.pos[1]
return abs(s1 + s2)
def get_mouse_pos(self):
return [mouse_x, mouse_y]
def get_time(self):
return age
class Script():
def __init__(self):
pass
# CONTROL
def wait(self, milsecs):
time.sleep(milsecs / 100)
def forever(self, func):
foreverfuncs += func
# OPERATORS
def contains(self, text, context):
save = ''
reset = True
for i in range(len(text)):
if text[i] == context[i]:
save += text[i]
elif reset:
save = ''
return save == text
def mod(self, n1, n2):
n1v, n2v = n1, n2
while n1 < n2:
n2 -= n1
return n2
# CONSOLE
def log(self, text):
print(text)
def warn(self, text):
print('WARING:', text)
def error(self, text, end):
print('Error:', text)
if end:
exit()
class Sensing():
def __init__(self):
pass
def touching(self, frame1, frame2):
x1, y1 = frame1.winfo_x(), frame1.winfo_y()
w1, h1 = frame1.winfo_width(), frame1.winfo_height()
x2, y2 = frame2.winfo_x(), frame2.winfo_y()
w2, h2 = frame2.winfo_width(), frame2.winfo_height()
if (x1 < x2 + w2 and x1 + w1 > x2 and y1 < y2 + h2 and y1 + h1 > y2):
return True
return False
def distance_from(self, frame):
s1 = self.obj.pos[0] - frame.pos[0]
s2 = self.obj.pos[1] - frame.pos[1]
return abs(s1 + s2)
def get_mouse_pos(self, dim):
if dim == 'x':
return mouse_x
elif dim == 'y':
return mouse_y
else:
return (mouse_x, mouse_y)
def get_time(self):
return age
Posts: 6,798
Threads: 20
Joined: Feb 2020
Mar-30-2025, 01:50 AM
(This post was last modified: Mar-30-2025, 01:50 AM by deanhystad.)
+= is not valid because foreverfuncs is an uninitialized local variable. If you want to use the global foreverfuncs, use global foreverfuncs in forever().
Using global will fix the uninitialized variable problem, but you will get a different error (probably a type error) if func is not a list.
def forever(self, func):
foreverfuncs += func You probably want to use foreverfuncs.append(func). If you use append, you don't need to use global foreverfuncs in forever(). Adding something to a list is not assignment, so no local variable is created.
Posts: 6,798
Threads: 20
Joined: Feb 2020
Mar-30-2025, 07:49 PM
(This post was last modified: Mar-30-2025, 07:49 PM by deanhystad.)
There are some problems with how you design your classes.
Painting creates frames without any parent. Where are these frames supposed to go? You can get away with making tkinter objects without any parent, they default to the root window, but it is sloppy and limits you to one view. If you wanted to make a game might want a main view and a status bar and score display. When you make widgets without a parent you can't have multiple views like that.
What is Painting? It appears to make a frame that you can move around. If that is what it is, why is the name of the class Painting? What does "Painting" have to do with a frame? I think it kind of looks like a pygame Sprite.
What is Sensing? Like Painting, the class name does not give much indication about what the class is. Sensing looks like a collection of functions that you might use with Painting objects. If that's all it is, why is it a class? It would be better to take the methods in Sensing and add them to Painting.
I don't like the way you implemented mouse tracking. Classes should not depend on global variables. Since Painting needs to know about the parent window so it can properly place the frames it creates, I would have the parent window track the mouse position and provide an interface that Painting could use to get that info from the window.
Using your ideas for the forever loops and the painting class I wrote a program that has some frames track the mouse in a window. Since the frame kind of acts like a sprite in PyGame I changed the name from Painting to Sprite.
import tkinter as tk
import math
class Forever:
"""A list of functions that is executed periodically."""
def __init__(self, parent, delay=1):
"""Initialize.
Arguments:
parent: A window we can use to call .after().
delay: How many milliseconds to wait before running again."""
self.parent = parent
self.delay = delay
self.functions = []
self.running = True
def add(self, function, *args):
"""Add functions to the function list.
Arguments:
function: Function to call.
arguments: Function arguments.
"""
self.functions.append((function, args))
def run(self):
"""Run the functions in the list."""
for function, args in self.functions:
function(*args)
def start(self):
"""Start periodic execution of the function list."""
self.running = True
self.parent.after(self.delay, self._schedule)
def stop(self):
"""Stop periodic execution of the function list."""
self.running = False
def _schedule(self):
"""Schedule function execution."""
if self.running:
self.run()
self.parent.after(self.delay, self._schedule)
class Sprite(tk.Frame):
"""A frame that acts like a pygame sprite."""
def __init__(self, parent, pos, size, speed=1, **kwargs):
"""Initialize
Arguments:
parent: Parent window.
pos: (x, y) position of sprite.
size: (width, height) of sprite.
"""
super().__init__(parent, width=size[0], height=size[1], **kwargs)
self.parent = parent
self._pos = pos
self.speed = speed
self.dir = 0
.
def get_x(self):
"""Return x position."""
return self._pos[0]
def set_x(self, x):
"""Set x position."""
self.set_pos((x, self._pos[1]))
# Create a property using the property() function. When getting the value of x, the get_x() function is called. When setting the value of x the set_x(value) function is called.
x = property(get_x, set_x)
# Create y property using decorators. This is the more common way of making properties.
@property
def y(self):
"""Return y position."""
return self._pos[1]
@y.setter
def y(self, y):
"""Set y position."""
self.set_pos((self._pos[0], y))
def get_pos(self):
"""Return (x, y) position."""
return self._pos
def set_pos(self, pos):
"""Set (x, y)."""
if self._pos == pos:
return
self._pos = pos
self.place(x=pos[0], y=pos[1])
self.update()
pos = property(get_pos, set_pos)
@property
def width(self):
"""Return width."""
return self["width"]
@width.setter
def width(self, width):
"""Set width."""
self["width"] = width
def get_height(self):
"""Return height."""
return self["height"]
def set_height(self, height):
"""Set height."""
self["height"] = height
height = property(get_height, set_height)
@property
def size(self):
"""Return (width, height)."""
return self.width, self.height # Uses width and height properties
@size.setter
def size(self, size):
"""Set size (width, height)."""
self.width, self.height = size
def get_right(self):
"""Return right position."""
return self.x + self.width
def set_right(self, x):
"""Set right position."""
self.place(x=x - self.width, y=self.y)
right = property(get_right, set_right)
def get_bottom(self):
"""Return bottom position."""
return self.y + self.height
def set_bottom(self, y):
"""Set bottom position."""
self.place(x=self.x, y=y - self.height)
bottom = property(get_bottom, set_bottom)
def track(self, other):
"""Move toward pos at speed."""
x = other.x - self.x
y = other.y - self.y
if x or y:
self.dir = math.atan2(y, x)
self.move_steps(self.speed)
def move_steps(self, steps):
"""Move in dir direction."""
self.x += steps * math.cos(self.dir)
self.y += steps * math.sin(self.dir)
def turn(self, angle):
"""Rotate aiming angle.
Arguments:
angle: Measured in radians, rotate dir clockwise this amount.
"""
self.dir += angle
def scale(self, percent):
"""Rescale to some percent (0..100) of the current size."""
scale = percent / 100
self.width *= scale
self.height *= scale
def distance(self, other):
"""Return (x, y) distance between self and other."""
if self.x < other.x:
x = self.right - other.x
else:
x = other.right - self.x
if self.y < other.y:
y = self.bottom - other.y
else:
y = other.bottom - self.y
return x, y
def touching(self, other):
"""Return True if self and other overlap."""
x, y = self.distance(other)
return self.x <= 0 and self.y <= 0
class MainWindow(tk.Tk):
"""Main window for application."""
def __init__(self, size):
super().__init__()
self.geometry(f"{size[0]}x{size[1]}")
self.title("ghostmansion window")
self.bind("<Motion>", self._track_mouse)
self.mouse_x = 0
self.mouse_y = 0
def _track_mouse(self, event):
"""Keep track of mouse position.
Arguments:
event: Mouse tracking event.
"""
self.mouse_x = event.x
self.mouse_y = event.y
def mouse_pos(self):
"""Return mouse position (x, y)."""
return self.mouse_x, self.mouse_y
class MouseSprite(Sprite):
"""A sprite that follows the mouse."""
def __init__(self, parent):
super().__init__(parent, (0, 0), (0, 0))
parent.bind("<Motion>", self._track_mouse)
def _track_mouse(self, event):
self._pos = (event.x, event.y)
window = MainWindow(size=(300, 300))
mouse = MouseSprite(window)
red = Sprite(window, pos=(100, 100), size=(50, 50), speed=2, background="red")
blue = Sprite(window, pos=(0, 0), size=(10, 10), background="blue")
script = Forever(window, 10)
script.add(red.track, mouse)
script.add(blue.track, red)
script.start()
window.mainloop() I gave the Sprite class some properties. A property is an attribute that is associated with functions that are called to get the property value or set the property value. Properties can be created using the property() function or the @property decorator. I used both methods, but normally you would pick one and stick with it. The decorator is the most common way to make properties.
When running I noticed that sometimes the sprites wander off toward the upper left corner. I tracked this down to the main window mouse tracking event occasionally erroneously reporting that as the location of the mouse.
Posts: 6,798
Threads: 20
Joined: Feb 2020
This is a bad programming practice.
def get_data(self, type):
if type == 'x':
return self.pos[0]
if type == 'y':
return self.pos[1]
if type == 'xs':
return self.size[0]
if type == 'ys':
return self.size[1]
if type == 'dir':
return self.dir A function should one well defined purpose. What is the purpose of get_data()? It returns some attributes of a Painting object. What attributes are those? I cannot tell which ones can be accessed through the function. Worse, I need a type code to specify what attribute to return. What are the type codes? I have to read your function to know how to use it? The user of a function should never have to read the code inside a function. I don't need to read the code for print() to use it, and print is way more complex than get_data().
Not only does the user have to remember all the type codes to use the function, they must also type the codes correctly and check the return type. If I forget that height is "ys" and instead us "YS", the function returns None. You could make the function safer by raising a TypeError or ValueError if the type argument doesn't match any of the supported types, but that error would not be caught until the error is encountered at runtime. The error message would be confusing and the problem difficult to debug. The reported error line would not indicate the location of the error in the code, just where the error caused the program to eventually crash.
get_data() should be implemented as getter functions for each of the type. Instead of get_data(type) you should have:
def get_data(self, type):
def get_x(self)
return self.pos[0]
def get_y(self):
return self.pos[1]
def get_width(self):
return self.size[0]
def get_height(self):
return self.size[1] Now the user doesn't have to remember any obscure type codes. Alll the attribute getters are exposed as part of the class interface. No need to read the code as get_x() and get_height() are self-explanatory.
If you are using an IDE to write your code, and why wouldn't you, breaking get_data into several functions lets the IDE editor help you write code.
IDE editors with autocomplete will offer suggestions as you type. By the time I type "get_", VSCode is asking if I want get_x(), get_y(), get_width() or get_height(). Less typing and much less chance I make a mistake. Even if I do make a mistake, python will provide a much better error message and point directly to the error in the code.
Since I have getter functions for all these attributes, I may as well write setter functions and make properties. I can write the properties like this:
def get_x(self)
return self.pos[0]
def set_x(self, value):
self.pos[0] = value
x = property(get_x, set_x) The property function isn't used a lot, but it is a great way to create properties when you already have getter and setter functions. I sometimes use the property() function so I have access to the getter and setter function. In your forever loop you have a list of functions. If I define properties using the property() function I can add those getter and setter functions to the forever loop. I can't do that if I use a decorator.
The @property decorator is the most common way to make properties.
@property
def y(self)
return self.pos[1]
@y.setter
def y(self, value):
self.pos[1] = value @property tells python to create a new property object using the decorated function as the property getter and using the decorated function name as the property name.
@y.setter tells python that the decorated function is the setter function for the y property.
Now that you have x and y properties you can use them in your code. Instead of this:
def move(self, x, y):
self.pos = (self.pos[0] + x, self.pos[1] + y)
self.place(x=pos[0], y=pos[1]) You can write this:
def move(self, x, y):
self.pos = (self.x + x, self.y + y) Assuming that self.pos is also a propety
@property
def pos(self):
return self._pos
@pos.setter
def pos(self, pos):
self._pos = pos
self.place(x=self._pos[0], y=self._pos[1])
|