Functions ¶
Functions are reusable pieces of programs. They allow you to give a label to a group of Python code, allowing you to run that block by using its name anywhere. This can be a challenging concept in programming, so let's get acquainted using an example: let's open up a cookie shop. Specifically, chocolate chip cookies 🍪.
PySnack ¶
But first, we need a recipe . The ingredients are:
- 1 cup salted butter softened
- 2 cups granulated sugar
- 2 teaspoons pure vanilla extract
- 2 large eggs
- 3 cups all-purpose flour
- 1 teaspoon baking soda
- 1/2 teaspoon baking powder
- 1 teaspoon sea salt
- 2 cups chocolate chips (14 oz)
with the following steps:
- Preheat oven to 375 degrees F. Line three baking sheets with parchment paper and set aside.
- In a medium bowl mix flour, baking soda, baking powder and salt. Set aside.
- Cream together butter and sugars until combined.
- Beat in eggs and vanilla until light (about 1 minute).
- Mix in the dry ingredients until combined.
- Add chocolate chips and mix well.
- Roll 2-3 Tablespoons (depending on how large you like your cookies) of dough at a time into balls and place them evenly spaced on your prepared cookie sheets.
- Bake in preheated oven for approximately 8-10 minutes. Take them out when they are just barely starting to turn brown.
- Let them sit on the baking pan for 2 minutes before removing to cooling rack.
We are not going to be your typical cookie shop. We are going to use robots 🦾 and we bought this building to put them in.
Definition ¶
Now, we have to create a room for the robot to make our cookies and then give it instructions.
For example, we can give the robot this exact set of instructions to perform whenever we give it the ingredients.
We can call this set of instructions
make_cookies
.
Now we are ready to give the robot instructions; we do this with the
def
keyword in Python.
This keyword tells Python we are ready to start giving it a set of instructions and that we are going to call it
make_cookies
.
def make_cookies():
Do not worry about the
()
for now, but do know that we use the
:
at the end to tell Python we are ready to start our instructions.
def make_cookies():
turn_on_oven()
prepare_cookie_sheet()
mix_dry_ingredients()
whip_wet_ingredients()
mix_everything()
add_chocolate()
place_dough_balls()
bake_cookies()
cool_cookies()
Scope ¶
You may be thinking, "Why is every line indented four spaces?"
Well, in Python, you need to indent lines that are within the function to communicate when it ends.
For example, if I defined
make_cookies
and then separately welcomed a customer I need to unindent the
print("Welcome!")
line like so.
def make_cookies():
turn_on_oven()
prepare_cookie_sheet()
mix_dry_ingredients()
whip_wet_ingredients()
mix_everything()
add_chocolate()
place_dough_balls()
bake_cookies()
cool_cookies()
print("Welcome!")
This means that
print("Welcome!")
is outside of the functions
scope
(which we will talk more about later) because I want the greet the customer, not the robot.
If I had
print("Welcome")
indented, then this would be part of the robot's instructions and it would be talking in an empty room.
That's just too weird.
Parameters ¶
Great!
We have a set of instructions that a robot can use to make cookies; however, we have not given the robot any ingredients.
We can not just give the robot ingredients, we have to tell it beforehand what to expect—Python is the same.
To tell Python what data it will need to use inside the function, we use placeholders called
parameters
that are placed inside the
()
.
Note that we also specify which ingredients are used where.
This is not important for understanding, I'm just trying to be somewhat realistic.
def make_cookies(butter, sugar, vanilla_extract, eggs, flour, baking_soda, baking_powder, salt, chocolate_chips):
turn_on_oven()
cookie_sheet = prepare_cookie_sheet()
dry_mixed = mix_dry_ingredients(flour, baking_soda, baking_powder, salt)
wet_mixed = whip_wet_ingredients(butter, sugar, eggs, vanilla_extract)
mix = mix_everything(dry_mixed, wet_mixed)
mix = add_chocolate(mix, chocolate_chips)
cookie_sheet = place_dough_balls(cookie_sheet, mix)
cookies = bake_cookies(cookie_sheet)
cookies = cool_cookies(cookies)
Holy cow! That is a long list of ingredients that make it hard to see everything without scrolling. Python gives us a different way to type the parameters like so.
def make_cookies(
butter, sugar, vanilla_extract, eggs,
flour, baking_soda, baking_powder,
salt, chocolate_chips
):
turn_on_oven()
cookie_sheet = prepare_cookie_sheet()
dry_mixed = mix_dry_ingredients(flour, baking_soda, baking_powder, salt)
wet_mixed = whip_wet_ingredients(butter, sugar, eggs, vanilla_extract)
mix = mix_everything(dry_mixed, wet_mixed)
mix = add_chocolate(mix, chocolate_chips)
cookie_sheet = place_dough_balls(cookie_sheet, mix)
cookies = bake_cookies(cookie_sheet)
cookies = cool_cookies(cookies)
I know the
):
on the left looks weird, but this is because we have not told Python we are done defining our function with
:
yet.
But hold up, we have the parameters indented, is that allowed?
Yes, it is.
When you are within a
( )
, Python treats that all within the original scope; meaning you can choose to indent or not.
The following Python code is valid as well, but it looks weird to Python programmers.
def make_cookies(
butter, sugar, vanilla_extract, eggs,
flour, baking_soda, baking_powder,
salt, chocolate_chips
):
turn_on_oven()
cookie_sheet = prepare_cookie_sheet()
dry_mixed = mix_dry_ingredients(flour, baking_soda, baking_powder, salt)
wet_mixed = whip_wet_ingredients(butter, sugar, eggs, vanilla_extract)
mix = mix_everything(dry_mixed, wet_mixed)
mix = add_chocolate(mix, chocolate_chips)
cookie_sheet = place_dough_balls(cookie_sheet, mix)
cookies = bake_cookies(cookie_sheet)
cookies = cool_cookies(cookies)
At this point, it is like we put little transfer boxes in the wall of our bakery with numbers to know where to put which ingredient.
Calling ¶
Let's try it out.
To perform a function, we call that
calling
or running the function.
We repeat the function definition without the
def
or
:
.
make_cookies(
butter_1_cup, sugar_2_cups, vanilla_extract_2_tsp, eggs_2,
flour_3_cups, baking_soda_1_tsp, baking_powder_half_tsp,
salt_1_tsp, chocolate_chips_2_cups
)
Hold up, why do I have different variable names here?
In my function, I said the parameters are
butter
,
sugar
, etc. but I have
butter_1_cup
,
sugar_2_cups
, and so on.
Let's take a look at what the robot sees.
That's interesting, the robot does not see
butter_1_cup
, but
butter
, just like in the function definition.
This is actually by design, as the function parameters are just placeholders for me to put my data and then define a label I can use inside the function.
So it does not matter what the variable name is when I call the function; I could use
Pitt
as my variable name as long as it contained 1 cup of butter.
What is important is the order I put the variables in.
It has to go in the same order as I defined them in my function.
If I accidentally call the function and put first
sugar_2_cups
and then
butter_1_cup
the robot cannot tell the difference.
All it knows is that whatever comes in the upper right transfer box is called
butter
and it will try to use it like
butter
.
Again, computers are dumb.
Returns ¶
Okay, so let's go back to calling the
make_cookies
function.
IT STILL HAS MY COOKIES! The robot just took the ingredients, made the cookies, and now is taunting me. I need those cookies!
Oh, we forgot to tell the robot to give us the cookies back.
In Python, we call this a
return
statement.
This tells Python that, once it finishes its instructions inside the function (or in the room this case) it needs to give us something back.
def make_cookies(
butter, sugar, vanilla_extract, eggs,
flour, baking_soda, baking_powder,
salt, chocolate_chips
):
turn_on_oven()
cookie_sheet = prepare_cookie_sheet()
dry_mixed = mix_dry_ingredients(flour, baking_soda, baking_powder, salt)
wet_mixed = whip_wet_ingredients(butter, sugar, eggs, vanilla_extract)
mix = mix_everything(dry_mixed, wet_mixed)
mix = add_chocolate(mix, chocolate_chips)
cookie_sheet = place_dough_balls(cookie_sheet, mix)
cookies = bake_cookies(cookie_sheet)
cookies = cool_cookies(cookies)
return cookies
Modular design ¶
When learning a new skill, it is useful to learn good habits at the beginning. One of those habits for programming is something called modular design. To explain this, we will continue our PySnack example by asking: What if we wanted to make brownies ?
Its ingredients are:
- 1 1/2 cups granulated sugar
- 3/4 cup all-purpose flour
- 2/3 cup cocoa powder
- 1/2 cup powdered sugar
- 1/2 cup chocolate chips
- 3/4 teaspoons sea salt
- 2 large eggs
- 1/2 cup canola oil
- 2 tablespoons water
- 1/2 teaspoon vanilla
with the following steps.
- Preheat the oven to 325°F. Lightly spray an 8x8 baking dish with cooking spray and line it with parchment paper. Spray the parchment paper.
- In a medium bowl, combine the sugar, flour, cocoa powder, powdered sugar, chocolate chips, and salt.
- In a large bowl, whisk together the eggs, olive oil, water, and vanilla.
- Sprinkle the dry mix over the wet mix and stir until just combined.
- Pour the batter into the prepared pan and use a spatula to smooth the top.
- Bake for 40 to 48 minutes, or until a toothpick comes out with only a few crumbs attached.
- Cool completely before slicing.
We see that there is a decent amount of overlap between the two recipes!
However, our
make_cookies
robot only knows how to make cookies, which is fine if we do not want to expand our business.
I could make another function called
make_brownies
and put the instructions there, but this would quickly become a lot of work if I wanted to add other baked goods on the menu like muffins and scones.
Ideally, we would have a
modular
framework that can adapt to most of the slight variations between recipes.
Looking at the instructions, we have some common actions:
-
oven_operation
: Turning on the oven, taking things in and out. -
mixing
: Take some number of ingredients, put them in a bowl (that may already have stuff in it), and mix it.
Then we have some specialized actions like making dough balls for cookies and pouring brownie batter.
Now, I can call
mix
in any recipe that needs things mixed.
Default parameters ¶
Let's take a closer look at our
mix
function and think about how we would define it.
- We need to be able to use a changing number of ingredients. Sometimes we need to mix two things or four things.
- Things can be added to a clean bowl or a bowl with other ingredients.
- Sometimes a whisk is needed, other times a spatula would work better.
mix
would need to be able to handle these situations different situations and adapt to the task.
Let's now define our function parameters.
First, instead of having a single parameter for each ingredient, we can use a single variable to just hold all the necessary ingredients.
This is called a
collection
which we are covering later.
The only thing you need to know is that we are telling Python to store multiple values (i.e.,
flour
,
sugar
) under one variable label.
Let's call this an ingredient.
def mix(ingredients):
Next, let's figure out this bowl situation.
One approach would be to just add a parameter called
bowl
that when we put it in the transfer box, may or may not have stuff inside already.
def mix(ingredients, bowl):
This is perfectly fine; however, most of the time we want a clean, empty bowl. It can get tedious to keep putting an empty bowl there every time we want ingredients mixed. For example, each function call would look like this.
mix(ingredients, "clean")
mix(ingredients, "filled")
mix(ingredients, "clean")
Oh, we have not seen that before!
I'm not passing a variable into the function, I'm passing the actual data.
That is perfectly fine!
Remember, when I call a function I am putting my data that gets its own label within the function.
In the last function call, the function would have
bowl
that is equal to
"clean"
inside of its scope.
Okay, now, what if we can tell the robot to—unless I explicitly put something in
bowl
—to grab a clean one from inside its room?
We can do that with
default parameters
.
def mix(ingredients, bowl=None):
What this does is tell Python that if I do not give you something to put into the
bowl
parameter, then set it equal to
None
.
I can also replace this default
None
value with my own during the function call if I would like.
mix(ingredients, bowl="filled")
Default parameters offer flexibility by allowing functions to be called with or without certain arguments, providing reasonable default values when necessary.
Caution
Only those parameters which are at the end of the parameters can be given default argument values. You cannot have a parameter with a default argument value preceding a parameter without a default argument value in the function's parameter list.
This is because the values are assigned to the parameters by position.
For example,
def mix(ingredients, bowl=None)
is valid, but
def mix(bowl=None, ingredients)
is not valid
We can take the same approach by specifying the default tool to use for mixing.
def mix(ingredients, bowl="filled", tool="whisk"):
When
tool
is equal to
"whisk"
, then the robot will use a whisk.
Benefits of keyword arguments:
- Order Flexibility: Because we're passing arguments by name rather than position, we can specify them in any order we want in the function call.
- Clarity: Giving arguments by name makes our code more readable, especially with functions that have many parameters. We can see at the call site exactly which values we're passing to the function.
- Default Values: By defining default values for parameters, we can omit those arguments when calling the function and the defaults will kick in. This allows for customizable function calls.
Local variables ¶
When you declare variables inside a function definition, they are not related in any way to other variables with the same names used outside the function—variable names are local to the function. This is called the scope of the variable. All variables have the scope of the block they are declared in starting from the point of definition of the name.
x = 50
def func(x):
print("x is", x)
x = 2
print("Changed local x to", x)
func(x)
print("x is still", x)
x is 50 Changed local x to 2 x is still 50
The parameter
x
in the function definition acts as a local variable within the scope of the function.
It is a variable that is only accessible and meaningful within the function.
In the function body, when the code assigns a new value (
x = 2
), it creates a new local variable
x
that only exists within the function.
This local variable shadows the parameter
x
.
Outside the function, there could be a variable with the same name
x
, but it is a different variable and has a different scope (it is not the same as the local
x
inside the function).
If there is a global variable named
x
in the broader scope of the program, it is not affected by the local variable
x
within the function.
Scope chain ¶
The resolution of variable references follows a scope chain. This is a sequential search through different levels of scope to find the value associated with a variable.
- When a variable is referenced, Python first looks in the local scope, which is the innermost context, such as within a function. If the variable is found locally, Python uses the value from the local scope.
- If not found, Python extends the search to enclosing scopes, including nested functions, checking each level hierarchically.
- If the variable is still not found, Python looks in the global scope, representing the entire script or module.
The scope chain ensures a systematic search for variable values, preventing unintended conflicts between local and global variables and allowing for proper variable resolution based on the hierarchical structure of the code.
Lifetime ¶
The local variable (
x
inside the function) only exists for the duration of the function call. These variables are created when the function is called and cease to exist once the function's execution is completed.
This temporary existence helps prevent variable name conflicts and ensures that each function call has its own isolated space for variables.
Now, let's delve into the breakdown of what happens in memory during the different stages of the function call.
Before the function call
At this point, a global variable
x
already exists with its own value.
This variable is separate from any local variable that may be created within a function.
During the function call
When the function is called, a parameter
x
is created as a local variable within the function's scope.
It is initialized with the value of the argument passed during the function call.
Subsequently, within the function, the local variable
x
is reassigned a new value.
This reassignment only affects the local variable and does not impact the global variable with the same name.
After the function call
Once the function call is complete, the local variable
x
that existed within the function is destroyed. The memory allocated to this local variable is released.
Importantly, the destruction of the local variable does not affect the global variable
x
, which maintains its original value and remains unaffected by the changes made within the function.
Global variables in functions ¶
If you want to assign a value to a name defined at the top level of the program (i.e. not inside any kind of scope such as functions or classes), then you have to tell Python that the name is not local, but it is global.
We do this using the
global
statement.
It is impossible to assign a value to a variable defined outside a function without the
global
statement.
You can use the values of such variables defined outside the function (assuming there is no variable with the same name within the function). However, this is not encouraged and should be avoided since it becomes unclear to the reader of the program as to where that variable's definition is.
Using the
global
statement makes it amply clear that the variable is defined in an outermost block.
x = 50
def func_global():
global x
print("x is", x)
x = 2
print("Changed global x to", x)
func_global()
print("Value of x is", x)
x is 50 Changed global x to 2 Value of x is 2
The function
func_global
has its own local scope.
When the function references
x
without the global statement, it would normally create a new local variable named
x
within the function, separate from the global variable.
By using
global x
inside the function, Python is explicitly instructed to look for the variable
x
in the global scope.
This means that any reference to
x
within the function refers to the global variable
x
defined outside the function.
The assignment
x = 2
inside the function modifies the global variable
x
rather than creating a new local variable.
This is because of the global statement, which directs Python to look for
x
in the global scope.
The
say
function has a default parameter
times
set to
1
.
When the function is called with only one argument (
'Hello'
), it uses the default value for times, printing the message once.
When called with two arguments (
'World'
and
5
), the explicitly provided value for times overrides the default, and the message is printed multiple times accordingly.
Default parameters offer flexibility by allowing functions to be called with or without certain arguments, providing reasonable default values when necessary.
Caution
Only those parameters which are at the end of the parameter list can be given default argument values. You cannot have a parameter with a default argument value preceding a parameter without a default argument value in the function's parameter list.
This is because the values are assigned to the parameters by position.
For example,
def func(a, b=5)
is valid, but
def func(a=5, b)
is not valid