A document from MCS 275 Spring 2021, instructor Emily Dumas. You can also get the notebook file.

Worksheet 4 Solutions

MCS 275 Spring 2021 - Emily Dumas

Solutions by Jennifer Vaccaro

Topics

The main topics of this worksheet are:

  • Variadic functions (lecture 7) in problems 1, 2
  • Argument unpacking (lecture 7) in problem 3
  • Decorators (lecture 8) in problems 4,5
  • Context managers (lecture 9) in problems 6,7

It is important to get some practice with each topic during discussion. If you find any one topic taking a long time, it would be a good idea to move ahead.

Instructions

Complete these coding exercises. Each one asks you to write one or more programs or modules. Even though these are not collected, you should get into the habit of following the coding standards that apply to all graded work in MCS 275.

1. Most bits set

Here is a function that takes an integer and determines how many of the bits in its binary expansion are equal to 1 (such a bit is also called "set"). There are other ways to do this; the one shown below was chosen for its code simplicity.

In [1]:
def bits_set(n):
    """Return the number of 1 bits when `n` is represented in binary"""
    return bin(n).count("1")

# Example: bits_set(9) returns 2 because 9 = 0b1001 which has two bits set to 1.

Write a function most_bits_set(...) that accepts any number of integers as positional arguments. The function should determine which of the arguments have the most bits equal to 1. Among arguments that have this maximum number of bits equal to 1, the function should return the one that appears earliest in the argument list.

In creating this function, you are encouraged to use the function bits_set given above.

In [2]:
# Worksheet 4 Problem 1 SOLN
# J Vaccaro
# This code was written in accordance with the rules in the syllabus.

def most_bits_set(*args):
    """Return the argument with the most bits set"""
    most_bits = -1
    for arg in args: # Iterate through args, which are an iterable like a tuple
        bits = bits_set(arg)
        if bits>most_bits: # Replace only if bits exceeds most_bits, so returns the first arg with that number of bits.
            most_bits_arg = arg
            most_bits = bits
    return most_bits_arg # raises Exception if no input argument
In [3]:
most_bits_set(0)
Out[3]:
0
In [4]:
# 9 is the only one with 2 bits set, so it is returned
most_bits_set(8,9,0,4)
Out[4]:
9
In [5]:
# 7,14,13 all have 3 bits set, which is the maximum seen here
# (9 has only 2 bits set, and 2 has only 1 bit set)
# Therefore 7 is returned
most_bits_set(2,7,14,9,13)
Out[5]:
7

2. kwarg name with longest value

Write a function kwarg_name_with_longest_value(...) which accepts any number of keyword arguments (a.k.a. kwargs). Every value passed as a keyword argument is expected to be a sequence, such as a string, list, or tuple. Note that sequences allow computation of length using len(). The function should determine which argument has the maximum length, and return the name of that argument. If several keyword arguments share the maximum length, it is acceptable to return any one of their names.

In [6]:
# Worksheet 4 Problem 2 SOLN
# J Vaccaro
# This code was written in accordance with the rules in the syllabus.

def kwarg_name_with_longest_value(**kwargs):
    """Returns the kwarg key with the longest name value"""
    longest_length = 0
    longest_key = ""
    for key, value in kwargs.items(): # iterate through kwargs, which is like a dictionary of key-value pairs
        value_length = len(value)
        if value_length > longest_length:
            longest_key = key
            longest_length = value_length
    return longest_key # If no arguments given, returns an empty string
In [7]:
kwarg_name_with_longest_value(alpha="air",b="Tornado",c="cat")
Out[7]:
'b'
In [8]:
# It is acceptable for this to return 'a' or 'b'
kwarg_name_with_longest_value(a="finite",b="robots",c=[2,3,4])
Out[8]:
'a'

3. Printing powers of 2 less than n

Write a function powers_2_less(n) that prints the powers of 2 that are less than n, all on one line separated by spaces, with no commas or brackets. Use the following specific structure to achieve this:

  • Make a list of all such powers of 2
  • Use a single call to print(), where the list is unpacked into a sequence of arguments
In [9]:
# Worksheet 4 Problem 3 SOLN
# J Vaccaro
# This code was written in accordance with the rules in the syllabus.

def powers_2_less(n):
    """Print out all of the powers of 2 less than n"""
    # Create the powers list using a list comprehension
    powers_list = [2**i for i in range(n) if 2**i < n]
    # Since print is a variadic fxn, can use the argument unpacking from powers_list. Other solutions would use join or replace
    print("Powers of 2 less than {}:".format(n),*powers_list)
In [10]:
powers_2_less(100)
Powers of 2 less than 100: 1 2 4 8 16 32 64
In [11]:
powers_2_less(128)
Powers of 2 less than 128: 1 2 4 8 16 32 64
In [12]:
powers_2_less(129)
Powers of 2 less than 129: 1 2 4 8 16 32 64 128
In [13]:
powers_2_less(10000)
Powers of 2 less than 10000: 1 2 4 8 16 32 64 128 256 512 1024 2048 4096 8192

4. Timing decorator

Here is a short program that computes the squares of the integers 1...1_000_000 and then prints how long the calculation took.

In [47]:
import time

t_start = time.time() # Time since 0:00 Jan 1 1970 UTC, in seconds
million_squares = [ (x+1)**2 for x in range(1_000_000) ]
t_end = time.time()
print("Running time: {:.4f} seconds".format(t_end - t_start))
Running time: 0.2264 seconds

Using the code above as a reference, write a decorator called timing that, when applied to a function, makes every call to that function print how long it took for the function to complete. The function's return value, if any, should still be returned. And the decorator should work with functions that take arguments.

In [28]:
# Worksheet 4 Problem 4 SOLN
# J Vaccaro
# This code was written in accordance with the rules in the syllabus.

import time

def timing(f):
    """Decorator for displaying how long a function took to run in seconds"""

    def inner(*args, **kwargs):
        """Measures and prints the execution time of f, and returns the output."""
        t_start = time.time() # Time since 0:00 Jan 1 1970 UTC, in seconds
        ret = f(*args, **kwargs) # Save the output to a variable
        t_end = time.time()
        print("Running time: {:.4f} seconds".format(t_end - t_start))
        return ret # return the output from f

    return inner
In [29]:
@timing
def squares_of_first(n):
    """Return a list of the squares of 1, 2, ...,n"""
    return [ (x+1)**2 for x in range(n) ]

@timing
def print_greeting(name="friend",salutation="Hello"):
    """Print a customizable greeting"""
    print("{}, {}.  It is nice to see you.".format(salutation,name))
In [30]:
squares_of_first(10)
Running time: 0.0000 seconds
Out[30]:
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
In [31]:
x=squares_of_first(100_000) # assign so the value is not printed!
Running time: 0.0369 seconds
In [32]:
print_greeting(name="Anushka",salutation="Howdy")
Howdy, Anushka.  It is nice to see you.
Running time: 0.0010 seconds
In [33]:
print_greeting(salutation="Greetings")
Greetings, friend.  It is nice to see you.
Running time: 0.0000 seconds

5. One argument limit decorator

Write a decorator unary that ensures a function only ever receives one positional argument, and never receives any keyword arguments. Any extra arguments (positional arguments after the first, or any keyword arguments at all) should simply be ignored. If no arguments are given, then the decorator is allowed to do anything (e.g. raise an exception).

In [34]:
# Worksheet 4 Problem 5 SOLN
# J Vaccaro
# This code was written in accordance with the rules in the syllabus.

def unary(f, *argv, **kwargs):
    """Decorator for only calling a function with its first (non-keyword) argument"""

    def inner(*args, **kwargs):
        """Returns f evaluated at args[0], ignores kwargs"""
        return f(args[0])

    return inner
In [35]:
@unary
def show(x):
    """Display a single value"""
    print("Value:",x)

@unary
def square(x):
    """Return the square of x"""
    return x*x
In [36]:
show(50)
Value: 50
In [37]:
show("Hello everyone")
Value: Hello everyone
In [38]:
show(3,4,5,6,7)
Value: 3
In [39]:
show(5,6,7,8,appreciated="GvR")
Value: 5
In [40]:
square(5,x=0)
Out[40]:
25
In [41]:
square(12,20)
Out[41]:
144

Note: Silently dropping arguments is usually not a good idea, so this decorator is more of a concept exercise than a model to emulate in your future programs!

6. Convert file I/O to use context managers

The code shown below creates a file, fills it with data, and then reads it back.

Convert this code to using context managers instead of manual cleanup.

In [69]:
outfile = open("mcs275ws4prob6data.txt","w") # delete if it exists!
outfile.write("{} {}\n".format("i","i**2"))
for i in range(32):
    outfile.write("{} {}\n".format(i,i**2))
outfile.close()

infile = open("mcs275ws4prob6data.txt","r")
max_seen = -1
for line in infile:
    fields = line.strip().split(" ")
    if len(fields) != 2:
        continue
    try:
        x = int(fields[1])
    except ValueError:
        # first line doesn't contain an integer
        # just skip it
        continue
    if x > max_seen:
        max_seen = x
infile.close()
print("The largest square appearing in the file is:",max_seen)
The largest square appearing in the file is: 961
In [42]:
# Worksheet 4 Problem 6 SOLN
# J Vaccaro
# This code was written in accordance with the rules in the syllabus.

with open("mcs275ws4prob6data.txt","w") as outfile:
    outfile.write("{} {}\n".format("i","i**2"))
    for i in range(32):
        outfile.write("{} {}\n".format(i,i**2))

with open("mcs275ws4prob6data.txt","r") as infile:
    max_seen = -1
    for line in infile:
        fields = line.strip().split(" ")
        if len(fields) != 2:
            continue
        try:
            x = int(fields[1])
        except ValueError:
            # first line doesn't contain an integer
            # just skip it
            continue
        if x > max_seen:
            max_seen = x
print("The largest square appearing in the file is:",max_seen)
The largest square appearing in the file is: 961

Secondary activity: Determine exactly how the second loop in this code works, and what formatting variations it will tolerate.

Answers

  • Additional whitespace at the beginning or end of the line
  • Skipping lines

7. Custom context manager

Here is a function that returns the abbreviated current weekday as a string (e.g. "Mon", "Tue", ...).

In [46]:
import datetime

def current_weekday():
    """Abbreviated weekday name"""
    now = datetime.datetime.now()
    return now.strftime("%a")

Write a class NotOnMonday that creates a context manager to ensure that a block of code does not start or end on a Monday.

The constructor should accept no arguments; in fact, no constructor is needed. The function of the context manager should be:

  • Setup upon entering a with-block: Check to see if it is Monday. If it is, print a message and wait until it is no longer Monday. (Repeatedly wait 5 seconds and check again, until the day is no longer Monday.)
  • Cleanup upon leaving a with-block: Check to see if it is Monday. If it is, print a message and wait until it is no longer Monday. (Repeatedly wait 5 seconds and check again, until the day is no longer Monday.)

Notice that this decorator checks the day of week in both setup and cleanup phases; this matters because the day might change during execution of the with-block.

Your class should use current_weekday() defined above to get the weekday. That way, you can temporarily change current_weekday() to return something else (e.g. return "Mon" all the time, or return "Mon" the first time it is called, and "Tue" every other time, ...) for testing purposes.

Example of use:

In [47]:
# Worksheet 4 Problem 7 SOLN
# J Vaccaro
# This code was written in accordance with the rules in the syllabus.

import time

class NotOnMonday():
    """Custom context class only continues into context when today is not Monday"""

    def __enter__(self):
        """Before entering context, and check if it's Monday every 5 seconds, only proceeding when it isn't"""
        if current_weekday() == "Mon":
            print("Waiting for it not to be Monday...")
            time.sleep(5)
            while current_weekday() == "Mon":
                time.sleep(5) # Waits for 5 seconds

    def __exit__(self,exc_type,exc,tb):
        """Before exiting context, and check if it's Monday every 5 seconds, only proceeding when it isn't"""
        if current_weekday() == "Mon":
            print("Waiting for it not to be Monday...")
            time.sleep(5)
            while current_weekday() == "Mon":
                time.sleep(5)
In [48]:
# The sample output was obtained on a Saturday using the actual day of week
with NotOnMonday():
    print("It is not Monday!")
It is not Monday!
In [59]:
# An example that shows what would happen if current_weekday() returned "Mon" the first time,
# and then returned "Tue" for each subsequent call
days = ["Mon"]
def current_weekday(): # 
    """At each function call, returns an elt of days the Tue forever"""
    if days:
        return days.pop()
    else:
        return "Tue"

with NotOnMonday():
    print("It is not Monday!")
Waiting for it not to be Monday...
It is not Monday!
In [60]:
# An example that shows what would happen if current_weekday() returned "Tue" the first time,
# "Mon" the second time, and "Tue" all subsequent times it is called.

days = ["Mon", "Tue"] # will return Tue, Mon, then Tue forever

with NotOnMonday():
    print("It is not Monday!")
It is not Monday!
Waiting for it not to be Monday...