PPy3/SekwencjeIIteracja
Spis treści
Sekwencje i iteracja
Sekwencje to typ danych charakteryzujący się tym, że składają się z elementów zgromadzonych w pewnej kolejności. Elementem sekwencji może być cokolwiek (w tym - inna sekwencja), ale w najprostszym przypadku elementami tymi są liczby.
Sekwencje to szczególny przypadek kolekcji - takich typów danych, które składają się z elementów, ale niekoniecznie są one uporządkowane. Innym przykładem kolekcji jest zbiór (set).
Najczęściej używanym typem sekwencji jest lista (list). Listę można stworzyć (i zapamiętać) np. nadając jej pewien początkowy skład:
numerki = [0, 1, 2, 3, 5, 8]
To co stoi po prawej stronie znaku równości to literalny zapis listy - ciąg elementów, oddzielonych przecinkami, wewnątrz pary nawiasów kwadratowych. Oczywiście jako elementy mogą występować zarówno stałe literalne - jak w tym przykładzie, jak i nazwy (zmienne) — oczywiście takie, którym uprzednio nadano wartości.
Z listy - podobnie jak z każdej sekwencji - można pobrać element stojący w dowolnej pozycji, podając jej numer:
print(numerki[4])
→ 5
Uwaga: pozycje numerowane są kolejnymi liczbami naturalnymi, począwszy od zera - podobnie, jak w większości języków programowania.
Sięgnięcie po element nie występujący w liście, to znaczy podanie numeru pozycji poza zakresem rozmiaru danej listy, będzie błędem i domyślnie spowoduje przerwanie wykonywania programu.
Można też podmienić istniejący element na inny:
numerki[0] = -1
print(numerki)
→ [-1, 1, 2, 3, 5, 8]
a nawet przedłużyć ją o kolejny element:
numerki.append(13)
print(numerki)
→ [-1, 1, 2, 3, 5, 8, 13]
oraz usunąć element (skracając listę):
del numerki[0]
print numerki
→ [1, 2, 3, 5, 8, 13]
Łatwo też ustalić, jaka jest ,,długość" listy, czyli liczba jej elementów:
print(len(numerki))
→ 6
Jest jeszcze sporo innych ,,gotowych do użytku" operacji na listach. Ale najważniejsza cecha listy jest taka: można zmieniać jej skład, a nawet długość, a pozostaje ona tą samą listą. Nawet jeśli występuje w programie pod więcej niż jedną nazwą.
Pętla for
Naczelne zastosowanie listy (czy ogólniej -sekwencji), to iteracja - czyli wykonanie jakiejś operacji dla każdego elementu. I tak wygląda najprostszy rodzaj iteracji - Pętla for:
for x in numerki:
print(x)
Zamiast print(x) pętlę for może tworzyć dowolny blok instrukcji, zgodnie z zasadami jakie już poznaliśmy przy omawianiu instrukcji warunkowej. Cały blok tworzący wnętrze pętli będzie wykonany wielokrotnie, tyle razy, ile elementów ma lista, a za każdym razem pod nazwę stojącą bezpośrednio po słowie for podstawiony będzie kolejny element listy (przywołanej po słowie in) - kolejny, tzn. zgodnie z kolejnością, w jakiej umieszczone są one w liście.
Inny, znany nam już rodzaj sekwencji to napis (string), zwany czasami łańcuchem znakowym. Napis składa się ze znaków, i jest w szczególności sekwencją, której elementami są znaki. Analogicznie jak w przypadku listy, można pobierać znaki stojące na poszczególnych pozycjach w napisie. Nie można natomiast podmieniać znaków w napisie, ani ich usuwać bądź dopisywać: w odróżnieniu od listy, napis raz utworzony jest niezmienny. Można go zastąpić innym napisem, i ewentualnie opatrzyć ten nowy napis tą samą nazwą, ale nie da się go zmodyfikować. I nie jest to dzielenie włosa na czworo, własność ta ma namacalne konsekwencje:
napis1 = 'abc'
napis2 = napis1
napis1 = 'ABC'
print(napis1, napis2)
→ ABC abc
# dla porównania, listy:
lista1 = ['a', 'b', 'c']
lista2 = lista1
lista1[0] = 'A'
print(lista1, lista2)
→ ['A', 'b', 'c'] ['A', 'b', 'c']
Powraca jeszcze raz pytanie: czym są poszczególne znaki? Tzn. jaki typ danych reprezentują? Otóż w Pythonie pojedyncze znaki też są po prostu napisami, tyle że o długości jeden. Poza długością nic ich szczególnie nie wyróżnia. Ta uwaga jest na użytek tych z Was, którzy znają jakiś inny język programowania, gdyż w wielu z nich znak jest oddzielnym, innym niż napis typem danych.
Pętlę for można zatem zastosować do napisu - zamiast do listy, wówczas w kolejnych iteracjach będą uwzględniane kolejne znaki, a których składa się napis.
Ogólna postać pętli for
for element in sekwencja:
<blok instrukcji>
else:
<blok instrukcji>
- Pierwszy blok instrukcji będzie wykonany w zasadzie tyle razy, ile elementów ma sekwencja
- Za każdym razem nazwa element oznaczać będzie kolejny element sekwencji, zgodnie z ich porządkiem
- sekwencja może być pusta; nie jest to błąd, ale wtedy pierwszy blok nie będzie wykonany ani razu
- Gdy elementy sekwencji się wyczerpią, wykonany zostanie blok po else
- Blok else (wraz z linijką go otwierającą) jest opcjonalny (niekonieczny)
- W pierwszym bloku mogą wystąpić specjalne polecenia: break i continue
- break znaczy: przerwij pętlę natychmiast, pomiń wszystkie pozostałe elementy sekwekcji, pomiń również ewentualny blok else
- continue oznacza: przerwij działania na aktualnym elemencie, i wróć do początku bloku biorąc kolejny element. Jeśli już zabrakło elementów, przejdź do bloku else, albo (gdy go nie ma) do polecenia następnego po pętli
- W miejscu gdzie stoi sekwencja może tak naprawdę stać dowolny obiekt iterowalny - np. kolekcja. Ale innych typów kolekcji jeszcze nie poznaliśmy...
Blok else w pętli for jest stosunkowo rzadko widywany. Nawet wielu programistów dość biegłych w Pythonie nie wie (lub zapomniało) o istnieniu tej opcji.
A co, jeśli chcielibyśmy za pomocą pętli for (lub inaczej) zmienić w jakiś sposób skład samej sekwencji? Na przykład:
for x in lista:
x = x + 1
TO NIE ZADZIAŁA - w tym sensie, że lista będzie dalej miała te same elementy co poprzednio, a nie - powiększone o 1. W istocie operacja taka nic nie osiąga - oprócz tego, że po jej zakończeniu nazwa x ma wartość ostatniego elementu listy, powiększonego o 1. To samo można dostać bez pisania pętli.
Jak więc osiągnąć zmianę - przeliczenie - wszystkich elementów sekwencji wg. jakiejś reguły (np. powiększ każdy z nich o 1)?
Pożytecznym sposobem tworzenia sekwencji jest funkcja range:
for x in range(3):
print(x)
→ 0
→ 1
→ 2
Argument (tu: 3) oznacza liczbę elementów w wyprodukowanej sekwencji. Domyślnie zaczyna się ona od zera, tak jak numeracja pozycji w sekwencjach - ale można użyć wywołania range(a, b) aby uzyskać elementy dowolnego ciągu arytmetycznego - o elemencie początkowym a i przyroście b.
Tu jest pewne oszustwo: wynikiem wywołania range nie jest tak naprawdę lista, ale pewnego innego rodzaju typ sekwencji, a więc mogący służyć za źródło elementów w iteracji. Na razie to jest jednak drobny szczegół. Jeżeli potrzebujemy dosłownej listy o takich elementach, wystarczy wykonać tzw. rzutowanie, czyli przyłożyć funkcję list: lista = list(range(3))
Wracając do problemu przeliczenia wszystkich elementów listy:
lista = [3, 5, 8]
for k in range(3):
lista[k] += 1
print lista
→ [4, 6, 9]
I to działa. Nie jest to jednak optymalne rozwiązanie.
- Pisząc to co powyżej wiedzieliśmy, że mamy do czynienia z listą o długości 3. W ogólności musielibyśmy użyć funkcji len;
- Można to jednak zrobić bardziej elegancko:
for k, x in enumerate(lista):
lista[k] = x + 1
Funkcja enumerate produkuje sekwencję par: nr pozycji, element - i jest bardzo przydatna w takich sytuacjach.
Z dokonywaniem wewnątrz pętli zmian na sekwencji, po której iterujemy, należy jednak uważać. To, co robimy w tym przykładzie jest akurat niegroźne, natomiast umieszczenie wewnątrz pętli operacji zmieniających długość sekwencji, po której iterujemy mogłoby dać nieoczekiwane wyniki. Spróbuj np.:
L = [0, 1, 2, 3, 4]
for i in range(5):
del L[i]
Co tu się stanie, i dlaczego?
Więcej o listach
Elementy listy (innych rodzajów sekwencji również) można numerować od końca, używając ujemnych wartości wskaźników pozycji:
numerki = [0, 1, 2, 3, 5, 8]
numerki[-1]
→ 8
numerki[-3]
→ 3
Z list (i innych sekwencji) można wyjmować nie tylko poszczególne elementy, ale również podsekwencje, zwane też wycinkami:
numerki = [0, 1, 2, 3, 5, 8]
nn = numerki[1:4]
→ [1, 2, 3]
nn = numerki[2:]
→ [2, 3, 5, 8]
nn = numerki[:3]
→ [0, 1, 2]
nn = numerki[1:4:2]
→ [1, 3]
nn = numerki[4:1:-1]
→ [5, 3, 2]
nn = numerki[:]
→ [0, 1, 2, 3, 5, 8]
Jak widać, pominięcie pozycji początkowej lub końcowej oznacza: ,,do oporu". Trzecia liczba charakteryzująca wycinek, o ile występuje - to ,,skok" wskaźnika. Przykładowo, a[::2] oznaczałoby listę zawierającą co drugi element listy a, natomiast a[::-1]: listę składającą się z tych samych elementów, co a - lecz w odwrotnej kolejności.
Wyrażenie numerki[:], tzw. pełny wycinek, tworzy pełną kopię listy numerki; tzn. nie jest to ta sama lista, chociaż składa się z tych samych elementów. Jest to tzw. kopia płytka - oznacza to tyle, że jeżeli wśród elementów kopiowanej listy były obiekty złożone (np. inne listy), to elementami kopii będą te same listy, a nie - ich kopie.
Wycinek jest zawsze nową listą, również w ostatnim przypadku - gdy jej skład jest taki sam, jak oryginalnej. Wtedy stanowi kopię pierwotnej listy. Można jednak też podstawiać do wycinka:
numerki[4:5] = [4]
→ [0, 1, 2, 3, 4, 8]
numerki[5:5] = [5, 6, 7]
→ [0, 1, 2, 3, 4, 5, 6, 7, 8]
i jest to jeszcze jeden sposób, by zmienić zawartość listy - w tym być może jej długość - zachowując jej tożsamość.
Listy można do siebie dodawać:
l = ['a', 'b', 'c'] + ['y', 'z']
→ ['a', 'b', 'c', 'y', 'z']
w ten sposób powstaje nowa lista, która ze składników bierze elementy, ale poza tym nie ma z nimi nic wspólnego; późniejsze zmiany w listach-składnikach nie mają wpływu na sumę.
Listy można też zwielokrotnić; np. aby uzyskać listę siedmiu dwójek:
l = [2] * 7
→ [2, 2, 2, 2, 2, 2, 2]
ta sama co poprzednio uwaga stosuje się i tu.
Elementy listy można posumować:
x = sum(range(101))
→ 5050
(wprawdzie range(101) nie jest dosłownie listą, ale z ,,prawdziwą" listą można zrobić to samo).
Podobnie jak inne obiekty, listy posiadają szereg metod, za pomocą których można na nich wykonywać różne działania. Przykładowo:
numerki.append(9)
→ [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
numerki.extend([10, 11])
→ [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
Zwróćcie uwagę na notację: kropka sygnalizuje ,,wyciągnięcie" metody z danego obiektu (tu: listy), w nawiasach wpisujemy dane dla tej metody (argumenty - tu pojedyncze, ale może być więcej).
Więcej o napisach
Napisy dopuszczają tylko część operacji dostępnych dla list, ze względu na własność niemodyfikowalności. Posiadają za to szereg swoistych metod. Trzeba pamiętać, że żadna z tych metod nie zmienia napisu, którego dotyczy - wynikiem może być jedynie nowy napis, stworzony na podstawie oryginalnego.
Niemniej napis również jest sekwencją, i może być np. zastosowany w pętli for. Ale sekwencją czego? Oczywiście znaków — z tym, że w Pythonie nie ma oddzielnego typu danych ,,znak". Znaki to po prostu napisy jednoelementowe.
Przykład: metoda upper (zamienia małe litery na wielkie):
'abcde'.upper()
→ 'ABCDE'
Napis zawsze można łatwo zamienić na listę znaków:
l = list('abcde')
→ ['a', 'b', 'c', 'd', 'e']
(choć rzadko zachodzi taka potrzeba).
Do operacji odwrotnej można użyć metody join:
''.join(l)
→ 'abcde'
', '.join(l)
→ 'a, b, c, d, e'
Pętla while
To jeszcze jeden sposób iteracji, tak naprawdę prostszy od pętli for — gdyż nie oparty na sekwencjach; zwykle mniej wygodny w użyciu, ale w niektórych przypadkach nieodzowny.
# postać najprostsza
while WARUNEK:
BLOK INSTRUKCJI
# ogólna postać:
while WARUNEK:
BLOK1
else:
BLOK2
Polega na powtarzaniu bloku instrukcji nieokreśloną liczbę razy; przed każdym kolejnym wykonaniem na nowo sprawdzany jest warunek, i jeśli okaże się fałszywy - powtarzanie się kończy.
Innym sposobem przerwania pętli while jest umieszczenie gdzieś w bloku instrukcji break - zwykle pod pewnym warunkiem (tzn. wewnątrz instrukcji warunkowej if). Wykonanie break polega na ,,wyskoczeniu" z pętli - i przejściu do dalszego ciągu programu, po bloku wewnętrznym pętli.
Oczywiście aby WARUNEK mógł okazać się za którymś razem fałszywy, i spowodować przerwanie pętli, musi on być zbudowany z wykorzystaniem zmiennych, których wartości ulegają zmianom wewnątrz pętli. Zdarza się jednak widzieć pętlę zaczynającą się od while True: - formalnie jest to pętla nieskończona, którą może zakończyć jedynie wywołanie instrukcji break występującej gdzieś w wewnętrznym bloku.
Wewnątrz bloku może wystąpić również instrukcja continue - działa ona analogicznie, jak w pętli for, a więc powoduje przerwanie wykonywania aktualnej iteracji i przeskok do początku pętli, czyli sprawdzenia warunku jej kontynuowania.
W ogólnej postaci, blok występujący po linijce else: jest wykonywany po zakończeniu pętli wskutek niespełnienia warunku początkowego, natomiast jest pomijany, jeśli przerwanie pętli było spowodowane instrukcją break.
Zasadniczo każdą pętlę for dałoby się przepisać jako równoważną pętlę while - przykładowo:
for x in lista:
działaj_na(x)
# równoważnie:
k = 0
while k < len(lista):
działaj_na(lista[k])
k += 1
nie należy jednak zazwyczaj tego robić, ponieważ (najczęściej) pętla for jest bardziej czytelna.
Ćwiczenia
1. Napisać program, który sumuje liczby naturalne od 1 do 100:
- z wykorzystaniem pętli for
- z wykorzystaniem pętli while
2. Wykorzystując pętlę while napisać program, który sprawdza czy zadana liczba naturalna jest liczbą pierwszą.
3. Napisać program, który sprawdza jaka jest najmniejsza liczba wyrazów szeregu harmonicznego, jaką należy posumować, aby wynik przekroczył daną liczbę dodatnią (,,próg").
4. Napisać program, który gra z użytkownikiem w zgadywanie liczby (,,sekretna" liczba do odgadnięcia jest wpisana w kod programu). Użytkownik jest proszony o próbę zgadnięcia, w odpowiedzi otrzymuje informację, czy sekretna liczba jest większa czy mniejsza od podanej przez niego; w przypadku trafnego zgadnięcia, program składa gratulacje i podaje, po ilu próbach udało się zgadnąć.