Functions

Functions provide a way to package often-used code into reusable and easy to use components. For example, here is some code that multiplies together two lists

In [1]:
list1 = [2, 4, 6, 8]
In [2]:
list2 = [10, 20, 30, 40]
In [3]:
list3 = []
In [4]:
for x, y in zip(list1,list2):
    list3.append(x * y)
In [5]:
list3
Out[5]:
[20, 80, 180, 320]

We don't want to keep typing out the above code every time we want to multiply the numbers in two lists. Instead, we can collect that code together into a function

In [6]:
def multiply(a, b):
    c = []
    for x,y in zip(a,b):
        c.append(x*y)
    return c

We can now call this function directly on our lists, e.g.

In [7]:
list3 = multiply(list1, list2)
In [8]:
list3
Out[8]:
[20, 80, 180, 320]

The function is called using its name, and passing in the values as two arguments, e.g.

In [9]:
list4 = multiply( [1,2,3], [4,5,6] )
In [10]:
list4
Out[10]:
[4, 10, 18]

The arguments are placed inside the round brackets. These are copied, in order, to the function. For example, [1,2,3] is copied into a, and [4,5,6] is copied as b. The code in the function is then executed. We can watch this code being run by adding in print statements, e.g.

def multiply(a, b):
    print("a = %s" % a)
    print("b = %s" % b)
    c = []
    for x,y in zip(a,b):
        print("%s times %s equals %s" % (x,y,x*y))
        c.append(x*y)
    print("c = %s" % c)
    return c

You must pass the right number of arguments into a function. For example, this is what happens if you get the number of arguments wrong...

In [11]:
list5 = multiply(list1)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-11-2ad4f6a161c6> in <module>()
----> 1 list5 = multiply(list1)

TypeError: multiply() missing 1 required positional argument: 'b'

You can write functions that take as many (or as few) arguments as you want. For example, here is a function that takes no arguments, and then a function that takes lots

In [12]:
def func0():
    return "no arguments to this function"
In [13]:
def func1(a, b, c, d, e=5):
    return a+b+c+d+e
In [14]:
func0()
Out[14]:
'no arguments to this function'
In [15]:
func1(1, 2, 3, 4, 5)
Out[15]:
15
In [16]:
func1(1, 2, 3, 4)
Out[16]:
15

Note that with the last function we have set a default value of the argument e. This is given the value of 5 if it is not specified. This allows us to pass 4 arguments instead of 5. Changing the default value by editing the definition of the function above will thus change the output of func1 when it is called with only four arguments.

Exercise

Here is the morse code dictionary from the last session, together with the code that converts a message from english into morse code.

In [17]:
letter_to_morse = {'a':'.-', 'b':'-...', 'c':'-.-.', 'd':'-..', 'e':'.', 'f':'..-.',
                   'g':'--.', 'h':'....', 'i':'..', 'j':'.---', 'k':'-.-', 'l':'.-..', 'm':'--',
                   'n':'-.', 'o':'---', 'p':'.--.', 'q':'--.-', 'r':'.-.', 's':'...', 't':'-',
                   'u':'..-', 'v':'...-', 'w':'.--', 'x':'-..-', 'y':'-.--', 'z':'--..',
                   '0':'-----', '1':'.----', '2':'..---', '3':'...--', '4':'....-',
                   '5':'.....', '6':'-....', '7':'--...', '8':'---..', '9':'----.',
                   ' ':'/' }
In [18]:
message = "SOS We have hit an iceberg and need help quickly"
In [19]:
morse = []
for letter in message:
    morse.append( letter_to_morse[letter.lower()] )
print(morse)
['...', '---', '...', '/', '.--', '.', '/', '....', '.-', '...-', '.', '/', '....', '..', '-', '/', '.-', '-.', '/', '..', '-.-.', '.', '-...', '.', '.-.', '--.', '/', '.-', '-.', '-..', '/', '-.', '.', '.', '-..', '/', '....', '.', '.-..', '.--.', '/', '--.-', '..-', '..', '-.-.', '-.-', '.-..', '-.--']

Exercise 1

Create a function called encode that takes a message and returns the morse code equivalent. Test this function by encodig the message SOS We have hit an iceberg and need help quickly and check that you get the same result as in the last session. Now try using your function to encode other messages.

In [20]:
def encode(message):
    morse = []
    for letter in message:
        morse.append( letter_to_morse[letter.lower()] )
    return morse
In [21]:
encode(message) == morse
Out[21]:
True
In [22]:
encode("Hello World")
Out[22]:
['....', '.', '.-..', '.-..', '---', '/', '.--', '---', '.-.', '.-..', '-..']

Exercise 2

Using the answer from Exercise 2 in the dictionaries lesson, write a function called decode that converts a morse code message back to english. Check that you can decode the above morse code message back to english.

In [23]:
morse_to_letter = {}
for letter in letter_to_morse.keys():
    morse_to_letter[ letter_to_morse[letter] ] = letter    
In [24]:
def decode(morse):
    english = []
    for code in morse:
        english.append( morse_to_letter[code] )
    return "".join(english)
In [25]:
decode(morse)
Out[25]:
'sos we have hit an iceberg and need help quickly'

Exercise 3

Below is a list of messages. Loop over the messages and check that encode( decode(message) ) equals the original message. Do any of the messages fail to encode and decode correctly? If so, why? How can your check be modified to account for the limitations of your encode and decode functions?

In [26]:
messages = [ "hello world", "this is a long message", "Oh no this may break", "This message is difficult to encode." ]
In [27]:
for message in messages:
    print("checking for message '%s'..." % message)
    print( message == decode( encode(message) ) )
checking for message 'hello world'...
True
checking for message 'this is a long message'...
True
checking for message 'Oh no this may break'...
False
checking for message 'This message is difficult to encode.'...
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
<ipython-input-27-58f3e4df6a62> in <module>()
      1 for message in messages:
      2     print("checking for message '%s'..." % message)
----> 3     print( message == decode( encode(message) ) )

<ipython-input-20-39fdf9cb0c17> in encode(message)
      2     morse = []
      3     for letter in message:
----> 4         morse.append( letter_to_morse[letter.lower()] )
      5     return morse

KeyError: '.'
In [28]:
for message in messages:
    print("checking for message '%s'..." % message)
    print( message.lower() == decode(encode(message)) )
checking for message 'hello world'...
True
checking for message 'this is a long message'...
True
checking for message 'Oh no this may break'...
True
checking for message 'This message is difficult to encode.'...
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
<ipython-input-28-d00d649e627e> in <module>()
      1 for message in messages:
      2     print("checking for message '%s'..." % message)
----> 3     print( message.lower() == decode(encode(message)) )

<ipython-input-20-39fdf9cb0c17> in encode(message)
      2     morse = []
      3     for letter in message:
----> 4         morse.append( letter_to_morse[letter.lower()] )
      5     return morse

KeyError: '.'
In [ ]: