Struktury danych

Dotychczas dane programu, np. wyniki obliczeń przechowywane były w zmiennych. Często jednak trzeba zastosować bardziej skomplikowaną strukturę danych.

W skrócie

Struktura danych umożliwia przechowywanie na raz więcej niż jednej wartości

Tablica

Tablica, zwana też zmienną indeksowaną przechowuje jednocześnie wiele zmiennych pod odpowiednimi numerami. Ilustruje to przykład poniżej:

Mamy tu trzy zmienne, każda przechowuje jeden napis. Mamy też jedną tablicę a w niej trzy napisy. Domyślacie się zapewne, że im więcej napisów, tym większa wygoda z korzystania z tablicy. Największą korzyścią, jaką oferuje tablica to dostęp do elementu o numerze a priori nie znamym.

Skrypt ten pokazuje jeszcze jedną ważną regułę:

W skrócie

Indeksy tablic zaczynają się od 0; N-elementowa tablica przyjmuje indeksy od 0 do N-1 włącznie

Numer wartości (czyli indeks do tablicy) jest najczęściej przechowywany w zmiennej (powyżej jest to zmienna k). Wartość ta może być np wczytana z klawiatury bądź obliczona. Niestety nie ma sposobu odczytania wartości zmiennej zmienna1 wiedząc, że k = 1. Jedynym rozwiązaniem tego problemu jest właśnie tablica. Można napisać zmienne[k] ale nie można skleić napisu zmienna z k aby dostać się do wartości "a".

Uwaga

Python oferuje specjalną konstrukcję pętli for, która biegnie po wszystkich elementach tablicy, bez konieczności tworzenia zmiennej będącej indeksem pętli:

tablica = [1, 2, 3, 4]
for element in tablica:
    print(element)

Pętlę taką (dla odróżnienia) nazywamy pętlą for each. Program działa nieco szybciej a zapis taki jest czytelniejszy. Dla skrócenia zapisu elementy po której biegnie pętla możemy wpisać od razu w samej pętli:

for element in ["Adam", "Jola","Ada"]:
    print(element)

Co ciekawe, w Pythonie możliwe jest jednoczesne iterowanie po elemencie tablicy i jej indeksie! Zagadnienie to zostanie omówione później.

Kolejny przykład losuje ośmioliterowe hasło, korzystając z tablicy znaków:

Skrypt ten pokazuje podstawowe elementy pracy z tablicą:

  • indeks tablicy może być wynikiem obliczeń; tu w linii 6 losujemy wartość rzeczywistą od 0 do 1 i mnożymy ją przez liczbę liter w tablicy; rozmiar tablicy sprawdzamy poleceniem len(), np. len(litery) (oznaczmy go jako N); ostatecznie r w linii 6 należy do przedziału [0,N)

  • indeks tablicy musi być liczbą całkowitą; ponieważ r jako wynik losowania jest liczbą rzeczywistą, musimy ją zamienić na całkowitą; służy do tego instrukcja int()

  • teraz już możemy odczytać i-tą literę instrukcją litery[int(r)] i dokleić ją do hasła

Totolotek

W tym dodatkowym przykładzie zobaczysz, jak bardzo tablice ułatwiają programowanie.

Lista

Niestety, w Pythonie tablice nie istnieją! 1

Na szczęście wszystko co napisałem powyżej jest poprawne. W Pythonie istnieją bowiem listy. Czym jest zatem lista i czym się różni się od tablicy? Przede wszystkim tym, że do listy (jak do … prawdziwej listy, np zakupów) można coś dopisać na jej końcu. Służy do tego instrukcja append(), która jest metodą listy. Zauważ też, że pętla automatycznie dostosowuje zakres iteracji do rozmiaru listy. Jeżeli lista się powiększy, to pętla wykona więcej przebiegów. W poniższym przykładzie znów wykorzystano pętlę for each.

W skrócie

Lista to taka tablica, do której można dodawać nowe elementy

W praktyce często tworzymy pustą listę a nastepnie zapełniamy ją odpowiednią zawartością, np. tak tworzymy listę 10 liczb od 0 do 9 włącznie:

Ćwiczenie 1

Napisz program, który wstawia do tablicy 5 kolejnych liczb nieparzystych

Wyrażenie listowe

Zapis taki jest jednak dość długi. Python oferuje specjalne wyrażenia listowe (ang. list comprehension), która odwzorowuje jedną listę na drugą. W praktyce możemy je wykorzystać do stworzenia nowej listy, jak poniżej:

Wyrażenie listowe operuje na podanej liście (w powyższym przykładzie - wygenerowanej instrukcją range()) i dla każdego jej elementu oblicza nową wartość, wyniki zaś zapisuje w nowej liście. Pierwsza linia poniższego skryptu jedynie kopiuje wartości z jednej listy do drugiej, druga zaś kopiuje na nową listę dwukrotności elementów pierwszej listy (bo i*2)

Ćwiczenie 2

Napisz program, który wstawia do tablicy 10 kolejnych liczb nieparzystych, wykorzystując wyrażenia listowe

Ćwiczenie 3

Napisz program, który wstawia do tablicy 20 liczb losowych, wykorzystując wyrażenia listowe. Dopisz w tym celu odpowiednie instrukcje w linii 4 w edytorze poniżej:

Tuple (krotki)

W skrócie

Krotka to taka tablica stworzona tylko do odczytu

W odróżnieniu od listy, którą deklarujemy za pomocą nawiasów kwadratowych (np. l = []), tuplę tworzymy stosując nawiasy okrągłe, jak poniżej:

Polecenia z linii 4 i 7 nie zadziałają, ponieważ krotka jest strukturą danych immutable - raz utworzona nie może już być modyfikowana. Z drugiej strony tuplę można rozpakować, czyli powstawiać jej pola do oddzielnych zmiennych. Pokazano to w linii 11. W odróżnieniu od krotek, nie można rozpakować listy. Krotka przydaje się np. wtedy, kiedy chcemy aby nasza funkcja zwróciła więcej niż jedną wartość. (Wykorzystaliśmy to podczas poprzedniego wykładu)

Tuple można rozpakowywać nawet w pętli! Ilustruje to poniższy przykład, w którym pętla for each biegnie po dwuelementowych krotkach zawartych w liście:

Spodziewalibyśmy się w takim przypadku zapisu for krotka in krotki:. Zmienna o nazwie krotka zawierała by w tym przypadku kolejne elementy listy - krotki dwuelementowe. W przykładzie poniżej jest jednak inaczej: mamy zapis for k,l in krotki:. Zmienne k oraz l to składowe każdej z tupli. Tuple jako takie nie pojawiają się w żadnej zmiennej. Innymi słowy: w przypadku pętli po tuplach, zmienna krotka w pierwszym przebiegu pętli przyjmie wartość (0,4) a w drugim (1,3). W przypadku tej drugiej pętli w pierwszej iteracji k=0 a l=4, w drugiej iteracji k=1 a l=3. Zauważ też, że program powyższy wykorzystuje wyrażenie listowe do stworzenia listy krotek a także formatowanie wg składni printf.

Słownik (mapa)

W skrócie

Słownik to taka tablica, której indeksami (kluczami) mogą być dowolne obiekty, np. napisy

W powyższych przykładach pracowaliśmy na tablicach, w których wartości mogły być dowolnego typu. Indeksy jednak zawsze muszą być liczbami naturalnymi. Czasem jednak chcemy powiązać obiekty z innymi obiektami, a nie z liczbami. Odpowiednią do tego celu strukturą danych jest słownik, który wiąże klucze z wartościami jak w przykładzie poniżej:

Zauważ, że nowy słownik tworzymy używając nawiasów klamrowych, tj. slownik = {}, do wartości słownika dostajemy się nawiasami kwadratowymi, np. slownik["klucz"]. Wartości do słownika dodawać można następująco: slownik["inny_klucz"] = "coś jeszcze" Klucze słownika muszą być unikalne, tzn nie mogą się powtarzać. Próba wstawienia różnych wartości pod ten sam klucz skutkuje nadpisaniem; w słowniku pozostanie ostatnio wstawiona wartość. Poniższy program oblicza ile razy pojawiła się każda z liter w napisie wejściowym:

Przykład pokazuje też, jak sprawdzić, czy dany klucz jest w słowniku - odbywa się to w linii 5 (wartość in słownik). A dokładniej - w linii piątej sprawdzamy, czy klucza nie ma w słowniku. Jak nie ma, to go dodajemy, kojarząc go z wartością 0.

Operator in

Próba wyjęcia ze słownika wartości, której tam nie włożono, kończy się wyjątkiem (błędem) i awaryjnym zakończeniem programu. Dlatego zawsze należy sprawdzać, czy słownik zawiera podany klucz (if klucz in slownik) jak wyżej. Operator logiczny in zwraca prawdę, jeżeli pewien element znajduje się w podanej strukturze danych. Operator ten stosuje się do wszystkich struktur danych, nie tylko do słowników. W przypadku słowników można skorzystać z funkcji get(), którą omówiono poniżej.

W linii 10 mamy pętlę, która biegnie po kluczach słownika, następnie w linii 11 pobieramy i drukujemy wartości skojarzone z kluczami. Przypomnijmy: w tym przypadku kluczami są litery a wartościami - ile razy dana litera się pojawiła. Zapis z linii 10, 11 przypomina pracę z listą. W przypadku słowników możemy pobrać parę klucz - wartość w jednym kroku, co pokazano w liniach 13, 14.

Ćwiczenie 3

Powyższy skrypt zlicza też znaki spacji. Dodaj instrukcję warunkową tak, aby tego nie robił

Jeżeli wykonałaś/wykonałeś powyższe ćwiczenie, to widzisz już, że tym sposobem na jeden znak, który chcesz pominąć, potrzebujesz napisać jedną linię if. Może to być kłopotliwe, gdybyśmy np. chcieli ignorować też znak nowej linii i wszystkie znaki interpunkcyjne. W końcu interesują nas tylko litery … Można ‘niechciane’ znaki zgromadzić w liście i ponownie skorzystać z instrukcji coś in wczymś, jak pokazano poniżej:

Tetrapeptydy

Słowniki bardzo się przydają do zliczania napisów. W tym dodatkowym przykładzie policzysz, jaki czteroaminokwasowy fragment najczęściej pojawia się w białkach.

Lista i słownik to obiekty

Lista i słownik to obiekty. Pojęcie programowania obiektowego zostanie wprowadzone na jednym z kolejnych wykładów rozdziału). Na aktualnym etapie wystarczy powiedzieć, że obiekt (np lista) to tak nietypowa “zmienna” która ma powiązane z nią funkcje. Funkcje te wywołujemy pisząc nazwę listy (czy też słownika) a potem po kropce nazwę tej funkcji. Funkcje obiektu nazywamy metodami. Dla przykładu, w linii 12 powyższego przykładu wykorzystano metodę items(), pisząc zliczenia.items(). Metod ta operuje na słowniku zliczenia i zwraca jego klucze oraz wartości jako dwuelementowe tuple, pary klucz - wartość. Zauważ, że w pętli (wciąż linia 12) nastepuje rozpakowanie tych tupli w w kolejnych liniach korzystamy już ze zmiennych litera oraz cnt.

Najważniejsze metody operujące na słowniku:

  • usuwanie wartości ze słownika: slownik.pop(klucz); metoda ta zwraca usuniętą wartość

  • pobieranie listy kluczy znajdujących się w słowniku: slownik.keys()

  • pobieranie listy wartości znajdujących się w słowniku: slownik.values()

  • pobieranie listy dwuelementowych tupli (klucz,wartość): slownik.items()

  • pobieranie wartości skojarzonej z kluczem: to oczywiście można osiągnąć stosując nawiasy kwadratowe: val = slownik[klucz] - ale tylko wtedy, kiedy klucz jest w słowniku; próba wyjęcia wartości dla niezarejestrowanego klucza skutkuje błędem (wyjątkiem). Dlatego najlepiej jest skorzystać z metody slownik.get(klucz, default). Jeżeli podanego klucza nie ma w tym słowniku, zostanie zwrócona wartość domyślna.

Złożone struktury danych

Lista list

Struktury danych można zagnieżdzać, np możemy stworzyć listę, która zawiera inne listy:

Strukturę taką nazywamy tablicą dwuwymiarową. Jej elementy wyjmujemy - jak widać powyżej - stosując dwukrotnie nawiasy kwadratowe: lista_list[i][j]. Pamiętajmy bowiem, że konstrukcja taka, to lista list. Pierwszy nawias (indeks i) wyjmuje z “zewnętrznej” listy wewnętrzne, np lista_list[0] to [11,12,13] czyli jest listą trójelementową. Drugi nawias (indeks j) wyjmuje z “wewnętrznej” listy wartość. Pętle te można nieco uprościć, jak pokazano poniżej. Zawsze jednak potrzebna będzie konstrukcja “pętla w pętli”, ponieważ ta struktura danych jest dwuwymiarowa

Słownik list

Analogicznie możemy stworzyć słownik list, czyli dwuwymiarową strukturę danych, w której dowolnym kluczom (w poniższym przykładzie - napisom) przypisujemy listy:

Słownik słowników

Słowniki również możemy zagnieżdżać, co w praktyce działa jak tablica wielowymiarowa indeksowana dowolnymi obiektami, np napisami. W poniższym przykładzie stworzono tablicę wszystkich możliwych wyników gry w Papier, Kamień i nożyce. Kto wygrał? - można się łatwo dowiedzieć, sprawdzając na przykład wygrał["kamień"]["papier"]:

Rozwiązanie wykorzystujące listy wymagałoby przypisania napisom numerów, np że "papier" to 0 a "kamień" to 1.

Ćwiczenie 4

Zmodyfikuj powyższy program tak, aby losowo generował napisy “graczy”, np. "papier" lub "kamień". W tym celu dodaj listę wszystkich trzech możliwości i losuj element z tej listy. Następnie wypisz na ekran wynik losowej gry.

Praca domowa

W poniższym programie zdefiniowano słowniki C, O oraz H, które dla pierwiastków węgiel, tlen oraz wodór definiują odpowiednie masy molowe ("masa") oraz liczby atomowe ("z"), które podają ile elektronów ma każdy atom danego pierwiastka. Dodatkowo w linii 7 podano wzór sumaryczny alkoholu etylowego C2H5OH.

  1. Dlaczego nie można zapisać tej substancji jako etanol = {"C":2, "H":5, "O":1, "H":1}?

  2. Uzupełnij słownik uop (Układ Okresowy Pierwiastów) tak, aby można było sprawdzać masy pierwiastków. Np następująca instrukcja: print(uop["C"]["z"],uop["O"]["masa"]) powinna wydrukować 6 16.0

  3. Oblicz masę molową etanolu oraz oblicz ile elektronów ma w sumie ta cząsteczka. W tym celu napisz pętlę biegnącą po odpowiednich kluczach słownika etanol i sumującą oddzielnie liczby masowe oraz liczby atomowe. Oczekiwane wyniki: suma_m = 46.068, suma_z = 26

Płytkie i głębokie kopie

Instrukcja l1 = [] tworząca listę zwraca tak naprawdę referencję do listy raczej niż listę jako taką.

Referencję można rozumieć jako liczbę, będącą numerem obiektu (a dokładniej numerem komórki pamięci). Numer ten można skopiować, co wcale nie powoduje stworzenia nowej listy. Zmienne l2 i l3 to jedynie kolejne kopie tego samego adresu, wskazującego na listę l1. Przekonać się o tym możemy poprzez:

  • wstawienie liczby do listy l2 (w linii 4), co (jak się okazuje) powoduje też powiększenie list l1 oraz l3

  • wydrukowanie adresów list (w linii 6) wykorzystując funkcję id()

Jak widać, l1, l2 i l3 to jedna i ta sama lista. Powyższy sposób nie nadaje się zatem do tworzenia niezależnych kopii list. Kopie utworzone w powyższy sposób nazywamy płytkimi. Jak zatem zrobić to poprawnie? Aby stworzyć głęboką kopię 2 listy, należy stworzyć zupełnie nową, pustą listę, a następnie przekopiować elementy starej listy do nowej:

Można to zrobić w dwóch liniach (linie 2 i 3) lub w jednej (linia 4) - wykorzystując wyrażenie listowe.

Przypisy

1

tablica dostepna jest jednak poprzez moduł array

2

Python dostarcza moduł copy a w nim - funkcję deepcopy, która automatycznie tworzy głębokie kopie dowolnych struktur danych