Helion - Myśl W Języku Python! Nauka Programowania. Wydanie II

You might also like

Download as pdf or txt
Download as pdf or txt
You are on page 1of 261

Helion~ Alllen B.

Downey
Spis treści

Przedmowa .............................................................................................................. 11

1. Jak w programie ....................................................................................................... 21


Czym jest program? 21
Uruchamianie interpretera języka Python 22
Pierwszy program 23
Operatory arytmetyczne 23
Wartości i typy 24
Języki
fo rmalne i naturalne 25
Debugowanie 26
Słownik 27
Ćwicze nia 29

2. Zmienne, wyrażenia i instrukcje „„„„„„„„„„„„„„„„„„„„„„„„„„„„„„„„„„„„„„„„ 31


Instrukcje przypisania 31
Nazwy zmiennych 31
Wyrażenia i instrukcje 32
Tryb skryptowy 33
Kolejność operacji 34
Operacje na łańcuchach 35
Komentarze 35
Debugowanie 36
Słownik 36
Ćwicze nia 38

3. Funkcje ..................................................................................................................... 39
Wywołania funkcji 39
Funkcje matematyczne 40
Złożenie 41
Dodawanie nowych funkcji 41

3
Definicje i zastosowania 42
Przepływ wykonywania 43
Parametry i argumenty 43
Zmien ne i param etry są lokalne 44
Diagramy stosu 45
Funkcje „owocne" i „puste" 46
Dlaczego funkcje? 47
Debugowanie 47
Słownik 48
Ćwiczenia 49

4. Ana liza przypadku: projekt interfejsu „„„„ „„„„ „„„„ „„„„ „„„„„„„„„„„. „ •• „„.„• • „ • • 53
Moduł turtle 53
Proste powtarzanie 54
Ćwiczenia 55
Hermetyzowanie 56
Uogólnianie 56
Projekt interfej su 57
Refakto ryzacj a 58
Plan p rojektowania 59
Notka dokumentacyjna 60
Debugowanie 60
Słownik 61
Ćwiczenia 62

5. Instrukcje warunkowe i rekurencja „.„„„„„„ „ „ „ „ „„„„„„„„ „„„„ „ „ „ „ „ „ „ „ .„. „ „ „„. 65


Dzielenie bez reszty i wartość bezwzględna 65
Wyrażenia boolowskie 66
Operatory logiczne 66
Wykonywanie warunkowe 67
Wykonywanie alternatywne 67
Łańcuchowe instrukcje warunkowe 68
Zagnież dżo ne instrukcje warunkowe 68
Rekurencja 69
Diagramy stosu dla funkcji reku rencyjnych 70
Rekurencja nieskończo na 71
Dane wprowadzane z klawiatury 71
Debugowanie 72
Słownik 73
Ćwiczeni a 74

4 Spis trełcl
6. Funkcje „owocne" „„„„„„„„„„ „„„„ „„„„ „„.„.„„.„•• „ •• „ •• „ •• „ •• „ •• „ • • „ •• „.„.„„„„.„ .„ 79
Wartości zwracane 79
Projektowanie przyrostowe 80
Złożenie 82
Funkcje boolowskie 82
Jeszcze wi ęcej rekurencji 83

„Skok wiary 85
Jeszcze jeden przykład 86
Sprawdzanie typów 86
Debugowanie 87
Słownik 88
Ćwicze nia 89

7. Iteracja „ „ „„„„ „„„„ „„„„ „„„„ „„„„ .„.„„ .„•• „ •• „ •• „ •• „ •• „ •• „.„ •• „.„• •„ „ .„ ••• „„„„• •„„ •• 91
Ponowne przypisanie 91
Aktualizowanie zmiennych 92
Instrukcja while 92
Instrukcja break 94
Pierwiastki kwadratowe 94
Algorytmy 96
Debugowanie 96
Słownik 97
Ćwiczenia 98

8. Łańcuchy .................................................................................................................101
Łańcuch jest ciągiem 101
Funkcja len 102
Operacja przech odzenia za po mocą pętli for 102
Fragmenty łańcuchów 103
Łańcuchy są niezmienne 104
Wyszukiwanie 104
Wykonywanie pętli i liczenie 105
Metody łańcuchowe 105
Operator in 106
Po równanie łańcuchów 107
Debugowanie 107
Słownik 109
Ćwicze nia 110

Spis tre~cl S
9. Ana liza przypadku: gra słów „ .„„ „„„„ „„„„ „„„„ „„„„ •• „ •• „ •• „„„ •• „ •• „ •• „ •• „ „.„.„ •• „. 113
Odczytywanie list słów 113
Ćwi czeni a 114
Wyszukiwanie 115
Wykonywanie pętli z wykorzystaniem indeksów 116
Debugowanie 117
Słownik 118
Ćwiczenia 118

1O. Listy „ „ •• „ •• „ •• „ •• „ •• „ ••• „. „ ••• „ . „ •• „ ••• „. „ •• „ •• „ •• „ •• „ •• „ •• „ ••• „. „ •• „ •• „ •• „ •• „ •• „ •• „ . „„ „„ „ 121


Lista to ciąg 121
Listy są zmienne 122
Operacja przechodzenia listy 123
Operacje na listach 123
Fragmenty listy 124
Metody list 124
Odwzorowywanie, filtrowanie i redukowanie 125
Usuwanie elementów 126
Listy i łańcuchy 127
Obiekty i wartości 127
Tworzenie aliasu 128
Argumenty listy 129
Debugowanie 131
Słownik 132
Ćwiczeni a 133

11. Słowni ki „. „„ „ „ „„ „„. „. „„ „„ „„ „ „ „„ „„ „„ „ „ „„ „ „ „„ „„ „„ „„ „„ „„ „„ „ „ „„ „„ „„ „.„. 13 7


Słownik to o dwzorowanie 137
Słownik jako kolekcja liczników 139
Wykonywanie pętli i słowniki 140
Wyszukiwanie odwrotne 140
Słowniki i listy 141
Wartości zapamiętywane 143
Zmienne globalne 144
Debugowanie 146
Słownik 146
Ćwiczenia 148

6 Spis trełcl
12. Krotki „„„„„„„„„„„„„„„„„„„„„„„„„„„„„„„„„„„„„„„„ „„„„ „ „ „ „ „ „ „ „ „„„„ „„„151

Krotki są niezmienne 151


Przypisywanie krotki 152
Krotki jako wartości zwracane 153
Krotki argumentów o zmiennej długości 153
Listy i krotki 154
Słowniki i krotki 156
Ciągi ciągów 157
Debugowanie 158
Słownik 159
Ćwicze nia 159

13. Analiza przypadku: wybór struktury danych .............................................................. 163


Analiza częstości występowania słów 163
Liczby losowe 164
H istogram słów 165
Najczęściej używa ne słowa 166
Parametry opcjonalne 167
O dejmowanie słowników 167
Słowa losowe 168
Analiza Markowa 169
Struktury danych 171
Debugowanie 172
Słownik 173
Ćwiczenia 174

14. Pliki .........................................................................................................................175


Trwałość 175
Odczytywanie i zapisywanie 175
Operator fo rmatu 176
Nazwy plików i ścieżki 177
Przechwytywanie wyjątków 178
Bazy danych 179
Użycie mo dułu piekle 180
Potoki 181
Zapisywanie modułów 182
Debugowanie 183
Słownik 183
Ćwiczenia 184

Spis tre~cl 7
15. Kia sy i obi ekty „„ „„ „„ „„ „„ „„ „„ „„ „„ „„ „„ „„ „„ „„ „„ „„ „„ . • „ „ „ . • „ . • „ . • „ .• „ „. „.„ •• „. 187
Typy definiowane przez programistę 187
Atrybuty 188
Prostokąty 189
Instancje jako wartości zwracane 190
Obiekty są zmienne 190
Kopiowanie 191
Debugowanie 192
Słownik 193
Ćwiczenia 194

16. Klasy i funkcje ......................................................................................................... 195


Klasa Time 195
Funkcje „czyste" 196
Modyfikatory 197
Porównanie prototypowania i planowania 198
Debugowanie 199
Słownik 200
Ćwiczeni a 201

17. Klasy i metody ........................................................ „ •• „ •• „ •• „ •• „ •• „ •• „ •• „ •••.•••.•••.•••.•• 203


Elementy obiektowe 203
Wyświetlanie obiektów 204
Kolejny przykład 205
Bardziej złożony przykład 206
Metoda init 206
Metoda _str_ 207
Przeciążanie operatorów 207
Przekazywanie oparte na typie 208
Polimorfizm 209
Interfejs i implementacja 210
Debugowanie 211
Słownik 211
Ćwiczenia 212

18. Dziedziczenie .„ •• „ •• „ •• „ •• „ •• „ •• „ •• „ •• „„„„„„ . • „„„ .• „ .• „ . • „ .• „ •• „ .• „ . • „ . • „ .• „ .• „ . •• „.„ •• „. 213


Obiekty kart 213
Atrybuty klasy 214
Porównywanie kart 215
Talie 216
Wyświetlanie talii 216

8 Spis trełcl
Dodawanie, usuwanie, przenoszenie i sortowanie 217
Dziedziczenie 218
Diagramy klas 219
Hermetyzacja danych 220
Debugowanie 221
Słownik 222
Ćwiczenia 223

19. Przydatne elementy .„„„„„„„„„„„„.„.„„.„„.. „..„.. „.. „.. „.. „.. „.. „.. „.. „.. „.. „.. „..„...227
Wyrażenia warunkowe 227
Wyrażenia listowe 228
Wyrażenia generatora 229
Funkcje any i all 230
Zbiory 230
Liczniki 232
defaultdict 232
Krotki z nazwą 234
Zbieranie argumentów słów kluczowych 235
Słownik 236
Ćwiczenia 236

20. Debugowanie „„„„„„„„ .. „..„.. „..„.. „..„.. „..„.. „..„.. „..„.. „.. „.. „.. „.„„.„ .. „.„„„„„„.237
B łędy składni owe 237
Błędy uruchomieniowe 239
B łędy semantyczne 242

21 . Analiza algorytmów .„„„„.„.... „.„ .. „.. „.......„.„ .......„.. „.. „.„.„„„„„„„„„„„„„„„„„.247


Tempo wzrostu 248
Analiza podstawowych operacji w języku Python 250
Analiza algorytmów wyszukiwania 252
Tablice mieszające 252
Słownik 256

Skorowidz „„„„„..„... „.„... „.„ .. „...„.„..„.. „..„.. „..„.. „... „.„.. „.. „.. „.. „.. „.. „.„„„„„...257

Spis tre~cl 9
10 Spis trełcl
Przedmowa

Dziwna historia książki


W styczniu 1999 r. przygotowywałem się do zajęć wprowadzających do programowania w języku
Java. Uczyłem tego trzy razy i byłem sfrustrowany. W przypadku tych zajęć wskaźnik braku zaliczenia był
zbyt wysoki. Nawet wśród studentów, którzy zajęcia zaliczyli, ogólny poziom wyników był za niski.
Jednym z problemów, jakie dostrzegłem, były książki. Były zbyt obszerne i zawierały za dużo niepo-
trzebnych szczegółów dotyczących języka Java, w niewystarczającym stopniu natomiast pojawiały się
w nich ogólne wytyczne związane z tym, jak programować. Wszyscy studenci padali ofiarą efektu
zapadni: zaczynali z łatwością, stopniowo przechodzili dalej, a następnie w okolicach rozdziału 5.
miało miejsce załamanie. Idąc tą drogą, musieliby przyswoić zbyt wiele nowego materiału w zbyt krót-
kim czasie. Ja byłbym zmuszony poświęcić resztę semestru na wybieranie materiału do nauczenia.

Dwa tygodnie przed pierwszym dniem zajęć zdecydowałem się na napisanie własnej książki. Moje
cele były następujące:
• Zapewnienie zwięzłości. Lepsze dla studentów będzie przeczytanie 10 stron niż nieprzeczytanie 50
stron.
• Zachowanie ostrożnościw zakresie terminologii. Spróbowałem zminimalizować żargon i zdefi-
niować każdy termin przy jego pierwszym użyciu.
• Stopniowe budowanie. Aby uniknąć efektu zapadni, najtrudniejsze zagadnienia podzieliłem
na serie złożone z niewielkich kroków.
• Skoncentrowanie się na programowaniu, a nie na języku programowania. Uwzględniłem mi-
nimalny podzbiór przydatnych elementów języka Java i pominąłem resztę.
Potrzebowałem tytułu, dlatego spontanicznie wybrałem następujący: How to Th ink Like a Com-
puter Scientist (w jaki sposób rozumować jak informatyk).
Moja pierwsza wersja była niedopracowana, ale się sprawdziła. Studenci przeczytali ją w całości i zro-
zumieli na tyle, że czas na zajęciach mogłem poświęcić na trudne i interesujące zagadnienia, a oni,
co najważniejsze, mogli ćwiczyć.
Książka została wydana w ramach licencji GNU Free Documentation License, która umożliwia
użytkownikom kopiowanie i modyfikowanie treści książki oraz jej dystrybucj ę.

11
To, co miało miejsce później, to ciekawa część. JeffElkner, nauczyciel w liceum położonym w stanie
Virginia, przystosował moją książkę do języka Python. Wysłał mi kopię swoich modyfikacji. Dzięki
temu miałem okazję w niezwykły sposób uczyć się języka Python, czytając własną książkę.
Pierwsza edycja książki przewidzianej dla języka Python została wydana w 2001 r. przez moje wydaw-
nictwo Green Tea Press.
W 2003 r. zacząłem prowadzić zajęcia na uczelni Olin College i po raz pierwszy uczyłem języka
Python. Kontrast między tym językiem a językiem Java był niesamowity. Studenci mieli mniejsze
trudności, więcej się uczyli, brali udział w bardziej interesujących projektach i ogólnie rzecz bio-
rąc, dawało im to o wiele więcej satysfakcji.

Od tamtego czasu w dalszym ciągu rozwijałem książkę, usuwając błędy, ulepszając niektóre przy-
kłady i dodając materiał, a zwłaszcza ćwiczenia.

Rezultatem jest niniejsza książka, która obecnie ma mniej okazały tytuł. Oto niektóre zmiany:
• Na końcu każdego rozdziału dodałem podrozdział poświęcony debugowaniu. Prezentuję w nim
ogólne techniki znajdowania i unikania błędów, a także ostrzeżenia dotyczące pułapek w kodzie
Python.
• Dodałem więcej ćwiczeń, które obejmują zarówno krótkie testy znajomości zagadnień, jak i kilka
pokaźnych projektów. Większość ćwiczeń zawiera odnośnik do mojego rozwiązania.
• Dodałem serię analiz przypadku - są to obszerniejsze przykłady z ćwiczeniami, rozwiąza­
niami i omówieniem.
• Rozszerzyłem omówienie planów projektowania programów oraz podstawowych wzorców
projektowych.
• Dołączyłem dodatki dotyczące debugowania i analizy algorytmów.

W drugim wydaniu książki pojawiły się następujące nowości:


• Treść książki wraz z całym dołączonym kodem zaktualizowano pod kątem języka Python 3.
• Dodałem kilka podrozdziałów i więcej szczegółów dotyczących technologii internetowych,
aby początkującym ułatwić uruchamianie kodu Python w przeglądarce. Oznacza to, że nie musisz
zajmować się instalacją środowiska języka Python, jeśli nie uznasz tego za konieczne.

• W podrozdziale „Moduł turtle" rozdziału 4. zrezygnowałem z własnego pakietu graficznego o na-


zwie Swampy na rzecz bardziej standardowego modułu turtle języka Python, który jest łatwiejszy
do zainstalowania, a ponadto zapewnia większe możliwości.
• Dodałem nowy rozdział zatytułowany „Przydatne elementy" zawierający wprowadzenie do
kilku dodatkowych elementów języka Python, których wykorzystanie nie jest bezwzględnie
konieczne, ale czasami okazują się one przydatne.
Mam nadzieję, że praca z tą książką sprawi Ci przyjemność, a ponadto ułatwi naukę programo-
wania i rozumowania jak informatyk, przynajmniej odrobinę.
- Allen B. Downey
Olin College

12 Pnedmowa
Konwencje zastosowane w książce
W książce zastosowano następujące konwencje typograficzne:
Kursywa
Wskazuje nowe pojęcia, adresy URL, adresy e- mail, nazwy plików oraz ich rozszerzenia.
Pogrubienie
Wskazuje terminy zdefiniowane w słowniku.
Czc ionka o stałej s ze rokośc i

Konwencja używana w treści akapitów w odniesieniu do takich elementów programu jak na-
zwy zmiennych lub funkcji, a także w przypadku baz danych, typów danych, zmiennych śro­
dowiskowych, instrukcji i słów kluczowych.
Pogrubiona czcionka o stałej szerokości

Służy do wskazania poleceń lub innego tekstu, który powinien zostać dosłownie wpisany przez
użytkownika.

Wykorzystanie przykładów z kodem


Dodatkowy materi ał (przykłady z kodem, ćwiczenia itp.) jest dostępny do pobrania pod adresem
f tp :/lftp. hel ion.pl!przyklady/myjep2 .zip.
Książka ma na celu ułatwienie Ci realizowania zadań. Ogólnie rzecz bi orąc, jeśli przykładowy kod
dołączono do książki, możesz używać go we własnych programach i dokumentacji. Nie musisz
kontaktować się z nami, aby uzyskać zgodę, chyba że wykorzystujesz ponownie znaczną część ko-
du. Na przykład tworzenie programu, w którym użyto kilku porcji kodu z książki, nie wymaga
zgody. Sprzedaż lub dystrybucja dysku CD-ROM z przykładami z ksi ążek wydawnictwa wymaga
zgody. Udzielanie odpowiedzi na pytanie poprzez zacytowanie fragmentu z książki i podanie
przykładowego kodu nie wymaga zgody. Dołączenie znacznej ilości przykładowego kodu z książ­
ki w dokumentacji własnego produktu wymaga uzyskania zgody.
Doceniamy podanie informacji o prawach autorskich, lecz nie wymagamy tego. Informacje takie
obejmują zwykle tytuł, autora, wydawcę oraz numer ISBN. Oto przykład: ,,Myśl w języku Python!
Nauka programowan ia. Wydanie II, Allen B. Downey, Helion, ISBN: 978-83-283-3002-3".
Jeśli uznasz, że zamierzasz wykorzystać przykłady z kodem w sposób wykraczający poza granice
dozwolonego użycia lub podanych powyżej wariantów uzyskiwania zgody, skontaktuj się z nami
za pośrednictwem adresu helion@helion.pl.

Podziękowania
Gorące podziękowania dla Jeffa Elknera, który przystosował do języka Python moją książkę po-
świ ęconą językowi Java. Dzięki temu projekt ten się rozpoczął i nauczyłem się czegoś, co okazało
się później moim ulubionym językiem.

Podziękowania 13
Podziękowani a również dla Chrisa Meyersa, który wziął udział w tworzeniu kilku podrozdziałów
książki How to Think Like a Computer Scientist.

Dziękuję organizacji Free Software Foundation za opracowanie licencji GNU Free Documenta-
tion License, która ułatwiła mi nawiązanie współpracy z Jeffem i Chrisem. Dziękuję organizacji
Creative Commons za licencję, z której korzystam obecnie.

Dziękuję redaktorom wydawnictwa Lulu, którzy zajmowali si ę książką How to Think Like a Com-
puter Scientist.
Podziękowania dla redaktorów wydawnictwa O'Reilly Media, którzy pracowali nad książką Think
Python.

Dziękuję wszystkim studentom, którzy brali udział w tworzeniu wcześniejszych wersji książki, oraz
wszystkim współpracownikom (wymienionym poniżej), którzy przesyłali poprawki i sugestie.

Lista współpracowników
W ciągu kilku minionych lat ponad 100 uważnych i wnikliwych czytelników przesłało mi sugestie
i poprawki. Ich wkład i entuzjazm związany z tym projektem okazał się ogromną pomocą.
Jeżeli przesyłając uwagi, dołączysz przynajmniej część zdania, w którym występuje błąd, ułatwi
mi to wyszukiwanie. Numery stron i nazwy podrozdziałów to również świetne informacje, ale aż
tak nie ułatwiają pracy. Dzięki!
• Lloyd Hugh Allen przesłał poprawkę dotyczącą podrozdziału 8.4.
• Yvon Boulianne przesłała poprawkę dotyczącą błędu semantycznego w rozdziale 5.
• Fred Bremmer przesłał poprawkę dotyczącą podrozdziału 2.1.
• Jonah Cohen napisał skrypty języka Perl dokonujące konwersji kodu źródłowego LaTeX książki
do postaci p ięknego kodu HTML.
• Michael Conlon przesłał poprawkę dotyczącą gramatyki w rozdziale 2. oraz poprawił styl w roz-
dziale 1. Michael zainicjował dyskusję poświęconą technicznym aspektom interpreterów.
• Benoit Girard przesłał poprawkę zabawnej pomyłki w podrozdziale 5.6.
• Courtney Gleason i Katherine Smith utworzyły plik horsebet.py, który we wcześniejszej wersji
książki odgrywa rolę analizy przypadku. Ich program można obecnie znaleźć w witrynie in-
ternetowej autora ksi ążki.
• Lee Harr wysłał więcej poprawek, niż można pomieścić w tym zestawieniu. Tak naprawdę
powinien zostać wymieniony jako jeden z głównych redaktorów zajmujących się tekstem.
• James Kaylin to student korzystający z treści książki. Przesłał liczne poprawki.
• David Kershaw naprawił funkcję catTw i ce z podrozdziału 3.10, która nie działała.
• Eddie Lam przesłał liczne poprawki do rozdziałów 1., 2. i 3. Poprawił też plik Makefile, dzięki
czemu tworzy on indeks przy pierwszym uruchomieniu. Eddie pomógł nam przygotować
schemat numeracj i wersji.

14 Pnedmowa
• Man-Yong Lee przesłał poprawkę dotyczącą kodu przykładu z podrozdziału 2.4.
• David Mayo wskazał, że słowo unconsciously z rozdziału 1. wymagało zmiany na słowo sub-
consciously.
• Chris McAloon przesłał kilka poprawek dotyczących podrozdziałów 3.9 i 3. 10.
• Matthew J. Moelter to od dawna zaangażowana osoba, która przesłała liczne poprawki i suge-
stie dotyczące książki.
• Simon Dicon Montford zgłosił brak definicji funkcji oraz kilka literówek w rozdziale 3. Zna-
lazł również błędy w funkcji i ncrement z rozdziału 13.

• John Ouzts poprawi ł definicję wartości zwracanej w rozdziale 3.


• Kevin Parks przesłał wartościowe komentarze i sugestie odnoszące się do sposobu uspraw-
nienia dystrybucji książki.
• David Pool przesłał informację o literówce w słowniku z rozdziału 1., a także miłe słowa otuchy.
• Michael Schmitt przesłał poprawkę do rozdziału poświęconego plikom i wyjątkom.
• Robin Shaw wskazał błąd w podrozdziale 13.1, gdzie funkcja pri ntTime została użyta w przy-
kładzie bez definicji.

• Paul Sleigh znalazł błąd w rozdziale 7„ a także błąd w skrypcie Perl Jonaha Cohena, który ge-
neruje kod HTML na podstawie kodu LaTeX.
• Craig T. Snydal sprawdza treść ksi ążki w ramach kursu na uczelni Drew University. Przeka-
zał kilka cennych sugestii i poprawek.

• Ian Thomas i jego studenci ko rzystają z t reści książki podczas kursu z zakresu programowa-
nia. Jako pierwsi sprawdzili treść rozdziałów drugiej połowy książki, a ponadto przesłali liczne
poprawki i sugestie.
• Keith Verheyden przesłał poprawkę do rozdziału 3.
• Peter Winstanley po i nformował nas o istniejącym od dawna błędzie w rozdziale 3. dotyczącym
czcionek Latin.
• Ch ris Wrobel zgłosił poprawki kodu w rozdziale poświęconym wyjątkom i plikowym opera-
cjom wejścia-wyjścia.
• Moshe Zadka miał bezcenny wkład w projekt zwi ązany z książką. Oprócz tego, że napisał pierw-
szą wersję roboczą rozdziału poświęconego słownikom, stale służył wskazówkami w początkowych
fazach powstawania książki.
• Ch ristoph Zwerschke przesłał kilka poprawek i sugestii natury pedagogicznej, a także wyja-
śnił różnicę mi ędzy słowami gleich i selbe.
• James Mayer przesłał nam całe mnóstwo informacj i o błędach pisowni i typograficznych, w tym
dwóch znajdujących się w obrębie listy wspó łpracowników.
• Hayden McAfee wychwycił niespój ność między dwoma przykładami potencjalnie powodują­
cą niejasności.

• Angel Amal należy do międzynarodowego zespołu tłumaczy zajmujących się hiszpańskojęzyczną


wersją tekstu książki. Znalazł też kilka błędów w wersji anglojęzycznej.

Lista współpracowników 15
• Tauhidul Hoque i Lex Berezhny stworzyli ilustracje z rozdziału 1. i ulepszyli wiele innych ilu-
stracji.
• Dr Michele Alzetta wychwyciła błąd w rozdziale 8., a także przesłała kilka interesujących ko-
mentarzy natury pedagogicznej oraz sugestii dotyczących ciągu Fibonacciego i gry Piotruś.
• Andy Mitchell wychwycił literówkę w rozdziale 1. i niedziałający przykład w rozdziale 2.
• Kalin Harvey zaproponował wyjaśnienie w rozdziale 7. i wychwycił kilka literówek.
• Christopher P. Smith znalazł kilka literówek i pomógł nam w aktualizacji książki pod kątem
języka Python 2.2.

• David Hutchins wyłapał literówkę w słowie wstępnym.


• Gregor Lingi uczy języka Python w liceum położonym w austriackim Wiedniu. Zajmuje si ę
niemieckojęzycznym tłumaczeniem książki i wychwycił kilka poważnych błędów w rozdziale 5.

• Julie Peters znalazła literówkę w przedmowie.


• Florin Oprina przesłała ulepszenie funkcji makeTime, poprawkę funkcji pr in tTime oraz znala-
zła ładną literówkę.

• D.J. Webre zasugerował wyjaśnienie w rozdziale 3.


• Ken znalazł kilka błędów w rozdziałach 8., 9. i 11.
• Ivo Wever wychwycił literówkę w rozdziale 5. i zasugerował wyjaśnienie w rozdziale 3.
• Curtis Yanko zasugerował wyjaśnienie w rozdziale 2.
• Ben Logan zgłosił kilka literówek i problemów z przekształceniem treści książki do formatu HTML.
• Jason Armstrong stwierdził brak słowa w rozdziale 2.
• Louis Cordier wykrył miejsce w rozdziale 16., w którym kod nie był dopasowany do tekstu.
• Brian Cain zasugerował kilka wyjaśnień w rozdziałach 2. i 3.
• Rob Black przesłał zestaw poprawek, w tym kilka zmian dotyczących języka Python 2.2.
• Jean-Philippe Rey z politechniki Ecole Centrale Paris przesłał kilka poprawek, w tym aktuali-
zacje dotyczące języka Python 2.2, oraz inne przemyślane ulepszenia.
• Jason Mader z uczelni George Washington University zgłosił kilka przydatnych sugestii i po-
prawek.
• Jan Gundtofte-Bruun uświadomił nam, że zamiast a error ma być an error.
• Abel David i Alexis Dinno wskazali nam, że liczba mnoga słowa matrix to matrices, a nie matrixes.
Błąd ten tkwił w książce przez lata, a w ten sam dzień zgłosiło go dwóch czytelników o iden-
tycznych inicjałach. Dziwne.
• Charles Thayer zachęcił nas do usunięcia średników umieszczonych na końcu niektórych in-
strukcji, a także do wyjaśnienia zasadności używania terminów argument i parametr.
• Roger Sperberg wskazał mętny fragment logiki w rozdziale 3.
• Sam Bull wskazał niejasny akapit w rozdziale 2.
• Andrew Cheung wskazał dwa przypadki użycia przed utworzeniem definicji.

16 Pnedmowa
• C. Corey Capel wychwycił brak słowa i literówkę w rozdziale 4.
• Alessandra pomogła wyeliminować niejasności związane z obiektem żółwia.
• Wim Champagne znalazł błąd myślowy w przykładzie słownika.
• Douglas Wright wskazał problem z dzieleniem bez reszty w funkcji arc.
• Jared Spindor znalazł niepotrzebne pozostałości na końcu jednego ze zdań.
• Lin Peiheng przesłał kilka bardzo pomocnych sugestii.
• Ray H agtvedt przesłał informację o dwóch błędach i czymś, co nie do końca jest błędem.
• Torsten Hubsch wskazał niespójność w pakiecie Swampy.
• Inga Petuhhov poprawi ła przykład w rozdziale 14.
• Arne Babenhauserheide przesłał kilka pomocnych poprawek.
• Mark E. Casida dobrze sobie radzi z wyłapywaniem powtórzonych słów.

• Scott Tyler uzupełnił to, czego brakowało, a następnie przesłał pakiet poprawek.
• Gordon Shephard przesłał kilka poprawek (wszystkie w osobnych wiadomościach e-mail).
• Andrew Tu rner zauważył błąd w rozdziale 8.
• Adam H obart usunął problem z dzieleniem bez reszty w fu nkcji arc.
• Daryl Hammond i Sarah Zimmerman wskazali, że zbyt wcześnie podałem funkcję math .pi .
Sarah wychwyciła literówkę.
• Geo rge Sass znalazł błąd w podrozdziale dotyczącym debugowania.
• Brian Bingham zaproponował ćwiczenie 11.5.
• Leah Engelbert-Fenton wskazał, że wbrew własnej radzie użyłe m tu ple jako nazwy zmiennej,
a także znalazł mnóstwo literówek i przypadek użycia przed utworzeniem definicji.
• Joe Funke wychwycił literówkę.
• Chao-chao Chen znalazł niespójność w przykładzie z ciągiem Fibonacciego.
• Jeff Paine zna różnicę między terminami spacja i spam.
• Lubos Pintes przesłał i nformację o literówce.
• Gregg Lind i Abigail Heithoff zaproponowali ćwiczenie 14.3.
• Max H ailperin przesłał kilka poprawek i sugestii. Max to jeden z auto rów wyjątkowej ksi ążki
Concrete Abstractions (Course Technology, 1998), którą możesz przeczytać po zakończeniu
lektury tej książki.
• Chotipat Pornavalai znalazł błąd w komunikacie o błędzie.
• Stanisław Anto! przesłał listę bardzo pomocnych sugestii.
• Eric Pashman przesłał kilka poprawek dotyczących rozdziałów od 4. do 11.
• Miguel Azevedo znalazł kilka literówek.

• Jianhua Liu przesłał długą listę poprawek.


• Nick King wskazał na brak słowa.

Lista współpracowników 17
• Martin Zuther przesłał długą listę sugestii.
• Adam Zimmerman znalazł niespójność w stosowaniu przeze mnie słowa instancja oraz kilka
innych błędów.
• Ratnakar Tiwari zaproponował przypis objaśniający trójkąty „zdegenerowane".
• Anu rag Go el zaproponował inne rozwiązanie dotyczące funkcji is_abecedari an przesłał
kilka dodatkowych poprawek. W ie również, jak zapisać nazwisko Jane Austen.
• Kelli Kratzer wychwycił jedną z literówek.
• Mark Griffiths wskazał niejasny przykład w rozdziale 3.
• Roydan Ongie znalazł błąd w mojej metodzie Newtona.
• Patryk Wołowiec pomógł mi przy problemie obecnym w wersji HTML.
• Mark Chonofsky poinfo rmował mnie o nowym słowie kluczowym w języku Python 3.
• Russell Coleman pomógł mi przy geometrii.
• W ei Huang wychwycił kilka błędów typograficznych.
• Karen Barber wyłapała najstarszą literówkę w ksi ążce.

• Nam Nguyen znalazł literówkę i wskazał, że użyłem wzorca Dekorator, lecz nie podałem jego
nazwy.
• Stephane Morin przesłał kilka poprawek i sugestii.
• Paul Stoop usunął literówkę w funkcji uses_only.
• Eric Bronner wskazał niejasność w omówieniu kolejności operacji.
• Alexandros Gezerlis zdefiniował nowy standard odnoszący się do liczby i jakości przesłanych
sugestii. Jesteśmy wielce wdzięczni!
• Gray Thomas wie, że jego prawa zaczyna się od jego lewej.
• Giovanni Escobar Sosa przesłał długą listę poprawek i sugestii.
• Alix Etienne poprawił jeden z adresów URL.
• Kuang He znalazł literówkę.
• Daniel Neilson usunął błąd związany z kolejnością operacji.
• Will McGinnis wskazał, że funkcja po l y l i ne została zdefiniowana w różny sposób w dwóch
m iejscach.
• Swarup Sahoo wychwycił brak średnika.
• Frank Hecker wskazał ćwiczenie, które nie było zbyt precyzyjne, a ponadto zawierało nie-
działające odnośniki.

• Animesh B pomogła mi poprawić niejasny przykład.


• Martin Caspersen znalazł dwa błędy zaokrąglania.
• Gregor Ulm przesłał kilka poprawek i sugestii.
• Dimitrios Tsirigkas zasugerował lepsze wyjaśni eni e ćwiczenia.

18 Pnedmowa
• Carlos Tafur przesłał stronę poprawek i sugestii.
• Martin Nordsletten znalazł błąd w rozwiązaniu ćwiczenia.

• Lars O.D. Christensen znalazł niedziałające odwołanie.


• Victor Simeone znalazł literówkę.
• Sven Hoexter wskazał, że zmienna o nazwie i nput „zasłania" funkcję wbudowaną.
• Viet Le znalazł literówkę.
• Stephen Gregory wskazał problem z funkcją cmp w języku Python 3.
• Matthew Shultz poinformował mnie o niedziałającym odnośniku.

• Lokesh Kumar Makani przesłał informację o kilku niedziałających odnośnikach oraz o pew-
nych zmianach w komunikatach o błędzie.
• Ishwar Bhat poprawił podane przez mnie ostat nie twierdzenie Fermata.
• Brian McGhie zasugerował wyjaśnienie.
• Andrea Zanella przetłumaczyła książkę na język włoski, a także przy okazji przesłała kilka
poprawek.
• Gorące wyrazy wdzięczności dla Melissy Lewis i Luciana Ramalha za znakomite komentarze
i sugestie dotyczące drugiego wydania.
• Podziękowania dla Harry'ego Percivala z firmy PythonAnywhere za jego pomoc, która po-
zwoliła użytkownikom na rozpoczęcie pracy z kodem Python w przeglądarce.

• Xavier Van Au bel wprowadził w drugim wydaniu kilka wartościowych poprawek.

Lista współpracowników 19
20 Pnedmowa
ROZDZIAŁ 1.

Jak w programie

Celem tej książki jest nauczenie Cię myślenia jak informatyk. Ten sposób rozumowania łączy w sobie
niektóre najlepsze elementy matematyki, inżynierii i nauk przyrodniczych. Tak jak matematycy,
informatycy używają języków formalnych do opisu idei (dokładniej rzecz biorąc, obliczeń). Tak
jak inżynierowie, informatycy projektują różne rzeczy, łącząc komponenty w systemy i oceniaj ąc
alternatywne warianty w celu znalezienia kompromisu. Podobnie do naukowców informatycy
obserwują zachowanie złożonych systemów, stawiają hipotezy i sprawdzają przewidywania.

W przypadku informatyka najważniejszą pojedynczą umiejętnością jest rozwiązywanie problemów.


Oznacza to zdolność formułowania problemów, kreatywnego myślenia o problemach i przedsta-
wiania ich w dokładny i przejrzysty sposób. Jak się okazuje, proces uczenia programowania to znako-
mita sposobność do sprawdzenia umiej ętności rozwiązywania problemów. Z tego właśnie powo-
du ten rozdział nosi tytuł „Jak w programie".

Na jednym poziomie będziesz uczyć się programowania, które samo w sobie jest przydatną
umiejętnością. Na innym wykorzystasz programowanie jako środek do osiągnięcia celu. W trak-
cie lektury kolejnych rozdziałów cel ten stanie się bardziej wyraźny.

Czym jest program?


Program to sekwencja instrukcji określających, w jaki sposób ma zostać przeprowadzone obliczenie.
Obliczenie może mieć postać jakiegoś działania matematycznego, tak jak w przypadku rozwiązy­
wania układu równań lub znajdowania pierwiastków wielomianu, ale może też być obliczeniem sym-
bolicznym (przykładem jest wyszukiwanie i zastępowanie tekstu w dokumencie) lub czymś w po-
staci operacji graficznej (jak przetwarzanie obrazu lub odtwarzanie wideo).

Szczegóły prezentują się inaczej w różnych językach, ale kilka podstawowych elementów pojawia
się w niemal każdym języku. Oto one:
dane wejściowe
Dane wprowadzone za pomocą klawiatury albo pochodzące z pliku, sieci lub jakiegoś urzą­
dzenia.
dan e wyjściowe
Dane wyświetlane na ekranie, zapisywane w pliku, wysyłane za pośrednictwem sieci itp.

21
działania matematycz ne
Podstawowe operacje matematyczne, takie jak dodawanie i mnożenie.

wykonywanie warunkowe
Sprawdzanie określonych warunków i uruchamianie odpowiedniego kodu.
powtarz anie
Wielokrotne wykonywanie pewnego działania (zwykle zmieniającego się w pewien sposób).
Czy temu wierzyć, czy nie, to naprawdę wszystko, co jest związane z programem. Każdy program,
jakiego dotąd używałeś, nieważne jak bardzo skomplikowany, tak naprawdę jest złożony z ele-
mentów podobnych do wyżej wymienionych. Oznacza to, że programowanie możesz postrzegać
jako proces dzielenia dużego i złożonego zadania na coraz mniejsze podzadania do momentu, aż
są one na tyle proste, że sprowadzają się do jednego z powyższych podstawowych elementów.

Uruchamianie interpretera języka Python


Jednym z wyzwań przy rozpoczynaniu przygody z językiem Python jest ewentualna konieczność
instalacji na komputerze tego języka wraz z powiązanym oprogramowaniem. Jeśli jesteś zaznajomiony
ze swoim systemem operacyjnym, a zwłaszcza z interfejsem wiersza poleceń, nie będziesz mieć
problemu z instalacją języka Python. Dla początkujących utrudnieniem może być jednak konieczność
równoczesnego przyswajania wiedzy z zakresu administrowania systemem i programowania.

Aby uniknąć tego problemu, zalecam na początek uruchomienie interpretera języka Python w prze-
glądarce.
Gdy będziesz zaznajomiony z tym językiem, zaprezentuję sugestie dotyczące instalowania go
na komputerze.

Dostępnych jest kilka stron internetowych umożliwiających uruchomienie interpretera języka Python.
Jeśli masz już swojego faworyta, po prostu z niego skorzystaj. W przeciwnym razie polecam witrynę
PythonAnywhere. Pod adresem http://tinyurl.com/ thinkpython2e zamieszczono szczegółowe instruk-
cje pozwalające na rozpoczęcie pracy.
Istniejądwie wersje języka Python, czyli Python 2 i Python 3. Po nieważ są one bardzo podobne, po
poznaniu jednej z nich z łatwością można zacząć korzystać z drugiej. Okazuje się, że występuje tylko
kilka różnic, z jakimi będziesz mieć do czynienia jako początkujący. Tę książkę napisano z myślą
o języku Python 3, ale uwzględniono kilka uwag dotyczących j ęzyka Python 2.

Interpreter języka Python to program odczytujący i wykonujący kod Python. Zależnie od używanego
środowiska w celu uruchomienia interpretera może być wymagane kliknięcie ikony lub wpisanie
polecenia python w wierszu poleceń. Po uruchomieniu interpretera powinny być widoczne dane
wyjściowe podobne do następujących:

Python 3. 4. 0 (defau lt, Jun 19 2015 , 14:20:21 )


[GCC 4.8 . 2] on 1 inux
Ty pe "help", "copyrig ht" , "credits" or "license" f or mare information .
>>>

Pietwsze trzy wiersze zawierają informacje dotyczące interpretera i systemu operacyjnego, w którym
go uruchomiono, dlatego możesz ujrzeć coś innego. Należy jednak sprawdzić, czy numer wersji,

22 Rozdział 1. Jak w programie


który w przykładzie ma postać 3 .4 .o, rozpoczyna się od cyfry 3 wskazującej, że uruchomiono inter-
preter języka Python 3. Jeśli numer wersji zaczyna się cyfrą 2, załadowano interpreter (pewnie się
domyśliłeś) języka Python 2.

Ostatni wiersz to wiersz zachęty wskazujący, że interpreter jest gotowy do przyjęcia kodu wpro-
wadzonego przez użytkownika. Jeśli wpiszesz wiersz kodu i naciśniesz klawisz Enter, interpreter
wyświetli następujący wynik:
>>> 1 + 1
2

Możesz teraz przejść do dzieła. Od tego momentu zakładam, że wiesz, jak załadować interpreter
języka Python i uruchomić kod.

Pierwszy program
Tradycyjnie pierwszy program, jaki piszesz w nowym języku, nosi nazwę Witaj, świecie!, ponieważ wy-
świetla on właśnie te słowa: Wi t aj , świec i e!. W języku Python wygląda to następująco:

»> pri nt{'Witaj, świecie!' )

Jest to przykład instrukcji print, choć w rzeczywistości nie powoduje ona drukowania niczego na
papierze. Instrukcja wyświetla wynik na ekranie. W tym przypadku wynikiem są następujące słowa:
Witaj, świecie!

Znaki pojedynczego cudzysłowu w kodzie programu oznaczają początek i koniec tekstu do wy-
świetlenia. Te znaki nie pojawiają się w wyniku.

Nawiasy okrągłe wskazują, że instrukcja pri nt to funkcja. Funkcjami zajmiemy się w rozdziale 3.
W języku Python 2 instrukcja pri nt jest trochę inna. Ponieważ nie jest funkcją, nie korzysta z na-
wiasów okrągłych.
>>> pri nt 'Witaj, świecie!'

To rozróżnienie nabierze wkrótce większego sensu, ale na początek tyle wystarczy.

Operatory arytmetyczne
Po programie Witaj, świecie! następny krok to arytmetyka. Język Python zapewnia operatory,
które są specjalnymi symbolami reprezentującymi takie obliczenia jak dodawanie i mnożenie.
Operatory +, - i * służą do wykonywania dodawania, odejmowania i mnożenia, tak jak w nastę­
pujących przykładach:

>» 40 + 2
42
»> 43 - 1
42
>>> 6 *
42

Operatory arytmetyane 23
Operator/ wykonuje operację dzielenia:
>» 84 / 2
42.0

Możesz się zastanawiać, dlaczego wynik to 42. O, a nie 42. Zostanie to wyjaśnione w następnym
podrozdziale.
I wreszcie, operator** służy do potęgowania, czyli podniesienia liczby do potęgi:
>>> 6**2 + 6
42

W niektórych innych językach na potrzeby potęgowania używany jest operator ", ale w języku
Python jest to operator bitowy o nazwie XOR. Jeśli nie jesteś zaznajomiony z operatorami bito-
wymi, następujący wynik zaskoczy Cię:
>>> 6"2
4

W tej książce nie są omawiane operatory bitowe, ale możesz o nich poczytać na stronie dostępnej
pod adresem http://wiki.python.org/moin/BitwiseOperators.

Wartości i typy
Wartość to jeden z podstawowych elementów używanych przez program, jak litera lub liczba.
Niektóre dotychczas zaprezentowane wartości to 2, 42. O oraz Witaj, św i ee i e !.
Wartości te należą do różnych typów: liczba 2 to liczba całkowita, 42 . O to liczba zmiennoprze-
cinkowa, a Witaj , świ ee i e ! to łańcuch (taka nazwa wynika z tego, że litery tworzą jedną całość).

Jeśli nie masz pewności, jakiego typu jest wartość, interpreter może zapewnić taką informację:
»> type (2)
<class ' int'>
»> type(42 . 0)
<class ' fl oat'>
»> type('Witaj , ś wi ecie !' )
<class 'str'>

W powyższych wynikach słowo c l ass odgrywa rolę kategorii. Typ to kategoria wartości.
Nie jest zaskoczeniem to, że liczby całkowite należą do typu i nt, łańcuchy do typu s t r , a liczby
zmiennoprzecinkowe do typu f l oat.

A co z wartościami takimi jak ' 2 ' i ' 42 . O'? Wyglądają one jak liczby, ale ujęto je w znaki cudzy-
słowu, tak jak łańcuchy:

»>type('2')
<class 'str'>
»> type( '42. 0 ' )
<class 'str'>

Są to łańcuchy.

24 Rozdział 1. Jak w programie


Gdy w krajach anglojęzycznych używana jest duża liczba całkowita, jej grupy cyfr są oddzielane
przecinkiem (np. 1,000,000). Choć tak zapisana liczba jest poprawna, w języku Python jest niedo-
zwoloną liczbq całkowitq:

»> 1,000 , 000


(1 , O, O)

Czegoś takiego zupełnie nie oczekujemy! W języku Python liczba 1,000, 000 interpretowana jest
jako sekwencja liczb całkowitych oddzielonych przecinkiem. W dalszej części rozdziału dowiesz
się więcej o tego rodzaju sekwencji.

Języki formalne i naturalne


Języki naturalne to języki, jakimi posługują się ludzie, takie jak angielski, hiszpański i francuski.
Nie zostały stworzone przez ludzi (choć ludzie próbują narzucać w nich jakiś porządek), lecz
rozwijały się w sposób naturalny.

Języki formalne to języki stworzone przez ludzi do konkretnych zastosowań. Na przykład nota-
cja, jaką posługują się matematycy, jest językiem formalnym, który sprawdza się szczególnie do-
brze przy opisywaniu relacji między liczbami i symbolami. Chemicy używają języka formalnego
do reprezentowania struktury chemicznej molekuł. I co najważniejsze:
Języki programowania to języki formalne, które zostały zaprojektowane do definiowania
obliczeń.

Języki formalne mają zwykle ścisłe reguły dotyczące składni, które decydują o strukturze instruk-
cji. Na przykład w matematyce równanie 3+3 = 6 ma poprawną składnię, ale wyrażenie 3 + = 3$6
już nie. W chemii H 2 0 to poprawny składniowo wzór, ale w przypadku 2Zz tak nie jest.

Występują dwie odmiany reguł dotyczących składni. Pierwsza odmiana związana jest z tokenami,
a druga ze strukturą. Tokeny to podstawowe elementy języka, takie jak słowa, liczby i symbole
chemiczne. Jednym z problemów w przypadku wyrażenia 3+ = 3$6 jest to, że znak$ nie jest w mate-
matyce uznawany za poprawny token (tak przynajmniej mi wiadomo). Podobnie wzór 2Zz nie jest
dozwolony, ponieważ nie istnieje element ze skrótem Z z.

Drugi typ reguły dotyczącej składni powiązany jest ze sposobem łączenia tokenów. Równanie 3+ = 3
jest niepoprawne, ponieważ nawet pomimo tego, że +i = to poprawne tokeny, niedozwolona jest
sytuacja, gdy jeden następuje bezpośrednio po drugim. Podobnie we wzorze chemicznym indeks
dolny musi znajdować się po nazwie elementu, a nie przed nią.

To jest zdanie w języku pol$kim @ poprawnej strukturze, które zawiera niewłaściwe t*keny.
Z kolei zdanie to wszystkie tokeny poprawne ma, nieprawidłową ale strukturę.
Gdy czytasz zdanie w języku polskim lub instrukcję w języku formalnym, musisz określić struktu-
rę (w języku naturalnym robisz to podświadomie). Proces ten nazywany jest analizą składni.

Chociaż języki formalne i naturalne mają wiele wspólnych elementów, takich jak tokeny, struktu-
ra i składnia, występują pomiędzy nimi także następujące różnice:

Języki formalne I naturalne 25


wieloznaczność

Języki naturalne są pełne wieloznaczności, z którą ludzie radzą sobie, posługując się wska-
zówkami kontekstowymi oraz innym i informacjami. Języki formalne są tak projektowane,
aby były prawie lub całkowi cie jednoznaczne. Oznacza to, że każda instrukcja, niezależnie od
kontekstu, ma dokładnie jedno znaczenie.
nadmiarowość

Aby zreko mpensować wieloznaczność i zmniejszyć liczbę nieporozumień, w językach natu-


ralnych występuj e m nóstwo nadm iarowości . W rezultacie języki te często cechują si ę rozwle-
kłością. Języki fo rmalne są mniej nadmiarowe i bardziej zwięzłe.

dosłowność

Języki naturalne są pełne idiomów i metafor. Jeśli ktoś powie „Mleko si ę rozlało", nie oznacza
to raczej, że gdzieś naprawdę rozlało się mleko (idiom ten znaczy, że wydarzyło się coś, czego
nie można j uż cofnąć). W językach formalnych znaczenie instrukcji jest w pełni zgodne z jej
treści ą.

Ponieważ wszyscy dorastamy, posługując się językam i naturalnymi, czasami trudno nam przy-
zwyczaić się do języków formalnych. Różnica między j ęzykiem fo rmalnym i naturalnym jest taka
jak m iędzy poezją i prozą, tym bardziej że:

Poezja
W przypadku słów istotne jest zarówno ich brzmienie, jak i znaczenie. Cały wiersz tworzy
efekt lub reakcję emocjo nalną. Wieloznaczność jest nie tylko typowa, ale często zamierzona.

Proz a
Ważniejsze jest dosłowne znaczenie słów, a struktura zawiera w sobie wi ęcej znaczenia. Proza
jest łatwiejsza do analizy niż poezja, ale i ona często cechuje się wieloznacznością.

Programy
Znaczenie programu komputerowego jest jed noznaczne i dosłowne. Program m oże zostać
całkowicie zrozum iany w wyniku analizy tokenów i struktury.

Języki fo rmalne są bardziej treściwe niż języki naturalne, dlatego w przypadku pierwszych z wymie-
nionych czytanie zajmuje wi ęcej czasu. Ponadto istotna jest struktura. Z tego powodu nie zawsze
najlepszym wariantem jest czytanie od góry do dołu oraz od lewej do prawej strony. Zamiast tego
naucz się analizować program w głowi e, identyfikując tokeny i interpretując st rukturę. I wreszcie,
istotne są szczegóły. Niewielkie błędy pisowni i interpunkcji, z którym i można sobie poradzić
w przypadku języków naturalnych, w języku formalnym mogą mieć decydujące znaczenie.

Debugowanie
Programiści popełniają błędy. Z dziwnych powodów błędy pojawiające się w czasie program owania są
potocznie nazywane pluskwami (ang. bugs), a proces ich wychwytywania to debugowanie.

26 Rozdział 1. Jak w programie


Programowanie, a zwłaszcza debugowanie, wywołuje czasami silne emocje. Jeśli borykasz się z trud-
nym do usunięcia błędem, możesz czuć wściekłość, zniechęcenie lub zakłopotanie.
Świadczy to o tym, że ludzie w naturalny sposób odpowiadają komputerom tak, jakby były ludźmi.
Gdy działają dobrze, traktujemy je jak kolegów z zespołu, a gdy okazują się „zawzięte" lub „nie-
miłe", reagujemy tak samo jak w przypadku upartych i niemiłych osób (Reeves i Nass, The Media
Equation: How People Treat Computers, Television, and New Media Like Real People and Places}.

Przygotowanie się na takie reakcje może ułatwić poradzenie sobie z nimi. Jednym ze sposobów
jest potraktowanie komputera jak pracownika z określonymi mocnymi stronami, takimi jak szybkość
i dokładność, a także z konkretnymi słabymi stronami, takimi jak brak empatii i niezdolność myślenia
całościowego.

Twoim zadaniem jest zostać dobrym menedżerem: znajdź sposoby na wykorzystanie mocnych
stron i zminimalizowanie tych słabych. Określ również sposoby użycia własnych emocji do zaan-
gażowania się w problem bez ryzyka, że Twoje reakcje będą uniemożliwiać efektywną pracę.

Uczenie się debugowania może być frustrujące, lecz debugowanie jest wartościową umiejętnością,
która okazuje się przydatna w przypadku wielu działań wykraczających poza programowanie. Na
końcu każdego rozdziału znajduje się podrozdział taki jak ten, w którym znajdziesz moje sugestie
dotyczące debugowania. Mam nadzieję, że będą pomocne!

Słownik
rozwiqzywanie problemu
Proces formułowania problemu, znajdowania rozwiązania i wyrażania go.

język wysokiego poziomu


Język programowania, taki jak Python, zaprojektowany tak, aby pozwalał ludziom w prosty
sposób pisać i czytać kod.
język niskiego poz iomu
Język programowania zaprojektowany tak, aby jego kod z łatwością mógł zostać uruchomio-
ny przez komputer. Język ten nazywany jest również językiem maszynowym lub językiem
asemblera.
przenośność

Właściwość programu umożliwiająca uruchomienie go na więcej niż jednym typie komputera.

interpreter
Program wczytujący inny program i wykonujący go.

zachęta

Znaki wyświetlane przez interpreter w celu wskazania, że jest on gotowy do pobrania danych
wejściowych od użytkownika.

Słownik 27
program
Zestaw instrukcji określających obliczenie.

instrukcja wyświetlająca

Instrukcja powodująca wyświetlenie przez interpreter języka Python wartości na ekranie.


operator
Specjalny symbol reprezentujący prostą operację, taką jak dodawanie, mnożenie lub łączenie
łańcuchów.

wartość

Jedna z podstawowych jednostek danych, takich jak liczba lub łańcuch, która jest przetwa-
rzana p rzez program.
typ
Kategoria wartości. Dotychczas zaprezentowane typy to liczby całkowite (typ int), liczby zmien-
noprzecinkowe (typ floa t) i łańcuchy (typ st r).

licz ba całkowita
Typ reprezentujący liczby całkowite.

licz ba z miennoprz ecinkowa


Typ reprezentujący liczby z częściami ułamkowymi.

łańcuch

Typ reprezentujący sekwencje znaków.

język naturalny
Dowolny naturalnie rozwinięty język, jakim mówią ludzie.
język formalny

Dowolny język stworzony przez ludzi do konkretnych celów, takich jak przedstawianie kon-
cepcji matematycznych lub reprezentowanie programów komputerowych. Wszystkie języki
programowania to języki fo rmalne.
token
Jeden z podstawowych elementów struktury składniowej programu analogiczny do słowa w języku
naturalnym.
składnia

Reguły zarządzające strukturą programu.

analiza składni
Ma na celu sprawdzenie programu i dokonanie analizy struktury składniowej.
pluskwa
Błąd w programie.

28 Rozdział 1. Jak w programie


debugowanie
Proces znajdowania i usuwania błędów.

Ćwiczenia
Ćwiczenie 1.1.

Dobrym pomysłem będzie czytanie tej książki przed komputerem, aby mieć możliwość spraw-
dzania przykładów na bieżąco.

Każdorazowo, gdy eksperymentujesz z nowym elementem, spróbuj popełnić błędy. Co będzie,


gdy na przykład w programie Witaj, świecie! zostanie pominięty jeden ze znaków cudzysłowu? Co
się stanie, jeśli pominiesz oba te znaki? Co będzie, gdy niepoprawnie wprowadzisz nazwę instrukcji
pr i nt ?

Tego rodzaju eksperyment ułatwia zapamiętanie tego, co czytasz. Pomocny jest również podczas
programowania, ponieważ postępując w ten sposób, poznajesz znaczenie komunikatów o błędzie.
Lepiej popełnić błędy teraz i świadomie niż później i przypadkowo.
1. Co się stanie, gdy w instrukcji pri nt zostanie pominięty jeden z nawiasów okrągłych lub oba?
2. Jeślipróbujesz wyświetlić łańcuch, co się stanie, gdy pominiesz jeden ze znaków cudzysłowu
lub oba?
3. Znaku minus możesz użyć do określenia liczby ujemnej (np. - 2). Co będzie, gdy przed liczbą
wstawisz znak plus? A co będzie w przypadku 2++2?
4. W zapisie matematycznym zera umieszczone na początku są poprawne (np. 02). Co się stanie,
jeśli czegoś takiego spróbujesz w przypadku języka Python?

5. Co się dzieje, gdy występują dwie wartości bez żadnego operatora między nimi?

Ćwiczenie 1.2.
1. Ile łącznie sekund jest w 42 minutach i 42 sekundach?
2. Ile mil mieści się w 10 kilometrach? Wskazówka: jednej mili odpowiada 1,61 kilometra.
3. Jeśli IO-kilometrowy dystans wyścigu pokonasz w czasie 42 minut i 42 sekund, jakie będzie
Twoje średnie tempo (czas przypadający na milę wyrażony w minutach i sekundach)? Jaka jest
Twoja średnia prędkość w milach na godzinę?

Ćwlaenla 29
30 Rozdział 1. Jak w programie
ROZDZIAŁ 2.

Zmienne, wyrażenia i instrukcje

Jedną z najbardziej przydatnych możliwości języka programowania jest modyfikowanie zmiennych.


Zmienna to nazwa odwołująca si ę do wartości.

Instrukcje przypisania
Instrukcja przypisania tworzy nową zmienną i nadaje jej wartość:

>>> message = 'A teraz odnośn ie do c z egoś z upełnie innego '


>>> n = 17
>>>pi = 3.141592653589793

W tym przykładzi e utworzono trzy przypisania. Pierwsze przypisuje łańcuch nowej zmiennej o na-
zwie message. Drugie przypisanie nadaje zmiennej n wartość w postaci liczby całkowitej 17, a trzecie
przypisuje wartość 7t (w przybliżeniu) zmiennej pi .

Typowym sposobem reprezentowania zmiennych na papierze jest zapisywanie nazwy zmiennej


ze strzałką wskazującą jej wartość. Tego rodzaju rysunek nazywany jest diagramem stanu, ponieważ
pokazuje, jaki stan m a każda zmienna (pot raktuj to jak „stan umysłu" zmiennej). Na rysunku 2.1
zap rezentowałem powyższy przykład.

message ----- 'A teraz odnośnie do czegoś zupe łnie innego'


n-17
pi - 3.1 415926535897932

Rysunek 2.1 . Diagram stanu

Nazwy zmiennych
Programiści wybierają zwykle dla swoich zmiennych sensowne nazwy, które dokumentują prze-
znaczenie zmiennej.

31
Nazwy zmiennych mogą być dowolnie długie. Mogą zawierać zarówno litery, jak i liczby, ale nie
mogą rozpoczynać się od liczby. Choć dozwolone jest użyci e dużych liter, w p rzypadku nazw
zmiennych wygodne jest stosowanie wyłącznie małych liter.

Znak podkreślenia(_} może się pojawić w nazwie. Znak ten jest często stosowany w nazwach zło­
żonych z wielu słów, takich jak twoje_i mi e lub szybkosc _w_powiet r zu_jaskol ki_ bez_ l adunku.

Jeśli zmiennej nadasz niepoprawną nazwę, zostanie wyświ etlony błąd składni:
>>> 76trombones = 'wielka parada'
SyntaxError: i nval id syntax
>>> more@ = 1000000
SyntaxError: i nval id syntax
>>>class = ' Zaawansowana zymologia teoretyczna'
SyntaxError: i nval id syntax
Nazwa 76trombones jest niepoprawna, gdyż rozpoczyna się od liczby. Nazwa more@ jest niewłaści ­
wa, po nieważ zawiera niedozwolony znak @. Co jednak jest nie tak w przypadku nazwy class?

O kazuje się, że nazwa class to jedno z słów kluczowych języka Python. I nterpreter używa słów
kluczowych d o rozpoznawania struktury programu. Słowa te nie mogą być stosowane jako nazwy
zmiennych.

W języku Python 3 występuj ą następujące słowa kluczowe:


Fal se class finally is return
None cont i nue for lambda try
True def fr om nonlocal whil e
and del gl obal not with
as el i f if or yield
assert el se import pass
break except in rai se
Nie ma potrzeby zapamiętywania tej listy. W większości śro dowi sk projektowania słowa kluczo-
we są wyświetlane przy użyciu innego koloru. Jeśli spróbujesz u żyć jedn ego z nich jako nazwy
zmiennej, dowiesz się o tym.

Wyrażenia i instrukcje
Wyrażenie to kombinacja wartości, zmiennych i operatorów. Sam a wartość jest uważa na za wy-
rażenie, jak również zmienna, dlatego poprawne są wszystkie następujące wyrażenia:
>>> 42
42
>>> n
17
>>> n + 25
42

Gdy wpiszesz wyrażenie w wierszu zachęty, interpreter wyznacza jego wa rtość. Oznacza to, że
znajduje wartość wyrażenia. W powyższym przykładzie wyrażenie m a wartość 17, a wyrażenie n + 25
zapewnia wartość 42.

Instrukcja to jednostka kodu powo dująca taki efekt jak utworzenie zmiennej lub wyświetleni e
wartości.

32 Rozdział 2. Zmienne, wyrażenia I Instrukcje


>>> n = 17
» > print(n)

Pierwszy wiersz to instrukcja przypisania zapewniająca wartość zmiennej n. W drugim wierszu


znajduje się instrukcja pr i nt wyświetlająca wartość zmiennej n.

Po wpisaniu instrukcji interpreter wykonuje j ą. Oznacza to, że realizuje działania określone w in-
strukcji. Instrukcje przeważnie nie zawierają wartości.

Tryb skryptowy
Do tej pory kod Python był uruchamiany w trybie interaktywnym, co oznacza, że prowadzona
była bezpośrednia interakcja z interpreterem. Wykorzystanie trybu interaktywnego to dobry spo-
sób na rozpoczęcie działań. Jeśli jednak masz do czynienia z więcej niż kilkoma wierszami kodu,
może on okazać się niewygodny.

Alternatywą jest zapisanie kodu w pliku nazywanym skryptem, a następnie uruchomienie interpretera
w trybie skryptowym w celu wykonania skryptu. Zgodnie z konwencją skrypty języka Python
mają nazwy zakończone rozszerzeniem .py.

Jeśliwiesz, jak tworzyć i uruchamiać skrypt na komputerze, możesz przejść do dzieła. W prze-
ciwnym razie polecam ponowne skorzystanie z witryny PythonAnywhere. Zamieściłem w niej in-
strukcje pozwalające uruchomić kod w trybie skryptowym (http://tinyurl.com/ thinkpython2e).

Ponieważ język Python zapewnia oba t ryby, przed umieszczeniem porcji kodu w skrypcie możesz
sprawdzić je w trybie interaktywnym. Między trybami interaktywnym i skryptowym istnieją jednak
różnice, które mogą powodować niejasności.

Jeśli na przykład używasz programu Python jako kalkulatora, możesz wpisać następujące wiersze
kodu:
>>> mi le5 = 26.2
>» mi le5 * 1. 6 1
42 . 182

W pierwszym wierszu zmiennej mil es przypisywana jest wartość. Nie powoduje to jednak żadne­
go widocznego efektu. W drugim wierszu znajduje się wyrażenie, dlatego interpreter wyznacza
jego wartość i wyświetla wynik. Okazuje się, że maraton to około 42 kilometrów.

Jeślijednak ten sam kod umieścisz w skrypcie i uruchomisz go, nie otrzymasz żadnych danych
wyjściowych. W trybie skryptowym samo wyrażenie nie zapewnia żadnego efektu wizualnego. Inter-
preter języka Python właściwie wyznacza wartość wyrażenia, ale nie wyświetla jej, chyba że zostanie
odpowiednio poinstruowany:
mi le5 = 26 . 2
print(mile5 * 1. 61)

Początkowo takie działanie może nie być zrozumiałe.


Skrypt zawiera zwykle sekwencję instrukcji. Jeśli istnieje więcej niż jedna instrukcja, wyniki są
prezentowane po jednym naraz w trakcie wykonywania instrukcji.

Tryb skryptowy 33
Na przykład skrypt
print(l)
X = 2
print(x)

zwraca wynik

Instrukcja przypisania nie generuje żadnych danych wyjściowych.

Aby sprawdzić słuszność rozumowania, wpisz następujące instrukcje w oknie interpretera języka
Python i przekonaj się, co uzyskasz:

X =
X +

Te same instrukcje umieść następnie


w skrypcie i uruchom go. Jaki jest wynik? Zmodyfikuj skrypt,
przekształcając każde wyrażenie w instrukcję wyświetlającą, po czym uruchom go ponownie.

Kolejność operacji
Gdy wyrażenie
zawiera więcej niż jeden operator, kolejność wyznaczania wartości zależy od ko-
lejnościoperacji. W przypadku operatorów matematycznych w języku Python stosowana jest kon-
wencja obowi ązująca w matematyce. Skrót NP MD DO ułatwi a zapamiętanie następujących reguł:
• Nawiasy okrągłe maj ą najwyższy priorytet i mogą posłużyć do wymuszenia wyznaczania wartości
wyrażenia w żądanej kolejności . Ponieważ dla wyrażeń w nawiasach okrągłych wartość jest
wyznaczana w pierwszej kolejności, w przypadku wyrażenia 2 * ( 3 - 1) wartość to 4, a dla
wyrażenia (1 + 1) ** ( 5 - 2) wartość wynosi 8. Możliwe jest też zastosowanie nawiasów okrągłych
do zwiększenia czytelności wyrażenia, tak jak w przypadku wyrażenia (mi nute * 100) / 60,
nawet wtedy, gdy nie powoduje to zmiany wyniku.
• Potęgowanie ma następny w kolejności priorytet, dlatego wartością wyrażenia 1 + 2**3 jest
liczba 9, a nie 27, w przypadku wyrażenia 2 * 3**2 wartość wynosi natomiast 18, a nie 36.
• Mnożenie i dzielenie mają wyższy
priorytet niż dodawanie i odejmowanie. Oznacza to, że
wartość wyrażenia 2 * 3 - 1 to 5, a nie 4, z kolei wartością wyrażenia 6 + 4 / 2 jest 8, a nie 5.

• Operatory o takim samym pierwszeństwie są przetwarzane od lewej do prawej strony (z wy-


jątkiem potęgowania). A zatem w wyrażeniu degrees / 2 * pi dzielenie jest wykonywane ja-
ko pierwsze, a wynik mnożony jest przez wartość pi. Aby podzielić przez wartość 2n, możesz
użyć nawiasów okrągłych lub wyrażenia w postaci degrees / 2 / pi.

Nie staram się zapamiętywać pierwszeństwa operatorów. Jeśli nie jestem w stanie określić tego po
przyjrzeniu się wyrażeniu, stosuję nawiasy okrągłe, aby pierwszeństwo było oczyv,riste.

34 Rozdział 2. Zmienne, wyrażenia I Instrukcje


Operacje na łańcuchach
Ogólnie rzecz bio rąc, nie możesz wyko nywać operacj i matematycznych w przypadku łańcuchów
nawet wtedy, gdy przypom inają one liczby. Oznacza to, że następujące wyrażenia są niepoprawne:
' jajka ' / 'prosto' 'trzeci 1
* 1
ale u rok 1

Istnieją jednak dwa wyjątki , czyli operatory + i *.

Operator + wykonuje operację konkatenacji łańcuchów, co oznacza łączenie łańcuchów przez doda-
wanie jednego łańcucha do końca d rugiego. Oto przykład:
>>> firs t = 'wietrzna'
>>>second = 'pogoda'
>>> firs t + s econd
wi etrznapogoda

Operator * również przetwarza łańcuchy, wykonując operację powtarzania. Na przykład wyrażenie


'Spam' * 3 zapewnia wynik ' SpamSpamSpam' . J eśli jedna z wartości to łańcuch, druga musi być licz-
bą całkowitą.

Takie użycie operatorów + i * nabiera sensu, gdy posłużymy si ę analogią dodawania i mnożenia.
Tak jak wyrażenie 4 * 3 jest równoważne wyrażeniu 4 + 4 + 4, tak też oczekujemy, że wyrażenie
' Spam' * 3 będzie tożsame z wyrażeniem ' Spam' + ' Spam' + ' Spam'. I tak rzeczywiście jest. Z kolei
konkatenacja łańcuchów i powtarzanie w znaczący sposób różnią się od dodawania i mnoże ni a
liczb całkowitych. Czy możesz wyobrazić sobie właściwość dodawania, jakiej pozbawiona jest
konkatenacja łańcuchów?

Komentarze
Gdy programy stają się coraz większe i bardziej złożone, ich czytelność zmniejsza się. Języki formalne
są treściwe. Często trudno po przyjrzeniu się porcji kodu stwierdzić, jakie działani e ten kod wy-
konuje lub dlaczego.

Z tego powodu warto dodawać uwagi do programów zapisane w języku naturalnym, które objaśniają
działania realizowane przez program. Takie uwagi są nazywane komentarzami i rozpoczynają się
symbolem#.
# oblic=enie wartości procentowej god=iny, jaka upłynęła
percentage = (minute * 100) / 60

W tym przypadku komentarz poj awia się w osobnym wierszu. Komentarze mogą też być umiesz-
czane na końcu wiersza:
percentage = (minute * 100 ) / 60 #wartość procentowa god=iny

Wszystko, począwszy od znaku# do końca wiersza, jest ignorowane. Nie ma to wpływu na wyko-
nywanie programu.

Komentarze są najbardziej przydatne, gdy dokumentują elementy kodu, które nie są oczywiste. Roz-
sądne jest przyjęcie, że czytający kod może stwierdzić, do czego ten kod służy. Bardziej pomocne
jest jednak wyjaśnieni e, dlacz ego kod dzi ała tak, a nie inaczej.

Komentarze 35
Następujący komentarz jest niepotrzebny w tym kodzie i bezwartościowy:
v = 5 # pr:ypisanie wartości 5 :miennej v

Poniższy komentarz zawiera przydatne informacje, których nie ma w kodzie:


v = 5 # prędkość w metrach na sekundę

Dobre nazwy zmiennych mogą ograniczyć konieczność stosowania ko mentarzy. Z kolei długie
nazwy mogą utrudnić analizowanie złożonych wyrażeń, dlatego niezbędny jest kompromis.

Debugowanie
W programie mogą wystąpić trzy rodzaje błędów: błędy składniowe, błędy uruchomieniowe i błędy
semantyczne. Warto rozróżnić te błędy, aby móc szybciej je wychwytywać.

Błąd składni owy

Termin „składniowy" odwołuje się do struktury programu oraz dotyczących jej reguł. Na
przykład nawiasy okrągłe muszą występować w dopasowanych parach. Oznacza to, że wyrażenie
( 1 + 2) jest poprawne, ale już 8) to błąd składniowy.

Jeśligdziekolwiek w programie występuje błąd składniowy, interpreter języka Python wy-


świetlakomunikat o błędzie i kończy pracę, co oznacza brak możliwości uruchomienia pro-
gramu. W czasie kilku pierwszych tygodni kariery programistycznej możesz poświęcić mnóstwo
czasu na wychwytywanie błędów składniowych. W miarę zdobywania doświadczenia będziesz
popełniać mniej błędów i szybciej je znajdować.

Błąd uruchomieniowy
Drugi typ błędu to błąd uruchomieniowy, nazwany tak, ponieważ nie pojawia się on do momentu
rozpoczęcia działania programu. Tego rodzaju błędy są też określane mianem wyjątków,
gdyż zwykle wskazują, że wydarzyło się coś wyjątkowego (i złego).

Jak się przekonasz w kilku pierwszych rozdziałach, błędy uruchomieniowe rzadko występują
w prostych programach. Z tego powodu może upłynąć trochę czasu, zanim napotkasz taki błąd.
Błąd składni owy

Trzeci typ błędu jest błędem semantycznym, czyli powiązanym ze znaczeniem. Jeśli w programie
obecny jest błąd semantyczny, program ten zostanie uruchomiony bez generowania komunika-
tów o błędzie, ale nie będzie działać właściwie. Taki program zrobi coś jeszcze, a dokładniej
rzecz ujmując, będzie postępować zgodnie z wytycznymi jego twórcy.
Identyfikowanie błędów semantycznych może być trudne, ponieważ podczas pracy wymaga „co-
fania się" przez sprawdzanie danych wyjściowych programu i podejmowanie prób stwierdze-
nia, jakie działania program wykonuje.

Słownik
z mienna
Nazwa odwołująca się do wartości.

36 Rozdział 2. Zmienne, wyrażenia I Instrukcje


przypisanie
Instrukcja przypisująca wartość zmiennej.

diagram stanu
Graficzna reprezentacja zestawu zmiennych i wartości, do których się one odwołują.
słowo kluczowe
Zastrzeżone słowo używane do analizy programu. Słowa kluczowe, takie jak i f , de f i whi l e ,
nie mogą być używane w roli nazw zmiennych.

argument
Jedna z wartości przetwarzanych przez operator.

wyrażenie

Kombinacja zmiennych, operatorów i wartości, która reprezentuje pojedynczy wynik.


wyz naczanie wartości
Operacja upraszczająca wyrażenie przez wykonywanie operacji mających na celu zapewnienie
pojedynczej wartości.

instrukcja
Sekcja kodu reprezentująca polecenie lub działanie. Do tej pory instrukcje miały postać przy-
pisań i instrukcji wyświetlających.

wykonywanie
Operacja uruchamiania instrukcji i instruowania jej o tym, jakie działania ma zrealizować.

tryb interaktywny
Sposób użycia interpretera języka Python przez wpisywanie kodu w wierszu zachęty.
tryb skryptowy
Sposób użycia interpretera języka Python polegający na wczytywaniu kodu ze skryptu i uru-
chamianiu go.
skrypt
Program zapisany w pliku.
kolejność operacji
Reguły zarządzające kolejnością wyznaczania wartości wyrażeń zawierających wiele operato-
rów i argumentów.

konkatenacja
Łączenie dwóch argumentów przez dodawanie jednego do końca drugiego.
komentarz
Informacje w programie przeznaczone dla innych programistów (łub dowolnej osoby czyta-
jącejkod źródłowy), które nie mają wpływu na wykonywanie programu.

Słownik 37
błqd składniowy

Błąd w programie uniemożliwiający jego analizę składniową (a tym samym interpretowanie).

wyjqtek
Błąd wykrywany podczas działania programu.
semantyka
Znaczenie programu.
błqd semantycz ny

Błąd w programie, który powoduje realizowanie przez ten program czegoś innego ni ż to, co
zostało zamierzone przez programi stę.

Ćwiczenia
Ćwiczenie 2.1.

Powtarzając moją radę z poprzedniego rozdziału, każdorazowo, gdy poznajesz nowy element,
wypróbuj go w t rybie interaktywnym i celowo popełniaj błędy w celu sprawdzenia, co przebiega
niepoprawnie.
• Pokazano, że przypisanie n = 42 jest poprawne. A co z przypisaniem 42 = n?
• Jak wygląda sytuacja w przypadku zapisu x = y = 1?
• W niektórych językach każda instrukcja zakończona jest średnikiem(;). Co będzie, jeśli średnik
zostanie umieszczony na końcu instrukcji języka Python?
• Co się stanie w przypadku wstawienia kropki na końcu inst rukcji?
• W notacji matematycznej pomnożenie x przez y jest możliwe za pomocą zapisu xy. Co będzie,
gdy spróbujesz tego w języku Python?

Ćwiczenie 2.2.

Wykonaj ćwiczenie, używając interpretera języka Python jako kalkulatora:

1. Objętość kuli o promieniu r wynosi ~nr 3 . Jaka jest objętość kuli o promieniu 5?
3
2. Załóżmy, że cena książki podana na okładce to 24,95 zł, ale księgarnie uzyskują 40% upustu.
Koszty wysyłki wynoszą 3 zł przy pierwszym egzemplarzu oraz 75 groszy dla każdego kolej-
nego. Jaka jest całkowita cena hurtowa w przypadku 60 egzemplarzy?
3. Jeśli wyjdę z domu o godzinie 6:52 rano i przebiegnę milę spokojnym tempem (jedna mila w cza-
sie 8 minut 15 sekund), a następnie szybciej 3 mile (jedna mila w czasie 7 minut 12 sekund)
i ponownie jedną milę wolnym tempem, po jakim czasie wrócę do domu na śni adanie?

38 Rozdział 2. Zmienne, wyrażenia I Instrukcje


ROZDZIAŁ 3.

Funkcje

W kontekście programowania funkcja jest sekwencją instrukcji wykonujących obliczenie. Pod-


czas definiowania funkcji określasz nazwę i sekwencję instrukcji. Późni ej funkcja jest wywoływa­
na za pomocą tej nazwy.

Wywołania funkcji
Zaprezentowałem już jeden przykład wywołania funkcji:
» > ty pe ( 42 )
<cl ass 'int' >

Nazwa funkcji to type. Wyrażenie w nawiasach okrągłych nosi nazwę argwnentu funkcji. W przypad-
ku tej funkcji wynikiem jest typ argumentu.
Często mówi się, że funkcja pobiera argument i zwraca wynik. Wynik jest również określany
mianem wartości zwracanej.
W języku Python zapewniane są funkcje dokonujące konwersji wartości z jednego typu na inny.
Funkcja int pobiera dowolną wartość i, jeśli jest to możliwe, konwertuje ją na liczbę całkowitą.
W przeciwnym razie zgłasza komunikat o błędzie:
»> int ( ' 32 ' )
32
»> int (' Witaj ')
Val ue Error : inva l id l iteral for int() : Witaj

Funkcja i nt może konwertować wartości zmiennoprzecinkowe na liczby całkowite, ale nie stosuje
zaokrąglani a. Zamiast tego funkcja obcina część ułamkową:
>>> int(3 . 99999)

»> int( -2. 3)


-2
Funkcja f l oat dokonuje konwersji liczb całkowitych i łańcuchów na liczby zmiennoprzecinkowe:
»> float(32)
32 . 0
»> float (' 3.141 59 ' )
3. 14159

39
I wreszcie, funkcja str przeprowadza konwersję swojego argumentu na łańcuch:
»> 5tr(32)
'32'
>>> 5tr(3.14159)
'3 .14159'

Funkcje matematyczne
Język Python oferuje moduł matematyczny, który zapewnia większość znanych funkcji matema-
tycznych. Moduł to plik zawierający kolekcję powiązanych funkcji.

Zanim użyjemy funkcj i modułu, musimy zaimportować go za pomocą instrukcji import:


»> import mat h

Instrukcja tworzy obiekt modułu o nazwie mat h. Jeśli go wyświetlisz, uzyskasz następujące infor-
macje o nim:
>>> mat h
<module 'math' (built-i n)>

Obiekt modułu zawiera funkcje i zmienne zdefiniowane w module. Aby uzyskać dostęp do jednej
z funkcji, konieczne jest podanie nazwy modułu oraz funkcji oddzielonych kropką. Taki format
nazywany jest notacją z kropką.
>>> ratio = 5ignal _power / noi5e_power
>>> decibel5 = 10 * math. l og lO{ratio)

>>> radian5 = 0.7


>>> height = math.sin(radians)

W pierwszym przykładzie użyto funkcji math. 1ogl O do obliczenia wyrażonego w decybelach sto-
sunku sygnału do szumu (przy założeniu, że zdefiniowano zmienne signal_power i noi se_power).
Moduł math zapewnia też funkcję l og, która oblicza logarytmy o podstawie e.

W drugim przykładzie określany jest sinus dla zmiennej rad i ans. Nazwa zmiennej jest wskazów-
ką, że s i n oraz inne funkcje trygonometryczne (cos, t an itp.) pobierają argumenty wyrażone w ra-
dianach. Aby dokonać konwersji stopni na radiany, wykonaj dzielenie przez 180 i pomnóż przez
liczbę n::
>>> degrees = 45
>>> radian5 = degrees / 180 . 0 * math.pi
>>> math . 5in(radian5)
o. 707106781187
Wyrażenie ma t h. pi uzyskuje zmienną pi z modułu math. Wartość wyrażenia jest aproksymacją
zmiennoprzecinkową liczby n: z dokładnością do około 15 cyfr.

Jeśli znasz trygonometrię, możesz sprawdzi ć powyższy wynik przez porównanie go z wynikiem
dzielenia pierwiastka kwadratowego z liczby 2 przez liczbę 2:
>>> mat h. sqrt(2) / 2. 0
o. 707106781187

40 Rozdział 3. Funkcje
Złożenie
Do tej pory zajmowaliśmy się elementami programu (takimi jak zmienne, wyrażenia i instrukcje)
w postaci wyizolowanej, nie wspominając o sposobie łączenia ich ze sobą.

Jedną z najbardziej przydatnych cech języków programowania jest zdolność pobierania niewiel-
kich bloków konstrukcyjnych i składania ich. Na przykład argumentem funkcji może być dowol-
nego rodzaju wyrażenie, w tym operatory arytmetyczne:
x = math . sin(degrees / 360 . 0 * 2 * math . pi)

Mogą to być nawet wywołania funkcji:


x = math . exp{mat h.l og( x + 1))

Prawie wszędzie możesz umieścić wartość i dowolne wyrażenie, z jednym wyjątkiem: lewa strona
instrukcji przypisania musi mieć postać nazwy zmiennej. Dowolne inne wyrażenie po lewej stro-
nie powoduje błąd składniowy {wyjątki od tej reguły zostaną zaprezentowane dalej).
>>> minutes = hours * 60 #poprawnie
>>> hours * 60 = minutes # :':le.I
Syntax Error : can't assign to ope rator

Dodawanie nowych funkcji


Dotychczas zostały zastosowane wyłącznie funkcje dołączone do języka Pytho n. Możliwe jest jednak
dodawanie nowych funkcji. Definicja funkcji określa nazwę nowej funkcji i sekwencję instrukcji
uruchamianych w momencie wywołania funkcji.

Oto przykład:
def print_lyrics():
print("Jestem drwalem i dobrze się z t ym c zuj ę . ")
print("~pię całą noc i p racuję przez cały d z i eń.")

def to słowo kluczowe wskazujące na definicj ę funkcji. Nazwa funkcji to pri nt_ lyri C$. Reguły obowią­
zujące w przypadku nazw funkcji są takie same jak przy nazwach zmiennych: dozwolone są litery,
liczby i znak podkreślenia, ale pierwszy znak nie może być liczbą. Słowo kluczowe nie może od-
grywać roli nazwy funkcji. Należy unikać stosowania zmiennej i funkcj i o identycznych nazwach.

Puste nawiasy okrągłe po nazwie oznaczają, że dana funkcja nie pobiera żadnych argumentów.
Pierwszy wiersz definicji funkcji jest nazywany nagłówkiem. Reszta jest określana mianem treści.
Nagłówek musi być zakończony dwukropkiem, a treść wymaga zastosowania wcięcia. Zgodnie z kon-
wencją wcięcie zawsze liczy cztery spacje. Treść może zawierać dowolną liczbę instrukcji.

Łańcuchy w instrukcjach wyświetlania są ujęte w podwójny cudzysłów. Pojedynczy i podwójny cudzy-


słów realizują to samo zadanie. Większość osób korzysta z pojedynczego cudzysłowu, z wyjątkiem
sytuacji takich jak ta, gdy pojedynczy cudzysłów Qest to również apostrof) pojawia się w łańcuchu.

Wszystkie znaki cudzysłowu {pojedyncze i podwójne) muszą być „proste" {zwykle są zlokalizo-
wane na klawiaturze obok klawisza Enter). Cudzysłów drukarski, taki jak w poprzednim zdaniu,
w języku Python jest niedozwolony.

Dodawanie nowych funkcji 41


Jeśli definicję
funkcji wpisujesz w trybie interaktywnym, interpreter wyświ etla kropki( ... ), aby
poinformować o tym, że definicja jest niekompletna:

>>> def print_ly ric5() :


print("Je5t em drwalem i dobrze 5i ę z tym c zu j ę. ")
print("Spię całą noc i pracuję przez cały d zień . ")

W celu zakończenia definicj i funkcji musisz wprowadzić pusty wiersz.


Definiowanie funkcj i powoduje utworzenie obiektu funkcji z typem fu net i on:
>>> print(print_lyric5)
<function print_lyric5 at Oxb7e99e9c>
>>> type(print_l yric5)
<c l a55 ' f unction'>

Składnia wywoływania nowej funkcji jest identyczna ze składnią wywoływania funkcji wbudowanych:
>>> print_l yric5( )
Je5tem drwalem i dobrze 5ię z tym c z uję .
Spię całą noc i prac uję przez cał y d zień.

Po zdefiniowaniu funkcji możesz ją zastosować w obrębie innej funkcji. Aby na przykład powtó-
rzyć powyższy refren, możesz utworzyć funkcję o nazwie repeat_ l yri es:
def repeat_lyri c5() :
print_l yr i c5 ()
print_l yr i c5 ()

W dalszej kolejności wywołaj funkcję repeat_l yries:


>>> repeat_lyri c5()
Je5tem drwal em i dobrze 5ię z tym c z uję .
Spię całą noc i pracuję przez c ały d zień.
Je5tem drwal em i dobrze 5ię z tym c z uję .
Spię całą noc i pracuję przez c ały d zień.

W rzeczywistości jednak piosenka ta nie brzmi w ten sposób.

Definicje i zastosowania
Złożenie razem fragmentów kodu z poprzedniego podrozdziału zapewni cały program o nastę­
pującej postaci:
def print_l yric5( ) :
print ( "Jestem drwalem i dobrze 5ię z tym c zu ję." )
print ( " $pię całą noc i pracu ję przez cały d zi eń . " )

def repeat_lyri cs() :


print_l yr i c5()
print_l yr i c5()

repeat_l yr ic5()

Program zawiera definicje dwóch funkcji: print_ l y ri es i repeat _ l yri es. Definicje funkcji są wy-
konywane tak jak inne instrukcje, ale efektem tego jest utworzenie obiektów funkcji. Instrukcje

42 Rozdział 3. Funkcje
wewnątrz funkcji nie są uruchamiane do momentu jej wywołania, a definicja funkcji nie generuje
żadnych danych wyjściowych.

Jak możesz się domyślić, zanim uruchomisz funkcję, musisz ją utworzyć. Inaczej mówiąc, defini-
cja funkcji wymaga uruchomienia przed wywołaniem tej funkcji.
W ramach ćwkzenia przenieś ostatni wiersz tego programu na sam jego początek, aby wywołanie funkcji
pojawiło się przed definicjami. Uruchom program i sprawdź, jaki komunikat o błędzie uzyskasz.

Przenieś następnie wywołanie funkcji z powrotem na dół programu, a definicję funkcji pr i nt_ l yr i es
umieść po definicji funkcji repea t _ l y r i es. Co się stanie, gdy uruchomisz taki program?

Przepływ wykonywania
Aby zapewnić, że funkcja zostanie zdefiniowana przed jej pierwszym użyciem, ko nieczna jest znajo-
mość kolejności uruchamiania instrukcji, która jest określana mianem przepływu wykonywania.
Wykonywanie zawsze rozpoczyna się od pierwszej instrukcji programu. Instrukcje są urucha-
miane po jednej naraz w kolejności od góry do dołu.

Choć definicje funkcji nie zmieniają przepływu wykonywania programu, pamiętaj, że instrukcje
wewnątrz funkcji nie zostaną uruchomione do momentu jej wywołania.
Wywołanie funkcji przypomina „objazd" w przepływie wykonywania. Zamiast przejścia do kolejnej
instrukcji w przepływie ma miejsce przeskok do treści funkcji, uruchomienie w niej instrukcji, a na-
stępnie powrót do miejsca, w którym przerwano przepływ wykonywania.

Wyda Ci się to proste, jeśli zapamiętasz, że jedna funkcja może wywołać drugą. Wykonując dzia-
łania w obrębie jednej funkcji, program może wymagać uruchomienia instrukcji w innej funkcji.
Później może się okazać, że w trakcie działania nowej funkcji program może być zmuszony do
uruchomienia jeszcze jednej funkcji!
Na szczęście język Python dobrze sobie radzi ze śledzeniem bieżącego miejsca wykonywania kodu,
dlatego każdorazowo po zakończeniu działania funkcji program powraca do miejsca, w jakim prze-
rwał wykonywanie funkcji, która wywołała zakończoną funkcję. Po osiągnięciu końca programu
następuje zakończenie jego działania.

Podsumowując, gdy czyta się kod programu, nie zawsze pożądane jest analizowanie go od po-
czątku do końca. Czasami większy sens ma prześledzenie przepływu wykonywania.

Parametry i argumenty
Niektóre z zaprezentowanych wcześniej funkcji wymagają argumentów. Gdy na przykład wywo-
łujesz funkcję ma t h. s i n, jako argument przekazywana jest liczba. Część funkcji pobiera więcej niż
jeden argument: funkcja ma th. pow używa dwóch argumentów, czyli podstawy i wykładnika.
Wewnątrz funkcji argumenty są przypisywane zmiennym nazywanym parametrami. Oto definicja
funkcji pobierającej argument:

Parametry I argumenty 43
def print_twice{bruce ) :
print (bruce )
print (bruce )

Funkcja przypisuje argument parametrowi o nazwie bruce. W momencie wywołania funkcja wy-
świetla
dwukrotnie wartość parametru (niezależnie od tego, jaka ona jest).

Fu nkcja ta obsługuje dowolną wartość, która może zostać wyświetlona:

>>> print_twice {'Spam')


Spam
Spam
>>> print twice {42)
42
42
>>> print_twice (math.pi )
3.14159265359
3.14159265359

Te same reguły tworzenia, które dotyczą funkcji wbudowanych, obowiązują również w przypad-
ku funkcji definiowanych przez programistę. Oznacza to, że w roli argumentu funkcji pri nt _t wi ce
można zastosować dowolnego rodzaju wyrażenie:
>>> print_t wice {'Spam ' * 4)
Spam Spam Spam Spam
Spam Spam Spam Spam
>>> print_t wice {math.cos(math. pi ))
- 1.0
- 1.0

Wartość argumentu wyznaczana jest przed wywołaniem funkcji, dlatego w p rzykładach dla wyra-
żeń ' Spam ' * 4imath. cos (mat h. pi) wartośćokreślanajest tylkoraz.

Możliwe jest też użycie zmiennej jako argumentu:


>>> michae l = 'Eryk, w połowie ps z c z oła . '
>>> print_twice {michael )
Eryk, w połowie ps z c z oła .
Eryk, w połowie ps z c z oła .

Nazwa zmiennej przekazywanej jako argument (mi chae l ) nie ma nic wspólnego z nazwą parame-
tru (bruce). Nie ma znaczenia to, jak wartość została nazwana w miejscu źródłowym (w elemencie
wywołującym). Tutaj w funkcji pri nt_ t wi ce wszystko nosi nazwę bruce.

Zmienne i parametry są lokalne


Gdy tworzysz zmienną wewnątrz funkcj i, jest ona lokalna. Oznacza to, że istnieje tylko w obrębi e
funkcji . Oto przykład:
def cat_twice(part l, part2) :
cat = part!+ part2
print twice{cat)

Fu nkcja pobiera dwa argumenty, łączy je i dwukrotnie wyświetla wynik. Oto przykład wykorzy-
stania tej funkcji:

44 Rozdział 3. Funkcje
>>> l i ne 1 ~ ' Bum bum '
>>> l i ne2 ~ ' bam bam . '
>>> cat_twice( l i nel, line2 )
Bum bum bam bam.
Bum bum bam bam.

Gdy funkcja cat_ t wi ce zakończy działanie, zmienna cat jest usuwana. Jeśli spróbujesz ją wyświetlić, uj-
rzysz wyjątek:
» > pri nt ( cat )
NameE rror : name ' cat' i 5 not def ined

Parametry również są lokalne. Na przykład poza obrębem funkcji pr in t _ twi ce nie istnieje coś ta-
kiego jak parametr bruce.

Diagramy stosu
Aby śledzić to, gdzie mogą być używane jakie zmienne, czasami przydatne jest sporządzenie diagramu
stosu. Podobnie do diagramów stanu, diagramy te prezentują wartość każdej zmiennej, ale też
pokazują, do jakiej funkcji należą poszczególne zmienne.

Każda funkcja reprezentowana jest przez ramkę. Jest to pole z nazwą funkcj i umieszczoną z boku
oraz znaj dującymi się w jego wnętrzu parametrami i zmiennymi funkcji. N a rysunku 3.1 pokaza-
no diagram stosu dla poprzedniego przykładu.

linel ---4-- 'Bum bum ·


_ mai n __
line2 - . 'bam bam.'

partl - - - 'Bum bum '


cat_twice part2 _ . 'bam bam.'
cat _ . 'Bum bum bam bam.'

print_twice bruce - 'Bum bum bam bam.'

Rysunek 3.1. Diagram stosu

Ramki tworzą stos wskazujący, jaka funkcja wywołała jaką funkcję itd. W przykładzie funkcja
print _ lwi ce została wywołana przez funkcję cat _ twi ce, a ta została wywołana przez funkcję _ ma i n_ ,
która ma specjalną nazwę powiązaną z najwyżej położoną ramką. Gdy utworzysz zmienną poza
obrębem dowolnej funkcji, należy ona do funkcji _ ma i n_.
Każdy paramet r odwołuje się do tej samej wartości co odpowiadający mu argument. A zatem pa-
rametr partl ma wartość identyczną z wartością argumentu l i nel , wartość parametru part2 jest
taka sama jak argumentu l i ne2, a parametr bruce ma wartość równą wartości argumentu cat.
Jeśli
podczas wywoływania funkcji wystąpi błąd, interpreter języka Python wyświetla jej nazwę,
nazwę funkcji, która wywołała tę funkcj ę, a także nazwę funkcji wywołującej drugq z wymienio-
nych funkcji, czyli prezentowane są nazwy wszystkich funkcji aż do funkcji_ma i n_.

Diagramy stosu 45
Jeśli na przykład spróbujesz uzyskać dostęp do zmiennej cat w obrębie funkcji pri nt _ twi ce, zo-
stanie wygenerowany błąd NameEr r or :
Trace bac k (innermost last) :
Fil e "t e$t. py" , 1 ine 13, i n main
cat_t wi ce ( l ine l, 1 ine2 )
Fil e "t e$t. py" , 1 i ne 5, in cat t wice
pr i nt _t wice (cat)
Fil e "t e$t. py" , l ine 9 , in print_t wice
pri nt(cat )
NameE rror : name 'cat ' i $ not def ined

Taka lista funkcji nazywana jest śledzeniem wstecznym (ang. traceback). Informuje ona o tym, w ja-
kim pliku programu i wierszu kodu wystąpił błąd, a także jakie wtedy były wykonywane funkcje. Lista
zawiera też wiersz kodu, który spowodował błąd.

Kolejność funkcji w śledzeni u wstecznym jest taka sama jak kolejność ramek w diagramie stosu.
Fu nkcja działająca w danym momencie znajduje się na dole.

Funkcje „owocne" i „puste"


Niektóre z zastosowanych już funkcj i, takie jak funkcje matematyczne, zwracają wyniki. Z braku lep-
szej nazwy nazywam je funkcjami „owocnymi". Inne funkcje, takie jak funkcja pr in t _ twi ce, wy-
konują działanie, lecz nie zwracają wartości. Są one nazywane funkcjami „pustymi" (ang. void).

W przypadku wywoływania funkcji „owocnej" prawie zawsze pożądane jest wykonanie jakiegoś dzia-
łania dla wyniku. Możesz na przykład przypisać go zmiennej lub użyć jako części wyrażenia:

x = mat h. cos(radians)
gol de n = (mat h. $q r t( 5) + 1) / 2

Gdy wywołujesz funkcję w trybie interaktywnym, interpreter języka Python wyświetla wynik:
>>> mat h. sqrt(5 )
2. 2360679774997898

Jeślijednak w przypadku skryptu wywołasz funkcję „owocną" zupełnie samą, wartość zwracana
zostanie na zawsze utracona!
math . sqrt (5)

W tym skrypcie obliczany jest pierwiastek kwadratowy z liczby 5, ale ponieważ skrypt nie zapisuje
ani nie wyświetla wyniku, nie jest specjalnie przydat ny.

Funkcje „puste" mogą wyświetlać coś na ekranie lub spowodować jakiś inny efekt, ale nie zapewniają
wartości zwracanej. Jeśli przypiszesz wynik zmiennej, uzyskasz specjalną wartość o nazwie None:

>>> resu l t = pri nt _twice(' Bum' )


Bum
Bum
>>> pri nt( resul t )
None

46 Rozdział 3. Funkcje
Wartość Nonenie jest tożsama z łańcuchem 'None '. Jest to specjalna wartość z własnym typem:
>>> print(type(None))
<cl ass ' NoneType ' >

Wszystkie dotychczas utworzone funkcje to funkcje „puste". Po przeczytaniu jeszcze kilku roz-
działów zaczniesz tworzyć funkcje „owocne".

Dlaczego funkcje?
Może nie być oczywiste, dlaczego warto zajmować się podziałem programu na funkcje. Oto kilka
powodów:
• Tworzenie nowej funkcji zapewnia możliwość nadania nazwy grupie instrukcji, co ułatwia
czytanie i debugowanie programu.
• Funkcje mogą przyczynić się do zmniejszenia programu przez wyeliminowanie powtarzającego
si ę kodu. Jeśli później dokonasz zmiany, będzie ona wymagana tylko w jednym miejscu.

• Podzielenie długiego programu na funkcje pozwala zdebugować części po jednej naraz, a następ­
nie złożyć je w działającą całość.
• Dobrze zaprojektowane funkcje często przydają się w wielu programach. Po napisaniu i zde-
bugowaniu funkcji możesz z niej ponownie skorzystać.

Debugowanie
Jedną z najważniejszych umiejętności, jaką zdobędziesz, jest debugowanie. Cho ć może być frustrujące,
debugowanie to jeden z najbardziej interesujących i ambitnych elementów programowania, który
pozwala sp rawdzić swoje możliwości intelektualne.

Pod pewnymi względami debugowanie przypomina pracę detektywa. Jesteś konfrontowany z tropami
i musisz wywnioskować, jakie procesy o raz zdarzenia doprowadziły do widocznych efektów.
Debugowanie podobne jest również do nauk badawczych. Po zorientowaniu się, co jest nie tak, mody-
fikujesz program i uruchamiasz go ponownie. J eśli hipoteza była słuszna, możesz p rzewidzieć wy-
nik modyfikacji i wykonać kolejny krok przybliżający do uzyskania działającego programu. Jeżeli
hipoteza okazała się niewłaściwa, m usisz określi ć nową. Jak wskazał Sherlock Holmes: „Ileż razy
mówiłem ci, że skoro wyeliminujesz rzeczy niemożliwe, to to, co pozostanie, chociaż nieprawdopo-
dobne, musi być prawdą" (A. Conan Doyle, Znak czterech).

Dla części osób programowanie i debugowanie to jedno i to samo. Oznacza to, że programowanie jest
procesem stopniowego debugowania programu do momentu, aż działa on zgodnie z oczekiwaniami.
Chodzi o to, że należy rozpocząć pracę z działającym programem i wprowadzać niewielkie mo-
dyfikacje, debuguj ąc je na bi eżąco.

Na przykład Linux to system operacyjny zawierający miliony wierszy kodu. Na początku jednak miał
on postać prostego programu, jakiego Linus Torvalds użył do eksplorowania układu Intel 80386.
Można tu przytoczyć wypowiedź Larry' ego Greenfielda: „Jednym z wcześniejszych projektów Linusa

Debugowanie 47
był program, który dokonywał przełączenia między wyświetlaniem łańcuchów AAAA i BBBB.
Później rozwi nął się on do postaci systemu Linux" (The Linux Users' Guide, wersja beta 1).

Słownik
funkcja
Nazwana sekwencja instrukcji, które realizują pewną przydatną operację. Funkcje mogą po-
bierać
argumenty, a także opcjonalnie zapewniać wynik.

definicja funkcji
Instrukcja tworząca nową funkcję oraz określająca jej nazwę, parametry i zawarte w niej in-
strukcje.
obiekt funkcji
Wartość tworzona przez definicję funkcji. Nazwa funkcji to zmienna odwołująca się do obiektu
funkcji.

nagłówek

Pierwszy wiersz definicji funkcji.


treść

Sekwencja instrukcji w obrębie definicji funkcji.

parametr
Nazwa stosowana wewnątrz funkcji do odwołania się do wartości przekazanej jako argument.

wywołanie funkcji

Instrukcja uruchamiająca funkcję. Składa się ona z nazwy funkcj i, po której następuje lista
argumentów w nawiasach okrągłych.

argument
Wartość zapewniana funkcji w momencie wywołania. Wartość ta jest przypisywana odpo-
wiedniemu parametrowi funkcji.

z mienna lokalna
Zmienna definiowana wewnątrz funkcji. Zmienna lokalna może być używana tylko w obrębie
swojej funkcji.
wartość z wracana

Wynik działania funkcji. Jeśli wywołania funkcji użyto jako wyrażenia, wartość zwracana jest
wartością wyrażenia.

funkcja „owocna"
Funkcja zwracająca wartość.

48 Rozdział 3. Funkcje
funkcja „pusta"
Funkcja, która zawsze zwraca wartość None.
None
Specjalna wartość zwracana przez funkcje „puste".

moduł

Plik zawierający kolekcję powiązanych funkcji i innych definicji.

instrukcja import
Instrukcja wczytująca plik modułu i tworząca obiekt modułu.

obiekt modułu
Wartośćtworzona przez instrukcję import, która zapewnia dostęp do wartości zdefiniowanych
w module.
notacja z kropkq
Składnia służąca do wywołania funkcji w innym module przez podanie nazwy modułu, a po
niej kropki i nazwy funkcji.

złożenie

Zastosowanie wyrażenia jako elementu większego wyrażenia lub instrukcji stanowiącej część
większej instrukcji.

przepływ wykonywania
Kolejność uruchamiania instrukcji.

diagram stosu
Graficzna reprezentacja stosu funkcji, ich zmiennych oraz wartości , do jakich się one odwołują.
ramka
Pole na diagramie stosu, które reprezentuje wywołanie funkcji. Ramka zawiera zmienne lo-
kalne i parametry funkcji.

śledzenie wstecz ne
Lista wykonywanych funkcji, które są wyświetlane w momencie wystąpienia wyjątku.

Ćwiczenia
Ćwiczenie 3.1.

Utwórz funkcję o nazwie right_justi fy, która jako parametr pobiera łańcuch s i wyświetla go z taką
liczbą spacji umieszczonych na początku, aby ostatnia litera łańcucha znalazła się w kolumnie 70 ekranu:

»> right _ justify('monty')


mon ty

Ćwlaenla 49
Wskazówka: skorzystaj z konkatenacji i powtarzania. Ponadto język Python zapewnia funkcję
wbudowaną o nazwie l en, która zwraca długość łańcucha. Oznacza to, że wartości ą wywołania
len ( ' mon ty ') jest liczba 5.

Ćwiczenie 3.2.

Obiekt funkcj i to wartość, jaką możesz przypisać zmiennej lub przekazać jako argument. Na
przykład do_t wi ce jest funkcją, która pobiera obiekt funkcji w postaci argumentu i wywołuje go
dwukrotnie:
def do_t wice( f ) :
f ()
f ()

Oto przykład, w którym wykorzystano funkcję do_ tw i ce do dwukrotnego wywołania funkcji o na-
zwie pr i nt_ spam:
def print_spam() :
print (' spam ' )
do_twice(print_spam)
1. Umieść ten przykładowy kod w skrypcie i sprawdź go.
2 . Zmodyfikuj funkcję do_t wi ce tak, aby pobierała dwa argumenty w postaci obiektu funkcji
i wartości, a ponadto dwukrotnie wywoływała funkcję, przekazując wartość jako argument.
3. Skopiuj do skryptu definicję funkcji pr i nt _t wi ce zam ieszczoną wcześniej w rozdziale.
4. Użyj zmodyfikowanej wersji funkcj i do_twi ce do dwukrotnego wywołania funkcji print_t wi ce,
przekazując łańcuch ' spam' jako argument.
5. Zdefiniuj nową funkcję
o nazwie do_four, która pobiera obiekt funkcji i wartość, czterokrotnie
wywołuje funkcję, przekazując wartość jako parametr. W treści tej funkcji zamiast czterech
instrukcji powinny być tylko dwie.
Rozwiązanie: plik doJour.py, który, tak jak pozostałe pliki z tego rozdziału, jest dostępny pod adresem
f tp:/lftp .helio n.p //przykłady/myjep2.z ip.
Ćwiczenie 3.3.

Uwaga: ćwiczenie powinno być realizowane tylko z wykorzystaniem dotychczas poznanych in-
strukcji i innych elementów.
1. Utwórz funkcję rysującą następującą siatkę:
+ - + +

+ - + +

+ - + +

SO Rozdział 3. Funkcje
Wskazówka: aby w wierszu wyświ etlić więcej niż jedną wartość, możesz użyć sekwencji war-
tości oddzielonych przecinkiem:

pri nt (' +', ' - ')

Domyślnie instrukcja pri nt dokonuje przejścia do następnego wiersza, ale w następujący spo-
sób możesz zmienić to zachowanie i u mi eścić na końcu spację:
pr int ( ' +', end=' ')
print (' - ' )

Wynikiem wykonania tych instrukcji jest łańcuch + -

Instrukcja pr i nt bez żadnego argumentu powoduje zakończenie bieżącego wiersza i przejści e


do następnego.
2. Utwórz funkcję, która rysuje podobną siatkę z czterema wierszami i kolumnami.
Rozwiązanie: plikgrid.py. Info rmacje o autorze: ćwiczenie oparte jest na ćwiczeniu zamieszczonym
w ksi ążce Oualline'a zatytułowanej Practical C Programming, Third Edition (wydawnictwo O'Reilly
Media, 1997).

Ćwlaenla 51
S2 Rozdział 3. Funkcje
ROZDZIAŁ 4.

Analiza przypadku: projekt interfejsu

W tym rozdziale zaprezentowałem analizę przypadku demonstrującą proces projektowania współpra­


cujących ze sobą funkcji.

Wykorzystamy moduł t urt l e, który umożliwia tworzenie obrazów za pomocą odpowiedniej gra-
fiki. Choć moduł ten dołączony jest do większości instalacji języka Python, jeśli uruchomisz jego
interpreter za pomocą witryny PythonAnywhere, nie będziesz w stanie skorzystać z przykładów
opartych na module t urt l e (tak było przynajmniej w czasie, gdy pisałem tę książkę).

Jeżeli zainstalowałeś już


na komputerze język Python, powinno być możliwe uruchomienie przy-
kładów. W przeciwnym razie to dobry moment na przeprowadzenie instalacji. Odpowiednie in-
strukcje zamieściłem pod adresem http://tinyurl.com/ thinkpython2e.

Przykładowy kod użyty w tym rozdziale znajdziesz w pliku polygon.py, który, tak jak pozostałe
pliki z tego rozdziału, jest dostępny pod adresem ftp:/ lftp.helion.pl!przyklady! myjep2.z ip.

Moduł turtle
Aby sprawdzić, czy dostępny jest moduł t ur t l e, otwórz okno interpretera języka Python i wpisz:
>» import t urt l e
>>> bob = turtl e .Tu rt l e()

Po uruchomieniu kod powinien utworzyć nowe okno z niewielką strzałką, która prezentuje ikonę
żółwia. Zamknij okno.

Utwórz plik o nazwie mypolygon.py i wpisz następujący kod:


i mport turt l e
bob = t urtl e. Turt l e()
print(bob)
t urtle .mai nl oop( )

Moduł tur t l e
(z małą literą t) zapewnia funkcję o nazwie Tur t l e (z dużą literą T) tworzącą obiekt
żółwia,który przypisywany jest zmiennej bob. Wyświetlenie zmiennej powoduje uzyskanie nastę­
pującego wyniku:

<turt le . Turt le object at Oxb7bfbf 4c>

Oznacza to, że zmienna bob odwołuje się do obiektu typu Turt l e zdefiniowanego w module turt l e.

53
Funkcja mai nl oop nakazuje oknu poczekać na wykonanie działania przez użytkownika, choć w tym
przypadku może on jedynie zamknąć okno.
Po utworzeniu obiektu żółwi a możesz wywołać metodę pozwalającą na przemieszczanie go w ob-
rębie okna. Metoda podobna jest do funkcji, lecz korzysta z trochę innej składni. Aby na przykład
obiekt żółwia przemieścić do przodu, użyj następującego wywołania:
bob . fd ( 100)

Metoda f d powiązana jest z obiektem żółwia o nazwie bob. Wywo łanie metody przypomina two-
rzenie żądania: prosisz obiekt bob o przemieszczenie się do przodu.

Argumentem metody f d jest odległość wyrażona w pikselach, dlatego rzeczywista wielkość zależy
od używanego wyświetlacza.

Inne metody możliwe do wywo łania w przypadku obiektu żółwia to: bk (powoduje przemieszcze-
nie do tyłu), l t (powoduje obrót w lewo) i rt (powoduje obrót w prawo). Argument metod l t i r t
to kąt podany w stopniach.

Ponadto każdy obiekt żółwia jest powiązany z piórem, które jest skierowane w dół lub w górę. W dru-
gim wariancie podczas przemieszczania obiekt żółwia zostawia ślad. Metody pu i pd odpowiadają
pióru skierowanemu w górę i w dół.

Aby narysować kąt prawy, do programu dodaj następujące wiersze (po utworzeniu obiektu bob i przed
wywołaniem fu nkcji ma i nl oop):
bo b. fd (100)
bo b. 1 t (90)
bo b. f d (100)

Po uruchomieniu tego programu obiekt bob powinien przemieszczać się na wschód, a następnie
na północ, pozostawiając za sobą dwa odcinki linii.

Zmodyfikuj program pod kątem rysowania kwadratu. Nie przerywaj pracy, dopóki nie uzyskasz
działającego
kodu!

Proste powtarzanie
Może się okazać, że utworzyłeś coś podobnego do następujących wierszy:
bo b. fd (100)
bo b. lt(90)

bo b. fd (100)
bo b. lt(90)

bo b. fd (100)
bo b. lt(90)

bo b. fd ( 100 )

W bardziej zwięzły sposób to samo możesz osiągnąć za pomocą instrukcji for. Do pliku mypolygon.py
dodaj następujący przykład i uruchom ten plik ponownie:

S4 Rozdział 4. Analiza przypadku: projekt Interfejsu


for i in range {4) :
print {' Witaj ! ')

Powinien zostać wyświetlo ny następuj ący wynik:


Witaj !
Witaj !
Witaj !
Witaj !

Jest to najprostszy wariant użyci a instrukcji for. Więcej p rzykładów zam ieszczono w dalszej czę­
ści książki. Powinno to jednak wysta rczyć do tego, aby przebudować program rysujący kwadrat.
Nie p rzerywaj pracy, dopóki tego nie ukończysz.

Oto instrukcja for rysuj ąca kwad rat:

f or i in range {4) :
bob . fd ( 100)
bob . l t (90)

Składnia instrukcj i for przypomina defi nicję fu nkcji. Zawiera ona nagłówek zakończony dwu-
kropkiem i treść z wcięciem. Treść może być złożo na z dowolnej liczby instrukcji.

Instrukcja for jest też nazywana pętlą , po nieważ p rzep ływ wykonywania obejmuje treść, po czym
następuje powrót do początku . W tym przypadku kod treści wykonywany jest cztery razy.

Ta wersja programu właściwie różni się nieznacznie od poprzedniej wersj i kodu rysującego kwa-
drat, p o ni eważ po narysowaniu ostatniego boku kwadratu program po raz kolejny wykonuje kod.
Zajmuje to dodatkowy czas, ale upraszcza kod, jeśli w ramach pętli każdorazowo realizowane jest
to samo działa nie. Taka wersja powoduje też przemieszczenie obiektu żółwia z powrotem do położe­
nia początkowego i ustawienie go w kierunku startowym.

Ćwiczenia
Po niżej zamieszczo no zestaw ćwi czeń opartych na „świ ecie żółwi ". Choć mają one być zabawne,
mają też cel. Wyko nując je, zastanów się, jaki jest ten cel.
W dalszych podrozdziałach tego rozdziału podano rozwiązania po niższych ćwiczeń, dlatego przed
wyko naniem tych zadań (lub przynajmniej przed podjęciem próby) nie rozpoczynaj lektury ko-
lejnych podrozdzi ałów.
1. Utwó rz funkcj ę o nazwie square, która pobiera parametr t (reprezentuje obiekt żółwia). Do
rysowania kwadratu funkcja powinna używać obiektu żółwia.
Napisz funkcję, która przekazuje funkcj i square obiekt bob jako argument, a następnie uruchom
ponownie program.
2. Dodaj do funkcji square kolejny param etr o nazwie l ength. Zmodyfikuj t reść funkcji tak, aby
długość boków wynosiła lengt h, a następnie wprowadź zmiany w wywołaniu fu nkcji w celu
zapewnienia d rugiego argumentu. Ponownie u ruchom p rogram. Przetestuj go za po mocą za-
kresu wartości podanych dla parametru l ength.

Ćwlaenla 55
3. Utwórz kopię funkcji square i zmień nazwę na polygon. Dodaj kolej ny parametr o nazwie n i tak
zmodyfikuj treść funkcj i, aby funkcja ta narysowała wielokąt foremny o n bokach.
Wskazówka: w przypadku wielokąta foremnego o n bokach kąty zewnętrzne mają 360/n
stopni.
4. Utwórz funkcję o nazwie ci rcle, która jako parametry pobiera obiekt żółwia t i promień r, a po-
nadto rysuje aproksymowane ko ło p rzez wywołanie funkcji polygon z odpowiednią długością
i liczbą boków. Przetestuj funkcję za pomo cą zakresu wa rtości promienia r.
Wskazówka: określ obwód koła i upewnij się, że l engt h * n = ci rcumference.
5. Utwórz bardziej ogólną wersję funkcji ci rc l e o nazwie arc pobie rającą dodatkowy paramet r
ang le, który określa, jaka część koła ma zostać narysowana. Parametr ten jest wyrażony w stop-
niach, dlatego w przypadku ang l e = 360 funkcja arc powinna narysować pełne koło.

Hermetyzowanie
W pierwszym ćwiczeniu jesteś proszony o umieszczenie kodu rysującego kwadrat w definicji funkcj i,
a następnie o jej wywołanie z przekazaniem obiektu żółwi a jako parametru. O to rozwiązanie:
def square( t) :
for i i n range (4) :
t. fd ( 100)
t. lt (90)
square (bob)
W przypadku najbardziej wewnętrznych instrukcji fd i l t zastosowano podwójne wcięcie, aby poka-
zać, że znajdują się one w obrębie pętli for, która wchodzi w skład definicji funkcji. Wywołani e
square (bob) w następnym wierszu wyrównano do lewego marginesu, który wskazuje koniec zarówno
pętli for, jak i definicji funkcji.

Wewnątrz funkcj i t odwołuje się do tego samego obiektu żółwia bob, dlatego wywołanie t. lt(90) ma
dokładnieten sam efekt co wywołanie bob. l t (90) . Dlaczego w tym przypadku nie wywołać parametru
bob? Chodzi o to, że t może być dowolnym obiektem żółwia, a nie tylko obiektem bob. Z tego po-
wodu możesz utworzyć drugi obiekt żółwia i przekazać go jako argument funkcji square:
al ice = Tu rt l e()
square(a l ice)
Opakowywanie porcji kodu w funkcji jest określane mianem hermetyzacji. J edną z korzyści za-
pewnianych przez hermetyzację jest do łączanie do kodu nazwy, która od grywa rolę czegoś w ro-
dzaju dokumentacji. Inną korzyścią jest to, że jeśli ponownie użyjesz kodu, bardziej zwięzłe bę­
dzie dwukrotne wywołanie funkcji niż skopiowanie i wklejenie jej treści !

Uogólnianie
Następnym krokiem jest dodanie parametru l engt h do funkcji square. O to rozwi ąza ni e:
def square (t, l ength) :
fo r i i n range (4) :

S6 Rozdział 4. Analiza przypadku: projekt Interfejsu


t. fd{l ength)
t. lt (90)
square{bob, 100)

Dodawanie parametru do funkcji określane jest mianem uogólniania, ponieważ sprawia, że funkcja
staje się bardziej ogólna: w poprzedniej wersji funkcji kwadrat zawsze ma taką samą wielkość, wobec-
nej wersji natomiast może mieć dowolny rozmiar.

Kolejnym krokiem również jest uogólnianie. Zamiast rysować kwadraty, funkcja polygon rysuje
wielokątyforemne o dowolnej liczbie boków. Oto rozwiązanie:
def pol ygon(t , n, length):
angle = 360 / n
for i inrange(n) :
t. fd(length)
t.lt(angle)

polygon(bob, 7, 70)
W tym przykładzie rysowany jest wielokąt o siedmiu bokach, z których każdy ma długość 70.
Jeśli korzystasz z języka Python 2, wartość zmiennej ang le może być niepoprawna z powodu dzielenia
liczby całkowitej. Prostym rozwiązaniem jest wykonanie obliczenia ang l e = 360 . O / n. Ponieważ
licznik to liczba zmiennoprzecinkowa, wynikiem dzielenia też jest wartość zmiennoprzecinkowa.

Gdy funkcja zawiera więcej niż kilka argumentów liczbowych, łatwo zapomnieć, jakie mają zna-
czenie lub w jakiej kolejności powinny się pojawić. W takiej sytuacji dobrym pomysłem jest dołączenie
nazw parametrów na liście argumentów:
polygon(bob, n = 7, length = 70)
Argumenty te są nazywane argumentami słów kluczowych, ponieważ uwzględniają nazwy parame-
trów jako „słowa kluczowe" (nie należy mylić ich ze słowami kluczowymi języka Python, takimi jak
whi le i def).

Taka składnia zwiększa czytelność programu. Przypomina również o sposobie działania argumentów
i parametrów: w momencie wywoływania funkcji argumenty są przypisywane parametrom.

Projekt interfejsu
Następnym krokiem jest utworzenie funkcji ci rcle, która jako parametr pobiera promień r. Oto
proste rozwiązanie korzystające z funkcji pol ygon do narysowania wielokąta o 50 bokach:
import mat h

def circle(t , r):


circumference = 2 * math.pi * r
n = 50
length = circumference / n
polygon(t , n, length)

W pierwszym wierszu obliczany jest obwód koła o promieniu r przy użyciu wzoru 2m. Ponieważ za-
stosowano funkcję math. pi, konieczne jest zaimportowanie modułu mat h. Przyjęte jest, że instrukcje
i mport znajdują się zwykle na początku skryptu.

Projekt Interfejsu 57
n to liczba odcinków liniowych w aproksymacji koła, dlatego l ength to długość każdego odcinka. A za-
tem funkcja pol ygon rysuje wielokąt o 50 bokach, który dokonuje aproksymacji koła o promieniu r .
Ograniczeniem tego rozwiązania jest to, że n to stała. Oznacza to, że w przypadku bardzo dużych kół
odcinki liniowe są zbyt długie, a w przypadku małych kół tracony jest czas na rysowanie bardzo nie-
wielkich odcinków. Rozwiązaniem byłoby uogólnienie funkcji, tak by pobierała stałą n jako parametr.
Zapewniłoby to użytkownikowi (każdemu, kto wywołuje funkcję ci re le) większą kontrolę, ale
interfejs byłby mniej p rzejrzysty.

Interfejs funkcji to „podsumowanie" dotyczące sposobu korzystania z niej. Jakie są parametry?


Jakie jest przeznaczenie funkcji? Jaka jest wartość zwracana? Interfejs jest przejrzysty, j eśli umoż­
liwia elementowi wywołującemu wykonanie żądanego działania bez zajmowania się zbędnymi
szczegó łami.

W tym przykładzie promień r należy do interfej su, ponieważ określa ko ło do narysowania. Stała n
jest mniej odpowiednia, gdyż powiązana jest ze szczegółami tego, jak koło powinno być renderowane.
Zamiast wprowadzać nieład w interfejsie, lepiej wybrać właściwą wartość stałej n, w zależności od
wartości ci rcumf e rence:
def circ l e(t , r ) :
c ircumfe rence = 2 * mat h. pi * r
n = i nt (c i r cumf erence / 3) + 1
l ength = c ircum fe rence / n
polygon(t , n, length)

Obecnie liczba odcinków jest liczbą całkowitą zbliżoną do wartości c i rcumference / 3, dlatego długość
każdego odcinka wynosi w przybliżeniu 3. Jest to wartość na tyle mała, aby koła dobrze się prezento-
wały, ale wystarczaj ąco duża, żeby była efektywna i akceptowalna w przypadku koła o dowo lnej
wielkości.

Refaktoryzacja
Gdy tworzyłem funkcję ci r e l e , byłem
w stanie ponownie zastosować funkcję po l ygon, ponieważ
wiel okąt o wielu bokach to dobra aproksymacja koła. Funkcja a r c nie jest jednak aż tak pomocna.
Nie jest możliwe użycie funkcji pol ygon ani funkcj i ci r e l e do narysowania łuku.
Alternatywą jest rozpoczęcie od kopii funkcji pol ygo n i p rzekształcenie jej w funkcj ę arc. Wyni-
kowy kod może wyglądać następująco:
def arc (t , r , angle) :
arc_l engt h = 2 * mat h. pi * r * angle / 360
n = int(arc_l engt h / 3) + I
step_l ength = arc_l engt h / n
step_ang l e = angle /n

for i in range(n) :
t . fd(step_l e ngth)
t . lt(step_ang le)

Druga połowa definicji tej funkcji przypomina funkcję pol ygon. Nie jest jednak możliwe ponowne
wykorzystanie funkcji polygo n bez zmodyfikowania interfejsu. Istnieje możliwość uogólnienia
funkcji po lygon w celu pobrania kąta jako trzeciego argumentu. W takiej sytuacji jednak funkcja

SB Rozdział 4. Analiza przypadku: projekt Interfejsu


ta nie będzie mieć już odpowiedniej nazwy! Zamiast tego bardziej ogólnej funkcji nadajmy nazwę
pol yl i ne (łamana):
det pol yl ine(t , n, l ength , angl e):
for i in range (n) :
t. td (l engt h)
t. lt (angl e )

Można teraz zmodyfikować funkcje pol ygon i a r c w celu użycia funkcji poly l i ne:
det pol ygon(t , n, l engt h) :
angle = 360 . 0 / n
pol yline(t , n, l ength , angl e)

det arc(t , r , ang l e) :


arc_l engt h = 2 * math . pi * r * ang le / 360
n =i nt(arc_lengt h / 3) + 1
~tep_ lengt h = arc_length /n
~tep_ang le = t loat(ang le) /n
pol yl ine(t , n, ~tep_ lengt h, ~t ep_angle)
I wreszcie, można przebudować funkcję ci re le, aby zastosować funkcję a rc:
det circl e (t , r) :
arc(t , r , 360)
Proces ten, czyli przebudowywanie programu w celu ulepszenia interfejsów i ułatwienia ponownego
wykorzystania kodu, określany jest mianem refaktoryzacji. W tym przypadku zauważyliśmy, że
w funkcjach arc i pol ygon występował podobny kod, dlatego został on „wydzielony" do funkcji
pol yl i ne.

Jeśli wcześniej zostałoby to zaplanowane, funkcja polyl i ne mogłaby zostać utworzona jako pierw-
sza, co pozwoliłoby uniknąć refaktoryzacji. Często jednak na po czątku pracy nad projektem nie
dysponuje się informacjami wystarczającymi do zaprojektowania wszystkich interfejsów. Po roz-
poczęciu tworzenia kodu lepiej zrozumiesz ten problem. Czasami refaktoryzacja jest oznaką, że
czegoś się nauczyłeś.

Plan projektowania
Plan projektowania to proces pisania programów. Proces, jaki został użyty w omawianej analizie
przypadku, to hermetyzacja i uogólnianie. Kroki tego procesu są następujące:
1. Rozpocznij tworzenie niewielkiego programu bez definicji funkcji.
2 . Gdy będziesz dysponować działającym programem, zidentyfikuj jego spójną część, umieść ją
w funkcji w ramach hermetyzacji i nadaj jej nazwę.
3. Uogólnij funkcję przez dodanie odpowiednich parametrów.
4. Powtarzaj kroki od 1. do 3. do momentu uzyskania zestawu działaj ących funkcji. Skopiuj i wklej
poprawny kod w celu uniknięcia konieczności jego ponownego wpisywania (i debugowania).
5 . Poszukaj możliwości ulepszenia programu z wykorzystaniem refaktoryzacji. Jeśli na przykład
w kilku miejscach występuje podobny kod, rozważ uwzględnienie go w odpowiedniej funkcji
ogólnej.

Plan projektowania 59
Proces ten ma swoje mankamenty (późni ej poznasz alternatywy), ale może okazać się przydatny,
jeśli początkowo nie wiesz, jak podzielić program na funkcje. Takie rozwiązani e pozwala na pro-
jektowanie na bieżąco.

Notka dokumentacyjna
Notka dokumentacyjna (ang. docstring) to łańcuch umieszczony na początku funkcji objaśniający
interfejs (doc to skrót od słowa documentation, czyli dokumentacja). Oto p rzykład:
def polyl ine(t , n , leng th, angle) :
"'"'Ry suje n odcinków liniowych dla podanej długości i
kąta (w stopniach) m1ęd:y nzm1 . t to obiekt ikony :':ółwia .

for i in range(n) :
t. fd (1 engt h)
t. lt (angle)

Zgodnie z konwencją wszystkie notki dokumentacyjne są łańcuchami z potrójnym znakiem cudzy-


słowu, znanymi również jako łańcuchy wielowierszowe, ponieważ taka liczba cudzysłowów po-
zwala na umieszczenie łańcucha w więcej niż jednym wierszu.

Notka jest krótka, ale zawiera kluczowe informacje, jakich może wymagać osoba, która będzie chciała
skorzystać z funkcj i. W notce w zwięzły sposób wyjaśnio no przeznaczenie funkcji (bez wgłębia nia
się w szczegóły realizowanych operacji). Objaśnione jest, jaki wpływ na zachowanie funkcji ma każdy
parametr, a także jakiego typu powinien być każdy z paramet rów Qeśli nie jest to oczywiste).
Tworzenie tego rodzaju dokumentacji stanowi ważną część projektu interfejsu. Dobrze zaprojektowa-
ny interfejs powinien być prosty do objaśnienia. Jeśli masz kłopot z wyjaśnieniem jednej z użytych
funkcji, być może interfejs mógłby zostać ulepszony.

Debugowanie
Interfejs jest jak kontrakt między funkcją i elementem wywołującym. Element ten „zgadza się" na
zapewnienie określo nych parametrów, a funkcja na zrealizowanie konkretnych dzi ałań.

Na p rzykład funkcja po ly l i ne wym aga czterech argumentów: t musi być obiektem ikony żółwia,
n liczbą całkowitą, parametr length powinien być liczbą dodatnią, a parametr angl e musi być liczbą (co
zrozumiałe, wyrażoną w stopniach).

Wymagania te są nazywane warunkami wstępnymi, ponieważ powinny one być sp ełnio ne przed
rozpoczęciem wykonywania funkcj i. W odwrotnej sytuacji warunki na końcu funkcji to warunki koń­
cowe. Obejmują one zamierzony efekt działania funkcji (np. narysowanie odcinków liniowych) oraz
dowolne efekty uboczne (np. przemieszczenie obiektu żółwi a lub wprowadzenie innych zmian).

Za warunki wstępne odpowiada element wywołujący. Jeśli element naruszy warunek wstępny (po-
prawnie udokumentowany!) i funkcja nie działa właściwie, błąd znajduje się w elemencie, a nie
w funkcji.

60 Rozdział 4. Analiza przypadku: projekt Interfejsu


Jeśli
warunki wstępne zostaną spełnione, a warunki końcowe nie, błąd tkwi w funkcji. Jeżeli oba
rodzaje warunków są przejrzyste, mogą ułatwić debugowanie.

Słownik
metoda
Funkcja powiązana z obiektem i wywoływana za pomocą notacji z kropką.

pętla

Część programu, która może być wielokrotnie uruchamiana.

hermetyzacja
Proces przekształcania sekwencji instrukcji w definicję funkcji.

uogólnianie
Proces zastępowania czegoś przesadnie konkretnego (np. liczby) czymś odpowiednio ogólnym
(np. zmienną lub parametrem).

argument słowa kluczowego


Argument uwzględniający nazwę parametru jako „słowo kluczowe".

interfejs
Opis sposobu użycia funkcji, który obejmuje jej nazwę oraz informacje o argumentach i wartości
zwracanej.
refaktoryzacja
Proces modyfikowania działającego programu w celu ulepszenia interfejsów funkcji oraz innych
elementów jakościowych kodu.

plan projektowania
Proces tworzenia programów.

notka dokumentacyjna
Łańcuch pojawiający się na początku definicji funkcji, który dokumentuje jej interfejs.
warunek wstępny

Wymaganie, jakie powinno zostać spełnione przez element wywołujący przed uruchomieniem
funkcji.

warunek końcowy
Wymaganie, jakie powinno zostać spełnione przez funkcję przed jej zakończeniem.

Słownik 61
Ćwiczenia
Ćwiczenie 4.1.

Pobierz kod użyty w tym rozdziale, plik polygon.py, który, tak jak pozostałe pliki z tego rozdziału,
jest dostępny pod adresem ftp:/ !ftp.helion.p//przyklady/ myjep2.z ip.
1. Narysuj diagram stosu prezentujący stan programu podczas wykonywania wywołania ci rc le(bob,
radi u5) . Operacje aryt metyczne możesz przeprowadzić ręcznie lub dodać do kodu instrukcje
pri nt.

2. Wersja funkcji arc w podrozdziale „Refaktoryzacja" nie jest zbyt dokładna, ponieważ aproksyma-
cja liniowa koła prawie zawsze powoduje wyjście poza obręb rzeczywistego koła. W efekcie ikona
żółwia znajduje się ostatecznie kilka pikseli od właściwego miejsca docelowego. Zaprezentowane
przeze mnie rozwiązanie zapewnia sposób zredukowania rezultatu tego błędu. Przeczytaj kod
i sprawdź, czy to ma sens. J eśli narysujesz diagram, możesz zrozumieć, jak to działa.

Ćwiczenie 4.2.

Utwórz odpowiedni, ogólny zestaw funkcji, któ re mogą rysować kwiatki (rysunek 4. 1).

Ry sunek 4.1. Kwiatki

Rozwiązanie: plikflower.py (wymagany jest również plik polygon.py).

Ćwiczenie 4.3.

Utwórz odpowiedni, ogólny zestaw funkcji, które mogą rysować kształty (rysunek 4.2).

Rysunek 4 .2. Ksz tałty

Rozwiązanie: plik pie.py.

62 Rozdział 4. Analiza przypadku: projekt Interfejsu


Ćwiczenie 4.4.

Litery alfabetu mogą być tworzone przy użyciu umia rkowanej liczby podstawowych elementów,
takich jak linie pionowe i poziome oraz kilka krzywych. Zaprojektuj alfabet, który m oże zostać
narysowany z wykorzystaniem minimalnej liczby podstawowych elementów, a następnie utwórz
funkcje rysujące litery.

Dla każdej litery należy utworzyć jedną funkcję o nazwach rysuj_ a, ry s uj _ b itd. Funkcje umieść
w pliku o nazwie letters.py. Kod takiej „maszyny do pisania" możesz znaleźć w pliku typewriter.py.
Ułahvi to przetestowanie kodu.

Rozwiązanie jest dostępne w pliku letters.py (wymagany jest również plik polygon.py).

Ćwiczenie 4.5.

Pod adresem http://en.wikipedia.org/wiki!Spiral poczytaj na temat spirali, a następnie unvórz program


rysujący spiralę Archimedesa (lub jedną z innych rodzajów spirali).

Rozwiązanie: plik spiral.py.

Ćwlaenla 63
64 Rozdział 4. Analiza przypadku: projekt Interfejsu
ROZDZIAŁ 5.

Instrukcje warunkowe i rekurencja

Głównym elementem opisanym w tym rozdziale jest instrukcja i f , która wyko nuje różny kod za-
leżnieod stanu programu. Na początku jednak chcę zaprezentować dwa nowe operatory: dziele-
nia bez reszty i wartości bezwzględnej.

Dzielenie bez reszty i wartość bezwzględna


Operator dzielenia bez reszty // przeprowadza dzielenie dwóch liczb i zaokrąglanie do liczby całko­
witej. Dla przykładu załóż my, że czas trwania filmu wynosi 105 minut. Możesz wymagać infor-
macji, jaka jest długość filmu w godzinach. W wyniku tradycyjnego dzielenia otrzymujemy liczbę
zmiennoprzecinkową:

>>> minutes = 105


>>> minutes / 60
1. 7 5

Standardowo jednak godziny nie są zapisywane przy użyciu separatora dziesiętnego. Dzielenie bez
reszty pozwala uzyskać całkowitą liczbę godzin z pominięciem części ułamkowej:
>>> minutes = 105
>» hours = minutes 1160
>>> hours

Aby uzyskać resztę, możesz odjąć jedną godzinę podaną w minutach:


>>> remainder = minutes - hours * 60
>>> rema i nder
45

Alternatywą jest zastosowanie operatora wartości bezwzględnej %, który przeprowadza dzielenie


dwóch liczb i zwraca resztę:
>>> remainder = minutes % 60
>>> rema i nder
45

Operator wartości bezwzględnej


jest bardziej przydatny, niż się wydaje. Dzięki niemu możesz na
przykład sprawdzić, czy jedna liczba jest podzielna przez drugą - jeśli x % y zapewnia zero, x jest
podzielne przez y.

65
Możliwe jest również wyodrębnieni e z liczby cyfry lub cyfr po łożonych najbardziej na prawo. Na
przykładdzielenie x % 10 zapewnia najbardziej położoną na p rawo cyfrę liczby x (o podstawie 10).
Analogicznie w wyniku dzielenia x % 100 uzyskujemy dwie ostatnie cyfry.

Jeśli korzystasz z języka Pyth on 2, dzielenie przebiega inaczej. Operator dzielenia / przeprowadza
dzielenie bez reszty, jeśli oba argumenty są liczbami całkowitymi, a dzielenie zmiennoprzecinkowe,
gdy dowolny z argumentów jest typu f loat.

Wyrażenia boolowskie
Wyrażenie boolowskie to wyrażeni e, które jest p rawdziwe lub fałszywe. W następuj ących przy-
kładach użyto operatora ==, który porównuje dwa argu menty i zap ewnia wartość True, gdy są o ne
równe, lub wartość Fal se w p rzeciwnym razie:
>>> 5
Tr ue
>>> 5 6
Fal se
Tr ue i Fal se to specjalne wartości należące do typu bool. N ie są to łańcuchy:
»> ty pe (Tr ue )

<c l ass ' bool ' >


»> t y pe ( Fal se)
<c l ass ' bool ' >

Operator == jest jednym z operatorów relacyjnych. Inne tego rodzaju operatory to:
X != y # x nie 1est równe y
X > y #x jest więks=e ni:': y
X < y #xjest mniejs=e ni:': y
X >= y #xjestwięks=e ni:': y lub równey
X <= y # x 1est mnieJs=e m:': y lub równe y

Te operacje prawdopodobnie są Ci znan e, ale symbole używane w języku Pytho n różnią się od
symboli matematycznych. Częstym błędem jest zastosowanie pojedynczego (=) zamiast podwójnego
znak u rów ności ( == ). Pamiętaj , że znak = to operator p rzypisania, a == to operato r relacyj ny. N ie
ma czegoś takiego jak =< lub =>.

Operatory logiczne
Ist nieją
trzy operatory logiczne: and (i), or (łub) oraz not (nie). Sem antyka (znaczenie) tych ope-
ratorów jest p odobna do ich znaczenia w języku angielskim. Na przykład wyrażenie x > O and x < 10
jest prawdziwe tylko wtedy, gdy x jest większe od zera i mniejsze ni ż 10.

Wyrażenie n%2 == O or n%3 == O jest prawdziwe, jeśli p rawdziwy jest dowolny z podanych waru n-
ków lub oba warunki, czyli wtedy, gdy liczba jest p odzielna przez 2 lub 3.

I wreszcie, operator not neguje wyrażenie boolowskie, dlatego wyrażenie not (x > y) jest prawdziwe,
jeśli wyrażeni ex > y jest fałszywe, czyli wtedy, gdy x jest mniejsze ody lub równe y.

66 Rozdział S. lnstrukcjewarunkowe I rekurencja


Mówiąc wprost, argumenty operatorów logicznych powinny być wyrażeniami boolowskimi, ale
język Python nie jest zbyt rygorystyczny w tym względzie. Dowolna liczba różna od zera interpreto-
wana jest jako wartość Tr ue:
>» 42 and True
True

Taka elastyczność może być przydatna, ale związane są z tym pewne subtelności, które mogą po-
wodować niejasności. Wskazane może być unikanie tego rozwiązania (chyba że wiesz, co robisz).

Wykonywanie warunkowe
Aby utworzyć przydatne programy, prawie zawsze konieczne jest sprawdzanie warunków i od-
powiednia zmiana zachowania programu. Umożliwiają to instrukcje warunkowe. Najprostszą
postacią takiej instrukcji jest instrukcja i f:

if X > O:
print(' x to l iczba dodat nia ')

Wyrażenie boolowskie występujące po instrukcji i f nosi nazwę warunku. Jeśli warunek jest speł­
niony, uruchamiana jest wcięta instrukcja. W przeciwnym razie nie ma miejsca żadne działanie.
Instrukcje i f mają taką samą strukturę jak definicje funkcji: po nagłówku następuje treść z zasto-
sowanym wcięciem. Tego rodzaju instrukcje są określane mianem instrukcji złożonych.

Nie ma ograniczenia odnośnie do liczby instrukcji, jakie mogą się poj awić w treści, ale musi wy-
stępować co najmniej jedna. Sporadycznie przydaje się treść pozbawiona instrukcji (zwykle jest ona
zastępowana przez kod, który nie został jeszcze napisany). W takiej sytuacji możesz użyć instrukcji
pass, która nie realizuje żadnego działania.
if X < O:
pass # DO_ZROBIENIA komec: na jest obsługa wartości u;emnych!

Wykonywanie alternatywne
Druga postać instrukcji i f to wykonywanie alternatywne, w przypadku którego występuj ą dwie moż­
liwości,
a warunek określa, jaka z nich zostanie zastosowana. Składnia prezentuje się następująco:
ifx % 2== 0 :
print(' x t o l iczba parzysta')
el se :
print(' x to l iczba ni eparzysta')

Jeśliw przypadku dzielenia x przez 2 reszta wynosi O, wiadomo, że x to liczba parzysta, a program wy-
świetla odpowiedni komunikat. Jeśli warunek nie zostanie spełniony, uruchamiany jest drugi zestaw
instrukcji. Ponieważ warunek musi mieć wartość True lub Fal se, zostanie użyta dokładnie jedna z al-
ternatyw. Alternatywy są nazywane gałęziami, ponieważ są gałęziami w przepływie wykonywania.

Wykonywanie alternatywne 67
Łańcuchowe instrukcje warunkowe
Czasami istnieją więcej niż dwie możliwości, a ponadto wymagane są więcej niż dwie gałęzie. Jednym
ze sposobów zapisania tego rodzaju obliczenia jest łańcuchowa instrukcja warunkowa:
if X < y:
pri nt( ' x j est mniejsze niż y')
el ifx >y :
print(' x jest więks ze ni ż y')
el s e:
print ( ' x i y są równe')

e l i f to skrót od słów else if. I tym razem zostanie użyta dokładnie jedna gałąź. Nie ma ograniczenia co
do liczby instrukcji el i f . Jeśli występuje klauzula el se, musi znaleźć się na końcu, ale nie jest ona
obowiązkowa.

if c hoi ce == 'a ' :


d raw_a()
el if choi ce == ' b' :
d raw_b()
el if choi ce == ' c' :
d raw_ c ()

Każdy warunek jest kolejno sprawdzany. Jeśli pierwszy jest fałszywy, sprawdzany jest następny
warunek itd. Jeżeli jeden z warunków jest prawdziwy, uruchamiana jest odpowiednia gałąź kodu,
a instrukcja kończy działanie. Jeśli nawet prawdziwy jest więcej niż jeden warunek, uruchamiana
jest tylko pierwsza gałąź powiązana z prawdziwym warunkiem.

Zagnieżdżone instrukcje warunkowe


Jedna instrukcja warunkowa może zostać zagnieżdżona w innej. W poprzednim podrozdziale
można było utworzyć następujący przykład:
if X == y:
print ( ' x i y są równe')
el se :
i f X < y:
print(' x jest mniejsze ni ż y ')
e l se :
print(' x jest więks ze ni ż y')

Zewnętrzna instrukcja warunkowa zawiera dwie gałęzie. W pierwszej gałęzi znajduje się prosta in-
strukcja. Druga gałąź zawiera kolejną instrukcję i f, która ma dwie własne gałęzie. Obie gałęzie mają
postać prostych instrukcji, ale mogłyby też być złożone z instrukcji warunkowych.

Choć wcięcie instrukcji uwidacznia strukturę kodu, zagnieżdżone instrukcje warunkowe bardzo
szybko stają się mało czytelne. Dobrym pomysłem jest unikanie ich, gdy tylko jest to możliwe.
Operatory logiczne zapewniają sposób uproszczenia zagnieżdżonych instrukcji warunkowych. Możli­
we jest na przykład przebudowanie następującego kodu za pomocą jednej instrukcji warunkowej:
if o < x:
if X < 10 :
print(' x to dodatnia l i c zba jednocyfrowa . ')

68 Rozdział S. lnstrukcjewarunkowe I rekurencja


Instrukcja pri nt jest wykonywana tylko wtedy, gdy zostaną spełnione warunki dla obu instrukcji
i f, dlatego ten sam efekt można uzyskać za pomocą operatora and:

i f O < x and x < 10 :


print(' x to dodatnia liczba j ednocyfrowa . ')

W przypadku tego rodzaju warunku j ęzyk Python zapewnia bardziej zwięzłą opcję:

if 0 < X < 10 :
print( 'x to dodatnia liczba j ednocyfrowa .' )

Rekurencja
Jedna funkcja może wywołać drugą. Dozwolone jest również wywołanie funkcji przez samą siebie.
Może nie być oczywiste, dlaczego jest to dobre rozwiązanie, ale ta opcja okazuje się jedną z najbardziej
magicznych rzeczy, jakie program może zrealizować. Dla przykładu przyjrzyj się następującej funkcji:
def countdown(n) :
if n <= O:
print('Odpaleni e ! ')
el s e:
print (n)
countdown(n - 1)

Jeślin to zero lub liczba ujemna, wyświetlane jest słowo Odpal eni e !. W p rzeciwnym razie prezen-
towana jest wartość n, a następnie funkcja o nazwie countdown wywołuj e samą siebie, przekazując
n - 1 jako argument.

Co się stanie, gdy funkcja ta zostanie wywołana w poniższy sposób?


>>> countdown(3)

Wykonywanie funkcji countdown rozpoczyna się od warunku n = 3, a ponieważ n jest większe od


zera, funkcja wyświetla wartość 3, po czym wywołuje samą siebie ...
Wykonywanie funkcji countdown rozpoczyna si ę od warunku n = 2, a ponieważ n jest
większe od zera, funkcja wyświetla wartość 2, po czym wywołuje samą siebie ...

Wykonywanie funkcj i countdown rozpoczyna się od warunku n = 1, a ponieważ n


jest większe od zera, funkcja wyświetla wartość 1, po czym wywołuje samą siebie„.

Wykonywanie funkcji countdown rozpoczyna się od warunku n = O, a po-


nieważ n nie jest większe od zera, funkcja wyświetla łańcuch Odpal enie!,
a następnie zwraca wynik.

Funkcja countdown z warunkiem n = 1 zwraca wynik.

Funkcja count down z warunkiem n = 2 zwraca wynik.

Funkcja countdown z warunkiem n = 3 zwraca wynik.

I ponownie następuje powrót do funkcji_mai n_ . A zatem dane wyj ściowe w całości mają nastę­
pującą postać:

Rekurencja 69
Od pal eni e!

Funkcja wywo łująca samą siebie jest rekurencyjna, a proces jej wykonywania określa ny jest mia-
nem rekurencji.
W ramach kolejnego przykładu moż na utwo rzyć funkcję, któ ra wyświetla łańcuch n razy:
def pri nt_n(s , n) :
i f n <= O:
ret urn
pr int(s)
pr int _n(s , n - 1)

Jeśli n <= O, instrukcja r eturn powoduje zako ńczenie funkcj i. Przepływ wykonywania natychmiast
wraca do elem entu wywołuj ącego, a pozostałe wiersze kodu fu nkcji nie są uruchamiane.
Reszta funkcj i przypomina kod funkcj i countdown: wyświetla ona wartość s, a następnie wywołuje
samą siebie w celu pokazania tej wartości n- 1 razy. Oznacza to, że liczba wierszy danych wyjściowych
wynosi 1 + ( n - 1) , co daje n.
W przypadku tego rodzaju prostych przykładów prawdopodobnie łatwiejsze będzie użycie pętli for.
Dalej zaprezentowane zostaną jednak przykłady z kodem, który trudno napisać z wykorzystaniem pę­
tli for, a łatwo to zrobić za pomocą reku rencji, dlatego warto wcześniej od niej zacząć.

Diagramy stosu dla funkcji rekurencyjnych


W podrozdziale „Diagramy stosu" rozdziału 3. użyto diagramu stosu do reprezentowania stanu pro-
gramu podczas wywołania funkcj i. Tego sam ego rodzaju diagram może ułatwić interpretację funkcji
rekurencyj nej.
Każdorazowo, gdy wywoływana jest funkcja, w języku Python tworzona jest ramka zawierająca jej
zmienne i parametry lokalne. W przypadku funkcji rekurencyjnej w tym samym czasie może istnieć
na stosie wi ęcej niż jedna ramka.
Na rysunku 5.1 pokazano diagram stosu dla funkcji countdown wywo łanej z argum entem n 3.

__mai n__

countdown

countdown n.-. 2

countdown n- 1

coun tdown n- - o

Ry sunek 5.1. Diagram stosu

70 Rozdział S. lnstrukcjewarunkowe I rekurencja


Jak zwykle na samej górze stosu znajduje się ramka funkcj i_ma i n_ . Ramka jest pusta, ponieważ
w funkcji tej nie utworzono jeszcze żadnych zmiennych ani nie przekazano jej żadnych argumentów.
Cztery ramki funkcji countdown mają różne wartości parametru n. Spód stosu, gdzie n = O, nosi nazwę
przypadku bazowego. Ponieważ nie jest tutaj tworzone wywołanie rekurencyjne, nie występuje
więcej ramek.

W ramach ćwiczenia narysuj diagram stosu dla funkcji pr int _ n wywoływanej z argumentami s =
'Wita j' i n = 2. Utwórz następnie funkcję o nazwie do_ n, która jako argumenty pobiera obiekt
funkcj i i liczbę n, a ponadto wywołuje daną funkcję n razy.

Rekurencja nieskończona
Jeślirekurencja nigdy nie osiągnie przypadku bazowego, bez końca będzie tworzyć wywołania reku-
rencyjne, a program nigdy nie zakończy działani a. Jest to znane jako rekurencja nieskończona,
co generalnie nie jest niczym dobrym. Oto minimalny program z rekurencją nieskończoną:
d ef r e curse () :
r e curse ()

W rzeczywistościw większości środowisk programowania program z rekurencją nieskończoną nie


działa bez końca. Interpreter języka Python zgłasza komunikat o błędzie w momencie osiągnięcia
maksymalnej „głębokości" rekurencji:
Fil e "<stdin> " , 1 ine 2, in r e curse
File "<stdin> " , 1 ine 2, i n r e curse
File "<std i n> " , 1 ine 2, i n r e curse

File "<std i n> " , 1 ine 2, i n r e curse


Ru ntimeE rror : Max imum r ecurs i on de pth exceeded

Powyższe dane śledzenia wstecznego są trochę pokaźniejsze niż te zaprezentowane w poprzednim


rozdziale. Po wystąpieniu błędu na stosie znajduje się 1000 ramek recur se!

Jeśli przypadkiem utworzysz rekurencję nieskończoną, przejrzyj funkcj ę w celu upewnienia się, czy
istnieje przypadek bazowy, który nie tworzy wywołania rekurencyjnego. A jeśli taki przypadek wy-
stępuje, sprawdź, czy gwarantowane jest osiągnięcie go.

Dane wprowadzane z klawiatury


Dotychczas napisane programy nie akceptowały żadnych danych wprowadzonych przez użytkownika.
Programy te po prostu za każdym razem wykonywały to samo działanie.

Język Python zapewnia funkcję wbudowaną o nazwie i nput, która zat rzymuje program i oczekuje na
wpisanie czegoś przez użytkownika. Gdy użytkownik naciśnie przycisk Return lub En ter, program
wznawia pracę, a funkcja i nput zwraca w postaci łańcucha to, co zostało wpisane przez użytkownika.
W języku Python 2 ta sama funkcja nosi nazwę raw_i npu t .

Dane wprowadzane z klawiatury 71


>>> t ext = input()
Na co czekasz?
>>> t ext
Na co czekasz?

Przed pobraniem danych wprowadzonych przez użytkownika dobrym pomysłem jest wyświetlenie
zachęty informującej
go, co ma wpisać. Funkcja i nput może pobrać zachętę jako argument:
>>> name = input( ' Ja k masz na i mię ?\ n')
Ja k masz na imię ?
Artur , Król Brytów !
>>> name
Artur , Król Brytów !

Ciąg \n na końcu zachęty reprezentuje znak nowego wiersza, który jest znakiem specjalnym powo-
dującym podzielenie wiersza. Z tego właśnie powodu dane wprowadzone przez użytkownika po-
jawiają się poniżej zachęty.

Jeśli
oczekujesz, że użytkownik wpisze liczbę całkowitą, możesz spróbować skonwertować wartość
zwracaną na wartość typu i nt:

>>> prompt = ' Ja ka j est s zyb ko5ć w powietrzu jas kół ki bez ład u n k u ?\n'
>>>speed = input{prompt)
Jaka jest s zyb k o5ć w powi etrzu jas k ół k i bez ład u n ku?
42
»> int {speed)
42

Jeśli jednak użytkownik wpisze coś innego niż ciąg cyfr, zostanie wygenerowany błąd:
>>>speed = i nput{prompt)
Ja ka jest s zyb k o5ć w powi etrzu jas k ółki bez ład u nk u ?
Co masz na my5 l i ? Jas k ół kę afry kańs k ą czy eu ropejs k ą?
» > int (speed)
ValueError : i nval id 1it e ra l for int() with base 10

Później dowiesz się, jak poradzić sobie z tego rodzaju błędem.

Debugowanie
Gdy wystąpi błąd składniowy lub błąd uruchomieniowy, komunikat o błędzie zawiera wiele informacji,
które mogą być przytłaczające. Najbardziej przydatne elementy komunikatu to zwykle:
• rodzaj błędu,
• miejsce wystąpienia błędu.

Błędy składniowe są
zazwyczaj łatwe do znalezienia, ale istnieje kilka pułapek. Błędy związane z biały­
mi znakami mogą być złożone, ponieważ spacje i tabulatory są niewidoczne, a ponadto przywy-
kliśmy do ignorowania ich.

>>> X = 5
>>> y = 6
Fil e "<stdi n>" , 1 ine 1
y = 6

l ndentati onError : unexpected indent

72 Rozdział S. lnstrukcjewarunkowe I rekurencja


W tym przykładzie problem polega na tym, że drugi wiersz jest wcięty o jedną spację. Komunikat
o błędzie wskazuje jednak na y, co jest mylące. Ogólnie komunikaty o błędzie pokazują miejsce wykry-
cia problemu, ale błąd może tak naprawdę występować wcześniej w kodzie, a czasami w poprzednim
wierszu.
To samo dotyczy błędów uruchomieniowych. Załóżmy, że próbujesz obliczyć stosunek sygnału
do szumu wyrażony w decybelach. Używany wzór ma postać: SNRdb = 10 log10 (P,Jgnai/P,z„,.,). W języku
Python możesz utworzyć następujący kod:
import mat h
signal _power = 9
noise_power = 10
rat i o = si gna l _power //noise _yower
decibels = 10 * mat h.l oglO{ratio)
print{decibels)

Po uruchomieniu tego programu generowany jest wyjątek:


Tracebac k {most recent call last) :
Fi 1e "s nr . py" , l i ne 5 , i n ?
decibels = 10 * math.l og l O{ratio)
ValueE rror : math domain erro r

Komunikat o błędzie
wskazuje na wiersz 5., ale w tym wierszu wszystko jest w porządku. Aby zlokali-
zować błąd, może być przydatne wyświetlenie wartości argumentu rat i o, która okazuje się zerem.
Problem tkwi w wierszu 4., w którym zamiast dzielenia zmiennoprzecinkowego stosowane jest dziele-
nie bez reszty.
Warto poświęcić czas na staranne przeczytanie komunikatów o błędzie, ale nie można zakładać,
że wszystko, co się nich znajduje, jest poprawne.

Słownik
dz ielen ie bez resz ty
Operator identyfikowany za pomocą znaków //, który przeprowadza dzielenie dwóch liczb i za-
okrąglanie (w stronę zera) do liczby całkowitej.

operator wartości bezwzględnej

Operator reprezentowany przez znak p rocenta (%),który przetwarza liczby całkowite i zwraca
resztę w przypadku dzielenia jednej liczby przez drugą.

wyrażen ie boa/owskie
Wyrażenie, którego wartość to True lub Fal se.
operator relacyjny
Jeden z operatorów porównujących argumenty: ==, ! =, >, <,>=i <=.

operator logicz ny
Jeden z operatorów łączących wyrażenia logiczne: and, or i not.

Słownik 73
instrukcja warunkowa
Instrukcja kontrolująca przepływ wykonywania zależnie od danego warunku.
warunek
Wyrażenie boolowskie w instrukcji warunkowej, które określa, jaka gałąź kodu zostanie uru-
chomiona.

instrukcja złożona
Instrukcja złożo na z nagłówka i treści. Nagłówek zakończony jest dwukropkiem. Treść jest
wcięta względem nagłówka.

gałqź

Jedna z alternatywnych sekwencji instrukcji w instrukcji warunkowej.

łańcuchowa instrukcja warunkowa


Instrukcja warunkowa z serią alternatywnych gałęzi.
zagnieżdżona instrukcja warunkowa
Instrukcja warunkowa pojawiająca się w jednej z gałęzi innej instrukcji warunkowej.

instrukcja return
Instrukcja powodująca natychmiastowe zakończenie funkcji i powrót do elementu wywołu­
jącego.

rekurencja
Proces wywoływania aktualnie wykonywanej funkcji.

przypadek bazowy
Gałąź instrukcji warunkowej w funkcj i rekurencyjnej, która nie tworzy wywołania rekuren-
cyjnego.
rekurencja nieskończona

Rekurencja, która pozbawiona jest przypadku bazowego lub nigdy nie osiąga go. Ostatecznie
rekurencja nieskończona powoduje błąd uruchomieniowy.

Ćwiczenia
Ćwiczenie 5.1.

Moduł t i me zapewnia funkcję również nazwaną t i me, która zwraca bieżący czas GMT (Greenwich
Mean Time) w „epoce", czyli arbitralnym czasie pełniącym funkcję punktu odniesienia. W syste-
mach uniksowych początkiem „epoki" jest 1 stycznia 1970 r.
»> import time
»> t ime.time( )
14 37746094. 57 35958

74 Rozdział S. lnstrukcjewarunkowe I rekurencja


Utwórz skrypt odczytujący bieżący czas i przekształcający go w porę dnia wyrażoną w godzinach,
minutach i sekundach, a także jako liczbę dni, jakie upłynęły od początku „epoki".

Ćwiczenie 5.2.

Zgodnie z ostatnim twierdzeniem Fermata nie istnieją żadne dodatnie liczby całkowite a, b i c takie, że:
a"+bn= C11

w przypadku dowolnych wartości n większych niż 2.


1. Utwórz funkcję o nazwie check_f ermat , która pobiera cztery parametry a, b, c i n, a następnie
sprawdza, czy spełnione jest twierdzenie Fermata. Jeśli n jest większe niż 2, a ponadto:
a"-rb" = c"
program powinien wyświetlić komunikat Do l i cha, Fermat s ię myl il'!. W przeciwnym razie
program powinien zaprezentować komunikat Nie, t o nie dzi ata .
2. Utwórz funkcję proszącą użytkownika o podanie wartości parametrów a, b, c i n, która prze-
kształca je w liczby całkowite i używa funkcji chec k_fer mat do sprawdzenia, czy naruszają one
twierdzenie Fermata.

Ćwiczenie 5.3.

Jeśli otrzymałbyś trzy patyki, mógłbyś ewentualnie ułożyć je w formę trójkąta. Jeżeli na przykład
jeden z patyków ma długość 12 cm, a dwa pozostałe po I cm, nie będzie możliwe połączenie ze
sobą w środku krótszych patyków. W przypadku dowolnych trzech długości istnieje prosty test
sprawdzający możliwość utworzenia trójkąta:

fest i dowolna z trzech długości jest większa od sumy dwóch pozostałych, nie jest możliwe ufor-
mowanie trójkqta. W innej sytuacji jest to możliwe (jeśli suma dwóch długości równa się trze-
ciej długości, tworzq one coś, co określane jest mianem trójkqta „zdegenerowanego").
1. Utwórz funkcję o nazwie is_ triangle, która pobiera trzy liczby całkowite jako argumenty, a po-
nadto wyświetla łańcuch Ta k lub Nie, zależnie od tego, czy jest albo nie jest możliwe uformo-
wanie trójkąta z patyków o danych długościach.
2. Utwórz funkcję proszącą użytkownika o podanie trzech długości patyków, która przekształca
je w liczby całkowite i używa funkcji i s_t ri ang l e do sprawdzenia, czy patyki o podanych dłu­
gościach mogą utworzyć trójkąt.

Ćwiczenie 5.4.

Jakie są dane wyjściowe poniższego programu? Narysuj diagram stosu prezentujący stan programu
w momencie wyświetlenia wyniku.
def recurs e(n, s ) :
if n == O:
pri nt(s)
el s e :
recurs e(n - 1, n+ s )
recurs e( 3, O)

Ćwlaenla 75
1. Co się stanie, jeśli powyższa funkcja zostanie wywołana w następuj ący sposób: recurse( - 1, O) ?
2. Utwórz notkę dokumentującą, która objaśnia wszystko (i nic ponadto), czego można wymagać
do skorzystania z tej funkcji.

W poniższych ćwiczeniach użyto modułu turt l e opisanego w rozdziale 4.

Ćwiczenie 5.5.

Zapoznaj się z poniższym kodem funkcj i i zastanów się, czy jesteś w stanie określić jej przeznaczenie
(sprawdź przykłady w rozdziale 4.). Uruchom następnie kod, aby przekonać się, czy miałeś rację.
def draw(t , length , n) :
i f n == O:
r e t urn
angl e = 50
t .fd( lengt h * n)
t. lt (angl e )
draw(t , length , n - 1 )
t. r t{ 2* angle)
draw(t , l engt h, n - I)
t. 1t (ang l e)
t . bk{l engt h * n)

Ćwiczenie 5.6.

Krzywa Kocha to fraktal wyglądający jak na rysunku 5.2. Aby narysować tę krzywą przy użyciu
długości
x, konieczne jest jedynie wykonanie następujących kroków:
1. Narysuj krzywą Kocha o długości x/3.
2. Dokonaj obrotu o 60 stopni.
3. Narysuj krzywą Kocha o długości x/3.
4. Dokonaj obrotu o 120 stopni.
5. Narysuj krzywą Kocha o długości x/3.
6. Dokonaj obrotu o 60 stopni.
7. Narysuj krzywą Kocha o długości x/3.

Rysunek 5.2. Krzywa Kocha

76 Rozdział S. lnstrukcjewarunkowe I rekurencja


Wyjątkiem jest sytuacja, gdy x jest mniejsze niż 3: w tym przypadku możesz po prostu narysować
linię prostą o długości x.

1. Utwórz funkcję o nazwie koch, która jako parametry pobiera obiekt żółwia i długość, po czym
za pomocą tego obiektu rysuje krzywą Kocha o podanej długości.
2. Utwórz funkcję o nazwie s nowfl a ke, która rysuje trzy krzywe Kocha w celu utworzenia obrysu
płatka śniegu.

Rozwiązanie: plik koch.py, dostępny pod adresemftp:/lftp.helion.pl!przyklady!myjep2.z ip.


3. Krzywa Kocha może zostać uogólniona na kilka sposobów. Sprawdź przykłady dostępne pod
adresemhttp://en.wikipedia.org/wiki/Koch_snowflake i zaimplementuj wybrany przez siebie.

Ćwlaenla 77
78 Rozdział S. lnstrukcjewarunkowe I rekurencja
ROZDZIAŁ 6.

Funkcje „owocne"

Wiele spośród użytych funkcji języka Python, takich jak funkcje matematyczne, tworzy wartości
zwracane. Wszystkie napisane dotychczas funkcje są jednak „puste": ich wykorzystanie powoduje
efekt taki jak wyświetlenie wartości lub przemieszczenie ikony żółwia, ale nie zapewniają one wartości
zwracanej. W tym rozdziale dowiesz się, jak tworzyć funkcje „owocne".

Wartości zwracane
Wywołanie funkcji powoduje wygenerowanie wartości zwracanej, która zwykle przypisywana jest
zmiennej lub używana jako część wyrażenia.
e = math .exp( l. O)
he ight = radiu5 * mat h. 5in(radian5)

Utworzone dotąd funkcje są „puste". Mówiąc ogólnie, nie zapewniaj ą one wartości zwracanej, a do-
kładniej,
ich wartość zwracana to wartość None.

W tym rozdziale zostaną (wreszcie) utworzone funkcje „owocne''. Pierwszym przykładem jest
funkcja area, która zwraca powierzchnię koła o danym promieniu:
def area{radiu5) :
a = mat h. pi * radiu5 **2
r eturn a

Co prawda instrukcję return zaprezentowałem już wcześniej, ale w przypadku funkcj i „owocnej"
instrukcja ta obejmuje wyrażenie. Instrukcja ma następujące znaczenie: natychmiastowy powrót
z danej funkcji i użycie następującego po niej wyrażenia jako wartości zwracanej. Wyrażenie może być
dowolnie złożone, dlatego powyższa funkcja mogłaby zostać utworzona w bardziej zwięzłej postaci:
def area(radiu5) :
ret urn math . pi * radiu5**2

Z kolei zmienne tymczasowe, t akie jak a, mogą ułatwić debugowanie.


Czasami przydatne jest zastosowanie wielu instrukcji r e turn, po jednej w każdej gałęzi instrukcji
warunkowej:
def ab5ol ute_va l ue{ x) :
if X < O:
return - x
el 5e :
return x

79
Po nieważ te instrukcje r eturn znajdują się w alternatywnej instrukcji warunkowej, u ruchamiana
jest tylko jedna z nich.

Od razu po uruchomieniu instrukcji r eturn funkcja kończy dzi ałanie bez wykonywania żadnych
kolejnych instrukcji. Kod pojawiający się po instrukcji ret urn lub dowolne inne miejsce, które nigdy
nie będzi e osiągalne dla p rzepływu wykonywania, nazywane jest kodem „martwym".

W przypadku funkcji „owocnej" dob rym po mysłem jest zapewnienie, że każda możliwa ści eżka
wykonywania kodu p rogramu natrafi na instrukcję r et urn. O to p rzykład:
def absolute_val ue (x) :
if X < O:
ret urn - x
if X > O:
ret urn x

Fu nkcja ta jest niepoprawna, ponieważ jeśli x będzie równe O, żaden warunek nie zostanie spełnio ny,
a fu nkcja zakończy działanie bez dotarcia do instrukcji ret urn. Jeżeli p rzepływ wykonywania osiągnie
koniec funkcj i, wartością zwracaną będzie wartość None, która nie jest wartością bezwzględną liczby O:
>>> absolut e_val ue (O)
None

Nawiasem mówiąc, j ęzyk Python zapewnia funkcję wbudowaną o nazwie abs, która oblicza war-
tości bezwzględne.

W ramach ćwiczenia napisz funkcję compare, która pobiera dwie wartości x i y oraz zwraca warto-
ści 1, O i - 1, jeśli warunki wynoszą odp owiednio x > y, x == y i x < y.

Projektowanie przyrostowe
Gdy two rzysz większe funkcje, możesz stwierdzi ć, że debugowanie zajmuje wi ęcej czasu.
Aby poradzić sobie z coraz bardziej złożo nymi p rogramami, możesz wyp róbować p roces określany
mianem projektowania przyrostowego. Jego celem jest uniknięcie długich sesji debugowania przez
jednoczesne dodawanie i testowanie tylko niewielkich porcji kodu.
W ramach przykładu załóż my, że chcesz ustali ć odległość m iędzy dwoma pu nktami określonymi
przez wspó łrzędne (x„ y ,) i (x 2, y 2) . Zgodnie z twierdzeniem Pitagorasa o dległość wynosi:

odległość= ~(X2 - 2
X1) + (Y2 - Y1 )
2

Pierwszym krokiem jest zastanowienie się, jak powinna wyglądać funkcja di stance w języku Python.
Inaczej mówi ąc, jakie są dane wejściowe (parametry), a jakie dane wyjściowe (wa rtość zwracana)?

W tym p rzypadku dane wejściowe to dwa punkty, które możesz reprezentować przy użyciu czterech
liczb. Wartość zwracana to odległość reprezentowana przez wartość zmie nnoprzeci nkową.

Od razu możesz utworzyć zarys funkcji:


def distance(xl, yl, x2 , y2 ) :
return O. O

80 Rozdział 6. Funkcje . owocne"


Oczywiście w tej wersji funkcji nie są obliczane odległości. Funkcja zawsze zwraca zero. Funkcja
jest jednak poprawna składniowo i działa. Oznacza to, że możesz ją przetestować, zani m stanie się
bardziej złożona.

W celu sprawdzenia nowej funkcji wywołaj ją z wykorzystaniem przykładowych argumentów:


>>> distance ( l, 2, 4, 6)
o.o
Wybrałem takie wartości, aby odległość pozioma wyniosła 3, a odległość pionowa była równa 4.
Dzięki temu wynikiem jest liczba 5, czyli przeciwp rostokątna w t rójkącie 3-4-5. Podczas testowania
funkcji warto znać właściwą odpowiedź.

Na tym etapie potwierdziliśmy, że funkcja jest poprawna składniowo. Można rozpocząć dodawanie
kodu do treści funkcji. Następnym rozsądnym krokiem jest ustalenie różnic xr x1 i yry1. W kolejnej
wersji funkcji wartości te są przechowywane w zmiennych tymczasowych i wyświetlane:
def distance(xl, yl, x2, y2 ) :
dx = x2 - xl
dy = y2 - y 1
print ( 'dx wynos i ' , dx)
print ( 'dy wynos i ' , dy)
ret urn O. O

Jeśli
funkcja działa, powinna wyświetlić łańcuchy dx wynos i 3 oraz dy wynos i 4. Jeśli tak jest, wiadomo,
żefunkcja uzyskuje właściwe argumenty i poprawnie przeprowadza pierwsze obliczenie. W przeciw-
nym razie trzeba sprawdzić tylko kilka wierszy kodu.
W dalszej kolejności zostanie obliczona suma argumentów dx i dy podniesionych do kwadratu:
def distance(xl, yl, x2, y2 ) :
dx = x2 - xl
dy = y2 - y 1
dsquared = dx**2 + dy**2
print ( 'dsqua red wynos i : ' , dsqua red )
return O. O

Ponownie należy uruchomić program na tym etapie i sprawdzić dane wyjściowe (powinna to być
liczba 25). I wreszcie, możesz użyć funkcji math. sqrt do obliczenia i zwrócenia wyniku:
def distance(xl, yl, x2, y2 ) :
dx = x2 - xl
dy = y2 - yl
dsquared = dx**2 + dy**2
r esult = math . sqrt{dsquared)
r eturn result

Jeśli kod zadziała poprawnie, funkcja jest gotowa. W przeciwnym razie może być wskazane wy-
świ etlenie wartości zmiennej resul t przed instrukcją return.

Po uruchomieniu ostateczna wersja funkcj i nie wyświetla niczego. Zwraca jedynie wartość. Użyte
instrukcje print przydają się podczas debugowania, ale po zapewnieniu działania funkcji należy je
usunąć. Tego rodzaju kod jest określany mianem szkieletu, ponieważ przydaje się przy budowaniu
programu, lecz nie stanowi części końcowego produktu.

Projektowanie przyrostowe 81
Po rozpoczęciu należy dodawać jednocześnie
tylko jeden lub dwa wiersze kodu. W miarę zdobywania
większego doświadczenia możesz zauważyć, że tworzysz i debugujesz większe porcje kodu. W każdym
razie projektowanie przyrostowe pozwala znacznie skrócić czas poświęcany na debugowanie.

O to kluczowe aspekty procesu:


1. Zacznij od działającego programu i dokonuj niewielkich, stopniowych zmian. Jeśli w jakimś
momencie wystąpi błąd, powinieneś dobrze orientować się w kwestii jego lokalizacji.
2 . Użyj zmiennych do przechowywania wartości pośrednich, aby możliwe było ich wyświetleni e
i sprawdzenie.
3. Po zapewnieniu działania programu możesz zdecydować o usunięciu części kodu szkieletowego
lub skonsolidowaniu wielu instrukcji do postaci złożonych wyrażeń, lecz tylko wtedy, gdy nie
spowoduje to zmniejszenia czytelności programu.
W ramach ćwiczenia zastosuj projektowanie przyrostowe do utworzenia funkcji o nazwie hypot enuse,
która zwraca długość przeciwprostokątnej trójkąta prostokątnego dla długości dwóch pozostałych bo-
ków podanych jako argumenty. W trakcie projektowania rejestruj każdy etap.

Złożenie
Jak powinieneś już się domyślić, możesz wywołać jedną funkcję w obrębie innej. W ramach przykładu
zostanie utworzona funkcja pobierająca dwa punkty (środek koła i punkt na obwodzie) i obliczająca
powierzchni ę koła.

Przyjmij, że środkowy punkt przechowywany jest w zmiennych xc i y c, a punkt na obwodzie w zmien-


nych xp i y p. Pierwszym krokiem jest ustalenie promienia koła, czyli odległości m iędzy dwoma
punktami. Utworzona została służąca właśnie do tego funkcja di s tance:
radius = distance( xc, yc, xp, yp)

Kolejnym krokiem jest wyznaczenie powierzchni koła o takim promieniu. Zostało to zapisane w po-
staci następującego kodu:
result = area(radius)

W wyniku hermetyzacji powyższych kroków w funkcji uzyskujemy kod:


def circ le_area( xc , yc, xp, yp} :
radius = distance(xc , yc, xp, yp)
resu l t = area(radi us)
return res ul t

Zmienne tymczasowe rad i us i res ul t przydają się podczas projektowania i debugowania, ale gdy
program działa, można zapewnić jego większą zwi ęzłość przez złożenie wywołań funkcji:
def circ l e_area( xc , yc , xp, yp} :
return area(distance(xc , yc , xp, yp)}

Funkcje boolowskie
Funkcje mogą zwracać wartości boolowskie, które często są wygodne w przypadku ukrywania
złożonych testów wewnątrz funkcji. Oto przykład:

82 Rozdział 6. Funkcje .owocne"


def is_divisible(x, y) :
i fx % y == O:
return True
el s e:
return Fal s e

Typowe jest nadawanie funkcjom boolowskim nazw, które brzmią jak pytania z odpowiedzią tak/nie.
Funkcja is_div isibl e zwraca wa rtość True lub Fal se, aby wskazać, czy xjest podzielne przez y.
Oto p rzykład:
>>> is_di visi ble(6, 4)
Fal se
>>> is_di visi ble(6, 3)
True

Wynikiem użycia operatora == jest wartość boolowska, dlatego można utworzyć bardziej zwięzłą
funkcję przez bezpośrednie zwrócenie tej wartości:

def is_divisible(x , y) :
return x % y == O

Funkcje boolowskie często są stosowane w instrukcjach warunkowych:


if is_di vi s ible (x, y ) :
print(' x jest podzi elne przez y' )

Kuszące może być utworzenie następującego kodu:


if is_di vi sible (x, y ) == True:
print( 'x jest podzielne przez y')

Dodatkowe porównanie nie jest jednak potrzebne.

W ramach ćwiczenia utwórz funkcję is_between( x, y, z ), która zwraca wartość True, jeśli x :5: y :5: z,
albo wa rtość Fa l se w przeciwnym razie.

Jeszcze więcej rekurencji


Choć omówiłem zaledwie niewielki podzbiór języka Python, możesz być zainteresowany informacją,
że podzbiór ten stanowi kompletny język programowania. Oznacza to, że wszystko, co może zostać
obliczone, można wyrazić za pom ocą tego języka. Dowolny już napisany program może zostać
przebudowany przy użyciu jedynie zaprezentowanych dotychczas elementów (w rzeczywistości
wymaganych będzie jeszcze kilka poleceń do kont rolowania urządzeń takich jak mysz, dyski itp.,
ale nic ponadto).
Udowodnienie powyższego stwierdzenia to nietrywialne ćwiczenie, które po raz pierwszy zostało
zrealizowane przez Alana Turinga, jednego z pierwszych informatyków (niektórzy będą utrzymywać,
że był on matematykiem, ale wielu pierwszych informatyków zaczynało jako matematycy).
Stwierdzenie to jest znane jako teza Turinga. W celu zaznajomienia się z bardziej kompletnym (i do-
kładnym) omówieniem tezy Turinga polecam książkę Michaela Sipsera, Introduction to the Theory
of Computation (Course Technology, 2012).

Aby umożliwić Ci zorientowanie się w możliwościach oferowanych przez dotychczas poznane


narzędzia, ocenimy kilka funkcji matematycznych zdefiniowanych z wykorzystaniem rekurencji.

Jeszae więcej rekurencj i 83


Definicja rekurencyj na przypomina definicję cykliczną w tym sensie, że pierwsza z definicji zawiera
o dwo łanie do tego, co jest defi niowane. Prawdziwie cykliczna definicja nie jest zbyt przydatna.
O to p rzykład:
ostry
Przym iotnik służący do opisania czegoś, co jest ostre.
Jeśli spotkałeś się z taką definicją w słowniku, możesz nie być z niej zadowolo ny. Jeżeli z kolei po-
szukasz d efinicji silni oznaczonej symbolem !, możesz uzyskać następujący wynik:
O!= 1
n! = n(n-1)!

Zgodnie z tą defi nicją silnia liczby O wynosi 1, a silnia dowolnej in nej wartości n to n po mnożone
przez silnię wartości n-1.

A zatem 3! to 3 razy 2!, a to odpowiada 2 razy 1!, co z kolei jest równe 1 razy O!. Podsumowując to
wszystko, 3! jest równe 3 razy 2 razy 1 razy 1, co daje 6.
Jeśli możesz utworzyć definicję rekurencyjną czegoś, j esteś w stanie napisać w języku Python program,
który to obliczy. Pierwszym krokiem jest zdecydowanie, jakie parametry powinny zostać użyte.
W tym przypadku powinno być jasne, że fu nkcja factor i a l pobiera liczbę całkowitą:
def f act orial (n}:

Jeżeli argumentem będzie wartość O, konieczne jest jedynie zwrócenie wartości 1:


def factorial (n}:
i f n == O:
r et urn I

W przeciwn ym razie, co stanowi i nt eresującą część całości, niezbędne jest utworzenie wywo ła nia
rekurencyjnego w celu określenia silni wartości n-1, a następ ni e po m nożenia jej p rzez wartość n:
def factoria l (n}:
i f n == O:
r et urn I
e l se :
recur s e = fact orial (n - I}
res ult = n * recurs e
ret urn r es ult

Przepływ wykonywania w przypadku tego p rogramu przypomina przepływ funkcj i countdown za-
prezentowanej w pod rozdziale „Reku rencja" w rozdziale 5. Jeśli funkcja factor ia l zosta nie wy-
wołana z wartości ą 3:

Ponieważ wartość 3 nie jest wartością O, używana jest d ruga gałąź i obliczana silnia wartości n - 1...
Ponieważ wartość 2 nie jest wartością O, używana jest druga gałąź i obliczana silnia wartości n - 1 ...
Po nieważ wartość 1 nie jest wa rtości ą O, używana jest d ruga gałąź i obliczana silnia
wartości n - 1 .. .
Ponieważ wartość Ojest równa wartości O, używana jest pierwsza gałąź i zwracana
wartość 1 bez tworzenia żad nych dodatkowych wywołań rekurencyjnych.
Wartość zwracana 1 jest mnożona przez wartość n wynoszącą 1, po czym zwracany jest wynik
Wartość zwracana 1 jest mnożona przez wartość n wynoszącą 2, po czym zwracany jest wynik

84 Rozdział 6. Funkcje . owocne"


Wartość zwracana 2 jest mnożona przez wartość n wynoszącą 3, a wynik 6 staje się wartością
zwracaną wywołania funkcj i, które rozpoczęło cały p roces.

Na rysunku 6.1 pokazano diagram stosu dla takiej sekwencji wywo łań funkcji.

_ mai n__
6
facto rial in-3 recurse - 2 result - 6
2
facto rial 1n - 2 recurse _ , result - 2

facto rial 11 ---- 11 recurse ---- 1 result ---- 1

factorial in - o

Rysunek 6.1. Diagram stosu

Jak widać na diagramie, wartości zwracane są przekazywane z powrotem do góry stosu. W każdej
ramce wartość zwracana jest wartości ą zmiennej resu l t, która jest wynikiem iloczynu wartości n
i zmiennej r ec urse.
W ostatniej ramce zmienne lokalne recurse i r esul t nie i stnieją, poni eważ nie uruchomiono kodu
gałęzi, która je two rzy.

„Skok wiary''
Śledzenie przepływu wykonywania to jeden sposób czytania programów, ale szybko może się on
okazać p rzytłaczający. Alternatywą jest to, co
nazywam „skokiem wiary". Zamiast śledzenia przepływu
wyko nywania w przypadku wywołania funkcji przyjmujesz , że działa ona poprawnie i zwraca wła­
ściwy wynik.

Okazuje się, że korzystaj ąc z funkcji wbudowanych, już praktycznie doświadczasz „skoku wiary". Gdy
wywo łuj esz funkcję math. cos lub math. exp, nie sprawdzasz jej zawartości. Po prostu zakładasz, że funk-
cje te działają, po nieważ osoby, które utworzyły funkcje wbudowane, były dobrymi programistami.
To samo dotyczy wywoływani a jednej z własnych funkcji. Na przykład w pod rozdziale „Funkcje
boolowskie" niniejszego rozdziału utworzona została funkcja o nazwie is_ di vi si ble, która określa, czy
jedna liczba jest podzielna przez i nną. Po przekonaniu się, że funkcja ta jest poprawna (dzięki przej-
rzeniu kodu i przetestowaniu go), moż na ją zastosować bez ponownego sprawdzania zawartości.
Tak samo jest w przypadku programów rekurencyj nych. W momencie użycia wywołania rekurencyj-
nego zamiast śledzi ć przepływ wykonywania, należy przyjąć, że wywołanie to dzi ała (zwraca po-
prawną wartość), a następ nie zadać sobie pytanie: „Zakładając, że jestem w stanie określić silni ę
wartości n-1, czy mogę obliczyć silnię wa rtości n?". Oczywiste jest to, że można to zrobić przez
pomnożenie przez wartość n.

Rzecz jasna trochę dziwne jest założenie, że funkcja działa poprawnie, gdy nie ukończono jej tworze-
nia, ale właśni e z tego powodu jest to określane mianem „skoku wiary"!

„Skok wiary" 85
Jeszcze jeden przykład
Po funkcji factori al najczęściej spotykanym przykładem funkcji matematycznej definiowanej z wyko-
rzystaniem rekurencj i jest funkcja f i bona ce i, której definicja jest następująca (zajrzyj pod adres
http://pl.wikipedia.org/wiki!Ci%C4%85g_Fibonacciego):
fibonacci(O) = O
fibonacci(l) = 1
fibonacci(n) = fibonacci(n-1)-t-fibonacci(n-2)
Po dokonaniu translacji na język Python ta defi nicja prezentuje się następująco:
def fibonacci (n) :
i f n == O:
return O
elif n == l :
return I
el se :
return fibonacci (n - I ) + fibonacci(n - 2)

Jeśliw tym przypadku spróbujesz prześledzić przepływ wykonyw·ania, nawet dla dość niewielkich
wartości n, może się to skończyć poważnym bólem głowy. Jeżeli jednak zgodnie z zasadą „skoku
wiary" przyjmiesz, że dwa wywołania rekurencyj ne działają poprawnie, jasne jest, że uzyskasz
właściwy wynik, gdy je zsumujesz.

Sprawdzanie typów
Co się stanie, gdy zostanie wywołana funkcja fac torial z argumentem o wa rtości 1,5?
>» fact or ial { I. 5)
Ru ntimeError : Ma ximum recursion depth exceeded

Wygląda to na rekurencję nieskończoną. Jak to możliwe? Funkcja dysponuje przypadkiem bazowym,


gdy n == O. Jeśli jednak n nie jest liczbą całkowitą, możliwe jest pominięcie przypadku bazowego i wy-
konywanie rekurencji bez końca.

W pierwszym wywo łaniu rekurencyjnym wartość n wynosi 0,5. W następnym wywołaniu jest to
wartość -0,5. Dalej wartość staje si ę mniejsza (bardziej ujemna), ale nigdy nie będzie równa O.

Dostępne są dwie opcje. Można podjąć próbę uogólnienia funkcji fac tor ial tak, aby o bsługiwała
liczby zmiennoprzecinkowe. Druga możliwość to spowodowanie sprawdzenia przez tę funkcję typu jej
argumentu. Pierwsza opcja nosi nazwę funkcji gam ma i omawianie jej wykracza poza zasięg tej
książki. Z tego względu skorzystamy z drugiej opcji.

Do sprawdzenia typu argumentu m ożna użyć funkcji wbudowanej is i ns tance. Zajmując się tym,
można też zapewnić, że argument będzie wartością dodatnią:

def f act o ri al {n) :


if not isinstance{n, int):
print('Silnia j est def i niowana tylko dla l icz b całkow i tych . ' )
return None
el if n < O:
pr int ( 'S i ln i a n ie j est def ini owana d la ujemnych li c zb cał kowity ch .' )

86 Rozdział 6. Funkcje . owocne"


return None
elif n == O:
return 1
el s e:
return n * factorial (n - 1)

Pierwszy przypadek bazowy obsługuje liczby inne niż całkowite, drugi przypadek natomiast zajmuje
się ujemnymi liczbami całkowitymi. W obu przypadkach program wyświetla komunikat o błędzie
i zwraca wartość None, aby wskazać, że coś się nie powiodło:
>» f actoria l ('fred ')
Silnia jest definiowana tylko dla liczb cał k owityc h.
None
»> fact oria l (- 2)
Silnia ni e jest defini owana dla ujemnych l iczb cał kowit ych .
None

Jeślioba sprawdzenia zakończą się pomyślnie, wiadomo, że wartość n jest dodatnia lub równa zero,
dlatego możliwe jest potwierdzenie zakończenia rekurencji.

Powyższy program demonstruje wzorzec nazywany czasami „strażnikiem". Pierwsze dwie instrukcje
warunkowe odgrywają rolę „strażników", chroniąc następujący po nich kod przed wartościami, które
mogą powodować błąd. „Strażnicy" umożliwiają potwierdzenie poprawności kodu.

W podrozdziale „Wyszukiwanie odwrotne" rozdziału 11. poznasz bardziej elastyczną alternatywę


wyświetlania komunikatu o błędzie, czyli zgłaszanie wyjątku.

Debugowanie
Dzielenie dużego programu na mniejsze funkcje powoduje utworzenie naturalnych punktów kontrol-
nych na potrzeby debugowania. Jeśli funkcj a nie działa, istnieją trzy następujące możliwości do
rozważenia:

• Występuje problem z argumentami uzyskiwanymi przez funkcję. Naruszany jest warunek


wstępny.

• Występuje problem z funkcją. Naruszany jest warunek końcowy.


• Występuje problem z wartością zwracaną lub sposobem korzystania z niej.

Aby wyeliminować pierwszą możliwość, możesz dodać instrukcję print na początku funkcji i wyświe­
tlić wartości parametrów (i być może ich typy). Ewentualnie możesz napisać kod jawnie spraw-
dzający warunki wstępne.

Jeśliparametry wyglądają na poprawne, dodaj instrukcję prin t przed każdą instrukcją return i wy-
świetl wartość zwracaną.W miarę możliwości sprawdź wynik ręcznie. Rozważ wywołanie funkcji
z wartościami ułatwiającymi sprawdzenie wyniku Qak w podrozdziale „Projektowanie przyrostowe"
niniejszego rozdziału).
Jeżeli funkcja wydaje się działać, przyjrzyj się jej wywołaniu, aby upewnić się, że wartość zwracana
jest poprawnie używana (lub w ogóle wykorzystywana!).

Debugowanie 87
Dodanie instrukcji print na początku i końcu funkcji może ułatwić zwiększeni e wido czności prze-
pływu wykonywania. Oto na przykład wersja funkcji fac t or i al z instrukcjami pr int:

def f actorial {n) :


space = ' ' * {4 * n)
pri nt (space , ' factoria l ' , n)
i f n == O:
print(space , ' ret urning l ' )
return 1
el se :
r e curse = fact orial (n - 1)
r esult = n * r e curse
print(space , 'ret urning ' , r esu l t)
r et urn r esult

space to łańcuch złożony ze spacji, który kontroluje wcięcie danych wyjściowych. Oto wynik wy-
wołania funkcji fac t or i a l ( 4 ) :
f actorial 4
factorial 3
factoria l 2
f actorial 1
factorial O
r et u r ni ng 1
r etu r n i ng
returning 2
r etu m i ng 6
ret urning 24

Jeślimasz wątpliwości dotyczące przepływu wykonywania, tego rodzaju dane wyjściowe mogą okazać
się pomocne. C hoć zaprojektowanie efektywnego kodu szkieletowego wymaga trochę czasu,
dzięki temu można zaoszczędzić mnóstwo czasu podczas debugowania.

Słownik
z mienna tymczasowa
Zmienna służąca do przechoW}"vania wartości pośredniej w złożonym obliczeniu.
kod „martwy"
Część programu, która nigdy nie może zostać uruchomiona. Często wynika to z tego, że znajduje
się ona po instrukcji r e tu rn.
projektowanie przyrosto we
Plan projektowania programu mający na celu uniknięcie debugowania przez jednoczesne do-
dawanie i testowanie tylko niewielkiej ilości kodu.

szkielet
Kod używany podczas projektowania programu, który jednak nie stanowi części jego ostatecznej
wersji.

„strażn ik"

Wzorzec programowania używający instrukcji warunkowej do sprawdzania i obsługi sytuacji,


które mogą spowodować błąd.

88 Rozdział 6. Funkcje .owocne"


Ćwiczenia
Ćwiczenie 6.1.

Narysuj d iagram stosu dla poniższego programu. Co zostanie przez niego wyświetlone?
def b( z ) :
prod = a( z, z )
print (z, prod)
return prod

def a(x, y ) :
X = X + 1
return x * y

def c( x , y, z) :
tot a 1 = x + y + z
5quare = b(t otal) **2
return 5quare

X =
y =
X + 1
print(c(x, y + 3 , x + y) )

Ćwiczenie 6.2.

Funkcja Ackermanna, A(m, n), ma następującą definicję:


n +1 gdy m= O
A(m, n)= A(m -1, 1) gdym> Oi n = O
i
A(m -1, A(m, n -1)) gdy m > Oin> O

Więcej informacji dostępnych jest pod adresem http://pl.wikipedia.org/wiki/Funkcja_Ackermanna.


Utwórz funkcję o nazwie ac k, która oblicza funkcję Ackermanna. Użyj funkcji do wyznaczenia
wartości wywołania ack( 3 , 4), która powinna wynieść 125. Co się dzieje w przypadku większych
wartości mi n?

Rozwiązanie: plik ackermann.py.

Ćwiczenie 6.3.

Palindrom to słowo, które b rzmi tak samo, niezależnie od tego, czy wymawiane jest od przodu,
czy od tyłu (np. sos lub radar). Z rekurencyjnego punktu widzenia słowo jest palindromem, jeśli
pierwsza i ostatnia litera są takie same, a środkowe litery to palindrom.
Oto funkcje pobierające argument w postaci łańcucha i zwracające pi erwszą, ostatnią oraz środkowe
litery:
def fir5t( word) :
r eturn word [O]

def 1a5t (word) :


r eturn wo rd [ - 1]

def mi ddle(wo rd):


retur n wo rd[l :-1]

Ćwlaenla 89
Działanie tych funkcji zostanie omówione w rozdziale 8.

1. Wpisz kod tych funkcji w pliku o nazwie palindrome.py i przetestuj je. Co się stanie, gdy wywo-
łasz funkcję middl e z łańcuchem liczącym dwie litery? A co będzie w przypadku jednej litery?
A jak sytuacja będzie się przedstawiać dla łańcucha pustego zapisywanego w postaci ' ' , który
nie zawiera żadnych liter?
2. Utwórz funkcję o nazwie is_pal i ndrome, która pobiera argument w postaci łańcucha i zwraca
wartość True, jeśli jest to palindrom, a wartość Fal se w przeciwnym razie. Pamiętaj, że do
sprawdzenia długości łańcucha możesz użyć funkcji wbudowanej l e n.

Rozwiązanie: plik palindrome_soln.py.

Ćwiczenie 6.4.

Liczba a jest potęgą liczby b, jeśli jest ona podzielna przez liczbę b. Z kolei al b to potęga liczby b.
Utwórz funkcję o nazwie is _powe r, która pobiera parametry a oraz b i zwraca wartość True, gdy liczba
a jest potęgą liczby b. Uwaga: konieczne będzie zastanowienie się nad przypadkiem bazowym.

Ćwiczenie 6.5.

Największy wspólny dzielnik (nwd) liczb a i b to najwi ększa liczba, która dzieli je bez żadnej
reszty.

Jeden ze sposobów określenia największego wspólnego dzielnika dwóch liczb oparty jest na obserwacji,
w przypadku której jeśli r jest resztą, gdy a zostanie podzielone przez b, wtedy nwd(a, b) = nwd(b, r).
Jako przypadek podstawowy można wykorzystać nwd(a, O)= a.

Utwórz funkcję o nazwie gcd, która pobiera parametry a i b, po czym zwraca ich największy
wspólny dzielnik.

Informacje o autorze: ćwiczenie oparte jest na przykładzie pochodzącym z książki Abelsona i Sussma-
na zatytułowanej Structure and Interpretation of Computer Programs (wydawnictwo MIT Press, 1996).

90 Rozdział 6. Funkcje .owocne"


ROZDZIAŁ 7.

Iteracja

Ten rozdział jest poświęcony iteracji, czyli możliwości wielokrotnego uruchamiania bloku in-
strukcji. Odmianę iteracji korzystającą z rekurencji zaprezentowałem w podrozdziale „Rekurencja"
rozdziału 5. Innego rodzaju iterację używającą pętli for pokazałem w podrozdziale „Proste powta-
rzanie" rozdziału 4. W tym rozdziale poznasz jeszcze jeden rodzaj iteracji oparty na instrukcji whi l e.
Chciałbym jednak wcześniej trochę miejsca poświęcić przypisaniu zmiennej.

Ponowne przypisanie
Jak być może już zauważyłeś, dozwolone jest utworzenie więcej niż jednego przypisania tej samej
zmiennej. Nowe przypisanie powoduje, że istniejąca zmienna odwołuje się do nowej wartości (a tym
samym przestaje odwoływać się do starej wartości).
>>> X =5
>>> X

>>> X
>>> X

Przy pierwszym wyświetleniu wartością zmiennej x jest 5. Za drugim razem jej wartość to 7.
Na rysunku 7.1 pokazano, jak ponowne przypisanie wygląda na diagramie stanu.

Rysunek 7.1. Diagram stanu

W tym miejscu chcę zająć się częstym źródłem niejasności. Ponieważ w języku Python na potrzeby
przypisania używany jest znak równości (=),kuszące jest interpretowanie instrukcji takiej jak a = b ja-
ko matematycznego twierdzenia równości, czyli stwierdzenia, że a i b są sobie równe. Taka inter-
pretacja jest jednak błędna.
Przede wszystkim równość to symetryczna relacja, a przypisanie nią nie jest. Jeśli na przykład w mate-
matyce a = 7, wtedy 7 =a. Jednakże w języku Python instrukcja a = 7 jest dozwolona, a 7 = a nie jest.

91
Ponadto w matematyce twierdzenie równości jest zawsze prawdziwe lub fałszywe. Jeżeli obecnie a= b,
a zawsze będzie równe b. W języku Python instrukcja przypisania może sprawić, że dwie zmienne
będą równe, ale taki stan nie musi być trwały:

>>> a =
>>> b = a # a i b są tera: równe
>>> a = # a i b me są;u::: równe
>>> b

W trzecim wierszu zmieniana jest wartość zmiennej a, ale nie wartość zmiennej b, dlatego te
zmienne nie są już sobie równe.

Ponowne przypisywanie zmiennych często jest przydatne, ale należy korzystać z tej możliwości z roz-
wagą. Jeśli wartości
zmiennych często się zmieniają, może to utrudnić czytanie i debugowanie kodu.

Aktualizowanie zmiennych
Typową odmianą ponownego przypisywania jest aktualizacja, w przypadku której nowa wartość
zmiennej zależy od starej wartości.
>>> X = X + 1

Oznacza to: „Uzyskaj bieżącą wartość zmiennej x, dodaj wartość, a następnie zaktualizuj zmienną x
za pomocą nowej wartości".

Jeśli spróbujesz zaktualizować zmienną, która nie istnieje, zostanie wygenerowany błąd, ponieważ
w języku Python przed przypisaniem wartości zmiennej x sprawdzana jest prawa strona przypisania:
>>> X = X + 1
NameE rror : name ' x ' i~ not def ined

Przed zaktualizowaniem zmiennej konieczne jest zainicjowanie jej, co zwykle odbywa się z wy-
korzystaniem prostego przypisania:
>>> X =Q
>>> X = X + 1

Aktualizowanie zmiennej przez dodanie liczby 1 nazywane jest inkrementacją. Z kolei odejmo-
wanie liczby 1 to dekrementacja.

Instrukcja while
Komputery służą często do automatyzowania powtarzających się zadań. Powtarzanie identycz-
nych lub podobnych zadań bez popełniania błędów to coś, z czym komputery dobrze sobie radzą,
a ludzie kiepsko. W programie komputerowym powtarzanie jest określane mianem iteracji.

Zaprezentowałem już dwie funkcje, countdown i pri nt_ n, które dokonują iteracji z wykorzystaniem
rekurencji. Ponieważ iteracja jest tak częsta, język Python zapewnia elementy, które ją ułatwiają.
Jeden z nich to instrukcja for omówiona w podrozdziale „Proste powtarzanie" rozdziału 4. Zaj-
miemy się nią ponownie później.

92 Rozdział 7. Iteracja
Kolejny element to instrukcja whi l e. Oto wersja fu nkcji countdown używającej instrukcji whi l e:
d e f countdown(n) :
wh i le n > O:
print (n)

print('Odpa leni e! ')

Instrukcję whi l e możesz odczytać,jakby była niemal częścią tekstu w języku angielskim. Znaczenie
powyższego kodu jest następujące: „Gdy argument n jest większy niż O, wyświetl jego wartość, a na-
stępnie dokonaj dekrementacji argumentu n. Po osiągnięciu zera wyświetl łańcuch Odpalenie! ".

Przedstawiając to bardziej formalnie, p rzep ływ wykonywania w przypadku instrukcji wh i le wy-


gląda następująco:

1. Określ, czy warunek jest prawdziwy, czy fałszywy.


2. Jeśli jest fałszywy, zakończ instrukcję whi le i kontynuuj wykonywanie od następ nej instrukcji.
3. Jeśli warunek jest prawdziwy, u ruchom treść instrukcji wh i l e , a następni e powróć do kroku 1.

Tego typu przepływ nazywany jest pętlą, ponieważ w trzecim kroku ma miejsce powrót na po-
czątek instrukcji wh i l e.

Kod będący treścią pętli powinien zmienić wa rtość jednej lub wi ększej liczby zmiennych tak, aby
ostatecznie warunek stał się fałszywy, co spowoduje zakończenie pętli . W przeciwnym razie pętla
będzie powtarzana bez końca, co jest określane mianem pętli nieskończonej . Niezmiennie źródłem
rozbawienia dla informatyków jest to, że wskazówki dotyczące używania szampo nu („Uzyskaj
pia nę, spłucz i powtórz") stanowią pętlę ni eskończo ną.

W przypadku funkcji countdown można potwierdzi ć, że pętla jest kończona: jeśli n to zero lub liczba
ujemna, pętla nigdy nie zostanie wykonana. W innej sytuacji n ma coraz mni ejszą wartość z każdym
wyko naniem pętli , dlatego ostatecznie musi zostać uzyskane zero.
W przypadku niektórych innych pętli nie tak łatwo to stwierdzić. Oto przykład:
def sequence(n) :
whi l e n ! = 1:
print (n)
i f n % 2 == O: # n jest par:yste
n = n / 2
e l se : # n jest niepar:yste
n = n * 3 +

W tej pętli warunek to n ! = 1, dlatego będzie ona kontynuowana do momentu, aż n uzyska wartość 1,
co spowoduje, że warunek będzie fałszywy.

Każdorazowo podczas wykonywania pętli program zwraca wartość n, a następnie sprawdza, czy
jest o na parzysta, czy nieparzysta. Jeśli to wartość parzysta, n dzielone jest przez 2. W przypadku
wartości nieparzystej wartość n jest zastępowana przez n * 3 + 1. Jeśli na przykład argument przeka-
zany fu nkcji sequence to liczba 3, wynikowe wartości n to 3, 10, 5, 16, 8, 4, 2 i 1.

Ponieważ wartość n jest czasami zwiększana, a czasami zmniejszana, nie ma oczywistego dowodu na
to, że n w ogóle osiągnie wartość 1 albo że program zostanie zakończony. W przypadku określo­
nych wartości n moż na po twierdzi ć zako ńczenie programu. Jeśli na p rzykład wartość początkowa

Instrukcja whlle 93
to liczba 2 podniesiona do potęgi, wartość n będzie parzysta dla każdego wykonania pętli do momentu
osiągnięcia przez nią liczby 1. W powyższym przykładzie uzyskuje si ę taki ciąg rozpoczynający si ę
od liczby 16.

Trudną kwestią jest to, czy można potwierdzić, że powyższy program zostanie zakończony dla wszyst-
kich dodatnich wartości n. Jak dotąd nikomu nie udało się tego udowodnić ani obalić (więcej informa-
cji znajdziesz pod adresem http://pl.wikipedia.org/ wiki/Problem_Collatza).

W ramach ćwiczenia przebuduj funkcję pri nt_ n z podrozdziału „Rekurencja" rozdziału 5., używając
iteracj i w miejsce reku rencji.

Instrukcja break
Czasami nie wiesz, czy w trakcie wykonywania kodu pętli jest odpowiedni moment, aby ją zakończyć.
W tym przypadku możesz skorzystać z instrukcji br ea k w celu opuszczenia pętli.
Dla przykładu załóżmy, że wymagasz pobierania od użytkownika wprowadzanych przez niego
danych do momentu wpisania łańcucha gotowe. Możliwe jest utworzenie następującego kodu:
wh i le True:
l i ne = i nput ( '> ' )
if line == ' got owe':
brea k
print (l ine )

pr i nt( 'Got owe!' )

Warunek pętli to wartość Tr ue . Ponieważ jest on zawsze spełniony, pętla jest wykonywana do
chwili natrafienia na instrukcję break.
Każdorazowo podczas wykonywania pętli użytkownikowi wyświetlany jest nawias> jako znak zachęty.
Jeśli użytkownik wpisze słowo gotowe, instrukcja break zakończy pętlę. W przeciwnym razie program
wyświ etli wszystko, co zostanie wprowadzone przez użytkownika, po czym powróci na początek
pętli. Oto przykładowe uruchomienie programu:

> niegotowe
niegotowe
> gotowe
Gotowe !

Taki sposób tworzenia pętli whi l e jest często używany, ponieważ dzięki tem u możesz sprawdzi ć
warunek w dowolnym miejscu pętli (a nie tylko na jej początku), a ponadto możesz wyrazić warunek
zatrzymania twierdząco (zatrzymaj, gdy to ma miejsce), a nie negatywnie (kontynuuj do momentu, aż
to ma miejsce).

Pierwiastki kwadratowe
Pętle są często używane w programach obliczających wyniki numeryczne p rzez rozpoczęcie od
aproksymowanej odpowiedzi i iteracyjne poprawianie jej.

94 Rozdział 7. Iteracja
Jeden ze sposobów obliczania pierwiastków kwadratowych to na przykład metoda Newtona. Za-
łóżmy, że chcesz uzyskać pierwiastek kwadratowy liczby a. Jeśli zaczniesz od niemalże dowolnej
wartości szacunkowej x, lepszą wartość szacunkową możesz obli czyć za pomocą następuj ącego
wzoru:
x+a l x
y =--
2
Jeżeli na przykład a to 4, a x to 3:
> >> a 4
> >> X
> >> y (x + a / x ) / 2
> >> y
2 . 1666666666 7

Wynik jest bliższy poprawnej odpowiedzi (/,i= 2 ). Jeśli proces zostanie powtórzony z użyciem
nowej wartości szacunkowej, wynik będzie jeszcze bliższy:
> >> X = y
> >> y = ( x + a / x ) / 2
> >> y
2. 0064 1025641

Po kilku dodatkowych aktualizacjach wartość szacu nkowa jest prawie dokładna:


> >> X = y
> >> y = ( x + a / x ) / 2
>>> y
2. 00001024003
>>> X = y
>>> y = (x + a / x ) / 2
>>> y
2. 00000000003

Ogólnie rzecz ujmując, nie wiadomo z góry, ile kroków będzie wymaganych do uzyskania właściwej
odpowiedzi, ale będziemy w stanie określi ć ten moment, gdyż wartość szacunkowa przestanie si ę
zmieniać:

>>> X = y
>>> y = (x + a / x ) / 2
>>> y
2.0
>>> X = y
>>> y = (x + a / x ) / 2
>>> y
2.0

Gdy y == x, można zakończyć obliczenia. Oto pętla zaczynająca się od początkowej wartości sza-
cunkowej x, która jest poprawiana do momentu, aż p rzestanie się zmieniać:
whi l e True :
print( x)
y = (x + a / x ) / 2
if y == x:
break
X = y

Pierwiastki kwadratowe 95
W przypadku większości wartości zmiennej a pętla działa świetnie, ale przeważnie niebezpieczne jest
testowanie równości z wykorzystaniem typu fl oat. Wartości zmiennoprzecinkowe są tylko w przybli-
żeniu właściwe: większość liczb wymiernych (np. 1/3) oraz liczb niewymiernych, takich jak J2,
nie może być dokładnie reprezentowana za pomocą typu f l oat.
Zamiast sprawdzać, czy x i y są dokładnie równe, bezpieczniej jest użyć funkcji wbudowanej abs
do obliczenia wartości bezwzględnej lub wielkości różnicy między tymi wartościami:
if abs(y - x) < e ps il on :
break

W powyższym kodzie e psi l o n ma wartość taką jak 0 . 0000001 , która określa, czy „blisko" jest wy-
starczająco blisko.

Algorytmy
Metoda Newtona to przykład algorytmu: jest to mechaniczny proces rozwiązywania kategorii
problemów (w omawianym przykładzie obliczania pierwiastków kwadratowych).

Aby zrozumieć, czym jest algorytm, pomocne może być rozpoczęcie od czegoś, co nim nie jest. Gdy
uczyłeś się mnożenia liczb wielocyfrowych, prawdopodobnie zapamiętałeś tabliczkę mnożenia.
W efekcie utrwaliłeś w pamięci 100 konkretnych rozwiązań. Tego rodzaju wiedza nie jest algorytmiczna.
Jeślijednak byłeś „leniwy", być może poznałeś kilka sztuczek. Aby na przykład wyznaczyć iloczyn
n i 9, możesz zapisać n-1 i 10-n odpowiednio jako pierwszą i drugą cyfrę. Sztuczka ta to ogólne
rozwiązanie w przypadku mnożenia dowolnej liczby jednocyfrowej przez 9. To jest właśnie algorytm!

Podobnie algorytmami są poznane techniki dodawania z przeniesieniem, odejmowania z pożyczaniem


oraz dzielenia przez liczbę wielocyfrową. Jedną z właściwości algorytmów jest to, że do zrealizowania
nie wymagają one inteligencji. Są to mechaniczne procesy, w których każdy krok wynika z poprzed-
niego zgodnie z prostym zestawem reguł.
Wykonywanie algorytmów jest nudne, ale ich projektowanie okazuje się interesujące, a także stanowi
wyzwanie intelektualne i centralny punkt informatyki.

Niektóre czynności wykonywane przez ludzi w naturalny sposób bez trudu lub świadomego myślenia
sąnajtrudniejsze do wyrażenia w sposób algorytmiczny. Dobrym przykładem jest zrozumienie ję­
zyka naturalnego. Wszyscy się nim posługujemy, ale jak dotąd nikt nie był w stanie wyjaśnić, w jaki
sposób się to odbywa, a przynajmniej nie w postaci algorytmu.

Debugowanie
Gdy zaczniesz pisać większe programy, być może stwierdzisz, że więcej czasu poświęcasz debu-
gowaniu. Więcej kodu oznacza większe ryzyko popełnienia błędu i więcej miejsc, w których mogą
ukrywać się błędy.

Jednym ze sposobów skrócenia czasu debugowania jest „debugowanie przez podział na póf'. Jeśli
na przykład w programie znajduje się 100 wierszy kodu i sprawdzasz je po jednym naraz, będzie
to wymagać wykonania 100 kroków.

96 Rozdział 7. Iteracja
Spróbuj zamiast tego rozbić problem na dwie części. Poszukaj mniej więcej w środkowej części
kodu programu wartości pośredniej możliwej do sprawdzenia. Dodaj instrukcję pr i nt (łub coś innego,
co zapewnia możliwy do sprawdzenia wynik) i uruchom program.

Jeżeli sprawdzenie w środkowej części kodu okaże się niepoprawne, problem musi występować
w pierwszej połowie programu. W przeciwnym razie problem tkwi w drugiej połowie.

Każdorazowo przy wykonywaniu tego rodzaju sprawdzenia liczba wierszy, które mają zostać prze-
szukane, jest dzielona na pół. Po sześciu krokach (czyli mniej niż po stu), przynajmniej w teorii, powi-
nien pozostać jeden wiersz kodu lub dwa.

W praktyce nie zawsze oczywiste jest, czym jest „środek programu", a ponadto nie za każdym razem
możliwe jest sprawdzenie tego. Nie ma sensu liczenie wierszy i znajdowanie dokładnego środka
kodu. Zamiast tego zastanów się nad miejscami w programie, w któ rych mo gą występować błędy,
a także nad miejscami, gdzie z łatwością można zdefiniować sprawdzenie. Wybierz następnie miejsce,
w przypadku którego uważasz, że są mniej więcej takie same szanse na to, że błąd występuje przed
kodem sprawdzającym lub po nim.

Słownik
ponowne przypisanie
Przypisanie nowej wartości już istniej ącej zmiennej.

aktualizacja
Przypisanie, w przypadku którego nowa wartość zmiennej zależy od starej.

inicjalizacja
Przypisanie zapewniaj ące wartość początkową zmiennej, która zostanie zaktualizowana.
inkrementacja
Aktualizacja zwiększająca wartość zmiennej (często o jeden).
dekrementacja
Aktualizacja zmniej szająca wartość zmiennej.

iteracja
Powtarzane wykonywanie zestawu instrukcji za pomocą wywołania funkcji rekurencyjnej lub
pętli.

Pftla nieskończona
Pętla, w przypadku której warunek zakończenia nigdy nie zostanie spełniony.
algorytm
Ogólny proces rozwiązywania kategorii problemów.

Słownik 97
Ćwiczenia
Ćwiczenie 7.1.

Skopiuj pętlę z podrozdziału „Pierwiastki kwadratowe" tego rozdziału i dokonaj jej hermetyzacji
w funkcji o nazwie mysqr t, która pobiera zmienną a jako parametr, wybiera rozsądną wartość
zmiennej x, a także zwraca wartość szacunkową pierwiastka kwadratowego z wartości zmiennej a.
Aby przetestować tę funkcję, utwórz funkcję o nazwie test_ square_root, która wyświetla nastę­
pującą tabelę:
a mysqrt (a) mat h. sqrt (a) di ff

1. 0 1. 0 1. 0 o.o
2. 0 1.41421 356237 1.41421 356237 2.22044604925e- 16
3. 0 1.73205080757 1.73205080757 o.o
4. 0 2.0 2. 0 o.o
5. 0 2 . 2360679775 2.2360679775 o.o
6. 0 2 . 44948974278 2.44948974278 o.o
7. 0 2 . 64575131106 2. 64575131106 o.o
8. 0 2.82842712475 2. 82842712475 4.4408920985e-16
9. 0 3. 0 3. 0 o.o
Pierwsza kolumna zawiera liczbę a. Druga kolumna zawiera pierwiastek kwadratowy z liczby a
obliczony za pomocą funkcji mysqr t. W t rzeciej kolumnie znajduje się pierwiastek kwadratowy
obliczony przez funkcję math . sqrt. Czwarta kolumna zawiera wartość bezwzględną różnicy mię­
dzy dwiema wartościami szacunkowymi.

Ćwiczenie 7.2.

Funkcja wbudowana eval pobiera łańcuch i przetwarza go za pomocą interpretera języka Python.
Oto przykład:
»> e va l (' 1 + 2 * 3')
7
»> import mat h
»> e val ( 'math . sqrt (5) ' )
2.2 360679774997898
»> e val (' type( ma t h. pi) ')
<c l ass ' fl oat '>

Utwórz funkcję o nazwie e va l _ loop, która iteracyj nie prosi użytkownika o podanie danych, pobie-
ra wprowadzone dane, przetwarza je za pomocą funkcji eva l , a następnie wyświetla wynik.
Działanie funkcji powinno być kontynuowane do m omentu wpisania przez użytkownika łań­
cucha got owe. Gdy to nastąpi, funkcja powinna zwrócić wartość ostatniego wyrażenia, dla którego ją
wyznaczyła.

98 Rozdział 7. Iteracja
Ćwiczenie 7.3.

Matematyk Srinivasa Ramanujan odkrył ciągi nieskończone, które mogą posłużyć do generowania
aproksymacji numerycznej wartości 1/rr:

..!._= ~ (4k)!(l l 03 +26390k)


2./2
1t 9801~ (k!)43964k

Utwórz funkcję o nazwie e s ti mate_pi, która używa powyższego wzoru do obliczenia i zwrócenia
wartości szacunkowej liczby n. Funkcja powinna stosować pętlę whi l e do obliczania składników
sumowania d o momentu, aż ostatni składnik jest mniejszy niż wartość le - 15 (w języku Python
jest to zapis wartości 10-15). Wynik możesz sprawdzić przez porównanie go z wynikiem funkcji
math . pi.

Rozwiązanie: plik pi.py.

Ćwlaenla 99
100 I Rozdział 7. Iteracja
ROZDZIAŁ 8.

Łańcuchy

Łańcuchy nie przypominają liczb całkowitych, wartości zmiennoprzecinkowych i wartości boolow-


skich. Łańcuch to ciąg, czyli uporządkowana kolekcja innych wartości. W tym rozdziale dowiesz się,
jak uzyskiwać dostęp do znaków tworzących łańcuch, a także poznasz niektóre metody zapewniane
przez łańcuchy.

Łańcuch jest ciągiem


Łańcuch to ciąg znaków. Operator w postaci nawiasu kwadratowego pozwala uzyskać dostęp do
jednego znaku naraz:
>>> f r ui t = ' ananas '
>>> lette r = f r uit [U

Druga instrukcja wybiera ze zmiennej frui t znak o numerze jeden i przypisuje go zmiennej l etter.

Wyrażenie w nawiasach kwadratowych nazywane jest indeksem. Indeks wskazuje żądany znak ciągu
(stąd też taka nazwa).
Możesz jednak uzyskać nie to, czego oczekujesz:
> >> l etter
, n'

Dla większości osób pierwszą literą w łańcuchu ana nas jest a, a nie n. W przypadku informatyków
indeks to przesunięcie względem początku łańcucha, które dla pierwszej litery wynosi zero.
> >> l etter = f r uit [O]
> >> l etter
'a '

A zatem litera a to zerowa litera łańcucha a na nas, litera n to jego pierwsza litera, a kolejna litera a
to druga litera tego łańcucha.
W roli indeksu możesz użyć wyrażenia zawierającego zmienne i operato ry:
> >> i = 1
> >> f r uit [i ]
, n'
>>> f r ui t [i + l]
, a,

10 1
Wartość indeksu musi być jednak liczbą całkowitą. W przeciwnym razie uzyskasz następujący błąd:
»> letter = fru it[l.5]
TypeError: string indices must be integers

Funkcja len
len to funkcja wbudowana, która zwraca liczbę znaków łańcucha:
>>> fruit =1
ananas 1
»> len(fruit)
ó

Aby uzyskać ost atnią literę łańcucha, możesz się pokusić o wypróbowanie czegoś takiego:
>>> l ength = len( fruit)
>>> last = fruit[length]
l ndexError: string index out of range

Powodem błędu IndexError jest to, że w łańcuchu ananas nie ma żadnej litery z indeksem 6. Ponieważ
liczenie znaków rozpoczęto od zera, pięć liter numerowanych jest od Odo 5. W celu uzyskania ostat-
niego znaku konieczne jest odjęci e liczby 1 od parametru l e ngth:
»>last = fruit[length - l]
>>> last

Możliwe jest też zastosowanie indeksów ujemnych, które liczą znaki od końca łańcucha. Wyrażenie
fru i t [ - 1]zapewnia ostatnią literę, wyrażenie fru i t [ -2] daje literę drugą od końca itd.

Operacja przechodzenia za pomocą pętli for


Wiele obl iczeń obejmuje przetwarzanie po jednym znaku łańcucha jednocześnie. Często rozpo-
czynają się one od początku łańcucha. Kolejno wybierany jest każdy znak, który następnie jest
w jakiś sposób przetwarzany. Proces ten jest kontynuowany aż do końca łańcucha. Taki wzorzec
przetwarzania określany jest m ianem przechodzenia. Jeden ze sposobów definiowania operacji
przechodzenia bazuje na pętli wh il e:
index = O
wh ile index < len(fruit) :
letter = fruit[index]
pri nt (letter)
index = index + 1

W przypadku tej pętli operacja przechodzenia dotyczy łańcucha. Każda jego litera jest wyświetlana
w osobnym wierszu. Warunek pętli to index < len( fruit }, dlatego w sytuacji, gdy wartość zmiennej
i ndex jest równa długości łańcucha, warunek jest fałszywy, a zawartość pętli nie zostanie uruchomiona.
Ostatni znak, do którego uzyskiwany jest dostęp, to znak o indeksie len{ fru i t } - 1, czyli ostatni znak
łańcuch a.

W ramach ćwiczenia utwórz funkcję pobierającą łańcuch jako argument i wyświetlającą litery od
tyłu, po jednej w każdym wierszu.

102 I Rozdział 8. lańcuchy


Inny sposób definiowania operacj i p rzechodzenia wykorzystuje pętlę for:
f or l ette r i n f r ui t:
print(lette r)

Przy każdym wykonaniu pętli zmiennej l etter przypisany jest następny znak ła ńcucha. Pętla jest
kontynuowana do m omentu, aż nie pozosta ną żadne znaki.
W po niższym przykładzie pokazano, jak użyć konkatenacji (łączenia łańcuchów) i pętli for do
wygenerowania ciągu w postaci kolej nych liter alfabetu. W książce Roberta McCloskeya zatytu-
łowanej Make Way for Ducklings użyto następujących imion kaczątek: Jack, Kack, Lack, Mack,
Nack, Ouack, Packi Quack. Poniższa pętla zwraca te imiona w kolejności alfabetycznej:
prefi xes = 'J KLMNOPQ'
s uffi x = 'ack '

f or lette r in prefi xes :


pri nt(lette r + s uffix )

Oto dane wyjściowe:


Jac k
Kac k
La ck
Ma ck
Na ck
Oack
Pa ck
Qac k

Oczywiście rezultat nie jest do końca poprawny, po nieważ imio na Ouack i Quack zostały wy-
świetlo ne z literówkami. W ramach ćwiczenia zmodyfikuj program w celu usunięcia tego błędu.

Fragmenty łańcuchów
Segment łańcucha nazywany jest fragmentem. Wybieranie fragmentu przypomina wybór znaku:
>>> s = ' Monty Python'
»> s[O: S]
' Monty '
»> s [6 : 12]
'Python'

Operator [n: m] zwraca część łańcucha od znaku o pozycji n do znaku o pozycji m z uwzględnieniem
pierwszego znaku, lecz bez ostatniego znaku. Taki sposób działania nie jest intuicyjny, ale może
ułatwić wyo brażenie sobie indeksów wskazujących między znakami (rysunek 8.1).

Rysunek 8. 1. Indeksy fragmentów łań cu cha

Fragmenty łańcuchów I 103


Jeślipominiesz pierwszy indeks (przed dwukropkiem), fragment łańcuch a będzie się rozpoczynać
na jego początku. W przypadku pominięcia drugiego indeksu fragment będzie obowiąZ}"vać aż do
końca łańcucha:

>>> fruit = 'ananas'


»> fru i t [ : 3]
1
ana'
»> fruit [3 : ]
1
nas.'

Jeśli pierwszy indeks jest większy od drugiego indeksu lub równy drugiemu indeksowi, wynikiem
jest pusty łańcuch reprezentowany przez dwa znaki apostrofu:
>>> fruit = 1 ananas. 1
»> fruit [3: 3]

Pusty łańcuch nie zawiera żadnych znaków i ma długość zerową, ale poza tym jest taki sam jak każdy
inny łańcuch .

Kontynuując omawiany przykład, jakie według Ciebie jest znaczenie wyrażenia f rui t [:] ? Sprawdź je
i przekonaj się.

Łańcuchy są niezmienne
Kuszące może być zastosowanie operatora [] po lewej stronie przypisania z zamiarem zm iany
znaku w łańcuchu. Oto przykład:
>>> greeting = ' Witaj, świecie ! '
»> greeting [O] = 'J'
TypeError : 'str ' object does not support i tem assignment

W tym przypadku obiektem jest łańcuch, a elementem znak, któ ry spróbowano przypisać. Na razie
terminu obiekt możesz używać tak samo jako terminu wartość, ale pó źniej definicja ta zostanie
doprecyzowana (w podrozdziale „Obiekty i wartości" rozdziału 10.).
Przyczyną błędu jest to, że łańcuchy są niezmienne. Oznacza to, że nie możesz zmodyfikować istnieją­
cego łańcucha. Najlepszym rozwiązaniem jest utworzenie nowego łańcucha, który jest wariantem
oryginalnego łańcucha:
>>> greeting = ' Witaj , ś w ieci e! '
>>> new_greeting = 'J' + greeting[l : ]
>>> new_greeting
' J itaj , ś w iecie ! '

W tym przykładzie pierwsza nowa litera jest łączona z fragm entem ła ńcucha przypisanego
zmiennej g reet i ng. Nie ma to żadnego wpływu na oryginalny łańcuch.

Wyszukiwanie
Do czego służy poniższa funkcja?
def fi nd( word , letter) :
index = O

104 I Rozdział 8. lańcuchy


wh i le index < len (word) :
i f word [i ndex] == lette r:
ret urn i ndex
index = i ndex + 1
r e t urn -1

W pewnym sensie funkcja f i nd jest odwrotnością operatora []. Zamiast pobierać indeks i wyod-
rębniać odpowiedni znak, funkcja ta pobiera znak i znajduje indeks identyfikujący jego położenie.
Jeśli nie znaleziono znaku, funkcja zwraca wartość - 1.

Jest to pierwszy przykład, w którym instrukcja return znajduje się wewnątrz pętli. Jeśli word [i ndex]
l et ter, funkcja przerywa wykonywanie pętli i natychmiast zwraca wynik.

Jeżeli znaku nie ma w łańcuchu, program kończy w standardowy sposób pętlę i zwraca wartość -1.

Taki wzorzec obliczania i wykonywania operacji przechodzenia ciągu oraz zwracania wyniku w mo-
mencie znalezienia tego, co jest szukane, określany jest mianem wyszukiwania.

W ramach ćwiczenia zmodyfikuj funkcję f i nd tak, aby miała trzeci parametr, czyli indeks w łań­
cuchu parametru word identyfikujący znak, od którego powinno zostać rozpoczęte wyszukiwanie.

Wykonywanie pętli i liczenie


Następujący program liczy, ile razy litera a pojawia się w łańcuchu:
word :::: ananas•
1

count = O
f or lette r in wo r d :
i f lette r == 'a ' :
count = count + 1
print (count)

Program demonstruje kolejny wzorzec obliczania nazywany licznikiem. Zmienna count jest ini-
cjowana za pomocą wartości O, a następnie inkrementowana każdorazowo w momencie znalezienia
litery a. Po zakończeniu pętli zmienna count zawiera wynik, czyli łączną liczbę wystąpień litery a.
W ramach ćwiczenia dokonaj hermetyzacji powyższego kodu w funkcji o nazwie count, a ponadto
uogólnij ją tak, żeby jako argumenty akceptowała łańcuch i literę.
Zmodyfikuj następnie funkcję
w taki sposób, aby zamiast wykonywać operację przechodzenia
łańcucha, używała wersji funkcji f i nd z trzema parametrami z poprzedniego podrozdziału.

Metody łańcuchowe
Łańcuchy zapewniają metody realizujące różne przydatne operacje. Metoda przypomina funkcję.
Pobiera argumenty i zwraca wartość, ale jej składnia jest inna. Na przykład metoda upper pobiera
łańcuch i zwraca nowy łańcuch złożony z samych dużych liter.

Zamiast składni funkcji upper (word) używana jest składnia metody word. upper() :
>>> word = ananas
1 1

>>> new wo rd = word . upper()


>>> new wo r d
'ANANAS'

Metody łańcuchowe I 105


Taka forma zapisu z kropką określa nazwę metody upper oraz łańcuch (zmienna wo r d), dla które-
go metoda zostanie zastosowana. Puste nawiasy okrągłe wskazują, że metoda nie pobiera żadnych
argumentów.

Użycie metody określane jest mianem wywołania. W tym przypadku można stwierdzić, że dla
łańcucha word wywoływana jest metoda upper.

Jak się okazuje, istnieje metoda łańcuchowa o nazwie f i nd, która niezwykle przypomina utworzoną
funkcj ę:
1 1
>>> word = ananas
>>>index = word .find('a ' )
>>> index
o
W przykładzie dla zmiennej word wywołano metodę f i nd, której jako parametr przekazano szukaną
literę.

Właściwie metoda f i nd jest bardziej ogólna niż funkcja. Metoda może znajdować podłańcuchy, a nie
tylko znaki:
>>> wo rd .f ind('a5 ' )
4

Domyślnie metoda f i nd zaczyna od początku łańcucha, ale może pobrać drugi argument, czyli indeks
reprezentujący znak, od którego metoda powinna zacząć:
»> word . f i nd ( ' a5 ' , 3)
4

Jest to przykład argumentu opcjonalnego. Funkcja f i nd może też pobrać trzeci argument w postaci
indeksu znaku, na którym powinna zakończyć przetwarzanie:
1
>>> name = jan 1
»> name .f ind(' j ' , 1, 2 )
-1

To wyszukiwanie nie powiedzie się, ponieważ litera b nie pojawia się w zakresie indeksów od 1 do 2
(z wyłączeniem 2). Wyszukiwanie aż do drugiego indeksu, lecz z jego wyłączeniem, powoduje, że
funkcja fi nd jest spójna z operatorem [ n: m] .

Operator in
Słowo i nto operator boolowski, który pobiera dwa łańcuchy i zwraca wartość True, gdy pierwszy
łańcuch okazuje się podłańcuchem drugiego:
1
>>> a 1 in 1
ananas 1
Tr ue
>>> 1
ziarno 1 in 1
ananas 1
Fa l 5e

Na przykład następująca funkcja wyświetla wszystkie litery łańcucha wordl, które występuj ą też
w łańcuchu word2 :
def in_bot h( word l, wo rd2) :
fo r lett e r in wo rd l:

106 I Rozdział 8. lańcuchy


if letter in word2 :
print (l et ter)

W przypadku dobrze wybranych nazw zmiennych kod Python czasami wygląda jak tekst w języku an-
gielskim. Powyższy kod pętli można odczytać następująco: „for (each) letter in (the first) word, if
(the) letter (appears) in (the second) word, print (the) letter (dla każdej litery w pierwszym wyrazie,
jeśli litera pojawia się w drugim wyrazie, wyświetl literę)".

Porównanie pomidorów (paTiidory) i pomarańczy (paTiaraficze) powoduje uzyskanie następującego


wyniku:
>>> in_both{'pomidory', 'pomarańc ze ')
p
o
m
r

Porównanie łańcuchów
Operatory relacyjne mogą być użyte w przypadku łańcuchów. Aby sprawdzić, czy dwa łańcuchy
są równe:

if word == 'banan' :
print {'W porz ądku, banany . ' )

Inne operatory relacyjne przydają się, gdy słowa mają zostać ustawione w kolejności alfabetycznej:
if word < 'banan' :
print{'Użyte słowo ' + word + ' umies zczane jest przed słowem banan . ')
elif word > 'banan ':
print{'Użyte słowo ' + word + ' umies zczane jest po słowi e banan . ')
el s e:
print {'W porz ądku , banany . ' )

W języku Python duże i małe litery nie są interpretowane tak samo, jak zinterpretowaliby je ludzie.
Wszystkie duże litery trafiają przed małe litery, dlatego:
Użyte słowo Mango umieszczane jest przed słowem banan.

Typowym sposobem rozwiązania tego problemu jest dokonanie przed wykonaniem operacji porów-
nania konwersji łańcuchów na postać standardowego formatu, takiego jak tylko małe litery. Pamiętaj
o tym w sytuacji, gdy konieczna będzie obrona przed kimś uzbrojonym w Mango.

Debugowanie
Gdy stosujesz indeksy do wykonania operacji przechodzenia względem wartości ciągu, trudnym
zadaniem jest właściwe ustalenie początku i końca tej operacji. Poniżej zaprezentowano funkcję,
która ma porównać dwa słowa i zwrócić wartość True, jeśli jedno ze słów jest odwrotnością dru-
giego. Funkcja ta zawiera jednak dwa błędy.
def is_reverse{word l, word2):
if len(wordl) != len{word2):
return Fal se

Debugowanie I 107
i = o
j = l e n( word2 )
whil e j > O:
i f word I [ i ] ! = word2 [j ] :
r e t urn Fals.e
i = i +
j =j - I
return Tr ue

Pierwsza instrukcja i f sprawdza, czy słowa mają identyczną długość. Jeśli nie, od razu może zostać
zwrócona wartość Fal se. W przeciwnym razie dla reszty funkcji można przyjąć, że słowa mają taką
samą długość. Jest to przykład wzorca „strażnika" zaprezentowanego w podrozdziale „Sprawdzanie
typów" rozdziału 6.

i oraz j to indeksy: i dokonuje przej ścia słowa wo r d l od przodu, j natomiast powoduje przejście
słowa wo rd2 od tyłu. Jeśli zostaną znalezione dwie litery, których nie dopasowano, od razu może
zostać zwrócona wartość Fal se. W przypadku wykonania całej pętli i dopasowania wszystkich liter
zwracana jest wartość True.
Jeśli funkcj a ta zostanie przetestowana przy użyciu słów i kar i ra ki , oczekiwana jest wartość
zwracana True. Uzyskiwany jest jednak błąd Inde xEr r o r:
>>> is._ revers.e ( ' i kar ', ' ra ki' )

Fil e "r evers.e . py " , line 15 , i n is. reve rs.e


i f wo r d I [ i] ! = wo rd2 [ j ] :
I nde xError : s.tri ng index out of range

W przypadku debugowania tego rodzaju błędu moim pierwszym działa niem jest wyświetlenie
wartości indeksów bezpo średnio przed wierszem, w którym pojawia się błąd.

wh i le j > O:
print(i , j) # w tym mie; scu wyświetlane są wartości

i f wor d 1 [ i] ! = word2 [ j ] :
r e t urn Fals.e
i = i + 1
j = j - 1

W momencie ponownego uruchomienia programu uzyskujemy więcej informacj i:


>>> is_ reve rs.e ( ' i kar' , ' r a ki' )
o4
l ndex Erro r : s.tri ng index out of range

Przy pierwszym wykonywaniu pętli wartość indeksu j wynosi 4. Wartość ta jest poza zakresem łańcu­
cha i ka r. Indeks ostatniego znaku ma warto ść 3, dlatego wartość po czątkowa indeksu j powinna
wynosi ć l en (wo r d2 ) - 1.

Jeśli usunę ten błąd i ponownie uruchomię program, uzyskam:


>>> is_ reve rs.e ( ' i kar' , ' r a ki' )
o3
12
2 I
True

108 I Rozdział 8. lańcuchy


Tym razem otrzymujemy poprawną odpowiedź, ale wygląda na to, że pętla została wykonana tylko
trzy razy, co jest podejrzane. Aby lepiej zorientować się w tym, co ma miejsce, warto narysować
diagram stanu. Na rysunku 8.2 pokazano ramkę funkcji i s_reverse podczas pierwszej iteracji.

word1 ------ 'ikar' worrd2 ------ 'raki'

j ,.. 3

Rysunek 8.2. Diagram stanu

Pozwoliłem sobie na uporządkowanie zmiennych w ramce i dodanie linii kreskowych w celu po-
kazania, że wartości indeksów i i j wskazują znaki w słowach wordl i word2.
Zaczynając od tego diagramu, uruchom program w wersji papierowej, zmieniając podczas każdej
iteracji wartości indeksów i i j. Znajdź i usuń drugi błąd obecny w tej funkcji.

Słownik
obiekt
Element, do którego może odwoływać się zmienna. Na razie terminów obiekt i wartość mo-
żesz używać wymiennie.

ciqg
Uporządkowana kolekcja wartości, w której każda wartość jest identyfikowana przez indeks
w postaci liczby całkowitej.
element
Jedna z wartości ciągu.
indeks
Wartość całkowitoliczbowa służąca do wybrania elementu ciągu, takiego jak znak w łańcu­
chu. W języku Python indeksy rozpoczynają się od zera.

fragment
Część łańcucha określona przez zakres indeksów.
pusty łańc uch
Łańcuch bez żadnych znaków o długości zerowej, który reprezentowany jest przez dwa znaki
apostrofu.

niezmie nność

Właściwość ciągu, którego elementy nie mogą się zmieniać.

przechodz enie
Działanie maj ące na celu iterację elementów ciągu przez wykonanie dla każdego z nich po-
dobnej operacji.

Słownik I 109
wyszukiwanie
Wzorzec operacji przechodzenia, która jest przerywana w momencie znalezienia tego, co jest
szukane.
licz nik
Zmienna używana do liczenia czegoś. Jest ona zwykle inicjowana za pomocą zera, a następnie
inkrementowana.

wywołanie

Instrukcja wywołująca metodę.

argument opcjonalny
Argument funkcji lub metody, który nie jest wymagany.

Ćwiczenia
Ćwiczenie 8.1.

Przeczytaj dokumentację dotyczącą metod łańcuchowych dostępną pod adresem http://docs.python.


org/3/library/stdtypes.html#string-methods. Możesz poeksperymentować z niektórymi z nich, aby
upewnić się, że rozumiesz zasady ich działania. Szczególnie przydatne są metody stri p i rep lace.

W dokumentacji użyto składni, która może być niejasna. Na przykład w przypadku składni metody
f ind( s ub[, start [, end]]) nawiasy kwadratowe wskazują argumenty opcjonalne. A zatem argument
s ub jest wymagany, lecz argument start jest opcjonalny. Jeśli argument ten zostanie uwzględniony,
argument end jest opcjonalny.

Ćwiczenie 8.2.

Istnieje metoda łańcuchowa o nazwie count podobna do funkcji omówionej w podrozdziale „Wy-
konywanie pętli i liczenie" tego rozdziału. Przeczytaj dokumentację dla tej metody i utwórz wy-
wołanie, które określa liczbę wystąpień litery a w łańcuchu banan.

Ćwiczenie 8.3.

Operacja uzyskiwania fragmentu łańcucha może uwzględniać trzeci indeks określający „wielkość
kroku", czyli liczbę odstępów między kolejnymi znakami. „Wielkość kroku" o wartości 2 oznacza
co drugi znak, o wartości 3 wskazuje co t rzeci znak itd.
>>> fruit = 'ananas '
>>> fruit[ 0:5:2]
1 1
aaa

„Wielkość kroku" o wartości -1 powoduj e przetwarzanie słowa od tyłu, dlatego operator [ : : - 1]


generuje odwrócony łańcuch.
Użyj tego idiomu do utworzenia jednowierszowej wersji funkcji i s _pal i ndrome z ćwi czeni a 6.3.

110 I Rozdział 8. lańcuchy


Ćwiczenie 8.4.

We wszystkich zamieszczonych poniżej funkcjach zastosowano wci~cie, aby sprawdzić, czy łańcuch
zawiera jakiekolwiek małe litery. Przynajmniej niektóre z tych funkcj i są jednak niepoprawne. W przy-
padku każdej funkcji opisz jej rzeczywiste przeznaczenie (zakładając, że parametr jest łańcuchem).
def any_lowe rcas el (s) :
f orcins :
i f c . is l ower () :
ret urn Tr ue
el s e:
ret urn Fa l se

def any_ lowe r case2 (s) :


f orcins :
if 'c' . is l o we r () :
ret urn ' True'
e l se :
ret urn ' Fal s e '

def any_ lowe r case 3(s) :


f orcins :
f l ag = c . islowe r()
r et urn fl ag

de f any_ lowercase4 (s) :


f lag = Fa l s e
for c i n s :
fl ag = fl ag o r c .i s l ower()
return fl ag

de f a ny_ lowercase5 (s) :


for c i n s :
if not c .i s l owe r() :
retu rn Fa l s e
return True

Ćwiczenie 8.5.

Szyfr Cezara to odmiana słabego szyfrowania, które uwzględnia „obracanie" każdej litery o ustaloną
liczbę miejsc. „Obrócenie" litery oznacza przesunięcie jej w alfabecie, a w razie potrzeby umieszczenie
jej na jego początku. A zatem litera A przesunięta o 3 pozycje to litera D, a litera Z przesunięta o jedną
pozycję to litera A.

W celu „obrócenia" słowa każdą literę przesuń o taką samą liczbę pozycji. Na przykład słowo okrzyk
„obrócone" za pomocą liczby 7 to łańcuch vrygfr, a słowo melon „obrócone" przy użyciu liczby-10 to
łańcuch cubed. W filmie 2001: Odyseja kosmiczna komputer pokładowy statku o nazwie HAL to słowo
uzyskane w wyniku „obrócenia" słowa IBM za pomocą liczby-I.

Utwórz funkcję o nazwie rotate_word, która jako parametry pobiera łańcuch i liczbę całkowitą, po
czym zwraca nowy łańcuch zawierający litery z oryginalnego łańcucha „obróconego" p rzy użyciu
podanej liczby.

Ćwlaenla I 111
Możesz skorzystać z funkcji wbudowanej ord, która przekształca znak w kod liczbowy, a także z funkcji
chr przekształcającej kody liczbowe w znaki. Litery alfabetu są kodowane w kolejności alfabetycznej,
dlatego na przykład:
»> ord ( 'c ' ) - ord ('a' )
2

Taki wynik bierze się stąd, że w języku angielskim c to litera alfabetu z indeksem 2. Bądź jednak
świadom tego, że w przypadku dużych liter kody liczbowe są inne.

Potencjalnie obraźliwe żarty publikowane w internecie są czasami kodowane za pomocą szyfru


ROT13, który jest szyfrem Cezara dokonującym „obrócenia" za pomocą liczby 13. Jeśli jesteś od-
porny na żarty, znajdź i zdekoduj niektóre z nich.
Rozwiązanie: plik rotate.py.

112 I Rozdział 8. lańcuchy


ROZDZIAŁ 9.

Analiza przypadku: gra słów

W tym rozdziale przedstawiłem drugą analizę przypadku, która obejmuje rozwiązywanie gier słow­
nych przez wyszukiwanie słów o określonych właściwościach. Znajdziemy na przykład najdłuższe pa-
lindromy występujące w języku angielskim i poszukamy słów, których litery ustawione są w kolejności
alfabetycznej. Zaprezentuję też kolejny plan projektowania programu, czyli uproszczenie na bazie
wcześniej rozwiązanego problemu.

Odczytywanie list słów


Do wykonania ćwiczeń zamieszczonych w tym rozdziale niezbędna jest lista słów języka angielskiego.
Choć w internecie dostępnych jest mnóstwo list słów, do naszych celów najbardziej odpowiednia
jest jedna z list słów zgromadzonych i publicznie udostępnionych przez Grady'ego Warda jako
część projektu leksykalnego Moby (http://wikipedia.org/ wiki/Moby_Project). Jest to licząca 113 809
pozycji lista oficjalnych słów krzyżówkowych, czyli słów uważanych za poprawne w krzyżówkach
oraz innych grach słownych. Plik kolekcji projektu Moby ma nazwę 113809offic. Pod adresem
Jtp:/lftp.helion.pl/przyklady/ myjep2.z ip znajduje się uproszczona wersja tego pliku o nazwie words. txt
dostępna do pobrania jako kopia.

Plik ma postać zwykłego pliku tekstowego, dlatego możesz go otworzyć za pomocą edytora tekstu.
Może też zostać odczytany z poziomu interpretera języka Python. Funkcja wbudowana open pobiera
nazwę pliku jako parametr i zwraca obiekt pliku, którego możesz użyć do wczytania pliku.
>>> f in = open ( 'word5 . t xt')

f i n to typowa nazwa obiektu pliku używanego na potrzeby danych wejściowych. Obiekt pliku zapew-
nia kilka metod służących do odczytywania, w tym metodę readl i ne, która odczytuje znaki z pliku
do momentu napotkania znaku nowego wiersza, a następnie zwraca wynik w postaci łańcucha:
>>> fi n. read li ne()
'aa \ r \ n'

Pierwsze słowo na tej konkretnej liście to aa, czyli rodzaj lawy. Ciąg \ r\ n reprezentuje dwa znaki
białej
spacji, powrót karetki i znak nowego wiersza, które oddzielają dane słowo od następnego.
Obiekt pliku śledzi położenie słowa w pliku, dlatego w przypadku ponownego wywołania metody
readl i ne uzyskasz następne słowo:
»> f in . read l i ne()
' aa h\ r \ n'

113
Kolejne słowo to aah, które jest zupełnie poprawne, dlatego nie powinno Cię zdziwić. Jeśli powodem
do zmartwienia jest biała spacja, można ją wyeli minować za po mocą metody łańcuchowej str i p:
>>>line = f in . readl ine ()
>>> word = 1 ine . strip()
>>> word
'aahed'

Możliwe jest też zastosowanie obiektu pliku jako części pętli fo r. Następuj ący program wczytuje
plik words.txt i wyświetla każde słowo w osobnym wierszu:
f i n = open (' words . t xt')
for 1 ine in fi n:
word = l i ne .str i p{)
pri nt (word)

Ćwiczenia
Rozwiązani a po ni ższych ćwiczeń znajdziesz w następ nym podrozdziale. Przed zaznajomieniem
się z rozwią zaniami podejmij jednak p rzynajmniej próbę wykonania tych zadań.

Ćwiczenie 9.1.

Utwórz program odczytuj ący plik words.txt i wyświetlaj ący wyłączni e słowa zawierające ponad 20
znaków (nie licząc białych spacji).

Ćwiczenie 9.2.

W 1939 r. Ernest Vincent Wrigh t opublikował liczącą 50 OOO słów powieść Gadsby, która nie za-
wiera żadnej litery e. Ponieważ w j ęzyku angielskim litera ta jest najczęściej występującą spośró d
wszystkich liter, nie było to łatwe zadanie.
Bardzo trudno sfo rmułować odosobni o ną myśl, pom ijaj ąc tę samogłoskę. Na początku powoli,
po godzinach prób i przy zachowaniu uwagi stopniowo m ożna nabrać wprawy.

No dobrze, na tym poprzestanę.

Utwórz funkcję o nazwie ha s_ no_e, która zwraca wartość Tr ue, jeśli dane słowo nie zawiera litery e.

Zmodyfikuj program z poprzedniego podrozdziału, aby wyświetlał tylko słowa bez litery e, a ponadto
obliczał wartość procentowąliczby słów na li ście, które są pozbawione litery e.

Ćwiczenie 9.3.

Utwórz funkcję o nazwie avoi ds , która pobiera słowo i łańcuch niedozwolonych liter, a po nadto
zwraca wartość True, j eśli w słowie nie użyto żad nej zabronionej litery.

Zmodyfikuj program tak, aby żądał od użytkownika wprowadzenia łańcucha niedozwolonych liter,
a następ nie wyświetlał liczbę słów, które nie zawierają żadnej z tych liter. Czy możesz znaleźć kombi-
nację pięciu zabro nionych liter, które wykluczają najmniejszą liczbę słów?

114 I Rozdział 9. Analiza przypadku: gra słów


Ćwiczenie 9.4.

Utwórz funkcję o nazwie uses _only, która pobiera słowo i łańcuch liter, a ponadto zwraca wartość
True, jeśli słowo zawiera wyłącznie litery podane na liści e. Czy możesz utworzyć zdanie tylko przy
użyciu literłańcucha acef hl o (inne niż Hoe alfalfa)?

Ćwiczenie 9.5.

Utwórz funkcj ę o nazwie uses_a 11 , która pobiera słowo i łańcuch wymaganych liter, a ponadto zwraca
wartość True, jeśli w słowie co najmniej raz zastosowano wszystkie wymagane litery. Ile występuje
słów zawierających wszystkie samogłoski tworzące łańcuch aei ou? A jak to wygląda w przypadku
liter łańcucha ae i ouy?

Ćwiczenie 9.6.

Utwórz funkcję o nazwie is_abecedari an, która zwraca wartość True, jeśli litery w słowie występują
w kolejności alfabetycznej (podwojone litery są dozwolone). Ile występuje tego rodzaju słów?

Wyszukiwanie
Wszystkie ćwiczeni a z poprzedniego podrozdziału mają coś wspólnego. W ich przypadku rozwią­
zanie może zostać uzyskane za pomocą wzorca wyszukiwania zaprezentowanego w podrozdziale
„Wyszukiwanie" rozdzi ału 8. Oto najprostszy przykład:
def has_no_e (word) :
f or l ett e r in word :
if let ter == ' e':
retu rn Fal se
return True

Pętla for wykonuje operację przechodzenia dla znaków słowa word. Jeśli zostanie znaleziona litera e,
natychmiast zwracana jest wartość Fal se. W przeciwnym razie konieczne jest przejście do następnej
litery. Jeżeli pętla zostanie zakończona w standardowy sposób, oznacza to, że nie znaleziono litery e.
Z tego powodu zwracana jest wartość True.

Choć operat or i n pozwala na bardziej zwięzłe zdefiniowanie tej funkcji, zacząłem od powyższej
wersji, ponieważ demonstruje ona logikę wzorca wyszukiwania.

Funkcja avoi ds to bardziej ogólna wersja funkcji has_no_e, ale ma identyczną strukturę:
def avoids(word , forbidden) :
f or letter in word :
if l etter in forbidden :
ret urn Fal se
ret urn True

Wartość Fal se może zostaćzwrócona od razu po znalezieniu niedozwolonej litery. W przypadku


osiągnięcia końca pętli zwracana jest wartość True.
Funkcja us es_on l y jest podobna, z tą różnicą, że znaczenie warunku jest odwrócone:
def uses_only (word , avai l abl e) :
f or letter in word :

Wyszukiwanie I 115
if letter not in available:
return Fals.e
return True

Zamiast listy zakazanych liter istnieje lista dostępnych liter. Jeśli w łańcuchu word zostanie znale-
ziona litera, której nie ma w łańcuchu a va i l abl e, zostanie zwrócona wartość Fal se.

Funkcja us es _a 11 różni się jedynie tym, że zamieniono rolę słowa i łańcucha liter:
def uses_al l (word , requi red) :
for letter in r equired:
i f letter not in word:
return False
return Tr ue

Zamiast wykonywać operację przechodzenia przez litery słowa wo r d, pętla wykonuje ją dla wyma-
ganych liter. Jeśli w słowie word nie wystąpi dowolna z wymaganych liter, zostanie zwrócona
wartość Fal se.

Jeżeli naprawdę myślałbyś jak informatyk, zauważyłbyś, że funkcja uses a l l jest przypadkiem
wcześniej rozwiązanego problemu, i utworzyłbyś następujący kod:
def uses_al l (word , requi red):
return uses_onl y(required , word)

Jest to przykład
planu projektowania programu nazywanego uproszczeniem na bazie wcześniej
rozwiązanego problemu. Oznacza to, że rozpoznałeś bieżący problem jako przypadek problemu
już rozwiązanego i zastosujesz istniejące rozwiązanie.

Wykonywanie pętli zwykorzystaniem indeksów


W poprzednim podrozdziale utworzyłem funkcje z pętlami for, ponieważ potrzebowałem łańcu­
chów złożonych wyłącznie ze znaków. Nie były konieczne żadne działania związane z indeksami.
W przypadku funkcji is_ abecedari an niezbędne jest porównanie sąsiednich liter, co przy pętli for
jest trochę utrudnione:
def is_abecedarian(word) :
previous = word[O]
for c in word :
if c < previ ous. :
return Fals.e
previous = c
return True

Alternatywą jest użycie rekurencji:


def is_abecedarian(word) :
i f len (word ) <= 1:
return True
i f word [OJ > word [ 1] :
ret urn Fal se
return is_abecedarian(word[ l: ])

116 I Rozdział 9. Analiza przypadku: gra słów


Inną opcją jest zastosowanie pętli whi l e:
def is_abecedarian(word) :
i = o
wh ile i < len(word) - 1:
i f word [i + 1] < word [ i] :
r e t u rn Fa l se
i = i + 1
r e t u rn True

Pętla rozpoczyna się od i = O i kończy, gdy i = l en(word} - 1. Przy każdym wykonaniu pętla porów-
nuje znak o indeksie i (możesz traktować go jako bieżący znak) ze znakiem o indeksie i + 1 (możesz
uznać go za następny znak).

Jeśli następny znak jest „mniejszy" niż bieżący znak (występuje wcześniej w alfabecie), wykryto
przerwę w ciągu alfabetycznym, dlatego zwracana jest wartość Fa l se.

Jeżeli zostanie osiągnięty koniec pętli bez znalezienia przerwy, słowo pomyślnie przechodzi test.
Aby przekonać się, że pętla jest poprawnie kończona, rozważ przykład taki jak ze słowem f lossy.
Długość tego słowa wynosi 6, dlatego przy ostatnim uruchomieniu pętli i ma wartość 4, co od-
powiada indeksowi przedostatniego znaku. W ramach ostatniej iteracji pętla porównuje przed-
ostatni znak z ostatnim, czyli zachowuje się zgodnie z oczekiwaniami.
Poniżej zaprezentowano wersj ę funkcji is _pa l i ndrome {zajrzyj do ćwiczenia 6.3), która używa
dwóch indeksów: pierwszy indeks rozpoczyna się na początku i jego wartość zwiększa się, drugi
zaczyna się od końca, a jego wartość się zmniejsza.
def is_ pa l indrome (word) :
i = o
j = len(word) - 1

wh i le i < j :
i f word [i] ! = word [ j] :
r e t urn Fa l 5e
i = i + 1
j -

r eturn True

Możliwe jest też uproszczenie na bazie wcześniej rozwiązanego problemu i utworzenie następującego
kodu:
def i5_ pa l ind r ome (word) :
ret u rn i5_ rever5e ( wo r d , word)

W kodzie wykorzystano funkcję is_reverse z rysunku 8.2.

Debugowanie
Testowanie programów jest trudne. Funkcje zamieszczone w tym rozdziale są stosunkowo łatwe do te-
stowania, ponieważ wyniki mogą być sprawdzane ręcznie. Nawet w tym przypadku wybranie zestawu
słów sprawdzających wszystkie możliwe błędy sytuuje się gdzieś między trudnym a niemożliwym.

Debugowanie I 117
Gdy pod uwagę weźmie się na przykład funkcję has_ no_e, do sprawdzenia są dwa oczywiste przypadki:
słowa zawierające literę e powinny spowodować zwrócenie wartości Fal se, a dla słów bez tej litery
powinna zostać zwrócona wartość True. Nie powinno być problemu z określeniem słów dla każdego
z tych wariantów.

W ramach każdego przypadku występują pewne mniej oczywiste przypadki podrzędne. Wśród
słów z literą e należy testować słowa zawierające tę literę na początku, na końcu i gdzieś w środku.
Powinno się sprawdzić długie słowa, krótkie słowa i bardzo krótkie słowa, takie jak pusty łańcuch.
Pusty łańcuch to przykład specjalnego przypadku, czyli jednego z nieoczywistych przypadków, w któ-
rych często kryją się błędy.

Oprócz sprawdzania wygenerowanych przypadków testowych możesz też testować program za po-
mocą listy słów, takiej jak plik words.txt. Skanując dane wyjściowe, możesz być w stanie wychwy-
cić błędy, ale bądź ostrożny: możesz wykryć jeden rodzaj błędu (słowa, które nie powinny zostać
uwzględnione, lecz są), a nie inny (słowa, które powinny zostać uwzględnione, ale nie są).

Ogólnie rzecz biorąc, testowanie może ułatwić znajdowanie błędów, ale wygenerowanie dobrego
zestawu przypadków testowych nie jest łatwe. Jeśli nawet się to uda, nie możesz mieć pewności, że
program jest poprawny. Zgodnie z wypowiedzią legendarnego informatyka:
Testowanie programów może posłużyć do potwierdz enia obecności błfdów, lecz nie umożliwia
potwierdz enia ich nieobecności!
-Edsger W. Dijkstra

Słownik
obiekt pliku
Wa rtość reprezentująca otwarty plik.

uprosz czenie na baz ie wcześniej rozwiązanego problemu

Sposób rozwiązywania problemu przez przedstawienie go jako przypadku wcześniej rozwią­


zanego problemu.

specjalny przypadek
Przypadek testowy, który jest nietypowy lub nieoczywisty, a ponadto z mniejszym prawdopodo-
bieństwem zostanie poprawnie obsłużony.

Ćwiczenia
Ćwiczenie 9.7.

To ćwiczenie jest oparte na następującej zagadce z audycji Puzzler nadawanej w radiu Car Talk
(http ://www. car talk. com/con ten tip uzzlers):
Podajcie mi słowo z trzema kolejnymi dwukrotnymi powtórz eniami jednej litery. Podam wam
kilka słów, które prawie spełniają to wymaganie, ale nie do końca. Na przykład słowo committee:

118 I Rozdział 9. Analiza przypadku: gra słów


c-o-m-m-i-t-t-e-e. Byłoby ono znakomite, gdyby nie to, że „ wkradła się" do niego litera i. Albo
słowo Mississippi: M-i-s-s-i-s-s-i-p-p-i. To słowo mogłoby być, jeśli usunięto by z niego literę i.
Istnieje jednak słowo z trz ema następujqcymi kolejno parami liter. O ile mi wiadomo, może to
być jedyne takie słowo. Oczywiście istnieje prawdopodobnie 500 kolejnych słów, ale mam na
myśli tylko jedno. Jakie to słowo?

Utwórz program, który znajdzie takie słowo.


Rozwiązanie: plik cartalkl.py.

Ćwiczenie 9.8.

Oto kolejna zagadka z audycji Puzzler radia Car Talk (http://www.cartalk.com/content/puzzlers):


Pewnego dnia jechałem autostradq i spojrzałem na licz nik mil. Jak większość takich licz ników,
poka zywał on sześć
cyfr informujqcych tylko o pełnych milach. A z atem gdyby na przykład mój
samochód przejechał 300 OOO mil, ujrzałbym 3-0-0-0-0-0.
To, co ujrzałemtamtego dnia, było bardzo interesujqce. Zauważyłem, że cztery ostatnie cyfry
tworzyły palindrom, czyli czytane od poczqtku lub od końca stanowiły identycz ny ciqg. Na
przykład ciqg 5-4-4-5 to palindrom, a na moim licz niku mil widniało 3-1-5-4-4-5.

Po przejechaniu kolejnej mili pięć ostatnich cyfr utworzyło palindrom. Mógłby to być na przy-
kład ciqg 3-6-5-4-5-6. Po pokonaniu następnej mili palindrom powstał z cz terech środkowych
cyfr. Czy jesteś na to gotowy? Jednq milę później wszystkie 6 cyfr utworzyło palindrom!
Pytanie brzmi: co było widoczne na licz niku mil, gdy spojrzałem na niego po raz pierwszy?
Utwórz program Python, który testuje wszystkie liczby sześciocyfrowe i wyświetla wszelkie liczby
spełniające powyższe wymogi.

Rozwiązanie: plik cartalk2.py.

Ćwiczenie 9.9.

Oto kolejna zagadka z audycji Puzzler radia Car Talk (http://www.cartalk.com/content/puzzlers),


którą możesz rozwiązać z wykorzystaniem wyszukiwania:
Ostatnio odwiedziłem mamę i zdałem sobie sprawę z tego, że dwie cyfry tworzqce mój wiek po
odwróceniu stanowiq jej wiek. Jeśli na przykład ma ona 73 lata, mój wiek to 37 lat. Zastana-
wialiśmy się, jak często miało to miejsce w ciqgu poprzednich lat, ale ostatecz nie poruszyliśmy
inne tematy i nigdy nie uzyskaliśmy odpowiedz i na to pytanie.
Po powrocie do domu stwierdziłem, że do tej pory cyfry w moim wieku sześć razy były odwrot-
nościq wieku mojej mamy. Doszedłem również do wniosku, że jeśli będzie nam to dane, taka
sytuacja wystqpi ponownie za kilka lat. Jeżeli będziemy mieć naprawdę dużo szczęścia, dojdz ie
do tego jeszcze raz. Inacz ej mówiqc, w sumie taka sytuacja z cyframi wieku wystqpiłaby osiem
razy. A zatem pytanie brz mi: ile mam lat?
Utwórz program Pytho n wyszukujący rozwiązania powyższej zagadki. Wskazówka: możesz uznać
za przydatną metodę zf i 11.
Rozwiązanie: plik cartalk3.py.

Ćwlaenla I 119
120 I Rozdział 9. Analiza przypadku: gra słów
ROZDZIAŁ 1O.

Listy

W tym rozdziale zap rezentowałem jeden z najbardziej przydat nych typów wbudowanych języka
Python, czyli listy. Dowiesz się również więcej o obiektach, a także o tym, co się może stać, gdy dla tego
samego obiektu użyje się więcej ni ż jednej nazwy.

Lista to ciąg
Podobnie do łańcucha, lista to ciąg wartości . W łańcuchu wartości są znakami, a w przypadku listy
mogą być dowolnego typu. Wartości listy są nazywane elementami lub niekiedy pozycjami.

Istnieje kilka sposobów utworzenia nowej listy. Najprostszym jest umieszczenie elementów w nawia-
sach kwadratowych ([ i ]):
[10 , 20 , 30 , 40]
[ ' z i e lona ż aba' , 'biał y bocian' , 'czerwony ra k ' ]

Pierwszy przykład to lista czterech liczb całkowitych . Drugi przykład prezentuje listę trzech łań­
cuchów. Elementy listy nie m uszą być tego samego typu. Następująca lista zawiera łańcuch, liczbę
zmiennoprzeci nkową, liczbę całkowitą i (a j akże!) kolej ną listę:

[ ' spam ' , 2. O, 5, [ 10 , 20] ]

Lista wewnąt rz innej listy to lista zagnieżdżona.

Lista bez żad nych elementów nazywana jest pustą li stą. Możliwe jest utworzenie listy z pustymi
nawiasam i kwadratowymi [].

Jak możesz oczekiwać, wa rtości listy mogą zostać przypisane zmiennym:


> >> cheeses = [' Cheddar ', ' Edam' , 'Gouda' ]
» > num bers = [ 42, 12 3]
» > empty = []
> >> print(cheeses , numbers , empty )
[ 'Cheddar ', ' Edam ', ' Gouda '] [42, 12 3] []

121
Listy są zmienne
Składnia związana z uzyskiwaniem dostępu do elementów listy, czyli operator w postaci nawiasów
kwadratowych, jest taka sama jak w przypadku korzystania ze znaków łańcucha. Wyrażenie wewnątrz
tych nawiasów określa indeks. Pamiętaj, że indeksy rozpoczynają się od zera:
>» c hees e s [O]
'C heddar'

W przeciwieństwie do łańcuchów, listy są zmienne. Gdy nawias kwadratowy pojawi się po lewej stro-
nie przypisania, identyfikuje element listy, który zostanie przypisany:
»> numbers = [42, 12 3]
»> numbers [ l] = 5
>>> numbers
[42 . 5]

Element listy number s o indeksie 1, który miał wartość 123, obecnie ma wartość 5.
Na rysunku 10.l pokazano diagram stanu list chees es , numbers i empty.

lista
cheeses ~ O- 'Cheddar'
1 ----;.. 'Edam'
2 ~ 'Gouda '

lista
numbers - 0 ~ 42

1 .. .
~
:m::
-

lista
empty~ D

Rysunek 10.1. Diagram stan u

Listy są reprezentowane przez pola z umieszczonym obok nich słowem lista oraz elementy listy
znajdujące się w polach. Zmienna cheeses odwołuje się do listy z trzema elementami o indeksach
O, 1 i 2. Zmienna numbers reprezentuje listę zawierającą dwa elementy. Na diagramie widać, że war-
tość 5 drugiego elementu zastąpiła wartość 123 w wyniku ponownego przypisania. Zmienna
empty odwołuje się do listy bez żadnych elementów.

Indeksy list działają tak samo jak indeksy łańcuchów:


• Jako indeks może zostać użyte dowolne wyrażenie z liczbami całkowitymi.
• Jeśli spróbujesz odczytać lub zapisać element, który nie istnieje, zostanie wygenerowany błąd
I ndex Error.

• Jeżeli indeks ma wartość ujemną, powoduje liczenie wstecz od końca listy.

122 I Rozdział 10. Listy


Operator i n również może być stosowany w przypadku list:
>>> cheeses = [ 'Cheddar' , ' Edam' , 'Gouda' ]
>>> ' Edam' in c hees es
True
>>> ' Bri e' in c hees es
Fal se

Operacja przechodzenia listy


Najpowszechniejszym sposobem wykonywania operacji przechodzenia listy jest zastosowanie pętli
for. Składnia jest identyczna ze składnią dla łańcuchów:
f or c heese in cheeses :
print(c heese)

Sprawdza sięto dobrze, gdy konieczne jest tylko odczytanie elementów listy. Jeśli jednak elementy
mają zostać zapisane lub zaktualizowane, niezbędne będą indeksy. Typowym sposobem zrealizowa-
nia tej operacji jest połączenie funkcji wbudowanych r ange i len:
for i in range(l e n(num bers)) :
numbers[i] = numbers [ i] * 2

Pętla ta dokonuje przejścia listy i aktualizuje każdy element. Funkcja l en zwraca liczbę elementów
listy. Funkcja range zwraca listę indeksów od O do n-1, gdzie n to długość listy. Przy każdym wy-
konaniu pętli zmienna i uzyskuje indeks następnego elementu. Instrukcja przypisania w treści pętli
używa tej zmiennej do odczytu starej wartości elementu i przypisania nowej wartości .

W przypadku pustej listy nigdy nie jest uruchamiany kod pętli for:
for x i n []:
print('To się nigdy nie zdarzy . ')

Lista może zawierać inną listę, jednak lista zagnieżdżona nadal liczona jest jako pojedynczy element.
Długość następującej listy wynosi 4:
[ 'spam' , 1, [ ' Brie', 'Roquefort' , 'Pol le Veq'], ( 1, 2, 3]]

Operacje na listach
Operator + wykonuje konkatenację list:
»> a = [ 1, 2, 3]
»> b = [ 4' 5 ' 6]
>>> c = a + b
>>> c
(1, 2, 3, 4, 5, 6]

Operator* powtarza listę podaną liczbę razy:


»> [O] * 4
[O, O, O, O]
>>> [l, 2 , 3] * 3
[ 1, 2 ' 3' 1, 2 ' 3' 1, 2 ' 3]

W pierwszym przykładzie listę [O] powtórzono cztery razy. W drugim przykładzie lista [1 , 2, 3]
powtarzana jest trzykrotnie.

Operacje na listach I 123


Fragmenty listy
Operator wyodrębniania fragmentu można zastosować w przypadku list:
>>> t = [ ł a b cł Id I e I ' I f IJ
I ' I I ' I ' I '

»> t [ 1: 3]
[ ' b ', 'c' J
»> t [: 4]

»> t [ 3:]

W razie pominięcia pierwszego indeksu operator ten rozpoczyna przetwarzanie od początku listy.
Jeśli pominięto drugi indeks, operator kontynuuje przetwarzanie do końca listy. A zatem gdy po mi-
niesz oba indeksy, wynik przetwarzania przez operator jest kopią całej listy:
»> t [: ]
[ • at , 1b1 , 'c• , 1d1 , 1e1 , ' f •]

Ponieważ listy są zmienne, często przydatne jest sporządzenie kopii przed wyko naniem operacji,
które modyfikują listy.
Operator wyodrębniania fragmentu po lewej stronie przypisania może zaktualizować wiele ele-
mentów:
>>> t = [ ł a I ' I bI ' I cł ' ł dI ' Ie I ' If ł]

»> t [ 1: 3] = ( ' X' , ' y ' ]


>>> t

Metody list
Język Python zapewnia metody przetwarzające listy. Na przykład metoda append dodaje nowy
element do końca listy:
>>> t = [ I aI t I bI t I ( ł J
>>> t . append('d ' )
>>> t

Metoda ex tend pobiera listę jako argument i dołącza wszystkie jej elementy:
>>> t1 = [ ' a' ' I bi t ' c ' ]
>>> t 2 = [ ' d' . ' e'J
>>> t 1. ext end ( t2)
>>> t1
[ ' a' ' l b' ' I (I t •d ł ' 'e'J

W tym przykładzie lista t 2 pozostaje niezmieniona.


Metoda sort rozmieszcza elementy listy w kolejności rosnącej:

>>> t = [ ł d I ' I cI ' Ie I ' ł bI ' I a I J


»> t. 50rt ()
>>> t

124 I Rozdział 10. Listy


Większość metod list to metody „puste". Modyfikują one listę i zwracają wartość None. Jeśli przy-
padkiem utworzysz kod t = t . sor t () ,będziesz rozczarowany wynikiem.

Odwzorowywanie, filtrowanie i redukowanie


Aby zsumować wszystkie liczby na liście, możesz zastosować następującą pętlę:

de f add_a l l (t) :
total = O
for x i n t:
total += x
r et urn tot a l

Zmienna total jest inicjowana za pomocą wartości O. Przy każdym wykonaniu pętli zmiennej x
przekazywany jest jeden element listy. Operator += zapewnia szybki sposób aktualizowania zmiennej.
Instrukcja przypisania rozszerzonego
total += x
jest równoważna następującej instrukcji:
total = tota l + x

W trakcie działania pętli zmienna tot a l akumuluje sumę elementów. Zmienna używana w ten
sposób jest czasami nazywana akumulatorem.

Sumowanie elementów listy to na tyle powszechna operacja, że język Python zapewnia funkcję
wbudowaną sum:

>» t = [ 1, 2. 3]
»> 5Um(t)
6

Tego rodzaju operacja, która łączy ciąg elementów do postaci pojedynczej wartości, określana jest
niekiedy mianem operacji redukowania.

Od czasu do czasu wymagane jest wykonanie operacji przechodzenia dla jednej listy podczas budowa-
nia innej. Na przykład następująca funkcja pobiera listę łańcuchów i zwraca nową listę, która zawiera
łańcuchy złożone z dużych liter:

def capital i ze_a l l (t) :


re5 = []
for 5 in t :
re5 . append(5 . capital i ze())
ret urn re5

Metoda res jest inicjowana za pomocą pustej listy. Przy każdym wykonaniu pętli do łączany jest
następny element. A zatem metoda ta to kolejna odmiana akumulatora.

Operacja taka jak wykonywana przez funkcj ę capi t al i ze_ a 11 jest czasami określana mianem od-
wzorowywania, ponieważ „odwzorowuje" funkcję (w tym przypadku metodę capi t a l i ze ) na każdy
z elementów ciągu.

Odwzorowywanie, flltrowanle I redukowanie I 125


Inną typową operacją jest wybieranie niektórych elementów listy i zwracanie listy podrzędnej. Na
przykład następująca funkcja pobiera listę łańcuchów i zwraca listę, która zawiera tylko łańcuchy
złożone z dużych liter:

def onl y_uppe r( t ) :


res = [ ]
for s i n t :
if s . i s upper() :
res . appe nd(s)
return res

i supper to metoda łańcuchowa, która zwraca wartość True, jeśli łańcuch zawiera wyłącznie duże
litery.
Operacja taka jak realizowana przez funkcję onl y_upper nazywana jest filtrowaniem, ponieważ
wybiera niektóre elementy i odfilt rowuje pozostałe.

Większość typowych operacji na listach można wyrazić jako kombinację operacji odwzorowywania,
filtrowania i redukowania.

Usuwanie elementów
Istnieje kilka sposobów usuwania elementów z listy. Jeśli znasz indeks żądanego elementu, mo-
żesz skorzystać z metody pop:
>>> t [ła b łJI t I I t I (

>>> X = t . pop ( l )
>>> t
[ ' a ' , 'c' ]
>>> X
'b'

Metoda ta modyfikuje listę i zwraca usunięty element. Jeśli nie podasz indeksu, metoda usuwa i zwra-
ca ostatni element.
Jeżeli nie potrzebujesz usuniętej wartości, możesz użyć operatora de l:
>>> t = [ła I t I b I t I ( ł]

»> del t [1]


>>> t

Jeśli wiesz, jaki element chcesz usunąć (bez znajomości indeksu), możesz zastosować metodę remove:
>>> t = [I a I b t I I t I( ł J
>>> t . remove( ' b' )
>>> t

W przypadku tej metody wartość zwracana to None.

Aby usunąć więcej niż jeden element, możesz użyć metody del z indeksem fragmentu:
>>> t = [ I a I ' I b. ' I cI ' ł dI ' Ie I ' I f ł]

»> del t [ 1: 5]
>>> t
[ 'a' , 'f ']

126 I Rozdział 10. Listy


Jak zwykle, w tym przypadku operato r wyodrębniania fragmentu wybiera wszystkie elementy aż
do podanego indeksu 5, lecz z wyłączeniem drugiego indeksu 1.

Listy i łańcuchy
Łańcuchto ciąg znaków, a lista to ciąg wartości. Lista znaków nie jest jednak tym samym co łańcuch.
Aby dokonać konwersji łańcucha na listę znaków, możesz skorzystać z funkcji l i st:
>>> s •spam 1
>>> t = l i st (s)
>>> t
[ ' s'. I pl t •a ł , ' m'J

Ponieważ l i st to nazwa funkcji wbudowanej, należy unikać jej jako nazwy zmiennej. Unikam również
litery l , gdyż zbyt przypomina liczbę 1. Z tego właśnie powodu używam litery t.
Funkcja li st dzieli łańcuch na osobne litery. Aby doko nać podziału łańcucha na słowa, możesz
zastosować metodę s pl i t:

>>> s ' wyszukiwa nie pięknych fiordów'


>>> t = s . split ()
>>> t
[ ' wys zukiwani e', 'pięknych' , 'fiordów' ]

Argument opcjonalny nazywany separatorem określa, jakie znaki zostaną użyte w roli ograniczników
słów. W następującym przykładzi e jako separatora użyto łącznika:
>>> s = 'spam - spam-spam'
>>> del imit er = ' -'
»> t = s . split(delimiter)
>>> t
[' spam ', 'spam', 'spam']

Metoda joi n stanowi odwrotność metody split. Pobiera ona listę łańcuchów i dokonuje konkatenacji
elementów. j o i n to metoda łańcuchowa, dlatego musi zostać wywołana w obiekcie separatora z listą
przekazaną jako parametr:

>» t = [' wys zuk iwanie' , ' pięknych', ' f iordów' ]


>>> del imiter = ' '
»> s = de l imiter . join(t)
>>> s.
'wys z u ki wani e pię k nych fi ordów'

W tym przypadku separatorem jest znak spacji, dlatego metoda joi n wstawia ją między słowami.
Aby dokonać konkatenacji ła ńcuchów bez spacji, w roli separato ra możesz wykorzystać pusty
łańcuch' ' .

Obiekty i wartości
Utwórz następujące instrukcje przypisania:
a = 'banan '
b = 'banan '

Obiekty I warto~cl I 127


W tym przypadku wiadomo, że zmienne a i b odwołują się do łańcucha, ale nie wiemy, czy odwołują się
do tego samego łańcucha. Na rysunku 10.2 pokazano dwa możliwe stany.

a~ 'banan·
a-....._ 'banan'
b ------- 'banan' b ____.,.

Rysunek 10.2. Diagram stanu

W pierwszym przypadku zmienne a i b odwołują się do dwóch różnych obiektów z taką samą wartością.
W drugim przypadku o dwołują się one do tego samego obiektu.
Aby sprawdzić, czy dwie zmienne odwołują się do tego samego obiektu, możesz użyć operatora i 5:
>>> a = 1 banan 1

>>> b = 'banan'
>>> a is b
Tr ue

W tym przykładzie kod Python utworzył tylko jeden obiekt łańcucha, a zmienne a i b odwołują
się do niego. Gdy jednak utworzysz dwie listy, uzyskasz dwa obiekty:
>>> a = [l, 2, 3]
>>> b = [l, 2, 3]
>>> a is b
Fal se

A zatem diagram stanu wygląda takjak na rysunku 10.3.

a - [ 1,2,3)
b ----- [ 1. 2, 3 ]

Rysunek 10.3. Diagram stanu

W tym przypadku można stwierdzić, że dwie listy są równoważne, ponieważ mają takie same ele-
menty, ale nie są identyczne, gdyż nie są tym samym obiektem. Jeśli dwa obiekty są jednakowe, są też
równoważne. Jeżeli jednak są równoważne, niekoniecznie są jednakowe.

Do tej pory terminy „obiekt" i „wartość" były używane wymiennie. Bardziej precyzyjne jednak jest
stwierdzenie, że obiekt ma wartość. Jeśli przetwarzana jest lista [l , 2, 3], uzyskasz obiekt listy, które-
go wartość jest ci ągiem liczb całkowitych. Jeżeli inna lista zawiera takie same elementy, mówi się,
że ma identyczną wartość, lecz nie jest tym samym obiektem.

Tworzenie aliasu
Jeśli zmienna a odwołuje się do obiektu i użyjesz instrukcji przypisania b = a, obie zmienne będą
odwoływać się do tego samego obiektu:
»> a = [ 1, 2, 3]
>>> b = a
>>> b is a
Tr ue

128 I Rozdział 10. Listy


Na rysu nku 10.4 pokazano diagram stanu.

a~
b ~ [ 1,2,3]

Rysunek 10.4. Diagram stanu

Skojarzenie zmiennej z obiektem nazywane jest odwołaniem. W tym przykładzie istniej ą dwa odwo-
łania do tego samego obiektu.

Obiekt z więcej niż jednym odwołaniem ma więcej niż jedną nazwę, dlatego mówi się, że dla obiektu
utworzono alias.

Jeśli
obiekt z utworzonym aliasem jest zmienny, modyfikacje dokonane w przypadku jednego aliasu
dotyczą też drugiego:

»> b[O] = 42
>>> a
(42, 2, 3]

Choć taki sposób działania może być przydatny, łatwo popełnić błąd. Ogólnie rzecz biorąc, bez-
pieczniejsze jest unikanie tworzenia aliasów, gdy masz do czynienia z obiektami zmiennymi.

W przypadku obiektów niezmiennych , takich jak łańcuchy, tworzenie aliasów nie stanowi takiego
problemu. O to przykład:
a = 'banan'
b = 'banan'

W tym przykładzie prawie nigdy nie ma znaczenia to, czy zmienne a i b odwołują się do tego samego
łańcucha, czy nie.

Argumenty listy
W momencie przekazania listy funkcji uzyskuje ona odwołanie do listy. Jeśli funkcja modyfikuje
listę, obiekt wywołujący stwierdza zmianę. Na przykład funkcja del et e_head usuwa pierwszy element
z listy:
def delet e_head(t) :
del t [O]

Oto sposób użycia tej funkcji:


>>> l etters = (' a ', ' b', 'c ' ]
>>> del ete_head(lett e rs)
>>> 1 etters
[ ' b', 'c 'J

Parametr t i zmienna l e t ter s to aliasy tego samego obiektu. Na rysunku 10.5 zaprezentowano
diagram stanu.

Argumenty Usty I 129


Rysunek 10.5. Diagram stan u

Ponieważ lista jest współużytkowana przez dwie ramki, została narysowana między nimi.
Ważne jest odróżnienie operacji modyfikujących listy i operacji tworzących nowe listy. Na przykład
metoda append modyfikuje listę, ale operator+ tworzy nową listę:
»> tl [l, 2]
>>> t 2 = t l. append(3)
>>> t l
[ !, 2 , 3]
>>> t2
None

Metoda append modyfikuje listę i zwraca wartość None:


>>> t3 = tl + [4]
>>> tl
[ l , 2, 3]
>>> t3
[ l , 2, 3, 4]
>>> tl

Operator+ tworzy nową listę i pozostawia oryginalną li stę bez zmian.

Różnica jest istotna w przypadku tworzenia funkcji, które mają mo dyfikować listy. Na przykład
następuj ąca funkcja nie usuwa nagłówka listy:
def bad_del ete_head(t) :
t = t [ 1: J #NIEPOPRAWNIE!

Operator wyo drębniania fragmentu tworzy nową listę, a przypisanie powoduje, że zmienna t od-
wołuje się do niej. Nie ma to jednak wpływu na obiekt wywo łujący.

»> t 4 = [ 1, 2, 3]
>>> bad_delete_head(t4)
>>> t4
( 1, 2, 3]

Na początku funkcji bad_delete_ head zmienne t i t 4 odwołuj ą się do tej samej listy. Na końcu zmienna t
odwołuje się do nowej listy, ale zmienna t 4 nadal odwołuje się do oryginalnej, niezmodyfikowa-
nej listy.

Alternatywą jest napisanie kodu funkcji, która tworzy i zwraca nową listę. Na przykład funkcja
tai l zwraca wszystkie elementy listy, z wyjątkiem pierwszego:
def ta il (t) :
return t [ 1: ]

130 I Rozdzlał10. Listy


Funkcja ta pozostawia oryginalną listę bez zmian. Oto sposób użycia funkcji:
>>> l et ters = [ 'a' , 'b' , 'c']
>>> rest = tail (letters)
>>> rest
[ ' b'. 'c ' ]

Debugowanie
Nieuważne korzystanie z list (oraz innych obiektów zmiennych) może doprowadzić do wielogo-
dzinnego debugowania. Oto niektóre typowe pułapki oraz sposoby pozwalaj ące ich uniknąć:
1. Większość metod związanych z listami modyfikuje argument i zwraca wartość No ne . Stanowi
to przeciwieństwo metod łańcuchowych, które zwracają nowy łańcuch i pozostawiają oryginalny
łańcuch bez zmian.

Jeśli utworzono następujący kod z metodą łańcuchową:


word = word.strip()

kuszące może być napisanie kodu z metodą listy o następującej postaci:


t = t.sort () #NIEPOPRAWNIE I

Ponieważ metoda sort zwraca wartość None , następna operacja, jaką wykonasz w przypadku
zmiennej t , prawdopodobnie nie powiedzie się.
Przed skorzystaniem z metod i operatorów powiązanych z listami należy dokładnie przeczytać
dokumentacj ę, a następnie przetestować je w trybie interaktywnym.

2. Wybierz idiom i pozostań przy nim.


Część problemu z listami polega na tym, że istnieje zbyt wiele sposobów realizowania działań.
Aby na przykład usunąć element z listy, możesz skorzystać z metody pop, remove lub del , a nawet
z przypisania z operatorem wyodrębniania fragmentu.
W celu dodania elementu możesz zastosować metodę append lub operator+. Zakładając, żet to li-
sta, a x to element listy, następujące wiersze kodu są poprawne:
t.append(x)
t = t + [x]
t += [x]

Z kolei te wiersze kodu są niepoprawne:


t. append ( [x] ) #NIEPOPRAWNE!
t = t.append{x) #NIEPOPRAWNE!
t + [x] #NIEPOPRAWNE!
t = t + X #NIEPOPRAWNE!

Sprawdź każdy z tych przykładów w trybie interaktywnym, aby mieć pewność, że zrozumiałeś ich
działanie. Zauważ, że tylko ostatni przykład powoduje błąd uruchomieniowy. Pozostałe trzy
wiersze są dozwolone, ale realizują niepoprawne działanie.
3. Twórz kopie w celu uniknięcia definiowania aliasów.
Aby skorzystać z metody takiej jak sort, która modyfikuje argument, i jednocześnie zachować
oryginal ną list ę, możesz utworzyć kopię:

Debugowanie I 131
»> t ~ [ 3 ' 1, 2]
»> t2 ~ t [ :J
»> t2.sort ()
>>> t
[ 3 , 1, 2]
>>> t2
[ l, 2, 3]

W tym przykładzie możesz również użyć funkcji wbudowanej sor ted, która zwraca nową, po-
sortowaną listę, a jednocześnie pozostawia oryginalną listę bez zmian:

>> > t2 ~ s or t ed (t )
>> > t
[ 3 , 1, 2]
>> > t2
[ l, 2, 3]

Słownik
lista
Ciąg wartości.

element
Jedna z wartości listy (lub innego ci ągu) nazywanych również pozycjami.

lista zagnieżdżona
Lista będąca elementem innej listy.

akumulator
Zmienna używana w pętli w celu sumowania lub akumulowania wyniku.

przypisanie rozsz erz one


Instrukcja aktualizująca wartość zmiennej za pomocą operatora, takiego jak + ~ .
redukowanie
Wzorzec przetwarzania dokonujący przejścia ciągu akumulujący elementy w postaci poje-
dynczego wyniku.

odwzorowywanie
Wzorzec p rzetwarzania dokonujący przejścia ciągu i wykonujący operację dla każdego ele-
mentu.

filtrowanie
Wzorzec przetwarzania dokonujący przejścia ciągu i wybierający elementy spełniające okre-
ślone kryterium.

obiekt
Coś, do czego może odwoływać się zmienna. Obiekt ma typ i wartość.

132 I Rozdzlał10. Listy


równoważny

Mający taką samą wartość.

identyczny
Ten sam obiekt (co sugeruje równoważność).

odwołanie

Skojarzenie między zmienną i jej wartością.

tworzen ie aliasu
Sytuacja, w której co najmniej dwie zmienne odwo łują si ę do tego samego obiektu.
separator
Znak lub łańcuch wskazujący miejsce, w którym łańcuch powinien zostać podzielony.

Ćwiczenia
Rozwiązania poniższych ćwiczeń znajdują się w pliku list_exercises.py, który jest dostępny pod
adresem ftp:/lftp.helion.pl/przyklady/myjep2.zip.

Ćwiczenie 10.1.

Utwórz funkcję o nazwie nes t ed_ sum, która pobiera listę złożoną z list liczb całkowitych, a ponadto
sumuje elementy wszystkich list zagnieżdżonych. Oto przykład:
»> t = [ [l, 2]' [3]' [4, 5, 6]]
>>> nested_sum(t)
21

Ćwiczenie 10.2.

Utwórz funkcję o nazwie c umsum pobierającą listę liczb i zwracającą sumę skumulowaną, czyli nową
listę, w
której i-ty element to suma pierwszych i+1 elementów z oryginalnej listy. Oto przykład:
»> t = [ 1, 2' 3]
>» cum sum (t)
[l, 3' 6]

Ćwiczenie 10.3.

Utwórz funkcję o nazwie middl e pobi erającą listę i zwracającą nową listę, która zawiera wszystkie
elementy, z wyjątkiem pierwszego i ostatniego. Oto przykład:
»> t = [ 1, 2' 3 ' 4]
»> middle(t )
[2' 3]

Ćwiczenie 10.4.

Utwórz funkcję o nazwie chop pobierającą listę, modyfikującą ją przez usu nięcie pierwszego i ostat-
niego elementu oraz zwracającą wartość None. Oto przykład:

Ćwlaenla I 133
»> t = [ 1, 2. 3 . 4]
»> c hop( t )
>>> t
[2. 3]

Ćwiczenie 10.5.

Utwórz funkcję o nazwie i s _ sorted, która pobiera listę jako parametr i zwraca wartość True, jeśli
lista sortowana jest w kolejności rosnącej, lub wartość Fal se w przeciwnym razie. Oto przykład:
»> is_sort ed( [ I , 2, 2] )
Tr ue
»> is_sort ed( [ ' b ', ' a' ] )
Fa l s e

Ćwiczenie 10.6.

Dwa słowa są anagramami, jeśli litery z jednego słowa można tak przestawić, że dają drugie sło­
wo. Utwórz funkcję o nazwie is_anagram, która pobiera dwa łańcuchy i zwraca wartość True, jeśli
są one anagramami.

Ćwiczenie 10.7.

Utwórz funkcj ę o nazwie has_ dupl i cat es, która pobiera listę i zwraca wartość True, jeśli istnieje
dowolny element występuj ący więcej niż raz. Funkcja nie powinna modyfikować oryginalnej listy.

Ćwiczenie 10.8.

Ćwiczenie dotyczy tak zwanego paradoksu dnia urodzin, na temat którego możesz przeczytać
pod adresemhttp://pl.wikipedia.org/ wiki/Paradoks_dnia_urodzin.

Jeśli w klasie znajduje się 23 uczniów, jakie są szanse na to, że dwie osoby spośród nich mają urodziny
w ten sam dzień? Prawdopodobieństwo tego możesz oszacować przez wygenerowanie losowych
próbek 23 dat urodzin i sprawdzenie zgodności. Wskazówka: możesz wygenerować losowe daty
urodzin przy użyciu funkcji rand i nt w module random.
Moje rozwiązanie możesz znaleźć w pliku birthday.py.

Ćwiczenie 10.9.

Utwórz funkcję wczytuj ącą plik words.txt i budującą listę z jednym elementem przypadaj ącym na
słowo.Napisz dwie wersje tej funkcji: jedną korzystającą z metody append oraz drugą używającą
idiomu t = t + [x]. Uruchomienie której wersji zajmuje więcej czasu? Z jakiego powodu?

Rozwiązanie: plik wordlist.py.

Ćwiczenie 10.10.

Aby sprawdzić, czy słowo znajduje się na liście słów, możesz użyć operatora i n, ale byłoby to roz-
wiązanie wymagające dłuższego czasu, gdyż słowa byłyby przeszukiwane kolejno.

134 I Rozdzlał10. Listy


Ponieważ słowa są uporządkowane w kolejności alfabetycznej, możliwe jest przyspieszenie operacji za
pomocą wyszukiwania z podziałem na połowę (nazywanego również wyszukiwaniem binarnym),
które przypomina wyszukiwanie słowa w słowniku. Możesz zacząć od środka i sprawdzić, czy szukane
słowo występuje przed słowem w środku listy. Jeśli tak jest, w ten sam sposób przeszukujesz
pierwszą połowę listy. W przeciwnym razie przeszukujesz drugą połowę.

W każdym przypadku pozostały obszar wyszukiwania jest dzielony na pół. Jeśli lista słów zawiera
113 809 słów, znalezienie słowa lub stwierdzenie jego braku będzie wymagać 17 kroków.
Utwórz funkcję o nazwie i n_ bi sect, która pobiera posortowaną listę i wartość docelową, a po-
nadto zwraca indeks wartości listy Qeśli się na niej znajduje) lub wartość None, gdy wartości na li-
ście nie ma.

Możesz też przeczytać dokumentację modułu bi sect i skorzystać z niego!

Rozwiązanie: plik inlist.py.

Ćwiczen ie 10.11.

Dwa słowa tworzą „odwróconą parę", jeśli każde z nich jest odwrotnością drugiego. Utwórz pro-
gram, który znajduje na liście słów wszystkie takie pary.
Rozwiązanie: plik reverse_pair.py.

Ćwiczenie 10.12.

Dwa słowa „zazębiają się", gdy w wyniku pobrania na przemian liter z każdego słowa uzyskuje się
nowe słowo. Na przykład słowa shoe i cold „zazębiają się", tworząc słowo schooled.

Rozwiązanie: plik interlock.py. Informacje o autorze: ćwiczenie inspirowane jest przykładem do-
stępnym pod adresem http://puzzlers.org/.
1. Utwórz program znajdujący wszystkie pary słów, które „zazębiają się". Wskazówka: nie wyli-
czaj wszystkich par!
2. Czy możesz znaleźć jakiekolwiek słowa, które „zazębiają się" w sposób trójkowy (oznacza to,
że co trzecia litera tworzy słowo), począwszy od pierwszej, drugiej lub trzeciej litery?

Ćwlaenla I 135
136 I Rozdzlał10. Listy
ROZDZIAŁ 11 .

Słowniki

W tym rozdziale zaprezentowałem kolejny typ wbudowany nazywany słownikiem. Słowniki to


jeden z najlepszych elementów języka Python. Odgrywają one rolę bloków konstrukcyjnych wielu
efektywnych i eleganckich algorytmów.

Słownik to odwzorowanie
Słownik przypomina li stę, ale jest bardziej ogólny. W przypadku listy indeksy muszą być liczbami
całkowi tymi. W przypadku słownika mogą być (p rawie) dowolnego typu.

Słownik zawiera kolekcję indeksów nazywanych kluczami oraz kolekcję wartości. Każdy klucz
powiązany jest z pojedynczą wartością. Skojarzenie klucza z wartością określane jest mianem pary
klucz-wartość lub czasami pozycji.

W języku matematycznym słownik reprezentuje odwzorowanie kluczy na wartości, dlatego można też
stwierdzić, że każdy klucz dokonuje „odwzorowania" na wartość. W ramach przykładu zbudujemy
słowni k odwzorowujący słowa języka angielskiego na słowa j ęzyka hiszpa ńskiego. Oznacza to, że
klucze i wa rto ści są łańcuchami.

Funkcja d i et nvo rzy nowy słownik bez żadnych elementów. Ponieważ di et to nazwa funkcji
wbudowanej, należy unikać stosowania jej jako nazwy zmiennej.
>>> eng2sp = diet()
>>> eng2sp
{}

Nawiasy klamrowe {} rep reze ntują pusty słownik. Aby do dać elementy do słowni ka, możesz sko-
rzystać z nawiasów kwadratowych:
>>> eng2sp [ ' one'] = 'uno'

W tym wierszu kodu tworzony jest element odwzorowujący klucz one na wartość uno. Jeśli słownik
zostanie ponownie wyświetlony, widoczna będzie para klucz-wartość z dwukropkiem umieszczonym
między kluczem i wartością:

>>> eng2sp
{ 'one ' : ' uno'}

137
Powyższy format danych wyjściowych jest też formatem danych wejściowych. Możesz na przy-
kład utworzyć nowy słownik z t rzema elementami:

>>> eng2sp = {'one' : ' uno' , 'two': 'dos', 'three ' : ' tres'}
Jeśli jednak wyświetlisz słownik eng2sp, możesz być zaskoczony:
>>> eng2sp
1 1 1 1 1
{ one : uno , t hr ee 1 : 1
t re5 1 , 1
two
1
:
1
do5
1
}

Kolejność par klucz-wartość może nie być taka sama. Jeśli identyczny przykład wprowadzisz na 5'Voim
komputerze, możesz uzyskać inny wynik. Ogólnie rzecz biorąc, kolej ność elementów w słowniku
jest nieprzewidywalna.
Nie stanowi to jednak problemu, ponieważ elementy słownika nigdy nie są indeksowane za pomocą
indeksów całkowitoliczbowych. Zamiast tego do wyszukiwania odpowiednich wartości używane są
klucze:
»> eng2sp ['two']
łd0$1

Klucz t wo zawsze odwzorowywany jest na wartość dos, dlatego kolejność elementów nie ma znaczenia.
Jeśli klucza nie ma w słowniku, uzyskasz wyjątek:
»> eng2sp[ ' four']
KeyError : ' four '
Funkcja l en przetwarza słowniki. Zwraca ona liczbę par klucz-wartość:
»> l en{ eng2sp)
3

Operator i n również przetwarza słowniki. Informuje on o tym, czy dany element ma postać klucza
w słowniku (reprezentacja w postaci wartości nie jest wysta rczająco dobra).
>>> 'one' in eng2sp
True
>>> ' uno' in eng2sp
Fa l se

Aby sprawdzić, czy dany element ma postać wartości w słowniku, możesz zastosować metodę va lues,
która zwraca kolekcję wartości, a następnie użyć operatora i n:
>>> val s = eng2sp.values()
1
>>> uno 1 in val5
True
Operator i n korzysta z różnych algorytmów w przypadku list i słowników. Dla list operator kolejno
przeszukuje ich elementy (opisano to w podrozdziale „Wyszukiwanie" rozdziału 8.). Wraz z wydłuża­
niem si ę listy czas wyszukiwania zwiększa się wprost proporcjonalnie.

W przypadku słowników język


Python zapewnia algorytm o nazwie tablica mieszająca o niezwy-
kłej właściwości polegającej na tym, że operator i n wymaga m niej więcej takiej samej ilości czasu
niezależ nie od liczby elementów w słowniku. W podrozdziale „Tablice m ieszające" rozdziału 21.
wyjaśnię, jak to jest możliwe, ale wyjaśnienie to może nie mieć dla Ciebie sensu do momentu przeczy-
tania kilku kolejnych rozdziałów.

138 I Rozdział 11. Słowniki


Słownik jako kolekcja liczników
Załóżmy, że dla danego łańcucha chcesz określić liczbę wystąpień każdej litery. Istnieje kilka spo-
sobów pozwalających na zrealizowanie tego zadania:
1. Możesz utworzyć 26 zmiennych, po jednej dla każdej litery alfabetu. W dalszej kolejności możesz
wykonać dla łańcucha operację przejścia,
a ponadto dla każdej litery zwi ększyć odpowiedni
licznik, korzystając prawdopodobnie z instrukcji warunkowej wchodzącej w skład łańcucha
instrukcj i.
2. Możesz utwo rzyć listę liczącą
26 elementów, a następnie przekształcić każdy znak w liczbę (za
pomocą funkcji wbudowanej ord), użyć liczby jako indeksu listy i dokonać inkrementacji od-
powiedniego licznika.
3. Możesz utworzyć słownik ze znakami jako kluczami i licznikami w roli odpowiednich warto-
ści.Przy pierwszym napotkaniu znaku do słownika zostanie dodany element, a następnie zo-
stanie zwiększona wartość istniejącego elementu.
Każda z powyższych opcj i powoduje wykonanie takiego samego obliczenia, ale w przypadku każ­
dej z nich operacja ta przebiega w inny sposób.

Implementacja to sposób wykonania obliczenia. Niektóre implementacje są lepsze od innych. Na


przykład korzyścią związaną z implementacją słownika jest to, że nie trzeba wiedzieć wcześniej ,
jakie litery występują w łańcuchu, a ponadto konieczne jest zapewnienie miejsca tylko dla liter
w nim się pojawiających.
Odpowiedni kod może mieć następującą postać:
def histogram(s) :
d = diet ()
for c i n s :
i f c not i n d :
d [c] = 1
el s e:
d [c] + = 1
return d

Nazwa funkcji to hi st ogram. Jest to termin używany w statystyce, który identyfikuje kolekcję licz-
ników (lub częstości występowania liter).

Pierwszy wiersz funkcji tworzy pusty słownik Pętla for wykonuje operację przejścia łańcucha. Jeśli
w przypadku każdego wykonania pętli znak c nie występuje w słowniku, tworzony jest nowy element
z kluczem c i wartością początkową 1 (ponieważ litera ta wystąpi ła jednokrotnie). Jeśli znak c jest
już w słowniku, ma miejsce inkrementacja d [c] .

Przebiega to w następujący sposób:


> >> h = histogram( 'brontosaur us ' )
> >> h

Histogram wskazuje, że litery a i b pojawiają się raz, litera o dwa razy itd.

Słownik Jako kolekcja llanlków I 139


Słowniki oferują metodę o nazwie get, która pobiera klucz i wartość domyślną. Jeśli klucz pojawia się
w słowniku, metoda ta zwraca odpowiednią wartość. W przeciwnym razie zwraca wartość do-
myślną. Oto przykład:
>>> h = histogram('a')
>>> h
{' a' : ll
>>> h. get( ' a' , O)

>>> h. get( ' b' , O)


o
W ramach ćwiczenia użyj m etody get do napisania kodu funkcji his tog ram w bardziej zwięzły
sposób. Powinno być możliwe wyeliminowanie instrukcji i f.

Wykonywanie pętli i słowniki


Jeżeli użyjesz słownika w instrukcji for, wykonuje ona operację przejścia dla kluczy słownika. Na
przykład funkcja prin t_ hi st wyświetla każdy klucz i odpowiadającą mu wartość:
def print_hi st(h) :
for c i n h:
print(c , h[c])

Wynik działania funkcj i jest następujący:


»> h = hi st og ram ( ' papuga')
>>> print_hist(h)
a 2
p 2
u 1
g 1

I tym razem klucze nie są uporządkowane w żadnej określonej kolejności. Aby dokonać przejścia
kluczy w kolejności z ustalonym sortowaniem, możesz skorzystać z funkcj i wbudowanej sor ted:
>>>for key in sorted(h) :
print( key, h[key])
a 2
g 1
p 2
u 1

Wyszukiwanie odwrotne
W przypadku słownika d i klucza k z łatwością można znaleźć odpowiadającą mu wartość v d [ k] .
Operacja ta jest nazywana wyszukiwaniem.

Co jednak będzie, gdy istnieje wartość v, dla której chcesz znaleźć klucz k? P ojawiają się tutaj dwa
problemy. Po pierwsze, może i stnieć więcej niż jeden klucz odwzorowywany na wartość v. Zależ­
nie od zastosowania, możliwe jest wybranie jednego klucza lub może być konieczne utworzenie listy
zawierającej wszystkie klucze. Po drugie, nie istnieje prosta składnia pozwalająca na przeprowadzenie
wyszukiwania odwrotnego. Musisz poszukać innego rozwi ązania.

140 I Rozdzlałll. Słowniki


Oto funkcja pobierająca wartość i zwracająca pierwszy klucz odwzorowywany na wartość:
def reverse_l ooku p{d , v) :
for k i n d :
if d [k] == v:
r et urn k
r aise Looku pError()

Powyższa funkcja to jeszcze jeden przykład wzorca wyszukiwania, wykorzystuje ona jednak niezapre-
zentowane dotąd rozwiązanie, czyli instrukcję rai se. Instrukcja rai se powoduje zgłoszenie wyjątku.
W powyższym przykładzie instrukcja ta generuje błąd LcokupError , który jest wbudowanym wyjątkiem
służącym do wskazania, że operacja wyszukiwania nie powiodła się.

Osiągnięcie końca pętli oznacza, że wartość v nie pojawia się w słowniku jako wartość, dlatego
zgłaszany jest wyjątek.

Oto przykład pomyślnego wyszukiwania odwrotnego:


>>> h = histogram( ' papuga')
>>> reverse looku p( h, 2)
>>>
'a '

A tutaj podano przykład operacji zakończonej niepomyślnie:

>>> k = reverse_looku p( h, 3)
Tracebac k (most recent cal l last) :
File "<stdin>" , l ine 1, i n <module>
File "<stdin>" , l ine 5, i n reverse_looku p
Looku pError

Efekt w przypadku zgłoszenia własnego wyjątku jest taki sam jak podczas wygenerowania go przez
interpreter języka Python: następuj e wyświetlenie danych śledzenia i komunikatu o błędzie.
Instrukcja r a i se może pobrać szczegółowy komunikat o błędzie jako argument opcjonalny. Oto
przykład:

>>> raise Loo ku p E rror(' wartość nie wyst ę pu j e w słow n iku ' )
Tracebac k (most recent cal l last) :
Fi l e "<st di n>" , l i ne 1, i n ?
Looku pError: wartość nie wy stępuje w słowni ku

Wyszukiwanie odwrotne jest znacznie wolniejsze od wyszukiwania standardowego. Jeśli wyszukiwanie


odwrotne musi być często przeprowadzane lub słownik znacznie się powiększy, ucierpi na tym
wydajność programu.

Słowniki i listy
Listy mogą występować w słowniku w postaci wartości. Jeśli na przykład dyspo nujesz słownikiem
odwzorowującym litery na częstości ich występowania, możesz zdecydować się na dokonanie
odwrócenia, czyli utworzenie słownika, który odwzorowuje częstości występowania na litery. Po-
nieważ może istnieć kilka liter o takiej samej częstości występowania, każda wartość w odwróconym
słowniku powinna być listą liter.

Słown iki I Usty I 141


Oto funkcja odwracająca słownik:

def invert_diet{d) :
i nverse = diet ()
fo r key ind :
val = d[ key]
i f val not i n i nve rse :
inverse[va l] = [key]
e l se :
i nverse [va l] . append (key )
return inverse

Każdorazowo w trakcie wykonywania pętli zmienna key uzyskuje klucz ze słownika d, a zmienna
va l odpowiednią wartość. Jeśli wartość zmiennej va l nie występuj e w słowniku i n verse, oznacza
to, że wartość ta nie wystąpiła wcześniej. Z tego powodu tworzony jest nowy element i zostaje on
zainicjowany za pomocą singletonu (listy zawierającej pojedynczy element). W przeciwnym razie
wartość ta pojawiła się j uż, dlatego do listy dołączany jest odpowiedni klucz.

O to przykład:
>» hist = histogr am{'papuga')
»> hist
{' a' : 2, 'p ': 2, ' u ': 1, 'g' : l}
>>> inverse = inve rt _diet (hist)
>>> i nverse
{l : [ ' u ' , 'g' ], 2: [ 'a ', 'p ' ] l

Na rysunku 11. 1 przedstawiono diagram stanu prezentujący zmienne hi st i i nverse. Słownik re-
prezentowany jest przez ramkę z umieszczonym nad nią typem d i et oraz znajdującymi się w jej
obrębie parami klucz-wartość. Jeśli wartości są liczbami całkowitymi, liczbami zmiennoprzecin-
kowymi lub łańcuchami, umieszczam je wewnątrz ramki. Listy umiejscawiam zwykle na zewnątrz
ramki, żeby po prostu nie zwi ększać złożoności diagramu.

diet diet li st
hist 'a' - 2 inverse o ------ 'u'
'p' - - - 2
'u'__,.._ 1
list
'g' - - - 1
o-.'a'
2 1 --- ·p·

Ry sunek 11.1. Diagram stan u

Jak pokazano w tym przykładzie, listy mogą być wartościami w słowniku, ale nie mogą być kluczami.
Próba użycia następujących wierszy kodu zakończy się w ten sposób:
» > t = [ 1, 2. 3]
» > d = di et {)
>>> d [t] = ' ojej '
Tracebac k {most recent call l ast) :
Fi l e "<st di n>" , l i ne 1, i n ?
Ty peError : l ist objects are unhas hable

142 I Rozdzlałll. Słowniki


Wspomniałem wcześniej, że słownik jest implementowany za pomocą tablicy mieszającej, co oznacza,
że klucze muszą zapewniać możliwość mieszania.

Funkcja mieszająca to funkcja pobierająca wartość (dowolnego rodzaju) i zwracająca liczbę cał­
kowitą. Słowniki korzystają z takich liczb nazywanych wartościami mieszania, aby przechowywać
i wyszukiwać pary klucz-wartość.

Mechanizm ten działa świetnie, jeśli klucze są niezmienne. Jeśli jednak klucze nie są trwałe, tak jak li-
sty, mają miejsce złe rzeczy. Gdy na przykład utworzysz parę klucz-wartość, interpreter języka Python
wykonuje operację mieszania dla klucza i zapisuje go w odpowiednim położeniu. W przypadku zmo-
dyfikowania klucza i ponownego poddania go operacji mieszania trafi on w inne miejsce. Wówczas
dla tego samego klucza mogą istnieć dwa wpisy lub może okazać się niemożliwe znalezienie klucza.
Niezależnie od sytuacji słownik nie będzie poprawnie działać.

Z tego właśnie powodu klucze muszą zapewniać możliwość mieszania. Dlatego też typy zmienne, takie
jak listy, nie są kluczami. Najprostszym sposobem obejścia tego ograniczenia jest zastosowanie krotek,
którymi zajmiemy się w następnym rozdziale.

Ponieważ słowniki są zmienne, nie mogą odgrywać roli kluczy, ale mogq być używane jako wartości.

Wartości zapamiętywane
Jeśli poeksperymentowałeś z funkcją fioonacc i zaprezentowaną w podrozdziale „Jeszcze jeden przy-
kład" rozdziału
6., być może zauważyłeś, że im większy podany argument, tym dłużej trwa wykony-
wanie kodu funkcji. Co więcej, szybko wydłuża się czas działania programu.
Aby zrozumieć, dlaczego tak jest, przyjrzyj się rysunkowi 11.2, który prezentuje graf wywołań w przy-
padku funkcji f i bonacc i z argumentem n o wartości 4.

fibonacci
n ----- 4

fibonacci
n ----- 3

fibonacci fibonacci fibonacci fibonacci


n ----- 2 n ----- 1 n ----- 1 n ----- O

fibonacci fibonacci
n ----- 1 n _. O

Rysunek 11.2. Graf wywołań

Wartości zapamiętywane I 143


Graf wywołań prezentuje zestaw ramek funkcji z liniami łączącymi każdą ramkę z ramkami wy-
woływanych przez nią funkcji. U samej góry grafu funkcja f i bo nacci z argumentem n o wartości 4
wywołuje funkcję f i bo nacci z argumentem n o wa rtościach 3 i 2. Z kolei funkcja f i bo nacc i z ar-
gumentem n o wartości 3 wywołuje funkcję fi bo nacci z argumentem n o wartościach 2 i 1 itd.
Określ liczbę wywołań f i bonacc i (O} i fi bonacci (1) . Jest to nieefektywne rozwiązanie problemu,
którego efektywność pogarsza się w miarę zwiększania się wartości argumentu.

Rozwiązaniem jest śledzenie już obliczonych wartości przez zapisywanie ich w słowniku. Wcześniej
obliczona wartość zapisywana w celu późniejszego użycia nazywana jest wartością zapamięty­
waną (ang. m emo). Oto „zapa miętana" wersja funkcji f ibonacc i :

known = {0 : 0 , 1: 1}
def fibonacc i (n} :
i f n in known :
return known[n]
r es = fibonacci (n - 1) + f i bonacc i (n - 2 )
known [n] = res
r etu r n r es

known to słownik śledzący j uż znane liczby Fibonacciego. Słownik rozpoczyna się dwoma elementami:
liczba O odwzorowywana jest na liczbę O, a liczba 1 na liczbę 1.
Każdorazowo w momencie wywołania funkcja fi bo nacc i sprawdza słownik known. Jeśli wynik znajduje
się już w słowniku, funkcja może go natychmiast zwrócić. W przeciwnym razie funkcja musi obliczyć
nową wartość, dodać ją do słownika i zwrócić.

Jeśli
uruchomisz tę wersję funkcji f i bonacc i i porównasz ją z oryginałem, stwierdzisz, że jest znacznie
od niego szybsza.

Zmienne globalne
W poprzednim przykładzie słownik known utworzono poza funkcją, dlatego należy on do specjalnej
ramki o nazwie _ mai n_ . Zmienne zawarte w tej ramce są czasem nazy\'rane globalnymi, ponieważ
mogą być dostępne z poziomu dowolnej funkcji. W przeciwieństwie do zmiennych lokalnych, które są
usu wane w momencie zakończenia wykonywania ich funkcji, zmienne globalne są utrzymywane
między kolejnymi wywołaniami funkcji.

Często ma miejsce stosowanie zmiennych globalnych na potrzeby flag, czyli zmiennych boolowskich
wskazujących („flagujących"), czy warunek jest prawdziwy. Na przykład niektóre programy uży­
wają flagi o nazwie verbo se do kontroli poziomu szczegółowości danych wyjści owych:

verbose = True

def e xample!(}:
if verbose :
print( ' Uruchami a nie fu nkcji exa mple !' )

Jeśli
spróbujesz ponownie przypisać zmienną globalną, możesz być zaskoczony. Następujący przykład
ma przypuszczalnie śledzić to, czy funkcja została wywołana:

144 I Rozdzlałll. Słowniki


been called = Fal5e

def exampl e2 () :
been called =T rue #NIE POPRAWNIE

Jeśli jednak uruchomisz tę funkcję, stwierdzisz, że wartość zmiennej been_call ed nie zmienia się.
Problem polega na tym, że funkcja example2 tworzy nową zmienną lokalną o nazwie been_cal l ed.
Zmienna lokalna jest usuwana w momencie zakończenia działania funkcji i nie ma żadnego
wpływu na zmienną globalną.

Aby można było ponownie przypisać wewnątrz funkcji zmienną globalną, musi o na zostać zade-
klarowana, zanim zostanie użyta:
been called = Fal5e

def exampl e2() :


global been called
been called = True

Instrukcja globalna przekazuje interpreterowi następuj ącą informację: „Gdy w tej funkcji użyto
zmiennej been_ ca l l ed, oznacza to zmienną globalną, dlatego nie twórz zmiennej lokalnej".
Oto przykład, w którym podjęto próbę zaktualizowania zmiennej globalnej:
count = O

def example3 () :
count = count + 1 #NIEPOPRAWNIE

Jeśli uruchomisz tę funkcj ę, uzyskasz następujący błąd:


UnboundlocalError: l ocal variable 'count ' referenced before a55ignment

Interpreter języka Python przyjmuje, że zmienna count jest lokalna. W ramach tego założenia zmienna
jest najpierw wczytywana, a następnie zapisywana. I tym razem rozwiązanie polega na zadeklarowaniu
zmiennej globalnej count:
def example3 () :
global count
count += 1

Jeżeli
zmienna globalna odwołuje się do wartości zmiennej, możesz ją zmodyfikować bez dekla-
rowania zmiennej:
known = {O: O, 1: l}

def exampl e4 {) :
known[2] = 1

A zatem masz możliwość dodawania, usuwania i zastępowania elementów listy lub słownika glo-
balnego. Aby jednak ponownie przypisać zm ienną, musisz ją zadeklarować:
def example5() :
globa l known
known = diet()

Zmienne globalne mogą być przydatne. Jeśli jednak istnieje ich wiele i często są modyfikowane,
spowoduje to utrudnienie debugowania programów.

Zmienne globalne I 145


Debugowanie
Gdy będziesz zajmować się większymi zbiorami danych, niewygodne może okazać się debugowanie
przez ręczne wyświetlanie i sprawdzanie danych wyjściowych. Oto kilka sugestii dotyczących debugo-
wania dużych zbiorów danych:

Zmniejszanie roz miaru danych wejściowych

W miarę możliwości zmniejsz rozmiar zbioru danych. Jeśli na przykład program wczytuje
plik tekstowy, zacznij po prostu od pierwszych 10 wierszy lub najmniejszego przykładu, jaki
możesz znaleźć. Możesz dokonać edycji samych plików lub, co jest lepszym rozwiązaniem,
zmodyfikować program w taki sposób, aby wczytywał tylko pierwsze n wierszy.

W przypadku wystąpienia błędu możesz zmniejszyć n do najmniejszej wartości powodującej


błąd, a następnie stopniowo zwiększać tę wartość do momentu znalezienia i usunięcia błędów.

Sprawdzanie podsumowań i typów


Zamiast wyświetlania i sprawdzania całego zbioru danych rozważ wyświetlenie podsumowań
danych. Może to być na przykład liczba elementów w słowniku lub suma dla listy liczb.
Częstą przyczyną błędów uruchamiania jest wartość
o niewłaściwym typie. Na potrzeby de-
bugowania tego rodzaju błędu często wystarczające jest wyświetlenie typu wartości.
Tworz enie automatycznych sprawdzeń
Czasami możesz utworzyć kod dokonujący automatycznego sprawdzenia pod kątem błędów.
Jeśli na przykład przetwarzasz średniej wielkości listę liczb, możesz sprawdzić, czy wynik nie
jest większy niż największy element na liście lub mniejszy od najmniejszego elementu. Ta me-
toda jest określana mianem „sprawdzania poczytalności", ponieważ wykrywa wyniki, które są
„szalone".
Innego rodzaju metoda sprawdzania dokonuje porównania wyników dwóch różnych obliczeń
w celu stwierdzenia, czy są spójne. Jest to nazywane „sprawdzaniem spójności".

Formatowanie danych wyjściowych

Formatowanie danych wyjściowych debugowania może ułatwić wychwycenie błędu. W podroz-


dziale „Debugowanie" rozdziału 6. zaprezentowano odpowiedni przykład. Moduł ppr i nt za-
pewnia funkcję ppri nt wyświetlającą typy wbudowane w formacie czytelniejszym dla czło­
wieka (ppr i nt to skrót od słów pretty print).
I tym razem czas poświęcony na tworzenie kodu szkieletowego może pozwolić na skrócenie czasu
potrzebnego na debugowanie.

Słownik
odwzorowanie
Relacja, w której każdemu elementowi jednego zestawu odpowiada element innego zestawu.

słownik

Odwzorowanie kluczy na odpowiadające im wartości.

146 I Rozdzlałll. Słowniki


para klucz-wartość
Reprezentacja odwzorowania klucza na wartość.

element
W przypadku słownika inna nazwa pary klucz-wartość.
klucz
Obiekt pojawiający się w słowniku jako pierwsza część pary klucz-wartość.
wartość

Obiekt pojawiający się w słowniku jako druga część pary klucz-wartość. W porównaniu z po-
przednim użyciem w książce terminu wartość jest to dokładniejsza definicja.

implementacja
Sposób wykonywania obliczenia.
tablica mieszajqca
Algorytm używany do implementowania słowników w języku Python.
funkcja miesz ajqca
Funkcja stosowana przez tablicę mieszającą do obliczenia położenia dla klucza.

możliwość mieszania
Typ powiązany z funkcją mieszającą. Typy niezmienne, takie jak liczby całkowite, liczby zmien-
noprzecinkowe i łańcuchy, zapewniają możliwość mieszania. Nie pozwalają na to typy zmienne,
takie jak listy i słowniki.

wysz ukiwanie
Operacja słownikowa polegająca na pobraniu klucza i znalezieniu odpowiedniej wartości.

wyszukiwanie odwrócone
Operacja słownikowa polegająca na pobraniu wartości znalezieniu jednego lub większej
liczby kluczy odwzorowywanych na tę wartość.
instrukcja r a i se
Instrukcja celowo zgłaszająca wyjątek.
singleton
Lista (łub inny ciąg) z pojedynczym elementem.

graf wywołań
Diagram prezentujący każdą ramkę utworzoną podczas wykonywania programu ze strzałką
prowadzącą od każdego elementu wywołującego do każdego elementu wywoływanego.
wartość zapamiętywana

Obliczana wartość zapisywana w celu uniknięcia w przyszłości niepotrzebnego obliczenia.

Słownik I 147
z mienna globalna
Zmienna zdefiniowana poza obrębem funkcji. Zmienne globalne mogą być dostępne z poziomu
dowolnej funkcji.
instrukcja globalna
Instrukcja deklarująca nazwę zmiennej jako zmiennej globalnej.

flaga
Zmienna boolowska używana do wskazania, czy warunek jest prawdziwy.

deklaracja
Instrukcja, taka jak g l obal , która informuje interpreter o zmiennej.

Ćwiczenia
Ćwiczenie 11.1.

Utwórz funkcję odczytuj ącą słowa zawarte w pliku words.txt i zapisuj ącą je jako klucze w słowniku.
Rodzaj wartości nie jest istotny. Możesz następnie użyć operatora i n jako szybkiego sposobu spraw-
dzenia, czy łańcuch znajduje się w słowniku.
Jeśli wykonałeś ćwi czeni e 10.10, możesz porównać szybkość tej implementacji z szybkości ą uzy-
skaną w przypadku operatora i n listy i wyszukiwania z podziałem na połowę.

Ćwiczenie 11.2.

Przeczytaj dokumentację metody słownikowej setdefa ult i użyj jej do utworzenia bardziej zwię­
złej wersji funkcji i nvert _di ct .

Rozwiązanie: plik invert_dict.py.

Ćwiczenie 11.3.

Zastosuj technikę zapamiętywania dla funkcji Ackermanna z ćwiczeni a 6.2 i sprawdź, czy umoż­
liwia ona określenie wartości dla funkcji w przypadku większych argumentów. Wskazówka: nie.
Rozwiązanie: plik ackermann_memo.py.

Ćwiczenie 11.4.

Jeśli wykonałeś ćwiczenie 10.7, dysponujesz już funkcją o nazwie has_dupl i cates, która pobiera listę
jako parametr i zwraca wartość Tr ue, gdy istnieje dowolny obiekt pojawiający się na liście więcej
niż raz.

Użyj słownika do utworzenia szybszej i prostszej wersji funkcji ha s_dupl i cates.


Rozwiązanie: plik has_duplicates.py.

148 I Rozdzlałll. Słowniki


Ćwiczenie 11.5.

Dwa słowa tworzą „obrotową parę", jeśli w wyniku obrotu jednego z nich uzyskuje się d rugie
(sprawdź funkcję rota te_word z ćwi czenia 8.5).

Utwórz program wczytujący listę słów i znajdujący wszystkie „obrotowe pary".


Rozwiązanie: plik rotate_pairs.py.

Ćwiczenie 11.6.

Oto kolejna zagadka z audycji Puzzler radia Car Talk (http://www.cartalk.com/content/puzzlers):


Została przesłana przez zaprzyjaźnionego z nami Dana O'Leary'ego. Natrafił on ostatnio na
typowe jednosylabowe słowo liczqce pięć liter, które cechuje się unikalnq właściwościq. A mia-
nowicie, gdy usunie się z niego pierwszą literę, pozostałe litery tworzq homofon oryginalnego
słowa, czyli słowo brzmiqce dokładnie tak samo jak inne słowo. Po przywróceniu pierwszej lite-
ry i usunięciu drugiej litery w rezultacie uzyskuje się jesz cze jeden homofon oryginalnego słowa.
A pytanie brzmi: jakie to słowo?
Podam teraz przykład, który się nie sprawdza. Przyjrzyjmy się 5-literowemu słowu wrack.
W-R-A-C-K, czyli na przykład wrack with pain. Jeśli usunę pierwszq literę, uzyskam 4-literowe
słowo R-A-C-K, jak w zdaniu: „Holy cow, did you see the rack on that buck!". Jest to idealny
homofon. Jeśli ponownie zostanie wstawiona litera w i usunięta litera r, uzyska się słowo wack,
które jest prawdziwym słowem, a nie jedynie homofonem dwóch innych słów.
Istnieje jednak co najmniej jedno słowo znane nam i Danowi, które zapewni dwa homofony,
gdy zostanie usunięta dowolna z dwóch pierwszych liter w celu utworzenia dwóch nowych słów
liczqcych cztery litery. Pytanie brzmi: jakie to słowo?

Aby sprawdzić, czy łańcuch znajduje się na liście słów, możesz użyć słownika z ćwiczenia 11.1.

W celu sprawdzenia, czy dwa słowa są homofonami, możesz skorzystać ze słownika CMU Pronoun-
cing Dictionary. Dostępny jest on do pobrania pod ad resem http://www.speech.cs.cmu.edu/cgi-bin/
cmudict lub w pliku c06d zamieszczonym pod adresem ftp://ftp.helion.pl/przyklady/myjep2.zip.
Możesz również użyć pliku pronounce.py, który zapewnia funkcję o nazwie read_di et i onary. Funkcja
wczytuje ten słownik i zwraca słownik języka Python, który odwzorowuje każde słowo na łańcuch
opisujący podstawową wymowę słowa.

Utwórz program wyszczegól ni ający wszystkie słowa sta nowi ące rozwiązanie powyższej zagadki.
Rozwiązanie: plik homophone.py.

Ćwlaenla I 149
150 I Rozdział 11. Słowniki
ROZDZIAŁ 12.

Krotki

W tym rozdziale omówiłem jeszcze jeden typ wbudowany, czyli krotkę. W dalszej części tekstu wyja-
śniłem, jak współpracują ze sobą listy, słowniki i krotki. Przedstawiłem również funkcję przydatną
w przypadku list argumentów o zmiennej długości, a mianowicie operatory zbierania i rozmieszczania.

Krotki są niezmienne
Krotka to ciąg wartości. Wartości mogą być
dowolnego typu i są indeksowane za pomocą liczb
całkowi tych, dlatego pod tym względem krotki bardzo przypominają listy. I sto tną różnicą jest to,
że krotki są niezmienne.

Ze składni owego punktu widzenia krotka jest listą wartości rozdzielonych przecinkiem:
> >> t = 'a ł ' ł b I ' I c ' ' Id I ' te I

Choć nie jest to konieczne, często umieszcza się krotki w nawiasach okrągłych:
>>> t = { I a'' ' bł ' I cI ' I d ' ' Ie I )

Aby utworzyć krotkę z jednym elementem, musisz dołączyć końcowy przecinek:


> >> t 1 = a I I t

» > t y pe(tl)
<class 't upl e'>

Wartość w nawiasach okrągłych nie jest krotką:


>» t2 = ('a')
»> t y pe (t2)
<class 'str'>

Innym sposobem utworzenia krotki jest zastosowanie funkcj i wbudowanej t up l e. Pozbawiona ar-
gumentu funkcja tworzy pustą krotkę:
»> t = t upl e()
>>> t
()

Jeśli argument jest ci ągiem (łańcuch, lista lub krotka), wynikiem jest krotka z elementami ciągu:
»> t = t upl e('1'ubiny ')
>>> t

Ponieważ t up l e to nazwa funkcj i wbudowanej, należy unikać używania jej jako nazwy zmiennej.

151
Większość operatorów listy można również zastosować w przypadku krotek. Operator w postaci
nawiasów kwadratowych indeksuje element:
>>> t = ( ł aI ' I bI ' I cł ' ł dI ' Ie I)
»> t [OJ
'a'

Operator wyodrębniania powoduje wybranie zakresu elementów:


»> t [l: 3J
( ' b'. ' c ' )

Jeśli jednak spróbujesz zmodyfikować jeden z elementów krotki, uzyskasz błąd:


»> t [OJ = 'A'
TypeError : object doe5n't 5Upport it em a$5ignment

Ponieważ krotki są niezmienne, nie możesz zmodyfikować elementów. Możesz jednak zastąpi ć
jedną krotkę inną:

»> t = ( ' A' , ) + t [ 1: J


>>> t
('Ał , 1b 1, 1c •, •d • , •e1)

Instrukcja ta tworzy nową krotkę, a następnie powoduje, że odwołuje się do niej zmienna t.

Operatory relacyjne współpracują z krotkami oraz innymi ciągami. Interpreter języka Python zaczyna
dzi ałani e od porównania pierwszego elementu z każdego ciągu. Jeśli są one takie same, przecho-
dzi do następnych elementów. Operacja jest kontynuowana do mo mentu znalezienia różniących
się elementów. Elementy po nich występujące nie są rozpat rywane (nawet jeśli są naprawdę duże).
»> (O , 1, 2 ) < (O, 3 , 4)
True
>>> (O, 1, 2000000) < (O, 3, 4)
Tr ue

Przypisywanie krotki
Często przydatna okazuje się możliwość zamiany wartości dwóch zmiennych. W przypadku tra-
dycyjnych przypisań konieczne jest użycie zmiennej tymczasowej. Oto przykład zamiany wartości
zmiennych a i b:
>>> temp = a
>>> a = b
>>> b = temp

Rozwiązani e to jest niezręczne. Bardziej eleganckie będzie przypisanie krotki:


>>> a , b = b, a

Po lewej stronie równania znajduje się krotka zmiennych, a po prawej widoczna jest krotka wyra-
żeń. Każda wartość przypisana jest odpowiadającej jej zmiennej. Wszystkie wyrażenia po prawej
stronie są przetwarzane przed jakimkolwiek przypisaniem.
Liczba zmiennych po lewej stronie i liczba wartości po prawej stronie musi być taka sama:
>>> a , b = 1, 2, 3
Va lueE rror : t oo many value5 t o unpac k

152 I Rozdział 12. Krotki


W bardziej ogólnym wariancie prawą stro nę może reprezentować dowolnego rodzaju ciąg (łańcuch,
lista lub krotka). Aby na przykład adres e-mail podzielić na nazwę użytkownika i domenę, możesz
użyć następ ujących wierszy kodu:

>>> addr = 'monty@python.org'


>>> uname, domain = addr.split( '@ ')

Wartość zwracana funkcji spl it to lista z dwoma elementami. Pierwszy element został przypisany
zmiennej uname, a drugi zmiennej domai n:
>>> uname
'manty'
>>> domain
'python.org'

Krotki jako wartości zwracane


Mówiąc wprost, funkcja może zwrócić tylko jedną wartość. Jeśli jednak wartością jest krotka, efekt jest
taki sam jak w przypadku zwracania wielu wartości. Aby na p rzykład podzielić dwie liczby całko­
wite oraz obliczyć iloraz i resztę, nieefektywne będzie obliczenie x / y, a następnie x % y. Lepszą opcją
będzie jednoczesne ich obliczenie.

Funkcja wbudowana di vmod pobiera dwa argumenty i zwraca krotkę dwóch wartości: iloraz i resztę.
Wynik możesz zapisać jako krotkę:
>>> t = divmod(7, 3)
>>> t
(2 ' 1)

Aby elementy zapi sać osobno, możesz sko rzystać z przypisania krotki:
>>> quot , rem = di vmod(7, 3)
>>> quot
2
>>> rem

Oto przykład funkcji zwracającej krotkę:

def min_max(t) :
return min(t) , max (t)

max i mi n to funkcje wbudowane, które znajdują odpowiednio największy i najmniejszy element ciągu.
Funkcja mi n_ma x wykonuje obliczenie dla obu tych elementów i zwraca krotkę dwóch wartości.

Krotki argumentów ozmiennej długości


Funkcje mogą pobierać zmienną liczbę argumentów. Nazwa parametru zaczynająca się od znaku *
powoduje zbieranie argumentów w krotce. Na przykład funkcja pri ntall pobiera dowolną li czbę
argumentów i wyświetla je:
def printal l {*args) :
print (args)

Krotki argumentów o zmiennej długo~d I 153


Choć parametr zbieraj ący może mieć dowolną nazwę, zwykle jest to nazwa ar gs. Działanie powyższej
funkcji jest następujące:
»> printal l (1 , 2. 0, '3')
(1 , 2. 0, '3')

Dopełnieniem zbierania jest rozmieszczanie. J eśli istnieje ciąg wartości, który ma zostać przeka-
zany funkcji w postaci wielu argumentów, możesz skorzystać z operatora *. Na przykład funkcja
di vmod pobiera dokładnie dwa argumenty. Nie zadziała ona w przypadku krotki:
»> t = ( 7. 3)
»> di vmod(t)
Ty peE rror : di vmod ex pect ed 2 arguments , got 1

Jeśli jednak dla krotki zastosujesz rozmieszczanie, funkcja będzie działać:


»> di vmod( *t)
(2. 1)

Wiele funkcji wbudowanych używa krotek argumentów o zmiennej długości. Na przykład funkcje max
i mi n mogą pobrać dowolną liczbę argumentów:
>>>max (!, 2, 3)
3

W przypadku funkcji s umjuż tak jednak nie jest:


>>> 5Um( l, 2, 3)
Ty peE rror : 5Um ex pect ed at mo5t 2 argument5 , got

W ramach ćwiczenia utwórz funkcj ę o nazwie s umal 1, która pobiera dowolną liczbę argumentów
i zwraca ich sumę.

Listy i krotki
zip to funkcja wbudowana pobierająca dwie lub większą liczbę ciągów i zwracająca listę krotek, z któ-
rych każda zawiera jeden element z każdego ciągu. Nazwa tej funkcj i odnosi się do suwaka, który łączy
naprzemiennie dwa rzędy zębów.
Oto przykład po łączenia łańcucha i listy:
~
1
>>> ::: abc 1
»> t = [O, 1, 2]
»> z ip(5 , t)
<z ip object at Ox7f7d0a9e7c48>

Wynikiem jest obiekt funkcji zip, który zawiera informacje o sposobie wyko nywania iteracji par.
Fu nkcja zi p jest najczęściej używana w pętli for:
>>> fo r pair in z ip(5 , t) :
print (pair)

( ' a' , O)
( ' b' . 1)
( ' c' . 2)

154 I Rozdział 12. Krotki


Obiekt funkcji zi p to rodzaj iteratora będącego dowolnym obiektem, który dokonuje iteracji ciągu.
Pod pewnymi względam i iteratory przypominaj ą listy, ale, w przeciwieństwie do nich, nie pozwalają
na użycie indeksu do wyboru elementu z iteratora.

Aby skorzystać z operatorów i metod list, możesz zastosować obiekt funkcj i z ip w celu utworzenia
listy:
> >> l ist( zip(s , t))
[ ( ' a' , O) , (' b ' , 1) , ('c ', 2 ) ]

Wynikiem jest lista krotek. W tym przykładzie każda krotka zawiera znak z łańcucha i odpowiednie
elementy listy.

Jeśli ciągi nie mają jednakowej długości, wynik ma długość krótszego z nich:
>» l is t (zip (' Anna ' , ' Ewa' ) )
[ ( ' A' , 'E ') , ('n' , ' w') , ('n' , ' a') ]

Aby wykonać operację przechodzenia listy krotek, w pętli for możesz zastosować przypisanie krotki:
t = [ ('a ' , O) , ('b' , 1) , ('c' , 2 )]
f or l ette r , numbe r in t :
print (numbe r , l et ter)

Podczas każdego wykonania pętli interpreter języka Python wybiera następną krotkę na liście i przypi-
suje elementy zmiennym 1etter i number. Oto dane wyjściowe takiej pętli:
Oa
1 b
2 c

Jeśli połączysz funkcję z i p, pętlę fo r i przypisania krotki, uzyskasz p rzydat ny idiom pozwalają­
cy wykonać j ednocześnie operację przejścia dla dwóch lub większej liczby ciągów. Na przykład funk-
cja has_matc h pobiera dwa ciągi t l i t2 oraz zwraca wartość True, j eśli istnieje indeks i taki, że
tl [ i ] == t2[i ] :
def has_match(t l, t 2) :
for x, y in z ip(t l. t 2) :
i f X == y :
ret urn Tr ue
ret urn Fa l se

Jeśli wymagasz wykonania operacji przejścia elementów ciągu i ich indeksów, możesz skorzystać
z funkcji wbudowanej enumerat e:
f or index, el ement in enumerate('abc' ) :
print(index, el ement)

Wynikiem wykonania funkcji enumerate jest obiekt wyliczeniowy, któ ry doko nuje iteracji ciągu
par. Każda para zawiera indeks (począwszy od liczby O) i element z danego ciągu. W przedstawionym
przykładzie dane wyj ściowe są następujące:

Oa
1 b
2 c

Po nownie.

Listy I krotki I 155


Słowni ki i krotki
Słowniki oferują metodę o nazwie i tems , która zwraca ciąg krotek. Każda z nich ma postać pary
klucz-wartość:
>>> d = { 'a' : O, 'b ' : 1, ' c' : 2 l
>>> t = d .i t ems {)
>>> t
di ct_ items( [ ('c ' , 2 ) , ('a' , O) , ( ' b' , l ) ] )

Wynikiem jest obiekt d i e t _ i t ems, czyli iterator dokonujący iteracji par klucz-wartość. Obiekt mo-
żesz zastosować w pętli for w następujący sposób:
>>> for key, val ue in d. it ems( ) :
print( key, val ue)

c 2
a O
b 1

Jak można się spodziewać w przypadku słownika, elementy nie są uporządkowane w żaden konkretny
sposób.

Zmierzając w przeciwnym kierunku, listy krotek możesz użyć do inicjalizacji nowego słownika:
»> t = [ ( ' a ' , O) , { ' c ' , 2 ) , ( ' b ' , 1)]
»> d = di ct(t )
>>> d
( ' a' : O, 'c ': 2 , 'b': l l

Połączenie funkcji di et i z ip zapewnia zwięzły sposób tworzenia słownika:


»> d = di ct(zi p ( ' abc', range (3) ) )
>>> d
( ' a ' : O, ' c ': 2, 'b': l l

Metoda słownika upda te również pobiera listę krotek i dodaje je do istniejącego słownika jako pary
klucz-wartość.

Częste jest użycie krotek w roli kluczy słowników (głównie z powodu braku możliwości zastosowania
list). Na przykład w książce telefonicznej może mieć miejsce odwzorowywanie par złożonych z nazwi-
ska i imienia na numery telefonów. Zakładaj ąc, że zdefiniowano zmienne last , fi r s t i number ,
można zapisać następuj ący wiersz kodu:

di rect ory [l ast , f irst ] = number

Wyrażenie w nawiasach kwadratowych to krotka. W celu wykonania operacj i przejścia takiego


słownika można użyć przypisania krotki:

fo r l ast , first in directory :


pr int(first , l ast , direct ory[ last ,fi rst] )

Powyższa pętla dokonuje przejścia kluczy słownika d i rect ory, które są krotkami. Pętla przypisuje
elementy każdej krotki zmiennym last i f irst, a następnie wyświetla imię i nazwisko oraz odpowia-
dający im numer telefonu.

156 I Rozdział 12. Krotki


Istnieją dwa sposoby reprezentowania krotek na diagramie stanu. Bardziej szczegółowa wersja
pokazuje indeksy i elementy dokładnie tak, jak pojawiają się na liście. Na przykład krotka (' Cleese',
'John ' ) zostanie przedstawiona takjak na rysunku 12.1.

krotka
o~ 'Cleese;
1- 'John'

Rysunek 12.1. Diagram stanu

W przypadku większego diagramu możesz jednak wymagać pominięcia szczegółów. Na przykład


diagram powiązany z książką telefoniczną może wyglądać jak na rysunku 12.2.

słownik

('Cleese', 'John') ---+- ' . 222'


('Chapman', 'Graham') ~ 'ÓB
('ldle', 'E ri c')~ '08700 100 222'
('Gilliam', 'Terry')i:':',:;)'ffe·.;;~p8700 100 222'
('Jones', 'Terry·ff~:\~'.X:~08700 100 222'
('Palin', 'Michael'l'c_.,,'L;~~'~bs700 100 222'

Rysunek 12.2. Diagram stanu

W tym przypadku krotki pokazano, używając składni języka Python jako skrótu graficznego. Numer
telefonu widoczny na diagramie powiązany jest z linią pozwalającą na składanie skarg dotyczących te-
lewizji BBC, dlatego proszę, nie dzwoń pod ten numer.

Ciągi ciągów
Skoncentrowałem się na listach krotek, ale niemal we wszystkich przykładach można użyć również
listy list, krotki krotek i krotki list. Aby uniknąć wyliczania możliwych kombinacji, łatwiej jest
omówić ciągi złożone z ciągów.

W wielu kontekstach różne rodzaje ciągów (łańcuchy, listy i krotki) mogą być używane wymien-
nie. A zatem jak należy wybrać jeden rodzaj zamiast innych?
Zaczynając od oczywistej rzeczy, łańcuchy są bardziej ograniczone niż inne sekwencje, ponieważ
ich elementy muszą być znakami. Łańcuchy są również niezmienne. Jeśli niezbędna jest możliwość
zmiany znaków łańcucha (zamiast tworzenia nowego łańcucha), może być wymagane użycie listy
znaków.
Listy są powszechniejsze niż krotki, głównie dlatego, że są zmienne. Istnieje jednak kilka sytuacji,
w których krotki mogą być preferowane:

Ciągi ciągów I 157


1. W niektórych kontekstach, takich jak instrukcja ret urn, pod względem składniowym prostsze
jest utworzenie krotki niż listy.
2 . Aby w roli klucza słownika użyć ciągu, musisz skorzystać z typu niezmiennego, takiego jak krotka
lub łańcuch.
3 . Jeżeli jako argument przekazujesz funkcji ciąg, zastosowanie krotek pozwala zmniejszyć ryzyko
nieoczekiwanego zachowania spowodowanego tworzeniem aliasów.
Ponieważ krotki są niezmienne, nie zapewniają takich metod jak sor t i reverse, które modyfikują ist-
niej ące listy. Język Python oferuje jednak funkcję wbudowaną sor t ed, która pobiera dowolny ci ąg
i zwraca nową listę z tymi samymi posortowanymi elementami. Udostępniana jest też funkcja
r eversed, która pobiera ciąg i zwraca iterator dokonujący przejścia listy w odwrotnej kolejności.

Debugowanie
Listy, słowniki i krotki to przykłady struktur danych. W rozdziale rozpoczynamy omawianie złożo­
nych struktu r danych, takich jak listy krotek lub słowniki, które zawi eraj ą krotki w postaci kluczy
i listy jako wartości. Złożone struktury danych są przydat ne, ale podatne na to, co nazywam błę­
dami „kształtu" , czyli błędami występującymi, gdy struktura danych ma niepoprawny typ, wiel-
kość lub budowę. Jeśli na przykład oczekujesz listy z j edną liczbą całkowitą, a ja zapewnię zwykłą
liczbę całkowitą (nie na liście), to nie zadziała.

Aby ułatwić debugowanie tego rodzaju błędów, utworzyłem moduł o nazwie s truct shape, który
oferuje funkcję o takiej samej nazwie: st ruc t shape. Funkcja pobiera jako argument dowolnego
rodzaju strukturę danych i zwraca łańcuch podsumowuj ący jej „kształt". Moduł możesz znaleźć
w pliku structshape.py.
Oto wynik uzyskany dla zwykłej listy:
>>> from s t ructs hape import st r uctshape
»> t = [ 1, 2 . 3]
>>> s tru ct s hape {t)
' t yp 1 ist z ł ożony z 3 int'

Oto lista list:


»> t2 = [[1 ,2] . [3, 4] . [5 , ó]J
>>> s tructs hape (t2)
' t yp 1 i s t z ł ożony z 3 1 ist z łoż ony z 2 int '

Jeśli elementy listy nie są tego samego typu, funkcja s truc ts hape grupuje je kolejno według typu:
»> t3 = [ l , 2 , 3, 4 . 0 , '5 ' . ' 6 ' . [7] . [8]. 9]
>>> s tructs hape {t3 )
' t yp 1 is t z ł ożony z (3 int , fl oat , 2 s tr , 2 1 i s t z ł ożony z i nt , int) '

Oto lista krotek:


>>> 5 = ab cł
1

»> 1 t = 1 i s t (zi p {t . s ))
>>> s truct s hape {lt )
'typ 1 is t z ł oż ony z 3 tu ple z ł ożony z {int , s tr)'

158 I Rozdział 12. Krotki


Oto słownik z trzema elementami odwzorowuj ącymi liczby całkowite na łańcuchy:
>>> d = dict(lt)
>>> structs hape {d)
'ty p diet z ł oż ony z 3 int->str'

Jeśli masz problem ze śledzeniem używanych struktur danych, funkcja structshape może być
pomocna.

Słownik
krotka
Niezmienny ci ąg elementów.

przypisanie krotki
Przypisanie z ciągiem po prawej stronie i krotką zmiennych po lewej stronie. Po przetworzeniu
prawej strony jej elementy są przypisywane zmiennym po lewej stronie.

z bieranie
Operacja polegaj ąca na tworzeniu krotki argumentów o zmiennej długości .

roz mieszcza n ie
Operacja polegająca na traktowaniu ciągu jako listy argumentów.

obiekt funkcji z ip
Wynik wywo łania funkcj i wbudowanej z ip. Jest to obiekt dokonujący iteracji ciągu krotek.
itera tor
Obiekt, który może dokonywać iteracji ciągu, ale nie zapewnia operatorów i metod listy.

struktura danych
Kolekcja powiązanych wartości, które często są uporządkowa ne w postaci list, słowników,
krotek itp.
błqd „kształtu"

Błąd spowodowany tym, że wartość ma niepoprawny „kształt ", czyli ni ewłaściwy typ lub
wielkość.

Ćwiczenia
Ćwiczenie 12.1.

Utwórz funkcję o nazwie most_freq uent pobierającą łańcuch i wyświetlającą litery zgodnie z kolejno-
ścią określoną przez zmniejszającą się częstość występowania w łańcuchu. Znajdź próbki tekstowe
z kilku róż nych języków i sp rawdź, jak zmienia się w nich częstość występowa ni a liter. Po równaj
wyniki z tabelami dostępnymi pod adresem http://en.wikipedia.org/ wiki/LetterJrequencies.
Rozwiązanie: plik mostJrequent.py.

Ćwlaenla I 159
Ćwiczenie 12.2.

Więcej anagramów!
1. Utwórz program wczytujący listę słów z pliku (zajrzyj do podrozdziału „Odczytywanie list
słów" rozdziału 9.) i wyświetlający wszystkie zestawy słów tworzących anagramy.

Oto przykład możliwych danych wyjściowych dla języka angielskiego:


['deltas ' , ' des alt', ' l asted ', ' s alted', ' s l a ted ', ' stal ed ' ]
['ret a i ne r s ' , ' ternar i es' ]
['generating' , ' greaten i ng ' ]
[ ' resmel ts ', ' s me l ters ', ' terml ess ' ]

Wskazówka: może być wskazane zbudowanie słownika odwzorowującego kolekcję liter na listę
słów, które mogą zostać zapisane za pomocą tych liter. Pytanie brzmi: jak możesz przedstawić
kolekcję liter w taki sposób, by pełniły one funkcję klucza?

2. Zmodyfikuj poprzedni program tak, aby najpierw wyświetlił naj dłuższą listę anagramów,
a następnie drugą w kolejności najdłuższą listę itd.
3. W grze Scrabble „bingo" ma miejsce w m omencie zagrania wszystkimi siedmioma płytkami
z własnego stojaka wraz z literą na planszy tak, że powstaje słowo liczące osiem liter. Jaka ko-
lekcja ośmiu liter tworzy najbardziej możliwe „bingo" (wskazówka: jest ich siedem)?

Rozwiązanie: plik anagram_sets.py.

Ćwiczenie 12.3.

Dwa słowa tworzą „parę metatezy", gdy jedno słowo zostanie przekształcone w drugie przez zamianę
dwóch liter (na przykład słowa converse i conserve). Napisz program znajdujący w słowniku wszystkie
takie pary. Wskazówka: nie sprawdzaj wszystkich par słów ani wszystkich możliwych zamian.
Rozwiązanie: plik metathesis.py. Informacja o twórcach: ćwiczenie inspirowane jest przykładem
dostępnym pod adresem http://puzzlers.org/.

Ćwiczenie 12.4.

Oto kolejna zagadka z audycji Puzzler radia Car Talk (http://www.cartalk.com/content/puzzlers):


Jakie jest najdłuższe słowo w języku angielskim, które w przypadku usuwania z niego po jednej
literze naraz do końca pozostanie poprawne dla tego języka?
Litery mogq być usuwane z dowolnego końca lub z e środka. Nie można jednak przestawić żad­
nej litery. Każdorazowo po usunięciu litery uzyskuje się kolejne angielskie słowo. Postępujqc w ten
sposób, ostatecznie uzyskamy jednq literę, która również będzie słowem w języku angielskim
z najdujqcym się w słowniku. Zależy mi na następujqcej informacji: jakie jest najdłuższe słowo i ile
liter zawiera?
Podam wam drobny przykład: słowo Sprite. W porzqdku? Na poczqtek z e środka tego słowa zo-
stanie usunięta litera r. W efekcie uzyska się słowo spite. Z końca tego słowa zostanie usunięta
litera e, co w efekcie zapewni słowo spit. Po usunięciu litery s pozostanie słowo pit. Kolejne usunięcia
litery dadz q słowa it i I.

160 I Rozdział 12. Krotki


Utwórz program znajdujący wszystkie słowa, które mogą zostać skrócone w ten sposób, a następ­
nie znajdź naj dłuższe takie słowo.
Ćwiczenie to stanowi trochę większe wyzwanie niż większość innych, dlatego podaję kilka sugestii:

1. Wskazane może być napisanie fu nkcji p obierającej słowo i określającej listę wszystkich słów,
które mogą zostać utworzone p rzez usunięcie jednej litery. Są to elementy podrzędne słowa.
2. Z rekurencyjnego punktu widzenia słowo może zostać skrócone, j eśli taką możliwość zapew-
nia dowolny z jego elementów podrzędnych . Jako przypadek bazowy możesz rozważyć pusty
łańcuch.

3. Podana p rzeze m nie lista słów w pliku words.txt nie zawiera słów jednoliterowych. Z tego
powod u możesz dodać słowa I i a oraz pusty ła ńcuch.
4 . Aby zwiększyć wydaj ność programu, możesz zdecydować się na zapamiętanie słów, o któ-
rych wiadomo, że mogą zostać skrócone.

Rozwiązanie: plik reducible.py.

Ćwlaenla I 161
162 Rozdział 12. Krotki
ROZDZIAŁ 13.

Analiza przypadku: wybór struktury danych

Poznałeś już podstawowe struktury danych języka Python, a także niektóre algorytmy, które z nich
korzystają. Jeśli
chcesz dowiedzieć się więcej o algorytmach, być może jest to właściwy moment
na przeczytanie rozdziału 2 1. Nie ma jednak potrzeby robienia tego przed kontynuowaniem lek-
tury kolejnych rozdziałów. Rozdział ten możesz przeczytać w dowolnym momencie, w którym
uznasz to za przydatne.
W tym rozdziale zaprezentowałem anali zę przypadku z ćwiczeniami pozwalającymi zastanowić
się nad wyborem struktur danych i ich praktycznym wykorzystaniem.

Analiza częstości występowania słów


Jak zwykle zanim przeczytasz zamieszczone przeze mnie rozwi ązania, powinieneś przynajmniej
podjąć próbę wykonania ćwiczeń.

Ćwiczen ie 13.1.

Utwórz program odczytujący plik, dzielący każdy wiersz na słowa, usuwający z nich białe spacje
i znaki interpunkcyjne oraz dokonujący konwersji słów na zawierające wyłącznie małe litery.

Wskazówka: moduł s tri ng zapewnia obiekt łańcucha o nazwie whi t espace, który zawiera spację,
znak tabulacji, znak nowego wiersza i tym podobne, a także oferuje łańcuch punctuat ion ze zna-
kami interpunkcyjnymi. Sprawdźmy, czy uda się sprawić, że interpreter j ęzyka Python wyświetli
coś dziwnego:
>» import 5t ring
>>> 5tri ng. punctuati on
, '"#$%&' (J *+,-. / : ; <=>?@[\J " _ ' (I !- '
Możesz też rozważyć użycie metod łańcuchowych s tri p, r epl ace i trans lat e.

Ćwiczenie 13.2.

Wyświetl witrynę Project Gutenberg (http://gutenberg.org/ ) i pobierz ulubioną książkę bez praw
autorskich w zwykłym formacie tekstowym.

163
Zmodyfikuj program z poprzedniego ćwiczenia tak, aby wczytał pobraną książkę, pomi nął in-
formacje nagłówkowe z początku pliku i przetwo rzył resztę słów, tak jak wcześniej.
Zmodyfikuj następnie program, by określał całkowitą liczbę słów w książce oraz liczbę wystąpień
każdego słowa.

Wyświetl liczbę różnych słów zastosowanych w książce. Porównaj różne książki napisane przez
różnych autorów i w różnych epokach. Który autor korzysta z najbardziej rozbudowanego słownika?

Ćwiczenie 13.3.

Zmodyfikuj program z poprzedniego ćwiczenia tak, aby wyświetlił 20 słów najczęściej używanych
w książce.

Ćwiczenie 13.4.

Zmodyfikuj poprzedni program tak, aby wczytał listę słów (zajrzyj do podrozdziału „Odczytywanie list
słów" rozdziału 9.), a następnie wyświetlił wszystkie słowa z książki, których nie ma na tej liście. Ile
spośród tych słów zawiera literówki? Ile z tych słów to typowe słowa, które powinny być na liście
słów, a ile to naprawdę mało znane słowa?

Liczby losowe
W przypadku tych samych danych wejściowych większość programów komputerowych generuje
każdorazowo identyczne dane wyjściowe, dlatego dane te są określane mianem deterministycznych.
Determinizm to zwykle coś dobrego, ponieważ oczekiwane jest, aby to samo obliczenie zawsze zapew-
niło taki sam wynik Jednak w niektórych zastosowaniach pożądane jest, aby komputer działał w spo-
sób nieprzewidywalny. Oczywistym przykładem są gry, ale istnieje więcej takich przykładów.
Zapewnienie dzi ałani a programu w sposób naprawdę niedeterministyczny okazuje się trudne, ale
istniejąmetody, które pozwalają na to, aby program sprawiał przynajmniej wrażenie niedetermi-
nistycznego. Jedną z tych metod jest zastosowanie algorytmów generujących liczby pseudolosowe.
Takie liczby nie są zupełnie losowe, gdyż są generowane w wyniku obliczenia deterministycznego.
Samo przyjrzenie się tym liczbom w żadnym razie nie pozwoli jednak na odróżni enie ich od liczb
losowych.
Moduł ra ndom zapewnia funkcje, które generują liczby pseudolosowe (od tego miejsca będę je na-
zywał po prostu losowymi).
Fu nkcja random zwraca losową liczbę zm iennoprzecinkową z zakresu od O.O do 1. 0 (z uwzględnie­
niem wartości O.O, lecz nie wartości 1.0). Każdorazowe wywołanie tej funkcji powoduje uzyskanie na-
stępnej liczby z długiej serii. Aby zapoznać się z przykładem, uruchom następującą pętlę:

import random
for i in range (IO) :
x = random . random( )
print (x)

Funkcja rand i nt pobiera parametry l ow i hi gh oraz zwraca liczbę całkowitą z przedziału od l ow do


hi gh (z uwzględnieniem obu parametrów):

164 I Rozdział 13. Analiza przypadku: wybór struktury danych


»> ra ndom . randint(S , 10)

»> ra ndom . randint(S , 10)


9

W celu wybrania losowego elementu z ciągu możesz użyć funkcji choi ce:
»> t = [ 1, 2. 3]
>» ra ndom . choi ce(t)
2
>» ra ndom . choi ce(t)

Moduł randomoferuje też funkcje generujące wartości losowe na podstawie rozkładów ciągłych, w tym
rozkładu Gaussa, rozkładu wykładniczego, rozkładu gamma i kilku innych.

Ćwiczenie 13.5.

Utwórz funkcję o nazwie c hoose_ f rom_h ist , która pobiera histo gram zdefiniowany w podrozdziale
„Słownik jako kolekcja liczników" rozdziału 11. i zwraca wartość losową z histogramu wybraną
z prawdopodobieństwem proporcjonalnym do częstości występowania tych słów. Na p rzykład
w przypadku następującego histogramu:
>>> t = a [I a I 'b JI I ' I I

>>> hist = hist ogram(t)


>» hist
{ ' a' : 2, 'b' : 1)

funkcja powinna zwrócić literę a z prawdopodobieństwem 2/3, a literę b z prawdopodobieństwem 1/3.

Histogram słów
Przed dalszą lekturą należy podjąć próbę wykonania poprzednich ćwiczeń. Moje rozwiązanie możesz
znaleźć w pliku analyz e_bookl.py. Wymagany będzie też plik emma.txt, oba są umieszczone
w archiwum dostępnym pod adresem ftp:/lftp.helion.pl!przyklady!myjep2.zip.

Oto program wczytujący plik i budujący histogram słów w pliku:


import string

def process_file(file name) :


hist = diet{)
f p = open( f i lename)
for l ine in f p:
process_li ne( li ne , hi st)
return hist

def process_li ne(li ne , hist) :


l i ne = li ne . rep lace (' - ' , ' ' )

f or word in li ne . spl it{) :


word = word . strip(string . punct uation + string .wh itespace)
word = word . l ower ()
hi st [word] = hist . get(word , OJ + 1

hist process file('emma . txt')

Histogram słów I 165


Program wczytuje plik emma.txt, który zawiera tekst z książki Emma napisanej przez Jane Austen.
Funkcja proces s _f i l e wykonuje pętlę dla wierszy pliku, przekazując je po jednym funkcji process _
li ne. Histogram hist odgrywa rolę akumulatora.

Funkcja process_ l i ne używa metody łańcuchowej r epl ace do zastępowania łączników spacjami
przed zastosowaniem metody spl i t do podziału wiersza na listę łańcuchów. Funkcja dokonuje
przejścia listy słów oraz za pomocą metod st r ip i l ower usuwa znaki interpunkcyjne i przekształca
słowa w zawierające wyłącznie małe litery (stwierdzenie, że łańcuchy są przekształcane, jest
uproszczeniem; pamiętaj, że łańcuchy są niezmienne, dlatego metody takie jak s t ri p i lower zwracają
nowe łańcuchy).
I wreszcie, funkcja proces s _l ine aktualizuje histogram, tworząc nowy element lub inkrementuj ąc
już ist niej ący.

Aby określić łączną liczbę słów w pliku, można zsumować częstości występowania w histogramie:
def tota l _words{ hist) :
return s um{hist .values())

Liczba różnych słów to po prostu liczba elementów w słowniku:


def di ff ere nt_words(hist) :
return len{hist)

Oto kod wyświetlający wyniki:


print{' Ł ąc z na l iczba słów: ' , t otal _words( hi s t ))
print{' Liczba róż nyc h słów: ' , d iffe rent _words( hist))

A tutaj wyniki:
Łąc z na liczba słów : 161080
Liczba róż ny c h słów: 7214

Najczęściej używane słowa


Aby znaleźć najczęściej używane słowa, można utworzyć listę krotek, w której każda krotka za-
wiera słowo i częstość jego występowania, a następnie posortować listę.

Następująca funkcja pobiera histogram i zwraca listę krotek złożonych ze słowa i częstości jego
występowania:

def most_common{ hist) :


t = []
for key, val ue in hist . it ems() :
t . appe nd{{ val ue , key ))

t . sort(reverse=Tr ue)
return t

W każdej krotce najpierw występuj e częstość, dlatego wynikowa lista sortowana jest według czę­
stości występowania danego słowa. Oto pętla wyświetlająca 10 najczęściej używanych słów:
t = most_common{ hist)
print{'Oto najc z ęśc i ej używane słowa : ' )
for f req , word in t [: 10]:
print(word , freq , sep=' \ t ' )

166 I Rozdział 13. Analiza przypadku: wybór struktury danych


Używam argumentu słowa kluczowego sep do poinstruowania funkcji pri nt, żeby w roli separa-
tora zamiast spacji zastosowała znak tabulacji. Dzięki temu druga kolumna jest wyrównana. Oto
wyniki uzyskane dla tekstu z książki Emma:
Oto najc z ęści ej u żywane słowa :
to 5242
the 5205
and 4897
of 4295
3191
a 3130
it 2529
her 2483
was 2400
she 2364

Kod można uprościć za pomocą parametru key funkcji sort. Aby dowiedzieć się więcej na ten temat,
możesz przeczytać informacje dostępne pod adresem https://wiki.python.org/moin/Ho wTo/Sorting.

Parametry opcjonalne
Wcześniej zaprezentowałem funkcje i metody wbudowane, które pobierają argumenty opcjonalne.
Możliwe jest również tworzenie definiowanych przez programistę funkcji z argumentami opcjonal-
nymi. Oto na przykład funkcja wyświetlająca słowa najczęściej występujące w histogramie:
def print_most_common( hist , num=lO) :
t = most_common(hist)
print('Oto najc z ęści ej używane słowa : ')
for freq , word in t [: num] :
print(word , freq , sep=' \t ' )

Pierwszy parametr jest wymagany, a drugi opcjonalny. Wartość domyślna parametru num wynosi 10.

Jeśli podasz tylko jeden argument:


print_most_common(hist)

parametr numotrzymuje wartość domyślną. W przypadku podania dwóch argumentów:


pr i nt_most_common( hist , 20)

parametr num uzyska wartość argumentu. Inaczej mówiąc, argument opcjonalny nadpisuje war-
tość domyślną.

Jeśli funkcja zawiera parametry zarówno wymagane, jak i opcjonalne, wszystkie parametry wymagane
są używane jako pierwsze, a po nich następują parametry opcjonalne.

Odejmowanie słowników
Znajdowanie w książce słów, których nie ma na liście słów pochodzących z pliku words.txt, stanowi
problem, jaki można określić mianem odejmowania słowników. Oznacza to, że wymagane jest
znalezienie wszystkich słów z pierwszego zbioru (słowa zawarte w książce), których nie ma w drugim
zbiorze (słowa z listy).

Odejmowanie słowników I 167


Funkcja subtract pobiera słowniki dl i d2 oraz zwraca nowy słownik, który zawiera wszystkie klucze
ze słownika d l nieobecne w słowniku d2. Ponieważ tak naprawdę wartości nie mają znaczenia,
wszystkie będą wartością Bra k:
def s ubtract(d l, d2) :
r es = d i et()
for key in dl :
i f key not i n d2:
r es [key] = Bra k
return r es

Aby znaleźć słowa z książki, których nie ma w pliku words.txt, możesz użyć funkcji process_f i l e
do zbudowania dla pliku histogramu, a następnie wykonać operację odejmowania:
wo rds = process_ fil e( 'wo rds . t xt' )
di ff = s ubtract (hist , wo rds)

print("S'towa z książki, który ch ni e ma na liśc i e s 'tów: ")


fo r word i n diff :
pr int (wo rd , end=' ')

Oto wyniki dla książki Emma:


Słowa z k siąż ki , kt ó ryc h ni e ma na li śc i e s'tów:
r encontre jane's blanc he woodhous es dis i ngenuousness
f ri end ' s venice apartment ...

Część tych słów to imiona i zaimki dzierżawcze. Inne słowa, takie jak rencontre, nie są już powszechnie
używane w języku angielskim. Kilka słów to jednak typowe słowa angielskie, które naprawdę powinny
znaleźć się na liście!

Ćwiczenie 13.6.

Język Python oferuje strukturę danych o nazwie set, która umożliwia wiele typowych operacji na zbio-
rach. Na ich temat możesz przeczytać w podrozdziale „Zbiory" rozdzi ału 19. lub w dokumentacji
dostępnej pod adresem http://docs.python.org/3/library/stdtypes.html#types-set.

Utwórz program używający odejmowania zbiorów do znalezienia w książce słów, których nie ma
na liście słów.

Rozwiązanie: plik analyz e_book2.py.

Słowa losowe
Aby z histogramu wybrać słowo losowe, najprostszym algorytmem jest zbudowanie listy z wieloma
kopiami każdego słowa (zgodnie z zaobserwowaną częstością występowania), a następnie dokonanie
wyboru z listy:
de f random_word {h) :
t = []
f o r word , f req in h. items( ) :
t. ext e nd{ [wo rd] * freq )

r etur n r andom . c hoice(t)

168 I Rozdział 13. Analiza przypadku: wybór struktury danych


Wyrażenie [word] * freq tworzy listę z kopiami freq łańcucha word. Metoda ex t end przypomina
metodę append z tą różnicą, że argumentem jest ciąg.
Taki algorytm działa, ale nie jest zbyt efektywny. Każdorazowo w momencie wybrania słowa lo-
sowego ponownie budowana jest lista, która wielkością odpowiada treści oryginalnej książki.
Oczywistym usprawnieniem będzie jednokrotne zbudowanie listy, a następnie dokonanie wielu
wyborów. Lista jest jednak w dalszym ciągu pokaźna.
Oto alternatywa:
1. Użyj kluczy do uzyskania listy słów z książki.
2. Zbuduj listę zawierającą skumulowaną sumę częstości występowania słów (sprawdź ćwicze­
nie 10.2). Ostatni element na tej liście to łączna liczba n słów w książce.
3. Wybierz liczbę losową z zakresu od 1 do n. Użyj wyszukiwania z podziałem na połowę (sprawdź
ćwiczenie 10.10), aby znaleźć indeks, w którym liczba losowa zostałaby wstawiona w sumie
skumulowanej.
4 . Za pomocą indeksu znajdź odpowiednie słowo na liście słów.

Ćwiczenie 13.7.

Utwórz program używający tego algorytmu do wyboru słowa losowego z książki.


Rozwiązanie: plik analyze_book3.py.

Analiza Markowa
Jeśli słowa z książki wybierasz losowo, możesz uzyskać coś w rodzaju słownika, lecz prawdopo-
dobnie nie da to zdania:
this the sma ll regard harriet whi c h kni g htl ey' s it most things

Ciąg słów losowych rzadko ma sens z powodu braku powiązania pomiędzy kolejnymi słowami.
Na przykład w przypadku rzeczywistego zdania w języku angielskim oczekuje się rodzajnika, ta-
kiego jak the, po którym będzie występować przymiotnik lub rzeczownik, raczej nie czasownik
lub przysłówek.

Jednym ze sposobów oceny relacji między słowami jest analiza Markowa, która dla danego ciągu
słów określa prawdopo dobieństwo słów, jakie mogą się pojawić jako kolejne. Na przykład piosenka
Eric, the Half a Bee rozpoczyna się następująco:
Half a bee, philosophically,
M ust, ipso facto, half not be.
But half the bee has gat to be
Vis a vis, its entity. D'you see?

But can a bee be said to be


Or not to be an entire bee
When half the bee is not a bee
Due to same ancient injury?

Anallza Markowa I 169


W powyższym tekście po frazie half the zawsze następuje słowo bee, ale już po frazie the bee może
występować czasownik has lub is.

Wynikiem analizy Markowa jest odwzorowanie każdego prefiksu (np. half the i the bee) na wszystkie
możliwe sufiksy (np. has i is).

Gdy dysponujesz takim odwzorowaniem, możesz wygenerować tekst losowy, zaczynając od do-
wolnego prefiksu i wybierając losowo jeden z możliwych sufiksów. W dalszej kolejności możesz
połączyć koniec p refiksu z nowym sufiksem w celu utworzenia następnego prefiksu, po czym
powtórzyć operację.

J eśli
na przykład zaczniesz od prefiksu Half a, następnym słowem musi być rzeczownik bee, ponieważ
prefiks ten pojawia się w tekście tylko raz. Kolejny prefiks to a bee, dlatego następnym sufiksem
może być słowo philosophically, be lub due.

W przykładzie długość prefiksu zawsze wynosi dwa, ale możliwe jest przeprowadzenie analizy
Markowa z wykorzystaniem dowolnej długości prefiksu.

Ćwiczenie 13.8.

Analiza Markowa:
1. Utwórz program wczytujący tekst z pliku i przeprowadzający analizę Markowa. Wynikiem
powinien być słownik odwzorowujący prefiksy na kolekcję możliwych sufiksów. Kolekcja
może być listą, krotką lub słownikiem. To, jakiego dokonasz wyboru, zależy tylko od Ciebie.
Program możesz przetestować za pomocą długości prefiksu wynoszącej 2. Należy jednak napisać
program w taki sposób, by łatwo można było sprawdzić inne długości.
2. Dodaj do poprzedniego programu funkcję generującą tekst losowy na podstawie analizy Markowa.
Oto przykład tekstu z ksi ążki Emma z długością prefiksu wynoszącą 2:
He was very clever, be it sweetness or be angry, ashamed or only amused, at sucha stroke. She had
never thought of Hannah till you were never meant for me?" "I can not make speeches, Emma:"
he soon cut it all himself.
W tym przykładzie pozostawiłem znaki interpunkcyjne dołączone do słów. Pod względem skła­
dniowym wynik jest niemal poprawny, ale jednak nie do końca. Z semantycznego punktu widze-
nia przykład prawie ma sens, ale nie całkowicie.
Co się stanie, jeśli zostanie zwiększona długość prefiksu? Czy tekst losowy ma większy sens?
3. Gdy program zacznie dzi ałać, możesz spróbować kombinacji: jeśli połączysz tekst z dwóch
lub większej liczby książek, wygenerowany tekst losowy połączy słownik i frazy ze źródeł w intere-
sujący sposób.

Informacje o autorach: powyższa analiza przypadku bazuje na przykładzie z książki The Practice
of Programming napisanej przez Kernighana i Pike'a (Addison-Wesley, 1999).
Przed kontynuowaniem lektury książki należy podjąć próbę wykonania tego ćwiczenia. Później
możesz zobaczyć moje rozwi ązanie dostępne w pliku markov.py. Niezbędny będzie również plik
emma.txt.

170 I Rozdział 13. Analiza przypadku: wybór struktury danych


Struktury danych
Użycie analizy Markowa do generowania tekstu losowego jest przyjemne, ale też powiązane z ćwicze­
niem polegającym na wyborze struktury danych. W rozwiązaniu poprzednich ćwiczeń konieczne
było wybranie:

• Sposobu reprezentowania prefiksów.


• Sposobu reprezentowania kolekcji możliwych sufiksów.
• Sposobu reprezentowania odwzorowania każdego prefiksu na kolekcję możliwych sufiksów.

Ostatni z podanych punktów nie stanowi problemu: słownik to oczywisty wybór w przypadku
odwzorowywania kluczy na odpowiednie wartości.
Dla prefiksów najbardziej oczywiste opcje to łańcuch, lista łańcuchów lub krotka łańcuchów.

Odnośnie do sufiksów jedną opcją jest lista, a drugą histogram (słownik).

W jaki sposób należy dokonać wyboru? Pierwszym krokiem jest zastanowienie się nad operacjami, ja-
kie będą niezbędne do zaimplementowania każdej struktury danych. W przypadku prefiksów ko-
nieczna będzie możliwość usunięcia słów z początku i dodania ich do końca. Jeśli na przykład bieżący
prefiks to Half a, a następnym słowem jest bee, wymagana będzie możliwość utworzenia kolejnego
prefiksu a bee.
Pierwszą opcją wyboru może być lista, ponieważ z łatwością można dodawać do niej elementy i usu-
wać je zniej. Konieczna może być też jednak możliwość użycia prefiksów jako kluczy w słowniku,
co eliminuje listy. W przypadku krotek nie możesz wykonywać operacji dołączania ani usuwania,
ale masz możliwość zastosowania operatora dodawania w celu utworzenia nowej krotki:
def 5hift(prefix, word) :
ret urn pref i x[l:] + (word, )

Funkcja sh if t pobiera krotkę słów, prefiks pref i x i łańcuch wo rd, a następnie tworzy nową krotkę,
która zawiera wszystkie słowa z prefiksu, z wyjątkiem pierwszego, a także słowa wo r d dodanego do
końca.

W przypadku kolekcji sufiksów operacje niezbędne do wykonania obejmują dodawanie nowego


sufiksu (łub zwiększanie częstości występowania już istniejącego} i wybieranie sufiksu losowego.

Dodawanie nowego sufiksu jest równie proste w przypadku implementacji listy i histogramu.
Wybór elementu losowego z listy jest łatwą operacją. Trudniejsze jest dokonanie efektywnego
wyboru z histogramu (zajrzyj do ćwiczenia 13.7).

Do tej pory była mowa głównie o łatwości implementacji. W przypadku wybierania struktur danych
rozważyć należy jednak inne kwestie. Jedna z nich to środowisko uruchomieniowe. Czasami występuje
teoretyczny powód nakazujący oczekiwać, że jedna struktura danych będzie szybsza od innej. Wspo-
mniałem na przykład, że operator i n jest szybszy w przypadku słowników niż list (przynajmniej
wtedy, gdy liczba elementów jest duża}.

Często jednak nie wiesz z góry, która implementacja będzie szybsza. Jedną z opcji jest zastosowa-
nie obu implementacji i przekonanie się, która jest lepsza. Takie rozwiązanie nosi nazwę analizy

Struktury danych I 171


porównawczej. Praktyczną alternatywą jest wybranie struktury danych najłatwiejszej do zaim-
plementowania, a następnie sprawdzenie, czy jest ona wystarczająco szybka w przypadku planowane-
go zastosowania. Jeśli tak, nie ma potrzeby kontynuowania sprawdzania. W przeciwnym razie istnieją
narzędzia, takie jak moduł prof i le, które umożliwiają identyfikację w programie miejsc o najdłuższym
czasie wykonywania.

Inną kwestią do rozważenia jest miejsce do przechowywania. Na przykład użycie histogramu na po-
trzeby kolekcji sufiksów może wymagać mniej miejsca, ponieważ niezbędne jest tylko jednokrotne
zapisanie każdego słowa, niezależnie od tego, ile razy pojawia się w tekście. W niektórych sytuacjach
oszczędność miejsca pozwala też na przyspieszenie działania programu. W ekstremalnym przypadku
program w ogóle może nie zadziałać, jeżeli zabraknie pamięci. Jednakże w większości sytuacji miejsce
to druga co do ważności kwestia po środowisku uruchomieniowym.

I jeszcze jedna myśl: w przedstawionym omówieniu zasugerowałem, że należy zastosować jedną


strukturę danych zarówno na potrzeby analizy, jak i generowania. Ponieważ jednak są to osobne fazy,
możliwe byłoby też użycie jednej struktury do analizy, a następnie przekształcenie jej w kolejną
strukturę w celu przeprowadzenia operacji generowania. Byłby to pełny sukces, gdyby czas zaosz-
czędzony podczas generowania był dłuższy od czasu przekształcania.

Debugowanie
Podczas debugowania programu, a zwłaszcza w czasie zajmowania się trudnym do usunięcia błę­
dem, należy wypróbować pięć następujących czynności:

Czytanie
Sprawdź kod, przeczytaj go ponownie i upewnij się, że jego znaczenie jest zgodne z oczekiwaniami.
Uruchamianie
Poeksperymentuj, wprowadzając zmiany i uruchamiając różne wersje kodu. Jeśli wyświetli sz
właściwą rzecz we właściwym miejscu programu, problem stanie się oczywisty. Czasami jednak
konieczne jest skorzystanie z metody budowania aplikacji określanej mianem scaffoldingu.
Rozmyślanie

Poświęć trochę czasu na przemyślenia! Jakiego rodzaju błąd wystąpił: składniowy, urucho-
mieniowy czy semantyczny? Jakie informacje możesz uzyskać z komunikatów o błędzie lub
z danych wyjściowych programu? Jakiego rodzaju błąd mógł spowodować zaistniały pro-
blem? Co zostało zmienione jako ostatnie przed pojawieniem się problemu?

Użycie metody gumowej kacz usz ki


Jeśli objaśniasz problem komuś innemu, czasami udaje Ci się znaleźć odpowiedź przed zakończe­
niem zadawania pytania. Często nie potrzeba do tego drugiej osoby. Możesz po prostu mówić
do gumowej kaczuszki. Właśnie stąd wzięła się nazwa dobrze znanej strategii określanej mianem
debugowania z użyciem gumowej kaczuszki. Niczego tutaj nie zmyślam. Więcej informacji
znajdziesz pod adresem https://pl. wikipedia.org/wiki/Metoda__gumowej_kacz uszki.

172 I Rozdział 13. Analiza przypadku: wybór struktury danych


Wycofywanie się
W pewnym momencie najlepszą rzeczą, jaką możesz zrobić, jest cofnięcie się i wycofywanie
ostatnio wprowadzonych zmian do momentu ponownego uzyskania działającego i zrozumiałego
programu. Wtedy możesz znów zacząć proces budowania.

Początkujący programiści kończą na jednej z powyższych czynności, zapominając o innych. Z każdą


z tych czynności wiąże się jej własny tryb niepowodzenia.

Na przykład czytanie kodu może okazać się pomocne, gdy problem wynika z błędu typograficznego.
Nie będzie tak jednak, jeśli problem spowodowany jest niezrozumieniem zagadnienia. Jeżeli nie
rozumiesz działania programu, możesz przeczytać jego kod 100 razy i nigdy nie zauważysz błędu,
ponieważ „tkwi" on w Twojej głowie.

Pomocne może być przeprowadzanie eksperymentów, zwłaszcza wtedy, gdy uruchamiasz niewielkie
i proste testy. Jeśli jednak korzystasz z eksperymentów, lecz nie zastanawiasz się nad kodem ani go nie
czytasz, możesz paść ofiarą schematu nazywanego przeze mnie programowaniem z wybiórczym
sprawdzaniem. Jest to proces polegający na wprowadzaniu losowych zmian do momentu zapewnienia
poprawnego działania programu. Nie trzeba dodawać, że taki proces jest czasochłonny.
Musisz się na spokoj nie zastanowić. Debugowanie przypomina naukę doświadczalną. Należy zdefi-
niować co najmniej jedną hipotezę dotyczącą tego, na czym polega problem. Jeśli istnieją dwie lub
wi ększa liczba możliwości, spróbuj po myśleć o teście, który wyeliminowałby jedną z nich.

Jednakże nawet najlepsze techniki debugowania zawiodą, jeśli występuje zbyt wiele błędów lub w sytuacji,
gdy kod, który próbujesz p oprawić, jest zbyt obszerny i złożo ny. Czasami najlepszą opcją jest wy-
cofanie się, co sprowadza się do upraszczania programu do momentu uzyskania czegoś, co dzi ała
i jest zrozum iałe.
Początkujący programi ści często niechętnie rezygnują, po nieważ nie mogą się pogodzić z usunię­
ciem jednego wiersza kodu (nawet wtedy, gdy jest niepoprawny). Jeśli dzięki temu lepiej się poczujesz,
skopiuj kod programu do innego pliku przed rozpoczęciem usuwania z niego wierszy. Możesz
następnie z powrotem kopi ować wiersze kodu po jednym naraz.

Znajdowanie trudnego do wykrycia błędu wymaga czytania, uruchamiania, rozmyślania, a czasami


wycofywania. Jeśli w przypadku jednej z tych czynności pojawi się kłopot, spróbuj wykonać inne.

Słownik
de term in istyczne
Powiązane z programem realizującym to samo działanie przy każdym uruchomieniu, gdy podano
identyczne dane wejściowe.
pseudolosowe
Powiązane z ciągiem liczb, które wydają się losowe, ale zostały wygenerowane przez program
deterministyczny.
wartość domyślna

Wartość przypisywana parametrowi opcjonalnemu, jeśli nie podano żadnego argumentu.

Słownik I 173
nadpisywanie
Zastępowanie wartości domyślnej argumentem.

analiza porównawcz a
Proces wybierania spośród struktur danych polegający na implementowaniu alternatyw i te-
stowaniu ich za pomocą próbki możliwych danych wejściowych.

debugowanie z użyciem gumowej kacz uszki


Debugowanie polegające na objaśnianiu problemu nieożywionemu obiektowi, takiemu jak
gumowa kaczuszka. Artykułowanie problemu może ułatwić jego rozwiązanie nawet wtedy,
gdy kaczuszka nie zna języka Python.

Ćwiczenia
Ćwiczenie 13.9.

Ranga słowa to jego pozycja na liście słów sortowanych według częstości występowania. Najczę­
ściej występujące słowo ma rangę 1, drugie z kolei ma rangę 2 itd.
Prawo Zipfa opisuje relację między rangami i częstościami występowani a słów w j ęzykach natu-
ralnych (https://pl. wikipedia.org/ wiki/Prawo_Zipfa). Dokładni ej rzecz ujmując, prawo to przewi-
duje, że częstośćf słowa o randze r wynosi:
f =er-'
si r to parametry zależne od języka i tekstu. W przypadku zastosowania logarytmu dla obu stron
tego równania uzyska się następujące równanie:
logf =log c-s log r
A zatem jeśli zostaną p orównane logarytmy logf i log r, powinniśmy uzyskać linię prostą o na-
chyleniu-si punkt przecięcia log c.
Ko rzystającz logarytmów logf i log r, utwórz program wczytujący tekst z pliku, określający częstości
występowania słów i wyświetlający jeden wiersz dla każdego słowa w kolejności zgodnej z malejącą
częstością występowania . Za pomocą wybranego programu obsługującego wykresy wyświetl wy-
niki na wykresie i sprawdź, czy tworzą one linię prostą. Czy m ożesz oszacować wartość s?

Rozwiązanie: plik z ipfpy. Aby uruchomić moje rozwiązanie, musisz skorzystać z modułu wykresów
mat plot l i b. Jeśli zainstalowałeś środowisko Anaconda, dysponujesz już tym modułem. W przeciwnym
razie konieczna będzi e instalacja tego środowiska.

174 I Rozdział 13. Analiza przypadku: wybór struktury danych


ROZDZIAŁ 14.

Pliki

W tym rozdziale zaprezentowałem pojęcie programów „tnvałych'', które przechowują dane w trwa-
łym magazynie, a także wyjaśniłem, jak używać różnego rodzaju trwałych magazynów, takich jak
pliki i bazy danych.

Trwałość
Większość dotychczas przedstawionych programów jest „przejściowa" w tym sensie, że działają przez
krótki czas i generują pewne dane wyjściowe. Gdy jednak zostaną zakończone, ich dane znikają.
Jeśli ponownie uruchomisz program, rozpoczyna pracę bez danych.

Inne programy są trwałe: działają przez długi czas (lub nieustannie), przynajmniej część swoich da-
nych utrzymują w magazynie tnvałym (takim jak na przykład dysk twardy), a jeśli zostaną zamknięte
i ponownie załadowane, kontynuują działanie od miejsca, w którym ostatnio zakończyły pracę.

Przykładami programów „trwałych" są systemy operacyjne, które działają naprawdę intensywnie


zawsze po włączeniu komputera, a także serwery WWW pracujące cały czas i oczekujące na po-
jawienie się żądań z sieci.

Jednym z najprostszych sposobów utrzymywania przez programy swoich danych jest odczytywanie
i zapisywanie plików tekstowych. Zaprezentowałem już programy wczytujące pliki tekstowe. W tym
rozdziale poznasz programy dokonujące zapisu tych plików.

Alternatywą jest zapamiętanie stanu programu w bazie danych. W tym rozdziale przedstawiłem
prostą bazę danych i moduł pi c kl e, który ułatwia zapisywanie danych programu.

Odczytywanie i zapisywanie
Plik tekstowy to ciąg znaków zapisanych na nośniku trwałym, takim jak dysk twardy, pamięć Flash lub
dysk CD-ROM. W podrozdziale „Odczytywanie list słów" rozdziału 9. pokazałem, jak otwierać i od-
czytywać plik.

Aby zapisać plik, musisz otworzyć go z trybem w jako drugim parametrem:


>>> f a ut = o pen( 'output . t xt ', 'w' )

175
Jeśliplik już istnieje, otwieranie go w trybie zapisu powoduje usunięcie starych danych i rozpoczęcie
bez żadnych danych, dlatego zachowaj ostrożność! Jeżeli plik nie istnieje, tworzony jest nowy plik.
Metoda open zwraca obiekt pliku, który zapewnia metody służące do pracy z plikiem. Metoda wri te
umieszcza dane w pliku:
»> 1 i nel = "A t utaj jest akac j a, \n"
>>> fout.write( l inel)
24

Wartość zwracana to liczba zapisanych znaków. Obiekt pliku śl edzi lokalizację znaków, dlatego
w przypadku ponownego wywo łania metoda wri te dodaje nowe dane na końcu pliku:
>>> 1 i ne2 = " godfo naszego kraj u. \n"
>>> fout.write( l ine2)
24

Po zakończeniu zapisu należy zamknąć plik:


>>> fout.close( )

Jeśli plik nie zostanie zamknięty, nastąpi to samoczynnie w momencie zakończenia pracy programu.

Operator formatu
Argument metody wri te musi być łańcuchem, dlatego w sytuacji, gdy w pliku mają zostać umieszczo-
ne inne wartości, muszą one zostać przekształcone w łańcuchy. Najprościej w tym celu zastosować
metodę str:

>>> X = 52
>>> fout.write(str(x))

Alternatywą jest użycie operatora formatu %. W przypadku zastosowania go dla liczb całkowitych od-
grywa on rolę operatora modulo. Gdy jednak pierwszym argumentem jest łańcuch, %jest operatorem
formatu.
Pierwszym argumentem jest łańcuch formatu, który zawiera jeden lub większą liczbę ciągów
formatu, które określają sposób sformatowania drugiego argumentu. Wynikiem jest łańcuch.

Na przykład ciąg formatu ' %d ' oznacza, że drugi argument powinien zostać sformatowany jako
dziesiętna liczba całkowita:
>>> camel s = 42
>>> '%d % camels
1

'42'

Wynikiem jest łańcuch ' 42 ', którego nie należy mylić z liczbą całkowitą 42.

Ciąg formatu może pojawić się gdziekolwiek w łańcuchu, dlatego możesz osadzić wartość w zdaniu:
>>> ' Dostrzegfem wielbfądy w l iczb ie %d .' % camels
'Dostrzegfem wi e lb fądy w li czbie 42.'

Jeśli w łańcuchu występuje więcej niż jeden ciąg formatu, drugi argument musi być krotką. Każdy
ciąg formatu dopasowywany jest do kolejnego elementu krotki.

176 I Rozdział 14. Pllkl


W następuj ącym przykładzi e użyto ciągu form atu ' %d ' do sformatowania liczby całkowi tej, ciągu
formatu ' %g ' do sformatowania liczby zmiennoprzecinkowej, a ciągu formatu ' %s ' do sformato-
wania łańcucha:
»> ' W c i ągu %d lat dostrzegł em %g %s . ' % (3 , 0. 1, 'w ie lbłądów')
' W ciągu 3 lat dos t rzeg łem 0.1 w ie lb ł ądów .'

Liczba elementów w krotce musi być zgodna z liczbą ciągów formatu w łańcuchu. Ponadto typy
elementów muszą być dopasowane do ciągów fo rmatu:
1
>>> %d %d %d 1 % {l, 2 )
TypeE rror : not enough arguments f or f ormat string
>>> 1 %d 1 % 1 dol ary 1
TypeE rror : %d f ormat : a numbe r i s r equired, not str

W pierwszym przykładzie nie ma wystarczającej liczby elementów, a w drugim element ma niepo-


prawny typ.
Więcej informacji na temat operatora formatu dostępnych jest pod adresem https://docs.python.org/31
library/stdtypes.html-printf-style-string-formatting. Alternatywą o większych możliwości ach jest meto-
da fo rmatująca łańcuchy, na temat której możesz przeczytać pod adresem https://docs.python.org/31
library/stdtypes.html-str format.

Nazwy plików i ścieżki


Pliki uporządkowane są w ramach katalogów (nazywanych również fo lderami). Każdy działaj ący
program ma katalog bieżący, który w przypadku większości operacj i odgrywa rolę katalogu do-
myślnego. Gdy na p rzykład otwierasz plik do odczytu, interpreter języka Python szuka go w katalogu
bieżącym.

Moduł os zapewnia funkcje pozwalające na pracę z plikami i katalogami (os to skrót od słów operating
system). Funkcja os .get cwd zwraca nazwę katalogu bieżącego :
>>> import os
> >> cwd = os . getcwd()
> >> cwd
' / home/jnowa k'

cwd to skrót od słów current working directory. W tym p rzykładzie wynikiem jest katalog
/ home/jnowak, który jest katalogiem domowym użytkownika j nowak.

Ła ńcuch, taki jak ' / home/j nowak ' , który identyfikuje plik lub katalog, nosi nazwę ścieżki.
Prosta nazwa pliku, taka jak memo.txt, również uważana jest za ścieżkę, ale ścieżkę względną, ponie-
waż powi ązana jest z katalogiem bieżącym . Jeśli katalog bieżący to /home/jnowak, nazwa pliku
memo.txt będzie odwoływać się do pliku !home/jnowak/ m emo.txt.

Ścieżka rozpoczynająca się znakiem / nie jest zależna od katalogu bieżącego. Określana jest ona mia-
nem ścieżki bezwzględnej. Aby ustalić ścieżkę bezwzględną do pliku, m ożesz sko rzystać z funkcji
os . path .abspath:
>>> os . path . abspath('memo . t xt')
'/ home/jnowa k/ memo . t xt '

Nazwy plików I ~cleikl I 177


Moduł os . pat h zapewnia inne funkcje służące do pracy z nazwami plików i ścieżkami. Na przy-
kład funkcja os . pat h. exi sts sprawdza, czy ist nieje plik łub katalog:

>>> os . pat h. exists('memo . t xt')


True

Jeśli obiekt istnieje, funkcja os . path . i sdi r sprawdza, czy jest on katalogiem:
>>> os . pat h. isd i r('memo . t xt')
Fal se
>>> os . pat h. isd i r('/home/jnowak')
True

W podobny sposób funkcja os . path. i sf i 1e sprawdza, czy obiekt jest plikiem.

Funkcja os . l i st di r zwraca listę plików (oraz innych katalogów) w danym katalogu:


>>> os . l istdir( cwd)
[ ' muzyka' , 'zdj ee i a' , 'memo . t xt' ]

W celu zademonstrowania tych funkcji w poniższej przykładowej funkcji wykonano operację przejścia
katalogu i wyświetlono nazwy wszystkich plików. Ponadto funkcja wywołuje się rekurencyjnie w przy-
padku wszystkich katalogów.
def walk (dirname) :
for name in os . listdir(dirname) :
path = os . pat h. jo in (dirname , name)

if os . path . is f i l e ( path) :
print (pat h)
else :
walk(path)

Funkcja os . pat h. j oi n pobiera katalog i nazwę pliku, a następnie łączy je, zap ewniając kompletną
ścieżkę.

Moduł os oferuje funkcję o nazwie wal k podobną do powyższej, lecz bardziej wszechstronną. W ra-
mach ćwiczenia przeczytaj dokumentację tej funkcji i użyj jej do wyświetlenia nazw plików w danym
katalogu oraz jego podkatalogach. Moje rozwiązanie możesz znaleźć w pliku walk.py, który, tak jak
i pozostałe pliki z tego rozdziału, jest dostępny pod adresem ftp:l/ftp.helion.pl!p rzyklady/ myjep2.zip.

Przechwytywanie wyjątków
Przy próbie odczytywania i zapiS)"vania plików może się nie powieść mnóstwo rzeczy. Jeśli spróbujesz
o two rzyć
plik, który nie istnieje, uzyskasz błąd IOE rror:
>>>fi n = open( ' bad_f ile')
IOError : [E rrno 2] No s uch file or directory : ' bad_file '

Jeśli nie dysponujesz uprawnieniem pozwalającym na dostęp do pliku, zostanie wyświetlo ny na-
stępujący błąd:
>>> fout = open('/etc/passwd' , 'w ')
Pe rmissi onError : [E rrno 13] Permission denied : ' /etc/passwd '

178 I Rozdział 14. Pllkl


Jeżeli podejmiesz próbę wyświetlenia katalogu w celu odczytania jego zawartości, uzyskasz nastę­
pujący błąd:
>>>fin = open('/home' )
l sADirectoryError : [Errno 21] Is a directory : '/ home '

Aby uniknąć takich błędów, możesz skorzystać z funkcji takich jak os. pat h .ex i sts i os . pat h . i s fi 1e.
Sprawdzenie wszystkich możliwości zajmie jednak wiele czasu i będzie wymagać sporej ilości kodu (je-
śli komunikat Errno 21 stanowi jakąkolwiek podpowiedź, istnieje co najmniej 21 rzeczy, które
mogą się nie powieść).

Lepszym rozwiązaniem jest kontynuowanie wykonywania kodu i sprawdzanie go (oraz zajmowanie


się problemami, gdy się pojawią). Właśnie do tego służy instrukcja t ry. Jej składnia przypomina skład­
nię instrukcji i f . . . e 1se:

t ry :
fin = open('bad_file')
ex ce pt :
print ( 'Coś się nie powiodło.')

Interpreter języka Python zaczyna pracę od wykonania klauzuli t ry. Jeśli wszystko się powiedzie, po-
mija klauzulę except i kontynuuje działanie. Jeżeli wystąpi wyjątek, następuje wyjście z klauzuli try
i uruchomienie klauzuli exce pt.
Obsługa wyjątku za pomocą instrukcji t ry nazywana jest przechwytywaniem wyjątku. W powyższym
przykładzieklauzula except wyświetla niezbyt pomocny komunikat o błędzie. Ogólnie rzecz biorąc,
przechwytywanie wyjątku zapewnia szansę usunięcia problemu, ponowienia próby lub przy-
najmniej zakończenia programu w p oprawny sposób.

Bazy danych
Baza danych to plik zorganizowany pod kątem przechowywania danych. Wiele baz danych zorgani-
zowanych jest podobnie do słownika w tym sensie, że o dwzorowują klucze na wartości. Największą
różnicą między bazą danych i słownikiem jest to, że baza danych znajduje się na dysku (lub w innym
magazynie trwałym), dlatego istnieje nadal po zakończeniu działania programu.

Moduł dbm zapewnia interfejs służący do tworzenia i aktualizowania plików bazy danych. W ramach
przykładu utworzę bazę danych, która zawiera nagłówki plików obrazów.
Otwieranie bazy danych przypomina otwieranie innych plików:
>» import dbm
>>>db = dbm . open('captions' , 'c')

Tryb ' c' oznacza, że baza danych powinna zostać utworzona, jeśli jeszcze nie istnieje. Wynikiem jest
obiekt bazy danych, który może być używany (w przypadku większości operacji) podobnie do słownika.
W momencie tworzenia nowego elementu moduł dbmaktualizuje plik bazy danych:
>>> db ( 'c leese . png '] = 'Z djęcie Johna Cleese ' a . '

Przy uzyskiwaniu dostępu do jednego z elementów moduł dbm odczytuje plik:


>>> db ( 'c leese . png ']
b' Zdjęcie Johna Cleese'a . '

Bazy danych I 179


Wynikiem jest obiekt bajtów. Z tego właśni e powodu na początku obiektu znajduje się litera b.
Pod wieloma względam i obiekt bajtów przypomina łańcuch. Gdy bardziej zagłębisz się w język
Python, różnica stanie si ę istotna, ale na razie można ją zignorować.

W przypadku utworzenia kolejnego przypisania do istniejącego klucza moduł dbm zastępuje sta rą
wartość:

>> > db [ 'c lees e. png' ] = 'Z dj ę c i e Johna Cl ees e' a c hod ząc ego w dziwny sposób . '
>> > db [ 'c lees e. png' ]
b ' Z djęci e Johna Clees e' a chod ząc ego w dzi wny sposób . '

Niektóre metody słownikowe, takie jak key s i i t ems, nie współp racuj ą z obiektami bazy danych.
Możliwa jest jednak iteracja z wykorzystaniem pętli for:
for key i n db :
print (key, db [key] )

Jak w przypadku innych plików, po zakończeniu pracy należy zamknąć bazę danych:
»> db . cl ose()

Użycie modułu piekle


Ograniczeniem modułu dbm jest to, że klucze i wartości muszą być łańcuch ami lub bajtami. Jeśli
spróbujesz użyć dowolnego innego typu, uzyskasz błąd.

Pomocny może si ę okazać moduł pie kl e. Dokonuje on t ranslacji obiektu prawie każdego typu na
łańcuch odpowiedni do p rzechowywania w bazie danych, a następnie z powrotem zamienia łań­
cuchy na obiekty.
Funkcja pi e kl e .dumps pobiera obiekt jako parametr i zwraca reprezentację łańcuchową {d umps to
skrót od słów dump string):
>>> import pie kl e
»> t = [ 1, 2. 3]
>>> pic kle. dumps(t)
b' \x80\x03]q\xOO( K\ x0 1K\x02K\x03e .'

Format nie jest oczywisty dla użytkowników. Został tak pomyślany, aby był łatwy do zinterpreto-
wania przez moduł pie kl e . Funkcja p i e kl e . loads {l oads to skrót od słów load string) ponownie
tworzy obiekt:
» > tl = [ 1, 2 , 3]
>>> s = pic kle. dumps(t l )
>>> t2 = pi ckl e .l oads(s)
>>> t2
[ 1, 2. 3]

Choć nowy obiekt ma wartość identyczną z wa rtością starego, nie jest to (na ogół) ten sam obiekt:
»> tl t2
Tr ue
»> tl i s t2
Fa l s e

Inaczej mówiąc, użycie modułu pie kl e , a następnie wykonanie operacji odwrotnej do operacji
przez niego realizowanej daje taki sam efekt jak kopiowanie obiektu.

180 I Rozdział 14. Pllkl


Modułu pi e kl e możesz użyć do przechowywania w bazie danych innych niż łańcuchy. Okazuje
się, że taka kombinacja jest na tyle powszechna, że została uwzględniona w module o nazwie she l ve.

Potoki
Większość systemów operacyjnych zapewnia interfejs wiersza poleceń, który nazywany jest też po-
włoką. Powłoki oferują zwykle polecenia umożliwiające nawigację w obrębie systemu plików i uru-
chamianie aplikacji. Na przykład w systemie Unix zmiana katalogów jest możliwa za pomocą polece-
nia cd, wyświetlanie zawartości katalogu przy użyciu polecenia l s, a uruchamianie przeglądarki
internetowej następuje przez wpisanie polecenia, na przykład f i r efox.
Dowolny program, który możesz wywo łać z poziomu powłoki , może też zostać uruchomiony
przez interpreter j ęzyka Python po zastosowaniu obiektu potoku reprezentującego działający
program.
Na przykład polecenie Is - l systemu Unix standardowo powoduje wyświetlenie zawartości katalogu
bieżącego w formacie rozszerzonym. Polecenie l s możesz wykonać za pomocą funkcji o s . popen 1:
>>> cmd = ' l s - 1 '
>>> f p = os . popen(cmd)

Argu ment jest łańcuchem zawierającym polecenie powłoki. Wa rtość zwracana to obiekt, który
zachowuje się podobnie do otwartego pliku. Dane wyjściowe procesu polecenia l s możesz wczy-
tywać za pomocą funkcji read l i ne po jednym wierszu naraz lub pobrać od razu cało ść przy użyciu
funkcj i read:
>>>res = f p. read()

Po zakończeniu pracy potok jest zamykany podobnie jak plik:


>>> stat = f p. close()
»> print(stat)
None

Wartością zwracaną jest końcowy status procesu polecenia l s. None oznacza, że zakończył się on
w normalny sposób (bez żadnych błędów).

Na przykład większość systemów uniksowych zapewnia polecenie o nazwie md5sum, któ re wczytuje za-
wartość pliku i oblicza sumę kontrolną. Na temat algorytmu MDS możesz przeczytać pod adre-
sem http://pl. wikipedia.org/ wiki!MDS. Polecenie md5s um oferuje efektywny sposób sprawdzania,
czy dwa pliki mają taką samą zawartość. Prawdopodobieństwo tego, że różne zawartości zapewnią
identyczną sumę kont rolną, jest bardzo małe (czyli raczej do tego nie dojdzie, zanim wszechświ at
przestanie istnieć).

Za pomocą potoku możesz uruchomić polecenie md5sum z poziomu interpretera języka Python i uzy-
skać wynik:

1
Funkcja popen nie jest j uż zaleca na. O znacza to, że wskaza ne jest zap rzes tanie korzystania z niej i rozpoczęcie używania
modu łu s ubprocess. Uważam jednak, że w prostych p rzypadkach moduł ten jest bardziej złoż ony, niż jest to konieczne.

A zatem będę korzystać z funkcji popen do moment u, aż zostan ie usunięta.

Potoki I 181
>>> filename = ' book. tex '
>>> cmd = 'md 5sum ' + f ilename
>>> fp = os . popen(cmd)
>>> res = fp . read{)
»> stat = fp. close ()
»> print (res)
le0033 f0ed0656636de0d75144 ba32e0 book. tex
»> print (stat)
None

Zapisywanie modułów
Dowolny plik zawierający kod Python może zostać zaimportowany jako moduł. Dla przykładu załóż­
my, że istnieje plik o nazwie wc.py z następującym kodem:
def linecount(fi lename) :
count = O
for l ine in open ( f il ename) :
count += 1
return count
print ( l i necount ( 'wc . py' ) )

Po uruchomieniu program samoczynnie się wczyta i wyświetli liczbę wierszy w pliku, których jest 7.
Plik może też zostać zaimportowany w następujący sposób:
>>> import wc
7

W efekcie uzyskuje się obiekt modułu we:


>>> WC
<modul e ' we' f rom ' wc. py'>

Obiekt modułu zapewnia funkcję li necount :


>>> wc. linecount('wc.py')
7

Właśnie w taki sposób zapisywane są moduły w języku Python.


W przypadku tego przykładu jedynym problemem jest to, że w momencie importowania modułu
na końcu uruchamia on kod testowy. Standardowo podczas importowania modułu definiuje on
nowe funkcje, lecz ich nie uruchamia.

Programy, które będą importowane jako moduły, często korzystają z następuj ącego idiomu:
if name ma i n ' :
print(linecount( 'wc . py' ))

_ name_ to zmienna wbudowana ustawiana w momencie uruchamiania programu. Jeśli program


działajako skrypt, zmienna _ name_ma wartość '_mai n_ '. W tym przypadku wykonywany jest kod
testowy. W przeciwnym razie, jeśli moduł jest importowany, kod testowy jest pomijany.

W ramach ćwiczenia wpisz kod powyższego przykładu w pliku o nazwie wc.py i uruchom go jako skrypt.
Załaduj następnie interpreter języka Python i wykonaj polecenie i mport we. Jaka jest wartość zmiennej
_ name_ ,gdy moduł jest importowany?

182 I Rozdział 14. Pllkl


Ostrzeżenie: jeśli importujesz moduł, który został już zaimportowany, interpreter języka Python
nie podejmuje żadnego działania. Nie wczytuje ponownie pliku nawet wtedy, gdy został zmodyfi-
kowany.

Aby ponownie załadować moduł, możesz skorzystać z funkcji wbudowanej r el oad. Użycie jej może
być utrudnione, dlatego najbezpieczniejszym rozwiązaniem będzie zrestartowanie interpretera, a na-
stępnie ponowne zaimportowanie modułu.

Debugowanie
Podczas wczytywania i zapisywania plików mogą pojawić się problemy ze znakiem białej spacji. Debu-
gowanie tego rodzaju błędów może być trudne, ponieważ standardowo spacje, znaki tabulacji i znaki
nowego wiersza są niewidoczne:
>>> s = ' 1 2\t 3\n 4'
»> print(s)
1 2 3
4

Pomocna może okazać się funkcja wbudowana repr. Pobiera ona dowolny obiekt jako argument
i zwraca reprezentację łańcuchową obiektu. W przypadku łańcuchów funkcja reprezentuje białe
znaki za pomocą ciągów złożonych ze znaku\:
>>> print(repr( s))
'l 2\t 3\n 4'

Może to być przydatne podczas debugowania.

Innym problemem, z jakim możesz się spotkać, jest to, że różne systemy korzystają z różnych znaków
do wskazania końca wiersza. N iektóre systemy używają znaku nowego wiersza reprezentowanego
jako \n. Inne stosują znak powrotu w postaci \ r. Część systemów korzysta z obu tych znaków. Jeśli
przenosisz pliki między różnymi systemami, takie niespójności mogą być przyczyną problemów.

W większości systemów dostępne są aplikacje dokonujące konwersji z jednego formatu na drugi.


Aplikacje te wyszczególniono na stronie (zawiera ona też więcej informacji na ten temat) dostępnej
pod adresemhttp://pl.wikipedia.org/wiki/Koniec_linii. Oczywiście możesz utworzyć własną aplikację.

Słownik
trwałość

Dotyczy programu działającego bez ograniczenia czasowego, który zachowuje przynajmniej


część swoich danych w magazynie trwałym.

operatorfor1r1atu
Operator %, który pobiera łańcuch formatu i krotkę, a następnie generuje łańcuch uwzględ­
niający elementy krotki sformatowane zgodnie z łańcuchem formatu.

łańcuch for1r1atu

Łańcuch używany z operatorem formatu, który zawiera ciągi formatu.

Słown ik I 183
ciqgformatu
Ciąg znaków w łańcuchu formatu, taki jak %d, który określa sposób formatowania wartości.

plik tekstowy
Ciąg znaków przechowywany w magazynie trwałym, takim jak dysk twardy.
katalog
Nazwana kolekcja plików określana również mianem folderu.
ścieżka

Łańcuch identyfikujący plik.

ścieżka względna

Ścieżka rozpoczynająca się od katalogu bieżącego.

ścieżka bezwzględna

Ścieżka rozpoczynająca się od katalogu znajdującego się najwyżej w systemie plików.

przechwytywanie
Operacja oparta na instrukcjach try i except zapobiegająca zakończeniu programu przez wy-
jątek.

baz a danych
Plik, którego zawartość uporządkowano podobnie do zawartości słownika z kluczami odpo-
wiadającymi wartościom.

obiekt bajtów
Obiekt podobny do łańcucha.
powłoka

Program umożliwiający użytkownikom wpisanie poleceń, a następnie wykonujący je przez


uruchamianie innych programów.

obiekt potoku
Obiekt reprezentujący działający program, który umożliwia programowi Python uruchamia-
nie poleceń i wczytywanie wyników.

Ćwiczenia
Ćwiczenie 14.1.

Utwórz funkcję o nazwie sed, która jako argumenty pobiera łańcuch wzorca, łańcuch zastępujący
oraz dwie nazwy plików. Funkcja powinna wczytać pierwszy plik i zapisać jego zawartość w drugim
pliku (tworząc go, jeśli to konieczne). Jeśli łańcuch wzorca pojawi się w dowolnym miejscu w pliku,
powinien zostać zastąpiony przez łańcuch zastępujący.

184 I Rozdział 14. Pllkl


W przypadku wystąpienia błędu podczas otwierania, odczytywania, zapisywania lub zamykania pli-
ków program powinien przechwycić wyjątek, wyświetlić komunikat o błędzie i zakończyć działanie.
Rozwiązanie: plik sed.py.

Ćwiczenie 14.2.

Jeślipobierzesz moje rozwiązanie ćwiczenia 12.2 dostępne w pliku anagram_sets.py, zauważysz,


że tworzony jest w nim słownik odwzorowujący posortowany łańcuch liter na listę słów, które mogą
być uzyskane przy użyciu tych liter. Na przykład łańcuch ' opst ' odwzorowywany jest na listę [ ' opts ',
'post ', 'pots ', ' spot', ' stop', 'tops '] .

Utwórz moduł importujący program anagram_sets i zapewniający dwie nowe funkcje. Funkcja
store_anagrams powinna przechowywać słownik anagramów na „półce". Funkcja read_anagrams
powinna wyszukiwać słowo i zwracać listę jego anagramów.

Rozwiązanie: plik anagram_db.py.

Ćwiczenie 14.3.

W dużej kolekcji plików MP3 może się znajdować więcej niż jedna kopia tej samej piosenki przecho-
wywanej w różnych katalogach lub pod różnymi nazwami pliku. Celem tego ćwiczenia jest wyszuki-
wanie duplikatów.
1. Utwórz program przeszukujący rekurencyjnie katalog i wszystkie jego podkatalogi oraz zwracają­
cy listę kompletnych ścieżek wszystkich plików z podanym sufiksem (np. . mp3). Wskazówka:
moduł os . path zapewnia kilka przydatnych funkcji służących do modyfikowania nazw plików
oraz ich ścieżek.
2. W celu rozpoznania duplikatów możesz zastosować polecenie mdSsum obliczające sumę
kon-
tro l ną dla każdego pliku. Jeśli dwa pliki mają identyczną sumę kontrolną, prawdopodobnie
ich zawartość jest jednakowa.
3. Aby ponownie się upewnić, możesz użyć polecenia di ff systemu Unix.
Rozwiązanie: plik find_duplicates.py.

Ćwlaenla I 185
186 I Rozdział 14. Pllkl
ROZDZIAŁ 15.

Klasy i obiekty

Wiesz już, jak używać funkcji do uporządkowania kodu oraz typów wbudowanych do zapewnienia
organizacji danych. Następnym krokiem jest poznanie programowania obiektowego, w przypadku
którego typy definiowane przez programistę służą do organizacji zarówno kodu, jak i danych. Pro-
gramowanie obiektowe to szerokie zagadnienie. Zaznajomienie się z nim będzie wymagać lektury
kilku rozdziałów.
Przykładowy kod użyty w tym rozdziale znajdziesz w pliku Pointl.py, który, tak jak pozostałe pliki
z tego rozdziału, jest dostępny pod adresemftp:/lftp.helion.p//przyklady/myjep2.z ip. Rozwiązania ćwi­
czeń znajdziesz w pliku Pointl_soln.py.

Typy definiowane przez programistę


Skorzystaliśmy z wielu typów wbudowanych języka Python. Pora zdefiniować nowy typ. W ramach
przykładu utworzymy typ o nazwie Point, który reprezentuje punkt w przestrzeni dwuwymiarowej.
W notacji matematycznej punkty są często zapisywane w nawiasach okrągłych z przecinkiem od-
dzielającym współrzędne. Na przykład notacja (0,0) reprezentuje początek, a notacja (x,y) identy-
fikuje punkt oddalony od początku w prawą stronę o x jednostek oraz w górę o y jednostek.
Istnieje kilka sposobów reprezentowania punktów w kodzie Python:
• Współrzędne mogą być przechowywane osobno w dwóch zmiennych x i y.
• Współrzędne mogą być przechowywane jako elementy listy lub krotki.
• Możliwe jest utworzenie nowego typu do reprezentowania punktów jako obiektów.
Ostatnie wymienione rozwiązanie jest bardziej złożone od dwóch pozostałych opcji, ale zapewnia
korzyści,które wkrótce staną się widoczne.
Typ definiowany przez programistę nazywany jest też klasą. Definicja klasy ma następującą postać:
class Po i nt :
"""Repre:entuje punkt w pr:estr:eni dwuwymiarowej. "'"'

Nagłówek wskazuje, że nowa klasa nosi nazwę Po int. Treść to notka dokumentacyjna objaśniająca
przeznaczenie klasy. W obrębie definicji klasy możesz zdefiniować zmienne i metody, ale zajmiemy się
tym później.

187
Definiowanie klasy o nazwie Poi nt powoduje utworzenie obiektu klasy:
»> Point
<class ' main . Point'>

Ponieważ klasa Poi nt definiowana jest na najwyższym poziomie, jej „pełna nazwa" to _mai n_ . Poi nt.
Obiekt klasy jest jak fabryka tworząca obiekty. W celu utworzenia obiektu Poi nt wywołujesz klasę
Poi nt tak, jakby była funkcją:
>>>blank = Poi nt()
»> blank
<__ mai n_ .Point object at Oxb7e9d3ac>

Wartość zwracana jest odwołaniem do obiektu Poi nt przypisanego zmiennej blank.

Tworzenie nowego obiektu określane jest mianem tworzenia instancji, a obiekt to instancja klasy.
W momencie wyświetlenia instancji interpreter języka Python informuje, do jakiej klasy ona należy,
a także gdzie jest przechowywana w pamięci (prefiks Ox oznacza, że następująca po nim liczba jest
szesnastkowa).

Każdy obiekt jest instancją jakiejś klasy, dlatego terminy obiekt i instancja mogą być wymiennie
stosowane. W rozdziale używam jednak terminu instancja do wskazania, że mam na myśli typ
zdefiniowany przez programistę.

Atrybuty
Wartości możesz przypisać instancji za pomocą notacji z kropką:
>>>blank.X = 3. 0
>>> blank.y = 4. 0

Składnia ta przypomina składnię stosowaną przy wybieraniu zmiennej z modułu takiego jak mat h. pi
lub s t r i ng . whi tespace. W tym przypadku jednak wartości są przypisywane nazwanym elementom
obiektu. Elementy te są określane mianem atrybutów.
Na poniższym diagramie pokazano wynik takich przypi sań. Diagram stanu prezentujący obiekt
i jego atrybuty nosi nazwę diagramu obiektu (rysunek 15.1).

Point

LY--,_3_.o~
blank ---1. _x__
4.0

Rysunek 15.1. Diagram obiektu

Zmienna blank odwołuje się do obiektu Poi nt, który zawiera dwa atrybuty. Każdy atrybut odnosi
się
do liczby zmiennoprzecinkowej.

Wartość atrybutu możesz odczytać za pomocą identycznej składni:

»> blank.y
4. 0

188 I Rozdział 15. Klasy I obiekty


>>> x bl ank.X
>>> X
3. 0

Wyrażenie blan k.x oznacza: „Przejdź do obiektu, do którego odwołuje się zmienna blank, i uzyskaj
wartośćatrybutu x". W przykładzie wartość ta jest przypisywana zmiennej x. Nie występuj e konflikt
między zmienną x i atrybutem x.

Notacja z kropką może stanowić część dowolnego wyrażenia. Oto przykład:


»> '(%9 , %9)' % (blank.x, blank.y)
'(3 . O, 4 . 0)'
> >> distance ~ mat h. sq rt(bl ank.x**2 + bl ank.y**2 )
> >> distance
5. 0

W standardowy sposób możesz przekazać instancję jako argument. Oto przykład:


def print_point(p) :
print( ' {%9 , %9)' % (p .x, p.y ))
Funkcja pri nt_poi nt pobiera punkt jako argument i wyświetla go za pomocą no tacji matematycznej.
W celu wywołania funkcji możesz przekazać zmienną blan k jako argument:
> >> print_point( bl ank)
(3 . O, 4. 0)

Wewnątrz funkcji p stanowi alias zmiennej blank, dlatego w przypadku zmodyfikowania p przez
funkcję zmieniona zostanie też zmienna blan k.

W ramach ćwiczenia utwórz funkcję o nazwie dist ance_between_poi nts, która jako argumenty
pobiera dwa punkty i zwraca odległość między nimi.

Prostokąty
Czasami oczywiste jest, jakie powinny być atrybuty obiektu. Innym razem jednak konieczne bę­
dzie podjęcie decyzji. Wyobraź sobie na przykład, że projektujesz klasę reprezentującą prostokąty.
Jakich użyłbyś atrybutów do określenia położenia i rozmiaru prostokąta? Możesz zignorować kąt.
Dla uproszczenia przyjmijmy, że prostokąt jest ustawio ny albo pionowo, albo poziomo.

Istnieją co najmniej dwie możliwości:

• Możliwe jest określenie jednego narożnika prostokąta (lub jego środka), szerokości i wysokości.
• Możliwe jest określenie dwóch przeciwległych narożników.

Na tym etapie trudno stwierdzić, czy jedna możliwość jest lepsza od drugiej, dlatego tylko jako
przykład zaimplementujemy pierwszą.

Oto definicja klasy:


class Rectan9le :
"""Repre=entuje prostokąt.
At1ybuty width, height, corner.

Notka dokumentacyjna wyszczególnia atrybuty: wi dt h i height to liczby, a corner to obiekt Poi nt


określający lewy dolny narożnik.

Prostokąty I 189
W celu uzyskania reprezentacji prostokąta konieczne jest utworzenie instancji klasy Re ctangl e
i przypisanie wartości atrybutom:
box = Recta ngle{)
box. width = 100 . 0
box.height = 200 . 0
box. corne r = Poi nt()
box. corner .X = O. O
box. corner .y = O. O

Wyrażenie box. corner . x oznacza: „Przejdź do obiektu, do którego odwołuje się zmienna box, i wybierz
atrybut o nazwie corner, a następnie przejdź do tego obiektu i wybierz atrybut o nazwie x".

Na rysunku 15.2 pokazano stan tego obiektu. Obiekt, któ ry jest at rybutem innego obiektu, jest
obiektem osadzonym.

Rectang1le
box
------ widt h - 100.0
hęight - - 200.0
Point
x- o.o
corner y-o.o
Rysunek 15.2. Diagram obiektów

Instancje jako wartości zwracane


Funkcje mogą zwracać instancje. Na przykład funkcja f i nd_center pobiera obiekt Re ctang l e jako
argument i zwraca obiekt Poi nt , który zawiera współrzędne środka prostokąta (obiekt Rectangl e ):
def fi nd_center (rect ) :
p = Poi nt {)
p.x = rect . corne r .x + rect .wi dth / 2
p.y = rect . corne r .y + rect . height / 2
retu rn p

O to przykład, w którym zmienna box jest przekazywana jako argument, a wynikowy obiekt Poin t
przypisywany jest zmiennej cente r:
>>> cent e r = f i nd_cente r(box)
>>> pri nt_poi nt {cent e r )
(50 , 100)

Obiekty są zmienne
Możliwa jest zmiana stanu obiektu przez utworzenie przypisania do jednego z jego atrybutów. Aby na
przykład zmienić rozmiar prostokąta bez modyfikowania jego położenia, możesz dokonać edycji
wartości atrybutów wi dth i he i ght:

box.w idt h = box.w idt h + 50


box.hei ght = box.height + 100

190 I Rozdział 15. Klasy I obiekty


Możliwe jest też tworzenie funkcji modyfikujących obiekty. Na przykład funkcja grow_ rectangl e
pobiera obiekt Rectangl e oraz dwie liczby (dwidth i dheight), po czym dodaje je do szerokości i wysokości
prostokąta:

def grow_rectangle(rect , dwidth , dheig ht ) :


rect . width += dwidth
rect . height += dhei ght

Oto przykład demonstrujący efekt:


>>> box.wi dth , box.height
{150. O, 300 . O)
»> grow_rectangl e{box, 50, 100)
>>> box.wi dth , box.height
(200. 0, 400 . 0)

Wewnątrz funkcj i argument rect to alias dla box, dlatego w momencie zmodyfikowania przez ni ą
tego argumentu zmieniany jest także box.
W ramach ćwiczenia utwórz funkcję o nazwie move _rectang l e pobi erającą obiekt Rectangle oraz
dwie liczby (dx i dy). Funkcja powinna zmienić położenie prostokąta przez dodanie liczb dx i dy
odpowiednio do wspó łrzędnych x i y at rybutu corner.

Kopiowanie
Tworzenie aliasów może utrudnić czytanie kodu programu, ponieważ zmiany dokonane w jednym
miejscu mogą spowodować nieoczekiwane efekty w innym miejscu. Trudno śledzić wszystkie zmienne,
które mogą odwoływać się do danego obiektu.

Kopiowanie obiektu to często alternatywa dla tworzenia aliasu. Moduł copy zawiera funkcję o na-
zwie copy, która może duplikować dowolny obiekt:
>>> pl = Point()
>>> pl. X 3. 0
>>> pl.y 4.0

>>> import copy


>>> p2 = copy. copy {p l )

Zmienne pl i p2 zawierają jednakowe dane, ale nie są tym samym obiektem Poi nt:
>>> print_point(p l )
(3 , 4)
>>> print_point(p2)
(3 , 4)
»> pl is p2
Fal se
»> pl p2
Fal s e

Operator i s wskazuje, że zmienne pl i p2 nie są tym samym obiektem, co jest zgodne z oczekiwa-
niami. Być może jednak spodziewałeś się, że operator == zapewni wartość True, ponieważ p u nkty
reprezentowane przez te zmienne zawierają identyczne dane. Będziesz prawdopodobnie rozcza-
rowany informacją, że w przypadku instancj i domyślne zachowanie operatora == jest takie samo
jak operatora is. Operator == sprawdza identyczność obiektów, a nie ich równoważność. Wynika to

Kopiowanie I 191
stąd, że w odniesieniu do typów definiowanych przez programistę interpreter języka Python nie
dysponuje informacją, co powinno być uważane za równorzędne. Tak przynajmniej jest na razie.
Jeśli użyjesz funkcji copy. copy do zduplikowania prostokąta, stwierdzisz, że kopiuje ona obiekt
Rectangl e, lecz nie osadzony obiekt Point:
>> > box2 = copy. copy ( box)
»> box2 i 5 box
Fa l 5e
>>> box2. corner i5 box. corne r
Tr ue

Na rysunku 15.3 pokazano diagram obiektów. Przedstawiona operacja nazywana jest „płytkim"
kopiowaniem, ponieważ powoduje skopiowanie obiektu i wszystkich odwołań, jakie zawiera, lecz
nie obiektów osadzonych.

box
-- wid th ----- 100.0
height ----- 200.0 x-o.o
100.0 ~ width
200.0 ~ height
~ box2

corner y-o.o :: co mer

Rysunek 15.3. Diagram obiektów

W przypadku większości zastosowań nie jest to pożądane. W powyższym przykładzie wywołanie


funkcji gr ow_rec tangl e dla jednego z prostokątów nie będzie mieć wpływu na drugi, ale wywołanie
funkcji move _rectang l e dla dowolnego prostokąta wpłynie na oba! Takie zachowanie jest niejasne
i nietrudno przez to o błędy.
Na szczęście moduł copy
zapewnia metodę o nazwie deepcopy, która kopiuje nie tylko obiekt, ale
również obiekty, do których się on odwołuje, oraz obiekty przywoływane przez te obiekty itd. Nie
będziesz pewnie zaskoczony informacją, że operacja ta nazywana jest „głębokim" kopiowaniem.
>>> box3 = copy. deepcopy {box)
» > box3 i 5 box
Fa l 5e
>>> box3. corner i5 box. corne r
Fa l 5e

box 3 i box to całkowicie odrębne obiekty.

W ramach ćwiczenia utwórz wersję funkcji move_rectang l e, która tworzy i zwraca nowy obiekt
Rectangl e, zamiast modyfikować stary obiekt.

Debugowanie
Rozpoczynając pracę z obiektami, prawdopodobnie natrafisz na kilka nowych wyjątków. Jeśli
spróbujesz uzyskać dostęp do atrybutu, który nie istnieje, uzyskasz błąd Attri but eEr ror :
»> p = Point()
>>> p.x 3
»> p.y = 4
>>> p. z
Att r ibuteError : Point i n5tance ha5 no attri bute ' z'

192 I Rozdział 15. Klasy I obiekty


Jeśli nie masz pewności, jakiego typu jest obiekt, możesz o to zapytać:
» > t ype (p)
<class ' main . Point ' >

Możliwe jest też zastosowanie funkcji i s i ns t ance do sprawdzenia, czy obiekt jest instancją klasy:
>>> isinstance (p , Po i nt)
True

Jeżeli nie jesteś pewien, czy obiekt ma określo ny atrybut, możesz skorzystać z funkcji wbudowa-
nej hasattr:
>>> hasattr(p , ' x ')
True
>>> hasattr(p , ' z ')
Fal s e

Pierwszym argumentem może być dowolny obiekt, a drugim jest łańcuch, który zawiera nazwę
atrybutu.

Masz też możliwość użycia instrukcji try, aby stwierdzić, czy obiekt zawiera wymagane atrybuty:
t ry:
X = p .X
except Attribut e Error :
X = 0

Takie podejście może ułatwić pisanie funkcji, któ re mają do czynienia z róż nymi typami. Więcej na
ten temat znajdziesz w podrozdziale „Polimo rfizm" rozdziału 17.

Słownik
klasa
Typ definiowany przez programistę. Definicja klasy powoduje utworzenie nowego obiektu
klasy.

obiekt klasy
Obiekt zawierającyinformacje o typie definiowanym przez programistę. Obiekt klasy może
posłużyć do utworzenia instancji typu.
instancja
Obiekt należący do klasy.

tworzenie instancji
Operacja polegająca na utworzeniu nowego obiektu.

atrybut
Jedna z nazwanych wartości powiązanych z obiektem.

obiekt osadz ony


Obiekt przechowywany jako atrybut innego obiektu.

Słownik I 193
„płytkie" kopiowanie

Operacja kopiowania zawartości obiektu, w tym wszystkich odwołań do obiektów osadzonych.


Operacja jest implementowana przez funkcję capy modułu copy.
„głębokie" kopiowanie

Operacja kopiowania zawartości obiektu, a także wszystkich obiektów osadzonych wraz z obiek-
tami w nich osadzonymi. Operacja jest implementowana przez funkcję deepcopy modułu capy.

diagram obiektów
Diagram prezentujący obiekty, ich atrybuty i wartości atrybutów.

Ćwiczenia
Ćwiczenie 15.1.

Utwórz definicję klasy o nazwie Ci re l e z atrybutami center i rad i us, gdzie atrybut center to obiekt
punktu, a atrybut rad i us to liczba.

Utwórz instancję w postaci obiektu Ci re l e, który reprezentuje koło ze środkiem o współrzędnych


(150, 100) i promieniem 75.

Napisz funkcję o nazwie poi nt_ i n_ ci rcle, która pobiera obiekty Ci rc l e i Poi nt, a ponadto zwraca
wartość True, jeśli punkt leży w obrębie koła lub na jego granicy.
Utwórz funkcję o nazwie rect_in_ci rcl e, która pobiera obiekty Ci rc le i Rectang le, a następnie
zwraca wartość True, j eśli prostokąt leży całkowicie w obrębie koła lub na jego granicy.
Utwórz funkcję o nazwie rect_ci re le_over l ap, która pobiera obiekty Ci re l e i Rectangl e, a następ­
nie zwraca wartość True, jeśli dowolny z narożników prostokąta znajduje się w obrębie koła. W ra-
mach bardziej ambitnej wersji funkcja może zwracać wartość True, jeśli dowolna część prostokąta
znajduje się wewnątrz koła.

Rozwiązanie: plik Circle.py.

Ćwiczenie 15.2.

Utwórz funkcję o nazwie draw_rect, która pobiera obiekt żółwia i obiekt Rect angle, a następni e za
pomocą pierwszego obiektu rysuje prostokąt. W rozdziale 4. zamieszczono przykłady wykorzy-
stujące obiekty żółwia.

Utwórz funkcję o nazwie draw_circle, która pobiera obiekt żółwia i obiekt Circle, a ponadto ry-
suje koło.

Rozwiązanie: plik draw.py.

194 I Rozdział 15. Klasy I obiekty


ROZDZIAŁ 16.

Klasy i funkcje

Wiesz już, jak tworzyć nowe typy. Następnym krokiem jest utworzenie funkcji pobierających jako
parametry obiekty definiowane przez programistę i zwracających je jako wyniki. W rozdziale za-
prezentowałem również styl programowania funkcyjnego oraz dwa nowe plany projektowania
programów.
Przykładowy kod użyty w tym rozdziale znajdziesz w pliku Timel.py, który, tak jak i pozostałe
pliki z tego rozdziału, jest dostępny pod adresem ftp:/ lftp.helion.p//przyklady/myjep2.z ip. Rozwiązania
ćwiczeń umieściłem w pliku Timel_soln.py.

Klasa Time
W ramach kolejnego przykładu typu definiowanego przez programistę zdefiniujemy klasę o nazwie
Ti me, która rejestruje porę dnia. Definicja tej klasy ma następującą postać:

c l ass Time:
"""Repre:entuje porę dnia .

Atrybuty hour, minute, second

Możliwe jest utworzenie nowego obiektu Time oraz przypisanie atrybutów dla godzin, minut i sekund:
time = Time()
time . hour = 11
time . minute = 59
time . second = 30

Diagram stanu obiektu Ti me wygląda podobnie jak na rysunku 16.1.

Time
time- - hour ----;- 11
minute ------ 59
second --- 30

Rysunek 16.1. Diagram ob iektów

195
W ramach ćwiczenia utwórz funkcję o nazwie pr int_t i me, która pobiera obiekt Time i wyświetla
czas w postaci godzina:minuty: sekundy. Wskazówka: ciąg formatu ' %. 2d ' powoduje wyświetlenie
liczby całkowitej przy użyciu co najmniej dwóch cyfr, w tym, jeśli to konieczne, zera początkowego.

Utwórz funkcję boolowską o nazwie is_afte r , która pobiera dwa obiekty czasu t l i t2, a ponadto
zwraca wartość True, jeśli t1 następuje chronologicznie po t2, a wartość Fal se w przeciwnym razie.
Wyzwanie: nie korzystaj z instrukcji i f.

Fu nkeje „czyste"
W kilku następnych podrozdziałach zostaną utworzone dwie funkcje, które dodają wartości czasu.
Reprezentują one dwa rodzaje funkcji: funkcje „czyste" i modyfikatory. Obrazuj ą też plan projektowa-
nia, który będę określać mianem prototypu i poprawek. Jest to sposób podchodzenia do złożonego
problemu, w przypadku którego zaczyna się od prostego prototypu i stopniowo zajmuje się różnymi
komplikacjami.

Oto prosty prototyp funkcji add _ti me:


def add_time (t l, t 2) :
s um = Time()
s um.hour = t l.hour + t 2.hour
s um. minute = t l. minut e + t2 . minut e
s um. second = t l. second + t2 . second
r eturn sum

Funkcja ta tworzy nowy obiekt Time, inicjuje jego atrybuty i zwraca odwołanie do nowego obiektu. Na-
zywana jest funkcją „czystą", ponieważ nie modyfikuje żadnego z obiektów przekazanych jej jako ar-
gumenty. Ponadto oprócz zwracania wartości nie powoduje żadnego efektu, takiego jak wyświe­
tlanie wartości lub uzyskiwanie danych wprowadzonych przez użytkownika.

Aby przetestować taką funkcję, utworzę dwa obiekty czasu: obiekt start zawiera czas początkowy
filmu takiego jak Monty Python i Święty Graal, a obiekt d urat i on zawiera czas trwania filmu, który
wynosi godzinę i 35 minut.

Funkcja add_t ime określa moment zakończenia filmu:


>>> start = Time()
>>> start .hour = 9
>>> start . minute = 45
>>> start . second = O
>>> duration = Time()
>>> duration .hour = 1
>>> duration . minute = 35
>>> duration . second = O

>>> done = add_time(start , duration)


>>> print_time(done)
10 : 80: 00

Wynik 10:80 :00 może nie być tym, czego się spodziewasz. Problem polega na tym, że funkcja ta nie ra-
dzi sobie z sytuacjami, w których suma sekund lub minut przekracza wartość 60. Gdy do tego dojdzie,
konieczne będzie „przeniesienie" dodatkowych sekund do kolumny minut lub dodatkowych minut
do kolumny godzin.

196 I Rozdział 16. Klasy I funkcje


Oto ulepszona wersja:
def add_time(t l, t2) :
sum = Time()
sum .hour = tl.hour + t 2.hour
sum . minut e = tl.minute + t2.minute
sum .second = tl.second + t2.second

i f sum . second > = 60 :


sum.second -= 60
sum.minute += 1

i f sum .minute>= 60 :
sum.minute -= 60
sum.hour+ =

return sum

Ta funkcja jest poprawna, ale jej kod zaczyna się robić pokaźny. Nieco dalej zostanie zaprezento-
wana krótsza alternatywa.

Modyfikatory
Czasami przydatne jest modyfikowanie przez funkcję obiektów, które uzyskuje ona jako parametry.
W tym przypadku zmiany są widoczne dla elementu wywołującego. Funkcje działające w ten sposób
nazywane są modyfikatorami.
Funkcja inc reme nt, która dodaje daną liczbę sekund do obiektu Time, może oczywiście zostać zapisana
jako modyfikator. Oto ogólna wersja robocza:
def i ncrement(time, s econds) :
time.second += seconds

i f time.second >= 60:


time.second -= 60
time.minute += 1

if time.minute>= 60:
time . minut e -= 60
time .hour+= 1

W pierwszym wierszu wyko nywana jest prosta operacja, a w pozostałych wierszach obsługiwane
są specjalne przypadki, które zostały j uż wcześniej zaprezentowane.

Czy ta funkcja jest poprawna? Co się stanie, gdy wartość atrybutu seconds znacznie przekracza 60?
W takiej sytuacji nie wystarczy jednorazowa operacja przenoszenia. Niezbędne jest wykonywanie prze-
noszenia do momentu, aż wartość t ime . second będzie m niejsza niż 60. Jednym z rozwiązań jest zastą­
pienie instrukcji if instrukcjami wh i l e. Sprawi to, że funkcja będzie poprawna, ale niezbyt efektyvma.
W ramach ćwiczenia utwórz poprawną wersję funkcji i ncrement pozbawioną jakichkolwiek pętli.

Wszystko, co jest m ożliwe do zrealizowania z wyko rzystaniem modyfikatorów, może też zostać
osiągni ęte przy użyci u funkcji „czystych". Okazuje się, że w niektórych językach programowania
dozwolone są wyłącznie funkcje „czyste". Potwierdzono, że programy korzystające z takich funkcj i

Modyfikatory I 197
mogą być szybciej tworzone, a ponadto są mniej podatne na błędy niż programy używaj ące mo-
dyfikatorów. Modyfikatory są jednak czasami wygodne w użyciu, a programy funkcyjne są zwykle
mniej wydajne.

Ogólnie rzecz biorąc, zalecam tworzenie funkcji „czystych" zawsze tam, gdzie jest to uzasadnione,
a sięganie po modyfikatory tylko wtedy, gdy zapewniają one istotną korzyść. Takie podejście
można określić mianem stylu programowania funkcyjnego.

W ramach ćwiczenia utwórz „czystą" wersję funkcji i nc rement, która zamiast modyfikować para-
metr, tworzy i zwraca nowy obiekt Ti me .

Porównanie prototypowania i planowania


Demonstrowany przeze mnie plan projektowania nosi nazwę prototypu i poprawek. W przypad-
ku każdej funkcji utworzyłem prototyp, który przeprowadził podstawowe obliczenia, a następnie
przetestowałem go, poprawiając po drodze błędy.

Takie podejście może być efektywne, zwłaszcza wtedy, gdy problem nie jest jeszcze dogłębnie po-
znany. Stopniowo wprowadzane poprawki mogą jednak spowodować wygenerowanie kodu, który jest
niepotrzebnie skomplikowany (z powodu obsługi wielu przypadków specjalnych) i niepewny (ze
względu na to, że trudno stwierdzić, czy znaleziono wszystkie błędy).

Alternatywą jest projektowanie zaplanowane, gdy ogólne rozpoznanie problemu może znacznie
ułatwićprogramowanie. W tym przypadku po rozpoznaniu stwierdzono, że obiekt Time to wrze-
czywistości liczba trzycyfrowa o podstawie w postaci liczby 60 (więcej informacji znajdziesz pod
adresem http://pl.wikipedia.org/wiki/Sz e%C5%9B%C4%87dz iesi%C4%85tkowy_system_licz bowy)!
Atrybut second reprezentuje kolumnę jedności, atrybut mi nute kolumnę sześćdziesiątek, a atrybut
hour kolumnę t rzydziestu sześciu setek.

Tworząc funkcje add_t ime i inc rement, w rzeczywistości wykonaliśmy operację dodawania z podstawą
w postaci liczby 60. Z tego właśnie powodu konieczne było przenoszenie między kolumnami.

Obserwacja ta sugeruje inne podejście do całości problemu. Możliwe jest przekształcenie obiektów
Ti me w liczby całkowite oraz wykorzystanie faktu, że komputer ma umiejętność wykonywania
operacj i arytmetycznych na liczbach całkowitych.

Oto funkcja przekształcająca obiekty Ti me w liczby całkowite:


def ti me to int{ti me) :
minutes = t ime .hour * 60 + time . mi nute
seconds = minutes * 60 + t i me. second
return s econds

Poniżej zaprezentowano funkcję, która przekształca liczbę całkowitą w obiekt Ti me (jak pami ętasz,
funkcja di vmod dzieli pierwszy argument przez drugi i zwraca jako krotkę iloraz oraz resztę).
def i nt_to_ti me{seconds) :
ti me = Time{)
minut es , time . second = divmod (seconds , 60)
ti me . hour , ti me. mi nute = di vmod( mi nut es , 60)
return time

198 I Rozdział 16. Klasy I funkcje


Być może będziesz zmuszony trochę się zastanowić i u ruchomić testy, aby przekonać się, że te
funkcje są poprawne. Jednym ze sposobów przetestowania ich jest sprawdzenie, czy dla wielu wartości
argumentu x prawdziwy jest kod t ime_ t o_int ( i nt_ t o_time( x )) == x. Jest to przykład sprawdzania
spójności.

Po przekonaniu się co do poprawności funkcji możesz użyć ich do przebudowania funkcji add_t i me:
def add_time( tl, t 2) :
s econds = time_t o_int(t l ) + time t o int(t2)
return i nt _to_time (seconds)

Ta wersja jest krótsza od oryginału i łatwiejsza do zweryfikowania. W ramach ćwiczenia przebuduj


funkcj ę i ncrement za pomocą funkcji t i me_ t o_ int i int_ to_ time.

Pod pewnymi względami konwersja z podstawy 60 na podstawę 10 i odwrotnie jest trudniejsza


niż sama obsługa czasu. Konwersja podstawy to operacja bardziej abstrakcyjna. W przypadku zajmo-
wania się wa rtościami czasu m amy lepszą intuicję.

Jeśli jednak posługiwanie się czasem jako liczbami o podstawie 60 nie stanowi dla Ciebie żadnego
problemu, a ponadto poczyniłeś starania związane z utworzeniem funkcji konwersji (ti me_ t o_ i nt
i i nt_ t o_ t i me), uzyskasz program, który ma krótszy kod, jest łatwiejszy do przeczytania i poddania
debugowaniu oraz bardziej niezawodny.
Łatwiejsze
jest też późniejsze dodawanie elementów. Wyobraź sobie na przykład odejmowanie dwóch
czasów w celu ustalenia okresu trwania. Naiwnym rozwiązaniem byłoby zaimplementowanie odej-
mowania z pożyczaniem. Użycie funkcji konwersji byłoby prostsze, a ponadto z większym praw-
dopodobieństwem poprawne.

Jak na ironię czasami komplikowanie problemu (lub uogólnianie go) sprawia, że staje się on łatwiejszy
do rozwiązania (z powodu mniejszej liczby przypadków specjalnych i możliwości wystąpienia błędu).

Debugowanie
Obiekt czasu jest dobrze zdefiniowany, gdy wartości atrybutów mi nute i second zawierają się w prze-
dziale od O do 60 (z uwzględnieniem zera, lecz nie 60), a ponadto at rybut hour ma wartość do datnią.
Atrybuty hour i mi nute powinny mieć wartości całkowite, ale dopuszczalne jest, aby atrybut second
zawierał część ułamkową.

Takie wymagania są nazywane niezmiennikami, ponieważ zawsze powinny być prawdziwe. Mówiąc
inaczej, jeśli nie są one prawdziwe, coś się nie powiodło.
Pisanie kodu w celu sprawdzania niezmienników może ułatwić wykrywanie błędów i znajdowa-
nie ich przyczyn. M ożesz na przykład dysponować funkcją taką jak va l id_t ime, która pobiera
obiekt czasu i zwraca wartość Fa l se, j eśli zostanie naruszony niezmiennik:
def val id_time(time) :
if time . hour< O or ti me .m inute< O or time . second< O:
return Fal se
if time . minute>= 60 or time . second >= 60 :
return Fal se
return True

Debugowanie I 199
Na początku każdej funkcji możesz sprawdzić argumenty, aby upewnić się, że są poprawne:
def add_t i me(tl , t2):
i f not val id_time(tl) or not valid_time(t2) :
raise Value Erro r('Niepoprawny obiekt Time w funk cji add_ti me' )
seco nds = t ime_t o_ int(tl) + time_to_ int(t2)
retur n int_t o_time(seconds )

Możesz też użyć instrukcji asercji, która sprawdza dany niezmiennik i zgłasza wyjątek, gdy w je-
go przypadku wystąpi niepowodzenie:
def add_t ime(tl , t2) :
assert val id_time(tl) and valid_time(t2)
seco nds = t ime_t o_ int(tl) + time_t o_ int(t2)
retur n int t o time(seconds )

Instrukcje assert są przydatne, ponieważ odróżniają kod zajmujący się zwykłymi warunkami od kodu,
który przeprowadza sprawdzenia pod kątem błędów.

Słownik
prototyp i poprawki
Plan projektowania uwzględniający pisanie ogólnej wersji roboczej programu, testowanie i usu-
wanie znalezionych błędów.

projektowanie z aplanowane
Plan projektowania obejmujący ogólne rozpoznanie problemu i planowanie w większym stopniu
niż w przypadku projektowania przyrostowego lub prototypowego.

funkcja „czysta"
Funkcja, która nie modyfikuje żadnych obiektów odbieranych jako argumenty. Większość
funkcj i „czystych" to funkcje „owocne''.

modyfikator
Funkcja modyfikująca jeden lub więcej obiektów odbieranych jako argumenty. Większość
modyfikatorów to funkcje „puste", czyli zwracające wartość None .

styl programowania funkcyjnego


Styl projektowania programów, w przypadku którego większość funkcji to funkcje „czyste".

niezmiennik
Warunek, który zawsze powinien być spełniony podczas wykonywania programu.
instrukcja asercji
Instrukcja sprawdzająca warunek i zgłaszająca wyjątek, gdy nie jest on spełniony.

200 I Rozdział 16. Klasy I funkcje


Ćwiczenia
Przykładowy kod użyty
w tym rozdziale znajdziesz w pliku Timel.py, który, tak jak i pozostałe
pliki z tego rozdziału,
jest dostępny pod adresem ftp:/ lftp.helion.pl!przyklady!myjep2.zip. Rozwiązania
ćwiczeń u mieściłem w pliku TimeCsoln.py.

Ćwiczenie 16.1.

Utwórz funkcję o nazwie mul _ t i me, która pobiera obiekt Ti me i liczbę, a ponadto zwraca nowy
obiekt Time zawierający iloczyn oryginalnego obiektu Time i liczby.

Użyj następnie funkcj i mul _ti me do utworzenia funkcji, która pobiera obiekt Ti me reprezentujący
czas ukończenia wyścigu, a także liczbę reprezentującą dystans. Funkcja zwraca obiekt Time repre-
zentujący śred nie tempo (czas przypadający na kilometr).

Ćwiczenie 16.2.

Moduł datet i mezapewnia obiekty t ime podobne do obiektów Time przedstawio nych w rozdziale,
które jednak oferują bogaty zestaw metod i operatorów. Przeczytaj dokumentację dostępną pod
adresem http://docs.python.org/3/library/datetime.html.
1. Użyj modułu datet i me do napisania programu, który uzyskuje bieżącą datę i wyświetla dzień
tygodnia.
2. Utwórz program pobierający datę urodzenia jako dane wejściowe, a także wyświetlający wiek
użytkownika oraz liczbę dni, godzin, minut i sekund, jaka pozostała do następnych urodzi n.

3. W przypadku dwóch osób urodzonych w różnych dniach występuj e dzień, gdy pierwsza oso-
ba jest dwa razy starsza od drugiej. Dla tych osób jest to „dzień podwajania". Utwórz p ro-
gram pobierający dwie daty urodzenia i obliczający dla nich „dzień podwajania".
4. W ramach trochę większego wyzwania napisz bardziej ogólną wersję programu obliczającą dzień,
w przypadku którego pierwsza osoba jest n razy starsza od drugiej.

Rozwiązanie: plik double.py.

Ćwlaenla I 201
202 I Rozdział 16. Klasy I funkcje
ROZDZIAŁ 17.

Klasy i metody

Choć korzystamy z niektórych elementów obiektowych języka Python, programy zaprezentowa-


ne w dwóch poprzednich rozdziałach nie są tak naprawdę obiektowe, ponieważ nie reprezentują
relacji między typami definiowanymi przez programistę i funkcjami, które przetwarzają te typy.
Następnym krokiem jest transformacja tych funkcji do postaci metod, które sprawiają, że relacje
są jednoznaczne.

Przykładowy użyty w tym rozdziale znajdziesz w pliku Time2.py, któ ry, tak jak pozostałe pliki
kod
z tego rozdziału,
jest dostępny pod adresem Jtp:/ lftp.helion.pl!przyklady/ myjep2.z ip. Rozwiązania
ćwiczeń zapisałem w pliku Point2_soln.py.

Elementy obiektowe
Python to obiektowy język programowania. Oznacza to, że zapewnia on elementy obsługujące
programowanie obiektowe charakteryzujące się z definicji następującymi cechami:
• Programy uwzględniają definicje klas i metod.
• Większość obliczeń wyrażana jest w kategoriach operacji na obiektach.
• Obiekty reprezentują często elementy otaczającego nas świata, a metody nierzadko odpowia-
dają sposobowi, w jaki te elementy prowadzą ze sobą interakcję.

Na przykład klasa Time zdefiniowana w rozdziale 16. odpowiada temu, jak ludzie rej estrują porę
dnia. Z kolei zdefiniowane w tej klasie funkcje odpowiadają rodzajom działań, jakie ludzie po-
dejmują odnośnie do czasu. Podobnie klasy Point i Rect angl e z rozdziału 15. odpowiadają pojęciom
matematycznym, takim jak punkt i prostokąt.

Do tej pory nie skorzystaliśmy z elementów, jakie język Python zapewnia pod kątem obsługi pro-
gramowania obiektowego. Elementy te nie są ściśle wymagane. Większość z nich oferuje alterna-
tywną składnię dla wcześniej wykonywanych działań. W wielu sytuacjach alternatywa jest jednak
bardziej zwarta i dokładniej oddaje strukturę programu.
Na przykład w kodzie pliku Timel.py nie występuje oczywiste połączenie między definicją klasy
i następującymi po niej definicjami funkcji. Po dodatkowym przyjrzeniu się okazuje się, że każda
funkcja pobiera jako argument co najmniej jeden obiekt Ti me.

203
Obserwacja ta stanowi motywacj ę do zastosowania metod. Metoda to funkcja skojarzo na z okre-
śloną klasą. Metody prezentowałem przy okazji łańcuchów, list, słowników i krotek. W tym roz-
dziale zostały utworzone metody dla typów defi niowanych przez programistę.

Pod względem semantycznym metody są takie same jak funkcje, ale występują następuj ące dwie
różnice składniowe po między metodą a funkcją:
• Metody są definiowane wewnąt rz definicji klasy, aby zapewnić jed noznaczność relacji m iędzy
klasą i metodą.

• Składnia powiązana z wywoływaniem metody różni się od składni służącej do wywoływania


funkcji.

W kilku następnych podrozdziałach funkcje z poprzednich dwóch rozdziałów zostaną poddane


transformacji do postaci metod. T ransformacja ta jest czysto mechaniczną operacją. Możesz j ą
wyko nać, postępując zgodnie z sekwencją kroków. Jeśli nie masz problemu z przekształcaniem
funkcji w metodę i odwrotnie, będziesz w stanie wybrać tę postać, która okaże się najlepsza podczas
realizowania danego działania.

Wyświetlanie obiektów
W rozdziale 16. zdefiniowałem klasę o nazwie Time , a w jego podrozdziale „Klasa Time" utworzyłem
fu nkcję o nazwie pri nt_ t i me:
cl ass Time:
"""Repre=entu;e porę dnia.'""'
def print_time (time ) :
pri nt( ' %.2d: %.2d: %.2d' % (time.hour, time.minute , time . s econd) )

Aby wywołać tę funkcję, musisz przekazać obiekt Ti me jako argument:


>>> start = Time( )
>>> start .hour = 9
>>> start . minute = 45
>>> start . second = OO
>>> print_time (star t)
09 : 45 : 00

W celu uzyskania metody pr i nt_t ime niezbędne jest jedynie przeniesienie definicji funkcji w ob-
ręb definicji klasy. Zwróć uwagę na zmianę wcięcia.

cl ass Time :
def pr i nt_t ime(time) :
print( ' %. 2d :%.2d:%.2d ' % (time.hou r , ti me . minute, time . second))

Możliwe są teraz dwa sposoby wywo łani a m etody pr i nt _ t i me. Pierwszym (i mniej powszechnym)
z nich jest użycie składni funkcji:
>>> Time . print_t ime(start)
09 : 45 : 00

W przypadku tego użycia notacji z kropką Ti me jest nazwą klasy, a pr i nt _ time to nazwa metody.
start przekazano jako parametr.

204 I Rozdział 17. Klasy I metody


Drugi sposób (bardziej zwięzły) polega na zastosowaniu składni metody:
>>> start . print_time ()
09 :45: 00

W przypadku tego użycia notacji z kropką print_time to nazwa metody (ponownie), a start jest
obiektem, dla którego wywołano metodę (nazywany jest on podmiotem). Tak jak podmiot zdania
jest tym, czego zdanie dotyczy, tak podmiot wywołania metody jest tym, czego ona dotyczy.

Wewnątrz metody podmiot przypisywany jest pierwszemu parametrowi, dlatego w tym przypadku
s t a r t przypisano parametrowi ti me.

Przyjęte jest, że pierwszy parametr metody nosi nazwę self, dlatego kod metody pri nt_ ti me częściej
miałby następującą postać:
class Time :
def print_time (self ) :
print('%.2d:%.2d:%.2d' % (sel f.hour , s elf.m inute , self. s econd))

Powodem takiej konwencji jest niejawna metafora:


• Składnia wywołania funkcji pr i nt_ time( s t art) sugeruje, że jest ona aktywnym agentem. Zna-
czenie wywołania jest następujące: „Witaj, pri nt_ ti me ! Oto obiekt, który masz wyświetlić".
• W programowaniu obiektowym obiekty są aktywnymi agentami. Wywołanie metody takiej
jak start.print_ t i me () oznacza: „Witaj, s tar t! Wyświetl sam siebie".

Z pewnej perspektywy zmiana ta może wydawać się bardziej elegancka, ale nie jest oczywiste, że
jest przydatna. W dotychczas zaprezentowanych przykładach może tak nie być. Czasami jednak
przeniesienie odpowiedzialności z funkcji na obiekty umożliwia tworzenie bardziej wszechstronnych
funkcji (lub metod), a ponadto ułatwia utrzymanie kodu i jego ponowne wykorzystanie.

W ramach ćwiczenia przebuduj funkcję t ime_ to_ int (z podrozdziału „Porównanie prototypowania
i planowania" rozdziału 16.) jako metodę. Możesz pokusić się o zrobienie tego samego również z funkcją
int _ t o_t ime. Nie ma to jednak tak nap rawdę sensu z powodu braku obiektu, dla którego metoda
zostałaby wywołana.

Kol ej ny przykład
Oto wersja funkcji i ncrement (z podrozdziału „Modyfikatory" rozdziału 16.) przebudowanej jako
metoda:
# wewnątr= klasy Time
def increment(self, seconds) :
seconds += self . time_to_ int()
return int to time(seconds)

W tej wersji przyjęto, że funkcję ti me_ to_ int utworzono jako metodę. Zauważ też, że jest to funkcja
„czysta", a nie modyfikator.
Metoda i ncrement zostałaby wywołana w następujący sposób:

>>> start . print_time()


09 : 45 :00
>>> end = start .i ncrement( l 337)

Kolejny przykład I 205


>>> e nd . pr int_t i me ()
10 : 07: 17

Podmiot st a rt przypisywany jest pierwszemu param etrowi self. Argument 1337 przypisano d ru-
giemu paramet rowi seconds.

Mechanizm ten może być niejasny, zwłaszcza w przypadku wygenerowania błędu. Jeśli na przykład
metodę i ncr ement wywo łasz z dwoma argumentami, uzyskasz następujący błąd:
>>> e nd = start . increme nt( 133 7, 460)
Ty peError : increment() takes 2 posit i onal arguments but 3 were gi ven

Komunikat o błędzie początkowo jest niejasny, po nieważ w nawiasach okrągłych występują tylko
dwa argu menty. Podmiot jest jednak również uważany za argument, dlatego w su mie istnieją trzy
argumenty.

Nawiasem mówiąc, argument pozycyjny to argument pozbawiony nazwy parametru. Oznacza


to, że nie jest to argument słowa kluczowego. W następującym wywołaniu fu nkcji:
s ketch(parrot , cage, dead=Tr ue)

pa rrot i cage to argumenty pozycyj ne, a dead to argument słowa kluczowego.

Bardziej złożony przykład


Przebudowa funkcji i s_afte r (z podrozdziału „Klasa Time" rozdziału 16.) jest trochę bardziej skom-
plikowana, gdyż jako parametry muszą zostać pobrane dwa obiekty Time. W tym przypadku wygod-
nym rozwiązaniem jest nadanie pierwszemu parametrowi nazwy self, a drugiemu nazwy ot her:
# wewnątr: klasy Time
def is_af t e r (self, othe r) :
ret urn s elf . time_to_ int() > ot her .ti me_t o_int()

Aby użyć tej m etody, musisz wywo łać ją w jednym obiekcie i przekazać drugiemu jako argument:
>>> end . is_af t e r(start)
Tr ue

M i łe jest to, że składnia ta brzmi prawie tak jak następuj ące zdanie w języku angielskim: „End is
after start?" (czy ko niec następuje po początku?).

Metoda init
Metoda i ni t (skrót od słowa initialization) to specjalna metoda wywoływa na w m omencie two-
rzenia instancji w postaci obiektu. Pełna nazwa tej metody to _ i nit_ (dwa znaki podkreślenia, po
których następuje słowo i nit, a następnie kolejne dwa znaki podkreślenia). W przypadku klasy Ti me
metoda in i t może p rezentować si ę następująco:
# wewnątr: klasy Time
def i ni t (self, hour =O, mi nute=O, second =O) :
self .hou r = hour
sel f. mi nute = minute
sel f. second = second

206 I Rozdział 17. Klasy I metody


Parametry metody _ i nit_ często mają takie same nazwy jak atrybuty. Instrukcja:
self. hour = hour

przechowuje wartość parametru hour jako atrybut obiektu self.


Parametry są opcjonalne, dlatego w przypadku wywołani a obiektu Time bez żadnych argum entów
uzyskasz wartości domyślne:
>>> time = Time()
>>> time . print_time()
00 :00 :00

Jeśli podasz jeden argument, nadpisze on parametr hour:


>>> time = Time {9)
>>> time . print_time()
09 :00 :00

W przypadku podania dwóch argumentów nadpiszą one parametry hour i minute:


>>> time = Time{ 9, 45)
>>> time . pr int_time()
09 : 45 :00

Jeżeli podasz trzy argumenty, spowodują one nadpisanie wszystkich trzech wartości domyślnych.

W ramach ćwiczenia utwórz m etodę i nit dla klasy Poi nt , która pobiera x i y jako param etry
opcjo nalne, a następni e przypisuje je odpowiednim at rybutom.

Metoda str
Metoda _ str_ to, tak jak _ i ni t _ , specjalna metoda, która ma zwracać reprezentację łańcuchową
obiektu.

Oto na przykład metoda str w przypadku obiektów klasy Ti me:


# wewnątr: klasy Time
def str_ (sel f):
return ' %. 2d :%.2d :%.2d' % (self.hour , self . minute, self . second)

W momencie użycia funkcji pr i nt dla obiektu interpreter języka Pytho n wywołuje m etodę str:
>>> time = Time{9, 45 )
»> print (time)
09 : 45 :00

Gdy tworzę nową klasę, prawie zawsze zaczynam od napisania metody _ i nit_ , która ułatwia tivorze-
nie instancj i w postaci obiektów, a także od metody_str_ przydającej się podczas debugowania.

W ramach ćwiczenia utwórz metodę str dla klasy Poi nt. Utwórz obiekt tej klasy i wyświetl go.

Przeciążanie operatorów
Definiując inne metody specjalne, możesz określić zachowanie operatorów w przypadku typów
definiowanych przez programistę. Jeśli na przykład definiujesz metodę o nazwie_add_dla klasy
Ti me, w odniesieniu do obiektów tej klasy możesz użyć operatora+.

Przecląianle operatorów I 207


Definicja metody_add _ może wyglądać następująco:

# wewnątr: klasy Time


def _ add {sel f, ot her) :
seconds = s el f . time to int() + other . time_to_ int()
return int _to_time (se conds)

Oto przykład zastosowania tej metody:


>>> start = Time(9 , 45)
>>> duration = Time( !, 35)
>>> print(start + duration)
11 : 20 : 00

W momencie zastosowania operatora+ w przypadku obiektów Ti ll'e interpreter języka Python W}"~O­
łuje metodę _ add_. Gdy wyświetlasz wynik, interpreter wywołuje metodę _ str_. Oznacza to, że
w tle dzieje się naprawdę sporo!
Zmiana zachowania operatora w taki sposób, by współpracował on z typami definiowanymi przez
programistę, nosi nazwę przeciążania operatora. W przypadku każdego operatora w j ęzyku Python
istnieje odpowiednia metoda specjalna, taka jak_add_ . Więcej szczegółów znajdziesz pod adresem
http://docs.python.org/3/reference/datamodel.html#specialnames.
W ramach ćwiczenia utwó rz metodę add dla klasy Po i nt.

Przekazywanie oparte na typie


W poprzednim podrozdziale dodaliśmy dwa obiekty Time. Możliwe jest też jednak dodanie liczby
całkowitej do obiektu Ti me. Oto wersja metody _ add_ sprawdzającej typ parametru other i wywołują­
cej metodę add _t ime lub i nc rement :
# wewnątr: klasy Time
def add (sel f, ot her) :
i f isinstance (ot her , Ti me) :
ret urn s e l f. add_ti me(ot her)
el se :
ret urn sel f. increment(ot her)
def add_time(self , other) :
seconds = self . time_to_ int() + ot her . time to int()
ret urn i nt_t o_time(seconds)
def increment(self , seconds) :
seconds += sel f. time_t o_ int()
ret urn i nt_t o_time(seconds)

Funkcja wbudowana i s i ns tance pobiera wartość i obiekt klasy, a następnie zwraca wartość True,
gdy wartością jest instancja klasy.
Jeśli other to obiekt Ti me, metoda _ acid_ wywołuje metodę add_t ime. W przeciwnym razie przyjmo-
wane jest, że parametr jest li czbą, i wywoływana jest metoda i ncre ment. Operacja ta nazywana jest
przekazywaniem opartym na typie, ponieważ obliczenie przekazywane jest innym metodom na
podstawie typu argumentów.

O to przykłady, w których użyto operatora + z różnymi typami:


>>> start = Ti me(9 , 45)
>>> duration = Time(! , 35)

208 I Rozdział 17. Klasy I metody


>>> print(5tart + duration)
11:20 : 00
>>> print(5tart + 1337)
10 : 07: 17

Niestety ta implementacja dodawania nie jest przemienna. Jeśli liczba całkowita jest pierwszym
argumentem, uzyskasz następujący błąd:
>>> print(1337 + 5tart)
TypeError : un5 upported operand type(5) for +: 'int' and 'in5tance'

Problem polega na tym, że zamiast żądać dodania liczby całkowitej przez obiekt Time, interpreter
języka Python żąda dodania tego obiektu przez liczbę całkowitą i co więcej, liczba ta nie dysponuje in-
formacją, jak ma to zrealizować. Istnieje jednak sprytne rozwiązanie tego problemu: metoda specjalna
_ radd _ , której nazwa to skrót od słów righ t-side add. Metoda ta wywoływana jest w momencie
pojawienia się obiektu Time po prawej stronie operatora+. Oto definicja:
# wewnqtr= klasy Time
def radd (5el f, ot her):
return 5el f. add (ot her)

Metoda używana jest w następujący sposób:

>>> print(1337 + 5tart)


10 : 07: 17

W ramach ćwicze nia utwórz metodę add dla obiektów Po i nt, która współpracuje z obiektem Point
lub krotką:
• Jeśli
drugim argumentem jest obiekt Po i nt , metoda powinna zwrócić nowy obiekt punktu, któ-
rego współrzędna x jest sumą współrzędnych x argumentów. Podobnie jest w przypadku
wspó łrzędnych y.

• Jeżeli drugi argument to krotka, metoda powinna do dać pierwszy element krotki do współ­
rzędnej x, drugi element do współrzędnej y, a następnie zwrócić nowy obiekt Poi nt z wyni-
kiem.

Polimorfizm
Przekazywanie oparte na typie jest przydatne w razie konieczności , lecz (na szczęści e) nie zawsze
jest niezbędne. Często możesz go uniknąć, tworząc funkcje, które działają poprawnie dla argumentów
z róż nymi typami.
Wiele funkcji napisanych na potrzeby łańcuchów dzi ała również w przypadku innych typów ci ą­
gów. Na przykład w podrozdziale „Słownik jako kolekcja liczników" rozdziału 11. zastosowaliśmy
funkcję hi stogram do obliczenia liczby wystąpień każdej litery w słowie:
def hi5togram(5) :
d = diet()
for c i n 5 :
i f c not i n d :
d [c] = 1
el5e:
d[c] = d[c] + 1
return d

Pollmorflzm I 209
Funkcja ta obsługuje też
listy, krotki, a nawet słowniki, pod warunkiem że elementy parametru s
zapewniają możliwość mieszania, aby mogły zostać użyte jako klucze w słowniku d:
1 1 1
>>> t = [ spam , jajko 1 ,
1
spam1 , 1
spam 1 , 1
bekon 1 , 1
spam 1 ]
>>> hi stogram(t)
( ' bekon' : 1, 'jaj ko ': 1, 'spam ' : 4}

Funkcje współpracujące z kilkoma typami są nazywane polimorficznymi. Polimorfizm może ułatwić


ponowne wykorzystanie kodu. Na przykład funkcja wbudowana sum, która dodaje elementy ciągu,
działa tylko wtedy, gdy elementy ciągu obsługują dodawanie.

Ponieważ obiekty Ti me zapewniają metodę add, mogą być użyte w przypadku funkcji sum:
>>> tl Time(?, 43)
>>> t 2 = Time(?, 41)
>>> t3 = Time(?, 37 )
»> tota l = sum ( [tl, t 2, t3])
>>> pri nt(total )
23 : 0 1: 00

Ogólnie rzecz biorąc, jeśli wszystkie operacje wewnątrz funkcji są wykonywane z użyciem danego
typu, jest on obsługiwany przez tę funkcję.
Najlepsza odmiana polimorfizmu to polimorfizm niezamierzony, w przypadku którego stwierdzasz,
że jużnapisana funkcja może zostać zastosowana dla typu, jaki nie był w ogóle brany pod uwagę przy
planowaniu.

Interfejs i implementacja
Jednym z celów projektu obiektowego jest zwiększenie możliwości utrzymania oprogramowania.
Oznacza to, że możliwe jest zapewnienie ciągłości działania programu, gdy zostaną zmienione inne
części systemu, a ponadto modyfikowanie programu po to, by spełnić nowe wymagania.

Zasada projektowa ułatwiająca osiągnięcie tego celu polega na zachowaniu niezależności interfejsów
od implementacji. W przypadku obiektów oznacza to, że metody zapewniane przez klasę nie powinny
być zależne od sposobu reprezentowania atrybutów.

Na przykład w tym rozdziale zaprojektowaliśmy klasę reprezentującą porę dnia. Metody oferowane
przez tę klasę to t ime_ t o_in t , is_after i add_t i me.
Metody te można zaimplementować na kilka sposobów. Szczegóły implementacji są zależne od
tego, jak reprezentowany jest czas. W rozdziale użyto następujących atrybutów obiektu Ti me: hour ,
mi nu te i second.

W ramach alternatywy można zastąpić te atrybuty pojedynczą liczbą całkowitą reprezentującą


liczbę sekund, jaka upłynęła od północy. Taka implementacja sprawiłaby, że łatwiejsze byłoby utwo-
rzenie niektórych metod, takich jak i s _a fter, ale utworzenie innych byłoby trudniejsze.

Po wdrożeniu nowej klasy możesz odkryć lepszą implementację. Jeśli inne części programu korzystają
z tej klasy, zmiana interfejsu może okazać się procesem czasochłonnym i podatnym na błędy.

Jeśli
jednak starannie zaprojektowałeś interfejs, możesz zmienić implementację bez zmiany interfejsu.
Oznacza to, że nie jest konieczna modyfikacja innych części programu.

210 I Rozdział 17. Klasy I metody


Debugowanie
Dozwolone jest dodawanie atrybutów do obiektów w dowolnym miejscu wykonywania programu, ale
jeśli istnieją
obiekty z tym samym typem, które nie mają identycznych at rybutów, z łatwością można
popełnić błędy. Za dobry pomysł uważa się i nicjalizację wszystkich atrybutów obiektu w meto-
dzie i ni t.
Jeśli nie masz pewności, czy obiekt zawiera konkretny atrybut, możesz skorzystać z funkcji wbu-
dowanej hasa tt r (zaj rzyj do podrozdziału „Debugowanie" rozdziału 15.).
Innym sposobem uzyskania dostępu do atrybutów jest wykorzystanie funkcji wbudowanej vars pobie-
rającej obiekt i zwracającej słownik, który odwzorowuje nazwy atrybutów Qako łańcuchy) na ich
wartości:

> >> p = Point( 3, 4)


» > vars{p)
{ ' y ' : 4, ' X' : 3}

Na potrzeby debugowania możesz uznać za przydatne skorzystanie z tej funkcji:


def pr int _at t r i but es{obj) :
f or attr in vars{obj) :
print (at t r, get at t r {obj, att r ))

Funkcja pr i nt_at t ri butes dokonuje przejścia słownika i wyświetla każdą nazwę atrybutu oraz
odpowiadającą jej wartość.

Funkcja wbudowana getat tr pobiera obiekt i nazwę atrybutu Qako łańcuch) oraz zwraca wartość
atrybutu.

Słownik
język obiektowy
Język zapewniający elementy takie jak typy definiowane przez programistę i metody, które uła­
twiają programowanie obiektowe.

programowanie obiektowe
Styl programowania, w przypadku którego dane i przetwarzające je operacje są uporządkowane
w ramach klas i metod.

metoda
Funkcja definiowana wewnątrz definicji klasy, która jest wywoływana w instancjach tej klasy.
podmiot
Obiekt, w którym wywoływana jest metoda.

argument pozycyjny
Argument, który nie uwzględnia nazwy parametru, dlatego nie jest argumentem słowa klu-
czowego.

Słownik I 211
przeciqżanie operatorów
Zmiana zachowania operatora takiego jak + tak, aby współpracował z typem definiowanym
przez programistę.
przekazywanie oparte na typie
Wzorzec programowania sprawdzający typ argumentu i wywołujący różne funkcje dla róż­
nych typów.

polimorficz ne
Powiązane z funkcją, która może obsługiwać więcej niż jeden typ.
ukrywanie informacji
Zasada, zgodnie z którą interfejs zapewniany przez obiekt nie powinien być zależny od jego
implementacji, a w szczególności od reprezentacji atrybutów obiektu.

Ćwiczenia
Ćwiczenie 17.1.

Kod z tego rozdziału znajdziesz w pliku Time2.py. Zmień atrybuty obiektu Time tak, aby miały po-
stać pojedynczej liczby całkowitej reprezentującej liczbę sekund, jaka upłynęła od północy. Zmo-
dyfikuj następnie metody (i funkcję i nt _to_ti me) pod kątem współpracy z nową implementacją.
Nie powinno być konieczne modyfikowanie kodu testowego w funkcji rrain. Po zakończeniu działań
dane wyjściowe powinny być takie same jak wcześniej.
Rozwiązanie: plik Time2_soln.py.

Ćwiczenie 17.2.

To ćwiczenie stanowi powiastkę umoralniającą na temat jednego z najczęściej występujących i naj-


trudniejszych do znalezienia błędów w kodzie Python. Utwórz definicję klasy o nazwie Kangaroo
z następującymi metodami:
1. Metodą _ i nit_ , która inicjalizuje atrybut o nazwie pouch_ contents w postaci pustej listy.
2. Metodą o nazwie put _ in_pouc h, która pobiera obiekt dowolnego typu i dodaje go do metody
pouc h_ contents.

3. Metodą _ s tr_ zwracającą reprezentację łańcuchową obiektu Kangaroo oraz zawartość torby
(ang. pouch).
Przetestuj kod, tworząc dwa obiekty Kangaroo, przypisując je zmiennym o nazwach kanga i roo, a na-
stępnie dodając wartość zmiennej roo do zawartości torby, czyli wartości zmiennej kanga.

Otwórz plik BadKangaroo.py. Zawiera on rozwiązanie poprzedniego problemu z jednym dużym


i poważnym błędem. Znajdź i usuń go.
Jeślisobie z tym nie poradzisz, możesz pobrać plik GoodKangaroo.py, w którym objaśniono problem
i przedstawiono jego rozwiązanie.

212 I Rozdział 17. Klasy I metody


ROZDZIAŁ 18.
Dziedziczenie

Dziedziczen ie to element języka, który jest najczęściej kojarzony z programowaniem obiekto-


wym. Jest to zdo lność do definiowania nowej klasy, która stanowi zmodyfikowaną wersję istni eją­
cej klasy. W rozdziale zademo nstrowa łem dziedziczenie z wykorzystaniem klas, które reprezen-
tują rozdane karty do gry, talie kart i rozdania pokerzysty.

Jeśli
nie grasz w pokera, na jego temat możesz poczytać pod adresem h ttp://pl.wikipedia.org/wiki/
Poker. Nie jest to jed nak konieczne. Dowiesz się, jaką wiedzę trzeba będzie sobie p rzyswoić do
wyko nania ćwi czeń.
Przykładowy kod użyty w tym rozdziale znajdziesz w pliku Card.py, który, tak jak pozostałe pliki
z tego ro zdziału ,
jest dostępny pod adresem ftp:/lftp.helion.pl/przyklady/ myjep2.z ip.

Obiekty kart
W talii znajdują się 52 karty, z których każ da należy do jednego z czterech kolorów, a także do
jednej spośród 13 rang. Kolory to pik, kier, karo i trefl (podane w kolej ności m alej ącej obowiązu­
jącej w b rydżu} . Rangi to as, 2, 3, 4, 5, 6, 7, 8, 9, 10, walet, dama i król. Zależni e od gry, w którą
grasz, as może być mocniejszy od króla lub słabszy od dwójki.
Aby zdefi niować nowy obiekt reprezentujący kartę do gry, oczywiste jest, jakie powinny być atry-
buty: r a nk (ranga} i s ui t (kolor}. Jedną z możliwości jest zastosowanie łańcuchów zawierających
słowa, takich jak ' Spade ' (w p rzypadku kolorów) i ' Dama ' (dla rang). Z taką i mplementacją zwi ą­
zany jest problem p olegający na tym, że nie będzie łatwe po równanie kart w celu stwierdzenia,
która miała większą ra ngę lub m ocniejszy kolor.

Alternatywą jest zastosowanie liczb całko witych do kodowania rang i kolorów. W tym ko ntekści e
termin kodowanie oznacza, że zostanie zdefiniowane odwzorowanie między liczbami i kolorami lub
między liczbami i rangami. Tego rodzaju kodowanie nie ma nic wspólnego z poufności ą (w tym
przypadku byłoby to szyfrowanie}.

Na przykład następująca tabela prezentuje kolory i odpowiadające im kody w postaci liczb całkowitych:
Pik 3
Kier 2

213
Karo 1
Trefl o
Takie kody ułatwiają porównanie kart. Ponieważ mocniejsze kolory są odwzorowywane na więk­
sze liczby, możliwe jest porównanie kolorów przez porównanie ich kodów.

Odwzorowanie związane z rangami jest dość oczywiste. Każda z rang liczbowych odwzorowywana jest
na odpowiednią liczbę całkowitą. W przypadku kart z postaciami odwzorowanie jest następujące:

Walet 11
Dama 12
Król 13

Używam symbolu-->, aby było wyraźnie widoczne, że te odwzorowania nie są częścią programu
z kodem Python. W chodzą one w skład projektu programu, ale nie pojawiają się bezpośrednio w kodzie.

Definicja klasy Card ma następującą postać:


cl ass Card :
"""Repre: entu;e standardową grę w karty."""
def i nit {self, su it =O, rank =2 ) :
s el f.su it = suit
s el f.rank = rank

Jak zwykłe metoda i nit pobiera dla każdego atrybutu parametr opcjonalny. Karta domyślna to
dwójka trefl.

W celu utworzenia obiektu karty wywołujesz klasę Card z kolorem i rangą żądanej karty:
queen of d 1amonds = Card( l, 12 )

Atrybuty klasy
Aby wyświetlić obiekty kart w sposób ułatwiający użytkownikom czytanie kodu, konieczne jest od-
wzorowanie kodów w postaci liczb całkowitych na odpowiadające im rangi i kolory. Oczywistym
sposobem pozwalającym na to jest zastosowanie list łańcuchów, które są przypisywane atrybutom
klasy:
# wewnątr: klasy Card
s uit_names =[ 'trefl ' , ' ka ro ', 'kier', 'pik']
rank~names. ===[B rak, 1 As 1 , 1 2 1 , 1 3 1 , 1 4 ' , ' 5 1 , 1
61, 1
71 ,

def ~str__ {sel f) :


ret urn '%s ko l o ru %s ' % {Card . ran k_names[self . ran k].
Card . s ui t _names [sel f . suit])

Zmienne, takie jak suit_names i r a nk_names, które są definiowane wewnątrz klasy, lecz poza jakąkolwiek
metodą, nazywane są atrybutami klasy, ponieważ są skojarzone z obiektem klasy Card.

Termin ten odróżnia te zmienne od zmiennych takich jak suit i rank, które są nazywane atrybutami
instancji, ponieważ są powiązane z konkretną instancją.

214 I Rozdział 18. Dzledzlaenle


Dostęp do obu rodzajów atrybutów uzyskiwany jest za pomocą notacji z kropką. Na przykład w meto-
dzie _ s tr_ obiekt self to obiekt Card, a se lf . ran k to ranga karty. Podobnie Card to obiekt klasy,
a Card. rank_ names to lista łańcuchów skojarzonych z klasą.

Każda karta ma własny kolor (sui t ) i rangę (rank), ale istnieje tylko jedna kopia zmiennych s uit_ names
i ran k names.

Podsu mowując, wyrażenie Ca r d . rank_ name s [ sel f . rank]oznacza: „Użyj atrybutu r an k obiektu self
jako indeksu listy r a nk_ names z klasy Ca r d i wybierz odpowiedni łańcuch".
Pierwszym elementem listy ran k_ names jest wartość No ne, ponieważ nie istnieje żadna karta z rangą
ze rową. Do łączaj ąc wartość No ne jako element utrzymuj ący miejsce, uzyskuje się odwzorowanie
o przydat nej właściwości powodującej , że indeks 2 odwzorowywany jest na ła ńcuch ' 2 ' itd. Aby
uniknąć takiego zabiegu, zam iast listy mo żna zastosować słownik.

Za pomocą dotychczas prezentowanych metod moż na utwo rzyć i wyświetlić karty:


>>> card l = Card (2, 11 )
>>> print(card l )
Wal et koloru ki e r

Na rysunku 18.l pokazano diagram obiektu klasy Ca r d i jed ną instancję w postaci obiektu Card.
type to typ obiektu klasy Card. card l to instancja klasy Ca r d, dlatego jej typ to Car d. Aby zaoszczędzić
miejsce, nie prezentowałem zawartości zmiennych su it_ name s i ran k_ names.

Card suit_ na mes

rank_names

Card
cardl suit ------ 1
rank _,... 11

Rysunek 18.1. Diagram obiektów

Porównywanie kart
W przypadku typów wbudowanych dostępne są operatory relacyjne (<, >, == itp.), które porównują
wartości i określają, kiedy jedna jest większa od drugiej, mniejsza od niej lub są sobie równe. Dla
typów definiowanych przez p rogramistę możliwe jest przesłonięcie zachowania operatorów wbudo-
wanych przez zapewnienie metody o nazwie _ l t _ , co jest skrótem od słów less than.

Metoda ta pobiera dwa parametry self i o t her oraz zwraca wartość True, j eśli wartość parametru
s e l f jest na pewno mniejsza niż wartość parametru o t he r.

Porównywanie kart I 215


Poprawne uporządkowanie kart nie jest oczywiste. Co na przykład jest lepsze: trójka trefl czy dwójka
karo? Pierwsza karta ma wyższą rangę, ale druga karta m a mocniejszy kolor. Aby porównać karty,
musisz zdecydować, czy waż niejsza jest ranga, czy kolor.

Choć odpowiedź może zależeć od gry, w j aką grasz, w celu uproszczenia przykładów dokonajmy
arbitralnego wyboru, zgodnie z którym waż niejszy jest kolor. Oznacza to, że wszystkie karty pik prze-
wyższają stopniem wszystkie karty karo itd.

Po podjęciu tej decyzji możesz zdefiniować metodę _ l t _ :


# wewnątr: klasy Card
def lt (5elf, ot her) :
# sprawd:eme kolorów
if 5elf. 5uit < ot her . 5uit : ret urn True
i f 5elf. 5uit > ot her . 5ui t: ret urn Fa l 5e
# kolory są identyc:ne, sprawd: enie rang
r et urn 5el f . r ank < ot her . rank

Korzystając z porównania krotek, powyższą definicję możesz zapisać w bardziej zwięzły sposób:
# wewnątr: klasy Card
def lt (5elf, ot her) :
t l = 5elf . 5uit , 5elf . rank
t 2 = othe r . 5uit , ot her . rank
r et urn t l < t 2

W ramach ćwiczenia utwórz metodę _ l t _ dla obiektów Ti me. Choć możesz użyć porównania krotek,
możesz też rozważyć porównanie liczb całkowitych.

Talie
Gdy już dysponujesz obiektami kart, następnym krokiem jest zdefiniowanie talii. Ponieważ talia zło­
żona jest z kart, naturalne jest, że każda talia będzi e zawierać li stę kart jako atrybut.

Poniżej zaprezentowano definicję klasy Dec k. Metoda init tworzy atrybut cards i generuje standardo-
wy zestaw 52 kart:
cla55 Dec k:
def init (5elf) :
5el f. card5 = []
f or 5uit in range( 4) :
f or rank in range(! , 14 ) :
card = Card(5uit , rank)
5el f. card5 . append{card)

Najprostszym sposobem zapełnienia talii jest zastosowanie pętli zagnieżdżonej. Pętla zew nętrzna
wylicza kolory od O do 3. Pętla wewnętrzna wylicza rangi od 1 do 13. Każda iteracja tworzy nowy
obiekt karty z bieżącym kolorem i rangą, a następnie do łącza go do self . cards.

Wyświetlanie talii
Oto metoda _ st r_ klasy De ck:
# wewnątr: klasy Deck
def __5tr__ (5el f ) :

216 I Rozdział 18. Dzledzlaenle


res = []
for card in sel f. cards :
res.append(str( card))
return '\n' . join (res)

Metoda demonstruje efektywny sposób akumulowania dużego łańcucha. Polega on na budowaniu li-
sty łańcuchów, a następnie użyciu metody łańcuchowej joi n. Funkcja wbudowana str wywołuje
metodę _s tr_ dla każdego obiektu karty i zwraca rep rezentację łańcuchową.

Ponieważ metoda j o i n wywoływana jest przy znaku nowego wiersza, karty są oddzielone tymi znakami.
Oto wynik:
>>> deck = Dec k()
»> print(dec k)
As koloru trefl
2 kol oru trefl
3 kol oru trefl

10 koloru pi k
Wal et koloru pik
Dama koloru pik
Król koloru pi k

Nawet pomimo tego, że wynik zajmuje 52 wiersze, ma postać jednego długiego łańcucha, który zawie-
ra znaki nowego wiersza.

Dodawanie, usuwanie, przenoszenie i sortowanie


Do rozdania kart pożądana będzie metoda, która usuwa kartę z talii i zwraca ją. Metoda listy pop za-
pewnia wygodny sposób, który to umożliwia:
# wewnqtr: klasy Deck
def pop_card(self) :
return self . cards . pop()

Poni eważ metoda pop usuwa ostatniq kartę na liście, rozdanie odbywa się od spodu talii.

W celu dodania karty możesz zastosować metodę listy append:


# wewnqtr: klasy Deck
def add_card(sel f, card) :
self.cards . append(card)

Metoda, która używa innej metody bez realizowania zbyt wielu działań, nazywana jest czasami
„okleiną". Metafora wywodzi się ze stolarstwa, gdzie okleina oznacza cienką warstwę dobrej jakości
drewna przyklejonego do powierzchni tańszego elementu z drewna w celu poprawienia jego wyglądu.

W tym przypadku add_card to „cienka" metoda wyrażająca operację na liście w sposób odpowiedni
odnośnie do talii. Metoda poprawia wygląd lub interfej s implementacji.

W ramach kolejnego przykładu można utworzyć metodę klasy Dec k o nazwie shuff l e , która ko-
rzysta z funkcj i s huf f l e modu łu random:
# wewnqtr: klasy Deck
def shuffle (sel f ) :
random . s huffl e(sel f. cards)

Dodawanie, usuwanie, pnenoszenle I sortowanie I 217


Nie zapomnij zaimportować modułu random.

W ramach ćwiczenia utwórz metodę klasy Dec k o nazwie s ort, która za pomocą metody listy sor t
sortuje karty talii (Dec k). Metoda sort używa metody _ l t _ zdefiniowanej w celu określenia ko-
lejności kart.

Dziedziczenie
Dziedziczenie to zdolność zdefiniowania nowej klasy, która jest zmodyfikowaną wersją klasy już
istniejącej. Przyjmijmy na przykład, że klasa ma reprezentować rozdanie, czyli karty trzymane przez
jednego gracza. Rozdanie przypomina talię: oba zawierają kolekcję kart i wymagają takich czynności
jak dodawanie i usuwanie kart.
Rozdanie różni się jednak od talii. Istnieją czynności pożądane w przypadku rozdania, które nie mają
sensu dla talii. Na przykład w pokerze można porównać zawartość dwóch rozdań, aby sprawdzić,
które wygrywa. W brydżu możliwe jest obliczenie wyniku dla zawartości rozdania w celu przeprowa-
dzenia licytacji.
Takie relacje między klasami, podobne, lecz różne, kwalifikują się do zastosowania dziedziczenia. Aby
zdefiniować nową klasę dziedziczącą z istniejącej klasy, jej nazwę umieść w nawiasach okrągłych:
cla55 Hand(Dec k) :
"""Repre: entu1e ro:danie : kartami w gr:e. """

Definicja ta wskazuje, że klasa Ha nd dziedziczy z klasy Deck. Oznacza to, że możliwe jest zastosowanie
metod takich jak pop_ card i a dd_ card zarówno dla obiektów klasy Ha nd, jak i obiektów klasy Deck.
Gdy nowa klasa dziedziczy z już istniejącej, istniejąca klasa nazywana jest nadrzędną, a nowa klasa to
klasa podrzędna.

W omawianym przykładzie klasa Hand dziedziczy metodę _ i ni t _ z klasy Deck, ale w rzeczywistości
nie realizuje tego, czego oczekujemy: zamiast wypełniać rozdanie 52 nowymi kartami, metoda ta
w przypadku obiektów klasy Hand powinna zai nicjować atrybut car d s za pomocą pustej listy.
Jeśli metoda ini t zostanie zapewniona w klasie Ha nd, przesłania metodę klasy Dec k:
# wewnątr: klasy Hand
def init (5elf, label =' ' ) :
5el f. card5 = []
5el f.l abel = la bel

W momencie utworzenia obiektu Ha nd interpreter języka Python wywołuje tę metodę in i t, a nie


odpowiadającą jej metodę klasy Deck:

>>>hand = Hand('nowa partia')


>>> hand . card5
[]
» > hand . 1abe1
'nowa partia'

Inne metody są dziedziczone z klasy Deck, dlatego do rozdania karty można użyć metod pop_ car d
i add card:
>>> dec k = Dec k()
>>> card = dec k. pop_card()

218 I Rozdział 18. Dzledzlaenle


> >>hand .add_ca rd(card)
»> pr int(hand)
Król kol oru pik

Następnym oczywistym krokiem jest hermetyzacja tego kodu w metodzie o nazwie move_ cards:
# wew nątr: klasy Deck
def move_card5(5elf, ha nd , num ) :
for i i n range(num ) :
hand .add_ca rd(5elf. pop_card())

Metoda move _ car d s pobiera dwa argumenty, czyli obiekt klasy Hand i liczbę kart do rozdania. Me-
toda modyfikuje obiekty hand i self oraz zwraca wartość None.

W niektórych grach karty są przemieszczane z jednej dłoni do drugiej lub z dłoni z powrotem na
talię. Za pomocą metody move_ car ds możesz wykonać dowolną z następujących operacji: obiekt
se l f może być obiektem klasy Dec k lub Hand, a obiekt hand (rozdanie), pomimo swojej nazwy, mo-
że też być obiektem klasy Deck.

Dziedziczenie to przydatna opcja. Niektóre programy, które bez dziedziczenia byłyby pełne powtó-
rzeń, dzięki niemu można pisać w bardziej elegancki sposób. Dziedziczenie może ułatwić ponowne
wykorzystanie kodu, ponieważ dzięki niemu możliwe jest dostosowanie zachowania klas nadrzędnych
bez konieczności modyfikowania ich. W niektórych sytuacjach struktura dziedziczenia odzwier-
ciedla naturalną strukturę problemu, co sprawia, że projekt jest łatwiejszy do zrozumienia.
Dziedziczenie może jednak spowodować, że programy staną się mniej czytelne. W momencie wywo-
ływania metody nie jest czasami jasne, gdzie szukać jej definicji. Odpowiedni kod może być roz-
mieszczony w kilku modułach. Ponadto wiele celów, których zrealizowanie możliwe jest z wykorzy-
staniem dziedziczenia, można też w podobnym lub lepszym stopniu osiągnąć bez niego.

Diagramy klas
Dotychczas zaprezentowałem diagramy warstwowe, które pokazują stan programu, a także diagramy
obiektów przedstawiające atrybuty obiektu i ich wartości. Diagramy te reprezentują chwilę czasu
w trakcie wykonywania programu, dlatego zmieni ają się podczas jego działania.
Diagramy są też bardzo szczegółowe, a w przypadku niektórych zastosowań zbyt dokładne. Dia-
gram klas to bardziej abst rakcyjna reprezentacja struktury programu. Zamiast prezentowania po-
szczególnych obiektów taki diagram pokazuje klasy i relacje między nimi.
Istnieje kilka rodzajów relacji między klasami:
• Obiekty w jednej klasie mogą zawierać odwołania do obiektów w innej klasie. Na przykład
każdy prostokąt zawiera odwołanie do punktu, a każda talia ma odwołania do wielu kart. Te-
go rodzaju relacja identyfikowana jest przez termin MA (np. prostokąt ma punkt).
• Jedna klasa może dziedziczyć z drugiej. Taka relacja identyfikowana jest przez termin JEST
(np. rozdanie jest odmianą talii).
• Jedna klasa może zależeć od innej w tym sensie, że obiekty w jednej klasie pobieraj ą jako pa-
rametry obiekty w drugiej klasie lub używają obiektów tej klasy jako części obliczenia. Tego
rodzaju relacja nazywana jest zależnością.

Diagramy klas I 219


Diagr am klas to graficzna reprezentacja tych relacji. Na przykład na rysunku 18.2 pokazano relacje
między klasami Card, Deck i Hand.

Deck (ard

Hand

Rysunek 18.2. Diagram klas

Strzałka z trójkątnym zakończeniem reprezentuje relację JEST. W tym przypadku wskazuje ona,
że klasa Hand dziedziczy z klasy Deck.

Standardowy grot strzałki reprezentuje relację MA. W tym przypadku klasa Dec k zawiera odwołania
do obiektów klasy Card.

Gwiazdka(*) widoczna w pobliżu grota trzeciej strzałki to mnogość. Wskazuje o na, ile obiektów
Card zawiera obiekt Deck. M nogość może być wyrażona zwykłą liczbą, taką jak 52, zakresem (np.
5 .. 7) lub gwiazdą, która wskazuje, że obiekt klasy Dec k może zawi erać dowo l ną liczbę obiektów
klasy Card.

W przypadku tego diagramu nie występują żadne zależności . Standardowo zostałyby one zaprezento-
wane p rzy użyciu kreskowanej strzałki. Jeśli ist nieje wiele zależności, czasami są one pomijane.

Bardziej szczegółowy diagram może pokazywać, że obiekt klasy Deck może zawierać listę obiektów kla-
sy Card, ale typy wbudowane, takie jak lista i słownik, nie są zwykle uwzględniane w diagramach klas.

Hermetyzacja danych
W poprzednich rozdziałach zademonstrowałem plan projektowania, który można określić mianem
projektu obiektowego. Zidentyfikowaliśmy niezbędne obiekty, takie jak Point, Rectangle i Ti me, a także
zdefiniowaliśmy reprezentujące je klasy. W każdym przypadku istnieje oczywiste powiązanie między
obiektem i pewnym elementem świata rzeczywistego (a przynajmniej świata matematycznego).
Czasami jednak mniej oczywiste jest, jakie obiekty są niezbędne, a także jak powinny prowadzić
ze sobą interakcję. W takiej sytuacji wymagany jest inny plan projektowania. W taki sam sposób,
w jaki poznałeś interfejsy funkcji z wykorzystaniem hermetyzowania i uogólniania, możesz poznać
interfejsy klas za pomocą hermetyzowania danych.
Analiza Markowa (omówiona w podrozdziale „Analiza Markowa" rozdziału 13.) zapewnia dobry
przykład. Jeśli pobierzesz mój kod znajdujący się w pliku markov.py dostępnym pod adresem
ftp:/lftp.helion.pl!przyklady/myjep2.zip, zauważysz, że użyto w nim dwóch zmiennych globalnych
suffi x_map i pref i x, które są odczytywane i zapisywane w przypadku kilku funkcji.
5u ffix_map = {}
pref i x = ()

220 I Rozdział 18. Dzledzlaenle


Ponieważ zmienne te są globalne, jednocześnie można uruchomić tylko jedną analizę. Jeśli zostałyby
wczytane dwa teksty, ich prefiksy i sufiksy zostałyby dodane do tych samych struktur danych (zapew-
nia to interesujący wygenerowany tekst).

Aby przeprowadzić wiele analiz i zapewnić ich odrębność, można dokonać hermetyzacji każdej
analizy w obiekcie. Odpowiedni kod ma następującą postać:
cl ass Markov :
def ini t (self) :
s el f. s uffix_map = {}
self. pref i x = ()

W dalszej kolejności funkcje są transformowane do postaci metod. Oto na przykład funkcja


process _ wo rd:
def process_word(sel f, word , order =2) :
i f len(self. prefi x) < orde r :
s elf. prefi x += (word , )
ret urn
try :
s elf. s uf fi x_map [s elf. pref i x] . appe nd (word)
ex cept KeyE rror :
# w pr:ypadku braku wpisu dla danego prejiksu;est on twor:ony
s elf. s uff i x_map [self. prefix] = [word]
s el f. pref i x = s hi f t(self. prefix, word)

Przekształcanie takiego programu, czyli zmiana projektu bez modyfikowania zachowania, to ko-
lejny przykład refaktoryzacji (zajrzyj do podrozdziału „Refaktoryzacja" rozdziału 4.).
Ten przykład sugeruje plan projektowania na potrzeby tworzenia obiektów i metod:
1. Zacznij od napisania funkcji, które odczytują i zapisują zmienne globalne (w razie potrzeby).
2. Po uzyskaniu dzi ałającego programu poszukaj skojarzeń między zmiennymi globalnymi i funk-
cjami, które z nich korzystają.
3. Dokonaj hermetyzacji powiązanych zm iennych jako atrybutów obiektu.
4. Przeprowadź transformację skojarzonych funkcji do postaci metod nowej klasy.

W ramach ćwiczenia pobierz mój kod analizy Markowa zawarty w pliku markov.py, a następnie
wykonaj opisane powyżej kroki w celu dokonania hermetyzacji zmiennych globalnych jako atry-
butów nowej klasy o nazwie Mar kov.

Rozwiązanie: plik Markov.py (zwróć uwagę na dużą literę M).

Debugowanie
Dziedziczenie może utrudnić debugowanie, ponieważ w momencie wywołania metody w obiekcie
stwierdzenie, jaka metoda zostanie wywołana, może okazać się trudne.

Załóżmy, że tworzysz funkcję działającą z obiektami klasy Ha nd . Wymagasz, aby funkcja współ­
działała
ze wszystkimi odmianami takich obiektów (np. Poke r Hand , Bri dge Hand itp.). Jeśli wywołasz
metodę taką jak shuf fle, możesz uzyskać metodę zdefiniowaną w klasie Dec k. Jeżeli jednak metoda ta

Debugowanie I 221
zostanie przesłonięta przez dowolną podklasę, uzyskasz wersję metody z tej klasy. Talcie zachowanie
jest zwykle pozytywne, ale może wprowadzać niejasności.

Zawsze gdy nie masz pewności odnośnie do przepływu wykonywania programu, najprostszym roz-
wiązaniem jest dodanie instrukcji print na początku odpowiednich metod. Jeśli metoda Deck . sh uffle
wyświetla komunikat o treści takiej jak Dz iał ani e met ody Deck . shuf f l e, oznacza to, że w trakcie
działani a programu metoda śledzi przepływ wykonywania.

W ramach alternatywy możesz skorzystać z poniższej funkcji, która pobiera obiekt i nazwę metody
Qako łańcuch), a następnie zwraca klasę zapewniającą definicję metody.
def f ind_defining_cl ass{obj , met h_name) :
for t y in t ype (obj ) . mro() :
if met h_name in ty. diet
ret urn t y

O to przykład:
>>> hand = Hand ( )
>>> fi nd_defining_cl ass( hand , ' s huffl e ')
<c l ass ' Card .Deck'>

A zatem dla tego obiektu Hand metoda shuf fl e jest metodą klasy Deck.

Funkcja fi nd_defi ni ng_class używa metody mro do uzyskania listy obiektów klasy (typów), które będą
przeszukiwane pod kątem metod. mro to skrót od słów method resolution order, czyli sekwencji
klas, jakie interpreter języka Python przeszukuje w celu ustalenia nazwy metody.

O to sugestia projektowa: w momencie p rzesłoni ęcia metody interfejs nowej metody powinien być
taki sam jak interfejs starej. Nowa metoda powinna pobierać identyczne parametry, zwracać ten
sam typ oraz przestrzegać jednakowych warunków wstępnych i końcowych. W przypadku postępo­
wania zgodnie z tą regułą stwierdzisz, że dowolna funkcja zaprojektowana pod kątem współpracy
z instancją klasy nadrzędnej talciej jak Deck będzie też współdziałać z instancjami klas podrzędnych ta-
kich jak Hand i PokerHand.

Jeżelinaruszysz tę regułę, która nazywana jest regułq zastępowania Liskov, kod niestety „rozsypie się"
jak domek z kart.

Słownik
kodowanie
Reprezentowanie jednego zbioru wartości za pomocą innego zbioru wartości przez utworze-
nie odwzorowania między nimi.

atrybut klasy
Atrybut skojarzony z obiektem klasy. Atrybuty klasy są definiowane wewnątrz definicj i klasy,
lecz poza ob rębem jakiejkolwiek metody.
atrybut instancj i
Atrybut skojarzony z i nstancją klasy.

222 I Rozdział 18. Dzledzlaenle


okleina
Metoda lub funkcja zapewniająca odmienny interfejs innej funkcj i bez przeprowadzania
nadmiernej ilości obliczeń.
dz iedz iczenie
Zdolność definiowania nowej klasy, która jest zmodyfikowaną we rsją wcześniej zdefiniowa-
nej klasy.

klasa nadrzędna

Klasa, z której dziedziczy klasa podrzędna.


klasa podrzędna
Nowa klasa, nazywana też podklasą, która tworzona jest przez dziedziczenie z istniejącej klasy.
relacja JEST
Relacja między klasą podrzędną i jej klasą nadrzędną.

relacja MA
Relacja między dwiema klasami, w ramach której instancje jednej klasy zawierają odwołania
do instancji drugiej klasy.
zależność

Relacja między dwiema klasami, w ramach której instancje jednej klasy używają instancji in-
nej klasy, ale nie przechowuj ą ich jako atrybutów.
diagram klas
Diagram prezentujący klasy w programie oraz relacje między nimi.

mnogość

Notacja na diagramie klas pokazująca w przypadku relacji MA, ile istnieje odwołań do instancji
innej klasy.

hermetyzowanie danych
Plan projektowania programu obejmujący prototyp używający zmiennych globalnych, a także
wersję finalną, która przekształca zmienne globalne w atrybuty instancji.

Ćwiczenia
Ćwiczenie 18.1.

Dla poniż szego kodu programu narysuj diagram klas UML, który prezentuje te klasy i relacje
między nimi.
class PingPongParent :
pass
class Ping(PingPongParent) :
def init (sel f, pong) :
self. pong = pong

Ćwlaenla I 223
class Pong(PingPong Parent) :
def __ init ~ (s elf, pings=None) :
if pings is None :
self . pings []
el se:
self . pings pings
def add_ping(self, ping) :
s elf . pings . append(ping)
pong = Pong ()
ping = Ping(pong)
pong . add_ping(ping)

Ćwiczenie 18.2.

Utwórz metodę klasy Dec k o nazwie deal_hands, która pobiera dwa parametry: liczbę rozdań oraz
liczbę kart przyp adających na rozdanie. Metoda powinna two rzyć odpowiednią liczbę obiektów
klasy Hand, zapewniać właściwą liczbę kart w rozdaniu i zwracać listę rozdań.

Ćwiczenie 18.3.

Oto możliwe rozdania w pokerze podane zgodnie ze zwiększającą się wartości ą i zmniejszającym
się prawdopodobieństwem:

para
Dwie karty o takiej samej randze.
dwie pary
Dwie pary kart o takiej samej randze.

trójka
Trzy karty o takiej samej randze.
strit
Pięć kart o rangach tworzących sekwencję (asy mogą być wysokie lub niskie, dlatego sekwen-
cja as-2-3-4-5 to strit, podobnie jak sekwencja 10-walet-dama-król-as, lecz sekwencja dama-
król-as-2-3 już nie).

kolor
Pięć kart w tym samym kolorze.

ful
Trzy karty o jednej randze oraz dwie karty o innej randze.

kareta
Cztery karty o tej samej randze.
poker
Pięć kart tworzących sekwencję Qak podano powyżej), które mają taki sam kolo r.
Celem tych ćwiczeń jest oszacowanie prawdopodobieństwa uzyskania wymienionych rozdań.

224 I Rozdział 18. Dzledzlaenle


1. W dołączonych do książki przykładach znajdź następujące pliki:
Card.py
Kompletna wersja klas Card, Deck i Hand omówionych w rozdziale.
PokerH and.py
Niekompletna implementacja klasy reprezentującej rozdanie pokerowe oraz kod testujący ją.
2. Po uruchomieniu program w pliku PokerHand.py zapewnia siedem rozdań pokerowych złożo­
nych z 7 kart, a ponadto sprawdza, czy dowolne z nich zawiera kolor. Przeczytaj uważnie ten
kod przed kontynuowaniem działań.
3. Do pliku PokerHand.py dodaj metody o nazwach has_pair, has_t wopai r itp„ które zwracają
wartość True lub Fa l se zależnie od tego, czy rozdanie spełnia odpowiednie kryteria. Napisany
kod powinien dzi ałać poprawnie w przypadku rozdań zawierających dowolną liczbę kart
(choć najczęstsze wielkości rozdań identyfikowane są przez liczby 5 i 7).

4. Utwórz metodę o nazwie classify określającą dla rozdania klasyfikację opartą na największej
wartości i ustawiającą odpowiednio atrybut l abel. Na przykład rozdanie złożone z siedmiu
kart może zawierać kolor i parę, dlatego powinno uzyskać etykietę kolor.
5. Gdy przekonasz się, że działają metody klasyfikacji, następnym krokiem będzie oszacowanie
prawdopodobieństw różnych rozdań. Utwórz w pliku PokerHand.py funkcję, która tasuje ta-
lię kart, dzieli ją na rozdania, klasyfikuje je i określa liczbę wystąpi eń różnych klasyfikacji.

6. Wyświetl tabelę klasyfikacji i ich prawdopodobieństw. Uruchamiaj program przy użyciu coraz
większej liczby rozdań do momentu, aż wartości wyjściowe przybliżą się do rozsądnego stopnia
dokładności. Porównaj wyniki z wartościami dostępnymi pod adresem http://en.wikipedia.org/
wiki/Hand_rankings.

Rozwiązanie: plik PokerHandSoln.py.

Ćwlaenla I 225
226 I Rozdział 18. Dzledzlaenle
ROZDZIAŁ 19.

Przydatne elementy

W przypadku tej książki jednym z moich celów było przekazanie Ci wiedzy na temat języka Python
w jak najmniejszym zakresie. Gdy istniały dwa sposoby osiągnięcia czegoś, wybierałem jeden z nich
i unikałem wspominania o drugim. Czasami przedstawiałem drugi sposób w ramach ćwiczenia.

Chcę teraz powrócić do paru dobrych rzeczy, które pominąłem. Język Python zapewnia kilka ele-
mentów, które nie są tak naprawdę niezbędne, ponieważ możesz bez nich pisać dobry kod. Korzysta-
jąc z nich, możesz jednak czasami utworzyć kod, który jest bardziej zwięzły, czytelny lub efektywny,
a niekiedy ma wszystkie trzy wymienione cechy.

Wyrażenia warunkowe
Wyrażenia warunkowe zaprezentowałem w podrozdziale „Wykonywanie warunkowe" rozdziału 5.
Instrukcje warunkowe są często używane na przykład do wyboru jednej z dwóch wartości:
if X > O:
y = mat h.l og(x)
el se :
y = float('nan')

Instrukcja ta sprawdza, czy x to wartość dodatnia. Jeśli tak, obliczana jest wartość funkcji ma-
th. l og. W przeciwnym razie funkcja ta zgło si błąd Val ueError. Aby uni knąć zatrzymania działania
programu, generowana jest wartość NaN, która jest specjalną wartością zmiennoprzecinkową re-
prezentującą wartość niebędącą liczbą (Not a Number).

Powyższa instrukcja może zostać zapisana bardziej zwięźle za pomocą wyrażenia warunkowego:
y = math .l og(x) if x >O else fl oat('nan ' )

Wyrażenie to może zostać odczytane prawie w postaci następującego zdania w języku polskim: ,,Dla y
obliczany jest logarytm x, jeśli x jest większe niż O; w przeciwnym razie y uzyskuje wartość NaN".

Funkcje rekurencyjne mogą czasami zostać zmodyfikowane z wykorzystaniem wyrażeń warun-


kowych. Oto przykład wersji rekurencyj nej funkcji fact or i al:
def factor i al (n) :
if n == O:
return 1
el se :
return n * facto rial (n - 1)

227
Funkcja może zostać zmodyfikowana w następujący sposób:
def factorial (n) :
return 1 if n == O els.e n * f actorial (n - 1)

Innym zastosowaniem wyrażeń warunkowych jest obsługa argumentów opcjonalnych. Oto na


przykład metoda i ni t programu GoodKangaroo (zajrzyj do ćwiczenia 17.2):

def init (s.el f, name, contents.=None):


s.el f. name = name
if contents. == None :
cant e nt s. = []
s.el f. pouch_contents. = cont e nts.

Kod można przebudować w następujący sposób:

def init (s.el f, name, cont ents.=None) :


s.el f. name = name
s.el f. pouch_contents. = [] if contents. == None els.e cont ent s.

Ogólnie rzecz biorąc, instrukcję warunkową możesz zastąpić wyrażeniem warunkowym, jeśli obie
gałęzie zawi erają proste wyrażeni a, które są zwracane lub przypisywane do tej samej zmiennej.

Wyrażenia listowe
W podrozdziale „Odwzorowywanie, filtrowanie i redukowanie" rozdziału 10. zaprezentowałem
wzorce odwzorowywania i filtrowania. Na przykład poniższa funkcja pobiera listę łańcuchów, odwzo-
rowuje metodę łańcuchową capi tal i ze na elementy i zwraca nową listę łańcuchów.
def capital i ze_al l (t) :
res. = []
for s. i n t :
res.. append (s. . capi tal i ze ())
return res.

Korzystając z wyrażenia listowego, można zapisać ten kod w bardziej zwi ęzłej postaci:
def capital ize_al l (t) :
return [s. . capital ize() for s. i n t ]

Operatory w postaci nawiasów kwadratowych wskazują, że tworzona jest nowa lista. Wyrażeni e
wewnątrz tych nawiasów określa elementy listy, a w przypadku klauzuli fo r wskazuje, dla jakiego
ciągu wykonywana jest operacja przechodzenia.

Składnia wyrażenia listowego jest trochę niewygodna, ponieważ w przedstawionym przykładzi e


zmienna pętli s pojawia się w wyrażeniu przed dotarciem do definicji.

Wyrażeni a listowe mogą być też użyte do filtrowania. Na p rzykład następująca funkcja wybiera
tylko te elementy argumentu t, które są dużymi literami, po czym zwraca nową listę:
def only_upper(t) :
res. = []
for s. i n t :
i f s. . is.upper() :
res. . append(s.)
return res.

228 I Rozdział 19. Przydatne elementy


Funkcję można przebudować za pomocą wyrażeni a listowego:
def onl y_upper(t) :
re turn [s fo r s i n t if s . isupper() ]

Wyrażenia listowe są zwięzłe i czytelne, przynajmniej w przypadku prostych wyrażeń. Dla pętli są
one zwykle szybsze (czasem znacznie szybsze) od swoich odpowiedników. A zatem jeśli jesteś na
mnie zły za to, że nie wspomniałem o nich wcześniej, rozumiem.

Jednakże na swoją obronę zaznaczę, że wyrażenia listowe są trudniejsze do debugowania, ponieważ


nie możesz wewnątrz pętli umieścić instrukcji print. Sugeruję korzystanie z tych wyrażeń tylko
wtedy, gdy obliczenie jest na tyle proste, że prawdopodobnie zostanie poprawnie przeprowadzone za
pierwszym razem. Oznacza to, że początkujący powinni unikać wyrażeń listowych.

Wyrażenia generatora
Wyrażenia generatora przypominają wyrażenia listowe, ale zamiast nawiasów kwadratowych używają
nawiasów okrągłych:
>>> g = (x**2 f or x i n range (5))
>>> g
<generator object <ge nex pr> at Ox7f 4c45a786c0>

Wynikiem jest obiekt generatora, który potrafi dokonać iteracji ciągu wartości. W przeciwieństwie
jednak do wyrażenia listowego, obiekt nie oblicza jednocześnie wszystkich wartości. Oczekuje na po-
jawienie się odpowiedniego żądania. Funkcja wbudowana next uzyskuje następną wartość z gene-
ratora:
» > next(g)
o
» > next(g)

Po osiągnięciu końca ciągu funkcja next zgłasza wyjątek St opi terat i on. W celu przeprowadzenia
iteracj i wartości możesz też zastosować pętlę for:
>>> f or val in g :
print(val )
4
9
16

Obiekt generatora śledzi swoje położenie w ciągu, dlatego pętla for zaczyna wykonywanie w miejscu,
w którym funkcja next je zakończyła. Gdy generator przestanie działać, zgłasza wyjątek St oplte rat i on:
»> next(g)
Stoplterat ion

Wyrażenia generatora są często używane z takimi funkcjami jak sum, ma x i mi n:


>>> sum(x**2 for x i n range(5))
30

Wyraienla generatora I 229


Funkcje any i all
Język Python zapewnia funkcję wbudowaną any, która pobiera ciąg wartości boolowskich i zwraca
wartość True, gdy dowolna z wartości to True. Funkcja przetwarza listy:
>» any ( [Fal s e, False, Tr ue])
Tr ue

Funkcja ta jest jednak często używana z wyrażeniami generatora:


» > any (lett e r == ' t' for letter in ' manty ' )
Tr ue

Przykład ten nie jest zbyt przydatny, ponieważ przedstawiony kod realizuje to samo działanie co
operator i n. Możliwe jest jednak użycie funkcji any do przebudowania niektó rych funkcji wyszu-
kiwania utworzonych w podrozdziale „Wyszukiwanie" rozdziału 9. Możliwe jest na przykład za-
pisanie funkcji avoi ds w następującej postaci:
def avoids(word , f orbidden) :
ret ur n not any (let t e r in forbidden f or let t er in word)

Funkcja może zostać odczytana prawie w postaci następującego zdania w języku angielskim: „word
avoids forbi dden if there are not any fo rbidden letters in word" (słowo unika bycia niedozwolonym, je-
śli nie ma w nim żadnych niedozwolonych liter).

Użycie funkcji any z wyrażeniem generatora jest efektywne, gdyż powoduje zatrzymanie funkcji
natychmiast po znalezieniu wartości True, dlatego funkcja nie musi sprawdzać całego ciągu.
Język Python oferuje kolejną funkcję wbudowaną al l, która zwraca wartość True, jeśli każdy element
ciągu to wartość True. W ramach ćwiczenia użyj tej funkcji do zmodyfikowania funkcji uses_all
z podrozdziału „Wyszukiwanie" rozdzi ału 9.

Zbiory
W podrozdziale „Odejmowanie słowników" rozdziału 13. użyłem słowników w celu znalezienia
słów, które pojawiaj ą się w dokumencie, lecz nie na li ście słów. Utworzona przeze mnie funkcja
pobiera słownik dl zawierający słowa z dokumentu w postaci kluczy oraz słownik d2, w którym
znajduje się lista słów. Funkcja zwraca słownik zawierający klucze ze słownika dl , których nie ma
w słowniku d2:
def s ubtract(d l, d2) :
res = d i et ()
fo r key i n dl :
i f key not in d2:
res [key] = None
return res

We wszystkich tych słownikach wartości są wartościami None, ponieważ nigdy z nich nie korzystamy.
W rezultacie tracona jest pewna przestrzeń magazynowania.

Język Python zapewnia kolejny typ wbudowany o nazwie set, który zachowuje się podobnie do
kolekcj i kluczy słownika bez żadnych wartości. Dodawanie elementów do zbioru to szybka operacja,

230 I Rozdział 19. Przydatne elementy


podobnie jak sprawdzanie członkostwa. Zbiory oferują metody i operatory do realizowania operacji
wyznaczania wspólnego zbioru.
Na przykład operacja odejmowania zbiorów jest dostępna jako metoda o nazwie di fference lub w po-
staci operatora - . Możliwe jest zatem zmodyfikowanie funkcji subt ract w następujący sposób:
def subtract(d l, d2) :
return set(d l ) - set(d2)

Rezultatem jest zbiór, a nie słownik, ale w przypadku operacji takich jak iteracja zachowanie jest
identyczne.

Niektóre z ćwiczeń zamieszczonych w książce można z wykorzystaniem zbiorów wykonać w spo-


sób zwięzły i efektywny. Oto na przykład rozwiązanie dla funkcji has_dupl i cates z ćwiczenia 10.7,
w którym zastosowano słownik:
def has_duplicates(t) :
d = (}
for x i n t :
ifin d:
X
return Tr ue
d [x] = Tr ue
return False

Gdy element pojawia się po raz pierwszy, dodawany jest do słownika. Jeśli ten sam element wy-
stąpi ponownie, funkcja zwraca wartość True.

Za pomocą zbiorów tę samą funkcję można zdefiniować następująco:


def has_duplicat es(t) :
return len(set(t)) < l e n(t)

Element może pojawić się w zbiorze tylko raz, dlatego w sytuacji wystąpienia elementu w zbiorze
t więcej niż raz zbiór będzie mniejszy niż zbiór t. Jeśli nie ma żadnych duplikatów, zbiór będzie
takiej samej wielkości jak zbiór t .

Możliwe jest też zastosowanie zbiorów do wykonania niektórych ćwiczeń z rozdziału 9. Oto na
przykład wersja funkcji uses _onl y z pętlą:
def uses_only(word, avai l abl e) :
f or l etter in word :
if letter not in avail abl e :
return Fal se
return True

Funkcja ta sprawdza, czy wszystkie litery słowa word znajdują się w zbiorze availabl e. Funkcję
można przebudować do następującej postaci:

def uses_only(word, available) :


return set(word) <= set(availabl e)

Operator < = sprawdza, czy jeden zbiór jest podzbiorem innego zbioru, uwzględniając taką możliwość,
że zbiory są sobie równe, co ma miejsce, gdy wszystkie litery słowa word pojawiają się w zbiorze
ava i lable.

W ramach ćwiczenia zmodyfikuj funkcję avoi ds za po mo cą zbiorów.

Zbiory I 231
Liczniki
Licznik przypomina zbiór, z tym wyjątkiem, że jeśli element pojawia się więcej niż raz, licznik śle­
dzi liczbę jego wystąpień. Być może jesteś zaznajomiony z matematycznym pojęciem wielozbioru:
licznik stanowi naturalny sposób reprezentowania go.

Licznik jest definiowany w standardowym module o nazwie co 11ect i ons, dlatego konieczne jest
zaimportowanie go. Licznik możesz zainicjować za pomocą łańcucha, listy lub innego elementu,
który obsługuje iterację:
>>> from collections i mport Counter
>>>count = Counter{'papuga')
>>> count
Counter{ {'p' : 2 , 'a' : 2, 'u' : I , ' g' : I})

Pod wieloma względami liczniki działają podobnie do słowników. Odwzorowują o ne każdy klucz
na liczbę jego wystąpień. Tak jak w przypadku słowników, klucze muszą zapewniać możliwość
mieszania.

W przeciwieństwie do słowników liczniki nie zgłaszają wyjątku przy próbie uzyskania dostępu do
elementu, który nie występuje. Zamiast tego liczniki zwracają wartość O:
»> count [ 'd ' ]
o
Za pomocą liczników można zmodyfikować funkcję is_anagr am z ćwiczenia 10.6:
def is_anagram{word l, word2) :
return Counter(wo rd ł ) == Count e r(wo r d2)

Jeżeli dwa słowa są anagramami, zawierają identyczną liczbę takich samych liter, dlatego ich liczniki
są równe.

Liczniki zapewniają metody i operatory pozwalające na realizowanie operacji podobnych do operacji


wykonywanych na zbiorach, w tym dodawania, odejmowania, łączenia i wyznaczania części wspólnej.
Liczniki oferują przydatną metodę most_ corrrnon, która zwraca listę par wartość-częstość występowania
posortowanych w kolejności od najczęściej do najrzadziej występujących:
>>>count = Counter{'papuga')
»> for va l , freq i n count. most_ common {3) :
print{ val, f req)
p 2
a 2
u I
g I

defaultdict
Moduł col l e cti ons zapewnia też obiekt defaul tdi et przypominający słownik, z tą różnicą, że przy
próbie uzyskania dostępu do klucza, który nie istnieje, obiekt może dynamicznie wygenerować
nową wartość.

232 I Rozdział 19. Przydatne elementy


W momencie tworzenia obiektu defaul td i et podawana jest nazwa funkcji, która służy do tworzenia
nowych wartości. Funkcja stosowana do hvorzenia obiektów jest czasami nazywana fabryką. W roli
fabryk mogą być używane funkcje wbudowane, które tworzą listy, zbiory oraz inne typy:
>>> from collections import defaultdict
>>> d = defaultdi ct(list )

Zauważ, że argument to l i st, będący obiektem klasy, a nie l i st (),czyli nowa lista. Podana funkcja
nie jest wywoływana do momentu podjęcia p róby uzyskania dostępu do klucza, który nie istnieje:
>» = d [ 'nowy klucz']
>>> t
[]

Nowa lista nazwana t również jest dodawana do słownika. Jeśli zatem lista ta zostanie zmodyfi-
kowana, zmiana pojawi się w słowniku d:
>>> t . append( ' nowa wart ość')
>>> d
defaultdict(<class ' li st '>, {'nowy kl ucz ' : ['nowa wart ość ' ]})

W przypadku tworzenia słownika list często można napisać prostszy kod za pomocą obiektu defaul tdi et.
W moim ro związa niu z ćwiczenia 12.2, które możesz znaleźć w pliku anagram_sets.py dostępnym
pod adresemftp:/lftp.helion.pl!przyklady/myjep2.z ip, two rzę słownik odwzorowujący posortowa-
ny łańcuch liter na listę słów, jakie mo gą zostać utworzone za pomocą tych liter. Na przykład łańcuch
' opst ' odwzorowywany jest na listę [ ' opt s ', ' post ', ' pot s ', ' spot ' , ' stop ', 'tops '].

Oto oryginalny kod:


def all _anagrams{ f il e name) :
d = {l
f or line in open( filename ) :
wo rd = line. strip() . lower()
t = signature{word)
i f t not i n d:
d [t] = [word]
el se :
d [t]. append (word)
return d

Kod ten można uprościć przy użyciu obiektu setdefaul t, z którego mogłeś skorzystać w ćwiczeniu 11.2:
def all _anagrams(filename) :
d = {l
f or line in open{ filename ) :
wo rd = line.stri p() . l ower{)
t = signat ure {word)
d. setdef ault ( t, [] ) . append {word)
r eturn d

Rozwiązanie to m a taki mankament, że każdorazowo utworzona zostaje nowa lista, niezależnie od


tego, czy jest ona potrzebna. W przypadku list nie stanowi to dużego problemu, ale jeśli funkcja fabryki
jest złożona, może być inaczej.

Problemu tego można uniknąć i up rościć kod za pomocą obiektu defaul tdi et:
def all _anagrams(filename) :
d = default dict (list)
f or line in open{ filename ) :

defaultdlct I 233
word = 1 ine. strip{) . lower()
t = s i gnat ure (word)
d [t ] . appe nd(word)
retu rn d

W moim rozwiązani u ćwiczenia 18.3, które możesz znaleźć w pliku PokerHandSoln.py dostęp­
nym pod adresem ftp:/lftp.helion.pl!przyklady/myjep2.z ip, obiekt setdefault został użyty w funkcji
has_st rai ghtf lush. Z rozwiązaniem tym łączy się niedogodność polegająca na tworzeniu obiektu
Hand każdorazowo podczas wykonywania pętli, niezależnie od tego, czy jest to konieczne, czy nie.
W ramach ćwiczenia zmodyfikuj funkcj ę za pomocą obiektu setdefaul t.

Krotki z nazwą
Wiele prostych obiektów to właściwie kolekcje powiązanych wartości . Na przykład obiekt Poin t
zdefiniowany w rozdziale 15. zawiera dwie liczby x i y. Podczas definiowania taka klasa zwykle
jest rozpoczynana od metod i ni t i str:
cl ass Point :
def in it (s el f, x=O, y=O) :
s el f.x = x
s e 1 f. y = y
def _ str__ (sel f ) :
r et urn ' (%g , %g) ' % (se1 f. x , s e 1f . y )

Jest to mnóstwo kodu, który zawiera niewielką ilość info rmacji. Język Python zapewnia bardziej
zwięzły sposób przekazania tego samego:

f rom col l ections import namedtuple


Po int = namedtupl e ( ' Po int ', ['x ' , 'y ' ] )

Pierwszy argument to nazwa klasy do utworzenia. Drugi argument jest listą atrybutów, jakie powinny
zawierać obiekty Poi nt takie jak łańcuchy. Wartość zwracana funkcji namedtupl e to obiekt klasy:

»> Point
<c l ass ' main . Point'>

Klasa Poi nt automatycznie zapewnia metody takie jak_ i nit_ i _ str_ , dlatego nie ma potrzeby
definiowania ich.
Aby utwo rzyć obiekt Poi nt , jako funkcji używasz klasy Poi nt :
»> p = Po int( !, 2)
>>> p
Point( x = 1, y = 2)

Za pomocą podanych nazw metoda i nit przypisuje argumenty atrybutom. Metoda str wyświetla
reprezentację obiektu Poi nt i jego at rybuty.

Używając nazwy, możesz uzyskać dostęp do elementów krotki z nazwą:


>>> p .x, p . y
( 1, 2)

Możesz też jednak potraktować nazwaną krotkę jako zwykłą krotkę:


»> p [O] , p[l]
( 1, 2)

234 I Rozdział 19. Przydatne elementy


>>> X, y p
>>> X, y
(1. 2 )

Krotki z nazwą zapewniają szybki sposób definiowania prostych klas. Mankamentem jest to, że takie
klasy nie zawsze takimi pozostają. Możesz później zdecydować, że konieczne jest dodanie metod
do krotki z nazwą. W tym przypadku możesz zdefiniować nową klasę, która dziedziczy z nazwanej
krotki:
class Po intier(Po int):
# w tym mze;scu doda; wzęce; metod

Możliwe jest też powrócenie do tradycyjnej definicji klasy.

Zbieranie argumentów słów kluczowych


W podrozdziale „Krotki argumentów o zmiennej długości" rozdziału 12. pokazano, jak utworzyć
funkcję, która zbiera w krotce jej argumenty:

def printal l (*args) :


print (args)

Funkcję tę możesz wywołać z dowolną liczbą argumentów pozycyjnych (czyli pozbawionych słów
kluczowych):
»> print al l (1 , 2. 0 , '3')
( 1, 2. o, ' 3')

Operator * nie zbiera jednak argumentów słów kluczowych :


»> printall(l, 2. 0 , third= ' 3')
TypeError : printall () got an unexpect ed keyword arg ume nt 'third'

W celu zebrania argumentów słów kluczowych możesz użyć operatora **:


def printall (*args , **kwargs) :
print(args , kwargs)

Parametrowi zbierającemu słowa kluczowe możesz nadać dowolną nazwę, ale często jest wybierana
nazwa kwa r gs. Wynikiem jest słownik odwzorowujący słowa kluczowe na wartości:
»> printall(l , 2 . 0 , third= '3')
(1, 2 . O) { 't hi rd' : '3 ' }

Jeśli dysponujesz słowni kiem słów kluczowych i wartości, w celu wywołania funkcji możesz sko-
rzystać z operatora rozmieszczania **:
>>> d = dict( x = 1, y = 2)
»> Point(**d)
Point(x = 1, y = 2 )

Bez operatora rozmieszczania funkcja potraktowałaby słownik d jako pojedynczy argument pozy-
cyjny, dlatego przypisałaby d elementowi x, a ponadto zgłosiłaby błąd, ponieważ nie ma nic, co
można przypi sać elementowi y:

>>> d = dict( x = 1, y = 2)
»> Point(d)
Tracebac k (most recent cal l last) :
File "<stdin> ", li ne 1, in <module>
TypeError : new_ () missing 1 required positional arg ume nt : 'y '

Zbieranie argumentów słów kluaowych I 235


Gdy korzystasz z funkcj i mających dużą li czbę parametrów, przydatne jest utworzenie i przekazy-
wanie słowników, które określają często używane opcje.

Słownik
wyrażenie warunkowe
Wyrażenie, które zależnie od warunku ma j edną z dwóch wartości.

wyrażenie listowe
Wyrażenie z pętlą for w nawiasach kwadratowych, które zapewnia nową listę.

wyrażenie generatora

Wyrażenie z pętlą for w nawiasach okrągłych, któ re zapewnia obiekt generatora.

wieloz biór
Twór matematyczny reprezentujący odwzorowanie między elementami zbioru i liczbą jego
wystąpień.

fabryka
Funkcja, która zwykle jest przekazywana jako parametr, służąca do tworzenia obiektów.

Ćwiczenia
Ćwiczenie 19.1.

Oto funkcja obliczająca rekurencyjnie współczynnik dwumianowy:


def binomial _coeff(n, k) :
"""Oblic=anie współc=ynnika dwumianowego "n = k".
n lic=ba p rób
k llc=ba powod:eń
Wartośc =wracana int

if k == O:
ret urn
i f n == O:
ret urn O
r es = binomia l coeff(n - 1, k) + binomia l _ coeff( n - 1, k - 1)
r eturn res

Używając zagnieżdżonych wyrażeń warunkowych, zmodyfikuj treść funkcj i.


Jedna uwaga: funkcja nie jest zbyt efektywna, ponieważ jej działanie kończy się ciągłym obliczaniem
tych samych wartości. Wartości zapamiętywane umożliwią zwiększenie efektywności tej funkcji
(zajrzyj do podrozdziału „Wartości zapamiętywane" rozdziału 11.). Stwierdzisz jednak, że użycie
wartości zapamiętywanych jest trudniejsze, gdy funkcja zostanie utworzona za pomocą wyrażeń
warunkowych.

236 I Rozdział 19. Przydatne elementy


ROZDZIAŁ 20.

Debugowanie

Podczas debugowania należy rozróżniać różnego rodzaju błędy, aby mieć możliwość szybszego wy-
chwytywania ich. Oto one:
• Błędy składniowe są wykrywane przez interpreter podczas translacji kodu źródłowego do postaci
kodu bajtowego. Błędy te wskazują na problemy ze strukturą programu. Przykład: pominięcie
dwukropka na końcu instrukcji def powoduje wygenerowanie następującego, w pewnym
stopniu zbędnego komunikatu: Synt axError : i nval i d syntax.
• Błędy uruchomieniowe są tworzone przez interpreter, jeśli coś złego wydarzy się w czasie działa­
nia programu. Większość komunikatów o błędzie uruchomieniowym uwzględnia informacje
o miejscu wystąpienia błędu, a także o tym, jakie funkcje były wykonywane. Przykład: rekurencja
nieskończona ostatecznie spowoduje błąd uruchomieniowy maximum recursion depth exceeded .

• Błędy semantyczne oznaczaj ą problemy z działającym programem, który nie generuje komu-
nikatów o błędzie, lecz nie wykonuje poprawnie operacji. Przykład: wyrażenie może nie być
przetwarzane w oczekiwanej kolejności, co powoduje uzyskanie niepoprawnego wyniku.
W przypadku debugowania pierwszym krokiem jest stwierdzenie, z jakiego rodzaju błędem ma się do
czynienia. Kolejne podrozdziały uporządkowałem w oparciu o typ błędu, ale niektóre techniki mogą
zostać zastosowane w więcej niż jednej sytuacji.

Błędy składniowe
Błędy składniowe są zwykle łatwe do usunięcia po zidentyfikowaniu ich. Niestety komunikaty o błędzie
często nie są pomocne. Najczęściej występujące komunikaty o błędzie to SyntaxE rror : i nval id
syntax i Synt axError : i nva l i d token. Żaden z nich nie zawiera zbyt wielu informacji.

Zdarza się jednak, że komunikat informuje, gdzie w programie wystąpił problem. Właściwie dowia-
dujesz się od interpretera języka Python, gdzie stwierdził problem, co niekoniecznie jest równo-
znaczne ze zlokalizowaniem błędu. Czasami błąd występuje przed lokalizacją komunikatu o błędzie
(często w poprzedzającym ją wierszu).

Jeśli stopniowo rozwijasz program, powinno Ci to u możliwić właściwe zorientowanie się w kwestii
położenia błędu, który będzie się znajdować w wierszu dodanym jako ostatni.

237
Jeżeli kopiujesz kod z książki, bardzo uważnie zacznij porównywać własny kod z kodem podanym
w książce. Sprawdź każdy znak. Pamiętaj jednocześnie, że w książce może być błąd, dlatego jeśli na-
potkasz coś, co może wyglądać na błąd składniowy, tak rzeczywiście może być.
Oto kilka sposobów unikania najczęstszych błędów składniowych:
1. Upewnij się, że jako nazwa zmiennej nie zostało użyte słowo kluczowe języka Python.
2. Sprawdź, czy na końcu nagłówka każdej instrukcji złożonej znajduje się dwukropek (np. for,
whi l e, i fi def).
3. Upewnij się, że wszystkie łańcuchy w kodzie zawierają dopasowane znaki cudzysłowu.
Sprawdź, czy wszystkie znaki cudzysłowu są cudzysłowami prostymi, a nie drukarskimi.
4. Jeśli istnieje wiele łańcuchów z potrójnymi cudzysłowami (pojedynczymi lub podwójnymi),
upewnij się, że łańcuch został poprawnie zakończony. Niezakończony łańcuch może spowo-
dować na końcu programu błąd i nva l id token lub traktowanie części programu następującej
po takim łańcuchu jako łańcucha do momentu wystąpienia następnego łańcucha. W drugim
przypadku w ogóle może nie zostać wygenerowany komunikat o błędzie!
5. Brak operatora otwierającego, takiego jak (, { lub [, może sprawić, że interpreter języka Python
będzie przetwarzać następny wiersz jako część bieżącej instrukcji. Błąd występuje przeważnie
prawie natychmiast w kolejnym wierszu.
6. Wewnątrz instrukcji warunkowej sprawdź, czy zamiast operatora == znajduje się w niej klasyczny
operator=.
7. Sprawdź wcięcia, aby mieć pewność, że są ustawione tak, jak powinny. Interpreter języka
Python może obsługiwać spację i znaki tabulacji. Pomieszanie ich może jednak spowodować
problemy. Najlepszym sposobem na uniknięcie tego problemu jest zastosowanie edytora tek-
stu, który rozpoznaje kod Python i generuje spójne wcięcia.
8. Jeśli
w kodzie znajdują się znaki inne niż znaki ASCII (w tym łańcuchy i komentarze), może
to spowodować problem, choć w języku Python 3 takie znaki są zwykle obsługiwane. Bądź
jednak ostrożny podczas wklejania tekstu ze strony internetowej lub innego źródła.

Jeżeli nic nie działa, przejdź do następnego podrozdziału ...

Ciągle wprowadzam zmiany i nic to nie zmienia


Jeśli
interpreter informuje o błędzie, którego nie widzisz, może to wynikać z tego, że interpreter
odwołuje się do innego kodu niż Ty. Sprawdź używane środowisko programistyczne, aby mieć
pewność, że edytowany program to program Python, który próbujesz uruchomić.

Przy braku pewności spróbuj na początku kodu programu umieścić oczywisty i zamierzony błąd
składniowy. Uruchom program ponownie. Jeżeli interpreter nie znajduje nowego błędu, oznacza
to, że nowy kod nie został wykonany.
Istnieje kilka prawdopodobnych przyczyn:
• Zmodyfikowano plik i zapomniano zapisać zmiany przed ponownym uruchomieniem pliku.
Niektóre środowiska programistyczne zajmują się tym automatycznie, ale niektóre nie.
• Zmieniono nazwę pliku, lecz nadal używana jest stara nazwa.

238 I Rozdzlał20. Debugowanie


• W środowisku projektowania coś zostało niepoprawnie skonfigurowane.
• Jeśli tworzysz moduł i korzystasz z instrukcji i mpor t, upewnij się, że moduł nie ma takiej samej na-
zwy jak jeden ze standardowych modułów języka Python.
• Jeżeli za pomocą instrukcji i mport ładujesz moduł, pamiętaj o konieczności ponownego uru-
chomienia interpretera lub zastosowania instrukcji r e l oa d do wczytania zmodyfikowanego
pliku. Po ponownym zaimportowaniu moduł nie wykonuje żadnego działania.
Jeśli
napotkasz problemy i nie możesz stwierdzić, co się dzieje, jednym z rozwiązań jest ponowne
rozpoczęcie od nowego programu, takiego jak Witaj, świecie!, i upewnienie się, że uzyskasz pro-
gram możliwy do uruchomienia. W dalszej kolejności stopniowo dodawaj do nowego programu
elementy oryginalnego programu.

Błędy uruchomieniowe
Gdy program jest poprawny pod względem składniowym, interpreter języka Python może go wczytać
i przynajmniej rozpocząć jego wykonywanie. Co ewentualnie może się nie powieść?

Mój program nie wykonuje absolutnie żadnego działania


Problem ten występuje najczęściej, gdy plik zawiera funkcje i klasy, lecz w rzeczywistości nie wy-
wołuje funkcji rozpoczynającej wykonywanie kodu. Może to być zamierzone, jeśli planujesz jedy-
nie zaimportować taki moduł w celu zapewnienia klas i funkcji.

Jeśli
jest inaczej, upewnij się, że w programie znajduje się wywołanie funkcji, a ponadto że jest ono
osiągalne dla przepływu wykonywania (zajrzyj do zamieszczonego dalej podpunktu „Przepływ
wykonywania").

Mój program zawiesza się


Jeśli
program przestaje działać i wydaje się nie wykonywać żadnej operacji, zawiesił się. Często
oznacza to, że utkwił w pętli nieskończonej lub rekurencji nieskończonej.
• Jeżeli występuje konkretna pętla, w przypadku której podejrzewasz problem, bezpośrednio
przed tą pętlą dodaj instrukcję print wyświetlającą komunikat wej ście do pęt l i oraz kolejną in-
strukcję zapewniającą komunikat wyj śc i e z p ę t li .

Uruchom program. Jeśli uzyskasz pierwszy komunikat, a nie drugi, oznacza to pętlę nieskoń­
czoną. Przejdź do poniższego podpunktu „Pętla nieskończona".

• Rekurencja nieskończona powoduje przeważnie działanie programu przez pewien czas, a następ­
nie wyświetlenie błędu: Runti me Erro r : Maxi mum rec ur si on dept h exceeded (Błąd uruchomieniowy:
osiągnięto maksymalną głębokość rekurencji). Jeśli do tego dojdzie, przejdź do poniższego pod-
punktu „Rekurencja nieskończona".
Jeżeli nie zostanie wygenerowany ten błąd, ale podejrzewasz, że występuje problem z metodą
lub funkcją rekurencyjną, także możesz skorzystać z technik omówionych w podpunkcie „Reku-
rencja nieskończona".

Błędy uruchomlenlowe I 239


• Jeśli nie sprawdzi się żaden z powyższych punktów, rozpocznij testowanie innych pętli, a tak-
że funkcji i metod rekurencyjnych.
• Jeśli to też nie pomoże, istnieje możliwość, że nie rozumiesz przepływu wykonywania wła­
snego programu. Przejdź do zamieszczonego dalej podpunktu „Przepływ wykonywania".

Pętla nieskońaona

Jeśli uważasz, że
istnieje pętla nieskończo na, a ponadto jesteś przekonany co do tego, jaka pętla
powoduje problem, na końcu tej pętli dodaj i nstrukcję pr i nt , która wyświetla wa rtości zmiennych
w warunku oraz jego wartość.
Oto przykład:
whi le x > O and y < O
# wykonaj jakieś d:iałanie w:ględem x
# wykona; ;akieś d:załame w:ględem y
print(' x: ' , x)
print(' y: ' , y)
print("warunek: " (x > O a nd y < O))

Po uruchomieniu programu dla każdego wykonania pętli zostaną wyświetlone trzy wiersze danych
wyjściowych. W przypadku ostatniego wykonania pętli warunek powinien mieć wartość Fal se. Je-
śli pętla dalej jest wykonywana, będziesz w stanie zobaczyć wartości x i y, a ponadto będziesz mógł
stwierdzić, dlaczego nie są poprawnie aktualizowane.

Rekurencja nieskońaona
W większości sytuacji rekurencja nieskończona powoduje działanie programu przez pewien czas,
a następnie wygenerowanie błędu Maximum r ecur s i on depth exceeded (osiągnięto maksymalną
głębokość rekurencji).

Jeśli podejrzewasz, że funkcja wywołuje rekurencję nieskończoną, upewnij się, że istnieje przypa-
dek bazowy. Powinien występować jakiś warunek, który powoduje zwrócenie przez funkcję wy-
niku bez tworzenia wywołania rekurencyjnego. J eśli tak nie jest, musisz ponownie zastanowić się
nad algorytmem i zidentyfikować p rzypadek bazowy.
Jeżeli ist nieje przypadek bazowy, lecz nie wydaje się on osiągalny dla programu, na początku
funkcji dodaj instrukcję print, która wyświetla parametry. Po uruchomieniu programu ujrzysz
kilka wierszy danych wyjściowych każdorazowo w momencie wywołania funkcji, a ponadto zobaczysz
wartości parametrów. Jeśli parametry nie zmierzają w stronę przypadku bazowego, będziesz w stanie
zorientować się, z jakiego powodu.

Przepływ wykonywania

Jeżelinie masz pewności, jak przepływ wykonywania przebiega w programie, na początku każdej
funkcji dodaj instrukcje print wyświetlające komunikat taki jak wejście do funkc ji foo, gdzie foo
to nazwa funkcj i.
Po uruchomieniu program wyświetli zapis dla każdej wywołanej funkcji.

240 I Rozdzlał20. Debugowanie


Wmomencie uruchomienia programu uzyskuję wyjątek
Jeśli podczas uruchamiania kodu wydarzy się coś złego, interpreter języka Python wyświetla komuni-
kat uwzględniający nazwę wyjątku, wiersz kodu programu, w którym wystąpił problem, oraz dane
śledzenia.

Dane śledzenia identyfikują aktualnie działającą funkcję, funkcję, która ją wywołała, a następnie
funkcję odpowiedzialną za wywołanie tej kolejnej funkcji itd. Inaczej mówiąc, śledzona jest sekwencja
wywołań funkcji, które miały miejsce do momentu osiągnięcia bieżącego miejsca w kodzie, z uwzględ­
nieniem numeru wiersza w pliku, gdzie wystąpiło każde wywołanie.
Pierwszym krokiem jest sprawdzenie miejsca w programie, w którym wystąpił błąd, i przekonanie się,
czy możliwe jest stwierdzenie, co się stało. Oto niektóre z najczęstszych błędów uruchomieniowych:
NameError
Podejmujesz próbę użycia zmiennej, która nie istnieje w bieżącym środowisku. Sprawdź, czy na-
zwa została zapisana poprawnie lub przynajmniej w logiczny sposób. Pamiętaj też, że zmienne
lokalne są lokalne. Nie możesz odwoływać się do nich poza obrębem funkcji, w której zostały
zdefiniowane.
TypeError
Istnieje kilka możliwych przyczyn:
• Podejmujesz próbę niewłaściwego użycia wartości. Przykład: indeksowanie łańcucha, listy
lub krotki za pomocą czegoś innego niż liczba całkowita.
• Nie występuje zgodność między elementami w łańcuchu formatu i elementami przekaza-
nymi w celu przeprowadzenia konwersji. Może do tego dojść, jeśli niezgodna jest liczba
elementów lub została zażądana niewłaściwa konwersja.
• Przekazujesz funkcji niepoprawną liczbę argumentów. W przypadku metody przyjrzyj się
jej definicji i sprawdź, czy pierwszy parametr to se l f . Przyjrzyj się następnie wywołaniu
metody. Upewnij się, że wywoływana jest metoda w obiekcie z właściwym typem, a po-
nadto że poprawnie zapewniane są inne argumenty.

KeyError
Próbujesz uzyskać dostęp do elementu słownika za pomocą klucza, którego w słowniku nie ma.
Jeśli klucze są łańcuchami, pamiętaj, że znaczenie ma wielkość liter.

AttributeError
Próbujesz uzyskać dostęp do at rybutu lub metody, która nie istnieje. Sprawdź poprawność
nazwy! Możesz skorzystać z funkcji wbudowanej vars, aby wyświetlić listę istni ejących atry-
butów.
Jeśli błąd Attri but eE r ror
wskazuje, że obiekt jest typu NoneTy pe, oznacza to, że jest to obiekt
None. A zatem problemem nie jest nazwa atrybutu, lecz obiekt.

Przyczyną tego, że obiekt jest obiektem None, może być to, że zapomniałeś zwrócić wartość z funkcji.
Jeśli osiągniesz koniec funkcji bez natrafienia na instrukcję return, funkcja zwraca wartość None.
Inną częstą przyczyną jest zastosowanie wyniku z metody listy takiej jak sort , która zwraca
wartość None.

Błędy uruchomlenlowe I 241


IndexError
Długość indeksu używanego do uzyskania dostępu do listy, łańcucha lub krotki jest wi ększa
od długości jednego z tych elementów pomniejszonej o jeden. Bezpośrednio przed miejscem
wystąpienia błędu IndexError dodaj instrukcję pr int w celu wyświetlenia wartości indeksu i długości
tablicy. Czy tablica ma właściwy rozmiar? Czy indeks ma poprawną wa rtość?
Debuger języka Python (pdb) przydaje się podczas śledzenia wyjątków, ponieważ umożliwia spraw-
dzanie stanu programu tuż przed pojawieniem się błędu . Na temat tego debugera możesz przeczytać
pod adresem https://docs.python.org/3/library/pdb.html.

Dodałem tak wiele instrukcji print, że zostałem przytłoczony danymi wyjściowymi


Jednym z problemów związanych z użyciem instrukcji print na potrzeby debugowania jest to, że
może się to zakończyć uzyskaniem ogrom nej ilości danych wyjściowych. Możliwe są dwa sposoby po-
stępowania: uproszczenie danych wyj ściowych lub uproszczenie programu.

Aby uprościć dane wyjściowe, instrukcje print, które nie są pomocne, możesz usunąć lub umieścić
w komentarzu. Możliwe jest t eż połączenie instrukcji lub sform atowanie danych wyjściowych tak,
aby były łatwiejsze do zrozumienia.

W celu uproszczenia programu można poczynić kilka działań. Po pierwsze, ogranicz skalę problemu,
jakim zajmuje się program. Jeśli na przykład przeszukiwana jest lista, niech operacja ta wykony-
wana jest dla niewielkiej listy. Jeżeli program pobiera od użytkownika dane wejściowe, zapewnij je
w najprostszej postaci, która powoduje problem.
Po drugie, wyczyść kod programu. Usuń nieużywany kod i przebuduj program tak, aby jak najbardziej
ułatwić jego czytanie. Jeśli na przykład podejrzewasz, że problem tkwi w głęboko zagnieżdżonej
części programu, spróbuj ją zmo dyfikować za pomocą prostszej struktury. W sytuacji, gdy podejrze-
wasz dużą funkcję, spróbuj podzielić j ą na mniejsze funkcje i przetestować je osobno.

Proces znajdowania minimalnego przypadku testowego często prowadzi do błędu. Jeśli stwierdzisz, że
program działa w jednej sytuacji, a w innej nie, będzie to stanowić wskazówkę odnośnie do tego,
co ma miejsce.

I podobnie przebudowanie porcji kodu może być pomocne w znalezieniu subtelnych błędów. Je-
żeli wprowadzisz zmianę, w przypadku której uważasz, że nie powinna mieć wpływu na program,
a jednak jest inaczej, może to być dodatkowa informacja.

Błędy semantyczne
Pod pewnymi względami błędy semantyczne są najtrudniejsze do debugowania, ponieważ interpreter
nie zapewnia żadnych informacji o tym, co niewłaściwego ma miejsce. Wiesz jedynie, co program po-
winien zrealizować.

Pierwszym krokiem jest utworzenie połączenia między tekstem wyświetlanym przez program i wi-
docznym zachowaniem. Niezbędna jest hipoteza dotycząca tego, co program w rzeczywistości realizuje.
Jedną z przyczyn utrudniających to jest fakt, że komputery działają tak szybko.

242 I Rozdzlał20. Debugowanie


Często będziesz chciał mieć możliwość spowolnienia programu do szybkości pracy człowieka. W przy-
padku niektórych debugerów jest to możliwe. Jednakże czas, jaki zajmuje wstawienie kilku dobrze
umiejscowionych instrukcji pr i nt, często jest krótki w porównaniu z konfigurowaniem debugera,
wstawianiem i usuwaniem punktów przerwania oraz wykonywaniem krokowym programu do
miejsca, w którym występuje błąd.

Mój program nie działa


Należy zadać sobie następujące pytania:
• Czy jest coś, co program miał wykonać, ale wydaje się, że tego nie robi? Znajdź sekcję kodu obsłu­
gującą odpowiednią funkcję i upewnij się, że jest ona wykonywana w momencie, w którym
uważasz, że powinna.

• Czy dzieje si ę coś, co nie powinno? Znajdź kod w programie obsługujący funkcję i sprawdź,
czy jest ona wykonywana wtedy, gdy nie powinna.
• Czy istnieje sekcja kodu powodująca efekt niezgodny z oczekiwaniami? Upewnij się, że ro-
zumiesz tę część kodu, zwłaszcza jeśli uwzględnia funkcje lub metody z innych modułów języka
Python. Przeczytaj dokumentację dotyczącą wywoływanych funkcji. Wypróbuj je przez utworze-
nie prostych przypadków testowych i sprawdzenie wyników.

Aby mieć możliwości programowania, musisz dysponować modelem poznawczym opisującym spo-
sób działania programów. Jeśli masz program, który nie działa zgodnie z oczekiwaniami, problem
często nie tkwi w programie, lecz w Twoim modelu poznawczym.

Najlepszym sposobem poprawienia modelu poznawczego jest rozbicie programu na jego kompo nenty
(zwykle są to funkcje i metody) oraz niezależne przetestowanie każdego z nich. Po znalezieniu
rozbieżności między modelem i rzeczywistością możesz rozwiązać problem.

Oczywiście w trakcie projektowania programu należy budować i testować komponenty. Jeśli napo-
tkasz problem, powinna występować tylko niewielka ilość nowego kodu, co do którego nie wiadomo,
czy jest poprawny.

Dysponuję wielkim i trudnym wyrażeniem ,


które nie działa zgodnie z oczekiwaniami
Tworzenie złożonych wyrażeń nie stanowi problemu, dopóki są czytelne. Mogą one być jednak trudne
do debugowania. Często dobrym pomysłem jest rozdzielenie złożonego wyrażenia na serię przypisań
zmiennym tymczasowym.
Oto przykład:
sel f.hands [ i] . addCard(self. hands [sel f .f indNeighbor(i)] . popCard())

Może to zostać zmodyfikowane w następuj ący sposób:

neighbor = self . fi nd Neig hbor(i)


pic kedCard = self. hands [neig hbor] . popCard()
sel f.hands [ i] . addCard(pic kedCard)

Błędy semantyane I 243


Powyższa jasno sprecyzowana wersja jest czytelniejsza, ponieważ
nazwy zmiennych zapewniają dodat-
kową dokumentację, a ponadto debugowanie jest prostsze z powodu możliwości sprawdzania typów
zmiennych pośrednich i wyświetlania ich wartości.

Innym problemem, jaki może wystąpić w przypadku dużych wyrażeń, jest to, że kolejność przetwa-
rzania może nie być zgodna z oczekiwaniami. Jeśli na przykład dokonujesz translacji wyrażenia ....:.._
2n
do postaci kodu Python, możesz użyć następującego wiersza:
y = x / 2 * math . pi
Nie jest to poprawny zapis, ponieważ operacje mnożenia i dzielenia mają identyczne pierwszeń­
stwo, a ponadto są przetwarzane od lewej do prawej strony. Oznacza to, że wyrażenie to wyko-
nuje obliczenie Xll I 2 .

Dobrym sposobem debugowania wyrażeń jest dodanie nawiasów okrągłych w celu doprecyzowa-
nia kolejności przetwarzania:
y = x / (2 * mat h.pi )
Zawsze gdy nie masz pewności odnośnie do kolej ności przetwarzania, użyj nawiasów okrągłych.
Kod programu będzie nie tylko poprawny (będzie działał zgodnie z oczekiwaniami), ale będzie
też czytelniejszy dla innych osób, które nie zapamiętały kolejności operacji.

Mam funkcję, która nie zwraca tego, czego oczekuję


Jeżeli użytoinstrukcji return ze złożonym wyrażeniem, nie będzie możliwe wyświetleni e wyniku
przed zwróceniem go. I tym razem możesz skorzystać ze zmiennej tymczasowej. Na przykład za-
miast następującego wiersza kodu:
ret urn s el f . hands[ i ]. r emoveMat c hes()

możesz użyć poniższych wierszy:


count = sel f.ha nds [ i] . r emoveMatc hes()
ret urn count

Dysponujesz teraz możliwością wyświetlenia wartości zmiennej count przed zwróceniem jej.

Naprawdę, ale to naprawdę nie wiem, co mam zrobić, i potrzebuję pomocy


Spróbuj najpierw odej ść
od komputera na kilka minut. Emituje on fale, które mają wpływ na mózg,
powodując następujące objawy:

• Frustrację i gniew.
• Przesądy (komputer „nienawidzi" mnie) i wnioskowanie bliskie magii (program działa tylko
wtedy, gdy odwrotnie założę czapkę).
• Programowanie w stylu „błądzenia losowego" (podejmowanie próby programowania polegającej
na tworzeniu każdego możliwego programu i wybieraniu tego, który działa poprawnie).

244 I Rozdzlał20. Debugowanie


Jeśli
uznasz, że dotyczy Cię dowolny z wymienionych objawów, wstań i idź na spacer. Gdy ochłoniesz,
zastanów się nad programem. Jak działa? Jakie są możliwe przyczyny takiego zachowania? Kiedy
po raz ostatni program działał poprawnie, a co zrobiłeś później?

Znalezienie błędu niekiedy wymaga po prostu czasu. Często identyfikuję przyczynę błędów po
odejściu od komputera i udaniu się na spacer. Niektóre z najlepszych miejsc na odkrycie błędów
to pociągi, prysznic, a także łóżko tuż przed zaśnięciem.

Nie, naprawdę potrzebuję pomocy


To się zdarza. Nawet najlepsi programiści sporadycznie znajdują się w sytuacji bez wyjścia. Cza-
sami pracujesz nad programem tak długo, że nie jesteś w stanie dojrzeć błędu. Niezbędne jest, by
ktoś inny na niego spojrzał.

Zanim spotkasz się z taką osobą, upewnij się, że jesteś na to gotowy. Program powinien być jak
najprostszy, a ponadto powinien działać w przypadku najmniejszej ilości danych wejściowych
powodujących błąd. W odpowiednich miejscach powinny się znajdować instrukcje pri nt (a genero-
wane przez nie dane wyjściowe powinny być zrozumiałe). Problem powinien być dla Ciebie na tyle
dobrze zrozumiały, żeby możliwe było opisanie go w zwięzły sposób.
Po poproszeniu kogoś o pomoc pamiętaj o przekazaniu tej osobie wymaganych przez nią informacji:
• Jeśli pojawił się komunikat o błędzie, jakie ma on znaczenie, a także jaka część programu
wskazała go?
• Jakie działanie zostało wykonane jako ostatnie przed wystąpieniem błędu? Jakie były ostatnie
napisane wiersze kodu lub jaki jest nowy przypadek testowy, który zakończył się niepowo-
dzeniem?
• Co dotychczas zostało wypróbowane i czego się dowiedziałeś?

Po znalezieniu błędu zastanów się przez chwilę nad tym, jakie działania mogły zostać przeprowadzone
w celu szybszego znalezienia go. Gdy następnym razem napotkasz coś podobnego, będziesz w stanie
zlokalizować błąd w krótszym czasie.

Pamiętaj, że celem nie jest jedynie zapewnienie działania programu, lecz także zdobycie wiedzy
na temat sposobu pozwalającego to osiągnąć.

Błędy semantyane I 245


246 I Rozdzlał20. Debugowanie
ROZDZIAŁ 21 .

Analiza algorytmów

Treść tego dodatkowego rozdziału jest poddanym edycji fragmentem książki Think Complexity
napisanej przez Allena B. Downeya, która też została wydana przez wydawnictwo O'Reilly Media
w 2012 r. Po przeczytaniu tej książki możesz zdecydować się na rozpoczęcie lektury Think Complexity.

Analiza algorytm ów to dziedzina informatyki poświęcona wydajności algorytmów, a w szczególności


ich wymaganiom dotyczącym czasu działania i miejsca w pamięci (sprawdź st ronę dostępną pod ad-
resem https:/!pl. wikipedia.org/wiki!Analiza_algorytm%C3%B3w).
Praktycznym celem analizy algorytmów jest przewidywanie wydajności różnych algorytmów, co
ułatwia podejmowanie decyzji projektowych.

W czasie kampanii prezydenckiej, która odbyła si ę w Stanach Zjednoczonych w 2008 r., kandydat
Barack Obama został poproszony o przeprowadzenie improwizowanej analizy podczas wizyty
w firmie Google. Eric Schmidt, prezes zarządu, żartobliwie zapytał go o najbardziej efektywny sposób
sortowania miliona 32-bitowych liczb całkowitych. Okazało się, że Obama był dobrze zo riento-
wany, gdyż szybko odpowiedział: „Myślę, że w tym przypadku sortowanie bąbelkowe nie byłoby
dobrą propozycją" (sprawdź st ronę dostępną pod adresem http://bi t.ly/lMpiwTj).

To prawda: ch oć pod względem pojęci owym sortowanie bąbelkowe jest proste, w przypadku du-
żych zbiorów danych okaże się powolne. Odpowiedź, jakiej Schmidt prawdopodobnie oczekiwał,
związana jest z sortowaniem pozycyjnym (https://pl. wikipedia.org/ wiki/Sortowanie__pozycyjne) 1•

Celem analizy algorytmów jest uzyskanie znaczących porównań algorytmów. Pojawiają si ę jednak
pewne problemy:
• Wydajność względna algorytmów może zależeć od parametrów sprzętu, dlatego jeden algo-
rytm może być szybszy w przypadku komputera A, a drugi algorytm w przypadku komputera B.
Ogólne rozwiązani e tego problemu polega na określeniu m odelu kom putera i przeanalizo-
waniu liczby kroków lub operacji, jakie są wymagane przez algorytm w ramach danego modelu.

1
Jeś li jedna k usłys zysz pytanie takie ja k w pr zytoczon ej ro zmo wie, m yślę, że lepsza od powiedź będzie brzmieć następ ują­
co: „Najszybszym sposobem poso rtowan ia mi liona liczb ca ł kowityc h byłoby zastoso wan ie dowol nej funkcji sortującej
zapewn ia nej przez używany p rzeze m n ie język. Jej wydaj ność jest wystarczająco dobra w p rzypadku zdecydowanej więk­
szości zastoso wań. Jeśli jednak okazałoby s ię, że moja aplikacja będzie zbyt wolna, skorzystałbym z narzędzia do profilo wan ia,
aby dowiedzieć s ię, na co zosta ł poś wi ęcony czas. Jeżeli okaza łoby się, że szybszy algorytm sorto wania m iałb y znaczny
wp ływ na wydajn ość, pos zu ka łbym dobrej imp lementacj i sortowania pozycyjnego".

247
• Wydaj ność względna może być zależna od szczegółów zbioru danych. Na przykład niektóre
algorytmy sortowania są szybsze, jeśli dane są już częściowo posortowane. W tym przypadku
inne algorytmy działają wolniej. Typowym sposobem unikania tego problemu jest analizo-
wanie najgorszego przypadku. Czasami przydatne jest analizowanie wydajności dla uśred­
nionego przypadku. Zwykle jest to jednak trudniejsze, a ponadto może nie być oczywiste, ja-
kiego zestawu przypadków użyto do uśrednienia.
• Wydaj ność względna może również zależeć od skali problemu. Algorytm sortowania, który
jest szybki w przypadku niewielkich list, może być wolny w odniesieniu do długich list. Ty-
powym rozwiązaniem tego problemu jest wyrażenie czasu działania (lub liczby operacj i) jako
funkcj i skali problemu oraz grupowanie funkcji za pomocą kategorii zależnie od tego, jak
szybko się one powiększają wraz ze wzrostem skali problemu.

W tego rodzaju porównaniu dobrą rzeczą jest to, że nadaje się ono do prostej klasyfikacji algo-
rytmów. Jeśli na przykład wiem, że czas dzi ałania algorytmu A będzie zwykle propo rcjonalny do
wielkości danych wejści owych n, a algorytm B będzie proporcjonalny do wartości n 2, oczekuję, że
pierwszy algorytm będzie szybszy od d rugiego, przynajmniej w przypadku dużych wa rtości n.
Tego rodzaju analizie towarzyszą pewne zast rzeżenia, którymi jednak zaj miemy się później.

Tempo wzrostu
Załóżmy, że przeanalizowano dwa algorytmy i wyrażono ich czasy działania przy użyciu wi elkości
danych wejściowych: rozwiązanie p roblemu z wi elkością n zajmuje algorytmowi A lOOn+l kro-
ków, algorytmowi B natomiast - n 2+n+l kroków.

W po niższej tabeli podano czas działania tych algorytmów dla różnych wielkości danych powią­
zanych z problemem.

Wielkość danych wejściowych Czas działania algorytmu A Czas działania algorytmu B

10 1001 111
100 10001 10101
1000 100001 1001001
10000 1000001 > 10 10

W przypadku n = 10 algorytm A prezentuje się naprawdę źle. Czas jego działania jest prawie 10 razy
dłuższy niż algorytmu B. Gdy jednak n = 100, czas dla obu algorytmów jest prawie taki sam, a w przy-
padku większych wartości algorytm A wypada znacznie lepiej.
Zasadniczym powodem jest to, że dla dużych wartości n dowolna funkcja zawierająca składnik n 2
będzie rosnąć szybciej niż funkcja, której składnik wi odący to n. Składnik wiodący to składnik
o najwyższym wykładniku.
W przypadku algorytmu A składnik wi odący ma duży współczyn nik 100. Z tego właśnie powodu
dla n o małej wartości algorytm B działa lepiej niż algorytm A. Niezależnie jednak od współczyn­
ników zawsze będzie istni eć dla dowolnych wartości a i b jakaś wartość n, gdzie an 2 > bn.

248 I Rozdzlał21. Analiza algorytmów


Ten sam argument odnosi się do składników niewiodących. Jeśli nawet czas działania algorytmu
A wyniósłby n-t-1000000, nadal byłby lepszy niż algorytmu B dla wystarczająco dużego n.
Ogólnie rzecz biorąc, oczekujemy algorytmu z mniejszym składnikiem wiodącym, który będzie
lepszym algorytmem w przypadku dużych problemów, ale jednak w odniesieniu do mniejszych
problemów może pojawić się punkt przejścia oznaczający, że lepszy jest inny algorytm. Położe­
nie tego punktu zależy od szczegółów algorytmu, danych wejściowych i sprzętu, dlatego punkt
ten jest zwykle ignorowany na potrzeby analizy algorytmicznej. Nie oznacza to jednak, że możesz
o nim zapomnieć.

Jeśli
dwa algorytmy mają taki sam składnik wiodący, trudno stwierdzić, który z nich jest lepszy. I tym
razem odpowiedź zależy od szczegółów. A zatem w przypadku analizy algorytmicznej funkcje z iden-
tycznym składnikiem wiodącym są uważane za równorzędne, jeśli nawet mają różne współczynniki.

Tempo wzrostu to zestaw funkcji, w przypadku których przebieg wzrostu uważany jest za jednakowy.
Na przykład 2n, lOOn i n-t-1 należą do tego samego tempa wzrostu, które zapisywane jest jako
O(n) za pomocą notacji „dużego O", nazywanej często liniową, ponieważ każda funkcja zestawu
rośnie liniowo wraz z n.

Wszystkie funkcje ze składnikiem wiodącym n 2 należą do O(n 2 ). Funkcje te są nazyv,rane kwa-


dratowymi.

W poniższej tabeli zaprezentowano kilka przykładów tempa wzrostu (w kolejności od najlepszych


do najgorszych) występujących najczęściej w analizie algorytmicznej.

Tempo wzrostu Nazwa


0(1) stałe

O(logb n) logarytmiczne (dla dowolnego b)


O(n) liniowe
O(n logb n) liniowo-logarytmiczne
O(n 2 ) kwadratowe
O(n 3 ) sześcienne

O(c") wykładnicze (dla dowolnego c)

W przypadku składników logarytmicznych podstawa logarytmu nie ma znaczenia. Zmiana pod-


staw odpowiada mnożeniu przez stałą, co nie zmienia tempa wzrostu. Podobnie wszystkie funkcje wy-
kładnicze mają to samo tempo wzrostu, niezależnie od podstawy wykładnika. Funkcje wykładnicze
rosną bardzo szybko, dlatego algorytmy wykładnicze są przydatne jedynie w odniesieniu do nie-
wielkich problemów.

Ćwiczen ie 21.1.

Przeczytaj stronę serwisu Wikipedia poświęconą notacji „dużego O" (https://pl. wikipedia.org/ wiki!
Asymptotycz ne_ tempo_wzrostu) i udziel odpowiedzi na następujące pytania:
1. Jakie jest tempo wzrostu funkcji n 3 -t-n 2? Jak to wygląda w wypadku funkcji 1000000n3 -t-n 2 , a jak dla
funkcji n3 -t-1000000n2?

Tempo wzrostu I 249


2. Jakie jest tempo wzrostu funkcji (n 2+n).(n+l)? Przed rozpoczęciem mnożenia pamiętaj o tym, że
wymagany jest jedynie składnik wiodący.
3. Jeśli J należy do O(g), co można powiedzieć na temat af+b w przypadku pewnej nieokreślonej
funkcjig?
4. Jeślif1 if2 należą do O(g), co można powiedzieć na tematf1+f2?
5. Jeślif1 należy do O(g), af2 należy do O(h), co można powiedzieć na tematf1+f2?
6. Jeślif1 należy do O(g), af2 należy do O(h), co można powiedzieć na tematfif2?
Programiści dbający o wydajność często uznają tego rodzaju analizę za trudną do przyj ęcia. Ar-
gumentują: czasami współczynniki i składniki niewiodące naprawdę wszystko zmieniają. Czasami
szczegóły dotyczące sprzętu, języka programowania i właściwości danych wejści owych powodują
dużą różnicę. W przypadku niewielkich problemów zachowanie asymptotyczne jest niewłaściwe.

Jeśli jednak będziesz pamiętać o tych zastrzeżeniach, analiza algorytmiczna okaże się przydatnym
narzędziem. Przynajmniej w przypadku dużych problemów „lepsze" algorytmy są zwykle lepsze,
a czasami z nacz nie lepsze. Różnica między dwoma algorytmami o takim samym tempie wzrostu
jest zazwyczaj stała, ale różnica między dobrym i złym algorytmem jest nie do przecenienia!

Analiza podstawowych operacji wjęzyku Python


W języku
Python większość operacji arytmetycznych ma stały czas. Mnożenie trwa zwykle dłużej
niż dodawanie i odejmowanie, a dzielenie zajmuje jeszcze więcej czasu. Jednakże czas trwania
tych operacji nie zależy od rozmiaru argumentów. Wyjątkiem są bardzo duże liczby całkowite.
W ich przypadku czas działania zwiększa się wraz z liczbą cyfr.

Operacje indeksowania (odczytywanie lub zapisywanie elementów w ciągu lub słowniku) mają
niezmienny czas, niezależnie od rozmiaru struktury danych.
Pętla for dokonująca przejścia ciągu lub słownika działa zwykle w sposób liniowy pod warunkiem, że
wszystkie operacje w obrębie pętli cechują się stałym czasem. Na przykład sumowanie elementów
listy to operacja liniowa:
t o tal = O
for x i n t :
total + = x

Funkcja wbudowana 5um również jest liniowa, ponieważ wykonuje to samo działanie, lecz zwykle
będzie szybsza, gdyż stanowi efektywniejszą implementację. W j ęzyku analizy algorytmicznej
mówi się, że ma ona mniejszy współczynnik wiodący.

Zgodnie z ogólną zasadą, jeśli treść pętli przynależy do O(n"), cała pętla należy do O(n"+1). Wyjątkiem
jest sytuacja, gdy możliwe jest pokazanie, że pętla kończy działanie po stałej liczbie iteracji. Jeśli
pętla wykonywana jest k razy niezależnie od n, należy ona do O(n"), nawet w przypadku dużego k.

Mnożenie przez k nie zmienia tempa wzrostu, ale też nie powoduje tego dzielenie. Jeśli zatem
treść pętli wykonywanej n/k razy przynależy do O(n"), pętla należy do O(n•+1), nawet w przypadku du-
żego k.

250 I Rozdzlał21. Analiza algorytmów


Większość operacji dotyczących łańcuchów i krotek jest liniowa, z wyjątkiem indeksowania i ope-
racji wykonywanej przez funkcję l en, któ re są niezmienne w czasie. Funkcje wbudowane mi n i max
są liniowe. Czas działania operacji wydzielania fragmentu łańcucha jest proporcjonalny do długo­
ści danych wyjściowych, lecz niezależny od wielko ści danych wejściowych.

Łączenie łańcuchów to operacja liniowa. Czas działani a zależy od sumy długości argumentów.

Wszystkie metody łańcuchowe są liniowe. Jeśli jednak długości łańcuchów są ograniczone przez
stałą, na przykład w przypadku operacji odnoszących się do pojedynczych znaków, są one uważane za
niezmienne w czasie. Metoda łańcuchowa j o i n jest liniowa. Czas działania zależy od całkowitej
długości łańcuchów.

Większość metod list jest liniowa. Występuje jednak kilka następujących wyjątków:
• Dodawanie elementu do końca listy to przeważnie operacja niezmienna w czasie. W przypadku
braku miejsca sporadycznie następuj e kopiowanie do położenia o większej pojemności. Ponieważ
jednak całkowity czas n operacji wynosi O(n), średni czas każdej operacji jest równy 0(1).
• Usuwanie elementu z końca listy cechuje się stałym czasem.
• Operacji sortowania odpowiad a tempo wzrostu O(n log n).

C hoć większość operacji i metod słownikowych jest niezmienna w czasie, występuje kilka wyjątków:
• Czas działania metody updat e jest p ropo rcjonalny do wielkości słownika przekazanego jako
parametr, a nie aktualizowanego słownika.
• Metody keys, va l ues i i tems cechuj ą się stałym czasem, ponieważ zwracają iteratory. Jeśli jed-
nak dla iteratorów użyto pętli, będzie ona liniowa.

Wydajność słowników to jeden z d robnych cudów informatyki. Sposób ich działania omówiono
w zamieszczonym dalej podrozdziale „Tablice mieszające".

Ćwiczenie 21.2.

Przeczytaj stronę serwisu Wikipedia poświęconą algorytmom sortowania (https://pl. wikipedia.org/ wiki/
Sortowanie) i udziel odpowiedzi na następujące pytania:
1. Czym jest so rtowanie za pom ocą porównań?
Jakie w jego przypadku jest najlepsze tempo
wzrostu w odniesieniu do najgorszych wariantów? Jakie jest najlepsze tempo wzrostu w od -
niesieniu do najgorszych wariantów dla dowolnego algorytmu sortowania?
2. Jakie jest tempo wzrostu sortowania bąbelkowego? Dlaczego Barack Obama uważa, że „nie
byłoby o no dobrą propozycj ą"?

3. Jakie jest tempo wzrostu sortowania pozycyjnego? Jakie warunki wstępne muszą zostać speł­
nione w celu skorzystania z niego?
4 . Czym jest sortowanie stabilne i dlaczego w praktyce może mieć znaczenie?
5. Jaki jest najgorszy algorytm sortowania (z posiadających nazwę)?

Analiza podstawowych operacji w Języku Python I 251


6. Z jakiego algorytmu sortowania korzysta biblioteka języka C? Jaki algorytm sortowania sto-
sowany jest w języku Python? Czy algorytmy te są stabilne? W celu uzyskania odpowiedzi na
te pytania może być konieczne skorzystanie z wyszukiwarki Google.
7. Wiele sortowań, które nie bazują na porównaniach, jest liniowych. Dlaczego zatem w języku
Python używane jest sortowanie za pomocą porównań O(n log n)?

Analiza algorytmów wyszukiwania


Wyszukiwanie to algorytm pobierający kolekcję i element docelowy oraz określający, czy znaj-
duje się on w kolekcji, zwracający często indeks elementu docelowego.
Najprostszym algorytmem wyszukiwania jest wyszukiwanie liniowe, które dokonuje przejścia
kolejnych elementów kolekcji i zatrzymuje się w momencie znalezienia elementu docelowego. W naj-
gorszym wariancie algorytm musi przejść całą kolekcję, dlatego czas działania jest liniowy.
Z wyszukiwania liniowego korzysta operator i n w przypadku ciągów. Tak samo jest w odniesieniu do
metod łańcuchowych, takich jak f i nd i count.

Jeśli elementy ciągu są uporządkowane, możesz zastosować wyszukiwanie z podziałem na połowę


z tempem wzrostu O(log n). Wyszukiwanie to przypomina algorytm, jakiego mogłeś użyć do znalezie-
nia słowa w słowniku (w papierowym słowniku, a nie strukturze danych). Zamiast zaczynać od
początku i sprawdzać kolejno każdy element, zaczynasz od elementu w środku i sprawdzasz, czy
szukane słowo występuje przed tym elementem, czy po nim. Jeśli słowo znajduje się przed ele-
mentem, wyszukiwanie dotyczy pierwszej połowy ciągu. W przeciwnym razie przeszukiwana jest
druga połowa. W każdym wariancie liczba pozostałych elementów obcinana jest o połowę.
Jeśli ciąg zawiera 1 OOO OOO elementów, znalezienie słowa lub stwierdzenie, że nie istnieje, zajmie około
20 kroków. Oznacza to, że wyszukiwanie to jest około 50 OOO razy szybsze od wyszukiwania liniowego.
Wyszukiwanie z podziałem na połowę może być znacznie szybsze niż wyszukiwanie liniowe, ale wy-
maga uporządkowania ciągu, co może nieść za sobą konieczność wykonania dodatkowych działań.

Istnieje kolejna struktura danych o nazwie tablica mieszająca, która jest jeszcze szybsza (może wyszu-
kiwać z zachowaniem niezmienności czasu), a ponadto nie wymaga sortowania elementów. W języku
Python słowniki są implementowane właśnie za pomocą tablic mieszających. Z tego powodu więk­
szość operacji słownikowych, w tym operacja związana z operatorem i n, ma niezmienny czas.

Tablice mieszające
Aby objaśnić sposób działania tablic mieszających, a także wyjaśnić, dlaczego ich wydajność jest tak
duża, zacznę od prostej implementacji odwzorowania, a następnie będę stopniowo ją ulepszał aż
do momentu uzyskania tablicy mieszającej.
Choć do zademonstrowania tych implementacji używam języka Python, w praktyce nie utworzyłbyś
takiego kodu za pomocą tego języka. Po prostu skorzystałbyś ze słownika! A zatem na czas lektury reszty
rozdziału musisz wyobrazić sobie, że słowniki nie istnieją, a ponadto że chcesz zaimplementować

252 I Rozdzlał2 1 . Analiza al gorytmów


strukturę danych odwzorowującą klucze na wartości. Oto operacje niezbędne do zaimplemento-
wania tej struktury:
acid ( k, V)
Dodaje nowy element odwzorowuj ący klucz k na wartość v. W przypadku słownika d języka
Python operacja ta zapisywana jest w następującej postaci: d [ kJ = v.
get ( k)
Wyszukuje i zwraca wartość odpowiadającą kluczowi k. W przypadku słownika d języka Python
operacja ta zapisywana jest w następującej postaci: d [ kJ lub d . get ( k) .

Przyjmuję teraz, że każdy klucz pojawia się tylko raz. Najprostsza implementacja tego interfejsu
korzysta z listy krotek, gdzie każda krotka to para złożona z klucza i wartości:
class LinearMap :
def init (self) :
s elf . i t ems = []
def add(sel f, k, v) :
s elf . it ems . append(( k, v))
def get(self, k) :
fo r key , val in self. items :
if key == k:
return val
raise KeyE rror

Metoda add dołącza krotkę z parą klucz-wartość do listy elementów, co zajmuje stały czas.

Metoda get używa pętli for do przeszukania listy: jeśli znajdzie klucz docelowy, zwraca odpowiednią
wartość. W przeciwnym razie funkcja zgłasza błąd KeyError. Oznacza to, że get to metoda liniowa.

Alternatywą jest zachowanie listy posortowanej według klucza. Metoda get może następnie sko-
rzystać z wyszukiwania z podziałem na połowę z tempem wzrostu O(log n). Wstawienie nowego
elementu w środku listy jest jednak liniowe, dlatego może nie być to najlepsza opcja. Istnieją inne
struktury danych, które mogą implementować metody add i get w sposób logarytmiczny. W dalszym
ciągu jednak nie jest to tak dobre jak niezmienność czasu, dlatego przejdźmy dalej.

Sposobem ulepszenia klasy Li nearMap jest podzielenie listy par klucz- wartość na mniejsze listy.
Poniżej zaprezentowałem implementację o nazwie BetterM ap w postaci listy 1OO obiektów Li nearMap.
Jak się zaraz okaże, tempo wzrostu w przypadku metody get nadal jest liniowe, ale klasa Bet terMap
stanowi krok na drodze do tablic mieszających:
class BetterMap :
def init (self , n = 100) :
self . maps = []
fo r i i n range(n) :
s elf . maps . append(LinearMap())
def find_map(sel f, k) :
index = hash( k) % len(self . maps)
return s elf . maps [index]
def add(self , k, v) :
m = s elf .f ind_map( k)
m. add ( k, v)
def get{self , k) :
m = self .f ind_ma p(k)
return m. get( k)

Tablice mieszające I 253


Metoda _ i ni t _ tworzy listę n obiektów Li nearMap.

Metoda f i nd_map używana jest przez metody add i get do stwierdzenia, w jakim odwzorowaniu ma
zostać umieszczony nowy element lub jakie odwzorowanie ma zostać przeszukane.

Metoda f i nd_map korzysta z funkcji wbudowanej hash, która pobiera niemal dowolny obiekt języka
Python i zwraca liczbę całkowitą. Ograniczeniem tej implementacji jest to, że działa tylko z kluczami
zapewniającymi możliwość mieszania. Nie oferują tego typy zmienne, takie jak listy i słowniki.

Obiekty z możliwością mieszania, które są uważane za równorzędne, zwracają taką samą wartość mie-
szania, ale odwrotna sytuacja niekoniecznie jest prawdziwa: dwa obiekty z różnymi wartościami
mogą zwracać identyczną wartość mieszania.

Metoda f i nd_map używa operatora dzielenia bez reszty do umieszczenia wartości mieszania w za-
kresie od O do len (self. maps) , dlatego wynikiem jest poprawny indeks listy. Oczywiście oznacza
to, że wiele różnych wartości mieszania będzie umieszczanych w tym samym indeksie. Jeśli jednak
funkcja mieszania dokona naprawdę równomiernego rozmieszczenia (właśnie z myślą o tym zostały
zaprojektowane funkcje mieszania), można oczekiwać n/ 100 elementów przypadających na obiekt
Li nearMap.

Ponieważ czas działania metody Li nearMap .get jest proporcjonalny do liczby elementów, oczekujemy,
że algorytm klasy BetterMap będzie około 100 razy szybszy niż algorytm klasy Li near Map. Tempo
wzrostu jest nadal liniowe, ale współczynnik wiodący jest mniejszy. To niezłe rozwiązanie, lecz
algorytm ten wciąż nie jest tak efektywny jak tablica mieszająca.

Poniżej (wreszcie) zaprezentowałem zasadnicze rozwiązanie zapewniające szybkość tablic mieszają­


cych. Jeśli możliwe jest ograniczenie maksymalnej długości obiektów Li nearMap, metoda Li nearMap . get
cechuje się niezmiennością czasu. Konieczne jest jedynie śledzeni e liczby elementów, a także
momentu, w którym liczba elementów przypadających na obiekt Li nearMap osiągnie próg. Gdy to
nastąpi, niezbędna będzie zmiana wielkości tablicy mieszającej przez dodanie kolejnych obiektów
Li nearMap.

Oto implementacja tablicy mieszającej:


cl ass HashMap :
def i nit (self) :
sel f. maps = BetterMap(2)
sel f. num = O
def get(sel f , k) :
ret urn sel f . maps . get( k}
def add (self , k, v) :
i f self. num == l en(sel f . maps .maps) :
sel f . res i ze()
sel f. maps . add( k, v)
sel f. num += 1
def resi ze(self ) :
new_maps = BetterMap(self. num * 2)
f orm in sel f . maps . maps :
f or k, v in m. items :
new_maps . add( k, v)
self . maps = new_maps

254 I Rozdzlał21. Analiza algorytmów


Każdy obiekt HashMap zawiera obiekt BetterMap. Metoda_init_ zaczyna od zaledwie dwóch obiektów
Li nearMap i inicjuje zmienną num, która śledzi liczbę elementów.

Metoda get po prostu kieruje dane do obiektu BetterMap. Prawdziwe działania mają miejsce w meto-
dzie add, która sprawdza liczbę elementów i wielkość obiektu Bet terMap: jeśli wartości te są równe,
średnia liczba elementów przypadających na obiekt Li nearMap wynosi 1, dlatego metoda wywołuje
metodę res i ze.

Metoda res i ze tworzy nowy obiekt BetterMap dwukrotnie większy od poprzedniego, a następnie
ponownie przeprowadza operację mieszania dla elementów ze starego odwzorowania, umiesz-
czając je w nowym odwzorowaniu.

Ponowne mieszanie jest niezbędne, ponieważ liczba obiektów Li nearMap zmienia mianownik operatora
dzielenia bez reszty w metodzie find_map. Oznacza to, że niektóre obiekty, które w ramach operacji
mieszania zostały odwzorowane na ten sam obiekt Li nearMap, zostaną rozdzielone (czy nie to było
przez nas pożądane?).

Operacja ponownego mieszania jest liniowa, dlatego metoda res i ze też taka jest, co może się wydać
niewłaściwe, ponieważ obiecałem, że metoda add będzie niezmienna w czasie. Pamiętaj jednak, że
operacja zmiany wielkości nie musi być wykonywana każdorazowo , dlatego metoda add jest zwykle
niezmienna w czasie i tylko niekiedy liniowa. Całkowita liczba jednostek pracy niezbędna do uru-
chomienia metody add n razy jest proporcjonalna do n. Wynika z tego, że średni czas dla każdego
wywołania tej metody jest stały!

Aby przekonać się,jak to działa, pomyśl o rozpoczęciu od pustej tablicy mieszającej i dodaniu
ciągu elementów. Zaczniemy od dwóch obiektów Li nearMap, dlatego pierwsze dwa uruchomienia
metody add przebiegają szybko (nie jest wymagana zmiana wielkości). Załóżmy, że każde takie
uruchomienie zajmuje jedną jednostkę pracy. Następne użycie metody add wymaga zmiany wiel-
kości, dlatego niezbędne jest wykonanie operacji ponownego mieszania dla pierwszych dwóch
elementów (przyjmijmy tutaj dwie dodatkowe jednostki pracy), a następnie dodanie trzeciego
elementu (kolejna jednostka pracy). Ponieważ dodanie następnego elementu wymaga jednej jed-
nostki, w przypadku czterech elementów dotychczas było wymaganych sześć jednostek pracy.

Następne użycie
metody add oznacza pięć jednostek, ale każde z trzech następnych uruchomień
tej metody wymaga tylko jednej jednostki pracy. W związku z tym w przypadku pierwszych
ośmiu wywołań metody add wymaganych było 14 jednostek pracy.

Następne użycie metody add wymaga dziewięciu jednostek, ale przed kolejną operacją zmiany wielkości
możemy wywołać metodę siedem razy, dlatego dla pierwszych 16 wywołań metody zastosowano
łącznie 30 jednostek.

Po 32 wywołaniach metody add wykorzystano w sumie 62 jednostki. Mam nadzieję, że zaczynasz


dostrzegać wzorzec. Po n wywołaniach metody add, gdzie n to potęga liczby dwa, całkowi ty koszt
wynosi 2n-2 jednostek pracy. Wynika z tego, że średni koszt p racy przypadający na wywołanie
metody add jest nieznacznie mniejszy niż 2 jednostki. Najlepszym wariantem jest sytuacja, gdy n
to potęga liczby dwa. W przypadku innych wartości n średni koszt pracy jest trochę większy, ale
nie jest to istotne. Ważne jest uzyskanie tutaj tempa wzrostu 0(1).

Tablice mieszające I 255


Na rysunku 21.1 pokazano w sposób graficzny, jak to dzi ała. Każdy blok reprezentuje jednostkę
pracy. Kolumny odczytywane od lewej do prawej st rony zawierają łączną liczbę jednostek pracy
dla każdego użycia metody add. Pierwsze dwa wywo łania metody wymagają jednej jednostki,
trzecie wywołanie oznacza t rzy jed nostki itd.

Rysunek 2 1.1. Kosz t operacji dodawania w przypadku tablicy mieszającej

Dodatkowy nakład pracy związa ny z operacją ponownego mieszania uwidoczniony jest w postaci
coraz wyższych kolumn i zwiększającego się o dstępu między nimi. Jeśli pozbyłbyś si ę wieży, roz-
mieszczając koszt ponownego mieszania we wszystkich wywo ła niach metody add, mógłbyś nary-
sunku zauważyć, że łączny koszt po n operacjach dodawania wynosi 2n-2.
Waż ną cechą tego algorytmu jest to, że w razie zmiany wielkości tablicy mieszającej zwiększa si ę
ona w sposób geom etryczny. Oznacza to, że wielkość jest mnożona przez stałą. Jeżeli zwiększysz
wielkość w sposób arytmetyczny, dodając każdo razowo stałą liczbę, średni czas przypadający na
wywo łanie metody add będzie liniowy.

Moją i mplementację klasy HashMap możesz znaleźć w pliku Map.py dostępnym pod adresem
ftp: /lftp.helion.pl!przyklady/myjep2.z ip. Pamiętaj jednak, że nie m a powodu, aby z niej korzystać.
Jeśli wymagasz odwzorowania, po prostu użyj słownika języka Python.

Słownik
analiza algorytmów
Sposób porównywania algorytmów pod względem czasu ich działa ni a i (lub) wym agań doty-
czących miejsca w pamięci.

model komputera
Uproszczona reprezentacja komputera używana do opisu algorytmów.
najgorszy przypadek
Dane wejściowe, które powo dują, że dany algorytm działa naj dłużej (lub wym aga najwięcej
miejsca).

składnik wiodqcy
W przypadku wielomianu jest to składnik o najwyższym wykładniku.

256 I Rozdzlał21. Analiza algorytmów


punkt przejścia
Skala problemu, w przypadku której dwa algorytmy wymagają takiego samego czasu działania
lub tyle samo miejsca.
tempo wz rostu
Zestaw funkcji, które rosną w sposób uważany za równorzędny w perspektywie analizy algo-
rytmów. Na przykład wszystkie funkcje rosnące liniowo cechują się tym samym tempem
wzrostu.
notacja „dużego O"
Notacja służąca do reprezentowania tempa wzrostu. Na przykład notacja O(n) reprezentuje
zestaw funkcji rosnących liniowo.

liniowy
Algorytm, którego czas działania jest proporcjonalny do skali problemu (przynajmniej dla
problemu o dużej skali).
kwadratowy
Algorytm, którego czas działania jest proporcjonalny do n 2 , gdzie n to miara skali problemu.

wysz ukiwanie
Problem polegający na lokalizowaniu elementu kolekcji (np. lista lub słownik) lub określaniu,
że nie istnieje w niej.

tablica mieszająca

Struktura danych reprezentująca kolekcję par klucz-wartość i przeprowadzająca wyszukiwa-


nie cechujące się niezmiennym czasem.

Słownik I 257
Skorowidz

A błędy
kształtu,
158
aktualizacja, 97 semantyczne, 38, 237, 242
aktualizowanie zmiennych, 92 składniowe, 36, 38, 237
akumulator, 132 uruchomieniowe, 36, 237, 239
algorytm, 96, 247
kwadratowy, 257 (
liniowy,257
wyszukiwania, 252 ciągi,101, 109, 121
alias, 128 ciągów, 157
analiza formatu, 184
algorytmów, 247, 256 częstość, 163
algorytmów wyszukiwania, 252 używania słów, 166
częstości, 163
Markowa, 169 D
operacji, 250
porównawcza, 174 dane
przypadku wprowadzane z klawiatury, 71
gra słów, 113 wyjściowe, 242

wybór struktury danych, 163 debugowanie,26,36,60,87,107,146, 192,237


składni, 25, 28 z użyciem gumowej kaczuszki, 174
argument, 43 definicja funkcji, 41, 48
funkcji, 37, 39, 48 deklaracja, 148
listy, 129 dekrementacja, 97
opcjonalny, 110 diagram
pozycyjny, 206, 21 1 klas, 219, 223
słowa kluczowego, 61, 235 obiektów, 188, 194, 215
atrybut, 188, 193 stanu, 31, 37, 142, 157
klasy, 214, 222 stosu, 45, 49
dla funkcji rekurencyjnych, 70
B dodawanie
kart, 217
bazy danych, 179, 184
nowych funkcji, 41
błąd
AttributeError, 241 dziedziczenie, 213, 218, 223
dzielen ie bez reszty, 65, 73
IndexError, 242
KeyError, 241
TypeError, 241

259
E H
elementy, 109, 121, 132, 147 hermetyzacja, 56, 61
obiektowe, 203 danych,220,223
histogram, 139
słów, 165
F
fabryka, 233, 236
filtrowanie, 125, 126, 132
flaga, 144, 148 iden tyczność, 133
formatowanie danych wyjściowych, 146 implementacja, 139, 147, 210
fragment, 109 indeks, 109, 116
listy, 124 inicjalizacja, 97
łańcucha, 103 inkrementacja, 97
funkcja, 39, 48, 195 instancja, 188, 193
„owocna", 48 instancje jako wartości zwracane, 190
„pusta", 49 instrukcja, 32, 37
alł,230 break, 94
any, 230 import, 40, 49
arc, 58 print, 23
avoids, 115 raise, 141, 147
diet, 156 return, 70, 74, 79
float, 39 whiłe, 92
len, 102, 138 instrukcje
os.path.isdir, 178 asercji, 200
os.path.join, 178 globalne, 145, 148
print_attributes, 211 przypisania, 31
randint, 164 rozszerzonego, 125
random, 164 warunkowe, 65, 74
str, 40 łańcuchowe, 68
uses_all, 116 zagnieżdżone, 68
uses_only, 115 wyświetlające, 28
zip, 156 złożone, 67, 74

funkcje interfejs, 57, 61, 210


boolowskie, 82 interpreter, 22, 27
czyste, 196, 200 iteracja, 91, 97
kwadratowe, 249 iterator, 159
matematyczne, 40
mieszające , 143, 147
J
owocne, 46, 79
polimorficzne, 21 O języki
puste, 46 formalne, 25, 28
naturalne, 25, 28
niskiego poziomu, 27
G
obiektowe, 211
gałąź, 74 wysokiego poziomu, 27
gra słów, 113
graf wywołań, 147

260 Skorowidz
K init, 206
items, 156
katalog, 184 metody
klasa, 187, 193, 203 list, 124
LinearMap, 254 łańcuchowe, 105
Time, 195 mieszanie, 143
klasy mnogość, 223
nadrzędne,218,223 model komputera, 256
podrzędne,218,223 moduł, 40, 49
klucz, 137, 147 collections, 232
kod „martwy", 88 copy, 192
kodowanie, 222 os.path, 178
rang i kolorów, 213 piekle, 180
kolejność operacji, 34, 37 random, 164
kolekcja liczników, 139 turtle, 53
komentarz, 35, 37 modyfikator, 197, 200
komunikat o błędzie, 39 możliwość mieszania, 147
konkatenacja łańcuchów, 35, 37
kopiowanie N
głębokie, 192
obiektu, 191 nadpisywanie, 174
płytkie, 192 nagłówek, 41, 48
koszt operacji dodawania, 256 najgorszy przypadek, 256
krotki, 151, 159 nazwy
argumentów, 153 plików, 177
jako wartości zwracane, 153 ścieżki, 177
z nazwą, 234 zmiennych, 31
niezmiennik, 199, 200
L, Ł niezmienność, 109
notacja
liczba „dużego O", 257
całkowita, 24, 28 z kropką, 40, 49
zmiennoprzecinkowa, 24, 28 notka dokumentacyjna, 60, 61
losowa, 164 NPMDD0,34
licznik, 105, 11 O, 232
lista, 121, 127, 132, 141, 154 o
zagnieżdżona, 121, 132
listy zmienne, 122 obiekt, 109, 127, 132, 187
łańcuch,24,28,35,101,127 bajtów, 180, 184
formatu, 176, 183 defaultdict, 232
niezmienny, 104 dict_items, 156
łańcuchowa instrukcja warunkowa, 68, 74 funkcji,48
funkcji zip, 159
M HashMap, 255
klasy, 193
metoda, 61, 203, 21 1 modułu,49
_init_,254 osadzony, 193
_str_,207 pliku, 113, 118
add,253 potoku, 181, 184
find_map, 254 Rectangle, 192
get, 253 obiektowy język programowania, 203

Skorowidz I 261
obiekty programowanie
kart, 213 funkcyjne, 200
zmienne, 190 obiektowe, 211
odczytywanie list słów, 113 projekt interfejsu, 53, 57
odejmowanie słowników, 167 projektowanie, 59
odwołanie, 129, 133 przyrostowe, 80, 88
odwzorowanie, 125, 137, 146 zaplanowane, 200
okleina, 223 proste powtarzanie, 54
operacje prostokąty, 189
na listach, 123 prototyp i poprawki, 200
na łańcuchach, 35 prototypowanie, 198
przechodzenia, 102, 123 przechodzenie, 102, 109
operator, 28 listy, 123
in, 106, 230 przechwytywanie wyjątków, 178, 184
operatory przeciążanie operatorów, 207, 212
arytmetyczne, 23 przekazywanie oparte na typie, 208, 212
formatu, 176, 183 przenoszenie kart, 217
logiczne, 66, 73
przenośność, 27
relacyjne, 66, 73
przepływ wykonywania, 43, 49, 240
wartości bezwzględnej, 65, 73
przypadek bazowy, 74
przypisanie, 37
p krotki, 152, 159
para klucz-wartość, 137, 147 rozszerzone, 125, 132
parametry, 43, 48 punkt przejścia, 249, 257
lokalne,44 pusty łańcuch, l 09
opcjonalne, 167
pętla, 55, 61, 140 R
for, 102
pętle nieskończone, 93, 97, 240 ramka, 45, 49
pierwiastki kwadratowe, 94 redukowanie, 125, 132
pierwszy program, 23 refaktoryzacja, 58, 61
plan projektowania, 59, 61 rekurencja, 65, 69, 74, 83
planowanie, 198 nieskończona,7 1 ,74,240

pliki, 175 relacja


nazwy, 177 JEST, 219, 223
odczytywanie, 175 MA,219, 223
tekstowe, 184 rozmieszczanie, 154, 159
zapisywanie, 175 rozwiązywanie problemu, 27
pluskwa, 26, 28 równoważność, 133
podmiot, 205, 211
polimorfizm, 209, 212 S, Ś
ponowne przypisanie, 91, 97
porównanie semantyka, 38
kart, 215 separator, 127, 133
łańcuchów, 107 singleton, 142, 147
prototypowania i planowania, 198 składnia, 25, 28
potoki, 181 składnik wiodący, 248, 256
powłoka, 184 skok wiary, 85
pozycja, 121 skrypt, 33, 37
program, 21, 28 słowa losowe, 168

262 Skorowidz
słownik, 137, 140, 146 wa rtość
słowo kluczowe, 37, 235 bezwzględna, 65
class, 32 domyślna, 173
sortowanie, 217 zapamiętywan a, 143, 144, 147
specjalny przypadek, 118 zwracana, 39, 48, 190
sprawdzanie warunek, 74
podsumowań, 146 końcowy,60
typów, 86, 146 wstępny, 60

stos, 45 wielozbiór, 232, 236


strażnik, 87, 88 wiersz zachęty, 23
struktura danych, 159, 163, 171 wybór struktury danych, 163
szkielet, 81, 88 wydajność względna algorytmów, 247
ścieżka, 177, 184 wyjątek, 38, 178
bezwzględna, 177, 184 wykonywanie, 37
względna, 177, 184 alternatywne, 67
śledzenie wsteczne, 49 pętli, 105, 116, 140
warunkowe, 67
T wykorzystanie indeksów, ll6
wyrażenia, 32, 37
tablica mieszająca, 147, 252, 257
boolowskie, 66, 73
talie, 216
generatora, 229, 236
tempo wzrostu, 248, 249, 257
listowe, 228, 236
token, 25, 28
warunkowe, 227, 236
treść, 48
wyszukiwanie, I 04, 11 O, 115, 147, 252
trwałość programów, 175, 183
odwrotne, 140, 147
tryb wyświetlanie
interaktywny, 37
obiektów, 204
skryptowy,33,37
talii, 216
tworzenie wywołanie, 106, 110
aliasu, 128, 133
funkcji, 39, 48
automatycznych sprawdzeń, 146
wyznaczanie wartości , 37
instancji, 188, 193
typy, 24, 28
definiowane przez programistę, 187 z
zachęta, 27
u zadeklarowanie zmiennej, 145
zagnieżdżona instrukcja warunkowa, 68, 74
ukrywanie informacji, 212
zależność, 223
uogólnianie, 56, 61
uruchamianie interpretera, 22 zapisywanie modułów, 182
usuwanie zbieranie, 159
elementów, 126 argum entów, 153
kart, 217 zbiory, 230
złożen ie, 41, 49, 82
użycie
metody gumowej kaczuszki, 172 zmienne, 31, 36, 44
modułu piekle, 180
globalne, 144, 148
lokalne,48
w znak
tymczasowe, 79, 88

wartości, 24, 28, 127, 147 nowego wiersza, 72


listy, 121 podkreślenia, 32

Skorowidz I 263

You might also like