Professional Documents
Culture Documents
Helion - Myśl W Języku Python! Nauka Programowania. Wydanie II
Helion - Myśl W Języku Python! Nauka Programowania. Wydanie II
Helion - Myśl W Języku Python! Nauka Programowania. Wydanie II
Downey
Spis treści
Przedmowa .............................................................................................................. 11
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
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
6 Spis trełcl
12. Krotki „„„„„„„„„„„„„„„„„„„„„„„„„„„„„„„„„„„„„„„„ „„„„ „ „ „ „ „ „ „ „ „„„„ „„„151
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
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
Skorowidz „„„„„..„... „.„... „.„ .. „...„.„..„.. „..„.. „..„.. „... „.„.. „.. „.. „.. „.. „.. „.„„„„„...257
Spis tre~cl 9
10 Spis trełcl
Przedmowa
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.
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.
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.
• 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.
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.
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.
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.
18 Pnedmowa
• Carlos Tafur przesłał stronę poprawek i sugestii.
• Martin Nordsletten znalazł błąd w rozwiązaniu ćwiczenia.
• 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.
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.
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.
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.
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:
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,
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:
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!'
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.
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 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 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ść
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.
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.
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
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.
łańcuch
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
analiza składni
Ma na celu sprawdzenie programu i dokonanie analizy struktury składniowej.
pluskwa
Błąd w programie.
Ć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.
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.
Instrukcje przypisania
Instrukcja przypisania tworzy nową zmienną i nadaje jej wartość:
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 .
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.
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.
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)
Tryb skryptowy 33
Na przykład skrypt
print(l)
X = 2
print(x)
zwraca wynik
Aby sprawdzić słuszność rozumowania, wpisz następujące instrukcje w oknie interpretera języka
Python i przekonaj się, co uzyskasz:
X =
X +
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.
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.
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
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
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ć.
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.
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.
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
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
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.
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?
Funkcje
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)
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.
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)
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)
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
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.
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.
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 ()
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ń . " )
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).
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.
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.
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.
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.
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:
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
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ł
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:
Ć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:
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 (' - ' )
Ćwlaenla 51
S2 Rozdział 3. Funkcje
ROZDZIAŁ 4.
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ę).
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.
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:
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:
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.
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) :
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
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.
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
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)
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)
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.
Słownik
metoda
Funkcja powiązana z obiektem i wywoływana za pomocą notacji z kropką.
pętla
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).
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).
Ćwiczenie 4.3.
Utwórz odpowiedni, ogólny zestaw funkcji, które mogą rysować kształty (rysunek 4.2).
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.
Ćwlaenla 63
64 Rozdział 4. Analiza przypadku: projekt Interfejsu
ROZDZIAŁ 5.
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.
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
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 )
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.
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.
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.
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 . ')
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.
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ąć.
__mai n__
countdown
countdown n.-. 2
countdown n- 1
coun tdown n- - o
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 ()
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.
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 .
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
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
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 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ź
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
Ćwiczenie 5.2.
Zgodnie z ostatnim twierdzeniem Fermata nie istnieją żadne dodatnie liczby całkowite a, b i c takie, że:
a"+bn= C11
Ć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.
Ć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.
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.
Ć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
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ą.
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.
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.
Kolejnym krokiem jest wyznaczenie powierzchni koła o takim promieniu. Zostało to zapisane w po-
staci następującego kodu:
result = area(radius)
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:
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
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.
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}:
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
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
factorial in - o
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
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ą:
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.
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:
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:
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"
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.
Ć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]
Ć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.
Ć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).
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.
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)
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! ".
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 )
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
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!
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:
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.
Ćwlaenla 99
100 I Rozdział 7. Iteracja
ROZDZIAŁ 8.
Łańcuchy
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.
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.
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 '
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).
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
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.
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
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:
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 ł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' )
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
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.
j ,.. 3
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ść
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
argument opcjonalny
Argument funkcji lub metody, który nie jest wymagany.
Ćwiczenia
Ćwiczenie 8.1.
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
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
Ć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.
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.
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.
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?
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
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.
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)
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.
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:
Ćwiczenie 9.8.
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.
Ćwiczenie 9.9.
Ć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ę:
Lista bez żad nych elementów nazywana jest pustą li stą. Możliwe jest utworzenie listy z pustymi
nawiasam i kwadratowymi [].
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
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.
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]
W pierwszym przykładzie listę [O] powtórzono cztery razy. W drugim przykładzie lista [1 , 2, 3]
powtarzana jest trzykrotnie.
»> 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 ł]
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
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:
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.
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 ( ł]
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
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 ']
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:
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:
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 '
a~ 'banan·
a-....._ 'banan'
b ------- 'banan' b ____.,.
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 - [ 1,2,3)
b ----- [ 1. 2, 3 ]
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
a~
b ~ [ 1,2,3]
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]
Parametr t i zmienna l e t ter s to aliasy tego samego obiektu. Na rysunku 10.5 zaprezentowano
diagram stanu.
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
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: ]
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.
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.
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.
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ść.
identyczny
Ten sam obiekt (co sugeruje równoważność).
odwołanie
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?
Ć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.
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.
Ć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
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.
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] .
Histogram wskazuje, że litery a i b pojawiają się raz, litera o dwa razy itd.
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.
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.
>>> 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
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.
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·
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
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
n ----- 1 n _. O
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:
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
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
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.
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.
Słownik
odwzorowanie
Relacja, w której każdemu elementowi jednego zestawu odpowiada element innego zestawu.
słownik
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
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 .
Ć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.
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).
Ćwiczenie 11.6.
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 )
» > t y pe(tl)
<class 't upl e'>
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'
Ponieważ krotki są niezmienne, nie możesz zmodyfikować elementów. Możesz jednak zastąpi ć
jedną krotkę inną:
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
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
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'
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
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.
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
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 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)
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.
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
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:
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.
krotka
o~ 'Cleese;
1- 'John'
słownik
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:
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'
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) '
»> 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)'
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.
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)?
Ć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.
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.
Ćwlaenla I 161
162 Rozdział 12. Krotki
ROZDZIAŁ 13.
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.
Ć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)
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
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.
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())
A tutaj wyniki:
Łąc z na liczba słów : 161080
Liczba róż ny c h słów: 7214
Następująca funkcja pobiera histogram i zwraca listę krotek złożonych ze słowa i częstości jego
występowania:
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 ' )
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.
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).
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)
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.
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 )
Ćwiczenie 13.7.
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?
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.
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.
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.
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
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.
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?
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.
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
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.
Ć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.
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ę.
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.
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
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.
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
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 '
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 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 '
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ść).
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 . '
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()
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.
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()
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.
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
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' ))
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?
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'
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.
Słownik
trwałość
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
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
ścieżka względna
ścieżka bezwzględna
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
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.
Ćwiczenie 14.2.
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.
Ć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.
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>
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
Zmienna blank odwołuje się do obiektu Poi nt, który zawiera dwa atrybuty. Każdy atrybut odnosi
się
do liczby zmiennoprzecinkowej.
»> blank.y
4. 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.
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.
• 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ą.
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
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:
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
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
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'
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.
Słownik I 193
„płytkie" 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.
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.
Ć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.
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 .
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
Time
time- - hour ----;- 11
minute ------ 59
second --- 30
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.
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.
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.
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
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 .
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.
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
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)
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 .
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.
Ć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.
Ćwlaenla I 201
202 I Rozdział 16. Klasy I funkcje
ROZDZIAŁ 17.
Klasy i metody
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ą.
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) )
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.
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))
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:
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.
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
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.
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+.
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.
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.
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)
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}
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.
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.
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.
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.
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.
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 ,
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ą.
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.
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.
rank_names
Card
cardl suit ------ 1
rank _,... 11
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.
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.
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 ) :
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.
Poni eważ metoda pop usuwa ostatniq kartę na liście, rozdanie odbywa się od spodu talii.
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)
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
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()
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ą.
Deck (ard
Hand
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 = ()
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 = ()
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.
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.
klasa nadrzędna
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ń.
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.
Ć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".
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)
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.
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.
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.
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
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,
Rezultatem jest zbiór, a nie słownik, ale w przypadku operacji takich jak iteracja zachowanie jest
identyczne.
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.
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:
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.
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.
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ść.
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 '].
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
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:
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.
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
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')
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 '
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
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.
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
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.
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.
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ść?
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").
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".
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.
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.
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.
• 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.
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.
Dysponujesz teraz możliwością wyświetlenia wartości zmiennej count przed zwróceniem jej.
• 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).
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.
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ąć.
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.
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.
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.
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.
Ć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?
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!
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.
Łą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ę)?
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ć
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)
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.
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.
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.
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
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
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
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
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
Skorowidz I 263