Chapter 8: Functions
|
Source: Python Crash Course, 3rd Edition by Eric Matthes |
In this chapter you’ll learn to write functions, which are named blocks of code designed to do one specific job. When you want to perform a particular task that you’ve defined in a function, you call the function responsible for it. If you need to perform that task multiple times throughout your program, you don’t need to type all the code for the same task again and again; you just call the function dedicated to handling that task, and the call tells Python to run the code inside the function. You’ll find that using functions makes your programs easier to write, read, test, and fix.
In this chapter you’ll also learn a variety of ways to pass information to functions. You’ll learn how to write certain functions whose primary job is to display information and other functions designed to process data and return a value or set of values. Finally, you’ll learn to store functions in separate files called modules to help organize your main program files.
Defining a Function
Here’s a simple function named greet_user() that prints a greeting:
def greet_user():
"""Display a simple greeting."""
print("Hello!")
greet_user()
This example shows the simplest structure of a function. The first line
uses the keyword def to inform Python that you’re defining a function.
This is the function definition, which tells Python the name of the
function and, if applicable, what kind of information the function needs
to do its job. The parentheses hold that information. In this case, the
name of the function is greet_user(), and it needs no information to
do its job, so its parentheses are empty. (Even so, the parentheses are
required.) Finally, the definition ends in a colon.
Any indented lines that follow def greet_user(): make up the body of
the function. The text on the second line is a comment called a
docstring, which describes what the function does. When Python
generates documentation for the functions in your programs, it looks for
a string immediately after the function’s definition. These strings are
usually enclosed in triple quotes, which lets you write multiple lines.
The line print("Hello!") is the only line of actual code in the body
of this function, so greet_user() has just one job: print("Hello!").
When you want to use this function, you have to call it. A function
call tells Python to execute the code in the function. To call a
function, you write the name of the function, followed by any necessary
information in parentheses. Because no information is needed here,
calling our function is as simple as entering greet_user(). As
expected, it prints Hello!:
Hello!
Passing Information to a Function
If you modify the function greet_user() slightly, it can greet the
user by name. For the function to do this, you enter username in the
parentheses of the function’s definition at def greet_user(). By
adding username here, you allow the function to accept any value of
username you specify. The function now expects you to provide a value
for username each time you call it. When you call greet_user(), you
can pass it a name, such as 'jesse', inside the parentheses:
def greet_user(username):
"""Display a simple greeting."""
print(f"Hello, {username.title()}!")
greet_user('jesse')
Entering greet_user('jesse') calls greet_user() and gives the
function the information it needs to execute the print() call. The
function accepts the name you passed it and displays the greeting for
that name:
Hello, Jesse!
Likewise, entering greet_user('sarah') calls greet_user(), passes
it 'sarah', and prints Hello, Sarah! You can call greet_user() as
often as you want and pass it any name you want to produce a predictable
output every time.
Arguments and Parameters
In the preceding greet_user() function, we defined greet_user() to
require a value for the variable username. Once we called the function
and gave it the information (a person’s name), it printed the right
greeting.
The variable username in the definition of greet_user() is an
example of a parameter, a piece of information the function needs to
do its job. The value 'jesse' in greet_user('jesse') is an example
of an argument. An argument is a piece of information that’s passed
from a function call to a function. When we call the function, we place
the value we want the function to work with in parentheses. In this case
the argument 'jesse' was passed to the function greet_user(), and
the value was assigned to the parameter username.
|
People sometimes speak of arguments and parameters interchangeably. Don’t be surprised if you see the variables in a function definition referred to as arguments or the variables in a function call referred to as parameters. |
Try It Yourself
8-1. Message: Write a function called display_message() that prints
one sentence telling everyone what you are learning about in this
chapter. Call the function, and make sure the message displays
correctly.
8-2. Favorite Book: Write a function called favorite_book() that
accepts one parameter, title. The function should print a message,
such as One of my favorite books is Alice in Wonderland. Call the
function, making sure to include a book title as an argument in the
function call.
Passing Arguments
Because a function definition can have multiple parameters, a function call may need multiple arguments. You can pass arguments to your functions in a number of ways. You can use positional arguments, which need to be in the same order the parameters were written; keyword arguments, where each argument consists of a variable name and a value; and lists and dictionaries of values. Let’s look at each of these in turn.
Positional Arguments
When you call a function, Python must match each argument in the function call with a parameter in the function definition. The simplest way to do this is based on the order of the arguments provided. Values matched up this way are called positional arguments.
To see how this works, consider a function that displays information about pets. The function tells us what kind of animal each pet is and the pet’s name, as shown here:
def describe_pet(animal_type, pet_name): (1)
"""Display information about a pet."""
print(f"I have a {animal_type}.")
print(f"My {animal_type}'s name is {pet_name.title()}.")
describe_pet('hamster', 'harry') (2)
| 1 | The definition shows that this function needs a type of animal and the animal’s name. |
| 2 | In the function call, the argument 'hamster' is assigned to the
parameter animal_type and the argument 'harry' is assigned to
the parameter pet_name. |
The output describes a hamster named Harry:
I have a hamster.
My hamster's name is Harry.
Multiple Function Calls
You can call a function as many times as needed. Describing a second,
different pet requires just one more call to describe_pet():
def describe_pet(animal_type, pet_name):
"""Display information about a pet."""
print(f"I have a {animal_type}.")
print(f"My {animal_type}'s name is {pet_name.title()}.")
describe_pet('hamster', 'harry')
describe_pet('dog', 'willie')
In this second function call, we pass describe_pet() the arguments
'dog' and 'willie'. As with the previous set of arguments, Python
matches 'dog' with the parameter animal_type and 'willie' with the
parameter pet_name. Now we have a hamster named Harry and a dog named
Willie:
I have a hamster.
My hamster's name is Harry.
I have a dog.
My dog's name is Willie.
Calling a function multiple times is a very efficient way to work. The code describing a pet is written once in the function. Then, anytime you want to describe a new pet, you call the function with the new pet’s information. Even if the code for describing a pet were to expand to 10 lines, you could still describe a new pet in just one line by calling the function again.
Order Matters in Positional Arguments
You can get unexpected results if you mix up the order of the arguments in a function call when using positional arguments:
def describe_pet(animal_type, pet_name):
"""Display information about a pet."""
print(f"I have a {animal_type}.")
print(f"My {animal_type}'s name is {pet_name.title()}.")
describe_pet('harry', 'hamster')
In this function call, we list the name first and the type of animal
second. Because the argument 'harry' is listed first this time, that
value is assigned to the parameter animal_type. Likewise, 'hamster'
is assigned to pet_name. Now we have a "harry" named "Hamster":
I have a harry.
My harry's name is Hamster.
If you get funny results like this, check to make sure the order of the arguments in your function call matches the order of the parameters in the function’s definition.
Keyword Arguments
A keyword argument is a name-value pair that you pass to a function. You directly associate the name and the value within the argument, so when you pass the argument to the function, there’s no confusion. Keyword arguments free you from having to worry about correctly ordering your arguments in the function call, and they clarify the role of each value in the function call.
Let’s rewrite pets.py using keyword arguments to call
describe_pet():
def describe_pet(animal_type, pet_name):
"""Display information about a pet."""
print(f"I have a {animal_type}.")
print(f"My {animal_type}'s name is {pet_name.title()}.")
describe_pet(animal_type='hamster', pet_name='harry')
The function describe_pet() hasn’t changed. But when we call the
function, we explicitly tell Python which parameter each argument should
be matched with. When Python reads the function call, it knows to assign
the argument 'hamster' to the parameter animal_type and the argument
'harry' to pet_name. The output correctly shows that we have a
hamster named Harry.
The order of keyword arguments doesn’t matter because Python knows where each value should go. The following two function calls are equivalent:
describe_pet(animal_type='hamster', pet_name='harry')
describe_pet(pet_name='harry', animal_type='hamster')
|
When you use keyword arguments, be sure to use the exact names of the parameters in the function’s definition. |
Default Values
When writing a function, you can define a default value for each parameter. If an argument for a parameter is provided in the function call, Python uses the argument value. If not, it uses the parameter’s default value. So when you define a default value for a parameter, you can exclude the corresponding argument you’d usually write in the function call. Using default values can simplify your function calls and clarify the ways your functions are typically used.
For example, if you notice that most of the calls to describe_pet()
are being used to describe dogs, you can set the default value of
animal_type to 'dog'. Now anyone calling describe_pet() for a dog
can omit that information:
def describe_pet(pet_name, animal_type='dog'):
"""Display information about a pet."""
print(f"I have a {animal_type}.")
print(f"My {animal_type}'s name is {pet_name.title()}.")
describe_pet(pet_name='willie')
We changed the definition of describe_pet() to include a default
value, 'dog', for animal_type. Now when the function is called with
no animal_type specified, Python knows to use the value 'dog' for
this parameter:
I have a dog.
My dog's name is Willie.
Note that the order of the parameters in the function definition had to
be changed. Because the default value makes it unnecessary to specify a
type of animal as an argument, the only argument left in the function
call is the pet’s name. Python still interprets this as a positional
argument, so if the function is called with just a pet’s name, that
argument will match up with the first parameter listed in the function’s
definition. This is the reason the first parameter needs to be
pet_name.
The simplest way to use this function now is to provide just a dog’s name in the function call:
describe_pet('willie')
This function call would have the same output as the previous example.
The only argument provided is 'willie', so it is matched up with the
first parameter in the definition, pet_name. Because no argument is
provided for animal_type, Python uses the default value 'dog'.
To describe an animal other than a dog, you could use a function call like this:
describe_pet(pet_name='harry', animal_type='hamster')
Because an explicit argument for animal_type is provided, Python will
ignore the parameter’s default value.
|
When you use default values, any parameter with a default value needs to be listed after all the parameters that don’t have default values. This allows Python to continue interpreting positional arguments correctly. |
Equivalent Function Calls
Because positional arguments, keyword arguments, and default values can
all be used together, you’ll often have several equivalent ways to call
a function. Consider the following definition for describe_pet() with
one default value provided:
def describe_pet(pet_name, animal_type='dog'):
With this definition, an argument always needs to be provided for
pet_name, and this value can be provided using the positional or
keyword format. If the animal being described is not a dog, an argument
for animal_type must be included in the call, and this argument can
also be specified using the positional or keyword format.
All of the following calls would work for this function:
# A dog named Willie.
describe_pet('willie')
describe_pet(pet_name='willie')
# A hamster named Harry.
describe_pet('harry', 'hamster')
describe_pet(pet_name='harry', animal_type='hamster')
describe_pet(animal_type='hamster', pet_name='harry')
Each of these function calls would have the same output as the previous examples. It doesn’t really matter which calling style you use. As long as your function calls produce the output you want, just use the style you find easiest to understand.
Avoiding Argument Errors
When you start to use functions, don’t be surprised if you encounter
errors about unmatched arguments. Unmatched arguments occur when you
provide fewer or more arguments than a function needs to do its work.
For example, here’s what happens if we try to call describe_pet() with
no arguments:
def describe_pet(animal_type, pet_name):
"""Display information about a pet."""
print(f"I have a {animal_type}.")
print(f"My {animal_type}'s name is {pet_name.title()}.")
describe_pet()
Python recognizes that some information is missing from the function call, and the traceback tells us that:
Traceback (most recent call last):
File "pets.py", line 6, in <module> (1)
describe_pet() (2)
^^^^^^^^^^^^^^
TypeError: describe_pet() missing 2 required positional arguments: 'animal_type' and 'pet_name' (3)
| 1 | The traceback first tells us the location of the problem, allowing us to look back and see that something went wrong in our function call. |
| 2 | The offending function call is written out for us to see. |
| 3 | The traceback tells us the call is missing two arguments and reports the names of the missing arguments. |
Python is helpful in that it reads the function’s code for us and tells us the names of the arguments we need to provide. This is another motivation for giving your variables and functions descriptive names. If you do, Python’s error messages will be more useful to you and anyone else who might use your code.
If you provide too many arguments, you should get a similar traceback that can help you correctly match your function call to the function definition.
Try It Yourself
8-3. T-Shirt: Write a function called make_shirt() that accepts a
size and the text of a message that should be printed on the shirt. The
function should print a sentence summarizing the size of the shirt and
the message printed on it. Call the function once using positional
arguments to make a shirt. Call the function a second time using keyword
arguments.
8-4. Large Shirts: Modify the make_shirt() function so that shirts
are large by default with a message that reads I love Python. Make a
large shirt and a medium shirt with the default message, and a shirt of
any size with a different message.
8-5. Cities: Write a function called describe_city() that accepts
the name of a city and its country. The function should print a simple
sentence, such as Reykjavik is in Iceland. Give the parameter for the
country a default value. Call your function for three different cities,
at least one of which is not in the default country.
Return Values
A function doesn’t always have to display its output directly. Instead,
it can process some data and then return a value or set of values. The
value the function returns is called a return value. The return
statement takes a value from inside a function and sends it back to the
line that called the function. Return values allow you to move much of
your program’s grunt work into functions, which can simplify the body of
your program.
Returning a Simple Value
Let’s look at a function that takes a first and last name, and returns a neatly formatted full name:
def get_formatted_name(first_name, last_name):
"""Return a full name, neatly formatted."""
full_name = f"{first_name} {last_name}" (1)
return full_name.title() (2)
musician = get_formatted_name('jimi', 'hendrix') (3)
print(musician)
| 1 | The function combines these two names, adds a space between them,
and assigns the result to full_name. |
| 2 | The value of full_name is converted to title case, and then
returned to the calling line. |
| 3 | When you call a function that returns a value, you need to provide a variable that the return value can be assigned to. |
Jimi Hendrix
This might seem like a lot of work to get a neatly formatted name when
we could have just written print("Jimi Hendrix"). However, when you
consider working with a large program that needs to store many first and
last names separately, functions like get_formatted_name() become very
useful. You store first and last names separately and then call this
function whenever you want to display a full name.
Making an Argument Optional
Sometimes it makes sense to make an argument optional, so that people using the function can choose to provide extra information only if they want to. You can use default values to make an argument optional.
For example, say we want to expand get_formatted_name() to handle
middle names as well. A first attempt to include middle names might look
like this:
def get_formatted_name(first_name, middle_name, last_name):
"""Return a full name, neatly formatted."""
full_name = f"{first_name} {middle_name} {last_name}"
return full_name.title()
musician = get_formatted_name('john', 'lee', 'hooker')
print(musician)
This function works when given a first, middle, and last name:
John Lee Hooker
But middle names aren’t always needed, and this function as written
would not work if you tried to call it with only a first name and a last
name. To make the middle name optional, we can give the middle_name
argument an empty default value and ignore the argument unless the user
provides a value. To make get_formatted_name() work without a middle
name, we set the default value of middle_name to an empty string and
move it to the end of the list of parameters:
def get_formatted_name(first_name, last_name, middle_name=''):
"""Return a full name, neatly formatted."""
if middle_name: (1)
full_name = f"{first_name} {middle_name} {last_name}"
else: (2)
full_name = f"{first_name} {last_name}"
return full_name.title()
musician = get_formatted_name('jimi', 'hendrix')
print(musician)
musician = get_formatted_name('john', 'hooker', 'lee') (3)
print(musician)
| 1 | Python interprets non-empty strings as True, so the conditional
test if middle_name evaluates to True if a middle name argument
is in the function call. |
| 2 | If no middle name is provided, the empty string fails the if test
and the else block runs. |
| 3 | If we’re using a middle name, we have to make sure the middle name is the last argument passed so Python will match up the positional arguments correctly. |
This modified version of our function works for people with just a first and last name, and it works for people who have a middle name as well:
Jimi Hendrix
John Lee Hooker
Optional values allow functions to handle a wide range of use cases while letting function calls remain as simple as possible.
Returning a Dictionary
A function can return any kind of value you need it to, including more complicated data structures like lists and dictionaries. For example, the following function takes in parts of a name and returns a dictionary representing a person:
def build_person(first_name, last_name):
"""Return a dictionary of information about a person."""
person = {'first': first_name, 'last': last_name} (1)
return person (2)
musician = build_person('jimi', 'hendrix')
print(musician) (3)
| 1 | The function puts the first and last name into a dictionary. |
| 2 | The entire dictionary representing the person is returned. |
| 3 | The return value is printed. |
{'first': 'jimi', 'last': 'hendrix'}
This function takes in simple textual information and puts it into a more meaningful data structure that lets you work with the information beyond just printing it. You can easily extend this function to accept optional values like a middle name, an age, an occupation, or any other information you want to store about a person. For example, the following change allows you to store a person’s age as well:
def build_person(first_name, last_name, age=None):
"""Return a dictionary of information about a person."""
person = {'first': first_name, 'last': last_name}
if age:
person['age'] = age
return person
musician = build_person('jimi', 'hendrix', age=27)
print(musician)
We add a new optional parameter age to the function definition and
assign the parameter the special value None, which is used when a
variable has no specific value assigned to it. You can think of None
as a placeholder value. In conditional tests, None evaluates to
False. If the function call includes a value for age, that value is
stored in the dictionary.
Using a Function with a while Loop
You can use functions with all the Python structures you’ve learned
about so far. For example, let’s use the get_formatted_name() function
with a while loop to greet users more formally. Here’s a first attempt
at greeting people using their first and last names:
def get_formatted_name(first_name, last_name):
"""Return a full name, neatly formatted."""
full_name = f"{first_name} {last_name}"
return full_name.title()
# This is an infinite loop!
while True:
print("\nPlease tell me your name:") (1)
f_name = input("First name: ")
l_name = input("Last name: ")
formatted_name = get_formatted_name(f_name, l_name)
print(f"\nHello, {formatted_name}!")
| 1 | The while loop asks the user to enter their name, prompting for
their first and last name separately. |
But there’s one problem with this while loop: we haven’t defined a
quit condition. Where do you put a quit condition when you ask for a
series of inputs? We want the user to be able to quit as easily as
possible, so each prompt should offer a way to quit. The break
statement offers a straightforward way to exit the loop at either
prompt:
def get_formatted_name(first_name, last_name):
"""Return a full name, neatly formatted."""
full_name = f"{first_name} {last_name}"
return full_name.title()
while True:
print("\nPlease tell me your name:")
print("(enter 'q' at any time to quit)")
f_name = input("First name: ")
if f_name == 'q':
break
l_name = input("Last name: ")
if l_name == 'q':
break
formatted_name = get_formatted_name(f_name, l_name)
print(f"\nHello, {formatted_name}!")
We add a message that informs the user how to quit, and then we break
out of the loop if the user enters the quit value at either prompt. Now
the program will continue greeting people until someone enters q for
either name:
Please tell me your name:
(enter 'q' at any time to quit)
First name: eric
Last name: matthes
Hello, Eric Matthes!
Please tell me your name:
(enter 'q' at any time to quit)
First name: q
Try It Yourself
8-6. City Names: Write a function called city_country() that takes
in the name of a city and its country. The function should return a
string formatted like this: "Santiago, Chile". Call your function with
at least three city-country pairs, and print the values that are
returned.
8-7. Album: Write a function called make_album() that builds a
dictionary describing a music album. The function should take in an
artist name and an album title, and it should return a dictionary
containing these two pieces of information. Use the function to make
three dictionaries representing different albums. Print each return
value to show that the dictionaries are storing the album information
correctly. Use None to add an optional parameter to make_album() that
allows you to store the number of songs on an album. If the calling line
includes a value for the number of songs, add that value to the album’s
dictionary. Make at least one new function call that includes the number
of songs on an album.
8-8. User Albums: Start with your program from Exercise 8-7. Write a
while loop that allows users to enter an album’s artist and title.
Once you have that information, call make_album() with the user’s
input and print the dictionary that’s created. Be sure to include a quit
value in the while loop.
Passing a List
You’ll often find it useful to pass a list to a function, whether it’s a list of names, numbers, or more complex objects, such as dictionaries. When you pass a list to a function, the function gets direct access to the contents of the list. Let’s use functions to make working with lists more efficient.
Say we have a list of users and want to print a greeting to each. The
following example sends a list of names to a function called
greet_users(), which greets each person in the list individually:
def greet_users(names):
"""Print a simple greeting to each user in the list."""
for name in names:
msg = f"Hello, {name.title()}!"
print(msg)
usernames = ['hannah', 'ty', 'margot']
greet_users(usernames)
We define greet_users() so it expects a list of names, which it
assigns to the parameter names. The function loops through the list it
receives and prints a greeting to each user. Outside of the function, we
define a list of users and then pass the list usernames to
greet_users() in the function call:
Hello, Hannah!
Hello, Ty!
Hello, Margot!
Modifying a List in a Function
When you pass a list to a function, the function can modify the list. Any changes made to the list inside the function’s body are permanent, allowing you to work efficiently even when you’re dealing with large amounts of data.
Consider a company that creates 3D printed models of designs that users submit. Designs that need to be printed are stored in a list, and after being printed they’re moved to a separate list. The following code does this without using functions:
# Start with some designs that need to be printed.
unprinted_designs = ['phone case', 'robot pendant', 'dodecahedron']
completed_models = []
# Simulate printing each design, until none are left.
# Move each design to completed_models after printing.
while unprinted_designs:
current_design = unprinted_designs.pop()
print(f"Printing model: {current_design}")
completed_models.append(current_design)
# Display all completed models.
print("\nThe following models have been printed:")
for completed_model in completed_models:
print(completed_model)
This program starts with a list of designs that need to be printed and
an empty list called completed_models that each design will be moved
to after it has been printed. As long as designs remain in
unprinted_designs, the while loop simulates printing each design by
removing a design from the end of the list, storing it in
current_design, and displaying a message that the current design is
being printed. It then adds the design to the list of completed models.
When the loop is finished running, a list of the designs that have been
printed is displayed:
Printing model: dodecahedron
Printing model: robot pendant
Printing model: phone case
The following models have been printed:
dodecahedron
robot pendant
phone case
We can reorganize this code by writing two functions, each of which does one specific job. Most of the code won’t change; we’re just structuring it more carefully:
def print_models(unprinted_designs, completed_models): (1)
"""
Simulate printing each design, until none are left.
Move each design to completed_models after printing.
"""
while unprinted_designs:
current_design = unprinted_designs.pop()
print(f"Printing model: {current_design}")
completed_models.append(current_design)
def show_completed_models(completed_models): (2)
"""Show all the models that were printed."""
print("\nThe following models have been printed:")
for completed_model in completed_models:
print(completed_model)
unprinted_designs = ['phone case', 'robot pendant', 'dodecahedron']
completed_models = []
print_models(unprinted_designs, completed_models)
show_completed_models(completed_models)
| 1 | We define the function print_models() with two parameters: a list
of designs that need to be printed and a list of completed models.
Given these two lists, the function simulates printing each design. |
| 2 | We define the function show_completed_models() with one parameter:
the list of completed models. Given this list, it displays the name
of each model that was printed. |
This program has the same output as the version without functions, but the code is much more organized. Look at the body of the program and notice how easily you can follow what’s happening:
unprinted_designs = ['phone case', 'robot pendant', 'dodecahedron']
completed_models = []
print_models(unprinted_designs, completed_models)
show_completed_models(completed_models)
The descriptive function names allow others to read this code and
understand it, even without comments. This program is easier to extend
and maintain than the version without functions. If we need to print more
designs later on, we can simply call print_models() again. If we
realize the printing code needs to be modified, we can change the code
once, and our changes will take place everywhere the function is called.
This example also demonstrates the idea that every function should have one specific job. The first function prints each design, and the second displays the completed models. If you’re writing a function and notice the function is doing too many different tasks, try to split the code into two functions. Remember that you can always call a function from another function, which can be helpful when splitting a complex task into a series of steps.
Preventing a Function from Modifying a List
Sometimes you’ll want to prevent a function from modifying a list. For
example, say that you start with a list of unprinted designs and write a
function to move them to a list of completed models, as in the previous
example. You may decide that even though you’ve printed all the designs,
you want to keep the original list of unprinted designs for your
records. But because you moved all the design names out of
unprinted_designs, the list is now empty, and the empty list is the
only version you have; the original is gone. In this case, you can
address this issue by passing the function a copy of the list, not the
original. Any changes the function makes to the list will affect only
the copy, leaving the original list intact.
You can send a copy of a list to a function like this:
function_name(list_name[:])
The slice notation [:] makes a copy of the list to send to the
function. If we didn’t want to empty the list of unprinted designs in
printing_models.py, we could call print_models() like this:
print_models(unprinted_designs[:], completed_models)
The function print_models() can do its work because it still receives
the names of all unprinted designs. But this time it uses a copy of the
original unprinted_designs list, not the actual unprinted_designs
list. The list completed_models will fill up with the names of printed
models like it did before, but the original list of unprinted designs
will be unaffected by the function.
Even though you can preserve the contents of a list by passing a copy of it to your functions, you should pass the original list to functions unless you have a specific reason to pass a copy. It’s more efficient for a function to work with an existing list, because this avoids using the time and memory needed to make a separate copy. This is especially true when working with large lists.
Try It Yourself
8-9. Messages: Make a list containing a series of short text messages.
Pass the list to a function called show_messages(), which prints each
text message.
8-10. Sending Messages: Start with a copy of your program from
Exercise 8-9. Write a function called send_messages() that prints each
text message and moves each message to a new list called sent_messages
as it’s printed. After calling the function, print both of your lists to
make sure the messages were moved correctly.
8-11. Archived Messages: Start with your work from Exercise 8-10. Call
the function send_messages() with a copy of the list of messages.
After calling the function, print both of your lists to show that the
original list has retained its messages.
Passing an Arbitrary Number of Arguments
Sometimes you won’t know ahead of time how many arguments a function needs to accept. Fortunately, Python allows a function to collect an arbitrary number of arguments from the calling statement.
For example, consider a function that builds a pizza. It needs to accept
a number of toppings, but you can’t know ahead of time how many toppings
a person will want. The function in the following example has one
parameter, *toppings, but this parameter collects as many arguments as
the calling line provides:
def make_pizza(*toppings):
"""Print the list of toppings that have been requested."""
print(toppings)
make_pizza('pepperoni')
make_pizza('mushrooms', 'green peppers', 'extra cheese')
The asterisk in the parameter name *toppings tells Python to make a
tuple called toppings, containing all the values this function
receives. The print() call in the function body produces output showing
that Python can handle a function call with one value and a call with
three values. Note that Python packs the arguments into a tuple, even if
the function receives only one value:
('pepperoni',)
('mushrooms', 'green peppers', 'extra cheese')
Now we can replace the print() call with a loop that runs through the
list of toppings and describes the pizza being ordered:
def make_pizza(*toppings):
"""Summarize the pizza we are about to make."""
print("\nMaking a pizza with the following toppings:")
for topping in toppings:
print(f"- {topping}")
make_pizza('pepperoni')
make_pizza('mushrooms', 'green peppers', 'extra cheese')
The function responds appropriately, whether it receives one value or three values:
Making a pizza with the following toppings:
- pepperoni
Making a pizza with the following toppings:
- mushrooms
- green peppers
- extra cheese
Mixing Positional and Arbitrary Arguments
If you want a function to accept several different kinds of arguments, the parameter that accepts an arbitrary number of arguments must be placed last in the function definition. Python matches positional and keyword arguments first and then collects any remaining arguments in the final parameter.
For example, if the function needs to take in a size for the pizza, that
parameter must come before the parameter *toppings:
def make_pizza(size, *toppings):
"""Summarize the pizza we are about to make."""
print(f"\nMaking a {size}-inch pizza with the following toppings:")
for topping in toppings:
print(f"- {topping}")
make_pizza(16, 'pepperoni')
make_pizza(12, 'mushrooms', 'green peppers', 'extra cheese')
In the function definition, Python assigns the first value it receives
to the parameter size. All other values that come after are stored in
the tuple toppings. Now each pizza has a size and a number of
toppings, and each piece of information is printed in the proper place:
Making a 16-inch pizza with the following toppings:
- pepperoni
Making a 12-inch pizza with the following toppings:
- mushrooms
- green peppers
- extra cheese
|
You’ll often see the generic parameter name |
Using Arbitrary Keyword Arguments
Sometimes you’ll want to accept an arbitrary number of arguments, but
you won’t know ahead of time what kind of information will be passed to
the function. In this case, you can write functions that accept as many
key-value pairs as the calling statement provides. One example involves
building user profiles: you know you’ll get information about a user,
but you’re not sure what kind of information you’ll receive. The
function build_profile() in the following example always takes in a
first and last name, but it accepts an arbitrary number of keyword
arguments as well:
def build_profile(first, last, **user_info):
"""Build a dictionary containing everything we know about a user."""
user_info['first_name'] = first (1)
user_info['last_name'] = last
return user_info
user_profile = build_profile('albert', 'einstein',
location='princeton',
field='physics')
print(user_profile)
| 1 | In the body of build_profile(), we add the first and last names
to the user_info dictionary because we’ll always receive these two
pieces of information from the user, and they haven’t been placed
into the dictionary yet. |
The definition of build_profile() expects a first and last name, and
then it allows the user to pass in as many name-value pairs as they
want. The double asterisks before the parameter **user_info cause
Python to create a dictionary called user_info containing all the
extra name-value pairs the function receives. Within the function, you
can access the key-value pairs in user_info just as you would for any
dictionary.
We call build_profile(), passing it the first name 'albert', the
last name 'einstein', and the two key-value pairs
location='princeton' and field='physics'. The returned dictionary
contains the user’s first and last names and, in this case, the location
and field of study as well:
{'location': 'princeton', 'field': 'physics', 'first_name': 'albert', 'last_name': 'einstein'}
The function will work no matter how many additional key-value pairs are provided in the function call.
|
You’ll often see the parameter name |
Try It Yourself
8-12. Sandwiches: Write a function that accepts a list of items a person wants on a sandwich. The function should have one parameter that collects as many items as the function call provides, and it should print a summary of the sandwich that’s being ordered. Call the function three times, using a different number of arguments each time.
8-13. User Profile: Start with a copy of user_profile.py. Build a
profile of yourself by calling build_profile(), using your first and
last names and three other key-value pairs that describe you.
8-14. Cars: Write a function that stores information about a car in a dictionary. The function should always receive a manufacturer and a model name. It should then accept an arbitrary number of keyword arguments. Call the function with the required information and two other name-value pairs, such as a color or an optional feature. Your function should work for a call like this one:
car = make_car('subaru', 'outback', color='blue', tow_package=True)
Print the dictionary that’s returned to make sure all the information was stored correctly.
Storing Your Functions in Modules
One advantage of functions is the way they separate blocks of code from
your main program. When you use descriptive names for your functions,
your programs become much easier to follow. You can go a step further by
storing your functions in a separate file called a module and then
importing that module into your main program. An import statement tells
Python to make the code in a module available in the currently running
program file.
Storing your functions in a separate file allows you to hide the details of your program’s code and focus on its higher-level logic. It also allows you to reuse functions in many different programs. When you store your functions in separate files, you can share those files with other programmers without having to share your entire program. Knowing how to import functions also allows you to use libraries of functions that other programmers have written.
There are several ways to import a module, and I’ll show you each of these briefly.
Importing an Entire Module
To start importing functions, we first need to create a module. A module
is a file ending in .py that contains the code you want to import into
your program. Let’s make a module that contains the function
make_pizza(). To make this module, we’ll remove everything from the
file pizza.py except the function make_pizza():
def make_pizza(size, *toppings):
"""Summarize the pizza we are about to make."""
print(f"\nMaking a {size}-inch pizza with the following toppings:")
for topping in toppings:
print(f"- {topping}")
Now we’ll make a separate file called making_pizzas.py in the same
directory as pizza.py. This file imports the module we just created
and then makes two calls to make_pizza():
import pizza
pizza.make_pizza(16, 'pepperoni') (1)
pizza.make_pizza(12, 'mushrooms', 'green peppers', 'extra cheese')
| 1 | To call a function from an imported module, enter the name of the module you imported, followed by the name of the function, separated by a dot. |
When Python reads this file, the line import pizza tells Python to
open the file pizza.py and copy all the functions from it into this
program. You don’t actually see code being copied between files because
Python copies the code behind the scenes, just before the program runs.
All you need to know is that any function defined in pizza.py will now
be available in making_pizzas.py.
This code produces the same output as the original program that didn’t
import a module. This first approach to importing, in which you simply
write import followed by the name of the module, makes every function
from the module available in your program. If you use this kind of
import statement to import an entire module named module_name.py, each
function in the module is available through the following syntax:
module_name.function_name()
Importing Specific Functions
You can also import a specific function from a module. Here’s the general syntax for this approach:
from module_name import function_name
You can import as many functions as you want from a module by separating each function’s name with a comma:
from module_name import function_0, function_1, function_2
The making_pizzas.py example would look like this if we want to import
just the function we’re going to use:
from pizza import make_pizza
make_pizza(16, 'pepperoni')
make_pizza(12, 'mushrooms', 'green peppers', 'extra cheese')
With this syntax, you don’t need to use the dot notation when you call a
function. Because we’ve explicitly imported the function make_pizza()
in the import statement, we can call it by name when we use the
function.
Using as to Give a Function an Alias
If the name of a function you’re importing might conflict with an existing name in your program, or if the function name is long, you can use a short, unique alias — an alternate name similar to a nickname for the function. You’ll give the function this special nickname when you import the function.
Here we give the function make_pizza() an alias, mp(), by importing
make_pizza as mp. The as keyword renames a function using the alias
you provide:
from pizza import make_pizza as mp
mp(16, 'pepperoni')
mp(12, 'mushrooms', 'green peppers', 'extra cheese')
The import statement shown here renames the function make_pizza() to
mp() in this program. Anytime we want to call make_pizza() we can
simply write mp() instead, and Python will run the code in
make_pizza() while avoiding any confusion with another make_pizza()
function you might have written in this program file.
The general syntax for providing an alias is:
from module_name import function_name as fn
Using as to Give a Module an Alias
You can also provide an alias for a module name. Giving a module a short
alias, like p for pizza, allows you to call the module’s functions
more quickly. Calling p.make_pizza() is more concise than calling
pizza.make_pizza():
import pizza as p
p.make_pizza(16, 'pepperoni')
p.make_pizza(12, 'mushrooms', 'green peppers', 'extra cheese')
The module pizza is given the alias p in the import statement, but
all of the module’s functions retain their original names. Calling the
functions by writing p.make_pizza() is not only more concise than
pizza.make_pizza(), but it also redirects your attention from the
module name and allows you to focus on the descriptive names of its
functions.
The general syntax for this approach is:
import module_name as mn
Importing All Functions in a Module
You can tell Python to import every function in a module by using the
asterisk (*) operator:
from pizza import *
make_pizza(16, 'pepperoni')
make_pizza(12, 'mushrooms', 'green peppers', 'extra cheese')
The asterisk in the import statement tells Python to copy every function
from the module pizza into this program file. Because every function
is imported, you can call each function by name without using the dot
notation. However, it’s best not to use this approach when you’re
working with larger modules that you didn’t write: if the module has a
function name that matches an existing name in your project, you can get
unexpected results. Python may see several functions or variables with
the same name, and instead of importing all the functions separately, it
will overwrite the functions.
The best approach is to import the function or functions you want, or import the entire module and use the dot notation. This leads to clear code that’s easy to read and understand. I include this section so you’ll recognize import statements like the following when you see them in other people’s code:
from module_name import *
Styling Functions
You need to keep a few details in mind when you’re styling functions. Functions should have descriptive names, and these names should use lowercase letters and underscores. Descriptive names help you and others understand what your code is trying to do. Module names should use these conventions as well.
Every function should have a comment that explains concisely what the function does. This comment should appear immediately after the function definition and use the docstring format. In a well-documented function, other programmers can use the function by reading only the description in the docstring. They should be able to trust that the code works as described, and as long as they know the name of the function, the arguments it needs, and the kind of value it returns, they should be able to use it in their programs.
If you specify a default value for a parameter, no spaces should be used on either side of the equal sign:
def function_name(parameter_0, parameter_1='default value')
The same convention should be used for keyword arguments in function calls:
function_name(value_0, parameter_1='value')
PEP 8 recommends that you limit lines of code to 79 characters so every line is visible in a reasonably sized editor window. If a set of parameters causes a function’s definition to be longer than 79 characters, press Enter after the opening parenthesis on the definition line. On the next line, press the Tab key twice to separate the list of arguments from the body of the function, which will only be indented one level:
def function_name(
parameter_0, parameter_1, parameter_2,
parameter_3, parameter_4, parameter_5):
function body...
If your program or module has more than one function, you can separate each by two blank lines to make it easier to see where one function ends and the next one begins.
All import statements should be written at the beginning of a file.
The only exception is if you use comments at the beginning of your file
to describe the overall program.
Try It Yourself
8-15. Printing Models: Put the functions for the example
printing_models.py in a separate file called printing_functions.py.
Write an import statement at the top of printing_models.py, and
modify the file to use the imported functions.
8-16. Imports: Using a program you wrote that has one function in it, store that function in a separate file. Import the function into your main program file, and call the function using each of these approaches:
import module_name
from module_name import function_name
from module_name import function_name as fn
import module_name as mn
from module_name import *
8-17. Styling Functions: Choose any three programs you wrote for this chapter, and make sure they follow the styling guidelines described in this section.
Summary
In this chapter, you learned how to write functions and to pass
arguments so that your functions have access to the information they
need to do their work. You learned how to use positional and keyword
arguments, and also how to accept an arbitrary number of arguments. You
saw functions that display output and functions that return values. You
learned how to use functions with lists, dictionaries, if statements,
and while loops. You also saw how to store your functions in separate
files called modules, so your program files will be simpler and easier
to understand. Finally, you learned to style your functions so your
programs will continue to be well-structured and as easy as possible for
you and others to read.
One of your goals as a programmer should be to write simple code that does what you want it to, and functions help you do this. They allow you to write blocks of code and leave them alone once you know they work. When you know a function does its job correctly, you can trust that it will continue to work and move on to your next coding task.
Functions allow you to write code once and then reuse that code as many times as you want. When you need to run the code in a function, all you need to do is write a one-line call and the function does its job. When you need to modify a function’s behavior, you only have to modify one block of code, and your change takes effect everywhere you’ve made a call to that function.
Using functions makes your programs easier to read, and good function names summarize what each part of a program does. Reading a series of function calls gives you a much quicker sense of what a program does than reading a long series of code blocks.
Functions also make your code easier to test and debug. When the bulk of your program’s work is done by a set of functions, each of which has a specific job, it’s much easier to test and maintain the code you’ve written. You can write a separate program that calls each function and tests whether each function works in all the situations it may encounter. When you do this, you can be confident that your functions will work properly each time you call them.
In Chapter 9, you’ll learn to write classes. Classes combine functions and data into one neat package that can be used in flexible and efficient ways.
Applied Exercises: Ch 8 — Functions
These exercises cover the same concepts as the chapter but use context
from real infrastructure, network security, and language learning work.
Save each as a separate .py file using lowercase and underscores, e.g.
ise_health_check.py.
Domus Digitalis / Homelab
D8-1. Node Greeter: Write a function called greet_node() that
accepts a hostname parameter. The function should print
Connecting to: <hostname>. Call it for at least three nodes using
positional arguments, then again using keyword arguments.
D8-2. VLAN Summary: Write a function called describe_vlan() that
accepts vlan_id, vlan_name, and an optional description parameter
defaulting to 'No description provided'. The function should print a
formatted summary. Call it three times: once with all three arguments,
once with only the required arguments, and once with a keyword argument
for description.
D8-3. Build Node Profile: Write a function called build_node() that
accepts hostname and role as required parameters and **node_info
for arbitrary keyword arguments. The function should return a dictionary
containing all the information. Call it with at least two extra
key-value pairs (e.g., ip, os, vlan). Print the returned
dictionary.
D8-4. Service Queue Processor: Write two functions:
process_services(pending, completed) that pops each service from
pending, prints Starting: <service>, and appends it to completed;
and show_completed(completed) that prints all completed services. Call
both functions with appropriate lists.
D8-5. Stack Module: Create a module file called domus_utils.py that
contains a function get_node_summary(hostname, role, ip) returning a
formatted string. In a separate main.py, import the module and call
the function using at least three of the import styles shown in the
chapter.
CHLA / ISE / Network Security
C8-1. Policy Greeter: Write a function called describe_policy_set()
that accepts name and auth_protocol as positional parameters and
result as an optional parameter defaulting to 'Allow'. The function
should print a formatted summary. Call it using positional arguments,
keyword arguments, and with the default result.
C8-2. Build Endpoint Profile: Write a function called
build_endpoint() that accepts mac_address and group as required
parameters and **endpoint_info for arbitrary keyword arguments. It
should return a dictionary. Call it with extra key-value pairs like
vlan, ip, auth_method. Print the result.
C8-3. Log Severity Formatter: Write a function called
format_log_entry() that accepts severity, source, and message,
with severity defaulting to 6 (Informational). The function should
return a formatted string like [INFO] ise-02: Auth success. Call it
with and without the severity argument. Use the function inside a
while True loop that accepts user input and formats each entry until
the user types quit.
C8-4. Pipeline Stage Processor: Write two functions:
process_stage(pending_logs, processed_logs) that simulates processing
each log type; and show_processed(processed_logs) that prints all
processed log types. Pass a copy of the list to process_stage() to
preserve the original, then print both lists after the call.
C8-5. ISE Utils Module: Create a module called ise_utils.py
containing a function build_policy_entry(name, protocol, result='Allow')
that returns a dictionary. In a separate script, import the function
using at least three different import styles and call it each time.
General Sysadmin / Linux
L8-1. Service Checker: Write a function called check_service() that
accepts service_name and an optional expected_state parameter
defaulting to 'active'. The function should print
Checking <service>: expected <state>. Call it with and without the
optional argument.
L8-2. Build Package Info: Write a function called build_package()
that accepts name and version as required parameters and
**pkg_info for arbitrary keyword arguments. It should return a
dictionary. Call it with extra fields like arch, repo, installed.
Print the result.
L8-3. Log Formatter: Write a function called format_syslog() that
accepts host, facility, message, and an optional severity
defaulting to 6. Return a formatted syslog-style string. Use the
function inside a while True loop that accepts user input for host and
message, formats each entry, and exits on quit.
L8-4. Mount Point Processor: Write two functions:
check_mounts(mount_list, checked) that pops each mount point, prints
Checking: <mount>, and appends it to checked; and
show_checked(checked) that prints all checked mount points. Pass a
copy of the original list and confirm the original is preserved.
L8-5. Sysadmin Utils Module: Create a module called sysadmin_utils.py
containing a function get_service_status(service, state='active')
that returns a formatted string. Import and call it from a separate
script using at least three import styles.
Spanish / DELE C2
E8-1. Vocabulary Greeter: Write a function called present_word()
that accepts palabra and definicion as parameters. The function
should print {palabra}: {definicion}. Call it for at least four
vocabulary words from Don Quijote using both positional and keyword
arguments.
E8-2. Build Vocab Entry: Write a function called build_vocab_entry()
that accepts palabra and definicion as required parameters and
**extra_info for arbitrary keyword arguments (e.g., chapter,
part_of_speech, example). It should return a dictionary. Call it
with and without the extra arguments. Print the returned dictionaries.
E8-3. Chapter Summarizer: Write a function called
summarize_chapter() that accepts number, tema, and an optional
notas parameter defaulting to 'Sin notas'. Return a formatted string
like Capítulo 30 — Tema: … — Notas: …. Use the function inside a
while True loop that accepts chapter number and theme from the user,
prints the summary, and exits on quit.
E8-4. Study Session Processor: Write two functions:
study_topics(pendientes, completados) that pops each topic, prints
Estudiando: <tema>, and appends it to completados; and
show_completados(completados) that prints all completed topics. Pass a
copy of pendientes to the first function and confirm the original list
is preserved.
E8-5. DELE Utils Module: Create a module called dele_utils.py
containing a function get_level_recommendation(nivel, objetivo='C2')
that returns a study recommendation string based on the current level.
Import and call it from a separate script using at least three different
import styles.