Chapter 8 - Functions

Chapter 8 - Functions of the book Python Crash Course by Eric Matthes covers how to write functions. Here are my programs from working through this chapter.

01_defining_functions.py

# 01_defining_functions.py

# In this chapter, you'll learn to write functions, which are named blocks of
# code that are 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 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 a 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:

# 1 def greet_user():
# 2     """Display a simple greeting."""
# 3     print("Hello!")
# 4 greet_user()

# This example shows the simplest structure of a function. The line at 1 uses
# the keyword def to informa 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 of the body of the
# function. The text at 2 is a comment called a docstring, which describes what
# the function does. Docstrings are enclosed in triple quotes, which Python
# looks for when it generates documentation for the functions in your programs.

# The line print("Hello!") 3 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 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, as shown
# at 4. Because no information is needed here, calling our function is as simple
# as entering greet_user(). As expected, it prints Hello!:

# Hello!
print("This program contains notes in the comments.")

02_passing_info_to_a_function.py

# 02_passing_info_to_a_function.py

# Passing Information to a Function

# Modified slightly, the function greet_user() can not only tell the user Hello!
# but also greet them 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 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.
print("This program contains notes in the comments.")

03_arguments_and_parameters.py

# 03_arguments_and_parameters.py

# def greet_user(username):
#     """Display a simple greeting."""
#     print(f"Hello, {username.title()}!")
# greet_user('jesse')

# Arguments and Parameters

# In the preceeding 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 paramenter username.

# NOTE

# People sometimes speak of arguments and paramenters interchangeably. Don't be
# surprised if you see the variables in a function refured to as arguments or
# the variables in a function call referred to as parameters.
print("This program contains in the comments.")

04_message.py

# 04_message.py

# 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.

def display_message():
  """Prints a simple message."""
  print("This is a simple message.")

display_message()

05_favorite_book.py

# 05_favorite_book.py

# 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.

def favorite_book(title):
  """Prints a message contain a favorite book."""
  print(f"My favorite book is {title.title()}.")

favorite_book('the imitation of christ')

06_positional_arguments.py

# 06_positional_arguments.py

# Passing Arguments

# Because a function definition can have multiple paramenters, 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):
#     """Display information about a pet."""
#     print(f"\nI have a {animal_type}.")
#     print(f"My {animal_type}'s name is {pet_name.title()}.")

# describe_pet('hamster', 'harry')

# The defintion shows that this function eeds a type of animal and the animal's
# name 1. When we call describe_pet(), we need to provide an animal type and a
# name, in that order. For example, 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 2. In the function body, these two parameters are
# used to display information about the pet being described.

# The output describes a hamster named Harry:

# I have a hamster.
# My hamster's name is Harry.

print("This program contains notes in the comments.")

07_multiple_function_calls.py

# 07_multiple_function_calls.py

# Multiple Function Calls

# You can call a function as many times as need. 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"\nI 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 wit hthe previous set of arguments we used, Python matches 'dog'
# wit the parameter animal_type and 'willie' with the parameter pet_name.

# As before, the function does its job, but this time it prints values for a dog
# named Willie. 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 wit hthe new pet's information. Even
# if the code for describing a pet were to expand to ten lines, you could still
# describe a new pet in just one line by calling the function again.

# You can use as many positional arguments as you need in your functions. Python
# works through the argumetns you provide when calling the function and matches
# each one with the corresponding parameters in the function's definition.

print("This program contains notes in the comments.")

08_order_matters_in_positional_arguments.py

# 08_order_matters_in_postional_arguments.py

# You can get unexpected results if you mix up the order of the arguments in
# a function call when using postional arguments:

# def describe_pet(animal_type, pet_name):
#     """Display information about a pet."""
#     print(f"\nI 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 then 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 defintion.
print("This program contains notes in the comments.")

09_keyword_arguments.py

# 09_keyword_arguments.py

# 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 (you won't end up
# with a harry named Hamster). Keyword arguments free you from having to
# worry about correctly ordering your aruments 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_pets():

# def describe_pet(naimal_type, pet_name):
#     """Display information about a pet."""
#     print(f"\nI 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' with 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')

# NOTE
# When you use keyword arguments, be sure to use the exact name of the
# parameters in the function's definition.
print("This program contains notes in the comments.")

10_default_values.py

# 10_default_values.py

# 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 in which your functions are
# typically used.

# For example, if you notice ethat 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"\nI have a {animal_type}.")
#     print(f"My {animal_type}'s name is {pet_name.title()}.")

# describe_pet(pet_name='willie')

# We've changed the definiton 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 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 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.

# NOTE

# 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.

11_equivalent_function_calls.py

# 11_equivalent_function_calls.py

# Becaue positional arguments, keyword arguments, and default values can be used
# together, often you'll have several equivalent ways to call a function.
# Consider the fuollowing 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 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.

# NOTE

# 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.

12_avoiding_argument_errors.py

# 12_avoiding_argument_errors.py

# 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"\nI 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):
# 1   File "pets.py", line 6, in <module>
# 2     describe_pet()
# 3 TypeError: describe_pet() missing 2 required positional arguments:
# 4 'animal_type' and 'pet_name'

# At 1 the traceback tells us the location of the problem, allowing us to look
# back and see that something went wrong in our function call. At 2 the
# offending function call is written out for us to see. At 3 the traceback
# tells us the call is missign two arguments and reports the names of the
# missing arguments. If this function were in a separate file, we could probably
# rewrite the call correctly without having to open that file and read
# the function code.

# 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.
print("This program contains notes in the comments.")

13_t_shirt.py

# 13_t_shirt.py

# 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.

def make_shirt(size, message):
  print(f"The shirt's size is {size.title()}.")
  print(f'The message on the shirt reads, "{message}"')

make_shirt('s', 'I like Python.')
make_shirt(size='m', message='I like C.')

14_large_shirts.py

# 14_large_shirts.py

# 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.

def make_shirt(size='l', message='I love Python.'):
  print(f"The shirt's size is {size.title()}.")
  print(f'The message on the shirt reads, "{message}"')

make_shirt()
make_shirt('m')
make_shirt('s', 'I prefer C.')

15_cities.py

# 15_cities.py

# 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.

def describe_city(city, country='the United States'):
  print(f"{city.title()} is located in {country}.")

describe_city('chicago')
describe_city('los angeles')
describe_city('paris', 'France')

16_returning_a_simple_value.py

# 16_returning_a_simple_value.py

# 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 a 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}"
#     return full_name.title()

# musician = get_formatted_name('jimi', 'hendrix')
# print(musician)

# The definition of get_formatted_name() takes as parameters a first and a last
# name 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 at 3.

# When you call a function that returns a value, you need to provide a variable
# that the return value can be assigned to. In this case, the returned value is
# assigned to the variable musician at 4. The output shows a neatly formatted
# name made up of the parts of a person's name:

# 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")

# But 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.

print("This program contains notes in the comments.")

17_making_an_argument_optional.py

# 17_making_an_argument_optional.py

# 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. The function
# takes in all three parts of a name and then builds a string out of them. The
# function adds space where appropriate and converts the full name to title
# case:

# 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:

# 1 def get_formatted_name(first_name, last_name, middle_name=''):
#       """Return a full name, neatly formatted."""
# 2     if middle_name:
#           full_name = f"{first_name} {middle_name} {last_name}"
# 3     else:
#           full_name = f"{first_name} {last_name}"
#       return full_name.title()

# musician = get_formatted_name('jimi', 'hendrix')
# print(musician)

# 4 musician = get_formatted_name('johm', 'hooker', 'lee')
# print(musician)

# In this example, the name is built from three possible parts. Because there's
# always a first and a last name, these parameters are listed first in the
# function's definition. The middle name is optional, so it's listed last in the
# defintion, and its default value is an empty string 1.

# In the body of the function, we check to see if a middle name has been
# provided. Python interprets non-empty strings as True, so middle_name
# evaluates to True if a middle name argument is in the function call 2. If a
# middle name is provided, the first, middle, and last names are combined to
# form a full name. This name is then changed to title case and returned to the
# function call line where it's assigned to the variable musician and printed.
# If no middle name is provided, the empty string fails the if test and the else
# block runs 3. The full name is made with just a first and last name, and the
# formatted name is returned to the calling line where it's assigned to musician
# and printed.

# Calling this function with a first and last name is straightforward. If we're
# using a middle name, however, we have to make sure the middle name is the last
# argument passed so Python will match up the positional arguments correctly 4.

# 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.
print("This program contains notes in the comments.")

18_returning_a_dictionary.py

# 18_returning_a_dictionary.py

# 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."""
# 1     person = {'first': first_name, 'last': last_name}
# 2     return person

# musician = build_person('jimi', 'hendrix')
# 3 print(musician)

# The function build_person() takes in a first annd last name, and puts these
# values into a dictionary at 1. The value of first_name is stored with the key
# 'first', and the value of last_name is stored with the key 'last'. The entire
# dictionary representing the person is returned at 2. The return value is
# printed at 3 with the original two pieces of textual information now stored
# in a dictionary:

# {'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. The strings 'jimi' and 'hendrix' are now labeld as a first name
# and last name. 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': firstname, '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. This function always
# stores a person's name, but it can also be modified to store any other
# information you want about a person.
print("This program contains notes in the comments.")

19_using_a_function_with_a_while_loop.py

# 19_using_a_function_with_a_while_loop.py

# You can use functions with all the Python structures you've learned about so
# far. For example, leet'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:
# 1     print("\nPlease tell me your name:")
#       f_name = input("First name: ")
#       l_name = input("Last name: ")

#       formatted_name = get_formatted_name(f_name, l_name)
#       print(f"\nHello, {formatted_name}!")

# For this example, we use a simple version of get_formatted_name() that doesn't
# involve middle names. The while loop asks the user to enter their name, and we
# prompt for their first and last name separately 1. But there's one problem
# with this whileLoop: We haven't defined a quit condition. Whre 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()

# This is an infinite loop!
# 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 ethat informas 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
print("This program contains notes in the comments.")

20_city_names.py

# 20_city_names.py

# 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.

def city_country(city, country):
  """Return a formatted string about a city and country."""
  formatted_string = f"{city.title()}, {country.title()}"
  return formatted_string

city_country_1 = city_country('tokyo', 'japan')
city_country_2 = city_country('seoul', 'south korea')
city_country_3 = city_country('beijing', 'china')
print(city_country_1)
print(city_country_2)
print(city_country_3)

21_album.py

# 21_album.py

# 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.

def make_album(artist_name, album_title, songs=None):
    """Return an album's information formatted in a dictionary."""
    album_dictionary = {'artist_name': artist_name, 'album_name': album_title}
    if songs:
        album_dictionary['songs'] = songs
    return album_dictionary

album_1 = make_album('TWICE', 'Twicetagram', '13')
album_2 = make_album('Itzy', 'Crazy in Love', '16')
album_3 = make_album('IU', 'Growing Up', '16')

print(album_1)
print(album_2)
print(album_3)

21_album.py

# 21_album.py

# 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.

def make_album(artist_name, album_title, songs=None):
    """Return an album's information formatted in a dictionary."""
    album_dictionary = {'artist_name': artist_name, 'album_name': album_title}
    if songs:
        album_dictionary['songs'] = songs
    return album_dictionary

album_1 = make_album('TWICE', 'Twicetagram', '13')
album_2 = make_album('Itzy', 'Crazy in Love', '16')
album_3 = make_album('IU', 'Growing Up', '16')

print(album_1)
print(album_2)
print(album_3)

22_user_albums.py

# 22_user_albums.py

# 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
# the 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.

def make_album(artist_name, album_title, songs=None):
    """Return an album's information formatted in a dictionary."""
    album_dictionary = {'artist_name': artist_name, 'album_name': album_title}
    if songs:
        album_dictionary['songs'] = songs
    return album_dictionary

# This is an infinite loop.
while True:
    print("\nPlease enter the name of an artist and album:")
    print("(enter 'q' at any time to quit)")

    artist_name = input("Artist name: ")
    if artist_name == 'q':
        break

    album_title = input("Album title: ")
    if album_title == 'q':
        break

    album_information = make_album(artist_name, album_title)
    print(album_information)

23_passing_a_list.py

# 23_passing_a_list.py

# 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 morking ith 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. At 1 we define a list of users and then pass
# the list usernames to greet_users() in our function call:

# Hello, Hannah!
# Hello, Ty!
# Hello, Margot!

# This is the output we wanted. Every user sees a personalized greeting, and you
# can call the function any time you want to greet a specific set of users.
print("This program contains notes in the comments.")

24_modifying_a_list_in_a_function.py

# 24_modifying_a_list_in_a_function.py

# 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 complete_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 making it more
# carefully structured. The first function will handle printing the designs, and
# the second will summarize the prints that have been made:

# def print_models(unprinted_designs, 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)

# def show_completed_models(completed_models):
#     """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)

# At 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 by emptying the list of
# unprinted designs and filling up the list of completed models. At 2 we define
# the function show_completed_models() with one parameter: the list of completed
# models. Given this list, show_completed_models() 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. The code that does most of the work has been
# moved to two separate functions, which makes the main part of the program
# easier to understand. Look at the body of the program to see how much easier
# it is to understand what this program is doing:

# unprinted_designs = ['phone case', 'robot pendant', 'dodecahedron']
# completed_models = []

# print_models(unprinted_designs, completed_models)
# show_completed_models(completed_models)

# We set up a list of unprinted designs and an empty list that will hold the
# completed models. Then, because we've already defined our two functions, all
# we have to do is call them and pass them the right arguments. We call
# print_models() and pass it the two lists it needs; as expected, print_models()
# simulates printing the designs. Then we call show_completed_models() and pass
# it the list of completed models so it can report the models that have been
# printed. 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 everwhere the
# function is called. This technique is more efficient than having to update
# code separately in several places in the program.

# 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. This is more beneficial than using one function to do
# both jobs. 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.
print("This program contains notes in the comments.")

25_preventing_a_function_from_modifying_a_list.py

# 25_preventing_a_function_from_modifying_a_list.py

# Preventing a Function from Modifying a List

# Sometimes you'll want to prevent a function from modifying a list. For
# example, sary 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
# only affect 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 would 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 perserve 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 to avoid using the time and memory needed to make a
# separate copy, especially when you're working with large files.
print("This program contains notes in the comments.")

26_messages.py

# 26_messages.py

# 8-9. Messages: Make a list containing a series of short text messages. Pass
# the list to a funciton called show_messages(), which prints each text message.

messages = [
    'I like TWICE.', 'I also like Itzy.', 'I like NMIXX.',
    'I also like BLACKPINK.', 'I also like fromis_9', 'I like FIFTY FIFTY too.'
]

def show_messages(messages):
    for message in messages:
        print(message)

show_messages(messages)

27_sending_messages.py

# 27_sending_messages.py

# 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.

messages = [
    'I like TWICE.', 'I also like Itzy.', 'I like NMIXX.',
    'I also like BLACKPINK.', 'I also like fromis_9', 'I like FIFTY FIFTY too.'
]

sent_messages = []

def show_messages(messages):
    for message in messages:
        print(message)

def send_messages(messages, sent_messages):
    """Move messages to sent mesages."""
    while messages:
        current_message = messages.pop()
        print(f"Sending message: {current_message}")
        sent_messages.append(current_message)

def print_messages(messages):
    print("This is what is in the messages list.")
    print(messages)

def print_sent_messages(sent_messages):
    print("This is what is in the sent_messages list.")
    print(sent_messages)

send_messages(messages, sent_messages)
print_messages(messages)
print_sent_messages(sent_messages)

28_archived_messages.py

# 28_archived_messages.py

# 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 orignal list has
# retained its messages.

messages = [
    'I like TWICE.', 'I also like Itzy.', 'I like NMIXX.',
    'I also like BLACKPINK.', 'I also like fromis_9', 'I like FIFTY FIFTY too.'
]

sent_messages = []

def show_messages(messages):
    for message in messages:
        print(message)

def send_messages(messages, sent_messages):
    """Move messages to sent mesages."""
    while messages:
        current_message = messages.pop()
        print(f"Sending message: {current_message}")
        sent_messages.append(current_message)

def print_messages(messages):
    print("This is what is in the messages list.")
    print(messages)

def print_sent_messages(sent_messages):
    print("This is what is in the sent_messages list.")
    print(sent_messages)

send_messages(messages[:], sent_messages)
print_messages(messages)
print_sent_messages(sent_messages)

29_passing_an_arbitrary_number_of_arguments.py

# 29_passing_an_arbitrary_number_of_arguments.py

# 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 an empty
# tuple called toppings and pack whatever values it receives into this tuple.
# 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. It
# treats the different calls similarly. 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 all 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

# This syntax works no matter how many arguments the function receives.
print("This program contains notes in the comments.")

30_mixing_positional_and_arbitrary_arguments.py

# 30_mixing_positional_and_arbitrary_arguments.py

# 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. The function calls include an argument for the size first, followed
# by as many toppings as needed.

# Now each pizza has a size and a number of toppings, and each piece of
# information is printed in the proper place, showing size first and toppings
# after:

# Making a 16-inch pizza with the following toppings:
# - pepperoni

# Making a 12-inch pizza with the following toppings:
# - mushrooms
# - green peppers
# - extra cheese

# NOTE

# You'll often see the generic parameter name *args, which collects arbitrary
# positional arguments like this.
print("This program contains notes in the comments.")

31_using_arbitrary_keyword_arguments.py

# 31_using_arbitrary_keyword_arguments.py

# 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_profiles() in the
# following example always takes in a first and a 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
#     user_info['last_name] = last
#     return user_info

# user_profile = build_profile('albert', 'einstein',
#                              location='princeton',
#                              field='physics')

# print(user_profile)

# The definition of build_profile() expexts a first and a 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 an
# empty dictionary called user_info and pack whatever name-value pairs it
# receives into this dictionary. Within the function, you can access the
# key-value pairs in user_info just as you would for any dictionary.

# 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 1, and they haven't been placed into the dictionary
# yet. Then we return the user_info dictionary to the function call line.

# 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'. We assign the returned profile to user_profile and print
# user_profile:

# {'location': 'princeton', 'field': 'physics',
# 'first_name': 'albert', 'last_name': 'einstein'}

# The returned dictionary contains the user's first and last names and, in this
# case, the location and field of study as well. The function would work no
# matter how many additional key-value pairs are provided in the function call.

# You can mix positional, keyword, and arbitrary values in many different
# ways when writing your own functions. It's useful to know that all these
# argument types exist because you'll see them often when you start reading
# other people's code. It takes practice to learn to use the different types
# correctly and to know when to use each type. For now, remember to use the
# simplest approach that gets the job done. As you progress you'll learn to use
# the most efficient approach each time.

# NOTE

# You'll often see the parameter name **kargs used to collect non-specific
# keyword arguments.
print("This program contains notes in the comments.")

32_sandwiches.py

# 32_sandwiches.py

# 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 nubmer of arguments each time.

def make_sandwich(*items):
    """Summarize the sandwich we are about to make."""
    print(f"Making a sandwich with the following items:")
    for item in items:
        print(f"- {item}")

make_sandwich('turkey', 'colby-jack cheese', 'wheat bread')
make_sandwich('ham', 'cheddar cheese', 'white bread')
make_sandwich('bacon', 'lettuce', 'tomatoes', 'toasted white bread')

33_user_profile.py

# 33_user_profile.py

# 8-13. User Profile: Start with a copy of user_profile.py from page 149. Build
# a profile of yourself by calling build_profile(), using your first and last
# names and three other key-value pairs that describe you.

def build_profile(first, last, **user_info):
    """Build a dictionary contain everything we know about a user."""
    user_info['first_name'] = first
    user_info['last_name'] = last
    return user_info

user_profile = build_profile('jane', 'doe',
                             favorite_anime='kimi ni todoke',
                             favorite_kpop_group='itzy',
                             favorite_programming_language='python')

print(user_profile)

34_cars.py

# 34_cars.py

# 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.

def make_car(manufacturer, model_name, **car_info):
    """Build a dictionary containing everything we know about a car."""
    car_info['manufacturer'] = manufacturer
    car_info['model_name'] = model_name
    return car_info

car = make_car('subaru', 'outback', color='blue', tow_package=True)
print(car)

35_importing_an_entire_module.py

# 35_importing_an_entire_module.py

# Storing Your Functions in Modules

# One advantage of functions is the way they separate blocks of code from your
# main program. By using descriptive names for your functions, you main
# program will be 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 hid the details of
# your program's code and focus on it's 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
# also having to share your entire program. Knowing know 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 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():

# pizza.py

# 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():

# making_pizzas.py

# import pizza

# pizza.make_pizza(16, 'pepperoni')
# pizza.make_pizza(12, 'mushrooms', 'green peppers', 'extra cheese')

# 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.

# To call a function from an imported module, enter the name of the module you
# imported, pizza, followed by the name of the function make_pizza(), separated
# by a dot 1. This code produces the same output as the original program that
# didn't import a module:

# Making a 16-inch pizza with the following toppings:
# - pepperoni

# Making a 12-inch pizza with the following toppings:
# - mushrooms
# - green peppers
# - extra cheese

# The 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()
print("This program contains notes in the comments.")

36_importing_specific_functions.py

# 36_importing_specific_functions.py

# 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 example would look like ethis 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 sytnax, 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.
print("This program contains notes in the comments.")

37_using_as_to_give_a_function_an_alias.py

# 37_using_as_to_give_a_function_an_alias.py

# 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. Any time 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
print("This program contains notes in the comments.")

38_using_as_to_give_a_module_an_alias.py

# 38_using_as_to_give_a_module_an_alias.py

# You can also provide an alias for a module name. Giving a module a short
# alias, like p for pizza, allows you t ocall the module's functions more
# quickly. Calling p.make_pizza() is more concise then 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 writing
# pizza.make_pizza(), but also redirects your attention from the module name and
# allows you to focus on the descriptive names of its functions. These function
# names, which clearly tell you want each function does, are more important to
# the readability of your code than using the full mode name.

# The general syntax for this approach is:

# import module_name as mn
print("This program contains notes in the comments.")

39_importing_all_functions_in_a_module.py

# 39_importing_all_functions_in_a_module.py

# 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 notaion. However,
# it's not best 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 some 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 *
print("This program contains notes in the comments.")

40_styling_functions.py

# 40_styling_functions.py

# 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 (https://www.python.org/dev/peps/pep-0008/) 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 paramters cause 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 TAB twice to separate the list of
# arguments from the body of the function, which will only be indented one
# level.

# Most editors automatically line up any additional lines of parameters to match
# the indentation you have established on the first line:

# 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
# 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.
print("This program contains notes in the comments.")

41_printing_models.py

# 41_printing_models.py

# 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.

# printing_functions.py

# def print_models(unprinted_designs, 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)

# def show_completed_models(completed_models):
#     """Show all the models that were printed."""
#     print("\nThe following models have been printed:")
#     for completed_model in completed_models:
#         print(completed_model)

# 41_printing_models.py

# from printing_functions import *

# unprinted_designs = ['phone case', 'robot pendant', 'dodecahedron']
# completed_models = []

# print_models(unprinted_designs, completed_models)
# show_completed_models(completed_models)

# If I imported using import printing_functions:

# import printing_functions

# printing_functions.print_models(unprinted_designs, completed_models)
# printing_functions.show_completed_models(completed_models)

print("This program contains notes in the comments.")

42_imports.py

# 42_imports.py

# 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 *

# module_name.py

# def function_name():
#     print("Hello world.")

# -----------------------------------------------------------------

# example_1.py

# Example 1: using import module_name

# import module_name

# module_name.function_name()

# -----------------------------------------------------------------

# example_2.py

# Example 2: using from module_name import function_name

# from module_name import function_name

# function_name()

# -----------------------------------------------------------------

# example_3.py

# Example 3: from module_name import function_name as fn

# from module_name import function_name as fn

# fn()

# -----------------------------------------------------------------

# example_4.py

# Example 4: import module_name as mn

# import module_name as mn

# mn.function_name()

# -----------------------------------------------------------------

# example_5.py

# Example 5: from module_name import *

# from module_name import *

# function_name()

# -----------------------------------------------------------------

43_styling_functions.py

# 43_styling_functions.py

# Chose any three programs you wrote for this chapter, and make sure they
# follow the styling guidelines described in this section.

# 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 (https://www.python.org/dev/peps/pep-0008/) 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 paramters cause 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 TAB twice to separate the list of
# arguments from the body of the function, which will only be indented one
# level.

# Most editors automatically line up any additional lines of parameters to match
# the indentation you have established on the first line:

# 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
# 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.

print("This program contains notes in the comments. "
      "My programs follow these guidelines.")

44_summary.py

# 44_summary.py

# 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 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 seperate 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 debut. 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 wheter 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.
print("This program contains notes in the comments.")