Container Encapsulation and Inheritance Lab
Starter Code
Steps
Check out the line
my_box = Box(50, 50, 4) # x, y, max_capacity. What should the__init__method look like? Code it inbox.py.Add three default attributes for the
Boxclass.contents: List[int] = [],width: int = 75, andheight: int = 50.Check out the line
my_box.draw(screen). Define adraw()method for this inbox.py. Don’t forget self and the ability to take a screen object as an argument.Check out the line
if my_box.is_clicked(mouse_location). Create the method definition for this inbox.py.
By this point, when you click the box, new contents should be added to it.
What happens when you click more than four times? What is the capacity? What is wrong with this? Identify the line of code that allowed this bug to happen. How can this be fixed? There are two possible solutions. The best solution envolves proper encapsulation of the
contentslist.Create an
add_itemmethod in theBoxclass. It should be defined to take a single integer (which we willl deem as a single item). This method will add the integer to thecontentslist, but only if the box isn’t already full.Change the un-encapsulated
my_box.contents.append(1)inmain.pyto make use of the handy newadd_itemmethod.
By this point, items should be added to the box if it isn’t full. So, the display text on the box shouldn’t go passed 4/4.
Note
Optional (Advanced)
If you wanted to display some message saying something to effect of “you can’t add anything else (because it’s full), you could have the Box object raise an error.
In
box.pycreate a new class calledBoxFullError.Have this error class inherit from
Exception.In your
add_itemmethod, if you try to add an item and the container is full, raise thisBoxFullError.
Now if you try to add an item to a full box, the program will crash. This is fine, because we can have main.py catch that error.
Where
main.pyadds a new item, wrap that line of code in atry/except.When the
BoxFullErrorhappens, you can simply print out to the console"Box is full". In reality, you could display some text near the box in the game or play a sound.
Why go through all this trouble when you can just print out the message in the Box class add_item method? The answer is in the form of a question: do you want every single person using your Box class to be forced to print the error message "Box is full" to the console, or do you want to give each programmer the opportunity to handle that particular error in whatever way that best fits their particular situation?
Let’s say we wanted the ability for some boxes to be lockable. Normal boxes would not have that capability, but those with locks should have the exact capabilities as a normal Box.
Create a new class (in
box.py) called LockableBox and inherit from theBoxclass. It should have all the functionality of a regular box, but with added functionality (don’t worry about this yet). This is achieved through inheritance.In
main.pycreate aLockableBoxalong-side the other box. You will need to call thedraw()method, as well as handle mouse clicks, just like the regular box. Confirm that the box functions exactly the same as the regular box. hint: make sure you import ``LockableBox`` from ``box``, just like the normal box. I forgot. Oops.It might be best to store these two boxes in a list now, so you can loop through them when checking for clicks and drawing them. Do this and ensure both boxes are functioning properly.
Now we add the extra functionality specific to LockableBox. We want to:
Override the
__init__method inLockableBoxand alockedattribute. Make this private and default toTrue(just because). The method definition should be exactly the same as the originalBox__init__method. If you wanted to, you could add alockedparameter to it.If you have not copy-and-pasted the original method, chances are your game doesn’t run and you get some
AttributeErrorsayingLockableBox has no attribute 'x'. Make sure to copy-paste the original method that initializesself.x,self.y,self.contentsetc.
Verify that the boxes work. You might notice that even though we have a locked attribute, that doesn’t prevent us from adding items. We will get to that in a minute.
For LockableBox.__init__, we need to make sure we don’t copy and paste code from the parent class (Box). That would defeat the advantage of inheritance. If we simply get rid of that code we get an error. So how do we get the original Box.__init__ method to run as well? We need to use super().__init__(). This will manually call the parent-class’s __init__ method that will take care of the attributes x, y, contents, and max_capacity.
In
LockableBox.__init__, before you set thelockedattribute, make a call tosuper().__init__(). Be sure to give that init method all the information it needs. Hint: ``Box(x, y, max_capacity)``. Verify that both boxes work.Lockable boxes now need to reject items if the boxes are locked. You need to override the
add_itemmethod. You can simply copy and paste the method from Box and add the code you need, but why not make use ofsuper()to call the original method? optional, advanced: create aBoxIsLockedErrorthat you can raise if they try to add an item when the box is locked. If you do this, don’t forget to import it intomain.pyand catch the specific error when you calladd_item.
Let’s display on the box if it is locked.
Override the
draw()method, callsuper(). Don’t forget to pass along thescreenargument. Then use the lines below to display"locked"on the box:
text = FONT.render("Locked", True, (0, 0, 0)) screen.blit(text, (self.x, self.y + self.height//2))
Let’s add a way to unlock the box. Becasue I instructed to make the
lockedattribute private, create a method calledtoggle_lock. This will lock when it is unlocked, and unlock when it is locked. The cool thing is ourmain.pycode doesn’t have to worry about how this happens, all you have to do is calllockable_box.toggle_lock()and the box itself will take care of it. You can also make more realisticlockandunlockmethods that can take a key as an argument, and depending on if the key matches, it will unlock or not.In
main.pywe need to modify the event section underMOUSEBUTTONDOWN. We want to be able to distinguish between a mouse right-click and a left click. Left-click will add items and right-click will toggle the lock. Because this is Pygame specific, I’ll give you the code to replace theMOUSEBUTTONDOWNsection of the event loop.elif event.type == MOUSEBUTTONDOWN: mouse_location = event.pos for box in boxes: if box.is_clicked(mouse_location): if event.button == 1: # left-click try: box.add_item(1) except BoxFullError: print("The box is full!") except BoxIsLockedError: print("The box is locked!") elif event.button == 3: # right-click # TOGGLE THE LOCK
Confirm this works on the lockable box. Note: if you call
.toggle_lock()on the regular box, you will get anAttributeError. You can usetry/exceptto handle it, or useif isinstance(box, LockableBox):to only call the method on the appropriate boxes.