PPy3/Kolekcje: Różnice pomiędzy wersjami

Z Brain-wiki
 
(Nie pokazano 18 pośrednich wersji utworzonych przez tego samego użytkownika)
Linia 8: Linia 8:
 
*na każdym typie kolekcji umie działać funkcja <tt>len</tt>, zwracająca liczbę elementów kolekcji będącej jej argumentem.
 
*na każdym typie kolekcji umie działać funkcja <tt>len</tt>, 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ł.
+
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 [[PPy3/NumPy|osobny rozdział]].
  
 
==Listy==
 
==Listy==
Linia 19: Linia 19:
 
L = [0, 1, 3]
 
L = [0, 1, 3]
 
L.append(5) # dołączamy element do końca, przedłużając listę
 
L.append(5) # dołączamy element do końca, przedłużając listę
→ [0, 1, 3, 5]
+
L == [0, 1, 3, 5]
 
L.extend(['a', 'b']) # przedłużamy od razu o całą listę dodatkowych elementów
 
L.extend(['a', 'b']) # przedłużamy od razu o całą listę dodatkowych elementów
→ [0, 1, 3, 5, 'a', 'b']
+
L == [0, 1, 3, 5, 'a', 'b']
L.insert(2, 'dwa') # pierwszy argument to pozycja, drugi to element wstawiany
+
L.insert(2, 'dwa') # pierwszy argument to pozycja, drugi to element wstawiany; dalsze elementy przesuwają się
→ [0, 1, 'dwa', 3, 5, 'a', 'b']
+
L == [0, 1, 'dwa', 3, 5, 'a', 'b']
 
x = L.pop() # usuwamy ostatni element i zwracamy go jako wynik
 
x = L.pop() # usuwamy ostatni element i zwracamy go jako wynik
 
→ teraz x == 'b', a L == [0, 1, 'dwa', 3, 5, 'a']
 
→ teraz x == 'b', a L == [0, 1, 'dwa', 3, 5, 'a']
Linia 36: Linia 36:
 
→ teraz L == [0, 1, 3, 5]
 
→ teraz L == [0, 1, 3, 5]
 
</source>
 
</source>
 +
 +
Oprócz operacji <tt>L.sort()</tt> istnieje jeszcze funkcja <tt>sorted(L)</tt> - różniąca się tym, że pozostawia niezmienioną listę <tt>L</tt>, zamiast tego tworząc '''nową''' listę uporządkowaną, zawierającą te same elementy, co <tt>L</tt>.
  
 
Notację postaci <tt>L.append(5)</tt> można czytać tak: zastosuj ''metodę'' obiektu <tt>L</tt>, o nazwie <tt>append</tt>, 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.
 
Notację postaci <tt>L.append(5)</tt> można czytać tak: zastosuj ''metodę'' obiektu <tt>L</tt>, o nazwie <tt>append</tt>, 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.
Linia 42: Linia 44:
 
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.
 
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.
 
</small></blockquote>
 
</small></blockquote>
 +
 +
Funkcja <tt>list</tt> tworzy listę z dowolnej sekwencji (np. z napisu). Wywołanie bez argumentów <tt>list()</tt> zwraca pustą listę; wywołanie <tt>list(s)</tt>, gdzie <tt>s</tt> jest napisem, zwraca listę znaków (napisów jednoelementowych), z których składa się napis <tt>s</tt>. 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ą.
 +
 +
===Przykład: suma cyfr===
 +
 +
Spróbujmy policzyć sumę cyfr, z jakich składa się zapis liczby naturalnej <tt>n</tt> - a wyrażając się precyzyjniej, sumę liczb reprezentowanych przez cyfry zapisu dziesiętnego liczby <tt>n</tt> (cyfra to znak, a więc w rozumieniu Pythona pewien napis jednoelementowy - a nie liczba; ponadto, ten sam problem można by postawić dla zapisu pozycyjnego o dowolnej podstawie, np. dwójkowego lub szesnastkowego).
 +
 +
Pierwszym rozwiązaniem tego problemu jest takie, jakie można by nazwać arytmetycznym:
 +
 +
  <source lang=python>
 +
  def sumacyfr(n):
 +
      suma = 0
 +
      while n:
 +
          suma += n % 10
 +
          n = n // 10
 +
      return suma
 +
  </source>
 +
 +
Proste: ostatnią cyfrę zapisu liczby <tt>n</tt> (a wyrażając się pedantycznie: odpowiadającą jej liczbę między 0 a 9) otrzymujemy jako resztę z dzielenia <tt>n</tt> przez 10; następnie ,,skracamy" <tt>n</tt> o tę ostatnią cyfrę, zastępując <tt>n</tt> przez jej iloraz całkowitoliczbowy przez 10. I tak do skutku, aż zabraknie cyfr.
 +
 +
Można jednak napisać znacznie krótszą wersję tej funkcji, korzystając z tego, że Python ,,sam" umie wyznaczyć cyfry zapisu dziesiętnego każdej liczby i nie musimy do tego pisać własnego kodu. Robi to funkcja <tt>str(n)</tt> - zastosowana do liczby całkowitej, zwraca ona napis składający się z cyfr dziesiętnych, reprezentujący tę liczbę. Rozwiązanie uproszczone wygląda więc tak:
 +
 +
  <source lang=python>
 +
  def sumacyfr(n):
 +
      suma = 0
 +
      for c in str(n):
 +
          suma += int(c)
 +
      return suma
 +
  </source>
 +
 +
Dalsze uproszczenie otrzymujemy zastępując pętlę tzw. ''wyrażeniem generatorowym'', i korzystając z wbudowanej funkcji <tt>sum</tt>:
 +
 +
  <source lang=python>
 +
  def sumacyfr(n):
 +
      return sum(int(c) for c in str(n))
 +
  </source>
 +
 +
Wyrażenia generatorowe to jakby odwrotny zapis pętli <tt>for</tt>. Notacja ta definiuje obiekt (''generator'') będący odpowiednikiem i uogólnieniem obiektu, jaki produkuje funkcja <tt>range</tt>: jest to ''niby-sekwencja'', która może zastąpić sekwencję (np. listę) w pętli <tt>for</tt>, a także w funkcjach (takich jak <tt>sum</tt> lub <tt>sorted</tt>), które jako argumentu oczekują sekwencji. W odróżnieniu od sekwencji, generator nie ma z góry ustalonego ciągu elementów, można powiedzieć, że produkuje kolejne elementy ,,na żądanie'', np. gdy są potrzebne dla kolejnego obiegu pętli <tt>for</tt>. Wyrażenie generatorowe określa generator na podstawie elementów podawanych przez inny generator (lub sekwencję), poddając je jakiemuś przekształceniu i/lub selekcji:
 +
 +
<source lang=python>
 +
gen = (f(x) for x in elementy if w(x))
 +
</source>
 +
 +
tu: <tt>elementy</tt> to sekwencja lub generator, <tt>w(x)</tt> to warunek decydujący o uwzględnieniu lub pominięciu elementu <tt>x</tt>, a <tt>f(x)</tt> to w ogólności wyrażenie, którego wartość będzie kolejnym elementem podawanym przez generator <tt>gen</tt>.
 +
 +
''Uwaga:'' nawiasy okrągłe otaczające wyrażenie generatorowe są obowiązkowe. Jeśli wyrażenie to wstawiamy bezpośrednio pod wywołanie funkcji jako jej jedyny argument, to wystarczają nawiasy zawarte w składni wywołania funkcji - nie potrzeba ich podwajać. Jeśli w miejsce nawiasów okrągłych napiszemy kwadratowe, otrzymamy nie generator - a listę o tych samych elementach.
  
 
==Krotki==
 
==Krotki==
Linia 71: Linia 119:
  
 
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.
 
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 &mdash; np. generatory) można ,,zrzutować" na krotkę, za pomocą funkcji <tt>tuple</tt>. Wyrażając się precyzyjniej, wyrażenie <tt>tuple(s)</tt> jest krotką o tej samej zawartości (i w tym samym porządku) co sekwencja (lub inny obiekt iterowalny) <tt>s</tt>.
  
 
==Słowniki==
 
==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 odpowiadającego mu 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:
 +
 +
<source lang=python>
 +
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
 +
</source>
 +
 +
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ę w zasadzie określonym porządkiem. Niemniej w nowszych wersjach Pythona obowiązuje zasada, że pozycje słownika zachowują porządek, w jakim były tworzone &mdash; i np. w iteracji będą się pojawiać zgodnie z tym porządkiem.
 +
 +
Dalsze podstawowe operacje na słowniku to sprawdzenie występowania klucza, oraz iteracja:
 +
 +
<source lang=python>
 +
'cztery' in slownik # operator `in' sprawdza przynależność do zbioru kluczy
 +
→ True
 +
'zero' in slownik
 +
→ False
 +
for klucz in slownik:
 +
    print(klucz, slownik[klucz])
 +
 +
jeden 1                                                                                                                             
 +
dwa 2                                                                                                                               
 +
trzy 3                                                                                                                             
 +
cztery 4
 +
</source>
 +
 +
Jak widać, w iteracji klucze słownika pojawiają się w porządku, w którym je dodawano. Takie zachowanie jest przepisane w definicji języka Python od wersji 3.7. Mając do czynienia z wersjami wcześniejszymi należy się liczyć z tym, że ten porządek nie będzie zachowany. Natomiast gwarantuje się, że każdy klucz pojawi się w iteracji dokładnie jeden raz.
 +
 +
W iteracjach (tzn. pętlach <tt>for</tt>) bardzo przydatne są kolekcje zwracane przez metody słowników: <tt>items</tt>, <tt>values</tt> i (rzadziej przydatna) <tt>keys</tt>. Zwracają one obiekty ,,listo-podobne"; jeżeli potrzebne są nam one w postaci dosłownych list, to wystarczy zastosować do nich funkcję <tt>list</tt>; jeżeli natomiast zamierzamy je wykorzystać w pętli <tt>for</tt>, to nie jest to potrzebne. Zawartość to odpowiednio:
 +
 +
*<tt>slownik.items()</tt>: pary (dwuelementowe krotki) postaci ''(klucz, wartość)''
 +
*<tt>slownik.values()</tt>: same wartości występujące w słowniku
 +
*<tt>slownik.keys()</tt>: same klucze występujące w słowniku.
 +
 +
Uwaga dotycząca porządku pozycji słownika stosuje się również do wyników zwracanych przez te metody.
 +
 +
Funkcją - konstruktorem słownika jest funkcja <tt>dict</tt>. 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==
  
 +
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 &mdash; łatwo odróżnić, że nie chodzi o słownik, gdyż zapis dla zbiorów nie zawiera dwukropków.
 +
 +
<blockquote><small>
 +
Wiąże się z tym jednak jeden problem: czy <tt>{}</tt> 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).</small></blockquote>
 +
 +
Przykłady:
 +
 +
<source lang=python>
 +
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
 +
</source>
 +
 +
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 <tt>set</tt>: 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 <tt>s</tt>:
 +
 +
<source lang=python>
 +
znaki = sorted(set(napis))
 +
</source>
 +
 +
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 <tt>frozenset</tt>, analogu funkcji <tt>set</tt>. Zbiory zamrożone dopuszczają wszystkie te same operacje, co zwykłe zbiory - za wyjątkiem operacji zmieniających skład (jak <tt>add</tt> i <tt>remove</tt>).
 +
 +
==Napisy (jeszcze raz)==
 +
 +
O napisach już nieco było wcześniej, ale dla kompletności przypomnijmy.
 +
 +
*Napis to niemodyfikowalna sekwencja
 +
*Jej elementami są znaki - napisy o długości 1
 +
*Operatorem <tt>in</tt> można badać nie tylko, czy dany znak występuje w napisie, ale również czy jeden napis jest fragmentem drugiego:
 +
<source lang=python>
 +
s = 'abc XYZ'
 +
'abc' in s
 +
→ True
 +
</source>
 +
*Napisy mają szereg użytecznych metod; ale ''żadna z tych metod nie zmienia napisu, którego dotyczy'' - może jedynie tworzyć (i zwracać) nowy napis, utworzony na podstawie istniejącego. Przykłady:
 +
<source lang=python>
 +
s.lower()
 +
→ 'abc xyz'
 +
s.upper()
 +
→ 'ABC XYZ'
 +
s.replace(' ', ';')
 +
→ 'abc;XYZ'
 +
s.find('XY') # znajduje pozycję, na jakiej występuje podany fragment w napisie s
 +
→ 4
 +
s.find('q')  # szukamy fragmentu, którego w s nie ma..
 +
→ -1
 +
</source>
 +
*Metod tych jest więcej, poznamy je w przykładach
 +
*Funkcją - konstruktorem napisu jest <tt>str</tt>; np. liczbę - zamienia w napis będący jej zapisem (najbardziej ,,standardowym"), i podobnie z innymi typami danych - zwraca najbardziej ,,standardową" ich reprezentację za pomocą napisu
 +
*Szczególnie ważną i przydatną operacją napisową jest metoda <tt>format</tt>, o niej zaraz.
 +
 +
===Formatowanie===
 +
 +
Idea formatowania jest następująca: mamy pewien napis, który służy jako szablon do tworzenia napisów, w ten sposób, że określone miejsca w szablonie wypełniane są wartościami (a dokładniej - ich przedstawieniami napisowymi), które zazwyczaj nie są z góry znane i są wyliczane dopiero w wyniku działania programu. Najczęściej chodzi po prostu o przedstawienie uzyskanych wyników w postaci komunikatów lub zapisów w plikach, o określonej przez programistę postaci.
 +
 +
Gdy korzystamy z metody <tt>format</tt>, napis pełniący rolę szablonu (literalny lub przywołany poprzez nazwę) stoi przed kropką, tzn. wywołujemy metodę <tt>format</tt> tegoż napisu - szablonu, natomiast argumentami tego wywołania są wartości, które mają być wstawione w przeznaczone na to pozycje w szablonie.
 +
 +
Język opisu szablonów jest stosunkowo złożony i kryje w sobie szereg możliwości, na razie wprowadzimy najprostsze przypadki.
 +
 +
<source lang=python>
 +
# Najprostsze formatowanie: wartości wstawiane są kolejno w miejsca zaznaczone poprzez {}
 +
'x = {}, y = {}'.format(1.2, 9)
 +
→ 'x = 1.2, y = 9'
 +
# Numerowanie pozycji (ten sam numerek może występować więcej niż raz w szablonie
 +
'x1 = {1}, x0 = {0}'.format(3, 15)
 +
→ 'x1 = 15, x0 = 3'
 +
# Obcięcie przedstawienia liczby ułamkowej do określonej liczby cyfr po kropce
 +
'{0:.4f}'.format(3.141592653589793)
 +
→ '3.1416'
 +
# Bardziej szczegółowa kontrola formatowania
 +
'{:4s}{:4s}{:*>4s}'.format('a', 'b', 'c')
 +
→ 'a  b  ***c'
 +
</source>
 +
W tym ostatnim przykładzie, liczby 4 specyfikują ''minimalną'' szerokość pól przeznaczonych na wypełnienie podanymi wartościami, natomiast <tt>*></tt> oznacza, że gwiazdka ma być ,,wypełniaczem" pustego miejsca, jeżeli takie pozostanie w zadanej wielkości pola po wstawieniu wartości, a znak <tt>></tt> oznacza wyrównanie (wewnątrz pola) do strony prawej. Więcej szczegółów - w [https://docs.python.org/3/library/string.html#formatstrings dokumentacji].
 +
 +
Począwszy od wersji 3.6 języka Python wprowadzono uproszczoną składnię formatowania napisów. Poprzedzając w zapisie napisu literalnego otwierający cudzysłów (lub apostrof) literą <tt>f</tt> (lub <tt>F</tt>), możemy w tym napisie skorzystać z sekwencji w parach nawiasów klamrowych, gdzie wewnątrz nawiasów umieszczamy (zamiast jak wyżej &mdash; numeru pozycji w liście argumentów wywołania <tt>format()</tt>) po prostu zapis wyrażenia w Pythonie, którego aktualna wartość znajdzie się w tym miejscu w wyniku ewaluacji, plus ewentualnie dwukropek a po nim dyrektywy formatowania, jak wyżej. Przykłady:
 +
 +
<source lang=python>
 +
x, y = 1.2, 9
 +
f'x = {x}, y = {y}'
 +
→ x = 1.2, y = 9
 +
f'wynik działania arytmetycznego: {2 * x + y}'
 +
→ wynik działania arytmetycznego: 11.4
 +
</source>
 +
Jeżeli chcemy, aby w wyniku wystąpiły dosłowne nawiasy klamrowe, a nie zostały one zinterpretowane jako pole do wypełnienia wartością wyrażenia, to musimy je podwoić (<tt>f'{{'</tt> → { itd.)
 +
 +
=Ćwiczenia=
 +
 +
1. Napisz funkcję, która wśród elementów sekwencji (lub generatora) napisów (drugi argument wywołania) znajdzie i zwróci te, które są permutacjami danego napisu (pierwszy argument), ale nie są z nim identyczne - bez rozróżniania wielkości liter. Przykładowo:
 +
 +
<source lang=python>
 +
permutacje('Three', ['Ether', 'one', 'there', 'two', 'three'])
 +
→ ['Ether', 'there']
 +
</source>
 +
 +
2. Napisz funkcję <tt>czy_palindrom(s)</tt>, która dla napisu <tt>s</tt> stwierdzi, czy jest on palindromem - tzn. czy pisany wspak jest identyczny z oryginałem, i zwróci wynik tego badania (<tt>True</tt> lub <tt>False</tt>); oraz drugą funkcję <tt>palindromy(words)</tt>, która mając daną sekwencję (lub generator) <tt>words</tt> zwróci generator zawierający jedynie palindromy występujące wśród elementów <tt>words</tt>:
 +
 +
<source lang=python>
 +
for word in palindromy(['Ala', 'ma', 'psa', 'Asa']):
 +
    print(word)
 +
 +
Ala
 +
Asa
 +
</source>
 +
 +
3. Napisz funkcję <tt>slowa_podobne(s, words)</tt>, która podobnie jak funkcja z poprzedniego przykładu ,,przefiltruje" sekwencję lub generator <tt>words</tt>, ale pozostawiając w wyniku jedynie napisy ,,podobne" do napisu <tt>s</tt> w tym sensie, że składające się z tych samych znaków &mdash; ale ewentualnie występujących inną liczbę razy (lecz nie identyczne z <tt>s</s>):
 +
 +
<source lang=python>
 +
for word in slowa_podobne('one', ['one', 'two', 'None', 'three', 'neon', 'eon']):
 +
    print(word)
 +
 +
None
 +
neon
 +
eon
 +
</source>
 +
 +
 +
''CDN''
  
 
----
 
----

Aktualna wersja na dzień 11:15, 15 maj 2023

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; dalsze elementy przesuwają się
 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]

Oprócz operacji L.sort() istnieje jeszcze funkcja sorted(L) - różniąca się tym, że pozostawia niezmienioną listę L, zamiast tego tworząc nową listę uporządkowaną, zawierającą te same elementy, co L.

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ą.

Przykład: suma cyfr

Spróbujmy policzyć sumę cyfr, z jakich składa się zapis liczby naturalnej n - a wyrażając się precyzyjniej, sumę liczb reprezentowanych przez cyfry zapisu dziesiętnego liczby n (cyfra to znak, a więc w rozumieniu Pythona pewien napis jednoelementowy - a nie liczba; ponadto, ten sam problem można by postawić dla zapisu pozycyjnego o dowolnej podstawie, np. dwójkowego lub szesnastkowego).

Pierwszym rozwiązaniem tego problemu jest takie, jakie można by nazwać arytmetycznym:

  def sumacyfr(n):
      suma = 0
      while n:
          suma += n % 10
          n = n // 10
      return suma

Proste: ostatnią cyfrę zapisu liczby n (a wyrażając się pedantycznie: odpowiadającą jej liczbę między 0 a 9) otrzymujemy jako resztę z dzielenia n przez 10; następnie ,,skracamy" n o tę ostatnią cyfrę, zastępując n przez jej iloraz całkowitoliczbowy przez 10. I tak do skutku, aż zabraknie cyfr.

Można jednak napisać znacznie krótszą wersję tej funkcji, korzystając z tego, że Python ,,sam" umie wyznaczyć cyfry zapisu dziesiętnego każdej liczby i nie musimy do tego pisać własnego kodu. Robi to funkcja str(n) - zastosowana do liczby całkowitej, zwraca ona napis składający się z cyfr dziesiętnych, reprezentujący tę liczbę. Rozwiązanie uproszczone wygląda więc tak:

  def sumacyfr(n):
      suma = 0
      for c in str(n):
          suma += int(c)
      return suma

Dalsze uproszczenie otrzymujemy zastępując pętlę tzw. wyrażeniem generatorowym, i korzystając z wbudowanej funkcji sum:

  def sumacyfr(n):
      return sum(int(c) for c in str(n))

Wyrażenia generatorowe to jakby odwrotny zapis pętli for. Notacja ta definiuje obiekt (generator) będący odpowiednikiem i uogólnieniem obiektu, jaki produkuje funkcja range: jest to niby-sekwencja, która może zastąpić sekwencję (np. listę) w pętli for, a także w funkcjach (takich jak sum lub sorted), które jako argumentu oczekują sekwencji. W odróżnieniu od sekwencji, generator nie ma z góry ustalonego ciągu elementów, można powiedzieć, że produkuje kolejne elementy ,,na żądanie, np. gdy są potrzebne dla kolejnego obiegu pętli for. Wyrażenie generatorowe określa generator na podstawie elementów podawanych przez inny generator (lub sekwencję), poddając je jakiemuś przekształceniu i/lub selekcji:

 gen = (f(x) for x in elementy if w(x))

tu: elementy to sekwencja lub generator, w(x) to warunek decydujący o uwzględnieniu lub pominięciu elementu x, a f(x) to w ogólności wyrażenie, którego wartość będzie kolejnym elementem podawanym przez generator gen.

Uwaga: nawiasy okrągłe otaczające wyrażenie generatorowe są obowiązkowe. Jeśli wyrażenie to wstawiamy bezpośrednio pod wywołanie funkcji jako jej jedyny argument, to wystarczają nawiasy zawarte w składni wywołania funkcji - nie potrzeba ich podwajać. Jeśli w miejsce nawiasów okrągłych napiszemy kwadratowe, otrzymamy nie generator - a listę o tych samych elementach.

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 — np. generatory) 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 odpowiadającego mu 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ę w zasadzie określonym porządkiem. Niemniej w nowszych wersjach Pythona obowiązuje zasada, że pozycje słownika zachowują porządek, w jakim były tworzone — i np. w iteracji będą się pojawiać zgodnie z tym porządkiem.

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

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

jeden 1                                                                                                                              
dwa 2                                                                                                                                
trzy 3                                                                                                                               
cztery 4

Jak widać, w iteracji klucze słownika pojawiają się w porządku, w którym je dodawano. Takie zachowanie jest przepisane w definicji języka Python od wersji 3.7. Mając do czynienia z wersjami wcześniejszymi należy się liczyć z tym, że ten porządek nie będzie zachowany. Natomiast gwarantuje się, że każdy klucz pojawi się w iteracji 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.

Uwaga dotycząca porządku pozycji słownika stosuje się również do wyników zwracanych przez te metody.

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).

Napisy (jeszcze raz)

O napisach już nieco było wcześniej, ale dla kompletności przypomnijmy.

  • Napis to niemodyfikowalna sekwencja
  • Jej elementami są znaki - napisy o długości 1
  • Operatorem in można badać nie tylko, czy dany znak występuje w napisie, ale również czy jeden napis jest fragmentem drugiego:
s = 'abc XYZ'
'abc' in s
 True
  • Napisy mają szereg użytecznych metod; ale żadna z tych metod nie zmienia napisu, którego dotyczy - może jedynie tworzyć (i zwracać) nowy napis, utworzony na podstawie istniejącego. Przykłady:
s.lower()
 'abc xyz'
s.upper()
 'ABC XYZ'
s.replace(' ', ';')
 'abc;XYZ'
s.find('XY') # znajduje pozycję, na jakiej występuje podany fragment w napisie s
 4
s.find('q')  # szukamy fragmentu, którego w s nie ma..
 -1
  • Metod tych jest więcej, poznamy je w przykładach
  • Funkcją - konstruktorem napisu jest str; np. liczbę - zamienia w napis będący jej zapisem (najbardziej ,,standardowym"), i podobnie z innymi typami danych - zwraca najbardziej ,,standardową" ich reprezentację za pomocą napisu
  • Szczególnie ważną i przydatną operacją napisową jest metoda format, o niej zaraz.

Formatowanie

Idea formatowania jest następująca: mamy pewien napis, który służy jako szablon do tworzenia napisów, w ten sposób, że określone miejsca w szablonie wypełniane są wartościami (a dokładniej - ich przedstawieniami napisowymi), które zazwyczaj nie są z góry znane i są wyliczane dopiero w wyniku działania programu. Najczęściej chodzi po prostu o przedstawienie uzyskanych wyników w postaci komunikatów lub zapisów w plikach, o określonej przez programistę postaci.

Gdy korzystamy z metody format, napis pełniący rolę szablonu (literalny lub przywołany poprzez nazwę) stoi przed kropką, tzn. wywołujemy metodę format tegoż napisu - szablonu, natomiast argumentami tego wywołania są wartości, które mają być wstawione w przeznaczone na to pozycje w szablonie.

Język opisu szablonów jest stosunkowo złożony i kryje w sobie szereg możliwości, na razie wprowadzimy najprostsze przypadki.

# Najprostsze formatowanie: wartości wstawiane są kolejno w miejsca zaznaczone poprzez {}
'x = {}, y = {}'.format(1.2, 9) 
 'x = 1.2, y = 9'
# Numerowanie pozycji (ten sam numerek może występować więcej niż raz w szablonie
'x1 = {1}, x0 = {0}'.format(3, 15)
 'x1 = 15, x0 = 3'
# Obcięcie przedstawienia liczby ułamkowej do określonej liczby cyfr po kropce
'{0:.4f}'.format(3.141592653589793) 
 '3.1416'
# Bardziej szczegółowa kontrola formatowania
'{:4s}{:4s}{:*>4s}'.format('a', 'b', 'c')
 'a   b   ***c'

W tym ostatnim przykładzie, liczby 4 specyfikują minimalną szerokość pól przeznaczonych na wypełnienie podanymi wartościami, natomiast *> oznacza, że gwiazdka ma być ,,wypełniaczem" pustego miejsca, jeżeli takie pozostanie w zadanej wielkości pola po wstawieniu wartości, a znak > oznacza wyrównanie (wewnątrz pola) do strony prawej. Więcej szczegółów - w dokumentacji.

Począwszy od wersji 3.6 języka Python wprowadzono uproszczoną składnię formatowania napisów. Poprzedzając w zapisie napisu literalnego otwierający cudzysłów (lub apostrof) literą f (lub F), możemy w tym napisie skorzystać z sekwencji w parach nawiasów klamrowych, gdzie wewnątrz nawiasów umieszczamy (zamiast jak wyżej — numeru pozycji w liście argumentów wywołania format()) po prostu zapis wyrażenia w Pythonie, którego aktualna wartość znajdzie się w tym miejscu w wyniku ewaluacji, plus ewentualnie dwukropek a po nim dyrektywy formatowania, jak wyżej. Przykłady:

x, y = 1.2, 9
f'x = {x}, y = {y}'
 x = 1.2, y = 9
f'wynik działania arytmetycznego: {2 * x + y}'
 wynik działania arytmetycznego: 11.4

Jeżeli chcemy, aby w wyniku wystąpiły dosłowne nawiasy klamrowe, a nie zostały one zinterpretowane jako pole do wypełnienia wartością wyrażenia, to musimy je podwoić (f'{{' → { itd.)

Ćwiczenia

1. Napisz funkcję, która wśród elementów sekwencji (lub generatora) napisów (drugi argument wywołania) znajdzie i zwróci te, które są permutacjami danego napisu (pierwszy argument), ale nie są z nim identyczne - bez rozróżniania wielkości liter. Przykładowo:

permutacje('Three', ['Ether', 'one', 'there', 'two', 'three'])
 ['Ether', 'there']

2. Napisz funkcję czy_palindrom(s), która dla napisu s stwierdzi, czy jest on palindromem - tzn. czy pisany wspak jest identyczny z oryginałem, i zwróci wynik tego badania (True lub False); oraz drugą funkcję palindromy(words), która mając daną sekwencję (lub generator) words zwróci generator zawierający jedynie palindromy występujące wśród elementów words:

for word in palindromy(['Ala', 'ma', 'psa', 'Asa']):
    print(word)

Ala
Asa

3. Napisz funkcję slowa_podobne(s, words), która podobnie jak funkcja z poprzedniego przykładu ,,przefiltruje" sekwencję lub generator words, ale pozostawiając w wyniku jedynie napisy ,,podobne" do napisu s w tym sensie, że składające się z tych samych znaków — ale ewentualnie występujących inną liczbę razy (lecz nie identyczne z s):

for word in slowa_podobne('one', ['one', 'two', 'None', 'three', 'neon', 'eon']):
    print(word)

None
neon
eon


CDN


poprzednie | strona główna | dalej

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