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

MCS 275 Spring 2022 Worksheet 4 Solutions

  • Course instructor: Emily Dumas
  • Solutions prepared by: Jennifer Vaccaro, Johnny Joyce

Topics

This worksheet focuses on variadic functions, argument unpacking, and decorators.

Resources

These things might be helpful while working on the problems. Remember that for worksheets, we don't strictly limit what resources you can consult, so these are only suggestions.

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 [4]:
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.
# We haven't covered the function bin() in class, but you can check the help to see what it does!

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.

Examples of the expected behavior of most_bits_set:

Solution

In [ ]:
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 [9]:
# 9 is the only one with 2 bits set, so it is returned
most_bits_set(8,9,0,4)
Out[9]:
9
In [12]:
# 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[12]:
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.

Example:

In [4]:
kwarg_name_with_longest_value(alpha="air",b="Tornado",c="shark")
Out[4]:
'b'
In [3]:
# It is acceptable for this to return 'a' or 'b'
kwarg_name_with_longest_value(a="finite",b="robots",c=[2,3,4])
Out[3]:
'a'
In [2]:
def kwarg_name_with_longest_value(*args, **kwargs):
    '''Returns name of keyword argument with longest length. Each kwarg should be iterable.'''
    longest_kwarg_name = None # Represents name of longest kwarg as we iterate
    longest_value = 0 # Represents length of longest value so far as we iterate
    
    for kwarg, value in kwargs.items():
        if len(value) > longest_value:
            longest_kwarg_name = kwarg
            longest_value = len(value)
            
    return longest_kwarg_name

3. Prepared print

Write a function prepared_print that accepts any number of arguments and keyword arguments. This function should return a function that takes no arguments, but which will call print() with the supplied arguments when called. That is, prepared_print gets a print statement ready but doesn't call it. Here's an example of its use:

In [8]:
f = prepared_print("hello","mcs",260,"students",sep="_") # Get "ready" to call print("hello",...)
g = prepared_print("goodbye")
In [9]:
f() # call the print function we've prepared
hello_mcs_260_students
In [10]:
g()
goodbye

Hint

You'll want to define a function inside prepared_print and return it.

Solution

In [1]:
def prepared_print(*args, **kwargs):
    '''Returns a function - when called, prints any number of args and kwargs'''
    
    def inner():
        '''Function which gets returned by prepared_print. When called, executes print()'''
        print(*args, **kwargs)

    return inner # Note the lack of parentheses: function itself is being returned

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.

Examples of how this should work:

In [49]:
@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 [50]:
squares_of_first(10)
Running time: 0.0000 seconds
Out[50]:
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
In [51]:
# Let's make a much bigger list so the running time is nonzero
x=squares_of_first(100_000) # assign so the value is not printed!
Running time: 0.0493 seconds
In [53]:
print_greeting(name="Anushka",salutation="Howdy")
Howdy, Anushka.  It is nice to see you.
Running time: 0.0001 seconds
In [54]:
print_greeting(salutation="Greetings")
Greetings, friend.  It is nice to see you.
Running time: 0.0003 seconds

Solution

In [5]:
import time

def timing(f):
    '''Acts as decorator. When given function f is called, timing() executes f and prints
    time taken for f to run.'''
    
    def inner(*args, **kwargs):
        '''Inner function to be returned by timing()'''
        t_start = time.time() # Time since 0:00 Jan 1 1970 UTC, in seconds
        result = f(*args, **kwargs)
        t_end = time.time()
        print("Running time: {:.4f} seconds".format(t_end - t_start))
        return result # Return the result because it may be needed elsewhere in the program
        
    return inner

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 [56]:
@unary
def show(x):
    """Display a single value"""
    print("Value:",x)

@unary
def square(x):
    """Return the square of x"""
    return x*x
In [57]:
show(50)
Value: 50
In [58]:
show("Hello everyone")
Value: Hello everyone
In [59]:
show(3,4,5,6,7)
Value: 3
In [60]:
show(5,6,7,8,appreciated="GvR")
Value: 5
In [63]:
square(5,x=0)
Out[63]:
25
In [61]:
square(12,20)
Out[61]:
144

Solution

In [ ]:
def unary(f):
    '''Runs and returns given function f, but with only the first argument given'''
    
    def inner(*args):
        '''Inner function to be returned by unary()'''
        return f(args[0])
        
    return inner