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
Box
class.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
contents
list.Create an
add_item
method in theBox
class. It should be defined to take a single integer (which we willl deem as a single item). This method will add the integer to thecontents
list, but only if the box isn’t already full.Change the un-encapsulated
my_box.contents.append(1)
inmain.py
to make use of the handy newadd_item
method.
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.py
create a new class calledBoxFullError
.Have this error class inherit from
Exception
.In your
add_item
method, 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.py
adds a new item, wrap that line of code in atry/except
.When the
BoxFullError
happens, 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 theBox
class. 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.py
create aLockableBox
along-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 inLockableBox
and alocked
attribute. 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 alocked
parameter to it.If you have not copy-and-pasted the original method, chances are your game doesn’t run and you get some
AttributeError
sayingLockableBox has no attribute 'x'
. Make sure to copy-paste the original method that initializesself.x
,self.y
,self.contents
etc.
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 thelocked
attribute, 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_item
method. 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 aBoxIsLockedError
that 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.py
and 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 thescreen
argument. 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
locked
attribute private, create a method calledtoggle_lock
. This will lock when it is unlocked, and unlock when it is locked. The cool thing is ourmain.py
code 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 realisticlock
andunlock
methods that can take a key as an argument, and depending on if the key matches, it will unlock or not.In
main.py
we 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 theMOUSEBUTTONDOWN
section 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/except
to handle it, or useif isinstance(box, LockableBox):
to only call the method on the appropriate boxes.