Struktury danych -------------------------------------- Dotychczas dane programu, np. wyniki obliczeń przechowywane były w zmiennych. Często jednak trzeba zastosować bardziej skomplikowaną strukturę danych. .. admonition:: W skrócie :class: def *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: .. raw:: html
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łę: .. admonition:: W skrócie :class: def *Indeksy tablic zaczynają się od 0; N-elementowa tablica przyjmuje indeksy od 0 do N-1 włącznie* .. raw:: html
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"``. .. admonition:: Uwaga :class: def 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: .. code-block:: python 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: .. code-block:: python 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: .. raw:: html
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 .. admonition:: Totolotek :class: def W :ref:`tym dodatkowym przykładzie` zobaczysz, jak bardzo tablice ułatwiają programowanie. Lista """""""""""""""""""""""""""" *Niestety, w Pythonie tablice nie istnieją!* [#]_ 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*. .. raw:: html
.. admonition:: W skrócie :class: def *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: .. raw:: html
.. admonition:: Ć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: .. raw:: html
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``) .. admonition:: Ćwiczenie 2 Napisz program, który wstawia do tablicy 10 kolejnych liczb nieparzystych, wykorzystując *wyrażenia listowe* .. admonition:: Ć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: .. raw:: html
.. _tuple: Tuple (krotki) """""""""""""""""""""""""""" .. admonition:: W skrócie :class: def *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: .. raw:: html
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 :ref:`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: .. raw:: html
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``. .. _slownik: Słownik (mapa) """""""""""""""""""""""""""" .. admonition:: W skrócie :class: def *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: .. raw:: html
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: .. raw:: html
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: .. admonition:: Operator ``in`` :class: def 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. .. admonition:: Ć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: .. raw:: html
.. admonition:: Tetrapeptydy :class: def Słowniki bardzo się przydają do zliczania napisów. W :ref:`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 :ref:`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: **Lista list** Struktury danych można zagnieżdzać, np możemy stworzyć listę, która zawiera inne listy: .. raw:: html
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 .. raw:: html
.. _slownik_list: **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: .. raw:: html
.. _slownik_slownikow: **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"]``: .. raw:: html
Rozwiązanie wykorzystujące listy wymagałoby przypisania napisom numerów, np że ``"papier"`` to 0 a ``"kamień"`` to 1. .. admonition:: Ć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. .. admonition:: 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`` .. raw:: html
Płytkie i głębokie kopie """""""""""""""""""""""""""" Instrukcja ``l1 = []`` tworząca listę zwraca tak naprawdę *referencję* do listy raczej niż listę jako taką. .. raw:: html
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ę* [#]_ listy, należy stworzyć zupełnie nową, pustą listę, a następnie przekopiować elementy starej listy do nowej: .. raw:: html
Można to zrobić w dwóch liniach (linie 2 i 3) lub w jednej (linia 4) - wykorzystując wyrażenie listowe. .. rubric:: Przypisy .. [#] tablica dostepna jest jednak poprzez moduł ``array`` .. [#] Python dostarcza moduł ``copy`` a w nim - funkcję ``deepcopy``, która automatycznie tworzy głębokie kopie dowolnych struktur danych