Advanced Python
In-Class Exercises, Session 5

Notes if you are using Jupyter Notebook: to call exit() from a notebook, please use sys.exit() (requires import sys). If a strange error occurs, it may be because Jupyter retains variables from all executed cells. To reset the notebook, click 'Restart Kernel' (the circular arrow) -- this will not undo any changes made.

FUNCTIONS WITH KEYWORD ARGUMENTS

Ex. 5.1 Write a function multiply() that takes one positional argument (an expected integer value) and one keyword argument (an optional multiplier called mul=). Make the default for mul= the value 2. Call as shown.
# your code here


x = multiply(5, mul=3)
print(x)                  # 15

y = multiply(5)
print(y)                  # 10
 
Ex. 5.2 Write a function greet() with one positional argument (a first name) and one keyword argument (an optional last name). Make the default for last= the value None. Call as shown.
# your code here


x = greet('Joe')
print(x)                 # Hi, Joe!

y = greet('Joe', last='Wilson')
print(y)                 # Hi, Joe Wilson!
 

OBJECT REFERENCES

Ex. 5.3 One list or two? How can we test?
x = ['a', 'b', 'c']

y = x
 
Ex. 5.4 Two lists or three? Is the list in the last item of y a duplicate list? How can we test?
x = ['a', 'b', 'c']

y = [1, 2, 3, x]
 

REFERENCES PASSED TO FUNCTIONS

Ex. 5.5 One list or two? Is the list in appendme() a new list? How can we test?
def appendme(arg):
    arg.append('d')

x = ['a', 'b', 'c']

appendme(x)
 
Ex. 5.6 greet() has been assigned to y. Call the function through variable y.
def greet():
    print('hello, world')

y = greet
 
Ex. 5.7 Call callit(), passing greet as argument, then inside callit(), call the function through the argument (do not call greet()).
def callit(func):
    # your code here

def greet():
    print('hello, world!')


# your code also here
 

SORTING

Ex. 5.8 Given the below code, sort the list in reverse numeric order and print it.
numlist = [1, 13, 23, 3, 9, 16]

# your code here
Expected Output:
[23, 16, 13, 9, 3, 1]
 
Ex. 5.9 Given the below code, sort the list in alphabetic order and print it.
charlist = ['a', 'z', 'b', 'c', 'd', 'f']

# your code here
Expected Output:
['a', 'b', 'c', 'd', 'f', 'z']
 
Ex. 5.10 Given the below code, sort the list using standard sort and print it (note the output below).
charlist = ['F', 'e', 'c', 'a', 'B', 'D']

# your code here
Expected Output:
['B', 'D', 'F', 'a', 'c', 'e']
 

COMPLEX SORTING

Ex. 5.11 Given your understanding that the key= argument to sorted() will in a sense process each element through whatever function we pass to it, sort the below list in alphabetical order and print it.

(Hint: this list will not sort alphabetically by default. We need a key= function that can modify each element in the list, and make them all the same case. The string upper() method or lower() method can do the job. To refer to this method, you can say str.upper or str.lower. However, make sure not to actually call the method (which is done with the parentheses). Instead, you simply refer to the method, i.e., mention the method without using the parentheses.)

charlist = ['F', 'e', 'c', 'a', 'B', 'D']

# your code here
Expected Output:
['a', 'B', 'c', 'D', 'e', 'F']

If you see this message:

TypeError: descriptor 'upper' of 'str' object needs an argument

it means that you added parentheses and attempted to call the str.upper or str.lower method. Keep in mind that we don't call this method -- we give it to the sorted() function to call. We're doing this because sorted() will use whatever function we wish, to modify each element for the purposes of sorting. If we give it str.upper it will sort 'a' as 'A' and 'B' as 'B' and 'c' as 'C' -- this should indicate to you how we are able to use it for alphabetic sorting with mixed-case strings.

 
Ex. 5.12 Given your understanding that the key= argument to sorted() will in a sense process each element through whatever function we pass to it, sort these strings by their length, and print the sorted list.
mystrs = ['I', 'was', 'hanging', 'on', 'a', 'rock']

# your code here
Expected Output:
['I', 'a', 'on', 'was', 'rock', 'hanging']

If you see this message:

TypeError: len() takes exactly one argument (0 given)

it means that you added parentheses and thus attempted to call the len function. Keep in mind that we don't call this method -- we give it to the sorted() function to call. We're doing this because sorted() will use whatever function we wish, to modify each element for the purposes of sorting. If we give it len it will sort 'I' as 1, 'was' as 3, 'hanging' as 7, etc -- perfect for our purposes.

 
Ex. 5.13 Given the following numbers, which were retrieved from a file as strings, sort the strings numerically without creating a new list. (Hint: based on your understanding that the key= argument to sorted() will in a sense process each element through whatever function we pass to it, use a function that converts numeric strings to integers.

If you see a message similar to the one mentioned in the previous exercise, it means you've done something similar to the issue mentioned there. Remember that key= references a function, it does not call it (thus we will not use parentheses).

mynums = ['5', '101', '10', '1', '3']

# your code here
Expected Output:
['1', '3', '5', '10', '101']
 
Ex. 5.14 Experiential exercise. Please run the following code:
def my_element_modifier(arg):
    lower_arg = arg.lower()
    print(f'sorting element "{arg}" by value "{lower_arg}"')
    return lower_arg

sorted_list = sorted(['e', 'c', 'D', 'B', 'a'],
                     key=my_element_modifier)
print(sorted_list)

Note the output:

sorting element "e" by value "e"
sorting element "c" by value "c"
sorting element "D" by value "d"
sorting element "B" by value "b"
sorting element "a" by value "a"

['a', 'B', 'c', 'D', 'e']

This exercise demonstrates that my_element_modifier() was called once for every element, in this case 5 times. arg is assigned each element in turn. And the value returned from my_element_modifier() is the value by which each element is sorted -- so 'a' is sorted by the value 'a', 'B' is sorted by the value 'b', 'c' by 'c', 'D' by 'd'. This facilitates the alphabetic sorting of these values. The values themselves don't change, but Python sorts according to the value returned from the function. If this makes sense to you, please go back to the previous examples and link this understanding to the other functions and methods we've used before -- using int, len, str.upper, etc. They are all passing a function to be applied to each element, and Python is sorting by the value returned from the function or method.

 
Ex. 5.15 Given the below list, sort line_list by the number at the end of each line. Loop through and print the sorted list. (Hint: call sorted() on line_list, and make your key= value the name of a custom function that takes the line as an argument and returns the value of the number at the end of the line. Your custom function will simply take an arg (that will be a string, the line from the file), split the line into elements, and return the last element as an integer.
line_list = [
   'the value 3',
   'this value 1',
   'that value is 4',
   'the value here is 2'
]

# your code here
Expected Output:
line_list = [
   'this value 1',
   'the value here is 2',
   'the value 3',
   'that value is 4'
]
 
Ex. 5.16 Sort the lines of the file pyku.txt by the number of words on each line. (Hint: write a custom sort function that takes a single line of text, splits the line into a list, and returns the length of the list. Pass the readlines() of the file to the sorted() function.) Also here I am using rstrip() to strip each line before printing.
fh = open('../pyku.txt')

lines = fh.readlines()

# your code here
Expected Output:
We're out of gouda.
Spam, spam, spam, spam, spam.
This parrot has ceased to be.
 
Ex. 5.17 Sort the lines of revenue.csv by the numeric value in the last field by passing the list of lines returned from readlines() of the file to sorted() and using a custom sort sub similar to an earlier exercise. (I am also rstrip()ping each line before printing it.)
fh = open('../revenue.csv')

lines = fh.readlines()


# your code here
Expected Output:
Dothraki Fashions,NY,5.98
Hipster's,NY,11.98
Awful's,PA,23.95
Westfield,NJ,53.90
The Clothiers,NY,115.20
The Store,NJ,211.50
Haddad's,PA,239.50
 

SORTING WITH LAMBDA FUNCTIONS

Ex. 5.18 Convert a function to lambda.

The following function takes one argument and returns the value doubled. Convert to lambda and use in the map() function.

def doubleit(arg):
    return arg * 2

seq = [1, 2, 3, 4]

seq2 = map(doubleit, seq)   # replace doubleit with a lambda here

print(list(seq2))           # [2, 4, 6, 8]
 
Ex. 5.19 Convert another function to lambda.

The below function by_last_float() takes a string argument and returns a portion of that string (converted to float) as return value. Replace this function with a lambda.

revenue = '../revenue.csv'

def by_last_float(line):
    words = line.split(',')
    return float(words[-1])

fh = open(revenue)
lines = fh.readlines()
for line in sorted(lines, key= # your lambda function here):
    line = line.rstrip()
    print line
 
Ex. 5.20 Use a lambda to sort strings as floats.

Given the below list of strings that have float values, write a lambda to sort these values by their float equivalents. (Although this is possible without using a custom function or lambda, we would like to practice lambdas here.)

fvals = ['39.5', '3.0', '300.3', '2.002', '20.95',
         '200.1', '21.3', '20.03']


svals = sorted(fvals, # your lambda here)

print(svals)     # ['2.002', '20.03', '29.95', '21.3',
                 #  '200.1', '3.0', '39.5', '300.3']
 
Ex. 5.21 Use a lambda to sort a dict by value.

As you know, sorting a dict means sorting its keys. Write a lambda that expects a dict key as argument, and returns the value for that key as return value.

d = {'a': 10, 'b': 2, 'c': 5}

skeys = sorted(d, key=# your lambda function here)

print(skeys)        # ['b', 'c', 'a'] (in order of value, low-to-high)
 

SORTING COMPLEX STRUCTURES

Ex. 5.22 Given the following list of dicts, sort the dicts by each dict's last name. Loop through and print each dict.
people = [
    { 'id':     '12345',
      'fname':  'Joe',
      'lname':  'Wilson',
      'addr':   '12345 Main Street' },

    { 'id':     '00355',
      'fname':  'Marie',
      'lname':  'Adams',
      'addr':   '888 Elm Street' },

    { 'id':     '98973',
      'fname':  'Alex',
      'lname':  'Wilson',
      'addr':   '23 Marsh Avenue' }
]

# your code here
 
Ex. 5.23 Given the following dict of dicts, sort the dict keys by the last name. Loop through and print each key.
people = {  '12345': { 'fname':  'Joe',
                       'lname':  'Wilson',
                       'addr':   '12345 Main Street' },

            '00355': { 'fname':  'Marie',
                       'lname':  'Adams',
                       'addr':   '888 Elm Street' },

            '98973': { 'fname':  'Alex',
                       'lname':  'Wilson',
                       'addr':   '23 Marsh Avenue' }
}

# your code here
 

HIGHER ORDER FUNCTIONS

Ex. 5.24 map() and filter() functions: call below functions to see result. Play with return value of functions to see altered result.
def doubleit(arg):
    return arg * 2

def is_positive(arg):
    if arg > 0:
        return True
    else:
        return False

proclist = [-3, -2, -1, 0, 1, 2, 3]

transformed_list = map(doubleit, proclist)

filtered_list = filter(is_positive, proclist)

print('* mapped list *')
print(transformed_list)

print()

print('* filtered list *')
print(filtered_list)
 
Ex. 5.25 greet has been assigned to callit. Call greet inside the function.
def callit(arg):
    # your code here

def greet():
    print('hello, world')

callit(greet)
 
Ex. 5.26 greet has been returned from returnit. Call greet outside the function.
def returnit():
    def greet():
        print('hello, world')
    return greet

x = returnit()
 
Ex. 5.27 Demo: a "decorator" function -- a function that both takes a function as argument and returns a function as return value. Use the prior concepts (regarding passing a function reference to another function and returning a function reference from a function) to understand what is happening here.
def my_decorator(func):    # func: function, 'whoop'

    def wrapper():
        print("Something is happening before whoop() is called.")

        func()             # calls 'whoop'

        print("Something is happening after whoop() is called.")

    return wrapper         # return function 'wrapper'


def whoop():
    print("Wheee!")


# now the same name points to a replacement function
whoop = my_decorator(whoop)          # function 'wrapper'

# calling the replacement function
whoop()                              # (calls function wrapper)
 
Ex. 5.28 Same function as last time -- this time use @ ("pie syntax") to produce a "true" Python decorator. Place the decorator just above the function def for this_function, and remove the call to my_decorator().
def my_decorator(func):        # func:  function, 'whoop'

    def wrapper():
        print("Something is happening before whoop() is called.")

        func()                 # calls whoop()

        print("Something is happening after whoop() is called.")

    return wrapper             # return function 'wrapper'

@my_decorator
def this_function():
    print("Wheee!")


# calling the replacement function
this_function()                # (calls function wrapper)
 
Ex. 5.29 Understanding *args and **kwargs: the below function has 2 positional arguments and 2 keyword arguments, and prints the arguments inside the function. Call the function as shown, and see the the arguments printed. Now replace the arguments in the signature with *args and **kwargs and print the variables args and kwargs inside the function.
def printargs(w, x, y=0, z=None):

    print(w, x, y, z)


printargs(5, 10, y=100, z='Hello!')
 
Ex. 5.30 Same decorator function as last time -- this_function() has been defined to take two positional arguments and two keyword arguments, printing these arguments inside this_function()). Inside wrapper(), we accept these arguments and pass them to myfunc() (which is this_function(). Run this program to see that it works. Now replace the positional arguments with *args and the keyword arguments with **kwargs and see that it works in the same way.
def my_decorator(func):             # func:  function, 'whoop'

    def wrapper(*args, **kwargs):   # args: tuple, (5, 10); kwargs: dict, {'x': 99, 'y': 'hey!'}
        print("Something is happening before whoop() is called.")

        func(*args, **kwargs)       # calling whoop(5, 10, x=99, y='hey!')

        print("Something is happening after whoop() is called.")

    return wrapper                  # return function 'wrapper'

@my_decorator                       # replacing whoop with wrapper
def whoop(a, b, x=None, y=None):
    print(f"Wheee!:  {a}, {b}, {x}, {y}")


# calling the replacement function
whoop(5, 10, x=99, y='hey!')        # calling wrapper(5, 10, x=99, y='hey!')
 
[pr]