PySnack ¶
Functions can be a challenging concept in programming, so let's get acquainted using an conceptual example: let's open up a cookie shop. Specifically, chocolate chip cookies 🍪.
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.