ICS 33 Fall 2024
Exercise Set 1

Due date and time: Friday, October 11, 11:59pm


Getting started

First of all, be sure that you've read the Reinforcement Exercises page, which explains everything you'll need to know, generally, about how the reinforcement exercises will work this quarter. Make sure you read that page before you continue with this one.


Problem 1 (2 points)

Write a Python function only_truthy, which is only capable of accepting keyword arguments (but no positional arguments), returning a dictionary whose keys are the names of any specified keyword arguments (each preceded by an underscore character: _), and whose values are the values of those same arguments. Notably, though, any argument whose value is considered falsy is left out of the result.

Here are a few examples of how your function should behave when it's finished.


>>> only_truthy()
    {}
>>> only_truthy(name = 'Boo')
    {'_name': 'Boo'}
>>> only_truthy(a = 0, b = 13, c = '', d = 'Boo')
    {'_b': 13, '_d': 'Boo'}      # Neither 0 nor '' are truthy

Limitations

The Python standard library is entirely off-limits in this problem. Your solution must not require any module to be imported.

What to submit

Submit one Python script named problem1.py, which contains your only_truthy function, along with any additional code (if any) necessary to make it work properly. Neither docstrings nor comments are required, since we've all agreed already on what problem we're solving here.

There is no credit being offered for writing automated tests — though you certainly might want to, since that's a great way to ensure that your code works as you expect — and we'd prefer you not submit them even if you do.


Problem 2 (1 point)

Python has long provided the ability to write functions that require certain of its parameters to be passed via keyword arguments (but not positionally), but it was much more recently (in late 2019) that Python incorporated the symmetric idea of arguments that can only be passed positionally (but not via keyword). When new features are added to a programming language that I'm familiar with, I'm usually motivated to want to understand why — partly because I'm naturally curious about that sort of thing, but partly because it helps me to know when I should be considering using a new feature (and how I might expect other members of the community, such as coworkers or open source developers, to use it).

So, let's consider the question of why Python added this particular feature. How is our ability to write a Python program improved by being able to write functions whose parameters can be specified as positional-only? One way to do that is to find some examples among Python's standard library where parameters are specified this way. Below is a link to the documentation for Python's standard library.

Find three examples — in three different modules, so we aren't biasing our point of view too much by finding three shades of the same color — in Python's standard library where functions have at least one positional-only parameter defined. (To be clear, we're talking about functions that actually use the new Python feature, not ones that simulate its behavior internally.) For each one, list the function's signature as written in the documentation, such as the ones below — which, notably, are not functions that specify positional-only parameters, so none of them would be good choices in your answer.


enumerate(iterable, start = 0)
math.gcd(*integers)
str.center(width[, fillchar])

Underneath the choices you listed, briefly explain why you think the design of the functions or methods are better (or worse) for having used this feature, as opposed to allowing the same parameters to be optionally specified via keyword. (We're not asking you to write a long-winded answer here, so there's no need for too much analysis; the focus is on what you've deduced, in total, from the three functions you chose.)

What to submit

Submit one PDF file named problem2.pdf, which answers the question asked above.


Problem 3 (1 point)

Consider the following short Python class, whose objects are used to describe albums, with each album performed by an artist and containing some number of songs. (There are no other details of the problem domain that are important to the problem.)


class Album:
    _songs = []

    def __init__(self, artist):
        self._artist = artist

    def artist(self):
        return self._artist

    def songs(self):
        return self._songs

    def add_song(self, song):
        self._songs.append(song)

Now, let's explore the use of the class a bit in the Python shell.


>>> a1 = Album('U2')
>>> a1.add_song('Zoo Station')
>>> a1.add_song('The Fly')
>>> a1.songs()
    ['Zoo Station', 'The Fly']                         # So far, so good!
>>> a2 = Album('Bruce Hornsby')
>>> a2.add_song('Circus on the Moon')
>>> a2.songs()
    ['Zoo Station', 'The Fly', 'Circus on the Moon']   # Uh-oh!

This is definitely not the outcome we were aiming for, with modifications to one album affecting another. In no more than two or three sentences, briefly explain what design mistake led to this outcome, and what we might have done differently to avoid it. (It's not necessary to rewrite the code; a brief explanation of what you would change and why is all we're after here.)

What to submit

Submit one PDF file named problem3.pdf, which briefly describes what you would fix and why.


Problem 4 (3 points)

Unit testing is a technique that pays off in multiple ways, but those benefits come at a cost: We need to design a program to be amenable to being unit tested in the first place. Many times, this is its own separate benefit; when we're disciplined about keeping separate things separate in our design, turning functionality into pure functions (whose outputs are determined only by their inputs) whenever we can, and so on, we often end up with a program whose design is clearer and better than it would have been if we had just written the first code that came to our mind, especially as our program (and the team around it) grows. But, when we're first learning about unit testing, this causes some real friction, because we haven't yet learned how to write programs that are amenable to unit testing; we might sometimes find ourselves unable to test certain parts of our programs, but not be able to recognize these problems before they happen, nor be clear on how we should adjust our designs when they do.

Let's suppose that we wrote the following Python module, containing two Python functions that print values to the standard output (i.e., to the Python shell, by using the built-in print function).

Now, let's suppose that we wanted to write unit tests for these functions. This presents a problem, since their outputs are the side effects of printing something to the standard output, meaning that our tests would revolve around calling one of the functions and asking the question "What did it print?" A better design for these functions might be to separate the handling of "What should we be printing?" from the actual printing of it, so that we had pure functions — ones that, say, returned a collection of strings instead of printing lines of output. Those pure functions would be much more straightforwardly testable.

However, let's imagine that we don't have that option, for whatever reason. (Sometimes, we don't have control over all of the code we use — because we lack ownership of it, because it's part of a third-party library of which we're loath to maintain a separate copy forever, or many other possible reasons.) So, what can we do to test these functions?

In our discussion of Context Managers, we talked about an alternative that could work here. By temporarily reconfiguring where our standard output is sent, we can test the behavior of our functions — essentially, allowing us to ask the question "What did it print?" — before restoring normal behavior again afterward.

Write a Python module problem4.py that uses the unittest module and the techniques described in the Context Managers notes to test the two functions in printing.py as completely as you can.

What to submit

Submit one Python module named problem4.py, which contains your unit tests. Do not submit the provided printing.py module, and do not make changes to it; the goal is to test it in its provided form.


Problem 5 (3 points)

Many of the benefits of learning to write unit tests extend beyond testing, which is partly why we're spending time on it in this course; we become better at a lot of things when we become better at writing unit tests. Among these many benefits is simply that writing unit tests means writing and maintaining a lot of code — at first, it's just the tests themselves, but then it's tools that allow us to write the tests more effectively. As you might expect, there's a lot of repetition in testing, and it can be just as beneficial to reduce the level of repetition in our test code as in any other code. When we replace a rote pattern in our tests with something shorter and simpler, the tests become easier to write, and when that simpler tool has a well-chosen name and a Pythonic structure, the tests are easier to read, too; everybody wins.

We recently learned about Python's Context Managers, which provide a way for us to implement what we might call automated wrap-up: specifying the things that happen when we exit a piece of code, regardless of how or why we're exiting it. The canonical first example of context managers is automating the closing of files or sockets, but context managers are a much broader concept than that, whose depths we can explore in this problem by implementing a different example related to unit testing.

Exceptions are generally used in unit tests as a way to communicate a test's failure, but when the expectation is that an exception will be raised — when we're testing code that should be raising one, to ensure that it does — we need to "reverse the polarity," so to speak; we want the appropriate kind of exception to cause the test to succeed, while causing it to fail if either the wrong kind of exception is raised or no exception is raised at all. Context managers offer one way to cleverly (but Pythonically) solve a problem like this.

Write a Python function should_raise, which accepts one argument: the type of exception that we expect a block of code to raise. It returns a context manager that reacts to exiting in one of two ways:


# This should succeed quietly, because the conversion of 'Boo' to an integer
# fails with a ValueError, as we expected.
with should_raise(ValueError):
    int('Boo')

# An exception of your custom type should be raised, since the context will
# not have been exited because no exception was raised.
with should_raise(IndexError):
    x = [1, 2, 3]
    x[0]

# An exception of your custom type should be raised, since the context will
# not have been exited because an exception other than ValueError was raised.
with should_raise(ValueError):
    x = [1, 2, 3]
    x[10]          # Raises an IndexError, rather than a ValueError

Limitations

The Python standard library is entirely off-limits in this problem. Your solution must not require any module to be imported.

What to submit

Submit one Python script named problem5.py, which contains your should_raise function, along with the additional code necessary to make it work properly. Neither docstrings nor comments are required, since we've all agreed already on what problem we're solving here.

The function and any additional code you've written that it calls are all you need to write in the module. There is no credit being offered for writing automated tests — though you certainly might want to, since that's a great way to ensure that your code works as you expect — and we'd prefer you not submit them even if you do.


Deliverables

In Canvas, you'll find a separate submission area for each problem. Submit your solution to each problem into the appropriate submission area. Be sure that you're submitting the correct file into the correct area (i.e., submitting your Problem 1 solution to the area for Problem 1, and so on). Under no circumstances will we offer credit for files submitted in the incorrect area.

Submit each file as-is, without putting it into a Zip file or arranging it in any other way. If we asked for a PDF, for example, all we want is a PDF; no more, no less. If you submit something other than what we asked for (e.g., a text file when we asked for a PDF, even if its filename ends in .pdf), we will not be offering you any credit on the submission. There are no exceptions to this rule.

Of course, you should also be aware that you're responsible for submitting precisely the version of your work that you want graded. We won't regrade an exercise simply because you submitted the wrong version accidentally.

Can I submit after the deadline?

Unlike some of the projects in this course, the reinforcement exercises cannot be submitted after the deadline; there is no late policy for these. Each is worth only 2% of your grade, with the lowest score dropped — see the Reinforcement Exercises page for details — so it's not a disaster if you miss one of them along the way.

You're responsible for making a submission in order to receive credit, which means you'll want to be sure that you've remembered to submit your work and verify in Canvas that it's been received. A later claim of having forgotten to submit your work or having misremembered the due date will not be grounds for a resubmission under any circumstances.

What do I do if Canvas adjusts my filename?

Canvas will sometimes modify your filenames when you submit them (e.g., by adding a numbering scheme like -1 or a long sequence of hexadecimal digits to its name). In general, this is fine; as long as the file you submitted has the correct name prior to submission, we'll be able to obtain it with that same name, even if Canvas adjusts it.