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