Python Forum
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
personal module
#1
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
Reply
#2
+= 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.
Reply
#3
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.
Reply
#4
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])
Reply


Possibly Related Threads…
Thread Author Replies Views Last Post
  Need to access Windows Machine or Personal Certificate Store and send to web app seswho 0 2,670 Sep-14-2020, 04:57 PM
Last Post: seswho
  how to import a module which is installed to personal folder using pip install --pre? geekgeek 2 3,272 Mar-09-2020, 02:38 PM
Last Post: geekgeek
  "Up to but not including" (My personal guide on slicing & indexing) Drone4four 5 5,353 Nov-20-2019, 09:38 PM
Last Post: newbieAuggie2019

Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020