Python Basics

Posted by Shan J. on April 12, 2019

All programming languages must be able to:

  1. remember things,
  2. repeat things,
  3. communicate, and
  4. make decisions.

Functions are all about #2: repeating things. Think of a function as a standalone mini-program: It takes some inputs, performs some operations, and generates an output. We define a function once, and we can execute the code define within a function as many times as we like – repeating all those lines of code with a single invocation.

The code we’ve written in Python have already employed functions – so far, we’ve defined a function named main and invoked it to kick off the execution of a program. But we’ll go way beyond that, defining many functions to do one small thing apiece, and putting them together into a larger program. Now that our programs are starting to get more robust and sophisticated, you’ll define functions beyond just main for pretty much everything you write. Not just in Python and not just in DS2000, but for all of your computer science/data science/programming going forward.

Why Functions

Functions help us to organize our code. They save us from writing a big, long, ugly main. A function is written once and then its code can be executed many times.

Functions are short and concise. Keep this rule of thumb in mind:

A  Function has one job.

We define a function to do its one little thing, and then we can execute that code many times, on all kinds of different inputs.

We define functions because they are:

  1. Modular. We end up with many small, single-purpose functions, which we can then piece together into a larger program. It’s the same reason IKEA gives us a bunch of separate shelves that we assemble; it might be annoying, but it’s much easier than if we had to build the whole bookshelf from scratch ourselves.
  2. Testable. We’ve written test cases for our Python programs in this class, which are great and useful. The drawback of what we’ve done so far, with everything in one big main, is that we need to provide input at the prompt and then eyeball the output to make sure it all worked as expected. Functions enable us to test our code programmatically – instead of a human eyeball, it’s Python code that determines whether a function generated the correct output given an input. Much more reliable.
  3. Reuseable. Once a function is defined, we can use it in many programs, sometimes with wildly different purposes. For example, Python provides a function that generates a random number, and I’ve used this function for programs that roll dice, deal a card from a deck, play a guessing game, play rock/scissors/paper, and basically any game of chance you can think of, because they’re my favorite programs to write. That one function has come in handy in tons of ways.

Name, Parameters, Return Type

A function is uniquely defined by three characteristics:

  1. Name (required) . Similar to a variable name, a function is named so we can refer to it elsewhere in the program.
  2. Parameters (zero or more). The data types of its expected input(s). A parameter is a variable that is attached to the function. Because variables must be initialized before we can meaningful use them, a parameter is initialized by the function caller.
  3. Return Type (zero or one). The data type of its expected output. A function performs some operation on its input, and returns the result to the function caller.

Here’s an example of a simple function in Python, defined by the function writer:

def divide(x, y):
    ''' function: divide
        parameters: two numbers, x and y
        returns: a float, the result of dividing x / y
    '''
    result = x / y
    return result

Let’s break it down to the important parts:

  • Name: divide
  • Parameters: two numbers, which could be floats or ints. The parameters are variables attached to the function, and they are named x and y. You can see that x and y are not initialized here in the function definition, but we know that we need them to be initialized so that we can use them – they’ll be given values by the function caller.
  • Return type: a float. The function returns the result of x / y, which is a float even if x and y are both integers.

A few other things to notice here:

  • There is a block comment written just under the function name. It must be in block comments so that pydoc will generate your function documentation correctly. For DS2000, we require both parameters and return type to be specified here, but including the name is optional (it’s just something I like to do so I can see them all together in IDLE’s green highlights).
  • The last line of the function is a return statement. Return is a reserve word in Python. Its job is to hand a value back to the caller. Once a return statement is executed, flow of control also returns to the caller. If any additional code were written here underneath the return statement, it would never be executed.

A function is not required to return a value. If it doesn’t return anything, we call it a “void function,” and we say that its return type is void.

The main functions we’ve defined in our programs so far don’t return anything; when the flow of control reaches the end of the function, the function just ends without explicitly requiring a return statement.

Calling the function. Here’s how it would look if I call the divide function from somewhere inside my main function:

What to notice here:

  • We had to know the name, parameters, and return type of this function in order to call it. That’s how we know the correct syntax for invoking it.
  • We’ve named our variables here a and b and initialized them to have the values 14 and 5. Therefore, 14 and 5 become the values associated with the function’s parameters x and y. The values 14 and 5 are the arguments to the divide function.
  • Because the function divide returns a value, the function caller must save the value it returns. Otherwise, the result of dividing 14/5 would be lost. Here, we’re saving the result in a variable, but we could also print it out, or use it as input to another function… but we can’t ignore it.

Flow of Control

Without functions, a basic little Python code snippet would be executed from top to bottom and left to right, just like reading a book. Here’s an example you might recognize from class: converting my dog’s weight in stone to his weight in pounds.

1 weight_in_stone = 5.35
2 weight_in_pounds = weight_in_stone * 14
3 print(‘Tug weighs’, round(weight_in_pounds, 2), ‘pounds!’)*

The flow of control is easy to follow and predictable. The lines are executed in this order:

  • Line 1
  • Line 2
  • Line 3

Now let’s turn this little bit of code into a function. Even though there’s not much going on with this calculation, it makes sense to turn it into a function, because:

  • It could easily be reused in lots of different contexts.
  • It clearly takes one input (weight in stone) and computes a value (weight in pounds).
  • It’s a calculation we might need more than once in the same program.

Turning this calculation into a function alters the flow of control. Flow of control begins with main, which is often called the “driver”, and should be the first place you look when trying to understand someone else’s code. Then, flow of control flows into a function when it is invoked. Flow of control goes back to the caller when a return statement is executed. Let’s take another look at this example, as a more complete Python program:


def stone_to_pounds(num_stone):
    num_pounds = num_stone * 14
    return num_pounds

def main():
    weight_in_stone = 5.35
    pounds = stone_to_pounds(weight_in_stone)
    print('Tug weighs', round(pounds, 2), 'pounds!')

main()

The flow of control really begins at line 10 ( at line 1 and line 5, we define functions but don’t execute them so the flow of control doesn’t go there yet). When we call the stone_to_pounds function, the flow of control goes up to line 1, and then returns to the caller at line 7. In all, here’s the flow:

  • Line 10
  • Line 5
  • Line 6
  • Line 7 # invoke stone_to_pounds
  • Line 1
  • Line 2
  • Line 3
  • Line 7 # flow of control returns to caller;

# variable pounds is initialized

  • Line 8

At line 3, the flow of control returns to the caller. Whatever value is stored in the variable num_pounds is handed back to the caller. This value is used to initialize the variable pounds in main.

A Word on Testing (and more soon…)

We said above that one of the reasons for writing functions in the first place is so we can test our code programmatically.

We’ll talk a lot more about testing and debugging in the next few weeks, but for now think of it this way: in the example above, we wrote a function to divide two numbers. Rather than calling the function and eyeballing the output, we can now call the function and write some code to verify that it worked as expected. Rather than hitting function+F5 over and over again to run the program with different inputs, we can use code to test our function many, many times without breaking a sweat. It’ll be great.

Testing and Debugging

We already know the most important part of testing and debugging: when something goes wrong, DON’T PANIC!!

The second most important part of this process is to find possible issues in our code early. Remember, most of the software you write will be used by people who aren’t you. We design and write a big, hopefully useful, piece of code, and hand it off to a group of users who want our software so they can accomplish something meaningful, creative, and substantive. And – I say this from experience – your users will act as if they are trying to break your program. Your job is not to let them.

Testing and debugging work together:

  • Testing is the process of running a program (or parts of a program) to ascertain whether it works as intended.
  • Debugging is the process of fixing a program that you already know does not work as intended.

Good programmers write their programs in a way that makes them easier to test and debug. It’s not something to think about after you’ve written your program, but to think about before you begin writing.

We’ll get some practice with testing in DS2000 by writing test suites. A test suite is a separate Python program, with its own main, that repeatedly calls a function you’ve written to validate that it works correctly. A test suite will contain tests for multiple functions.