Files
python-2020-5-day-class/docs/day4.md
2020-11-29 16:36:27 -05:00

15 KiB

Day 4

"While"

The last section covered the for loop - a useful loop when you have a finite number of things you need to do something with. A grocery list, api data, and application arguments could be massive but will always have a definite size. Let's talk about times when you don't know how many times to loop.

Example 1: A Game

Let's imagine you're coding a checkers app. You might approach it like this:

  1. Game Start
  2. Player 1 moves
  3. Check if player 2 has no remaining pieces
  4. Player 2 moves
  5. Check if player 1 has no remaining pieces
  6. Player 1 moves
  7. Check if player 2 has no remaining pieces
  8. Player 2 moves
  9. Check if player 1 has no remaining pieces
  10. ""
  11. ""
  12. ""
  13. ...

You'll need to do a lot of the same stuff over and over - that's great for a loop. Since you'll have no idea how long the game will go on you can't use a for loop because there aren't a finite number of things to loop through (although you could simple use the longest checkers game as your starting point I suppose).

What we need is a loop that run indefinitey until a condition is met - namely that one player wins the game.

Example 2: User Input

Let's build a menu system for a text-based app.

  1. Open your python interpretor by typing python and pressing ++enter++

  2. In your python interpretor paste the following:

    program_choices = ["print", "count", "add", "exit"]
    while True:
        choice = input(f"Please choose one of the following options: [{', '.join(program_choices)}]: ")
        if choice == "print":
            print("hello")
        elif choice == "count":
            print(", ".join(["1","2","3","4","5"]))
        elif choice == "add":
            num1 = input("first number: ")
            num2 = input("second number: ")
            try:
                print(f"Answer: {int(num1) + int(num2)}")
            except ValueError:
                print("You did not provide 2 valid numbers")
        elif choice == "exit":
            break
        else:
            print("invalid choice!")
    
  3. Exit the menu by typing 'exit'

  4. Type exit() to exit the python interpretor

This lets a user select from a variety of options and returns them to the start of the menu upon a selection. If their selection is invalid we tell them it's invalid and return them to the start anyway.

Since we don't know how many things a user will want to do with our menu we should use a while loop to loop indefinitely.

Note the line that says break. This is how you break out of a loop in python. as soon as you call break it will stop the loop.

Easy looping

Now that we've seen a few examples let's run through the core of a while loop:

  1. Type python and press ++enter++ to open the python interpretor.

  2. We can use a while loop to do something indefinitely. Type the following and press ++enter++ twice:

    while True:
        print("hello")
    
  3. Press ++ctrl+c++ to stop the loop

  4. We can use a while loop like a for loop (though this isn't recommended - it's too easy to get stuck in an infinite loop). Type the following and press ++enter++ twice:

    count_to = 10
    start_at = 1
    while start_at <= count_to:
        print(start_at)
        start_at += 1
    
  5. We can use a while loop to check multiple conditions. Type the following and press ++enter++:

    import random
    day = 1
    raining = True
    temperature = "cold"
    while temperature == "cold" or raining == True:
        print(f"Day {day}: Stay inside")
        day += 1
        temperature = random.choice(["cold", "warm"])
        raining = random.choice([True, False])
    
    print(f"Day {day}: It's safe")
    
  6. We can put while loops inside while loops (notice how we break out of each loop - this requires 2 break statements). Type the following and press ++enter++:

    while True:
        print("loop 1")
        while True:
            print("loop 2")
            break
        break
    

Let's build a menu

We're going to build a piece of software that lets us interact with our people API. We should be able to:

  1. List the people from our API
  2. Update the list by calling the API
  3. Clear the list
  4. Exit

Let's get started:

  1. Create a new file in your root directory called "menu.py"

  2. Add the following to the top:

    import requests
    
  3. Create the list_people function by adding the following:

    import requests
    
    def list_people(people):
        print(people)
        return people
    
  4. Now create the update_people function by adding the following:

    def list_people(people):
        print(people)
        return people
    
    def update_people(people):
        try:
            response = requests.get("http://localhost:8000/people")
            people = response.json()["data"]
            print("successfully updated people.")
        except requests.exceptions.ConnectionError:
            print("Server is not running. Failed to update.")
        return people
    
  5. Now create the clear_people function by adding the following:

    def update_people(people):
        try:
            response = requests.get("http://localhost:8000/people")
            people = response.json()["data"]
            print("successfully updated people.")
        except requests.exceptions.ConnectionError:
            print("Server is not running. Failed to update.")
        return people
    
    def clear_people(people):
        people = None
        return people
    
  6. Finally add the main loop:

    def clear_people(people):
        people = None
        return people
    
    if __name__ == "__main__":
        people = None
        choices = ["list", "update", "clear", "exit"]
        while True:
            choice = input(f"Please choose an option [{', '.join(choices)}]: ")
            if choice == "list":
                people = list_people(people)
            elif choice == "update":
                people = update_people(people)
            elif choice == "clear":
                people = clear_people(people)
            elif choice == "exit":
                break
            else:
                print("Invalid choice. Please try again.")
    
  7. Check your work. Your whole program should be 35 lines long and look like this:

    import requests
    import json
    
    def list_people(people):
        print(people)
        return people
    
    def update_people(people):
        try:
            response = requests.get("http://localhost:8000/people")
            people = response.json()["data"]
            print("successfully updated people.")
        except requests.exceptions.ConnectionError:
            print("Server is not running. Failed to update.")
        return people
    
    def clear_people(people):
        people = None
        return people
    
    if __name__ == "__main__":
        people = None
        choices = ["list", "update", "clear", "exit"]
        while True:
            choice = input(f"Please choose an option [{', '.join(choices)}]: ")
            if choice == "list":
                people = list_people(people)
            elif choice == "update":
                people = update_people(people)
            elif choice == "clear":
                people = clear_people(people)
            elif choice == "exit":
                break
            else:
                print("Invalid choice. Please try again.")
    
  8. Run the program by typing python menu.py and pressing ++enter++.

  9. Type "list" and press ++enter++. You should see "None" printed to the screen. We haven't updated our people yet!

  10. Type "update" and press ++enter++. You should see "Server is not running. Failed to update.". We didn't run our server!

  11. Open a new terminal by clicking the plus icon.

  12. Type python manage.py runserver and press ++enter++.

  13. Flip back over to your other terminal by using the dropdown menu.

  14. Type "update" and press ++enter++. You should see "successfully updated people."

  15. Type "list" again and press ++enter++. You should see your people print out.

  16. Type "clear" and press ++enter++.

  17. Type "list" again and press ++enter++. Your people should be cleared.

  18. Type "exit" to exit.

  19. Flip back over to your django terminal with the dropdown menu

  20. Press ++ctrl+c++ to stop the server

Threading

One of the more difficult concepts in programming is threading and multiprocessing. It's rarely taught at an intro level but it's fairly easy to use.

A program runs in a "thread". When you run python menu.py it creates one thread that executes all code in order. Code at the end of your file can't run before code at the beginning of your file.

...unless it could. What if you have a super slow internet connection and you need to make an api call? You don't want it to slow down your whole menu.

Here's the idea: we tell our computer to make the slow call to our API in the background and continue letting the user mess with the menu.

Let's make that a reality by simulating a slow internet connection.

  1. Open views.py

  2. For this part we're going to need the time library. "time" lets us pause code execution for a bit, simulating a slow internet. At the top of views.py add the following:

    import random
    import time
    from django.shortcuts import render
    from django.http import JsonResponse
    
  3. Jump to the very bottom and add the highlighted code:

    def api(request):
        people = [
            {"first name" : "Jim", "last name": "Fowler", "age": 24},
            {"first name" : "Bob", "last name": "Jones", "age": 36},
            {"first name" : "Alice", "last name": "Appleseed", "age": 52}
        ]
    
        return JsonResponse({"data": people})
    
    def slow_api(request):
        people = []
        first_names = ["Liam", "Noah", "Oliver", "William", "Olivia", "Emma", "Ava", "Sophia", "Isabella"]
        last_names = ["Smith", "Johnson", "Anderson", "Brown", "Garcia", "Miller", "Martinez", "Chavez"]
        num_people = random.randint(1,100)
    
        for person in range(num_people):
            time.sleep(.1)
            first_name = random.choice(first_names)
            last_name = random.choice(last_names)
            age = random.randint(1,100)
            people.append({"first_name": first_name, "last_name": last_name, "age": age})
    
        return JsonResponse({"data": people})
    
  4. Save with ++ctrl+s++

  5. We need to add a URL, open urls.py

  6. Add the highlighted code:

    from django.urls import path
    from . import views
    
    urlpatterns = [
        path('', views.index),
        path('weather/', views.weather),
        path('people/', views.api),
        path('slow/', views.slow_api),
    ]
    
  7. Save with ++ctrl+s++

  8. Open a new terminal by clicking the plus icon.

  9. Type python manage.py runserver and press ++enter++.

  10. Flip back over to your other terminal by using the dropdown menu.

  11. Navigate to http://localhost:8000/slow/ to see your api results. Notice how long the page takes to load.

  12. Let's try our menu.py with the new API. Open menu.py and update line 10:

    def update_people(people):
    try:
        response = requests.get("http://localhost:8000/slow")
        people = response.json()["data"]
        print("successfully updated people.")
    
  13. Save with ++ctrl+s++

  14. Open a new terminal by clicking the plus icon

  15. Type python menu.py and press ++enter++

  16. Type "list" and press ++enter++. You should see None

  17. Type "update" and press ++enter++. Notice the delay. While we're waiting for our api to respond our menu won't respond to any thing we type.

  18. Type "exit" to exit.

  19. Let's add the magic that will unblock our menu during an API call. At the top of menu.py add the following:

    import requests
    import json
    from concurrent.futures import ThreadPoolExecutor
    
  20. We need to modify our main program to call the update_people function in a different thread. To do this we need to keep track of a "future" object.

    A future is something that hasn't finished executing yet. We don't know when it will finish but we assume that it will.

    if __name__ == "__main__":
        people = None
        future = None
        choices = ["list", "update", "clear", "exit"]
        while True:
            choice = input(f"Please choose an option [{', '.join(choices)}]: ")
            if choice == "list":
                future, people = list_people(future, people)
            elif choice == "update":
                future = ThreadPoolExecutor().submit(update_people, people)
            elif choice == "clear":
                people = clear_people(people)
            elif choice == "exit":
                break
            else:
                print("Invalid choice. Please try again.")
    
  21. We need to modify the list_people function to handle a future. This ensures that new people will print after an update. Change the following:

    def list_people(future, people):
    if future is not None and future.done():
        people = future.result()
        future = None
    print(people)
    return (future, people)
    
  22. Save with ++ctrl+s++

  23. Your menu.py should look like this:

    import requests
    import json
    from concurrent.futures import ThreadPoolExecutor
    
    def list_people(future, people):
        if future is not None and future.done():
            people = future.result()
            future = None
        print(people)
        return (future, people)
    
    def update_people(people):
        try:
            response = requests.get("http://localhost:8000/slow")
            people = response.json()["data"]
            print("successfully updated people.")
        except requests.exceptions.ConnectionError:
            print("Server is not running. Failed to update.")
        return people
    
    def clear_people(people):
        people = None
        return people
    
    if __name__ == "__main__":
        people = None
        future = None
        choices = ["list", "update", "clear", "exit"]
        while True:
            choice = input(f"Please choose an option [{', '.join(choices)}]: ")
            if choice == "list":
                future, people = list_people(future, people)
            elif choice == "update":
                future = ThreadPoolExecutor().submit(update_people, people)
            elif choice == "clear":
                people = clear_people(people)
            elif choice == "exit":
                break
            else:
                print("Invalid choice. Please try again.")
    
  24. Now run python menu.py

  25. Type list and press ++enter++. Notice how there's nothing in the list.

  26. Type update and press ++enter++. Notice the menu returns instantly.

  27. In a moment you'll see "successfully updated people" print. Type list and press ++enter++.

  28. Type clear and press ++enter++

  29. Now type update and press ++enter++ and quickly type list and press ++enter++ before it updates. Notice you can interact with the menu before the result returns!

  30. You've successfully written a multithreaded program. Type exit to exit.

  31. Flip back to your django server terminal with the dropdown

  32. Press ++ctrl+c++ to stop the server.