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.
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.
Instrukcji proste opisują pojedyncze niezwiązane ze sobą polecenia. Służyły nam one do:
zmienna = wartość # obliczona wartość wyrażenia zostanie zapamiętana w zmiennej
del zmienna # instrukcja del usuwa wskazaną zmienną
zmienna = wartość # zastąpienie istniejącej wartości zmiennej nową wartością zmienna += wartość zmienna -= wartość zmienna *= wartość zmienna /= wartość
input('naciśnij Enter...') # wstrzymanie pracy programu terminalowego plik.readline() # przeczytanie wiersza pliku bez zapamiętywania go, # a jedynie w celu przesunięcia głowicy do początku następnego wiersza
max([a,b,c]) # funkcja max() wyznacza największy element ciągu y = 1 + math.sin(2*x) # funkcja sin() z modułu math oblicza wartość sinusa dane2 = sorted(dane) # funkcja sorted() zwraca ten sam ciąg po uporządkowaniu jego elementów
print(napis.title() # metoda title() przekształca tekst tak, by każde jego słowo rozpoczynało się z wielkiej litery dane.sort() # metoda sort() porządkuje ciąg danych
Instrukcja pusta pass
nie powoduje żadnej akcji. Przydaje się
w sytuacjach szczególnych.
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
.
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:
if
,elif
,else
,""" 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
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.
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.
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')
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.
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'
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.
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.
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:
wrzuć łopatę węgla do piwnicy; wrzuć łopatę węgla do piwnicy; wrzuć łopatę węgla do piwnicy; wykonaj trzy razy: wrzuć łopatę węgla do piwnicy; niech n = 10; wykonaj n razy: wrzuć łopatę węgla do piwnicy; dopóki przed wsypem leży węgiel, powtarzaj: wrzuć łopatę węgla do piwnicy;
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.
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.
Sumowanie ciągu danych |
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 stop−poczatek 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.
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?
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 |
|
W drugim wariancie iteracja przebiega zbiór elementów listy |
|
W ostatnim wariancie iteracji kolejne wartości elementów listy |
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 >>>
.
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.
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()
.
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.
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) |
Wyczerpywanie losowych porcji z zasobnika.
Wyczerpywanie losowe |
Metoda bisekcji (więcej szczegółów w rozdziale 9.)
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
).
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.
Warunek wewnątrz pętli
Pętla i układ warunków wewnątrz pętli
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).
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
.
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.
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
.
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.
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.
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.
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.
Poprawne wywołanie | Niepoprawne 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łania | Niepoprawne 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.
Poprawna deklaracja | Deklaracja 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 deklaracja | Deklaracja 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 deklaracja | Deklaracja 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.
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łanie | Niepoprawne 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.
Poprawne wywołanie | Niepoprawne 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 |
is
(patrz
podrozdział 10.1.3.)global
Dotyczy to wszystkich obiektów istniejących w chwili wywołania podprogramu.
Nie muszą one istnieć w miejscu, w którym znajduje się deklaracja podprogramu.
Przy wywoływaniu takiego podprogramu argumenty z wartością domyślną wolno pominąć:def powieksz(obiekt, skala = 1.0, srodek = [0, 0]): ''' tworzy obraz obiektu płaskiego przekształcony w jednokładności o zadanej skali i centrum. obiekt powinien być listą dwuwymiarowych punktów. ''' obraz = [] for punkt in obiekt: obraz.append([srodek[0] + skala*(punkt[0]-srodek[0]), srodek[1] + skala*(punkt[1]-srodek[1])]) return obraz
Wolno także nadawać im wartości inne niż domyślne za pomocą podstawienia w nagłówku:a = powieksz(cos) # parametry skala i srodek mają wartości takie, jak w nagłówku deklaracji
Kolejność nazwanych parametrów w wywołaniu nie ma znaczenia, jednak w wywołaniu funkcji po argumencie nazwanym nie można już umieścić argumentu bez nazwy.b = powieksz(cos, skala = 2) # srodek jest taki, jak w deklaracji c = powieksz(cos, srodek = [-1, 1.2]) # skala jest taka, jak w deklaracji d = powieksz(cos, skala = math.sqrt(2), srodek = [-1, 3.3]) # oba parametry mają wartości jawnie zadane e = powieksz(cos, srodek = [-1, 3.3], skala = math.sqrt(2))
b = powieksz(cos, 2.5, [0.1, -2.1]) # kolejność argumentów zgodna z kolejnością w deklaracji b = powieksz(cos, 2.5) # pominięte argumenty mają wartości domyślne c = powieksz(cos, srodek = [-1, 1.2]) d = powieksz(cos, skala = math.sqrt(2), srodek = [-1, 3.3])b = powieksz(cos, [0.1, -2.1]) # pomylona kolejność argumentów bez użycia nazw b = powieksz(cos, [0.1, -2.1], 2.5) # pomylona kolejność argumentów bez użycia nazw b = powieksz(skala = 2, cos) # argument bez nazwy po argumencie nazwanym c = powieksz(cos, srodek = [-1, 1.2], 2.5) # argument bez nazwy po argumencie nazwanym
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.
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ć.
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
.
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.
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 |
for
w Pythonie?