Python #4 (struktury danych)

Python #4 (struktury danych)
Photo by Point Normal / Unsplash

PRZYDATNE: Zakomentowanie/Odkomentowanie w VS Code - Ctrl + /

W Pythonie struktury danych są potrzebne, ponieważ pozwalają nam efektywnie przechowywać, organizować i przetwarzać informacje.

  1. Organizacja danych - struktury danych (np. listy, krotki, słowniki, zbiory) pomagają logicznie uporządkować informacje. Dzięki temu program jest bardziej czytelny i łatwiej nim zarządzać.
  2. Efektywność - każda struktura danych ma swoje zalety wydajnościowe. Dzięki temu programy mogą działać szybciej i zużywać mniej pamięci. Na przykład:
  • Wyszukiwanie elementu w słowniku jest bardzo szybkie;
  • Usuwanie duplikatów łatwo zrobić używając zbioru.
  1. Elastyczność i łatwość rozbudowy - struktury danych to podstawowe klocki, na których można budować własne rozwiązania.

Podstawowe struktury danych wbudowane

Lista (list)

  • Dynamiczna tablica przechowująca elementy w określonej kolejności.
  • Może zawierać różne typy danych.
  • Przykład listy : liczby = [1, 2, 3]
mojeksiazki = ["Blade Runner", "Fundacja", "Władca Pierścieni", "Forest Gump", "Kłamca", "Amerykańscy Bogowie"]

# Sprawdź co jest pod indeksem 4
print(mojeksiazki[4])
# # fajnie gdy nie mieszamy typów na liście

mojalista =  ["nagolenniki" , "zbroja" , "hełm" , "miecz"]

kiepska_lista = ["Nagolenniki" , 3.14 , 6 , "kot"]
# Dodawanie obiektu do listy
mojalista.append("sztylet")
print(mojalista)           
## Wynik:['nagolenniki', 'zbroja', 'hełm', 'miecz', 'sztylet']

# Dodawanie obiektu inaczej (też na końcu jak append)
mojalista = ["a", "b", "c", "d", "e"]
mojalista += ["white"]
print(mojalista)              # ['a', 'b', 'c', 'd', 'e', 'white']

# Usuwanie obiektu z listy
mojalista.remove("nagolenniki")
print(mojalista)
## Wynik: ['zbroja', 'hełm', 'miecz', 'sztylet']

# Alternatywne do .remove usuwanie obiektu o danym indeksie
lista = ['a', 'b', 'c', 'd', 'b']
del lista[1]              # ['a', 'd', 'b']

# Usuwanie ostatniego obiektu na liście (lub obiektu o określonym indeksie)
mojalista.pop()
print(mojalista)
## Wynik: ['zbroja', 'hełm', 'miecz']

# Rozszerza listę o elementy z innej kolekcji (np. innej listy).
mojalista.extend(["Różdżka" , "Mikstura"])
print(mojalista)
## Wynik: ['zbroja', 'hełm', 'miecz', 'Różdżka', 'Mikstura']

# insert(i, x) Wstawia element x na pozycję i.
mojalista.insert(0 , "Nagolenniki")
print(mojalista)
## Wynik: ['Nagolenniki', 'zbroja', 'hełm', 'miecz', 'Różdżka', 'Mikstura']

# index(x) Zwraca indeks pierwszego wystąpienia elementu x.
print(mojalista.index("hełm"))
## Wynik: 2

# count(x) Liczy, ile razy element x występuje w liście.
print(mojalista.count("hełm"))
## Wynik: 1

# Wyszukiwanie i sprawdzanie w liście
owoce = ["jabłko", "banan", "pomarańcza", "banan"]

# Sprawdź czy element jest w liście
print("banan" in owoce)      #True
print("gruszka" in owoce)    #False
print("gruszka" not in owoce and "banan" in owoce)   #True


# Znajdź pozycję elementu
print(owoce.index("pomarańcza"))    #2
   # lub
pozycja = owoce.index("pomarańcza")    #2
print(pozycja)

# Policz ile razy element występuje
print(owoce.count("banan"))           #2

# Długość listy   len to Funkcja!
print(len(owoce))        #4

# Łączenie list (+)
lista1 = [1, 2]
lista2 = [3, 4]
lista3 = lista1 + lista2  # [1, 2, 3, 4]

# Dodaj do lista1 [10, 11, 12]
lista1.extend([10, 11, 12])    #[1, 2, 3, 10, 11, 12]

# Sortowanie listy:

# ascending
liczby1 = [12, 45, 7, 23, 89, 56, 34, 78, 15, 67, 3]
liczby1.sort()      
print(liczby1)       #[3, 7, 12, 15, 23, 34, 45, 56, 67, 78, 89]

# descending
liczby1.sort(reverse=True)
print(liczby1)       #[89, 78, 67, 56, 45, 34, 23, 15, 12, 7, 3]

# alfabetycznie
imiona = ["Anna", "Bartosz", "Damian", "Celina", "Ewa", "Filip"]
print(sorted(imiona))  # ['Anna', 'Bartosz', 'Celina', 'Damian', 'Ewa', 'Filip']

print(sorted(imiona, reverse = True)) # ['Filip', 'Ewa', 'Damian', 'Celina', 'Bartosz', 'Anna']
  • Metody są „wbudowane” w dany typ danych i pozwalają wykonywać na nim typowe operacje. Np. dla list mamy zestaw metod do dodawania, usuwania, sortowania czy wyszukiwania elementów.

obiekt. metoda()

  • Indeksowanie - Chcąc “wyciągnąć” konkretny element będący częścią listy lub krotki wpisujemy identyfikator w nawiasach kwadratowych po nazwie zmiennej .
    nazwa_zmiennej[indeks]
mojalista =  ["nagolenniki" , "zbroja" , "hełm" , "miecz"]
print(mojalista [1])

# Wynik: "zbroja"
  • Wycinki list (slicing)
Składnia: print(lista[start:stop:step])

start - od którego indeksu zaczynamy (włącznie)

stop - na którym indeksie kończymy (ale jego nie bierzemy)

step - co który element bierzemy (domyślnie 1)

Odwrócenie kolejności obiektów listy poprzez slicing ->[::-1]

Zapamiętaj: lista. reverse() zmienia oryginalną listę, więc jeśli chcesz nową listę, użyj slicingu!

Krotka (tuple)

  • Podobna do listy, ale niemodyfikowalna (immutable).
  • Często używana do przechowywania stałych zestawów danych.
  • Przykład krotki: zwierzęta = ("kot" , "pies" , "chomik")
  • Przykłady:
# Podstawowe tworzenie
krotka = (1, 2, 3)
mieszana = ("tekst", 42, True)

# UWAGA! Jeden element wymaga przecinka
jeden = (42,)  # To krotka
nie_krotka = (42)  # To zwykły int!

# Krotka zagnieżdżona w liście
colors = [
    ("żółty", (255, 255, 0)),
    ("zielony", (0, 255, 0)),
    ("biały", (255, 255, 255)),
    ("czerwony", (255, 0, 0))
]
  • Dostęp do obiektów:
kolory = ("czerwony", "zielony", "niebieski")

print(kolory[0])      # czerwony (przez indeks)
print(kolory[-1])     # niebieski (ostatni)
print(kolory[0:2])    # ('czerwony', 'zielony') - slicing
print(kolory[::-1])   # ('niebieski', 'zielony', 'czerwony')
  • Tylko 2 metody (bo niezmienne!): index i count
# Metody
liczby = (1, 2, 3, 2, 4, 2)
print(liczby.count(2))   #3

print(liczby.index(3))   #2 - znajduje indeks
  • Operacje na krotkach:
# Operacje na krotkach
a = (1, 2, 3)
b = (4, 5, 6)

print(a + b)        # (1, 2, 3, 4, 5, 6) - łączenie
print(a * 2)        # (1, 2, 3, 1, 2, 3) - powtarzanie
print(2 in a)       # True - sprawdzanie
print(len(a))       # 3 - długość

Słownik (dict)

    • Struktura przechowująca dane w parach {"klucz":"wartość"}.
    • Bardzo szybki dostęp do elementów.
    • Zapisujemy w nawiasach {"klucz":"wartość"} - Klucz musi być unikalny, nie jest modyfikowalny (np. nie może być nim lista)!
# Pusty słownik
slownik = {}

# Słownik z danymi
bohaterzy = {"istari" : "Gandalf" ,
             "hobbit1" : "Frodo" ,
             "hobbit2" : "Sam" ,
             "hobbit3" : " Meriadok",
             "hobbit4" : "Peregrin" , 
             "elf" : "Legolas" , 
             "krasnolud" : "Gimli" ,
             "człowiek1" : "Aragorn" ,
             "człowiek2" : "Boromir"
             }

# print(len(bohaterzy)) # 9
print(bohaterzy.get("człowiek2")) #Boromir
print(bohaterzy.get("istari"))   #Gandalf

# Dodawanie 
bohaterzy["elf2"] = "Elrond"
print(bohaterzy) 
# Wynik: {'istari': 'Gandalf', 'hobbit1': 'Frodo', 'hobbit2': 'Sam', 'hobbit3': ' Meriadok', 'hobbit4': 'Peregrin', 'elf': 'Legolas', 'krasnolud': 'Gimli', 'człowiek1': 'Aragorn', 'człowiek2': 'Boromir', 'elf2': 'Elrond'}

# Modyfikowanie
bohaterzy["elf"] = "Galadriel"
print(bohaterzy)     # Zamiast Legolas pojawi się Galadriel pod kluczem elf

# Usuwanie #1
del bohaterzy["elf"]
print(bohaterzy.get("elf"))  #None

# Sprawdzanie czy klucz istnieje
if "elf" in bohaterzy:
    print(True)
else:
    print(False)  #True

  • Najważniejsze metody
Metody słowników można podzielić na kilka grup:
  • modyfikacja: update, pop, popitem, clear, setdefault
  • odczyt: get, keys, values, items
  • tworzenie/kopiowanie: copy, fromkeys
# update - dodaje elementy z innego słownika lub z sekwencji par (klucz, wartość).
bohaterzy.update({"elf3" : "Arwen"})
print(bohaterzy)

# get - wyciąga wartość po kluczu
print(bohaterzy.get("elf"))

# keys - zwraca widok wszystkich kluczy
print(bohaterzy.keys())

# values -zwraca widok wszystkich wartości
print(bohaterzy.values())

# items - zwraca widok par (klucz , wartość)
print(bohaterzy.items())

# pop - usuwa wartość po kluczu
bohaterzy.pop("elf")
print(bohaterzy)

# popitem - usuwa i zwraca ostatnią parę (klucz, wartość)
bohaterzy.popitem()
print(bohaterzy)

# clear – usuwa wszystkie elementy
bohaterzy.clear()
print(bohaterzy)

Zbiór (set)

    • Kolekcja unikalnych elementów, bez gwarancji kolejności.
    • Idealny do usuwania duplikatów czy szybkiego sprawdzania przynależności.

Właściwości zbioru:

  1. Unikalność – każdy element występuje tylko raz
  2. Brak porządku – elementy nie mają ustalonej kolejności (nie ma indeksowania!)
  3. Mutable - można dodawać i usuwać elementy
liczby1 = {1 ,3, 5, 8, 10}
liczby2 = {2, 1, 4, 12, 8, 5, 6}

## Sprawdż czy w zbiorze jest obiekt 6

print(6 in liczby2)  # True
print(6 in liczby1)  # False

## Suma (| lub .union()) lub .update

print(liczby1 | liczby2) # {1, 2, 3, 4, 5, 6, 8, 10, 12}, NIE ZMIENIA ORYGINALNNYCH ZBIORÓW
print(liczby1.union(liczby2)) # {1, 2, 3, 4, 5, 6, 8, 10, 12}, NIE ZMIENIA ORYGINALNNYCH ZBIORÓW

liczby1.update({99, 100})    # {1, 3, 99, 5, 100, 8, 10} MODYF. ORYGINALNY ZBIÓR
print(liczby1)

liczby1 |= liczby2
print(liczby1)         # {1, 2, 3, 4, 5, 6, 8, 10, 12} MODYF. ORYGINALNY ZBIÓR

## Część wspólna (& lub .intersection())

print(liczby1 & liczby2)  # {8, 1, 5}
print(liczby1.intersection(liczby2)) # {8, 1, 5}

## Różnica (- lub .difference())

print(liczby1 - liczby2) # {10, 3}
print(liczby1.difference(liczby2)) # {10, 3}
print(liczby2 - liczby1) # {2, 4, 12, 6}

## Różnica symetryczna - Można powiedzieć, że różnica symetryczna to:
„to, co odróżnia oba zbiory od siebie”, czyli bierzesz oba zbiory, wyrzucasz część wspólną i zostaje Ci to, co jest wyłącznie w jednym lub drugim, ale nie w obu jednocześnie. ^ lub .symmetricdifference

print(liczby1 ^ liczby2) #{2, 3, 4, 6, 10, 12}
print(liczby1.symmetric_difference(liczby2)) #{2, 3, 4, 6, 10, 12}

Nie modyfikują oryginału (tworzą nowy zbiór): Operatory: | , & , - , Metody: .union(), .intersection(), .difference(), .symmetric_difference()
Modyfikują oryginalny zbiór: Modyfikują oryginał (nadpisują istniejący zbiór) Operatory ze znakiem równości: |= , &= , -=, ^= Metody z _update: .update(), .intersection_update(), .difference_update(), .symmetric_difference_update()

Zmiana listy na zbiór i odwrotnie

ksiazki = ["Endymion", "Hobbit", "Fundacja", "Endymion"]
zbior_ksiazek =  set(ksiazki)
ksiazki_bez_powtorzen =  list(zbior_ksiazek)

Ćwiczenie

# Gracz ma ekwipunek dostępny pod zmienną ekwipunek

# ekwipunek = ["tarcza", "miecz", "napój leczący"]

# Dodaj do ekwipunku "pył magiczny"

# Jeśli w ekwipunku jest zarówno "pył magiczny" jak i "napój leczący" to dodaj do ekwipunku
# "większy napój leczący" i usuń wykorzystane składniki

ekwipunek = ["tarcza", "miecz", "napój leczący"]
ekwipunek.append("pył magiczny")

if "pył magiczny" in ekwipunek and "napój leczący" in ekwipunek:
    ekwipunek.remove("pył magiczny")
    ekwipunek.remove("napój leczący")
    ekwipunek.append("większy napój leczący")
print(ekwipunek)

Pomysły na struktury danych w quizie

Zadanie:
  • osobna zmienna / struktura danych do wyników użytkowników -> słownik zawierający "użytkownik": suma pkt; historia odp
  • pytania bez powtórzeń -> set z id wyświetlonych pyt
  • kolejność odpowiedzi losowo -> random.shuffle()
  • przechowujemy odp i pkt w tym samym miejscu / tej samej strukturze -> krotki? zagnieżdżone w liście żeby odp były razem
  • kolejność przechowania punktów za odp i samego wariantu odpowiedzi ma znaczenie. Warto to zaznaczyć przy wyborze struktury danych.
Pomysły:

Słownik nadrzędny (slownik_pytan), w którym są 'podsłowniki', czyli pytania (każde ma identyfikator).

Każde pytanie to też słownik składający się z:

  • "pytanie": "tekst pytania"
  • "odpowiedzi": lista krotek [(odpowiedź1, punkty), (odpowiedź2, punkty), (Odpowiedź3, punkty) <- wtedy odpowiedzi będą powiązane z punktami
  • pytania mają identyfikator (żeby można było ocenić czy pytania o danym identyfikatorze już się pojawiły czy nie) np. "identyfikator" : 1
slownik_pytan ={
pytanie1 = {
    "identyfikator": 1,
    "pytanie": "Gdzie w komórce eukariotycznej znajduje się DNA?",
    "odpowiedzi": [
        ("W jądrze komórkowym", 1),
        ("W błonie komórkowej", 0),
        ("W cytoplazmie", 0)]}
pytanie2 = {
     "identyfikator": 2,
     "pytanie": "Jaką strukturę przestrzenną ma cząsteczka DNA?",
     "odpowiedzi": [
        ("Potrójna spirala", 0),
        ("Podwójna helisa", 1),
        ("Pojedynczy łańcuch, 0)]}
        }
  • Żeby zapewnić brak duplikatów pytań proponuję set z identyfikatorami poszczególnych pytań, które były już wyświetlone w quizie (Nie wiem jak powiązać set z identyfikatorami pytań)
  • Żeby odpowiedzi były w randomowej kolejności - funkcja random.shuffle() na liście krotek. miesza całe krotki razem, więc odpowiedź i punkty dalej będą razem.
  • osobna zmienna / struktura danych do wyników użytkowników - tutaj też sugerowałąbym słownik, który np. zawiera "Imię użytkownika : liczba punktów", "Identyfikator pytania1" : liczba punktów, "Identyfikator pytania2" : liczba punktów itd. LUB można zrobić słownik zagnieżdżony dla porządku: {Identyfikator pytania1: liczba punktów, Identyfikator pytania2: liczba punktów}
  • warto rozważyć liczenie czasu odpowiedzi na poszczególne pytania.