Day 5: Functions and Modules#

Functions are useful in two distinct scenarios: To repeat behavior without repeating an entire procedure, or to vary behavior based on the situation at hand. In programming, a function is a block of organized, reusable code used to perform a single, related action. They provide better modularity for your application and a high degree of code reusing.

Basic Structure of a Function#

In Python, a function is defined using the def keyword, followed by a function name and parentheses (). The code block within every function starts with a colon : and is indented.

Note about code blocks - Keep your eyes peeled for blocks of code in Python. Whitespace is syntax: a code block shares the same level of indentation. Blocks always start with a line ending in a colon. When the indentation resumes its previous level, the block has ended.

# definition
def some_function():
    print("Hello world!")
some_function() # function call
Hello world!

Parameters and Arguments: Parameters are variables listed inside the parentheses in the function definition. Arguments are the values passed to these parameters. Arguments are specified after the function name, inside the parentheses.

Return Values: Functions can return values using the return statement. The return value can be any object, and a function can return multiple values.

def do_math():
    return 2 * 2

x = do_math() # x takes the value of do_math()'s return value, 4.

Writing Your First Function#

Using def Keyword: To create a function, use the def keyword, followed by a chosen function name and parentheses. Example: def my_function():. Empty parentheses indicate that the function takes no arguments.

Function Naming Conventions: Function names should be lowercase, with words separated by underscores as necessary to improve readability.

Calling a Function: To call a function, use the function name followed by parentheses. Example: my_function(). Arguments go inside the parentheses, and the function call’s arguments must match up with the definition.

A function can contain all the same variable definitions, mathematical operations, conditional statements, and loops that you’re already familiar with. The only difference is that code written directly into your Python file or Jupyter notebook cell is executed immediately (“sequentially”); code inside a function is merely being defined. It won’t execute until the function is called.

Parameters and Arguments#

Understanding Local Variables: Variables declared inside a function are called local variables. Their scope is limited to the function, meaning they can’t be accessed outside of it.

Positional vs. Keyword Arguments: Positional arguments are arguments that need to be included in the proper position or order. Keyword arguments are arguments accompanied by an identifier (e.g., name='John') and can be listed in any order.

Default Parameter Values: The value provided to a keyword argument in the function’s definition is the default value, and it’s not mandatory to provide a value for that argument to call the function. The value can be overridden by whatever value is provided when the function is called, though.

Using Keywords to Reorder Arguments: With keyword arguments, the order of arguments can be shuffled, allowing more flexibility. Arguments passed in with no keyword have to match up with a position. Positional arguments can’t follow keyword arguments.

Local vs. Global Variables: Global variables are defined outside a function and can be accessed throughout the program, while local variables are confined within the function they are declared in.

Variable Shadowing: Don’t Do This. Avoid using the same name for local and global variables as it can lead to confusing “shadowing” effects, where the local variable “shadows” the global variable. The local variable will be used inside the function without modifying the global. This can be especially confusing for beginners, who may struggle with the same name being used for two separate variables in different scopes.

Using the global keyword, you can explicitly name the global keyword, but this is generally regarded as a bad practice or anti-pattern. It’s not necessary to call methods on objects from your enclosing scope, just to assign them – see the examples below!

x = 42 # global scope variable

def g():
  x = 50 # shadowing; don't do this. You didn't change global 'x'.

def h():
  global x
  x += 1 # modifies global variable 'x'

stuff = []

def i():
  stuff.append("thing") # allowed. 'stuff' will be found in the global scope that encloses this function.

Test Your Understanding on Args#

Consider this function:

def f(x, y=4):
  z = 2 * x + y
  print(f"x = {x}, y = {y}, z = {z}")
  return z

Questions:

  • What are the arguments?

  • Which ones are required to call the function?

  • What would the function print out if called with f(6)? How about f(7, 2)? How about f(1, 2, 3)? How about f(y=2, x=3)?

Answers:

  • The arguments are x and y.

  • x is mandatory, y is optional with a default value of 4.

  • Function calls:

  • f(6) prints x = 6, y = 4, z = 16.

  • f(7, 2) prints x = 7, y = 2, z = 16.

  • f(1, 2, 3) causes a TypeError: you can’t call a function with more arguments than it has defined.

  • f(y=2, x=3) prints x = 3, y = 2, z = 8. Note that the use of keywords has assigned the function’s values by name, not by position.

Return Values and return Statement#

  • How to Return Data from a Function: To send back a result from a function to its caller, use the return statement.

  • Multiple Return Values; Tuple Value Unpacking: Functions in Python can return multiple values, usually in the form of a tuple, which can be unpacked into multiple variables.

  • None: The Default Return Value: If no return statement is used, or the return statement does not have an accompanying value, the function returns None.

Test Your Understanding on Return Values#

Questions:

  • If we defined the function from the previous section and ran n = f(2, 4), what would n’s value be?

Answers:

  • Since functions evaluate to their return value, n is assigned the value that f returns (8).

Get New Functionality Fast Via Modules#

Modules are a way to bring complex capabilities into your Python programs with the use of the import keyword. The Python standard library is sometimes described as “batteries included”, which includes functionality like: interacting with JSON data or CSV files, making web requests, mathematical functions, random numbers, interacting with dates and times, file compression, logging, cryptography, and the list goes on!

Modules are also very important to the performance of Python programs. You may have heard that Python is a “slow language”. This is more or less true in many circumstances. However, when you import a popular data science or machine learning library like Pandas, Scikit, Keras, or PyTorch, the module itself is written in another language and has very few performance constraints. Modules written in C, C++, or Rust can run at the speed of highly efficient compiled languages, then we use Python as “glue code” to load these modules, provide them with data, and utilize their output.

import Syntax#

When you import something, that module will become available in your script’s namespace.

import math

After importing, the module is an object that you can interact with:

math
<module 'math' from '/home/john/Development/Python-3.11.6/build/lib.linux-x86_64-3.11/math.cpython-311-x86_64-linux-gnu.so'>
type(math)
module

In fact, if you save any Python code in a .py file in the same directory as your notebook or additional files, you can import them as modules and access their contents. Just like using a function on any other object, you can use the dot operator (.) to access the contents of a module. Modules can contain any Python object.

math.pi
3.141592653589793
math.pow
<function math.pow(x, y, /)>

The name of the module is not forced upon you, though. You can change the name of something at the time of import, or simply import all of the contents into your current namespace. The import ... as ... syntax allows you to import a module with a specific new name. Here’s one you’ll see frequently:

import matplotlib.pyplot as plt

The module matplotlib contains a module pyplot, and typing matplotlib.pyplot dozens of times in your code would be exhausting. When importing as, the given name plt becomes an alias for the module.

You can import specific parts of a module, leaving the rest behind:

from math import pi

This would solely create a variable pi, no dots required, sourced from the math module.

Use this sparingly, but it’s possible to import all of the contents of a module without using a dot operator.

from math import *

This would bring all of the contents into your current file; variables or functions like pi or pow would now be available directly. It’s often not recommended because it’s difficult to trace back a variable to the module it comes from. If you are using your own .py files to encapsulate imports and definitions, it’s occasionally OK to do this. Readability is greatly impacted, so use star imports with great caution.

Standard Library Highlights#

There’s no way I could summarize the standard library adequately. As homework, peruse the Python Standard Library documentation.

  • the random module: very handy for games or testing your functions with a lot of diverse input.

import random
dice_roll = random.randint(1, 6)
random.random() # float between 0.0 and 1.0
0.21684689911321242
  • the json module: important for sending and retrieving data over web services or saving to file.

import json
data = {'a': 1, 'b': 2}
txt = json.dumps(data) # dump to string
loaded = json.loads(txt) # load from string
  • the time module: for measuring elapsed time or deliberately slowing down your program.

import time
start = time.time()
for i in range(5):
  print(".", end='')
  time.sleep(1.5)
print()
end = time.time()
print(f"Elapsed: {end - start:.2f} seconds")
.
.
.
.
.
Elapsed: 7.50 seconds
  • the datetime module: for interacting with calendars, timestamps, timezones, and durations.

from datetime import datetime
print(datetime.now())
2024-02-18 22:30:12.518690

Every AI/ML Developer Must Know NumPy#

Let’s introduce NumPy by demonstrating the matrix multiplication required in a linear regression calculation. We’ll compute the loss, which is a measure of how far our model’s predictions are from the actual outcomes.

Here’s the step-by-step process:

  1. Import NumPy Library

    • First, we import NumPy: import numpy as np

  2. Define Coefficients and Data

    • Suppose we have a linear model with coefficients a and b (for a simple linear equation y = ax + b). We’ll represent these as a NumPy array:

      coefficients = np.array([a, b])
      
    • Let’s also define our data points. In a simple linear regression, we often have input (x) and output (y) pairs. For this example, x and y are both arrays. Our x data needs an additional column of ones to account for the intercept b:

      x_data = np.array([[x1, 1], [x2, 1], ..., [xn, 1]])
      y_data = np.array([y1, y2, ..., yn])
      
  3. Perform Matrix Multiplication for Prediction

    • We compute the predicted y values (y_pred) using matrix multiplication between our data (x_data) and coefficients. In NumPy, matrix multiplication is done using the @ operator or np.dot() function:

      y_pred = x_data @ coefficients
      
  4. Compute the Loss

    • The loss function quantifies how far our predictions are from the actual values. A common loss function in linear regression is Mean Squared Error (MSE), calculated as the average of the squares of the differences between actual (y_data) and predicted (y_pred) values:

      loss = np.mean((y_data - y_pred) ** 2)
      
import numpy as np

# Example coefficients for the linear model (y = ax + b)
a, b = 2, 1  # Replace with actual values
coefficients = np.array([a, b])

# Example data (Replace with actual data points)
x_data = np.array([[1, 1], [2, 1], [3, 1]])  # Add a column of ones for the intercept
y_data = np.array([3, 5, 7])  # Actual y values

# Predict y using matrix multiplication
y_pred = x_data @ coefficients

# Calculate the loss (Mean Squared Error)
loss = np.mean((y_data - y_pred) ** 2)

print("Predicted y:", y_pred)
print("Loss (MSE):", loss)
Predicted y: [3 5 7]
Loss (MSE): 0.0

Week 1 mini-project#

Synthesize everything you’ve learned so far into one program that solves a problem. Here are some examples:

Number Guessing Game#

Program Specification:

The Number Guessing Game selects a random number within a specified range (e.g., 1 to 100). The user is prompted to guess this number. After each guess, the program provides feedback: either the user guessed correctly, or their guess was too high or too low. The user continues to guess until they find the correct number. The program should keep track of the number of attempts made by the user and display this number once they have successfully guessed the correct number. Inputs include the user’s guesses, and outputs are the feedback messages and the total number of attempts once the game is successfully completed. Give the user a meaningful score based on their performance.

import random
import math

def guess():
    """Function to get a valid integer guess from the user."""
    while True:
        try:
            user_input = int(input("Enter your guess: "))
            return user_input
        except ValueError:
            print("Invalid input. Please enter a valid integer.")

def calculate_score(attempts, range_size):
    """Calculate score based on attempts and range size."""
    # Score calculation:
    # 100% for attempts fewer than binary search worst case (log2(range_size))
    # 0% for attempts equal to range_size (guessing every number)
    worst_case_binary_search = round(math.log2(range_size))
    if attempts < worst_case_binary_search:
        return 100
    elif attempts > range_size:
        return 0
    else:
        # Linear interpolation between the two extremes
        return round(100 * (1 - (attempts - worst_case_binary_search) / (range_size - worst_case_binary_search)))

def guessing_game(min_value=1, max_value=100):
    """Number guessing game with configurable range and scoring system."""
    number_to_guess = random.randint(min_value, max_value)
    attempts = 0
    range_size = max_value - min_value + 1

    while True:
        user_guess = guess()
        attempts += 1

        if user_guess == number_to_guess:
            score = calculate_score(attempts, range_size)
            print(f"Congratulations! You guessed the right number in {attempts} attempts. Your score is {score}%.")
            break
        elif user_guess < number_to_guess:
            print("Too low. Try again.")
        else:
            print("Too high. Try again.")

# Call the game function with custom range if desired
guessing_game(1, 100)
---------------------------------------------------------------------------
StdinNotImplementedError                  Traceback (most recent call last)
Cell In[19], line 47
     44             print("Too high. Try again.")
     46 # Call the game function with custom range if desired
---> 47 guessing_game(1, 100)

Cell In[19], line 34, in guessing_game(min_value, max_value)
     31 range_size = max_value - min_value + 1
     33 while True:
---> 34     user_guess = guess()
     35     attempts += 1
     37     if user_guess == number_to_guess:

Cell In[19], line 8, in guess()
      6 while True:
      7     try:
----> 8         user_input = int(input("Enter your guess: "))
      9         return user_input
     10     except ValueError:

File ~/Development/book_100daysml/venv/lib/python3.11/site-packages/ipykernel/kernelbase.py:1260, in Kernel.raw_input(self, prompt)
   1258 if not self._allow_stdin:
   1259     msg = "raw_input was called, but this frontend does not support input requests."
-> 1260     raise StdinNotImplementedError(msg)
   1261 return self._input_request(
   1262     str(prompt),
   1263     self._parent_ident["shell"],
   1264     self.get_parent("shell"),
   1265     password=False,
   1266 )

StdinNotImplementedError: raw_input was called, but this frontend does not support input requests.
guessing_game(1, 100)
---------------------------------------------------------------------------
StdinNotImplementedError                  Traceback (most recent call last)
Cell In[20], line 1
----> 1 guessing_game(1, 100)

Cell In[19], line 34, in guessing_game(min_value, max_value)
     31 range_size = max_value - min_value + 1
     33 while True:
---> 34     user_guess = guess()
     35     attempts += 1
     37     if user_guess == number_to_guess:

Cell In[19], line 8, in guess()
      6 while True:
      7     try:
----> 8         user_input = int(input("Enter your guess: "))
      9         return user_input
     10     except ValueError:

File ~/Development/book_100daysml/venv/lib/python3.11/site-packages/ipykernel/kernelbase.py:1260, in Kernel.raw_input(self, prompt)
   1258 if not self._allow_stdin:
   1259     msg = "raw_input was called, but this frontend does not support input requests."
-> 1260     raise StdinNotImplementedError(msg)
   1261 return self._input_request(
   1262     str(prompt),
   1263     self._parent_ident["shell"],
   1264     self.get_parent("shell"),
   1265     password=False,
   1266 )

StdinNotImplementedError: raw_input was called, but this frontend does not support input requests.

Basic Quiz Game#

Program Specification: The Basic Quiz Game presents a series of questions to the user, each with multiple choice answers. The user selects their answer for each question, and the program records this selection. After all questions have been answered, the program calculates the total number of correct answers and displays the user’s score. Inputs are the user’s answers to each question, and the output is the user’s total score. The program can optionally display correct answers for the questions the user got wrong.

In my example solution, I’ve added a few other features:

  • JSON file format to load questions from disk.

  • bonus points for answering quickly.

  • shuffle the answers around when displaying the question.

import json
import time
import random

def example_quiz(filename):
    """Create a sample quiz and save it to a JSON file."""
    sample_quiz = [
        {
            "question": "What is the capital of France?",
            "answer": "Paris",
            "wrong_answers": ["Rome", "London"]
        },
        # Add more questions in the same format
    ]
    with open(filename, 'w') as file:
        json.dump(sample_quiz, file, indent=4)

def load_quiz(filename):
    """Load quiz questions from a JSON file."""
    with open(filename, 'r') as file:
        quiz = json.load(file)
        # Check each question for at least two wrong answers
        for question in quiz:
            if len(question["wrong_answers"]) < 2:
                raise ValueError("Each question must have at least two wrong answers.")
        return quiz

# Uncomment to create a sample quiz file
example_quiz("sample_quiz.json")
def quiz_game(filename):
    questions = load_quiz(filename)
    total_score = 0

    for question in questions:
        attempts = 0

        print(question["question"])
        all_answers = question["wrong_answers"] + [question["answer"]]
        random.shuffle(all_answers)

        # Automatically generate letters for answers
        for i, answer in enumerate(all_answers):
            print(f"{chr(97 + i)}. {answer}")

        while attempts < 2:
          start_time = time.time()
          user_answer = input("Your answer: ").strip().lower()
          end_time = time.time()
          time_taken = end_time - start_time

          correct_answer_index = chr(97 + all_answers.index(question["answer"]))
          if user_answer == correct_answer_index:
              if attempts == 1:  # Assuming 'attempts' variable tracks the attempt count
                  total_score += 0.5
              elif time_taken < 5:
                  total_score += 3
              elif time_taken < 10:
                  total_score += 2
              else:
                  total_score += 1
              print(f"Correct! {time_taken:.1f} seconds to answer.")
              break
          else:
              if attempts > 0:
                print("Wrong! The correct answer was", question["answer"])
                break
              else:
                print("Wrong! Try again?")
                attempts += 1

    print(f"Your total score is {total_score}.")
quiz_game("sample_quiz.json")
What is the capital of France?
a. Rome
b. London
c. Paris
---------------------------------------------------------------------------
StdinNotImplementedError                  Traceback (most recent call last)
Cell In[23], line 1
----> 1 quiz_game("sample_quiz.json")

Cell In[22], line 18, in quiz_game(filename)
     16 while attempts < 2:
     17   start_time = time.time()
---> 18   user_answer = input("Your answer: ").strip().lower()
     19   end_time = time.time()
     20   time_taken = end_time - start_time

File ~/Development/book_100daysml/venv/lib/python3.11/site-packages/ipykernel/kernelbase.py:1260, in Kernel.raw_input(self, prompt)
   1258 if not self._allow_stdin:
   1259     msg = "raw_input was called, but this frontend does not support input requests."
-> 1260     raise StdinNotImplementedError(msg)
   1261 return self._input_request(
   1262     str(prompt),
   1263     self._parent_ident["shell"],
   1264     self.get_parent("shell"),
   1265     password=False,
   1266 )

StdinNotImplementedError: raw_input was called, but this frontend does not support input requests.
quiz_game("sample_quiz.json")
What is the capital of France?
a. London
b. Paris
c. Rome
---------------------------------------------------------------------------
StdinNotImplementedError                  Traceback (most recent call last)
Cell In[24], line 1
----> 1 quiz_game("sample_quiz.json")

Cell In[22], line 18, in quiz_game(filename)
     16 while attempts < 2:
     17   start_time = time.time()
---> 18   user_answer = input("Your answer: ").strip().lower()
     19   end_time = time.time()
     20   time_taken = end_time - start_time

File ~/Development/book_100daysml/venv/lib/python3.11/site-packages/ipykernel/kernelbase.py:1260, in Kernel.raw_input(self, prompt)
   1258 if not self._allow_stdin:
   1259     msg = "raw_input was called, but this frontend does not support input requests."
-> 1260     raise StdinNotImplementedError(msg)
   1261 return self._input_request(
   1262     str(prompt),
   1263     self._parent_ident["shell"],
   1264     self.get_parent("shell"),
   1265     password=False,
   1266 )

StdinNotImplementedError: raw_input was called, but this frontend does not support input requests.
quiz_game("sample_quiz.json")
What is the capital of France?
a. Rome
b. London
c. Paris
---------------------------------------------------------------------------
StdinNotImplementedError                  Traceback (most recent call last)
Cell In[25], line 1
----> 1 quiz_game("sample_quiz.json")

Cell In[22], line 18, in quiz_game(filename)
     16 while attempts < 2:
     17   start_time = time.time()
---> 18   user_answer = input("Your answer: ").strip().lower()
     19   end_time = time.time()
     20   time_taken = end_time - start_time

File ~/Development/book_100daysml/venv/lib/python3.11/site-packages/ipykernel/kernelbase.py:1260, in Kernel.raw_input(self, prompt)
   1258 if not self._allow_stdin:
   1259     msg = "raw_input was called, but this frontend does not support input requests."
-> 1260     raise StdinNotImplementedError(msg)
   1261 return self._input_request(
   1262     str(prompt),
   1263     self._parent_ident["shell"],
   1264     self.get_parent("shell"),
   1265     password=False,
   1266 )

StdinNotImplementedError: raw_input was called, but this frontend does not support input requests.
quiz_game("sample_quiz.json")
What is the capital of France?
a. Paris
b. Rome
c. London
---------------------------------------------------------------------------
StdinNotImplementedError                  Traceback (most recent call last)
Cell In[26], line 1
----> 1 quiz_game("sample_quiz.json")

Cell In[22], line 18, in quiz_game(filename)
     16 while attempts < 2:
     17   start_time = time.time()
---> 18   user_answer = input("Your answer: ").strip().lower()
     19   end_time = time.time()
     20   time_taken = end_time - start_time

File ~/Development/book_100daysml/venv/lib/python3.11/site-packages/ipykernel/kernelbase.py:1260, in Kernel.raw_input(self, prompt)
   1258 if not self._allow_stdin:
   1259     msg = "raw_input was called, but this frontend does not support input requests."
-> 1260     raise StdinNotImplementedError(msg)
   1261 return self._input_request(
   1262     str(prompt),
   1263     self._parent_ident["shell"],
   1264     self.get_parent("shell"),
   1265     password=False,
   1266 )

StdinNotImplementedError: raw_input was called, but this frontend does not support input requests.

Your Prompt#

Feel free to make up your own project, if you have any ideas. If you can’t think of something, choose an example prompt or pick them apart for inspiration:

Simple Contact Book Application#

Program Specification: The Simple Contact Book Application allows users to store, retrieve, edit, and delete contact information including names, phone numbers, and email addresses. The user interface should enable adding new contacts, displaying all contacts, searching for a specific contact, editing existing contacts, and deleting contacts. Inputs include user commands for different actions (add, view, search, edit, delete) and the relevant contact details for each action. Outputs are the display of contact information or confirmation messages of successful operations. The program should handle invalid inputs gracefully and provide user-friendly error messages.

Warehouse Ordering System#

Program Specification: The Warehouse Ordering System manages inventory and processes orders for a warehouse. It should allow users to add new items to the inventory, update existing items, remove items, and process orders. Each inventory item should include details such as item ID, name, quantity, and price. The system should enable viewing current inventory, adding new orders, and updating inventory based on orders. Inputs include inventory management commands (add, update, remove) and order details (item ID, quantity). Outputs are the current status of the inventory and confirmation of order processing. The system should also provide warnings for low stock levels and handle errors in inventory management.

Fast Food Shop Cash Register Program#

Program Specification: The Fast Food Shop Cash Register Program is designed to manage customer orders at a fast food shop. The program should allow users to create new orders, add items to orders, and finalize orders with a total cost calculation, including applicable taxes. Each menu item should have a name and price. Inputs include commands to manage orders (create, add items, finalize) and customer choices. Outputs are the details of the order, including items ordered and the total cost. The program should also provide options for handling special requests or modifications to standard menu items and manage errors or invalid inputs effectively.