Kurs Python · Moduł 2: Struktury danych
Projekt: Safari Species Tracker
W tej lekcji8
Witaj! Po tygodniach na szlaku Twój dziennik pęka w szwach, a zespół co chwilę pyta: które zwierzęta są zagrożone, co jest najcięższe, czy widzieliśmy już zebrę? Ręczne przeszukiwanie notatek zajmuje godziny, a program odpowie w ułamku sekundy. Czas zbudować Safari Species Tracker, narzędzie, które krok po kroku połączy wszystko, co poznałeś/aś w tym module.
Krok 1: karta gatunku
Każdy gatunek opisujemy tymi samymi rubrykami, jak kartę w katalogu odkryć. Słownik nadaje się do tego idealnie, bo każda informacja ma czytelną nazwę:
1lion = {
2 "name": "Lew",
3 "category": "ssak",
4 "weight_kg": 190,
5 "endangered": True,
6 "sightings": 5
7}Klucze piszemy po angielsku, jak nazwy zmiennych, a wartości mogą być po polsku. Najważniejsze, żeby każda karta miała te same klucze, bo wtedy kod może bezpiecznie sięgać po ["weight_kg"] przy dowolnym gatunku. Gdyby jednej karcie zabrakło tej rubryki, program zatrzymałby się na błędzie KeyError.
Krok 2: baza gatunków
Bazę budujemy jako listę słowników, w której kolejność kart odpowiada kolejności odkryć:
1database = [
2 lion,
3 {"name": "Zebra", "category": "ssak", "weight_kg": 350, "endangered": False, "sightings": 12},
4 {"name": "Flaming", "category": "ptak", "weight_kg": 3, "endangered": False, "sightings": 25},
5 {"name": "Krokodyl", "category": "gad", "weight_kg": 450, "endangered": False, "sightings": 7}
6]
7print(database[1]["name"]) # ZebraZapis database[1]["name"] czytaj od lewej: indeks listy wybiera drugą kartę, a klucz słownika wybiera z niej nazwę. Nowy gatunek dopisujesz znaną metodą append():
1database.append({"name": "Słoń", "category": "ssak", "weight_kg": 5000,
2 "endangered": True, "sightings": 3})
3print(len(database)) # 5Słoń trafił na koniec bazy, a wcześniejsze karty zostały na swoich miejscach.
Krok 3: raporty
Teraz baza zaczyna odpowiadać na pytania. List comprehension przechodzi przez wszystkie karty i z każdej wyciąga to, czego potrzebujesz, a warunek if zostawia tylko pasujące karty:
1names = [s["name"] for s in database]
2endangered = [s["name"] for s in database if s["endangered"]]
3print(names) # ['Lew', 'Zebra', 'Flaming', 'Krokodyl', 'Słoń']
4print(endangered) # ['Lew', 'Słoń']Zmienna s to po kolei każda karta z bazy. Obie listy są nowe, więc baza nie zmieniła się ani trochę. Tak samo policzysz statystyki: funkcja sum() doda liczby z listy, a len() poda liczbę kart.
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.6Średnia waga wynosi prawie 1200 kg, bo słoń mocno podbija wynik. Pytanie o kategorie zwierząt dotyczy unikalnych wartości, więc odpowiedzią jest zbiór. Wystarczy zamienić nawiasy kwadratowe na klamrowe:
1categories = {s["category"] for s in database}
2print(sorted(categories)) # ['gad', 'ptak', 'ssak']Trzy ssaki dały w zbiorze jedną pozycję 'ssak'. Zbiór nie ma kolejności, dlatego do wydruku przepuściłem go przez funkcję sorted(), która zwraca uporządkowaną listę.
Krok 4: funkcja jako klucz
Pytanie o najcięższe zwierzę wymaga porównania kart po wadze. Funkcje max(), min() i sorted() przyjmują parametr key, czyli funkcję, która dla każdej karty podaje wartość do porównania. Najkrócej zapiszesz ją jako lambdę, czyli jednolinijkową funkcję bez nazwy. Zapis lambda s: s["weight_kg"] czytaj: weź kartę s i oddaj jej wagę.
1heaviest = max(database, key=lambda s: s["weight_kg"])
2lightest = min(database, key=lambda s: s["weight_kg"])
3print(heaviest["name"]) # Słoń
4print(lightest["name"]) # FlamingFunkcja max() porównuje wagi, ale zwraca całą kartę słonia, a nie samą wagę. Tę samą lambdę możesz zapisać zwykłą funkcją def get_weight(s): return s["weight_kg"] i podać jako key=get_weight, bez nawiasów. Działa identycznie, ale lambda nie zaśmieca programu nazwą używaną tylko raz, dlatego polecam ją do prostych kluczy.
Krok 5: sortowanie
Ten sam klucz posortuje całą bazę, a parametr reverse=True ułoży karty od najcięższej:
1by_weight = sorted(database, key=lambda s: s["weight_kg"], reverse=True)
2ranking = [s["name"] for s in by_weight]
3print(ranking) # ['Słoń', 'Krokodyl', 'Zebra', 'Lew', 'Flaming']Funkcja sorted() zwróciła nową listę, więc database wciąż trzyma karty w kolejności odkryć. Metoda database.sort() z tym samym kluczem też by zadziałała, ale nadpisałaby tę historię. A jeśli chcesz pogrupować zwierzęta według kategorii, a w każdej grupie ułożyć je od najlżejszego? Klucz może zwrócić krotkę, a Python porówna krotki element po elemencie: najpierw kategorię, a przy remisie wagę.
1grouped = sorted(database, key=lambda s: (s["category"], s["weight_kg"]))
2order = [s["name"] for s in grouped]
3print(order) # ['Krokodyl', 'Flaming', 'Lew', 'Zebra', 'Słoń']Gad, ptak i ssaki stoją w alfabetycznej kolejności kategorii, a trzy ssaki ułożyły się od lwa do słonia. Krotka, którą znasz jako niezmienny zapis, pracuje tu jako klucz sortowania na dwóch poziomach.
Krok 6: wyszukiwanie
Zespół pyta, czy zebra jest już w bazie. Poniższa funkcja przegląda karty po kolei i zwraca pierwszą pasującą. Metoda tekstu lower() zamienia litery na małe, więc "zebra" i "Zebra" znaczą to samo.
1def find_species(database, name):
2 for species in database:
3 if species["name"].lower() == name.lower():
4 return species
5 return NoneGdy return odda kartę, funkcja od razu się kończy. Dopiero gdy pętla przejdzie przez całą bazę bez trafienia, zwracamy None. Bazę przekazujemy jako parametr, więc funkcja zadziała z każdą listą kart.
Krok 7: dziennik obserwacji
Każda obserwacja w terenie to para: gatunek i miejsce. Takiego wpisu nie należy zmieniać, więc zapisujemy go jako krotkę, a cały dzień jako listę krotek. Zapis x += 1 to skrót od x = x + 1:
1log = [("Zebra", "wodopój"), ("Lew", "sawanna"), ("Zebra", "rzeka")]
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"]) # 14Pętla rozpakowuje każdą krotkę na animal i place, a warunek is not None przepuszcza tylko gatunki z bazy. Znaleziona karta to ten sam słownik, który leży w bazie, a nie kopia, dlatego zebra ma teraz 14 obserwacji zamiast 12. Zmieniają się karty, nie krotki: wpisy w dzienniku zostają takie, jak zanotował je tropiciel.
Na koniec dnia zadaję zespołowi pytanie: których gatunków z bazy dziś nie widzieliśmy? To różnica dwóch zbiorów, a comprehension rozpakowuje krotkę tak samo jak pętla:
1seen_today = {animal for animal, place in log}
2all_species = {s["name"] for s in database}
3print(all_species - seen_today) # {'Flaming', 'Krokodyl', 'Słoń'}Zebra pojawiła się w dzienniku dwa razy, ale w zbiorze seen_today jest tylko raz. Operator - zostawił gatunki, których nie ma w dzienniku, i to ich jutro poszukają tropiciele.
Krok 8: menu
Ostatni element to menu, dzięki któremu z trackera skorzysta cały zespół. Pętla while True pyta o wybór przez input(), a break kończy program:
1while True:
2 choice = input("1 - zagrożone, 2 - szukaj, 0 - koniec: ")
3 if choice == "1":
4 print(endangered)
5 elif choice == "2":
6 name = input("Gatunek: ")
7 print(find_species(database, name))
8 elif choice == "0":
9 breakPorównujemy wybór z tekstem "1", a nie z liczbą, bo input() zawsze zwraca napis. Każda opcja wywołuje coś, co już masz, więc menu pozostaje krótkie, a kolejna opcja, na przykład sortowanie, to po prostu nowa gałąź elif.
W zadaniach, które zaraz dostaniesz, zbudujesz tracker krok po kroku, a gotowy program wyślesz mentorowi. Pamiętaj: dobry tracker to dobrze dobrane struktury, czyli słownik na kartę, lista na bazę, krotka na wpis z terenu i zbiór na pytania o unikalne gatunki.
Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Która operacja modyfikuje oryginalną listę?
2. Co się stanie, gdy użyjemy dict['key'], a klucz nie istnieje?
3. Którą strukturę wybierzesz do przechowywania unikalnych elementów?
Zadania praktyczne w grze
- Edytor kodu
Utwórz słownik 'species_template' z kluczami: name, category, weight, dangerous, habitat
- Edytor kodu
Utwórz listę 'species_list' z 3 gatunkami. Każdy gatunek to słownik z kluczami jak w species_template: name, category, weight, dangerous, habitat
- Edytor kodu
Napisz funkcję find_species(database, name), która przegląda listę słowników database i zwraca słownik gatunku o podanej nazwie albo None, gdy takiego gatunku nie ma
- Układanie w poziomie
Ułóż elementy w prawidłowej kolejności:
- Edytor kodu
Utwórz listę species_list z 3 gatunkami (słowniki z kluczami name i weight), posortuj ją funkcją sorted() z parametrem key od najlżejszego gatunku według klucza 'weight' i zapisz wynik w zmiennej by_weight
- Edytor kodu
Stwórz kompletny Safari Species Tracker: listę słowników z gatunkami, co najmniej 3 funkcje (def) - dodawanie gatunku (append), wyszukiwanie po nazwie i sortowanie według wagi (sorted) - oraz menu w pętli while, które pobiera wybór przez input(); opcja 0 kończy program
- Układanie w poziomie
Ułóż elementy w prawidłowej kolejności:
- Układanie w pionie
Uporządkuj kroki algorytmu Bubble Sort: