PPy3/Kolekcje

Z Brain-wiki

Kolekcje

Kolekcje to są takie typy danych, które składają się z elementów. Szczególnym przypadkiem, omawianym już wcześniej przy okazji wprowadzenia pętli, są sekwencje - kolekcje uporządkowane. Rozmaite typy kolekcji różnią się sposobem dostępu do elementów, modyfikowalnością - lub przeciwnie, optymalizacją pod względem różnych zastosowań.

  • Szereg typów kolekcji jest ,,wbudowanych" w pythonie - to znaczy, że można z nich swobodnie korzystać bez żadnych dodatkowych zabiegów;
  • niektóre inne są zaimplementowane w modułach biblioteki standardowej - a więc korzystanie z nich wymaga wcześniejszego polecenia import (p. Moduły);
  • ważne dla zastosowań do obliczeń naukowych typy tablicowe są zaimplementowane w bibliotece NumPy, która nie jest częścią biblioteki standardowej, i na ogół np. nie znajdzie się w domyślnym zestawie pakietów typowej dystrybucji Linuxa. Na pewno będzie jednak dostępna jako opcjonalny element instalacji;
  • na każdym typie kolekcji umie działać funkcja len, zwracająca liczbę elementów kolekcji będącej jej argumentem.

Omówimy tu kolejno w skrócie najważniejsze i najbardziej przydatne wg. nas typy kolekcji, z pominięciem tablic NumPy - którym będzie poświęcony osobny rozdział.

Listy

O listach już nieco wiemy. Lista to kolekcja uporządkowana, i modyfikowalna. Elementem listy może być cokolwiek - również inna lista. A nawet ta sama lista - tak, lista może być elementem samej siebie, jednak sztuczka taka nie jest chyba przydatna, choć legalna.

O adresowaniu elementów przez pozycję (lub indeks) oraz o pobieraniu (i podstawianiu do) wycinków już było. Dodamy tu jeszcze parę użytecznych operacji na listach:

L = [0, 1, 3]
L.append(5) # dołączamy element do końca, przedłużając listę
 L == [0, 1, 3, 5]
L.extend(['a', 'b']) # przedłużamy od razu o całą listę dodatkowych elementów
 L == [0, 1, 3, 5, 'a', 'b']
L.insert(2, 'dwa') # pierwszy argument to pozycja, drugi to element wstawiany
 L == [0, 1, 'dwa', 3, 5, 'a', 'b']
x = L.pop() # usuwamy ostatni element i zwracamy go jako wynik
 teraz x == 'b', a L == [0, 1, 'dwa', 3, 5, 'a']
y = L.pop(2) # możemy zrobić to samo, wskazując inną pozycję zamiast ostatniej
 teraz y == 'dwa', a L == [0, 1, 3, 5, 'a']
L.reverse() # odwrócenie porządku elementów
 teraz L == ['a', 5, 3, 1, 0]
L.sort() # uporządkowanie elementów listy
 BŁĄD - nie zadziała, gdyż lista zawiera elementy nieporównywalne (liczby i napis)
L.pop(0) # usuwamy napis, stojący w pozycji 0
L.sort()
 teraz L == [0, 1, 3, 5]

Notację postaci L.append(5) można czytać tak: zastosuj metodę obiektu L, o nazwie append, do liczby 5. Metoda jest to operacja związana z pewnym obiektem (w tym przypadku - listą) - tym, do którego odnosi się nazwa stojąca przed kropką. Metodę można uważać za pewien rodzaj funkcji - takiej , która oprócz ewentualnych argumentów umieszczonych w nawiasach po jej nazwie, ,,wie" jeszcze o tym, z jakiego obiektu została wywołana.

W Pythonie każda dana jest obiektem jakiegoś rodzaju (klasy), nawet liczba; i każdy obiekt posiada właściwe sobie metody (na ogół wyznaczone przez klasę, do jakiej przynależy). Programista może zresztą sam tworzyć klasy obiektów na potrzeby swojego programu. Więcej na ten temat później.

Funkcja list tworzy listę z dowolnej sekwencji (np. z napisu). Wywołanie bez argumentów list() zwraca pustą listę; wywołanie list(s), gdzie s jest napisem, zwraca listę znaków (napisów jednoelementowych), z których składa się napis s. Wywołanie, gdzie argumentem jest lista, zwraca duplikat tej listy - tzn. listę o tych samych elementach (i w tym samym porządku), ale nie tożsamą.

Krotki

Krotki to prawie listy; zasadnicza różnica polega na tym, że krotki są niemodyfikowalne. Podobnie jak napisy - krotki raz stworzonej nie można zmienić, w sensie zmiany jej zawartości (elementów), a tym bardziej - jej długości. Można ją najwyżej zastąpić inną krotką. W związku z tym, krotki pozbawione są metod modyfikujących zawartość, jakie posiadają listy. Inne operacje, jak adresowanie elementów i wycinków, dodawanie (sklejanie) i mnożenie (powielanie) działają analogicznie jak dla list.

Literalne krotki zapisuje się zwykle używając nawiasów okrągłych:

T = (3, 5, 8)

chociaż tak naprawdę, to nawiasy są opcjonalne, a krotkę tworzą przecinki stojące pomiędzy elementami. Pominięcie nawiasów powoduje jednak, że musimy pamiętać o tym jaka jest kolejność operacji, jeżeli literalny zapis krotki jest elementem większego wyrażenia. Zwykle prościej i czytelniej jest użyć nawiasów.

Krotka może oczywiście składać się z jednego elementu, a nawet być pusta:

T1 = 1, # krotka o jednym elemencie - liczbie 1
T0 = () # krotka pusta

w tym ostatnim przypadku nie można się obyć bez nawiasów. Oczywiście tworzenie takich krotek rzadko bywa przydatne; mogą się one jednak pojawiać jako wyniki rozmaitych operacji.

Po co są w ogóle krotki, skoro to tylko jakby ,,słabsze" listy?

  • Czasami warto użyć typu niemodyfikowalnego, aby próba zmiany zawartości kolekcji (np. w wyniku błędu) nie mogła się udać;
  • optymalizacja - krotki są ,,lżejsze" od list, co może mieć znaczenie jeśli potrzebujemy je tworzyć w dużej liczbie;
  • w niektórych przypadkach użycie niemodyfikowalnego typu danych jest konieczne; o tym dalej.

Warto jednak pamiętać, że choć sama krotka jest niemodyfikowalna - to jej elementem może być np. lista, której zawartość może być zmieniona niezależnie od tego, że jest elementem krotki.

Każdą sekwencję (a i pewne inne obiekty) można ,,zrzutować" na krotkę, za pomocą funkcji tuple. Wyrażając się precyzyjniej, wyrażenie tuple(s) jest krotką o tej samej zawartości (i w tym samym porządku) co sekwencja (lub inny obiekt iterowalny) s.

Słowniki

Słownik (dictionary) to taka kolekcja, której elementy - zamiast być uporządkowane - są powiązane z kluczami. Kluczem może być liczba (całkowita lub ułamkowa, napis, ale również dana szeregu innych typów (chociaż nie każdego). Pobranie elementu słownika wiąże się z podaniem odpowiedniego klucza. Klucze w słowniku są niepowtarzalne - a więc każdy z nich jest związany z dokładnie jedną wartością. Wartości za to mogą się powtarzać, i nie podlegają żadnym ograniczeniom co do typu. Zapis literalny słowników posługuje się nawiasami klamrowymi:

slownik = {'jeden' : 1, 'dwa' : 2, 'trzy' : 3}
slownik['dwa']
 2
slownik['cztery']

KeyError                                  Traceback (most recent call last)
<ipython-input-2-22c7b016c2c6> in <module>()
----> 1 slownik['cztery']

KeyError: 'cztery'
slownik['cztery'] = 4
slownik['cztery'] == 4
 True

W notacji literalnej w nawiasach stoją, oddzielone przecinkami, pary klucz-wartość, a klucz od wartości oddziela dwukropek. Nazwa słownik bierze się stąd, że jednym z możliwych zastosowań jest przekład kluczy (tu: słowa oznaczające liczby) na odpowiadające im wartości (tu: liczby). Widzimy też, że próba pobrania wartości odpowiadającej kluczowi, którego w słowniku nie ma, kończy się błędem. Z drugiej strony, w każdej chwili można do słownika dodać następną parę klucz-wartość; analogicznie, można wymienić wartość związaną z kluczem już istniejącym na inną.

Warto podkreślić jeszcze raz, że pary klucz-wartość w słowniku nie charakteryzują się żadnym określonym porządkiem.

Dalsze podstawowe operacje na słowniku to sprawdzenie występowania klucza, oraz iteracja:

'cztery' in slownik # operator `in' sprawdza przynależność
 True
'zero' in slownik
 False
for klucz in slownik:
    print(klucz, slownik[klucz])

jeden 1                                                                                                                              
trzy 3                                                                                                                               
dwa 2                                                                                                                                
cztery 4

Jak widać, w iteracji klucze słownika pojawiają się w porządku, który niekoniecznie jest tym, w którym je dodawano. Należy przyjąć ten porządek za nieprzewidywalny, chociaż nie jest on losowy - jest zdecydowanie deterministyczny. Ale reguły języka nie zobowiązują autorów interpretera do zachowania jakiegokolwiek konkretnego porządku, w szczególności - ma on prawo zmienić się bez ostrzeżenia pomiędzy wersjami interpretera. Gwarantowane jest jedynie to, że każdy klucz zostanie uwzględniony dokładnie jeden raz.

W iteracjach (tzn. pętlach for) bardzo przydatne są kolekcje zwracane przez metody słowników: items, values i (rzadziej przydatna) keys. Zwracają one obiekty ,,listo-podobne"; jeżeli potrzebne są nam one w postaci dosłownych list, to wystarczy zastosować do nich funkcję list; jeżeli natomiast zamierzamy je wykorzystać w pętli for, to nie jest to potrzebne. Zawartość to odpowiednio:

  • slownik.items(): pary (dwuelementowe krotki) postaci (klucz, wartość)
  • slownik.values(): same wartości występujące w słowniku
  • slownik.keys(): same klucze występujące w słowniku.

I znów, porządek elementów nie jest tu określony. Jeżeli jednak pomiędzy wywołaniami tych metod nie dokonamy żadnej operacji zmieniającej zawartość słownika, to porządki te będą zgodne.

Funkcją - konstruktorem słownika jest funkcja dict. Zamienia ona np. sekwencję par (klucz, wartość) w odpowiedni słownik. Jeżeli klucze w tej sekwencji się powtarzają, ,,wygrywa" ostatnie wystąpienie.

Wspomnieliśmy wcześniej, że nie każdy typ wartości może być kluczem słownika. Dokładniej - kluczami w słowniku mogą być tylko wartości typów niemodyfikowalnych, a więc nie listy ani same słowniki. Mogą nimi natomiast być napisy, liczby i krotki. Nota bene: ponieważ liczby ułamkowe należy traktować jako przybliżone, używanie ich jako kluczy słownika na ogół nie jest dobrym pomysłem. Nie ma ograniczeń co do mieszania różnych typów kluczy (ani wartości) w jednym słowniku.

Zbiory

Zbiory w Pythonie mają sens taki, jak zbiory w matematyce (teorii mnogości). Mówiąc po prostu, zbiór jest kolekcją nieuporządkowaną i nie dopuszczającą powtórzeń: dany element albo do zbioru należy, albo nie; nie może należeć dwukrotnie (ani więcej razy). Standardowe zbiory są obiektami modyfikowalnymi.

Zbiór literalny zapisuje się za pomocą nawiasów klamrowych (łatwo odróżnić, że nie chodzi o słownik, gdyż zapis dla zbiorów nie zawiera dwukropków.

Wiąże się z tym jednak jeden problem: czy {} oznacza zbiór pusty, czy pusty słownik? Dość arbitralnie przyjęto, że jednak pusty słownik (notację dla zbiorów wprowadzono później niż dla słowników).

Przykłady:

zbior = {'a', 'b', 'c'}
'a' in zbior
 True
6 in zbior
 False
zbior.add('d')
 zbior == {'a', 'b', 'c', 'd'}
zbior.remove('a')
 zbior == {'b', 'c', 'd'}
innyzb = {1, 2, 'b'}
zbior | innyzb  # suma zbiorów
 {'a', 1, 2, 'c', 'd', 'b'}
zbior & innyzb  # iloczyn (część wspólna) zbiorów
 {'b'}
zbior - innyzb  # różnica zbiorów
 {'a', 'c', 'd'}
zbior ^ innyzb == (zbior | innyzb) - (zbior & innyzb)  # definicja różnicy symetrycznej zbiorów
 True
innyzb <= zbior # pierwszy jest podzbiorem drugiego?
 False
{'b', 'c'} <= zbior
 True
{'b', 'c'} < zbior  # podzbiór właściwy?
 True
# znaki nierówności mogą być skierowane odwrotnie, z odp. zamianą argumentów

Elementy zbiorów podlegają analogicznemu ograniczeniu co do typów danych, co klucze słownika: nie mogą być to obiekty modyfikowalne.

Konstruktorem zbioru jest funkcja set: można jej podać jako argument dowolną kolekcję, a ona zwróci zbiór jej elementów (z dokładnością do ograniczeń co do typów danych tych elementów). Na przykład, typowy chwyt na usunięcie powtórzeń elementów: aby uzyskać uporządkowaną listę znaków, z jaki składa się napis s:

znaki = sorted(set(napis))

Podobnie jak lista ma swój odpowiednik niemodyfikowalny (krotkę), tak niemodyfikowalnym odpowiednikiem zbioru jest zbiór zamrożony (frozenset). Można taki wyprodukować za pomocą funkcji frozenset, analogu funkcji set. Zbiory zamrożone dopuszczają wszystkie te same operacje, co zwykłe zbiory - za wyjątkiem operacji zmieniających skład (jak add i remove).

Ćwiczenia


poprzednie | strona główna | dalej

RobertJB (dyskusja) 16:06, 5 lip 2016 (CEST)