Referencing an appended list with user generated class objects from definition within a class?

Question:

I want to create a class Ball, and make a list of objects generated by user for that class. I will then reference this list in run() to draw user generated objects in pygame. My problems are as follows:

When I try to reference Ball(a, v, r...) it says Ball is referenced before my definition. Any help is appreciated!

HERE IS FULL ERROR:

File "/Users/.../Desktop/.../python/promot.py", line 66, in run
    balls.append(Ball(a, v, r, c, x_pos, y_pos)) #here i get error
                 ^^^^
UnboundLocalError: cannot access local variable 'Ball' where it is not associated with a value
import pygame
import math
 
pygame.init()
 
#screen
framespd = 30 #what is frame speed for gravity = 9.81
gravity = 1
t = 0.01
clock = pygame.time.Clock()
width, height = 1000, 1000
pygame.display.set_caption("Orbit Take 1")
screen = pygame.display.set_mode([width, height])
 
class Ball:
    def __init__(self, angle, velocity, radius, color, x_pos, y_pos):
        self.angle = angle
        self.velocity = velocity
        self.radius = radius
        self.color = color
        self.x_pos = x_pos
        self.y_pos = y_pos
 
    def draw(self):
        self.circle = pygame.draw.circle(screen, self.color, (self.x_pos, self.y_pos), self.radius)
 
    def true_velocity_x(self):
        true_velocity_x = self.velocity * math.cos(self.angle)
 
    def true_velocity_y(self):
        true_velocity_y = self.velocity * math.sin(self.angle)
        new_velocity_y = true_velocity_y + (gravity * t)
 
    #def update position(self):
        #x pos = x velocity
        #y pos += y velocity
 
balls = [] 
 
def run():
    value = True
    while value:
        clock.tick(framespd)
 
        # Balls in sim
 
        number_balls = int(input("How many balls: "))
 
        while len(balls) < number_balls:
            # Variables for Ball
            a = input("Angle: ")
            v = input("Velocity: ")
            r = input("Radius: ")
            c = input("Color: ")
            x_pos = '0'
            y_pos = '500'
            balls.append(Ball(a, v, r, c, x_pos, y_pos)) #here i get error "local variable "Ball" defined in enclosing scope on line 24 referenced before assignment"
 
        screen.fill('black')    
 
        #draw ball
        for Ball in balls:
            Ball.draw(pygame.display.set_mode((width, height)))
        #update position
 
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                value = False
            pygame.display.flip()
    pygame.quit()       
 
run()

My first attempt is referenced in bold as "something else I tried", but I am having trouble using classes outside of their initialization with out errors. My next attempt was "current attempt" which does not show anything when I run.

Asked By: Amelia

||

Answers:

For the first ball this was working correctly but then it encountered the

for Ball in balls:

statement and then you had a Ball in your namespace so the global variable is overwritten, instead we can call the object ball and run .draw on the ball object.

There was a way around this (if you insist on calling the local variable Ball) by passing the class into the function that I demonstrated as well.

def run(cls = Ball):

import pygame
import math

pygame.init()

# screen
framespd = 30  # what is frame speed for gravity = 9.81
gravity = 1
t = 0.01
clock = pygame.time.Clock()
width, height = 1000, 1000
pygame.display.set_caption("Orbit Take 1")
screen = pygame.display.set_mode([width, height])


class Ball:
    def __init__(self, angle, velocity, radius, color, x_pos, y_pos):
        self.angle = angle
        self.velocity = velocity
        self.radius = radius
        self.color = color
        self.x_pos = x_pos
        self.y_pos = y_pos
        self.balls = []

    def draw(self, width, height):
        # We need the inputs of width and height to pass to set_mode
        pygame.display.set_mode((width, height))
        #self.circle = pygame.draw.circle(screen, (0, 0, 255), (250, 250), (self.x_pos, self.y_pos), self.radius)
        # below will always draw a blue circle - tweak color, center and radius to be variables so that you can
        # generate multiple balls with different properties
        self.circle = pygame.draw.circle(screen, color=(0, 0, 255), center=(250, 250), radius=75)

    def true_velocity_x(self):
        true_velocity_x = self.velocity * math.cos(self.angle)

    def true_velocity_y(self):
        true_velocity_y = self.velocity * math.sin(self.angle)
        new_velocity_y = true_velocity_y + (gravity * t)

    # def update position(self):
    # x pos = x velocity
    # y pos += y velocity


balls = []


def run():
    value = True
    while value:
        clock.tick(framespd)

        # Balls in sim

        number_balls = int(input("How many balls: "))

        while len(balls) < number_balls:
            # Variables for Ball
            a = input("Angle: ")
            v = input("Velocity: ")
            r = input("Radius: ")
            c = input("Color: ")
            x_pos = '0'
            y_pos = '500'
            balls.append(Ball(a, v, r, c, x_pos,
                              y_pos))  # here i get error "local variable "Ball" defined in enclosing scope on line 24 referenced before assignment"

        screen.fill('black')

        # draw ball
        # call our ball 'ball' so it doesn't overwrite out global class
        for ball in balls:
            ball.draw(width, height)
        # update position

        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                value = False
            pygame.display.flip()
    pygame.quit()


run()
Answered By: Ted Possible

Ball is the (global) name of your class.

In your run function, you wrote

for Ball in balls:
    Ball.draw(pygame.display.set_mode((width, height)))

so the name Balls will be successively given to each of the balls in balls (and will no longer refer to the class, btw).

Python’s rule is simple: if you assign to a variable somewhere in the body of a function, then that variable is considered to be local to the function, unless declared otherwise. That’s what happens here to Ball.

So, as you try to use it before that line, it’s a local variable that hasn’t received any value, hence the error message:

UnboundLocalError: cannot access local variable 'Ball' where it is not associated with a value

The solution is simple anyway: don’t use this name which should only refer to your class. Just rewrite the loop using a new name for your variable:

for ball in balls:
    ball.draw(pygame.display.set_mode((width, height)))

Note also that this name is compliant with the PEP8 recommendation for variable names, which states that variable names should be all lowercase. Capitalized names are for classes.

Answered By: Thierry Lathuille