Python course Β· Module 2: Data Structures
Project: Safari Species Tracker
In this lesson8
Welcome! After weeks on the trail your journal is bursting at the seams, and the team keeps asking: which animals are endangered, what is the heaviest, have we seen a zebra yet? Searching the notes by hand takes hours, while a program answers in a fraction of a second. It is time to build the Safari Species Tracker, a tool that will bring together, step by step, everything you have learned in this module.
Step 1: The Species Card
We describe every species with the same fields, like a card in the catalog of discoveries. A dictionary is perfect for this, because every piece of information has a readable name:
1lion = {
2 "name": "Lion",
3 "category": "mammal",
4 "weight_kg": 190,
5 "endangered": True,
6 "sightings": 5
7}We write the keys in English, like variable names, while the values can be in any language. The most important thing is that every card has the same keys, because then the code can safely reach for ["weight_kg"] on any species. If one card were missing that field, the program would stop with a KeyError.
Step 2: The Species Database
We build the database as a list of dictionaries, in which the order of the cards follows the order of discoveries:
1database = [
2 lion,
3 {"name": "Zebra", "category": "mammal", "weight_kg": 350, "endangered": False, "sightings": 12},
4 {"name": "Flamingo", "category": "bird", "weight_kg": 3, "endangered": False, "sightings": 25},
5 {"name": "Crocodile", "category": "reptile", "weight_kg": 450, "endangered": False, "sightings": 7}
6]
7print(database[1]["name"]) # ZebraRead database[1]["name"] from the left: the list index picks the second card, and the dictionary key picks the name from it. You add a new species with the familiar append() method:
1database.append({"name": "Elephant", "category": "mammal", "weight_kg": 5000,
2 "endangered": True, "sightings": 3})
3print(len(database)) # 5The elephant landed at the end of the database, and the earlier cards stayed in their places.
Step 3: Reports
Now the database starts answering questions. A list comprehension goes through all the cards and takes what you need from each one, while the if condition keeps only the matching cards:
1names = [s["name"] for s in database]
2endangered = [s["name"] for s in database if s["endangered"]]
3print(names) # ['Lion', 'Zebra', 'Flamingo', 'Crocodile', 'Elephant']
4print(endangered) # ['Lion', 'Elephant']The variable s is each card from the database in turn. Both lists are new, so the database did not change at all. You calculate statistics the same way: the sum() function adds up the numbers from a list, and len() gives the number of cards.
1total_sightings = sum([s["sightings"] for s in database])
2average_weight = sum([s["weight_kg"] for s in database]) / len(database)
3print(total_sightings) # 52
4print(average_weight) # 1198.6The average weight is almost 1200 kg, because the elephant pushes the result up a lot. The question about animal categories concerns unique values, so the answer is a set. All you need is to swap the square brackets for curly braces:
1categories = {s["category"] for s in database}
2print(sorted(categories)) # ['bird', 'mammal', 'reptile']Three mammals gave a single 'mammal' entry in the set. A set has no order, which is why I passed it through the sorted() function for printing, since it returns an ordered list.
Step 4: A Function as the Key
The question about the heaviest animal requires comparing the cards by weight. The max(), min() and sorted() functions accept a key parameter, which is a function that gives, for each card, the value to compare. The shortest way to write it is a lambda, a one-line function without a name. Read lambda s: s["weight_kg"] like this: take the card s and hand back its weight.
1heaviest = max(database, key=lambda s: s["weight_kg"])
2lightest = min(database, key=lambda s: s["weight_kg"])
3print(heaviest["name"]) # Elephant
4print(lightest["name"]) # FlamingoThe max() function compares the weights, but it returns the elephant's whole card, not just the weight. You could write the same lambda as an ordinary function, def get_weight(s): return s["weight_kg"], and pass it as key=get_weight, without parentheses. It works identically, but a lambda does not clutter the program with a name used only once, which is why I recommend it for simple keys.
Step 5: Sorting
The same key sorts the whole database, and the reverse=True parameter orders the cards starting from the heaviest:
1by_weight = sorted(database, key=lambda s: s["weight_kg"], reverse=True)
2ranking = [s["name"] for s in by_weight]
3print(ranking) # ['Elephant', 'Crocodile', 'Zebra', 'Lion', 'Flamingo']The sorted() function returned a new list, so database still holds the cards in the order of discoveries. The database.sort() method with the same key would work too, but it would overwrite that history. And what if you want to group the animals by category and order each group from the lightest? The key can return a tuple, and Python compares tuples element by element: first the category, and in case of a tie, the weight.
1grouped = sorted(database, key=lambda s: (s["category"], s["weight_kg"]))
2order = [s["name"] for s in grouped]
3print(order) # ['Flamingo', 'Lion', 'Zebra', 'Elephant', 'Crocodile']The bird, the mammals and the reptile follow the alphabetical order of their categories, and the three mammals lined up from the lion to the elephant. The tuple, which you know as an unchangeable record, works here as a two-level sorting key.
Step 6: Searching
The team asks whether the zebra is already in the database. The function below goes through the cards one by one and returns the first match. The string method lower() turns letters into lowercase, so "zebra" and "Zebra" mean the same thing.
1def find_species(database, name):
2 for species in database:
3 if species["name"].lower() == name.lower():
4 return species
5 return NoneWhen return hands back a card, the function ends immediately. Only when the loop goes through the whole database without a match do we return None. We pass the database as a parameter, so the function will work with any list of cards.
Step 7: The Sighting Log
Every sighting in the field is a pair: a species and a place. Such an entry should not change, so we store it as a tuple, and the whole day as a list of tuples. Writing x += 1 is a shortcut for x = x + 1:
1log = [("Zebra", "waterhole"), ("Lion", "savanna"), ("Zebra", "river")]
2for animal, place in log:
3 found = find_species(database, animal)
4 if found is not None:
5 found["sightings"] += 1
6print(database[1]["sightings"]) # 14The loop unpacks each tuple into animal and place, and the is not None condition lets through only the species from the database. The card you find is the same dictionary that sits in the database, not a copy, which is why the zebra now has 14 sightings instead of 12. The cards change, not the tuples: the log entries stay exactly as the tracker wrote them down.
At the end of the day I ask the team: which species from the database did we not see today? That is the difference of two sets, and a comprehension unpacks a tuple just like a loop does:
1seen_today = {animal for animal, place in log}
2all_species = {s["name"] for s in database}
3print(all_species - seen_today) # {'Flamingo', 'Crocodile', 'Elephant'}The zebra appeared in the log twice, but it is in the seen_today set only once. The - operator left the species that are not in the log, and those are the ones the trackers will look for tomorrow.
Step 8: The Menu
The last piece is the menu, which lets the whole team use the tracker. The while True loop asks for a choice with input(), and break ends the program:
1while True:
2 choice = input("1 - endangered, 2 - search, 0 - quit: ")
3 if choice == "1":
4 print(endangered)
5 elif choice == "2":
6 name = input("Species: ")
7 print(find_species(database, name))
8 elif choice == "0":
9 breakWe compare the choice with the text "1", not with a number, because input() always returns a string. Every option calls something you already have, so the menu stays short, and another option, such as sorting, is simply a new elif branch.
In the tasks you are about to get, you will build the tracker step by step and then send the finished program to your mentor. Remember: a good tracker comes down to well-chosen structures, a dictionary for the card, a list for the database, a tuple for a field entry and a set for questions about unique species.
Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. Which operation modifies the original list?
2. What happens when you use dict['key'] and the key doesn't exist?
3. Which data structure would you choose to store unique elements?
Hands-on tasks in the game
- Code editor
Create a dictionary called 'species_template' with keys: name, category, weight, dangerous, habitat
- Code editor
Create a list called 'species_list' with 3 species. Each species is a dictionary with the same keys as species_template: name, category, weight, dangerous, habitat
- Code editor
Write a function find_species(database, name) that goes through the list of dictionaries database and returns the dictionary of the species with the given name, or None when there is no such species
- Horizontal ordering
Arrange the elements in the correct order:
- Code editor
Create a list species_list with 3 species (dictionaries with the keys name and weight), sort it with sorted() and the key parameter from the lightest species by the 'weight' key, and store the result in the variable by_weight
- Code editor
Create a complete Safari Species Tracker: a list of species dictionaries, at least 3 functions (def) - adding a species (append), searching by name and sorting by weight (sorted) - and a menu in a while loop that reads the choice with input(); option 0 ends the program
- Horizontal ordering
Arrange the elements in the correct order:
- Vertical ordering
Arrange the Bubble Sort algorithm steps in order: