Functions

Last week, we practiced invoking or calling functions. In class and lab, we used built-in functions (print in Python and fitMedia in EarSketch). Now, we'll learn how to actually define our own functions!

First, let's think about why we'd want to define functions. There are two main purposes functions serve:

  • they give us code reusability by placing instructions for a common procedure in one place

    • this also lets us easily modify the behavior by editing only one part of our code

  • they provide us with a tool for abstraction

Abstraction

Abstraction is an extremely important tool that computer scientists use for solving problems. It lets us effectively solve problems by starting at a coarse-grained level and iteratively refine down to the details.

You can imagine abstraction as a sort of "zooming" that lets you change the level of detail you're thinking about

  • "zooming in": this is like writing an outline for a paper to develop a broad plan before zooming in one level and adding more detail

  • "zooming out": once we've thought through the instructions for a particular procedure, we can simply use it without thinking through the approach again

Getting started

Let’s consider a real world example of ordering coffee at a cafe. Let’s say I wanted to order a latte, with a certain strength (the number of espresso shots) and sweetness (the number of sugar spoonfuls). When I place my order, the order taker passes a note to the barista that says:

order -- latte, 2, 3

Hmm… well, we know I’m telling the coffee shop three things:

  • the type of drink to be made

  • the number of shots in that drink

  • the number of sugar spoonfuls to add

So, does this mean:

  • make a latte with 2 shots of espresso and 3 spoonfuls of sugar, or

  • make a latte with 3 shots of espresso and 2 spoonfuls of sugar

Order matters!

For there not to be confusion, the order taker and barista agree to a protocol ahead of time:

  • first, give a word (think string) that indicates the drink type

  • second, give a number (think int) that indicates the strength of the drink (number of shots)

  • third, give a number (think int) that indicates the sweetness (number of sugar spoonfuls)

Recall that we said each of these pieces of information is called a parameter and we implicitly associate their individual types. Why do the types matter in the protocol? This helps to avoid confusion and prevent incorrect calls of our function. It also makes it more clear to both the user of the function and the provider what sort of values could be valid for the parameters. We'll use type-hinting to set this protocol.

Notice that when I place a coffee order, I just need to know how to order my coffee. I do not need to know how to make the coffee.

As another example, consider an ATM machine. It requires two pieces of information to be given (or, passed) to it – these are its parameters:

  • a bank card

  • a PIN number

Now notice that, even if there were no signs for “Enter your card here” or “Enter your PIN here,” the little slot and keypad already tell you something about what the ATM expects to be passed:

  • the little slot is familiar to most ATM users, and they will assume that their bank card should be input there

  • the keypad is also familiar, and they will assume that their PIN should be typed there

Therefore, the types of the expected input already give information as to how to use the ATM and prevent incorrect interaction.

Now, let’s switch roles so that now we are the barista who makes the coffee. In this case, we need to know how to make a latte, how big a shot is, and how to measure the sugar. However, we don’t really care what the person who ordered the coffee does after we give them their coffee.

Similarly, in the ATM case, the ATM needs software and hardware to check the validity of the ATM card, and to carry out the user’s request. It needs to carry out the detailed instructions involved in doing this, but it doesn’t really care what the customer does after they take their money and ATM card.

In both examples, the required instructions can be done many times for many different customers. The barista has learned how to make lattes, and follows the same instructions over and over, with just slight variations. Similarly, the ATM was programmed to dispense money, and it does this over and over for different customers.

In programming, we separate the notion of defining a function from invoking a function. When we define a function, we are writing down instructions. We give these instructions a name so we can refer to them later. When we invoke a function, we use its name, and then Python looks up the definition of the function and executes the instructions associated with that name.

Function structure

A function definition associates a block of code with an identifier. It is composed of two parts:

  • the signature or declaration, which establishes the protocol for interaction

  • the body, which contains the code that should be executed upon invocation

The declaration may include parameters, which can be used to specify inputs, while the body may use return statements to give back output to the function “caller.”

Functions in Python

Signature/Declaration

When we begin to develop a function, we want to be clear about our expectations of its usage and behavior.

  • In most programming languages, a function must be specified with a function declaration that captures the expected usage.

  • A comment is typically used to indicate the behavior.

Every function has a function signature containing the agreed upon protocol for users to invoke it. This consists of:

  • the name (e.g., orderCoffee)

  • the parameter list, ideally with well-named identifiers (e.g., drinkType, numberOfShots)

In Python, the signature is captured by a declaration using this syntax:

def *function_identifier*( *parameter1*, *parameter2*, ... ):

With type-hinting, the syntax is:

def *function_identifier*( *parameter1*:*type1*, *parameter2*:*type2*, ... ) -> *return_type*:

For example:

def orderCoffee( drinkType:str, numberOfShots:int, numberOfDrinks:int) -> str:

""" Takes in the drinkType (a string which could be "cappuccino", "latte", etc.),

the numberOfShots of espresso (an int) and

the number of drinks being ordered (an int).

Makes the coffee and returns the total cost (as a float)"""

Notice the use of the indented multi-line comment below the function signature to describe the parameters and their expected types. This comment is called a “docstring” and is a standardized way of documenting our code for other programmers (as well as ourselves).

Definition/Body

The function signature alone is not valid code in Python; every function must have a body:
a block of code which gives the set of instructions to be executed when the function is invoked. Python’s syntax requires the function body to follow the signature as an indented block of code.

def *function_identifier*( *parameter1*:*type1*, *parameter2*:*type2*, ... ) -> *return_type* :

''' documentation via docstring convention '''

# function body, indented here


# optional return statement

return <some_value_of_type_return_type>

Sometimes we get loose with terminology and refer to the body as the function "definition" -- technically, though the definition includes the declaration as well!

Examples

def helloWorld() -> None:

"""Print out "Hello, World!"""

print( "Hello, World!" )


def helloYou( name:str ) -> None:

"""Takes in a name as a string and prints a greeting."""

print( "Hello, " + name )


def courseName() -> None:

"""Returns the course name."""

return "Introduction to Computer Science"


def square( x : int ) -> int:

"""Takes in a number x and returns the square of x."""

return x*x

What happens when we run this code?

Here's another program:

def helloWorld() -> None:

"""Print out "Hello, World!"""

print( "Hello, World!" )


def helloYou( name : str ) -> None:

"""Takes in a name as a string and prints a greeting."""

print( "Hello, " + name )


helloWorld()

helloYou("Mary Lyon")

name : str = input("What is your name?")

helloYou(name)

What happens when we run it?

Parameters, arguments and return statements

It will take some time and practice to get used to this!

To finish the treatment of functions, though, here are some last technical details...

Parameters and arguments

When we are defining a function, we give our parameters a general identifier (such as drinkType or name); the parameter names are then used in the function, just like variables. However, if we look at the function definition, we can see that the parameter is not given a value there.

def helloYou( name : str ) -> None:

"""Takes in a name as a string and prints a greeting."""

print( "Hello, " + name )


Instead, the parameter is given a value when the function is invoked:

helloYou("Mary Lyon")


When we are invoking our functions, we will pass a particular argument to the function for that specific execution (such as "Mary Lyon"). The parameter will be assigned that value during that execution.

Return statements

Often, we will want the function to calculate something and give us the answer so that we can use that answer. For example, suppose we had a function that calculated the square of a number. We could use it like this:

userInput : str = input("What number would you like to square?")

num : int = int( userInput ) # convert the input value to an int type

squared_num : int = square(num)

print( "The square of " + str(num) + " is " + str(squared_num) )


In Python, we use the keyword return within a function definition to signal that the function is giving an answer back to whatever invoked it. When a return statement is executed, the function stops execution and gives “control” back to the function caller.

For example, here is the definition of the square function. The square function just multiplies the parameter value by itself and returns the result.

def square( x:int ) -> int :

return x*x

Let's try it!

You don't have to return anything!

Some functions don't return any values. In this case, you can omit the return statement and use the value None for type-hinting. We saw this in the previous sample code:

def helloYou( name : str ) -> None:

"""Takes in a name as a string and prints a greeting."""

print( "Hello, " + name )

What's the difference between print and return?

You might be wondering what the difference is between a print statement and a return statement. A print statement is used to display information to the user, as in the last line of the code shown above. A return statement is used to allow a function to tell the code that called it what value it computed. This is what allows us to put a function call on the right hand side of an assignment statement.

Code after a return statement is not executed

A function stops executing when it executes the return statement and control returns to the caller.

When the following code is run, observe that the print statement after the return statement did not execute. This is because the return statement causes the square function to stop executing. Execution continues at the place where the square function was called. So, never put code after a return statement. It won’t be executed!

def square( x:int ) -> int:

newNum = x * x

return newNum

print ("Executing the statement after the return statement!")


num : int = 3

squared_num = square (num)

print ("The square of 3 is " + str(squared_num))

Output:

The square of 3 is 9

Takeaways

  • Functions give us code reuse and abstraction.

  • Function declarations provide the interface for how to use/interact with the function.

    • Parameters customize behavior.

  • Function definitions specify the templated behavior.

    • Indentation matters!

    • Use the parameter names as if they are variables (but do not assign values to them).

    • Return statements are optional; they give data back to the "caller."

  • Invoking a function requires passing the correct number of arguments; the arguments are used to assign values to the parameter names referenced in the function definition.

  • A tool like mypy can help check that your code respects the types specified by type-hinting in the declaration.

  • This will all take TIME and PRACTICE to get used to.

    • Don't forget: you are learning a new language! Some immersion and feelings of being confused are to be expected!