Spis treści Skorowidz Poziom główny Poziom nadrzędny ©
«« Początek »» Koniec

Kod, czyli jak się bawić własnymi zabawkami

Kilka najprostszych instrukcji zostało wprowadzonych w rozdziale 5. dotyczącym interpretera. Za ich pomocą potrafimy już zapamiętywać wartości obliczanych wyrażeń oraz korzystać z czynności złożonych znając ich nazwy.

W bieżącym rozdziale znajomość instrukcji zostanie rozszerzona: w pierwszej kolejności usystematyzujemy znajomość instrukcji prostych. Następnie wprowadzimy instrukcje pozwalające zarządzać wykonywaniem innych instrukcji. Bez tej możliwości programowanie sprowadzałoby się do pisania długich i nudnych ciągów poleceń wykonywanych w tej samej kolejności, w jakiej zostały napisane. To właśnie instrukcje sterujące przesądzają o potędze automatycznego przetwarzania.

Instrukcje

Podsumujemy naszą dotychczasową wiedzę o instrukcjach. Ich podstawową rolą jest wyrażanie poleceń. Instrukcje są przeznaczone do wykonywania w ustalonej kolejności; mogą też być w różnych celach grupowane.

Instrukcje proste

Instrukcji proste opisują pojedyncze niezwiązane ze sobą polecenia. Służyły nam one do:

Instrukcja pusta pass nie powoduje żadnej akcji. Przydaje się w sytuacjach szczególnych.

del, pass słowami kluczowymi Pythona.

Instrukcje sterujące

Do zarządzania przebiegiem programu, a w szczególności do decydowania o wykonywaniu innych instrukcji, służą instrukcje sterujące. Należą do nich przede wszystkim:

Pierwsze trzy typy instrukcji sterujących zostaną omówione w kolejnych podrozdziałach. Obsługi wyjątków nie będziemy szczegółowo omawiać; wstępne objaśnienia zamieszczamy w podrozdziale 8.4.

Każda instrukcja sterująca Pythona kończy się dwukropkiem. O zasięgu grupowania decydują wcięcia następujących po niej instrukcji. Blok instrukcji podrzędnych składa się z instrukcji wciętych głębiej, niż instrukcja sterująca.

ta instrukcja:
    grupuje
    instrukcje
    podrzędne
    takie grupowanie:
	może
	być
	wielopoziomowe
    chociaż wcale nie musi
grupowanie:
    zawsze powinno czemuś służyć:
	np. wykonaniu warunkowemu
	lub powtarzaniu czynności
    zawsze się gdzieś kończy
    # choćby na końcu pliku

Python jest bodaj jedynym popularnym językiem programowania, w którym układ typograficzny kodu jest ważny.

Układ typograficzny kodu źródłowego jest w Pythona wymuszony przez składnię języka. W innych językach programowania jest on nieobowiązkowy, choć należy do dobrej praktyki. Podobne reguły typograficzne dotyczą np. schludnie sformatowanych wielopoziomowych wyliczeń w dokumentach.

Głębokość wcięć jest dowolna, ale na danym poziomie grupowania instrukcji musi być taka sama, a na głębszym — większa. Mieszanie wcięć robionych za pomocą spacji i znaków tabulacji nie jest rozsądne. Stosujmy albo jedno, albo drugie.

Wiersze nie zawierające instrukcji są traktowane jak nieistniejące i mogą zawierać dowolne wcięcia.

Blok instrukcji podrzędnych nie może być pusty. Jeżeli z jakichś względów chcemy, by nie zawierał on żadnej sensownej instrukcji, możemy umieścić w nim instrukcję pustą pass.

Instrukcje warunkowe

Często zachodzi potrzeba zdecydowania o kierunku dalszego działania na podstawie szczegółów zaistniałej sytuacji. Wtedy przydaje się konstrukcja algorytmiczna, która analizuje zadany warunek lub układ warunków i uzależnia od jego wyników wybór dalszej ścieżki postępowania. W większości języków programowania konstrukcja taka ma postać instrukcji warunkowej. W niektórych językach istnieje również możliwość użycia wyrażeń warunkowych.

Istnieją też języki (Python do nich nie należy), w których odrębna konstrukcja językowa realizuje instrukcję wyboru, w której decyzja zależy do wartości wyrażenia, niekoniecznie mającego typ logiczny.

Składnię instrukcji warunkowej w Pythonie można nieformalnie opisać w następujący sposób:

if warunek:
    ...
elif warunek:
    ...
else:
    ...

W instrukcji warunkowej występuje:

""" instrukcja warunkowa
widzisz, dlaczego wcięcia są ważne?
"""
if (warunek1):
    wykonaj
    te instrukcje
    w przypadku
    spełnienia warunku warunek1
elif (warunek2):
    wykonaj
    te instrukcje
    w przypadku niespełnienia wyrażenia warunek1
    i spełnienia wyrażenia warunek2
else:
    wykonaj
    te instrukcje
    w przypadku
    niespełnienia żadnego warunku

if, elif, else słowami kluczowymi Pythona.

Warunki następujące po słowach if oraz elif powinny być wyrażeniami typu logicznego. Jeżeli tak nie jest, dokonywana jest ich konwersja do typu logicznego.

Łączenie instrukcji warunkowych

W bieżącym podrozdziale pokażemy na kilka sytuacji, które wymagają radzenia sobie z układami warunków. Zależnie od wzajemnych związków między tymi warunkami, albo będziemy sprawdzać je niezależnie, albo alternatywnie, albo przez zagnieżdżanie instrukcji warunkowych.

Techniki zilustrowane w poniższych przykładach są wspólne dla wszystkich metod opisu algorytmów, nie tylko dla języków programowania, a w szczególności nie tylko dla Pythona.

Niezależne sprawdzanie warunków

Mamy dane: wzrost osobnika w metrach i wiek w latach. Chcemy wygenerować raport, w którym dokonana będzie (umowna) klasyfikacja wzrostu: wysoki/niski i wielu: młody/stary.

Pojedyncza klasyfikacja polega w tym przypadku na porównaniu wartości testowanej z pojedynczą wartością graniczną. Każdą klasyfikację przeprowadzamy za pomocą instrukcji warunkowej niezależnie od klasyfikacji innych kryteriów.

if wzrost < progWzr:
    klasaWzrost = 'niski'
else:
    klasaWzrost = 'wysoki'

if wiek < progWiek:
    klasaWiek = 'młody'
else:
    klasaWiek = 'stary'

Odrębne sprawdzanie warunków wzrost < progWzr i wzrost >= progWzr jest niepożądane, gdyż dopełniają się one wzajemnie; ze spełnienia jednego wynika niespełnienie drugiego. Stąd użycie frazy else.

W poniższym przykładzie sprawdza się niepotrzebnie i warunek, i jego zaprzeczenie:

if wzrost < progWzr:
    klasaWzrost = 'niski'
if wzrost >= progWzr:
    klasaWzrost = 'wysoki'

W poniższym przykładzie obie instrukcje warunkowe są potrzebne, gdyż w obrębie pierwszej zachodzi modyfikacja zmiennej, która może mieć wpływ na wartość drugiego warunku.

if ciezar < wagaDopuszczalna:
    ciezar += przedmiot
if ciezar >= wagaDopuszczalna:
    print('Za ciężko')

Sprawdzanie warunków wzajemnie się wykluczających

Mamy za zadanie zaklasyfikować wzrost osobnika jako niski, średni, wysoki lub bardzo wysoki. Dysponujemy wartościami progowymi rozdzielającymi poszczególne grupy. Z uwagi na fakt, że klasy wzrostu są rozłącznymi przedziałami na osi liczbowej, będziemy sprawdzać spełnienie pojedynczych nierówności za pomocą fraz elif.

if wzrost < prog1:
    klasaWzrost = 'niski'
elif wzrost < prog2:
    klasaWzrost = 'średni'
elif wzrost < prog3:
    klasaWzrost = 'wysoki'
else:
    klasaWzrost = 'bardzo wysoki'

W poniższym przykładzie sprawdza się za dużo:

if wzrost < prog1:
    klasaWzrost = 'niski'
elif prog1 <= wzrost < prog2:
    klasaWzrost = 'średni'
elif prog2 <= wzrost < prog3:
    klasaWzrost = 'wysoki'
else:
    klasaWzrost = 'bardzo wysoki'

Kolejność sprawdzania ma wpływ na liczbę niezbędnych porównań, co widać zwłaszcza przy dużej liczbie wartości progowych. Wtedy lepiej skorzystać z profesjonalnego algorytmu wyszukiwania. Nie będziemy rozwijać tego tematu w tym miejscu.

Zagnieżdżenia instrukcji warunkowych

W ostatnim przykładzie znów dysponujemy wzrostem i wiekiem, ale progi decydujące o klasie wzrostu mamy zastosować wyłącznie dla osobników dojrzałych. Zatem klasyfikację dotyczącą wzrostu zagnieżdżamy w instrukcji warunkowej dotyczącej klasyfikacji wieku.

if wiek < progWiek:
    klasaWzrost = None
    klasaWiek = 'młody'
else:
    if wzrost < prog1:
	klasaWzrost = 'niski'
    elif wzrost < prog2:
	klasaWzrost = 'średni'
    elif wzrost < prog3:
	klasaWzrost = 'wysoki'
    else:
	klasaWzrost = 'bardzo wysoki'    
    klasaWiek = 'stary'

Analogicznie należałoby postąpić, gdyby progi klasyfikacyjne jednej cechy (np. masy ciała) zależały od wyniku klasyfikacji innej cechy (np. wieku osobnika).

W poniższym przykładzie autor kodu nie przeprowadził rzetelnej analizy układu warunków, skutkiem czego instrukcja jest skomplikowana, nieczytelna i trudna do przebudowy:

if wiek < progWiek:
    klasaWzrost = None
    klasaWiek = 'młody'
elif wiek >= progWiek and wzrost < prog1:
    klasaWzrost = 'niski'
    klasaWiek = 'stary'
elif wiek >= progWiek and  wzrost < prog2:
    klasaWzrost = 'średni'
    klasaWiek = 'stary'
elif wiek >= progWiek and wzrost < prog3:
    klasaWzrost = 'wysoki'
    klasaWiek = 'stary'
else:
    klasaWzrost = 'bardzo wysoki'    
    klasaWiek = 'stary'

Dalsze przykłady

[]

Zagnieżdżenia warunków w rozwiązywaniu równania liniowego

[]

Zagnieżdżenia warunków w rozwiązywaniu równania drugiego stopnia

Python oferuje także alternatywną składnię uwzględniania warunków w ramach pojedynczej instrukcji obliczającej wartość wyrażenia. Zamiast o „instrukcji warunkowej” w tym przypadku mówi się o „wyrażeniu warunkowym”.

wartość1 if warunek else wartość2

Wynikiem tego wyrażenia jest albo wartość1, albo wartość2, w zależności od tego, czy warunek jest spełniony. Wszystkie trzy argumenty mogą być dowolnymi wyrażeniami, z tym że warunek winien zwracać wartość typu logicznego. W przypadku, kiedy warunek ma wartość True, wyrażenie wartość2 w ogóle nie jest obliczane. Analogicznie jest z wyrażeniem wartość1 w sytuacji, kiedy warunek ma wartość False.

Podobne rozwiązania znane są w niektórych innych językach programowania, np. w C, a także w arkuszach kalkulacyjnych.

Instrukcje wyboru

Instrukcja wyboru pozwala rozgałęzić akcję na ustaloną liczbę przypadków. Każdy z nich jest rozpoznawany za pomocą określonej wartości testowanego wyrażenia. W klasycznym ujęciu wartości te winny być zdefiniowane jako stałe, tj. nie mogą wymagać obliczenia; w szczególności więc nie mogą zależeć od wartości zmiennych ani w żaden inny sposób od stanu procesu obliczeniowego.

W składni Pythona instrukcja wyboru jest dostępna w postaci instrukcji match począwszy do wersji 3.10 języka.

Składnia instrukcji w uproszczeniu jest następująca:

match wyrażenie:
     case wartość1: 
        akcja1
     case wartość2: 
        akcja2
     ...
     case wartośćN: 
        akcjaN
     case _:
        akcja domyślna

Wartości wskazane w poszczególnych frazach case służą do identyfikacji przypadków. Kolejność i liczba tych fraz jest dowolna. Specjalna wartość _ służy do opisania przypadku domyślnego. Przypadek taki jest opcjonalny, ale jeśli występuje, musi być ostatni w kolejności.

Instrukcje iteracji

Sumując elementy danego zbioru lub ciągu, musimy powtórzyć czynność dodawania tyle razy, ile elementów ma ten zbiór czy też ciąg. Poszukując rozwiązania pewnego problemu metodą kolejnych przybliżeń, musimy powtarzać te same proste operacje tak długo, aż przekonamy się, że osiągnęliśmy dostatecznie dobre rozwiązanie. W obu tych sytuacjach zachodzi konieczność wielokrotnego wykonania tych samych operacji, przy czym w pierwszej z nich liczba koniecznych powtórzeń jest z góry wiadoma, zaś w drugiej — nie.

Instrukcje iteracji umożliwiają:

Iteracje są jednym z najważniejszych mechanizmów sterowania. Rozważmy różnice między następującymi przykładami ciągów poleceń wyrażonymi w języku naturalnym:

W pierwszym opisie nie użyto techniki iteracji. W pozostałych iteracja jest stosowana, przy czym powtarzanie odbywa się, tak jak w wariantach drugim i trzecim, ustaloną liczbę razy, albo tak jak w wariancie ostatnim, tak długo, jak długo spełniony jest warunek, którego sprawdzenie następuje przed każdorazowym powtórzeniem czynności.

Większość języków programowania, w tym Python, ma instrukcje opisujące oba warianty iteracji.

Przebieganie zbioru elementów dowolnej listy

for element in lista:
    wykonaj te instrukcje
    po kolei 
    dla każdego
    elementu listy
else:
    wykonaj te instrukcje
    jeden raz 
    po zakończeniu iteracji, 
    o ile nie przerwano jej
    instrukcją break

Fraza else: i następujący po niej blok nie są obowiązkowe.

for, in, else, są słowami kluczowymi Pythona.

Przykład

[]

Sumowanie ciągu danych
będącego zawartością listy

Przebieganie zbioru kolejnych liczb całkowitych

Funkcja range() zwraca listę składającą się z kolejnych liczb całkowitych.

Najprostsza postać użycia range(n) z jednym argumentem typu całkowitoliczbowego daje ciąg liczb od 0 do n−1 włącznie.

range(1)
[0]
range(10)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
k = 4
range(k)
[0, 1, 2, 3]
range(2*k)
[0, 1, 2, 3, 4, 5, 6, 7]

Nieco inne użycie funkcji range(poczatek, stop) pozwala rozpocząć numerację od wskazanej liczby całkowitej poczatek i zakończyć ją na wartości stop−1:

range(1,10)
[1, 2, 3, 4, 5, 6, 7, 8, 9]
k = 4
range(1, k)
[1, 2, 3]
range(k, 2*k)
[4, 5, 6, 7]

Trzecia forma funkcji range, mająca postać range(poczatek, stop, krok), tworzy listę rozpoczynającą się od wartości poczatek. Jej elementy tworzą ciąg arytmetyczny z krokiem krok i nie osiągają wartości stop. krok musi być liczbą całkowitą różną od zera (może być ujemna, ale wtedy poczatek winien być większy od stop; w przeciwnym razie lista będzie pusta).

range(1, 10, 2)
[1, 3, 5, 7, 9]
range(10, 0, -1)
[10, 9, 8, 7, 6, 5, 4, 3, 2, 1]

Jeżeli zachodzi potrzeba numerowania przebiegów iteracji, to wykorzystanie pętli for przebiegającej listę range() jest najlepszym rozwiązaniem.

for element in range(liczba):
    wykonaj te instrukcje liczba razy, 
    przy czym zmienna element przy każdym powtórzeniu
    będzie przyjmować kolejną wartość całkowitą
    z zakresu od 0 do liczba−1
for element in range(poczatek, stop):
    wykonaj te instrukcje stoppoczatek razy, 
    przy czym zmienna element przy każdym powtórzeniu
    będzie przyjmować kolejną wartość całkowitą
    z zakresu od poczatek do stop−1

Python nie posiada tradycyjnej „pętli for” sterowanej licznikiem, znanej z innych języków programowania.

Z pętli sterowanej za pomocą listy range winniśmy korzystać za każdym razem, kiedy istotne są nie tylko wartości, ale i numery porządkowe elementów ciągu. Jest tak np. w sytuacjach, kiedy interesuje nas modyfikacja elementów listy, kiedy działamy na dwóch listach jednocześnie, kiedy chcemy mieć dostęp do elementu poprzedzającego element bieżący, itp.

W Pythonie 3 funkcja range() została co nieco skomplikowana. Jej wynikiem nie jest już lista, lecz użycie range() w instrukcji for prowadzi do tych samych wyników co poprzednio. Nie da się tego w pełni wyjaśnić bez wgłębiania się w zaawansowane podstawy struktur danych Pythona. Dociekliwym sugerujemy zapoznanie się z pojęciem iterator w dokumentacji języka.

Przykład

[]

Drukowanie kolejnych liczb całkowitych

W trakcie wykonywania pętli for istnieje możliwość modyfikacji iterowanej listy. Operacja taka bywa czasami użyteczna, np. podczas konwersji elementów ciągu

i = 0
for element in lista:
    lista[i] = str(element)
    i += 1

lecz nie jest szczytem elegancji — znacznie lepiej jest zastosować iterację po numerach elementów

for i in range(len(lista)):
    lista[i] = str(lista[i])

Nierozważna modyfikacja iterowanej listy jest częstą przyczyną nieczytelności algorytmów oraz trudnych do wykrycia błędów. Np. do czego doprowadzi pętla for, w której bieżący element jest usuwany z listy? jest dopisywany na koniec listy?

Przebieganie list złożonych

W sytuacji, kiedy iterowana struktura składa się z list (np. kiedy jest listą punktów, listą wektorów albo innych obiektów), zmienna sterująca iteracji for przyjmuje wartości będące listami. Dostęp do jej elementów uzyskamy pobierając jej pierwszy, drugi i następne składniki.

Operację tę można uprościć, zmieniając postać zmiennej sterującej. Zamiast nadawać jej nazwę, opiszemy ją jako listę zbudowaną z nazwanych elementów.

[]

W pierwszym wariancie iteracja przebiega zbiór liczb całkowitych będących numerami elementów listy punkty. Stosujmy tę metodę np. wtedy, kiedy potrzebujemy jednoczesnego dostępu do elementu bieżącego i elementu poprzedzającego go.

[]

W drugim wariancie iteracja przebiega zbiór elementów listy punkty. Są one po kolei przypisywane zmiennej sterującej punkt. Współrzędne wyłuskuje się z niej przez podanie indeksu (0 lub 1).

[]

W ostatnim wariancie iteracji kolejne wartości elementów listy punkty są przypisywane krotce (x, y). Nie ma potrzeby numerowania elementów tej krotki, gdyż mają one swoje nazwy, przez co kod jest bardziej czytelny. Taki wariant wymaga, by wszystkie elementy przeglądanej listy cechowały się tą samą długością. W przeciwnym razie nie uda się „wypakowanie” elementu listy do zmiennych sterujących (x, y).

Generowanie list

Utworzenie listy zawierającej ciąg obliczonych przez nas wartości przebiegać może według następującego schematu: najpierw tworzy się pustą listę, a potem po kolei dołącza się do niej następne elementy. Jeżeli są one wszystkie tworzone w ten sam sposób, warto wykorzystać instrukcję iteracji for.

Tym sposobem w przykładzie poniżej tworzona jest lista kwadraty zawierająca drugie potęgi kolejnych liczb całkowitych od 1 do 10.

kwadraty = []
for i in range(1, 11):
    kwadraty.append(i**2)

print(kwadraty)
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

Dla informacji, że instrukcja nie została zakończona, zgłoszenie interpretera przybiera postać ... zamiast >>>. Wstawienie pustego wiersza powoduje zakończenie wciętego bloku, tym samym zamknięcie ciała instrukcji iteracji i powrót do zwykłego zgłoszenia >>>.

Inne podejście do generowania list: wyrażenia iteracyjne

Podczas generowania ciągów zbudowanych według ustalonego wzorca wygodnie jest użyć osobnego wariantu iteracji for. Ma on bardziej charakter „wyrażenia iteracyjnego” niż „instrukcji iteracji” i wygląda następująco:

[ wyrażenie for zmienna in lista ]

przy czym wyrażenie może korzystać z wartości zmienna, przebiegającej kolejno wszystkie elementy listy. Wynikowa wartość takiego wyrażenia jest listą. W poniższym przykładzie generujemy tę samą co poprzednio tablicę kwadratów kolejnych liczb całkowitych.

kwadraty = [ i**2 for i in range(1, 11) ]
print(kwadraty)
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

Koncepcja wyrażeń iteracyjnych jest charakterystyczna dla Pythona. Przypomina ona trochę koncepcję formuł tablicowych, znaną z arkuszy kalkulacyjnych. W większości klasycznych języków programowania wyrażenia iteracyjne nie istnieją.

Stosowanie wyrażeń iteracyjnych jest pożyteczne, ale wymaga wcześniejszego opanowania podstawowych konstrukcji algorytmicznych, jakimi są klasyczne instrukcje iteracji.

Iteracje for: niekoniecznie na podstawie listy

We wszystkich dotychczasowych przykładach użycia pętli for zakładaliśmy, że wykorzystuje ona listę elementów. Jest to pewne uproszczenie. Python nie wymaga, by kolekcja obiektów „odwiedzanych” w trakcie iteracji for była listą. W dokumentacji języka jest mowa o obiektach iterowalnych (iterables), tj. takich, których elementy składowe da się ustawić w ciąg. Oczywiście listy są iterowalne; oprócz tego do typów iterowalnych należą napisy i krotki, a także pliki, zbiory i słowniki. Typy te były wspomniane w podrozdziale 6.5.

Jeżeli ponadto elementy danego obiektu wolno numerować liczbami całkowitymi — jest tak np. w listach, napisach i krotkach; nie jest zaś w plikach, zbiorach i słownikach; dokumentacja Python mówi o typach „indeksowalnych” (scriptable) — to do iteracji można wykorzystać listę ich numerów, utworzoną za pomocą funkcji range().

Iteracja sterowana warunkiem

Iteracje for są związane z przetwarzaniem elementów danego ciągu. Czasami nie jest to naturalne podejście. W innym ważnym modelu iteracji instrukcje powtarzane są tak długo, jak długo spełniony jest pewien warunek.

Iteracje tego typu są w Pythonie opisywane za pomocą instrukcji while. Warunek jest sprawdzany każdorazowo przed przystąpieniem do wykonywania instrukcji podległych.

while warunek:
    wykonuj te instrukcje
    tak długo, jak długo 
    sprawdzanie wartości warunku 
    daje wynik True
else:
    wykonaj te instrukcje
    jeden raz 
    po zakończeniu iteracji, 
    o ile nie przerwano jej
    instrukcją break

Pętla while nie potrzebuje żadnej listy ani żadnego licznika. Nic nie stoi na przeszkodzie, by w razie potrzeby użyć w niej własnej konstrukcji do zliczania przebiegów iteracji.

while i else słowami kluczowymi Pythona.

Przykłady

[]

Drukowanie kolejnych liczb całkowitych

[]

Sumowanie i zliczanie elementów czytanych z pliku (więcej szczegółów w rozdziale 10.4. poświęconym plikom)

Przykład

Wyczerpywanie losowych porcji z zasobnika.

[]

Wyczerpywanie losowe

Przykład

Metoda bisekcji (więcej szczegółów w rozdziale 9.)

Dodatkowe instrukcje sterujące przebiegiem iteracji

Instrukcja break nakazuje przerwanie iteracji bez względu na to, czy wynika to z logiki głównej instrukcji sterującej (for lub while). Jeżeli instrukcja iteracji zawiera frazę else, to podległy jej blok instrukcji nie zostanie wykonany.

Instrukcja continue przerywa wykonywanie bieżącej iteracji i rozpoczyna następny jej cykl. O tym, czy zostanie on wykonany, decyduje logika głównej instrukcji sterującej iteracją (for lub while).

break i continue słowami kluczowymi Pythona.

Podobnie jak w instrukcji warunkowej if, także w instrukcji while warunkiem powinno być wyrażenie typu logicznego. Jeżeli tak nie jest, dokonywana jest konwersja do typu logicznego.

Zagnieżdżanie instrukcji sterujących

Przykład: sumowanie dodatnich elementów ciągu

Warunek wewnątrz pętli

[]

Przykład: znajdowanie położenia największych elementów w każdym wierszu macierzy

Pętla i układ warunków wewnątrz pętli

[]

Główna pętla sterująca programem

Typowy program użytkowy oczekuje na zlecenie od operatora, po czym interpretuje otrzymaną komendę i wykonuje ją. Taki cykl powtarza się aż do chwili, kiedy operator wyda polecenie zakończenia pracy.

Do tego modelu działania „pasuje” pętla sterowana warunkiem; w Pythonie jest to pętla while.

opcja = -1
while opcja != 0:
    opcja = wywołaj funkcję decyzyjną
    wykonaj czynność określoną dla wybranej opcji

Pętla taka zakończy się, kiedy funkcja decyzyjna zwróci wartość 0, po wykonaniu czynności przewidzianych na tę okoliczność.

Bywa, że warunkiem sterującym takiej pętli jest True, zaś zakończenie działania wymusza się jawnym wywołaniem funkcji exit() w odpowiednim miejscu.

Poniższy przykład przedstawia szkic aplikacji sterowanej dialogiem z modułu easygui (patrz podrozdział 11.1.4).

[]

Bardziej eleganckie rozwiązanie podobnego zagadnienia przestawimy w końcowej sekcji podrozdziału 7.6.8, jako ilustrację zastosowania podprogramów.

W aplikacjach pisanych z wykorzystaniem profesjonalnych narzędzi do tworzenia interfejsu użytkowego, np. TkInter, GTK+, Qt, Windows, wxWidgets, pętla ta jest już zdefiniowana w bibliotece. Autor aplikacji „podpina” jedynie do gotowej pętli napisane przez siebie podprogramy tak, by były wywoływane w określonych okolicznościach (np. po wybraniu odpowiedniej opcji z menu).

Podprogramy, czyli wymyślanie nowych zabaw

def nazwa(argumenty):
    wykonaj te instrukcje za każdym razem w przypadku
    wywołania funkcji nazwa z konkretnymi wartościami 
    argumentów przekazanymi w instrukcji wywołującej
    return wynik

Jeżeli podprogram ma być używany bez przekazywania mu argumentów, to nawias w nagłówku powinien być pusty — ale nie wolno go pominąć.

Instrukcja return powoduje zakończenie wykonywania podprogramu i przekazuje sterowanie do miejsca, z którego został on wywołany.

Instrukcja return bez argumentu jest równoważna return None. Jeżeli podczas wykonywania kodu podprogramu nie dojdzie do wykonania instrukcji return, to wynikiem jest None — tak samo, jakby ostatnią instrukcją w podprogramie była return None.

def, return słowami kluczowymi Pythona.

Analogia: okienko napraw w punkcie usługowym. Pracownik pozostaje w gotowości świadczenia usługi; klient wymusza usługę swoją decyzją. Pracownik zna swoją pracę; klient nie ma wglądu w tok jego czynności. Z punktu widzenia klienta efektem naprawy jest przywrócenie określonych wartości użytkowych towaru, będące przedmiotem zlecenia (mamy tu zgodność wyniku działania z jego specyfikacją). Pracownik pracuje z anonimowym „towarem do naprawy”; dla klienta przekazany towar jest rozpoznawalny. Pracownik oddaje przez okienko „towar do wydania” (return); klient odbiera swój towar. Pracownik nie wie, co klient zrobi z odebranym towarem, i nie ma na to wpływu.

Samo zadeklarowanie podprogramu sprawia jedynie, że jest on do dyspozycji autora kodu. Instrukcja używająca zadeklarowanego podprogramu nosi nazwę instrukcji wywołania. Postać wywołania zależy od sposobu zadeklarowania podprogramu, od sposobu w jaki wykonuje on swoją pracę, wreszcie od potrzeb.

Funkcje

Rodzaj podprogramów zwany funkcjami charakteryzuje się istnieniem wartości wynikowej, podobnie jak to jest z funkcjami w sensie matematycznym.

W Pythonie wartość wynikowa funkcji jest deklarowana za pomocą instrukcji return wartość.

def odleglosc(x, y):
    """ oblicza długość wektora XY na płaszczyźnie """
    return math.sqrt((x[0]-y[0])**2 + (x[1]-y[1])**2)
def odlegloscRn(x, y):
    """ oblicza długość wektora XY o dowolnej liczbie współrzędnych """
    if len(x) != len(y):
	return None
    suma = 0.0
    for i in range(len(x)):
	suma += (x[i]-y[i])**2
    return math.sqrt(suma)

Podstawowy sposób korzystania z funkcji polega na używaniu ich w obliczanych wyrażeniach. W takim przypadku następuje wywołanie funkcji, a wynik jest wstawiany w to miejsce w wyrażeniu, z którego nastąpiło wywołanie.

ab = odleglosc(punktA, punktB)
ac = odleglosc(punktA, punktC)
oc = odleglosc([0.0, 0.0], punktC)

Wynik funkcji może być dowolnym obiektem, nawet bardzo skomplikowanym.

Funkcji Pythona można także używać jako poleceń, „gubiąc” wynik (ma to sens, o ile funkcja oprócz obliczenia wyniku, „przy okazji” robi jeszcze coś sensownego — w informatyce nazywa się to „efektem ubocznym”).

plik = open(nazwa)
plik.readline()		# pomijamy nieistotny dla nas wiersz przy czytaniu pliku, przesuwając czytnik do początku następnego wiersza
plik.readline()		# drugi wiersz pomijamy podobnie jak pierwszy
dane = plik.readline()	# pobieramy zawartość trzeciego wiersza pliku do zmiennej dane

Wynik może też być ciągiem obiektów (patrz: wielokrotne przypisania omówione pod koniec podrozdziału 5.4.1.)

def cokolwiek():
    ...
    return w1, w2
...
a, b = cokolwiek() # zmienna a dostanie wartość w1, zmienna b dostanie w2

Mówiąc ściślej, w powyższym przykładzie wynikiem działania funkcji cokolwiek jest krotka (tuple) o wartości (w1, w2). W toku przypisania jej składniki są „wypakowywane” do zmiennych a i b.

Procedury

Terminem procedura określa się podprogramy, które nie mają wartości wynikowej

def zapiszWektor(x, nazwa):
    """ zapisuje wektor x do pliku o wskazanej nazwie """
    plik = open(nazwa, 'w')
    for element in x:
	plik.write(str(element)+'\n')
    plik.close()

Deklaracje takich podprogramów są definicjami czynności złożonych lub zależnych od parametrów, zaś każde wywołanie jest rozkazem wykonania w danym momencie uprzednio zadeklarowanej czynności. Tym samym nazwy podprogramów stają się instrukcjami wyższego poziomu.

zapiszWektor([1.0, -3.5, 12.4, 3.1], 'dane1.txt')
a = [-1, 3, 5, -2.17, 81.4]
zapiszWektor(a, 'dane2.txt')
zapiszWektor(['kura', 'koza', 'małpa', 'nic'], 'slowa')

W Pythonie każda procedura jest funkcją o wyniku None. Myślmy o funkcji, kiedy efekt jej działania jest zwracany jako wynik, i o procedurze, kiedy bardziej liczy się jej działanie zewnętrzne.

W innych językach programowania różnice między funkcjami (których wywołanie jest wyrażeniem) a procedurami (których wywołanie jest instrukcją) mogą być wyraźniejsze.

Metody

Szczególny rodzaj podprogramów wiąże się z obiektowym traktowaniem danych.

Funkcje lub procedury, przypisane do konkretnych obiektów i będące ich „właściwościami” noszą w programowaniu nazwę metod.

Wywołanie metody ma w Pythonie postać wynik = nazwaobiektu.nazwametody(argumenty) (o ile jest ona funkcją), lub nazwaobiektu.nazwametody(argumenty) (o ile jest ona procedurą), np.

# użyj metody readline() należącej do obiektu plik, a wynik umieść w zmiennej o nazwie tekst
tekst = plik.readline()	

# użyj metody write() należącej do obiektu plik, i przekaż jej argument tekst
plik.write(tekst)

# użyj metody close() należącej do obiektu plik
plik.close()

# użyj metody encode() z parametrem kodowanie należącej do obiektu napis, a wynik umieść w zmiennej napis 
# (dotychczasowa wartość napis zostanie utracona)
napis = napis.encode(kodowanie)

Deklarowanie przez autora programu własnych metod jest możliwe, lecz w podstawowym kursie nie omówimy wyczerpująco tego zagadnienia. Pewne wskazówki są zawarte w podrozdziale 10.5. poświęconym tworzeniu własnych typów obiektowych. Podrozdział ten ma charakter uzupełniający.

Dokumentowanie podprogramów

We wszystkich podanych przez nas przykładach bezpośrednio po deklaracji podprogramu rozpoczynającej się słowem kluczowym def znajduje się tekst w cudzysłowach. Nie jest to przypadek. W programie Pythona występujące samodzielnie stałe tekstowe pełnią rolę komentarza. Dodatkowa konwencja zakłada, że taka stała umieszczona zaraz po nagłówku deklaracji podprogramu zawiera dokumentację jego użycia. Jest ona pokazywana w systemach pomocy, z których najbardziej elementarnym jest polecenie help().

Do tego typu komentarzy stosowana bywa nazwa napis dokumentujący lub docstring.

Po zdefiniowaniu procedury zapiszWektor(x, nazwa) zgodnie z deklaracją zamieszczoną w podrozdziale 7.6.2, możemy zapytać o sposób jej użycia:

help(zapiszWektor)
Help on function zapiszWektor in module __main__:

zapiszWektor(x, nazwa)
    zapisuje wektor x do pliku o wskazanej nazwie
(END)  
>>>

W środowiskach edycyjnych wspomagających tworzenie kodu źródłowego, takich jak IDLE, WingIDE czy PythonWin, ten sam opis pojawia się w postaci podręcznej podpowiedzi po wpisaniu nazwy funkcji.

Uwagi o wywoływaniu podprogramów

W wywołaniu podprogramu po nazwie podprogramu muszą wystąpić nawiasy z argumentami. W najprostszym przypadku liczba argumentów musi być taka sama, jak zadeklarowana w nagłówku podprogramu (istnieją sposoby deklaracji podprogramów umożliwiające bardziej elastyczne traktowanie parametrów; nie będziemy ich szczegółowo omawiać). Jeżeli w danym wywołaniu argumentów się nie podaje, to nawiasy powinny pozostać puste.

Deklaracje i wywołania
Poprawne wywołanieNiepoprawne wywołanie
import datetime

def dzisiaj():
    return str(datetime.date.today())	# OK

print(dzisiaj())		# OK: wywołanie bez argumentów
import datetime

def dzisiaj():
    return str(datetime.date.today())	# OK

print(dzisiaj)		# błąd rzeczowy: dzisiaj jest podprogramem, a nie zmienną; zobaczysz adres liczbowy tego podprogramu
Poprawne wywołaniaNiepoprawne wywołania
def dodaj(a, b):
    return a + b

print(dodaj(1, 3.5))	# OK: działanie liczba + liczba
print(dodaj('Ala ',' ma kota'))	# OK: tekst + tekst
def dodaj(a, b):
    return a + b

print(dodaj(1, 3.5, 2))	# błąd: dodaj() wymaga 2 argumentów
print(dodaj('Ala ', 3))	# błąd: dodawanie liczba + tekst

Podprogram powinien być zadeklarowany w pliku źródłowym w taki sposób, by jego deklaracja została wczytana przed pierwszym jego wywołaniem. W programie linearnym (bez warunków i iteracji) znaczy to, że deklaracja musi się znajdować przed pierwszą instrukcją wywołania.

Zakresy deklaracji
Poprawna deklaracjaDeklaracja w złym miejscu
def f(a):
    return a**2

x = 1.0
y = f(x)	# OK: f() zdefiniowano wcześniej
x = 1.0
y = f(x)	# błąd: w tym miejscu nie wiadomo, co to jest f()

def f(a):
    return a**2
Poprawna deklaracjaDeklaracja w złym miejscu
def f(a):
    return a**2

def g(a):
    return f(2*a)+1	# OK: deklaracja g() korzysta z f()
    
x = 1.0
y = g(x)	# OK: g() i f() są znane w tym miejscu
def f(a):
    return a**2

x = 1.0
y = g(x)	# błąd: w tym miejscu nie wiadomo, co to jest g()

def g(a):
    return f(2*a)+1	# OK: deklaracja g() korzysta z f()
Poprawna deklaracjaDeklaracja w złym miejscu
def g(a):
    return f(2*a)+1	# OK: deklaracja g() korzysta z f()
			# w tym miejscu nie wiadomo co to 
			# jest f(), ale nikt jej jeszcze 
			# nie każe wywoływać

def f(a):
    return a**2

    
x = 1.0
y = g(x)	# OK: g() i f() są znane w tym miejscu
def g(a):
    return f(2*a)+1	# OK: deklaracja g() korzysta z f()
			# w tym miejscu nie wiadomo co to 
			# jest f(), ale nikt jej jeszcze 
			# nie każe wywoływać
x = 1.0
y = g(x)	# błąd: w tym miejscu znamy g(), ale nie możemy
		# jej obliczyć, bo nie znamy f()

def f(a):
    return a**2

Podprogramy zadeklarowane w modułach można wywołać w dowolnym miejscu kodu po zaimportowaniu modułu.

Import modułu a odwołania do jego zawartości
Poprawne wywołanie
import datetime

def dzisiaj():
    return str(datetime.date.today())	# OK: datetime jest wczytany, 
					# chociaż nie musi

print(dzisiaj())			# OK: datetime jest wczytany
Poprawne wywołanieNiepoprawne wywołanie
def dzisiaj():
    return str(datetime.date.today())	# OK: datetime nie musi 
					# być wczytany w tym miejscu

import datetime
print(dzisiaj())			# OK: datetime jest wczytany
def dzisiaj():
    return str(datetime.date.today())	# OK: datetime nie musi 
					# być wczytany w tym miejscu

print(dzisiaj())		# błąd: nie wczytano modułu datetime
import datetime

Jeżeli podprogram jest zadeklarowany w obrębie innego podprogramu, to nie da się go używać „na zewnątrz”. Ta sama reguła dotyczy także obiektów przechowujących dane.

Użycie funkcji zadeklarowanych lokalnie
Poprawne wywołanieNiepoprawne wywołanie
def cosinus(a, b):
    """ oblicza cosinus kąta między wektorami na płaszczyźnie """
    # OK: deklaruje lokalne funkcje pomocnicze dlugosc() i iloczyn()

    def dlugosc(x):
	return (x[0]**2 + x[1]**2)**0.5

    def iloczyn(x, y):
	return x[0]*y[0] + x[1]*y[1]

    m = dlugosc(a)*dlugosc(b)
    if m != 0.0:
	return iloczyn(a, b)/m

print(cosinus([1,0], [0,1]))	# OK

def cosinus(a, b):
    """ oblicza cosinus kąta między wektorami na płaszczyźnie """
    # OK: deklaruje lokalne funkcje pomocnicze dlugosc() i iloczyn()

    def dlugosc(x):
	return (x[0]**2 + x[1]**2)**0.5

    def iloczyn(x, y):
	return x[0]*y[0] + x[1]*y[1]

    m = dlugosc(a)*dlugosc(b)
    if m != 0.0:
	return iloczyn(a, b)/m

print(cosinus([1,0], [0,1]))	# OK
print(dlugosc([1,0]))	# na tym poziomie nie wiemy co to jest dlugosc()
Poprawne wywołanie
def dlugosc(x):
    return (x[0]**2 + x[1]**2)**0.5

def iloczyn(x, y):
    return x[0]*y[0] + x[1]*y[1]

def cosinus(a, b):
    """ oblicza cosinus kąta między wektorami na płaszczyźnie """
    # OK: korzysta z zewnętrznych funkcji dlugosc() i iloczyn()
    m = dlugosc(a)*dlugosc(b)
    if m != 0.0:
	return iloczyn(a, b)/m

print(cosinus([1,0], [0,1]))	# OK
print(dlugosc([1,0]))	# OK

Argumenty podprogramów i zmienne w podprogramach

Przykłady

[]

Funkcje jako argumenty podprogramów

W klasycznie rozumianych algorytmach podział na kod i dane jest ściśle przestrzegany. Funkcja — rozumiana jako wyodrębniona i opatrzona nazwą część kodu źródłowego — mogła być użyta tylko w celu wykonania instrukcji zawartych w jej deklaracji. Współczesne języki programowania pozwalają na większą elastyczność.

W Pythonie funkcje, czyli fragmenty kodu zadeklarowane za pomocą instrukcji def, są — podobnie jak zmienne — obiektami programistycznymi. Python pozwala nie tylko na wywoływanie funkcji, ale dopuszcza także przechowywanie ich w zmiennych i przekazywanie ich innym funkcjom jako argumentów. Zastosowanie tej techniki stwarza ogromne możliwości.

Przykład: sumowanie wartości funkcji

Chcemy dla danej listy x obliczyć sumę wartości f(a), gdzie a przebiega zbiór wszystkich elementów listy, zaś f jest wcześniej zadeklarowaną funkcją. Kod jest oczywisty:

# x jest listą wartości liczbowych
# f jest funkcją jednej zmiennej o wartościach typu float
s = 0.0
for a in x:
    s += f(a)
# wynik jest w s

W innym miejscu chcielibyśmy zrobić to samo, ale z inną funkcją g w miejsce f. Sytuacja taka zachęca do mechanicznego kopiowania kodu i podmiany nazwy funkcji:

s = 0.0
for a in x:
    s += g(a)

Nie jest to korzystne ani dla programu (przyczynia się do zwiększenia jego objętości), ani dla jego autorów (zmniejsza czytelność, sprawia problemy przy przebudowie kodu). Aby tego uniknąć, utworzymy funkcję

def sumujwartosci(funkcja, lista):
    """ sumuje wartości funkcji po wszystkich elementach listy"""
    s = 0.0
    for a in lista:
	s += funkcja(a)
    return s

i używamy jej, w miejsce argumentu funkcja wstawiając nazwę podprogramu obliczającego potrzebną nam funkcję:

...
s = sumujwartosci(f, x)
...
s = sumujwartosci(g, x)
...
# możemy także użyć go dla zsumowania wartości dowolnej innej funkcji jednej zmiennej w dowolnym zbiorze liczb
u = sumujwartosci(math.sin, p)
...

Oczywiście funkcje f i g muszą być gdzieś wyżej zadeklarowane za pomocą instrukcji def f(x): i def g(x):, tak by w chwili wywołania system wiedział, co ma obliczać.

Mapowanie

Standardowa funkcja map(funkcja, lista) buduje listę, której elementy są wartościami danej funkcji obliczonymi dla wszystkich elementów danej listy. Na przykład instrukcja y = map(fff, x) zbuduje tablicę wartości funkcji fff dla listy argumentów pobranej z tablicy x.

Przykład: nazwa funkcji jako argument przekazywany metodzie bisekcji

Zamiast budować procedurę rozwiązywania przybliżonego równań osobno dla każdego typu równania, tworzymy „biblioteczną” funkcję wyznaczającą przybliżone rozwiązanie równania postaci f(x) = 0, gdzie f jest podprogramem opisującym lewą stronę równania, przekazywanym jako argument.

Procedury jako argumenty

W ten sam sposób wolno przekazywać nie tylko nazwy funkcji używanych w obliczanych wyrażeniach, ale także nazwy procedur, których używa się w charakterze instrukcji.

W przykładzie niżej pokazujemy sposób wykorzystania numerowanej listy procedur do obsługi żądań operatora w aplikacji użytkowej. Przykład ten jest kontynuacją przykładu z podrozdziału 7.5.9.

[]

W pierwszej wersji programu skorzystano z interfejsu terminala znakowego

[]

Wersja druga wykorzystuje okno dialogowe modułu graficznego easygui

Pytania kontrolne

  1. Podaj sensowne przykłady wykorzystania różnych wariantów instrukcji warunkowej
  2. Podaj sensowne przykłady wykorzystania różnych wariantów instrukcji iteracji
  3. Jakie obiekty mogą być przedmiotem iteracji for w Pythonie?
  4. W jaki sposób definiować własne funkcje?
  5. Czym różni się deklaracja funkcji od jej wywołania?
  6. Czym różni się funkcja od procedury?
  7. Co to są argumenty funkcji lub procedury?
  8. Czym jest wynik funkcji i w jaki sposób można z niego korzystać?
© Copyright 2000–2022 by Jan Jełowicki, Katedra Matematyki Uniwersytetu Przyrodniczego we Wrocławiu
Ostatnia modyfikacja w marcu 2022 r.
janj@aqua.up.wroc.pl
http://karnet.up.wroc.pl/~jasj