# Functions This week, we will be talking about functions: how they work, how to use them, and how to make our own functions. This will be a turning point for how we code and what we are able to do with our programs! ## What are functions? Functions are a *named* and *defined* block of *reusable* code that is created to perform a **specific** operation. They are helpful for programming for multiple reasons. These reasons include: * They break down a program into smaller, more manageable chunks of code * They improve the readability of our code (because we are dividing and labeling chunks of code based their individual purposes) * They promote reusability, allowing us to use a chunk of code multiple times throughout our program We have already encountered one type of function: the **built-in function**. Built-in functions are simply functions that are already defined by Python. They already have a name and a purpose. Some built-in functions we have encountered include: `print()`, `input()`, `int()`, `float()`, `range()`, etc.. This week, we'll talk about another type of function: the **user-defined function**. User-defined functions are created by us (the programmers) to perform a specific task that we might want/need throughout out code. Once we *define* our own functions, we can use them the same way we use our built-in functions now! ## The general syntax of defining/declaring a function The general syntax for defining/declaring a user-defined function is as follows: ``` def function_name(parameters): # code that we want the function to do return expression ``` We can break up this syntax into the following parts: ![Screenshot 2025-03-08 at 10.40.22 AM](https://hackmd.io/_uploads/SJGQkfcjJx.png) *annotated syntax from [GeekForGeeks](https://www.geeksforgeeks.org/python-functions/)* **The parts of a function declaration:** * `def`: The `def` keyword is the keyword we use to tell Python that we are creating our own function. Similar to other keywords, it is *required* for our function declaration * `function_name`: The name we want to give our function. * The **function naming conventions** are similar to variable naming conventions. * We typically name functions based on the *action(s)* they do. (in other words, function names typically contain a verb of some sort). * `()`: The `()` after the `function_name` is **required** for every function definition, even if there is nothing inside the `()`. * `parameters`: Functions can take `parameters`. Parameters are just variables that are defined with the function declaration. They can be used in the function like any other variable inside the function body. You can read more about parameters [here](https://www.geeksforgeeks.org/deep-dive-into-parameters-and-arguments-in-python/#parameters). * `#statement`: represents the function body. This is where we write the code for the function we are declaring. * `return`: The return keyword can be used in two ways: 1. it can be used if we want to "return" a specific value when the function is finished (that value is "brought back" to where the function was called/used) 2. it can be used to exit the function (with or without a value) ## Using a function We have already had practice with using built-in functions (e.g. `print()`, `input()`, etc.). The syntax of using a function (aka *calling* a function) is simple. When you want to use a function, use the following syntax: ``` function_name(arguments) ``` **Parts of a function call** * `function_name`: in order to call/use a function, we must use the name of the function we want to use. * `()`: similar to function declarations, the `()` is a required part of a function call * `arguments`: arguments are related to parameters. If the function definition contains parameters, our function call will *probably* include arguments. Similarly, if the function definition does *not* contain parameters, it will not have arguments! You can read more about arguments [here](https://www.geeksforgeeks.org/deep-dive-into-parameters-and-arguments-in-python/#arguments). ## Functions Recap: So, in short: **Function declaration:** we declare functions when we want to write a chunk of code that we can reuse multiple times throughout our code. **Function call:** we call functions when we want to use them in our code. When we call a function, it executes the code definied in the function's declaration. ## Declaring our own functions The above syntax is how we define/declare functions in general. However, that is just the general syntax. There are lots of ways use functions. Let's start from the most basic and build our way up. ### The most basic function The most basic function is a function that does not contain parameters, nor does it use the return keyword. That means the syntax for these functions would look a little more like: ``` def function_name(): # function body ``` The most common reason we might use a function as basic as this one is that we want to repeat specific print (or input) operations. For example, say we need to print the *same* information to the terminal multiple times throughout our program. Instead of rewriting the same x-number of print statements whenever we want to do this, we can, instead, put them in a function that we can *call* every time we want to print the given statements. It might look something like this: ```python= # function for printing menu prices def print_menu_prices(): print("Welcome to Costco!") print("Menu:") print("Hot dog: $1.50") print("Pizza Slice: $2.00") print("Chicken Bake: $3.99") ``` Instead of re-writing the same 5 print statements every time we want to print the menu in our code, we can instead call our function: ```python= print_menu_prices() # no parameters in function dec # so no arguments in function call ``` **Another example:** Say we want to write a function that prints the sum of two random numbers. We could use this basic function: ```python= # function for printing sum of two random numbers import random # to use randint function # function definition def random_sum(): # no parameters needed rand1 = random.randint(1,100) rand2 = random.randint(1,200) print(rand1 + rand2) # function call -- whenever we want to execute the function # and generate a random sum random_sum() ``` ### Functions with parameters We can build on the basic function by adding the use of parameters to it. Parameters are variables that are *bound* to the function. Their values depend on the values of the *argument(s)* used in the function call. I like to think of functions as their own little islands of code. They do not really have access to variables outside of the function declaration. Thus, if we want a function to be able to use data from outside the function (in another part of the program), then we must use parameters and arguments. * **arguments**: are the "outside" data we provide to the function during the function call * **parameters**: are variables linked to the values provided by the arguments The order of parameters determines the order of the arguments. The first argument is "paired with" or "saved to" the first parameter, the second argument with the second parameter, etc.. ```python= # function definition/declaration def add_num(parameter1, parameter2): # print a formatted string print(f"{parameter1} + {parameter2} = {parameter1 + parameter2}") # function call -- 2 parameters = 2 arguments some_function(1, 2) ``` When we call `some_function` in this way, we must consider how the function call's arguments match/align with the function calls's parameters. 1. the number of parameters = the number of arguments * if a function uses 2 parameters, the function call will likely require 2 arguments * This is a little different in the case of default arguments, but we do not work with those in this class. If you would like to read about them, look at [this website](https://www.geeksforgeeks.org/default-arguments-in-python/). 2. The order we write the arguments determines the result/output of the function. * the first argument in the function call, 1, will be bound to (saved under, linked to, however you want to say it) the first parameter: `parameter1`. The second argument in the function call, 2, will be bound to the second parameter, `parameter2` * the order of arguments is **very important**. If we do not use the correct order when doing our function calls, it could lead to unexpected results or even **errors**. #### Using the wrong arguments: We have to be mindful about the arguments we use in our function calls. If we write the arguments in the wrong order, or use the wrong data type for an argument, it can have negative effects (unexpected results, errors, etc.). For example, take the `add_num` example above. If we were to, say, use strings as the parameter, it would have a very different outcome (aka **string concatenation**): ```python= # function definition/declaration def add_num(parameter1, parameter2): # print a formatted string print(f"{parameter1} + {parameter2} = {parameter1 + parameter2}") # function call add_num("hello", "world") ``` Similarly, if we were to use a string and an int in our argument, we get an *error* (can you think of why?). ```python= # function definition/declaration def add_num(parameter1, parameter2): # print a formatted string print(f"{parameter1} + {parameter2} = {parameter1 + parameter2}") # function call add_num(1, "world") ``` ### Functions with return statements The return statement can be used to exit a function while bringing a piece of data with it. In other words, functions with return statements will provide some sort of information or data that was obtained during the function's execution. We often want to use the return statement when we want to *use* the information computed by the function in another part of our program. #### Examples of built-in functions that return values * `int(x)` - returns x as an integer * `len(x)` - returns the length of x (x being a sequence type) * `random.randint(x,y)` - returns a random number between x and y * Note: `randint()` is a function from the `random` module, and technically is called a method. We won't dwell on this difference. #### How do we return information? Since all functions have a specific task/operation/purpose, the expression/information being returned should relate to the function's task. As a result, we should expect each function to always return the same type of thing. For example, say we have a function that computes some mathematical expression and we want to use the result of that expression in the body of your program. We would expect the function the *return* the result to use wherever we made the function call. ```python= # function with return statement def calculate_circumference(r): circumference = 2 * 3.14 * r return circumference # call the function calculate_circumference(2) # nothing happens circumference = calculate_circumference(2) print(circumference) # it worked! # we can also use the function call # directly where the returned value is needed # assuming we don't want to save # the result to use again print(calculate_circumference(2)) ``` **Beware:** Similarly to parameters, we have to be mindful of what data types our functions return. If a function returns a string, we have to treat the returned value as a string; if a function returns an integer, we have to treat the returned value as an integer, etc. We can run into errors if we use the returned value incorrectly. For example, say we have a function that returns a string and, for some reason, we try to do arithmetic with the function's returned value. Example of using returned values incorrectly: ```python= # function that asks for user's name and # returns inputted value def get_name(): user_name = input("Enter your name: ") return user_name name = get_name() print(2 + name) ``` ### Example Functions **Example 1:** write a function that finds the sum of x random numbers. The amount of random numbers we generate and add together will be determined by the parameter, x. ```python= import random def get_random_sum(x): sum = 0 for x in range(x): sum += random.randint(1,100) return sum # function call -- saving the returned # val to variable rand_sum = get_rand_sum(5) print(rand_sum) ``` Reminder: x represents the number of ints we want to add together thus, it is the number of times we want to iterate **Example 2:** write a function that determines if a number is odd. The number we are evaluating for odd vs even will be our parameter. This example demonstrates how we can put return statements anywhere in the function, as long as it's appropriate. You can think of the return statement as the function version of the break statement (with some extra pizazz). ```python= def is_odd(num): if num % 2 == 1: return True return False """ could also say: if num % 2 == 1: return True else: return False same thing """ print(is_odd(5)) print(is_odd(2)) ``` Here is an example of how we could apply this function to an example we've done in class![^1] [^1]: ```python= # function definition def is_odd(num): if num % 2 == 1: return True return False # iterate through a list of ints # and count how many are odd: # here is our list numbers = [1,5,10,2,6,9,13,44] odd_count = 0 # iterate through list and determine # odd or even for num in numbers: if is_odd(num): count += 1 print(f"the list had {odd_count} odd numbers") ``` **Example 3:** given a list, find and return the largest number in the list. Let's assume the list contains only positive numbers. Our parameter is the list we are iterating through. ```python= # function definition def get_largest_number(num_list): max = 0 # iterate through list for x in num_list: if x > max: max = x return max # define the list number_list = [1,6,13,25,9,2] # function calls print(get_largest_num(number_list)) ``` Question: Why must we assume that the list contains only positive numbers?[^2] Question: What part of our function is written based on that assumption?[^3] [^2]: We must assume that the list contains only positive number because we need to initialize a value to compare to each element in the list. We need to choose this value before we iterate through the list because it must exist outside the list (similar to how sum and count variables must be created before a loop). Since we are deciding our starting number to compare to, we must be sure that there is at least one number in the list that is equal to or greater than our initial max value. If there isn't, then no number in the list is greater than or equal to our initial max value, and we will return a value that does not even exist in the list. If negative numbers are allowed in the list, then the largest number in the list could always be smaller than our chosen initial max value (e.g. even if we create max as `max = -100`, the largest number in the list could still be `-101`, etc.). Thus, we must decide the smallest value the list can contain (does not have to be restricted to only positive numbers) so that our initial max value can be smaller than that. When we learn about lists, we will learn a way to bypass this limitation. [^3]: Line 3 is based on the assumption that the list contains only positive numbers. This ensures that the value returned is in the list and is (probably) the largest number in the list. (A more advanced thought): If our list returns the value `0`, we know that `0` should not be in the list and thus can assume the list is *empty*. **Example 4:** Write a function that counts the number of vowels in a given string. Our parameter will be the string whose vowels we are counting. ```python= # function declaration def count_vowels(s): # s for string count_v = 0 for letter in s: if s in "aeiouAEIOU": count_v += 1 return count_v # now we can count the vowels of any string! print(count_vowels("Chapman University")) print(count_vowels("I love python!")) ``` --- ## Practice Problems For functions practice, please refer to the file posted to canvas under `Week 7`. An answer key will be posted separately.