Managing Application State¶

🎨 enumerate¶

In [ ]:
prophets = ['Moses', 'Elijah', 'Nephi', 'Malachi', 'Alma']

for index, prophet in enumerate(prophets):
    print(f'{index}: {prophet}')

👨🏾‍🎨 Lights 💡¶

Description¶

lights.py simulates a smart-home light-management system.

This program allows the user to view the status ("ON" or "OFF") of the lights in the home, as well as toggle the status of each light (ON->OFF or OFF->ON).

Requirements¶

  • The program remembers the status of each light inbetween invocations of the program.
  • When the program starts, the user is shown the main menu with the following options:
    • View status
      • When selected, the program lists each light and its status in the format shown below.
    • Toggle light
      • When selected, the program displays a menu of the lights. The status of the selected light is toggled.
      • The program then prints a statement about the current status of the light that just changed.
    • Quit
      • Exit the program

Example¶

(0) Show statuses
(1) Toggle light
(2) Quit
Option: 0

Front Porch: OFF
Entry: OFF
Kitchen: OFF
Bedroom: OFF

(0) Show statuses
(1) Toggle light
(2) Quit
Option: 1

Which light do you want to toggle?

(0) Front Porch
(1) Entry
(2) Kitchen
(3) Bedroom
Option: 1
Entry is now ON

(0) Show statuses
(1) Toggle light
(2) Quit
Option: 0

Front Porch: OFF
Entry: ON
Kitchen: OFF
Bedroom: OFF

(0) Show statuses
(1) Toggle light
(2) Quit
Option: 2

lights.py¶

NOTES

  • Draw out an outline of the program
    • Main menu -> view status, -> toggle light, -> exit
    • Identify places where input happens

Goal 1: Main menu¶

NOTES

  • Add a menu with the following options
    • Quit
    • View statuses
    • Toggle light
  • Show how to validate menu inputs
    • Store a list of valid string inputs
    • Convert to int only if it was valid
  • Test the menu
    • print the action associated with the option
def main_menu():
    menu = [
        'Quit',
        'Show status',
        'Toggle light'
    ]
    valid_options = []
    print()
    for index, option in enumerate(menu):
        print(f'({index}) {option}')
        valid_options.append(str(index))

    while True:
        response = input('Option: ').strip()
        if response not in valid_options:
            print(f'Invalid option: {response}')
        else:
            return int(response)


def run_lights():
    while True:
        option = main_menu()
        if option == 0:  # Show status
            print()
            print("Show status")
        elif option == 1:  # Toggle light
            print()
            print('Toggle light')
        else:  # quit
            return

Goal 2: Initial state¶

NOTES

  • Use the json library to load lights.json
  • Include if __name__ == '__main__': block
    • wire up sys.argv[1]
def load_lights(lights_file):
    with open(lights_file) as file:
        return json.load(file)


def run_lights(lights_file):
    lights = load_lights(lights_file)
    ...


if __name__ == '__main__':
    run_lights(sys.argv[1])

Goal 3: Show status¶

NOTES

  • Add a function call for show_status(lights) in run_lights
    • why not just put this logic in run_lights?
    • Single responsibility principle
      • Gluing ideas together counts as a responsibility
  • Implement the stub
  • Test the feature
def show_status(lights):
    print()
    for name, status in lights.items():
        if status:
            status = "On"
        else:
            status = "Off"
        print(f'{name}: {status}')
Single Responsibility Principle: Try to focus your functions to one clear, specific objective.

Goal 4: Toggle light¶

NOTES

  • Add a function call for toggle_light(lights) in run_lights
  • Implement the stub
    • Hmm, we need to do the same thing as the menu...
    • Refactor main_menu into prompt_menu(menu) and main_menu
    • Use prompt_menu in toggle_light
    • Refactor/write get_status(bool_status) to return "ON" or "OFF"
      • Use this in show_status and in toggle_light
    • New:
      • lights.keys()
      • lights[key] = not lights[key] (the "boolean toggle")
  • Test the feature
def prompt_menu(menu, message):
    valid_options = []
    print()
    print(message)
    for index, option in enumerate(menu):
        print(f'({index}) {option}')
        valid_options.append(str(index))

    while True:
        response = input('Option: ').strip()
        if response not in valid_options:
            print(f'Invalid option: {response}')
        else:
            return int(response)

def get_status(bool_status):
    if bool_status:
        return "ON"
    else:
        return "OFF"

def toggle_light(lights):
    menu = list(lights.keys())
    option = prompt_menu(menu, "Which light do you want to toggle?")
    key = menu[option]
    lights[key] = not lights[key]
    print(f'{key} is now {get_status(lights[key])}')
DRY: Don't Repeat Yourself. Consolidate duplicated code into a function you call multiple times.

Goal 5: Persistent state¶

NOTES

  • We want to save the state of each light to a file so the statuses persist between invocations of the program
  • Write save_lights(lights, lights_file)
  • Call save_lights right after toggle_light
def save_lights(lights, lights_file):
    with open(lights_file, 'w') as file:
        json.dump(lights, file)
def run_lights(lights_file):
        ...
        elif option == 1:  # Toggle light
            toggle_light(lights)
            save_lights(lights, lights_file)
        ...

Review Code¶

  • How did we keep the code DRY?
  • How did we follow the single responsibility principle?

Key Ideas¶

  • Initial state loaded from file
  • Mutability vs immutability
    • Do you modify the existing dictionary in place, or do you create a copy that has a change in it?
  • Menu pattern
    • Display options
    • Prompt selection
    • Validate response
    • if-elif-else block to choose action based on option
  • Each option does something to the state
    • Read-only actions like printing the state
    • Adding entries
    • Changing entries
  • Save state as you go
    • Whenever the state is changed, save it to a file
  • Validating input
    • Sometimes it's nice to have an option for "going back" or "cancelling" an activity
  • Test-as-you-go
  • DRY: don't repeat yourself
  • Single responsibility principle