ICS H32 Fall 2025
Exercise Set 4 Solutions


What's here?

These are solutions to a set of exercises, along with a fair amount of additional background explanation. Note that I've gone into more detail with those explanations that we would have wanted or expected students to do, but I'm using these as an opportunity to solidify your understanding; don't take this as an indication that you should be writing your answers in as much detail as I am here.


Problem 1

A fairly complete solution is available at the link below.

A few design notes about the solution:

So, all in all, what do we have here, now that we're done? If we were writing a program that dealt with SRLs (e.g., something akin to Project 1 but that ran online in a world where SRLs actually existed), we now have a tool a bit like the Path objects provided by pathlib, which make it impossible for us to have an invalid SRL, and which make it possible for us to understand an SRL's meaning quickly and safely anywhere we need to.


Problem 2

Python is replete with flexibility. While that flexibility can sometimes be a blessing, it's quite often at least as much a curse instead. One of the most important things to learn about a language as flexible as Python is how not to use it; using its flexibility with the appropriate restraint is the key to being able to write and maintain large programs.

The reason we opted to initialize all of an object's attributes in the __init__ method of its class is so we can be sure of two things.

  1. Any necessary attributes will be guaranteed to exist at any point where we subsequently use them.
  2. Those attributes will have reasonable default values (i.e., the object will have been initialized to be "fully meaningful" from the get-go).

A good way to understand why we did that is to imagine what would happen if we didn't. What would happen if we felt free to add attributes in arbitrary methods? What would happen if we felt free to delete attributes arbitrarily? Let's imagine that we had written the Counter example from the Classes notes a little differently.


class Counter:
    def count(self) -> int:
        self._count += 1
        return self._count

    def peek(self) -> int:
        return self._count

    def reset(self) -> None:
        self._count = 0

This is almost the same code that we wrote in the lecture, except we've left the __init__ method out. On the one hand, you might argue that this is an improvement; after all, there's less code used to solve the same problem, so why shouldn't that be better? Let's try to use our new version of Counter and see what happens.


>>> c1 = Counter()
>>> c1.count()
    Traceback (most recent call last):
      File "<pyshell#10>", line 1, in <module>
        c1.count()
      File "<pyshell#8>", line 3, in count
        self._count += 1
    AttributeError: 'Counter' object has no attribute '_count'
>>> c1.peek()
    Traceback (most recent call last):
      File "<pyshell#11>", line 1, in <module>
        c1.peek()
      File "<pyshell#8>", line 6, in peek
        self._count += 1
    AttributeError: 'Counter' object has no attribute '_count'
>>> c1.reset()
>>> c1.peek()
    0
>>> c1.count()
    1

What does this tell us? If you create a Counter, you have to remember to call the reset method on it before you'll be able to use it for anything else. Once you've done that, you're good to go; subsequent calls to any of its methods will work the way you expect. So, in essence, a Counter isn't really a Counter until you remember to call reset on it.

And, of course, you could write this on a sticky note and attach it to your computer's display. If there were other people working on the project, you could inform them of this new constraint, then ask them to tell new people this fact when they join the project, too.

But what do you think the odds are that everyone will remember this? What do you think the odds are that you'll only have this one weird constraint, if you allow yourself to design classes this way? Won't your display run out of space for sticky notes? Won't you start to forget these little details as time goes on, or get them confused with other details in situations that feel similar but are actually different? And, what's worse, the penalty for being wrong about any of these things is a run-time error, which means we might not even realize we've screwed up until much later, especially if we're careless in our testing.

If we think of the construction of an object as being initialization — the reason Python's designers may have chosen the name __init__ in the first place — and if we think of that as meaning "Don't just make us an object, but make us an object that represents a meaningful state," then we simply can't have a "bogus" or "half-baked" object of this class. One of two things will always happen when we construct one:

  1. The __init__ method will succeed, in which case we know that all of the object's attributes will exist and that the object, generally, will be in a meaningful state.
  2. The __init__ method will raise an exception, in which case we know not only that it failed, but we won't have an object.

Furthermore, if we need information in order to initialize our object to be meaningful, our __init__ method can accept parameters, in which case those parameters will have to be supplied in order to construct an object of our class. This, too, is a good thing.

Avoiding invalid states is much easier when they can't be represented in the first place, so we should endeavor for that in our designs.


Problem 3

A partial solution, with one of the five shapes implemented, is available below.

The other shapes would be more of the same, with obviously more complicated formulas for figuring out things like areas and whether or not shapes contain points.


Problem 4

  1. The type of element in the list x might best be described as Drawable. Drawable objects are those that can be drawn on a dislpay, using this method (which is part of that Drawable interface):
    • def draw(self, display: Display) -> None
  2. The type of element in the list x might best be described as HasLength, which are objects that have a length (i.e., it's possible to pass them as a parameter to the built-in len function in Python).
    • What makes objects compatible with the built-in len function turns out to be the presence of this method:
      • def __len__(self) -> int
      Whatever that method returns, that's the object's length.
    It's worth noting, though we've not discussed this yet, that Python's designers already gave a name to this concept: Sized (i.e., a sized object is one that has a length).
  3. There are three interfaces in use in this function:
    • The type of x might best be described as Componentized, i.e., objects that contain a collection of components. Componentized objects include a method def all_components(self), the result of which is stored in the variable y.
    • The type of y might best be described as ComponentCollection, which is an object that can return a collection of components, one at a time. It consists of this method:
      • def next_component(self) This method returns each component, which is stored in the variable z.
    • The type of z might best be described as Component, though you might also take the idea a level higher and call it HasSize, because the only thing we need it to do is this:
      • def size(self) -> int