3.4. Functions

As you may have noticed throughout the work we’ve been doing so far, a lot of enciphering and deciphering requires repetition of the same operations. We take a letter (an input), perform a series of operations with the input, and calculate the output, another letter. This input/output relationship is often called a function in mathematics. In Python, we can write our own functions that we can call by name to help us from having to rewrite the same blocks of code over and over in our programs.

You’ve been using functions all throughout this course that are default functions in Python. The print() function is a good example. It works the same way every time and you don’t need to know how it was programmed, just how to use it. We’ll be writing our own functions for the same reason in this course.

3.4.1. Why Use Functions?

  1. Functions are a convenient way to divide your code into useful chunks that do one thing very well. This becomes especially useful when you need to repeat whatever task your function accomplishes many times.

  2. The variables that are created inside a function exist and can only be used from inside that function. They are called local variables, as opposed to global variables which can be accessed by any part of your program. Once the function is done performing its task, it returns specified information back to your main program and deletes all the local variables created while running. Not having to know about these variables is convenient, much like how we don’t need to know what’s going on inside the print() function.

  3. Because of points 1 and 2, functions are portable. You can copy/paste functions from one notebook to another, and they’ll work exactly the same way for a given set of inputs. It ensures you don’t need to worry about having variables named the same way as someone else, or the same way you named them in a different project.

3.4.2. Defining a Function

A Python function, much like a mathematical function, requires a name for the function, a name for the input (or inputs), a definition about what to do with the input, and an output.

For example, the mathematical function:

\[ f(x,y) = x + y \]

has the name \(f\), inputs \(x, y\), a definition that explains how to use \(x\) and \(y\), and the output \(x + y\).

We can use Python to accomplish the same result:

def f(x, y):
    sum = x + y
    return sum

print( f(2,3) )
5

Notice some key features of how we defined the function:

  • use the def command to indicate you’re defining a function

  • name the function. You can use a word, you’re not limited to a single character

  • open parentheses ( and then list the name or names of the variables you’ll provide for the function

  • close parentheses ) and then type a colon (:)

  • indent any code that is specific to the function

  • when the function’s task is complete, indicate what you want the function to return as the output using the return command

    • As soon as a function reaches a return command, it will return whatever is specified to the main program and exit the function, even if the function was in the middle of a loop or other operation

    • A function can have more than one return statement. This could be useful when combined with an if/elif/else logic statement if you want the function to return different values depending on certain conditions.

    • Unless you have a good reason not to, every function should include at least one return statement.

This function does not print the output by itself, instead it returns an value that we then pass into the print() function. This is a nice feature because we may want to use the function sometimes without printing the output. For example:

print( f(1,2) + f(3,4) )
10

If the function f printed the sum as part of it’s task, the output of the previous command would have been:

3
7
10

Because each time function f was used it would have printed to the screen. It’s usually best for functions to return the needed information to the main program, and let the main program decide what to do with it. This retains flexibility in how functions can be used.

3.4.3. railfence_encipher() Function

Now that we have a function that cleans text, we can use that as a part of other functions. For example, if we were to write the 2-row railfence encipher as a function, we can use the text_clean() function to clean whatever string we pass into the railfence_encipher() function we write.

def railfence_encipher(text, rails):
    """
    Arguments: 
        text (str): plaintext message
        rails (int): the number of rails to use in the cipher
    Returns:
        (str): ciphertext after implementing the railfence cipher
    """
    
    # We'll learn how the text_clean function works later
    cleanedtext = text_clean(text)
    
    if rails == 2:
        ciphertext = cleanedtext[0::2] + cleanedtext[1::2]
    
    return ciphertext
print( railfence_encipher('test message.', 2) )
print( railfence_encipher('This is WAY easier than doing this by hand!', 2) )
TSMSAEETESG
TIIWYAIRHNONTIBHNHSSAESETADIGHSYAD

3.4.4. Optional Keyword Arguments

Python functions can include optional keyword arguments, or default arguments. This feature allows you to create arguments that are optional to include when using a function, and if they are not included, you can specify the default value that they’ll be assigned. This is helpful when writing functions that have a very common use case, but may occasionally be need to be used differently. For example, suppose you write a function caesar that implements the Caesar cipher. You may choose to assign a boolean value to variable encipher that is used to decipher if the function will encipher or decipher a message.

def caesar( text, key ):
    encipher = True
    
    if encipher == True:
        # code for enciphering would go here
        ...
        ...
        
    else:
        # code for deciphering would go here
        ...
        ...
    
    return caesar_output

This function would work just fine, however it does require you to change the code for the function every time you want to switch it’s mode from enciphering to deciphering. This can be annoying for you to keep changing, but it becomes a big issue if you don’t have easy access to the source code for a function, or need to be able to run the function in different modes one time after the other without an opportunity to change the code. This is a great case for an optional keyword argument! Here’s how you can implement this feature:

def caesar( text, key, encipher = True ):
        
    if encipher == True:
        # code for enciphering would go here
        ...
        ...
        
    else:
        # code for deciphering would go here
        ...
        ...
    
    return caesar_output

You can see that the majority of the code remains the same, but the assignment of the variable encipher now occurs in the header of the caesar function. This allows the user of this function to decide whether or not to override the default value of True assigned to encipher, or leave it as it.

Now, when calling the function you can do either of the following ways and it will return the ciphertext output:

caesar( 'testmessage', 7 )
caesar( 'testmessage', 7, encipher = True )

In order to decipher a message with this function, you would need to override the default value assigned to encipher to False as follows:

caesar( 'ALZAT LZZHN L', 7, encipher = False )

If you find yourself always tweaking a value in your code to change how it runs, you probably should consider using an optional keyword argument in your function!

3.4.5. Function Documentation

Functions are very powerful, but you need to understand the input arguments and what to expect from the return statements in order to use them properly. It is difficult enough to remember the order of the arguments and format of the return statements for functions you’ve written yourself, let alone for functions written by others. Comments inside the function are good way to document how code works, however you won’t always have access to the source code for functions, so you’ll need to know how to create good documentation for functions you write, and access documentation for functions that you or others have written.

3.4.5.1. The help Function

You can access the documentation for functions by using the built-in Python function help. For example:

help( print )
Help on built-in function print in module builtins:

print(...)
    print(value, ..., sep=' ', end='\n', file=sys.stdout, flush=False)
    
    Prints the values to a stream, or to sys.stdout by default.
    Optional keyword arguments:
    file:  a file-like object (stream); defaults to the current sys.stdout.
    sep:   string inserted between values, default a space.
    end:   string appended after the last value, default a newline.
    flush: whether to forcibly flush the stream.

You can see that the help function shows you how to use the print function should be used and all the possible arguments it takes in. We normally just use the first argument, value, but we can see there are optional keyword arguments with default values, sep, end, file, and flush that alter how that function works. This information is displayed because the print function has what’s called a documentation string or docstring.

3.4.6. Writing a docstring

A docstring is a string object that’s included inside a function that is displayed when the help function is used. The text_clean and railfence_cipher functions shown previously in this section both contain a docstring. A docstring is included between sets of three double-quotation marks """  """ immediately underneath the definition of the function. An example is shown below

def sum_function( x, y ):
    """
    This is a docstring.
    It can span multiple lines
    It starts right underneath the definition
    It's wrapped in three double-quotes
    """

    return x+y

There are a variety of ways to use a docstring when writing functions, and many businesses and organizations standardize what should be included in a docstring. In our usage in this course and online resource we’ll set the following standard.

A docstring should include:

  • a list of arguments (inputs)

  • a list of possible returns (outputs)

  • an indication of the object type for arguments and returns (int, str, float, bool, etc)

  • an indication if an argument is an optional keyword argument

  • a brief description about what arguments and returns they represent.

An example of a docstring that meets these requirements is shown below:

def caesar( text, key, encipher=True ):
    """
    Arguments:
        text (str): a string of either plaintext or ciphertext
        key (int): an integer that represents the key for enciphering/deciphering
        encipher (bool, optional): True indicates the function should encipher a message. False indicates deciphering.
    Returns:
        (str): the output string will represent the completed enciphering/deciphering of the input string
    """
    ...
    ...
    return caesar_output

3.4.7. Exercise for the Reader

  • Modify the railfence_encipher() function so it can also encipher messages that use 3 rails

  • Write a function to implement the Atbash cipher for encryption