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); ostatecznierw linii 6 należy do przedziału [0,N)indeks tablicy musi być liczbą całkowitą; ponieważ
rjako wynik losowania jest liczbą rzeczywistą, musimy ją zamienić na całkowitą; służy do tego instrukcjaint()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 metodyslownik.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.
Dlaczego nie można zapisać tej substancji jako
etanol = {"C":2, "H":5, "O":1, "H":1}?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.0Oblicz 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
etanoli 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 listl1orazl3wydrukowanie 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