0

I haven't found any similar questions and I'm struggling to understand what my code is doing right now.

I'm writing a small puzzle game with Pygame, which is also my first foray into object-oriented programming. Pygame provides Rect objects to define a portion of the screen (with attributes like x, y, width, height, and so on). Since my game is text-heavy, I created an object Region to group a Rect object with a font object and some color information for when I need to blit to screen:

    def __init__(self, rect, bg_color, font, font_color):
        self.rect = rect
        self.bg_color = bg_color
        self.font = font
        self.font_color = font_color

I then created a Layout class that would hold a collection of Region objects, along with a centralized color palette and some methods for redrawing screen elements. Since this game only has one "board," I only need one Layout object, so I made the Region objects class attributes:

   ...
   progress_box: region(pygame.Rect(5, 340, 275, 10),
                                   colors["white"],
                                   pygame.font.SysFont('arialblack', 12),
                                   colors["black"])

(Snipped out the other Region objects for readability.)

Later in the code, I have a function that draws a progress bar in progress_box like so:

def display_progress(score, viz, max_score):
    progress = round(score/max_score, 4)
    progress_bar = region(viz.boxes["progress_box"].rect, "gold", None, None)
    progress_bar.rect.width = int(progress * progress_box.rect.width)
    viz.box_blit(progress_bar) ##viz is my layout object

What should happen, as I understand it, is that progress_bar copies the rect attribute from progress_box object, but then I reset the width attribute to a percentage of the progress_box.rect width. I'm not assigning anything to progress_box itself, and progress_bar is a Region object not connected to the Layout class, so I assumed that progress_box.rect.width would remain constant over every loop.

Instead, whatever value I assign to progress_bar.rect.width is also assigned to progress_box.rect.width. That means, of course, that on the first pass through the loop, both those values are set to 0 because the player's score is 0, and after that they can never change.

The function works as intended if I change progress_bar.rect.width = int(progress * progress_box.rect.width) to progress_bar.rect.width = int(progress * 275), but that doesn't explain to me why the variable is changing when I don't assign anything directly to it. I understand that these attributes are only initialized once, so any change to a class attribute will persist through every loop -- I'm just not sure why it's changing in the first place.

  • 2
    *"What should happen, as I understand it, is that progress_bar copies the rect attribute from progress_box object"* Nope, a direct object assignment like that does not perform a "deep copy" so modifying one object will also mutate the other. [Relevant](https://stackoverflow.com/questions/4794244/how-can-i-create-a-copy-of-an-object-in-python) – Cory Kramer Jul 09 '21 at 17:42
  • Thanks, @CoryKramer, that was not clear from the reference material I was using! – MadMaudlin034 Jul 09 '21 at 20:48

1 Answers1

1

Looks like Cory Kramer already pointed this out in his comment, but wanted to expand on it a bit for anyone who comes across this.

The Data Model section of the official docs gives a good explanation of how objects and object references work in Python.

An object’s identity never changes once it has been created; you may think of it as the object’s address in memory. The ‘is’ operator compares the identity of two objects; the id() function returns an integer representing its identity.

So you can think about when you're assigning objects to different variables or attributes, you are actually assigning a reference to the object, not a copy of the object.


You can make a quick example in the repl to show this off:

class Region:
    def __init__(self, width, height):
        self.width = width
        self.height = height

class Layout:
    def __init__(self, region):
        self.region = region
my_region = Region(width=5, height=5)
layout1 = Layout(my_region)
layout2 = Layout(my_region)

So I created a single Region instance and passed it into two separate Layout instances. As expected, the widths are the same initially:

layout1.region.width
Out: 5

layout2.region.width
Out: 5

Then change the width of the region in layout1:

layout1.region.width = 10

And checking the widths again, you'll see both changed:

layout1.region.width
Out: 10

layout2.region.width
Out: 10

You can also check the memory addresses of each layout region to confirm that they are actually pointing to the same slot in memory (they are the same object essentially):

id(layout1.region)
Out: 140631994345120

id(layout2.region)
Out: 140631994345120

Copying and equality can be a big topic, but in short you should check out the copy module.

For example, if you wanted truly unique objects in that example above, you could either create unique Region instances for each Layout instance, or you could make a copy if you aren't able to instantiate new objects, needing to just duplicate an existing one:

"""
Unique instances of Region.
"""

layout1 = Layout(Region(width=5, height=5))
layout2 = Layout(Region(width=5, height=5))

# unique references in memory

id(layout1.region)
Out: 140631995857072

id(layout2.region)
Out: 140631995436384
"""
Copy's of an existing Region instance.
"""

import copy

region = Region(width=5, height=5)

layout1 = Layout(copy.copy(region))
layout2 = Layout(copy.copy(region))

# unique references in memory

id(region)
Out: 140631993964480

id(layout1.region)
Out: 140631995585920

id(layout2.region)
Out: 140631995588176
Holden Rehg
  • 917
  • 5
  • 10