Dawson M. - Python Dla Każdego. Podstawy Programowania. Wydanie III

Share Embed Donate


Short Description

Download Dawson M. - Python Dla Każdego. Podstawy Programowania. Wydanie III...

Description

Spis treści

O autorze ...........................................................................11 Wstęp ...............................................................................13 Rozdział 1. Wprowadzenie. Program Koniec gry ....................................15 Analiza programu Koniec gry ..........................................................15 Co warto wiedzieć o Pythonie? .......................................................16 Konfiguracja Pythona w systemie Windows ......................................19 Konfiguracja Pythona w innych systemach operacyjnych ...................20 Wprowadzenie do IDLE ..................................................................20 Powrót do programu Koniec gry ......................................................26 Podsumowanie .............................................................................29

Rozdział 2. Typy, zmienne i proste operacje wejścia-wyjścia. Program Nieistotne fakty ...................................................31 Wprowadzenie do programu Nieistotne fakty ...................................31 Użycie cudzysłowów przy tworzeniu łańcuchów znaków .....................32 Używanie sekwencji specjalnych w łańcuchach znaków .....................36 Konkatenacja i powielanie łańcuchów .............................................40 Operacje na liczbach .....................................................................42 Pojęcie zmiennych .........................................................................45 Pobieranie danych wprowadzanych przez użytkownika ......................48 Używanie metod łańcucha ..............................................................50 Stosowanie właściwych typów ........................................................54 Konwersja wartości .......................................................................56 Powrót do programu Nieistotne fakty ..............................................59 Podsumowanie .............................................................................61

Rozdział 3. Rozgałęzianie kodu, pętle while, projektowanie programu. Gra Odgadnij moją liczbę ....................................................63 Wprowadzenie do gry Jaka to liczba? ..............................................63 Generowanie liczb losowych ...........................................................64 Używanie instrukcji if .....................................................................67

6

Python dla każdego. Podstawy programowania Używanie klauzuli else ...................................................................71 Używanie klauzuli elif .....................................................................73 Tworzenie pętli while .....................................................................76 Unikanie pętli nieskończonych ........................................................80 Traktowanie wartości jako warunków ..............................................83 Tworzenie umyślnych pętli nieskończonych ......................................86 Korzystanie z warunków złożonych ..................................................88 Projektowanie programów ..............................................................93 Powrót do gry Jaka to liczba? .........................................................95 Podsumowanie .............................................................................97

Rozdział 4. Pętle for, łańcuchy znaków i krotki. Gra Wymieszane litery ........................................................99 Wprowadzenie do programu Wymieszane litery ................................99 Liczenie za pomocą pętli for .........................................................102 Stosowanie funkcji i operatorów sekwencji do łańcuchów znaków ...105 Indeksowanie łańcuchów .............................................................107 Niemutowalność łańcuchów .........................................................111 Tworzenie nowego łańcucha .........................................................113 Wycinanie łańcuchów ..................................................................116 Powrót do gry Wymieszane litery ...................................................128 Podsumowanie ...........................................................................131

Rozdział 5. Listy i słowniki. Gra Szubienica ........................................133 Wprowadzenie do gry Szubienica ..................................................133 Korzystanie z list .........................................................................135 Korzystanie z metod listy .............................................................140 Kiedy należy używać krotek zamiast list? .......................................144 Używanie sekwencji zagnieżdżonych ..............................................145 Referencje współdzielone ............................................................149 Używanie słowników ....................................................................152 Powrót do gry Szubienica .............................................................159 Podsumowanie ...........................................................................165

Rozdział 6. Funkcje. Gra Kółko i krzyżyk .............................................167 Wprowadzenie do gry Kółko i krzyżyk .............................................167 Tworzenie funkcji .........................................................................169 Używanie parametrów i wartości zwrotnych ....................................172 Wykorzystanie argumentów nazwanych i domyślnych wartości parametrów ..............................................................................176 Wykorzystanie zmiennych globalnych i stałych ...............................181 Powrót do gry Kółko i krzyżyk ........................................................185 Podsumowanie ...........................................................................196

Spis treści

Rozdział 7. Pliki i wyjątki. Gra Turniej wiedzy .....................................199 Wprowadzenie do programu Turniej wiedzy ....................................199 Odczytywanie danych z plików tekstowych .....................................200 Zapisywanie danych do pliku tekstowego ......................................206 Przechowywanie złożonych struktur danych w plikach .....................209 Obsługa wyjątków ........................................................................214 Powrót do gry Turniej wiedzy .........................................................218 Podsumowanie ...........................................................................223

Rozdział 8. Obiekty programowe. Program Opiekun zwierzaka .............225 Wprowadzenie do programu Opiekun zwierzaka .............................225 Podstawy programowania obiektowego .........................................227 Tworzenie klas, metod i obiektów .................................................228 Używanie konstruktorów ..............................................................230 Wykorzystywanie atrybutów ..........................................................232 Wykorzystanie atrybutów klasy i metod statycznych ........................236 Hermetyzacja obiektów ................................................................240 Używanie atrybutów i metod prywatnych ........................................241 Kontrolowanie dostępu do atrybutów ............................................245 Powrót do programu Opiekun zwierzaka ........................................249 Podsumowanie ...........................................................................252

Rozdział 9. Programowanie obiektowe. Gra Blackjack ........................255 Wprowadzenie do gry Blackjack ....................................................255 Wysyłanie i odbieranie komunikatów .............................................256 Tworzenie kombinacji obiektów ....................................................259 Wykorzystanie dziedziczenia do tworzenia nowych klas ...................263 Rozszerzanie klasy poprzez dziedziczenie ......................................263 Modyfikowanie zachowania odziedziczonych metod ........................269 Polimorfizm ................................................................................273 Tworzenie modułów .....................................................................273 Powrót do gry Blackjack ...............................................................277 Podsumowanie ...........................................................................287

Rozdział 10. Tworzenie interfejsów GUI. Gra Mad Lib ............................289 Wprowadzenie do programu Mad Lib .............................................289 Przyjrzenie się interfejsowi GUI .....................................................291 Programowanie sterowane zdarzeniami .........................................292 Zastosowanie okna głównego .......................................................293 Używanie przycisków ...................................................................298 Tworzenie interfejsu GUI przy użyciu klasy .....................................300 Wiązanie widżetów z procedurami obsługi zdarzeń .........................303 Używanie widżetów Text i Entry oraz menedżera układu Grid ...........305

7

8

Python dla każdego. Podstawy programowania Wykorzystanie pól wyboru ............................................................310 Wykorzystanie przycisków opcji .....................................................314 Powrót do programu Mad Lib ........................................................318 Podsumowanie ...........................................................................322

Rozdział 11. Grafika. Gra Pizza Panic ...................................................323 Wprowadzenie do gry Pizza Panic ..................................................323 Wprowadzenie do pakietów pygame i livewires ..............................325 Tworzenie okna graficznego .........................................................325 Ustawienie obrazu tła ..................................................................328 Układ współrzędnych ekranu graficznego .......................................331 Wyświetlanie duszka ...................................................................332 Wyświetlanie tekstu ....................................................................336 Wyświetlanie komunikatu .............................................................339 Przemieszczanie duszków ............................................................342 Radzenie sobie z granicami ekranu ...............................................344 Obsługa danych wejściowych z myszy ...........................................347 Wykrywanie kolizji ........................................................................350 Powrót do gry Pizza Panic .............................................................353 Podsumowanie ...........................................................................360

Rozdział 12. Dźwięk, animacja i rozwijanie programu. Gra Astrocrash .................................................................361 Wprowadzenie do gry Astrocrash ..................................................361 Odczyt klawiatury ........................................................................363 Obracanie duszka .......................................................................365 Tworzenie animacji ......................................................................368 Przegląd obrazów eksplozji ...........................................................369 Wykorzystywanie dźwięku i muzyki ................................................371 Planowanie gry Astrocrash ...........................................................376 Utworzenie asteroidów .................................................................377 Obracanie statku ........................................................................380 Poruszanie statku .......................................................................382 Wystrzeliwanie pocisków ..............................................................385 Regulowanie tempa wystrzeliwania pocisków .................................388 Obsługa kolizji ............................................................................390 Dodanie efektów eksplozji ...........................................................393 Dodanie poziomów gry, rejestracji wyników oraz tematu muzycznego ...........................................................397 Podsumowanie ...........................................................................405

Spis treści

Dodatek A. Opis pakietu livewires ......................................................407 Pliki archiwów .............................................................................407

Dodatek B. Opis pakietu livewires ......................................................409 Pakiet livewires ...........................................................................409 Klasy modułu games ...................................................................409 Funkcje modułu games ................................................................417 Stałe modułu games ...................................................................418 Stałe modułu color ......................................................................422

Skorowidz ........................................................................423

9

O autorze Michael Dawson pracował zarówno jako programista, jak i projektant i producent gier komputerowych. Oprócz praktycznego doświadczenia zdobytego w sferze produkcji gier Mike uzyskał licencjat w dziedzinie informatyki na Uniwersytecie Południowej Kalifornii. Obecnie uczy programowania gier na Wydziale Produkcji Gier Szkoły Filmowej w Los Angeles. Mike uczył także studentów programowania gier w ramach zajęć prowadzonych na UCLA Extension i Digital Media Academy w Stanfordzie. Jest autorem trzech innych książek: Beginning C++ through Game Programming, Guide to Programming with Python oraz C++ Projects: Programming with Text-Based Games. Możesz odwiedzić jego stronę internetową pod adresem www.programgames.com, aby dowiedzieć się więcej lub uzyskać pomoc w kwestiach dotyczących dowolnej z jego książek.

Wstęp Z

ekranu wpatrywała się we mnie postać, której twarz wydała mi się znajoma — to była moja twarz. Ziarnista i spikselizowana, ale pomimo wszystko moja. Patrzyłem z obojętnym zaciekawieniem na moje oblicze poskręcane i powykrzywiane ponad wszelką ludzką miarę, aż w końcu z mojej głowy wyskoczył embrion kosmity. Głos za mną powiedział: „Chcesz to zobaczyć jeszcze raz?”. Nie, to nie był jakiś koszmarny sen, to była moja praca. Pracowałem w firmie produkującej i projektującej gry komputerowe. Musiałem także „zagrać główną rolę” w naszej pierwszej produkcji, grze przygodowej, w której gracz goni mnie po ekranie kliknięciami. A jeśli graczowi nie uda się w porę znaleźć rozwiązania… cóż, myślę, że wiesz, czym to się kończy. Pracowałem także jako programista w ważnej firmie oferującej usługi internetowe, podróżując w różne miejsca kraju. I chociaż te dwa kierunki pracy mogą wydawać się całkiem różne, podstawowe umiejętności niezbędne do odniesienia sukcesu w każdym z nich zaczęły się kształtować, gdy pisałem proste gry na moim domowym komputerze jako dziecko. Celem tej książki jest nauczenie Cię języka programowania Python oraz umożliwienie Ci uczenia się programowania w taki sam sposób, w jaki uczyłem się ja — poprzez tworzenie prostych gier. Nauka programowania poprzez pisanie programów, które bawią, ma w sobie coś ekscytującego. Lecz nawet w przykładach, które są zabawne, spotkasz się z dozą poważnego programowania. Omawiam wszystkie podstawowe tematy, jakich mógłbyś oczekiwać w tekście o charakterze wprowadzenia, a nawet poza nie wykraczam. W dodatku pokazuję koncepcje i techniki, które mógłbyś zastosować w bardziej mainstreamowych projektach. Jeśli programowanie jest dla Ciebie czymś nowym, dokonałeś właściwego wyboru. Python jest doskonałym językiem dla początkujących. Ma przejrzystą i prostą składnię, która sprawi, że zaczniesz pisać użyteczne programy niemal natychmiast. Python udostępnia nawet tryb interaktywny, który oferuje bezzwłoczną informację zwrotną, co pozwoli Ci na przetestowanie nowych pomysłów prawie natychmiast. Jeśli trochę już przedtem programowałeś, to mimo wszystko dokonałeś właściwego wyboru. Python ma w sobie całą moc i elastyczność, jakiej mógłbyś oczekiwać od nowoczesnego, obiektowego języka programowania. Ale nawet przy całej jego mocy

14

Python dla każdego. Podstawy programowania

możesz być zaskoczony tym, jak szybko możesz budować programy. Faktycznie, koncepcje tak szybko przekładają się na język komputera, że Python został nazwany „programowaniem z szybkością myśli”. Jak każda dobra książka i ta rozpoczyna się od początku. Pierwszą rzeczą, jaką omawiam, jest instalacja Pythona w systemie Windows. Potem przedstawiam poszczególne koncepcje, jedną po drugiej, poprzez pisanie małych programów w celu zademonstrowania każdego kroku. Zanim zakończę książkę, omówię atrakcyjnie brzmiące tematy, takie jak struktury danych, obsługa plików, wyjątki, projektowanie obiektowe oraz programowanie interfejsu GUI i obsługi multimediów. Mam też nadzieję na pokazanie Ci nie tylko, jak programować, ale także, jak tworzyć projekty. Nauczysz się, jak organizować swoją pracę, dzielić problemy na możliwe do ogarnięcia kawałki oraz jak udoskonalać swój kod. Czasem spotkasz się z wyzwaniami, ale nigdy nie będziesz przytłoczony. Przede wszystkim, ucząc się, będziesz się dobrze bawić. I przy okazji utworzysz kilka małych, lecz fajnych gier komputerowych. Pełny kod programów zaprezentowanych w tej książce wraz z niezbędnymi plikami pomocniczymi możesz pobrać ze strony internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm). Strona ta zawiera również pliki instalacyjne oprogramowania, które będzie Ci potrzebne do uruchamiania programów. Bardziej szczegółowy opis tego, co jest dostępne na stronie internetowej, znajdziesz w dodatku A, „Strona internetowa książki”. Na całej trasie podróży poprzez treść tej książki umieszczam pewne drogowskazy w celu podkreślenia ważnych koncepcji.

Wskazówka To dobre rady, jakie doświadczeni programiści lubią przekazywać innym.

Pułapka Istnieje kilka obszarów, w których łatwo o zrobienie błędu. Pokazuję je.

Sztuczka To propozycje technik i skrótów, które ułatwią Twoje życie programisty.

W świecie rzeczywistym Kiedy przeanalizujesz gry przedstawione w tej książce, pokażę Ci, jak występujące w nich koncepcje są wykorzystywane w celach wykraczających poza tworzenie gier.

Sprawdź swoje umiejętności Na końcu każdego rozdziału zaproponuję Ci kilka programów, jakie możesz napisać w oparciu o przyswojone umiejętności.

1 Wprowadzenie. Program Koniec gry P

� � �")r

ragramowanie polega zasadniczo na spowodowaniu, 7,e

Nie jest to za bardzo techniczna definicja, ale mimo to

tter coś zrobił.

okładna. Dzięki

�Miewielkie narzędzie czy �t�iczny interfejs u7,ytkownika (ang. graphical user interface- GUl). Będzie ca� �wój - coś, co sam wykonałeś poznaniu Pythona potrafisz utworzyć program- pro

też produkt biznesowy wyposażony w pełni profes

- i będzie robił dokładnie to, co mu kazałeś. P�mowanie jest po części nauką, po części sztuką oraz jedną wiel ką p r zygod zpoczyn ając c zyta nie tego rozdziału,



�),{ython. Z rozdziału tego dowiesz się: Vego wspaniałego; co to jest Python i co w nim � jak zainstalować Pytho �a iwoim komputerze; i_ jak wypisywać tekst �nie;

wkraczasz na drogę programowania • • • •

co to są komen



jak wykorzy



ak ich używać;

�ać zmtegrowane środowisko programistyczne do tworzenia, edycji, ur�ania i zapisywania na dysku swoich programów.

Analiza programu Koniec gry

Projekt o nazwie Koniec gry, przedstawiony w tym rozdziale, sprowadza się do wyświetlenia dwóch słów- cieszących się w świecie gier komputerowych złą sławą- "Koniec gry".

Na rysunku 1.1 pokazano ten program w działaniu.

16

Rozdział

1. Wprowadzenie. Program Koniec gry

��

Rysunek 1.1. Aż nadto znajomy komunikat pojawiający si

� � o okno, w którym

To, co widać na rysunku 1.1, nazywa się oknem konso

�;cVe' jak okna graficznego ...Jt�JI) aplikacje konsolowe są interfejsu użytkownika (ang. Graphical User Inte�'Ł__ łatwiejsze do napisania i stanowią dobry punkt � � dla początkującego programisty. Program Koniec gry jest dość prosty; praw�\eJl wiąc, jest on jednym z najprostszych może hyć wyświetlany tylko tekst. Chocia7, nie tak sym

programów w języku Python, jakie można

.@

ać. Właśnie dlatego jest prezentowany

�omnego programu poznajesz wszystkie czynności konfiguracyjne niezbędn��poczęcia programowania w Pythonie, takie jak instalacja tego języka w wo ·m �mie. Wykonujesz też cały proces tworzenia, w tym rozdziale. Poprzez utworzenie



zapisywania na dysku i uruc

�ch czynnoś�k,ę

podstawo

programamL

W

ia�ia programu. Po opanowaniu tych wszystkich

sz gotów zająć się większymi i bardziej interesującymi



świecie �wistym

Program Koniec gry jest w gruncie rzeczy jedynie odmianą tradycyjnego programu Witaj świecie, który wyświetla słowa "Witaj świecie" na ekranie i jest często pierwszym programem, jaki pisze nowicjusz w celu postawienia swojego pierwszego kroku w nowym języku. Jest to tak popularny pierwszy program, że termin "Witaj świecie" stał się powszechnie zrozumiałym pojęciem w dziedzinie programowania.

Co warto wiedzieć o Pythonie?

Python jest potężnym, lecz mimo to łatwym w użyciu językiem programowania

opracowanym przez Guida van Rossuma i opublikowanym po raz pierwszy w 1991 r.

Za pomocą Pythona można szybko napisać mały projekt. Ale Pythona cechuje także

Co warto wiedzieć o Pythonie?

dobra skalowalność i może być używany do tworzenia komercyjnych aplikacji

o kluczowym znaczeniu.

Jeśli zajrzysz do dokumentacji Pythona, hyć może zauważysz niepokojącą liczhę

odniesień do spamu, jaj i liczby 42. Wszystkie te odniesienia są wyrazem hołdu dla

angielskiej trupy komików Monty Python, która stała się inspiracją dla autora przy

wyborze nazwy języka. Mimo że Guido van Rossum wyhrał nazwę Python, nawiązując

do grupy komediowej, oficjalną maskotką języka stał się wąż pyton. (Co było faktycznie

najlepszym rozwiązaniem, ponieważ i tak byłoby dość trudno wkomponować twarze

sześciu brytyjskich komików w ikon\ programu).

Jest na świecie wiele języków programowania. Co takiego wspaniałego jest w Pythonie?

Pozwól, że Ci wyjaśnię.

Python jest łatwy w użyciu Głównym celem każdego języka programowania jest zapełnie

�� �

�lu�1

��

programisty a komputerem. Większość popularnych językó

słyszałeś, takich jak Visual Basic, C# i J ava, jest uważana z



ędzy mózgiem

�c zapewne

wysokiego poziomu,

�..syVwej. I tak jest rzeczywiście. � nawet jeszcze bliższy języka Ale Python, ze swoimi prostymi i klarownymi re� LK proste, że zostało nazwane angielskiego. Tworzenie programów w Pytho � � "programowaniem z szybkością myśli". Łatwo� zystania z Pythona przekłada si\ na wydajność profesjonalnych progr.:_ ��ogramy pisane w Pythonie są krótsze co oznacza, że są one bliższe języka człowieka niż maSZ)

i są tworzone w krótszym czasie niż

Python jest mocnl'\

�ur

w wielu innych popularnych językach.



�by oczekiwać od nowoczesnego języka programowania. 1" książki, potrafisz pisać programy, które stosują interfejs Zanim skończysz lektur� �orzystują różnorodne struktury danych. GUl, przetwarzają.f\ Python ma całą moc, jakiej

Python jest



zająca silnym językiem, aby zainteresować twórców

oprogramowania na całym świecie, a także takie firmy, jak Google, IBM, Industrial Light

+

Magie, Microsoft, NASA, Red Hat, Verizon, Xcrox i Yahoo!. Python jest również

narzędziem wykorzystywanym przez profesjonalnych programistów gier. Kod tworzony

w Pythonie zawierają gry publikowane przez takich producentów, jak Electronic Arts,

2K Games i Disney Interactive Media Group.

Python jest językiem obiektowym Programowanie obiektowe (ang. object-orientedpro gramming- OOP) to nowoczesne

podejście do rozwiązywania problemów przy użyciu komputerów. Ucieleśnia ono intuicyjną

metodę reprezentowania informacji i działań w programie. Z pewnością nie jest to jedyny

sposób pisania programów, ale zwłaszcza przy większych projektach stanowi często najlepszą drogę.

17

18

Rozdział

1. Wprowadzenie. Program Koniec gry

Języki takie jak C#,Java i Python są obiektowe. Ale Python pod jednym względem je

przewyższa. W C# czyJavie programowanie OOP nie jest opcjonalne. To sprawia, że

krótkie programy stają się niepotrzebnie skomplikowane i niezbędna jest pewna ilość

wyjaśnień, zanim nowy programista będzie mógł zrobić coś znaczącego. W Pythonie

przyjęto inne podejście- korzystanie z technik OOP jest opcjonalne. Masz całą potęgę

OOP do swojej dyspozycji, ale możesz jej używać wtedy, kiedy rzeczywiście jej potrzebujesz.

Masz do napisania krótki program, który naprawdę nie wymaga stosowania OOP?

Nie ma problemu. Tworzysz duży projekt z udziałem zespołu programistów, w którym

korzystanic z OOP jest niczb) wpisz print("Koniec gry"), a potem naciśnij klawisz Enter. Interpreter reaguje przez wyświetlenie na ekranie tekstu: Koniec gry

Ta-dam! Napisałeś swój pierwszy program w Pythonie! Jesteś prawdziwym programistą (pozostało Ci co prawda jeszcze trochę do nauki, ale to dotyczy nas wszystkich).

21

22

Rozdział 1. Wprowadzenie. Program Koniec gry

Rysunek 1.4. Python w trakcie interaktywnej sesji oczekuje na Twoje polecenie

Użycie funkcji print Spójrz na wiersz, który wprowadziłeś, print("Koniec gry"). Zauważ, jakie to proste. Bez żadnej wiedzy o programowaniu potrafiłeś zapewne odgadnąć, jaka jest funkcja tego kodu. To jest właśnie kwintesencja Pythona, który jest zwięzły i przejrzysty. Docenisz to jeszcze mocniej, kiedy dowiesz się, jak tworzyć w tym języku bardziej skomplikowane rzeczy. Funkcja print() wyświetla tekst ujęty w cudzysłowy, który umieszczasz wewnątrz nawiasów. Jeśli nie umieścisz niczego wewnątrz nawiasów, zostanie wyprowadzony pusty wiersz.

Pułapka Python uwzględnia wielkość liter — nazwy funkcji składają się umownie z małych liter. Dlatego polecenie print("Koniec gry") zostanie wykonane, ale polecenia Print("Koniec gry") i PRINT("Koniec gry") już nie.

Nauka żargonu Teraz, kiedy zostałeś programistą, musisz sypać wokół tymi wymyślnymi terminami, które są zrozumiałe tylko dla programistów. Funkcja to jakby miniprogram, który startuje i wykonuje pewne określone zadanie. Zadaniem funkcji print() jest wyświetlenie

Wprowadzenie do IDLE

jakiejś wartości (lub ciągu wartości). Uruchamiasz, czyli wywołujesz funkcję, używając jej nazwy, po której należy umieścić parę nawiasów. Wykonałeś dokładnie tę czynność w trybie interaktywnym, kiedy wprowadziłeś tekst print("Koniec gry"). Czasem podajesz, czyli przekazujesz do funkcji wartości, od których będzie zależeć jej działanie. Umieszczasz te wartości, zwane argumentami, między nawiasami. W przypadku swojego pierwszego programu przekazałeś do funkcji print() argument "Koniec gry", którego funkcja użyła do wyświetlenia komunikatu Koniec gry.

Wskazówka Funkcje w Pythonie również zwracają wartości, czyli dostarczają informacje z powrotem do tej części programu, która wywołała daną funkcję. Nazywają się one wartościami zwracanymi. Dowiesz się więcej o wartościach zwracanych w rozdziale 2.

W tym szczególnym przypadku możesz być jeszcze dokładniejszy, mówiąc, że wartość "Koniec gry", którą przekazałeś do funkcji print(), jest łańcuchem. Nie oznacza to nic

innego jak ciąg znaków, takich, jakie znajdują się na Twojej klawiaturze. Nazwa „łańcuch” może wydawać się dziwna — „tekst” lub „słowa” byłyby może bardziej zrozumiałe — ale u jej źródła jest myśl, że tekst jest łańcuchem, czyli ciągiem znaków. Od strony technicznej "Koniec gry" jest łańcuchowym literałem, ponieważ jest w sensie dosłownym ciągiem znaków, które tworzą ten tekst. Wiersz, który wprowadziłeś w interpreterze, jest traktowany również jako instrukcja. W języku polskim zdanie wyraża kompletną myśl. W Pythonie instrukcja jest kompletnym poleceniem. Powoduje wykonanie czegoś. Każdy program zawiera pewną liczbę instrukcji. W końcu teraz, kiedy już jesteś programistą, możesz komuś oznajmić, że napisałeś jakiś kod w Pythonie. Kod oznacza instrukcje programu. Możesz również używać czasownika „kodować”, mając na myśli czynność programowania. Możesz na przykład powiedzieć, że siedziałeś przy komputerze całą noc, pogryzając Hot Cheetos, popijając Mountain Dew i kodując jak szalony.

Generowanie błędu Komputery biorą wszystko dosłownie. Jeśli pomylisz się w nazwie funkcji, choćby to dotyczyło tylko jednej litery, komputer nie będzie miał absolutnie żadnego pojęcia, co masz na myśli. Na przykład jeśli w trybie interaktywnym wpiszę po znaku zachęty primt("Koniec gry"), interpreter odpowie czymś takim: Traceback (most recent call last): File "", line 1, in primt("Koniec gry") NameError: name 'primt' is not defined

W tłumaczeniu na język polski interpreter mówi: „Hę?”. Kluczowy wiersz komunikatu o błędzie to NameError: name 'primt' is not defined. Jest to stwierdzenie, że interpreter nie rozpoznaje słowa primt. Jako istota ludzka możesz zignorować moją literówkę

23

24

Rozdział 1. Wprowadzenie. Program Koniec gry

i zrozumiesz, co miałem na myśli. Komputery nie są tak wyrozumiałe. Na szczęście z takimi błędami w programie (czyli pluskwami, ang. bugs) można sobie łatwo poradzić przez poprawienie nieprawidłowej litery.

Co to jest podświetlanie składni? Prawdopodobnie zauważyłeś, że słowa na ekranie są wyświetlane w różnych kolorach (nie dotyczy to oczywiście książki). To stosowanie kolorów, zwane podświetlaniem składni, pomaga szybko zrozumieć wprowadzoną treść poprzez jej wizualną kategoryzację. A w tym szaleństwie kolorowania jest metoda. Słowa specjalne, które są częścią języka Python, takie jak print, są wyświetlane na fioletowo. Łańcuchy znaków, takie jak "Koniec gry", mają kolor zielony, a dane wyjściowe — wszystko to, co interpreter wyświetla jako wynik tego, co wpisujesz — są wyróżnione kolorem niebieskim. Kiedy będziesz pisać większe programy, ten system kolorów okaże się bardzo przydatny, ułatwiając Ci ogarnięcie kodu jednym spojrzeniem i zauważenie ewentualnych błędów.

Programowanie w trybie skryptowym Korzystanie z trybu interaktywnego daje Ci natychmiastową informację zwrotną. Jest to wspaniałe, ponieważ od razu widzisz wyniki. Ale tryb interaktywny nie został zaprojektowany do tworzenia programów, które możesz zapisać na dysku i uruchomić później. Na szczęście IDLE Pythona oferuje także tryb skryptowy, w którym możesz tworzyć, edytować, ładować i zapisywać swoje programy. Jest to jakby procesor tekstu przystosowany do obsługi Twojego kodu. Prawdę mówiąc, możesz wykonywać tak znane czynności, jak „znajdź i zamień” czy „wytnij i wklej”.

Pisanie swojego pierwszego programu (ponownie) Możesz otworzyć okno trybu skryptowego z okna interaktywnego, którego używałeś do tej pory. Z menu File (plik) wybierz New Window (nowe okno). Ukaże się nowe okno, które wygląda dokładnie tak samo jak to na rysunku 1.5. W tym nowym oknie skryptowym wpisz tekst print("Koniec gry") i naciśnij klawisz Enter. Nic się nie dzieje! To dlatego, że jesteś w trybie skryptowym. To, co teraz robisz, jest wprowadzaniem listy instrukcji dla komputera, które mają zostać wykonane później. Po zapisaniu swojego programu na dysku możesz go uruchomić.

Zapisywanie i uruchamianie programu Aby zapisać program na dysku, wybierz z menu File (plik) opcję Save As (zapisz jako). Ja nadałem swojemu programowi nazwę koniec_gry.py. Aby później było można go łatwo znaleźć, zapisałem go na pulpicie.

Wprowadzenie do IDLE

Rysunek 1.5. Twoje puste okno czeka. Python jest gotowy — możesz zacząć pisanie programu w trybie skryptowym

Wskazówka Pamiętaj, aby zapisywać swoje programy z rozszerzeniem .py. To umożliwia różnym aplikacjom, nie wyłączając IDLE, rozpoznawanie tych plików jako programów w języku Python.

Aby uruchomić swój program Koniec gry, po prostu wybieram z menu Run (uruchom) opcję Run Module (uruchom moduł). Wtedy w oknie interaktywnym wyświetla się wynik programu. Popatrz na rysunek 1.6. Pewnie zauważysz, że okno interaktywne zawiera stary tekst, który pozostał po moich poprzednich działaniach. Nadal widoczna jest instrukcja, którą wprowadziłem w trybie interaktywnym, print("Koniec gry"), oraz jej wynik — komunikat Koniec gry. Poniżej tego wszystkiego widać komunikat RESTART, świadczący o ponownym uruchomieniu powłoki, a pod nim wynik uruchomienia mojego programu w trybie skryptowym: Koniec gry. Aby uruchomić swój program w IDLE, musisz najpierw go zapisać w postaci pliku na dysku.

25

26

Rozdział 1. Wprowadzenie. Program Koniec gry

Rysunek 1.6. Wynik uruchomienia programu Koniec gry w środowisku IDLE

Tryb interaktywny wspaniale się nadaje do szybkiego wypróbowania małego pomysłu. Tryb skryptowy sprawdza się doskonale przy pisaniu programów, które będzie można uruchomić później. Łączenie obydwu trybów to znakomity sposób kodowania. Chociaż wystarczy mi sam tryb skryptowy do napisania programu, to jednak zawsze pozostawiam otwarte okno interaktywne. W trakcie pisania swoich programów w trybie skryptowym przeskakuję do okna interaktywnego, aby wypróbować nowy pomysł lub upewnić się, że mój sposób użycia jakiejś funkcji jest prawidłowy. Okno skryptowe jest miejscem, gdzie dopracowuję swój produkt końcowy. Okno interaktywne odgrywa rolę szkicownika, w którym mogę eksperymentować. Łączne ich użycie pomaga mi pisać lepsze programy i robić to szybciej.

Powrót do programu Koniec gry Do tej pory uruchamiałeś pewną wersję programu Koniec gry, wykorzystując IDLE. Kiedy jesteś w trakcie pisania programu, uruchamianie go poprzez IDLE jest znakomitym sposobem postępowania. Ale jestem pewien, że chciałbyś, aby Twoje gotowe produkty funkcjonowały tak jak każdy inny program na Twoim komputerze. Chciałbyś, aby użytkownik mógł uruchamiać Twój program po prostu przez dwukrotne kliknięcie jego ikony.

Powrót do programu Koniec gry

Gdybyś spróbował uruchomić w ten sposób tę wersję programu Koniec gry, którą wcześniej pokazałem, zobaczyłbyś, jak okno się pojawia i równie szybko znika. Prawdopodobnie pomyślałbyś, że nic się nie wykonało. Ale coś jednak by się zdarzyło — za szybko jednak, abyś mógł to zauważyć. Program zostałby uruchomiony, tekst Koniec gry zostałby wyświetlony i program by się zakończył — wszystko w ułamku sekundy. To, czego brakuje temu programowi, to sposób na zachowanie otwartego okna konsoli. W tej poprawionej wersji programu Koniec gry — finalnym projekcie prezentowanym w tym rozdziale — okno pozostaje otwarte, tak aby użytkownik mógł zobaczyć komunikat. Po wyświetleniu tekstu Koniec gry program wyświetla także wskazówkę Aby zakończyć program, naciśnij klawisz Enter. Kiedy tylko użytkownik naciśnie klawisz Enter, program kończy pracę, a okno konsoli znika. Prześledzę z Tobą cały kod, fragment po fragmencie. Program ten możesz pobrać ze strony dedykowanej tej książce (http://www.helion.pl/ksiazki/pytdk3.htm), z folderu rozdziału 1.; nazwa pliku to game_over.py. Ale lepiej będzie, jeśli napiszesz ten program samodzielnie, a potem go uruchomisz.

Sztuczka W systemie operacyjnym Windows możesz bezpośrednio otworzyć program Pythona w IDLE przez kliknięcie ikony pliku prawym przyciskiem myszy i wybranie opcji Edit with IDLE (edytuj za pomocą IDLE).

Używanie komentarzy Dwa pierwsze wiersze programu wyglądają następująco: # Koniec gry # Przykład użycia funkcji print

Te dwa wiersze nie są instrukcjami, które komputer ma wykonać. Tak naprawdę to komputer całkowicie je zignoruje. Te uwagi, zwane komentarzami, są przeznaczone dla ludzi. Komentarze wyjaśniają kod programu w języku naturalnym (polskim, angielskim lub jakimś innym). Komentarze są bezcenne dla innych programistów, bo pomagają im zrozumieć Twój kod. Ale komentarze są pożyteczne także dla Ciebie. Przypominają Ci, w jaki sposób zrealizowałeś coś, co może nie jest oczywiste na pierwszy rzut oka. Do tworzenia komentarzy używa się symbolu kratki (#). Wszystko, co się znajduje po tym symbolu (z wyjątkiem sytuacji, gdy jest on elementem łańcucha znaków), aż do końca wiersza, jest komentarzem. Komentarze są ignorowane przez komputer. Zwróć uwagę, że komentarze są w IDLE wyróżnione kolorem czerwonym. Dobrym pomysłem jest rozpoczynanie wszystkich programów od kilku komentarzy, podobnie jak ja zrobiłem w swoim przykładzie. Warto umieścić w nich tytuł programu i jego cel. Chociaż ja tego tu nie zrobiłem, powinieneś także podać nazwisko programisty i datę utworzenia programu. Być może myślisz sobie: „Po co mi w ogóle komentarze? Skoro sam napisałem ten program, to wiem, co on robi”. To może być prawdą miesiąc po napisaniu Twojego

27

28

Rozdział 1. Wprowadzenie. Program Koniec gry

kodu, ale doświadczeni programiści wiedzą, że po upływie kilku miesięcy od utworzenia programu Twoje pierwotne intencje mogą już nie być takie jasne. Gdy chcesz zmodyfikować jakiś stary program, kilka dobrze umieszczonych komentarzy może bardzo ułatwić Ci życie.

W świecie rzeczywistym Komentarze są jeszcze użyteczniejsze dla innego programisty, który musi modyfikować napisany przez Ciebie program. Tego rodzaju sytuacje występują często w świecie profesjonalnego programowania. Tak naprawdę szacuje się, że większość czasu i wysiłku programisty jest zużywana na konserwację kodu, który już istnieje. Wcale nierzadko programista otrzymuje zadanie zmodyfikowania programu napisanego przez kogoś innego, a może się zdarzyć, że w pobliżu nie będzie twórcy oryginalnego kodu, który mógłby odpowiedzieć na ewentualne pytania. Tak więc dobre komentarze mają kluczowe znaczenie.

Używanie pustych wierszy Z technicznego punktu widzenia kolejny wiersz naszego programu jest pusty. Komputer na ogół ignoruje puste wiersze; w tym programie zostały także umieszczone tylko ze względu na ludzi, którzy będą czytać kod. Puste wiersze mogą ułatwić czytanie programu. Jeśli chodzi o mnie, zazwyczaj łączę wiersze zawierające wewnętrznie powiązany kod w sekcje, które oddzielam pustym wierszem. W omawianym programie pustym wierszem oddzieliłem komentarze od wywołania funkcji print.

Wypisywanie łańcucha znaków Kolejny wiersz programu powinien być Ci znajomy: print("Koniec gry")

To Twój stary przyjaciel — funkcja print. Ten wiersz, dokładnie tak samo jak w trybie interaktywnym, wyświetla komunikat Koniec gry.

Czekanie na reakcję użytkownika Ostatni wiersz programu: input("\n\nAby zakończyć program, naciśnij klawisz Enter.")

wyświetla podpowiedź Aby zakończyć program, naciśnij klawisz Enter i czeka, aż użytkownik naciśnie klawisz Enter. Kiedy użytkownik naciśnie ten klawisz, program się zakończy. Jest to niezły sposób na zachowanie otwartego okna konsoli, dopóki użytkownik nie zakończy pracy w aplikacji. Jak można by się spodziewać, nadszedł czas, abym wyjaśnił, co takiego dzieje się w tym wierszu. Ale zamierzam potrzymać Cię w niepewności. Przykro mi. Będziesz musiał poczekać do następnego rozdziału, aby w pełni docenić ten jeden wiersz programu.

Podsumowanie

Podsumowanie W tym rozdziale dowiedziałeś się wielu podstawowych rzeczy. Poznałeś nieco Pythona i jego silne strony. Zainstalowałeś ten język na swoim komputerze i wykonałeś w nim taką małą testową przejażdżkę. Nauczyłeś się wykorzystywać tryb interaktywny Pythona do natychmiastowego wykonywania instrukcji programu. Zobaczyłeś, jak należy używać trybu skryptowego do tworzenia, edytowania, zapisywania i uruchamiania dłuższych programów. Dowiedziałeś się, jak wyświetlać tekst na ekranie i jak czekać na decyzję użytkownika przed zamknięciem okna konsoli programu. Wykonałeś całą pracę u podstaw niezbędną do rozpoczęcia przygody z programowaniem w Pythonie.

Sprawdź swoje umiejętności 1. Wygeneruj swój oryginalny błąd poprzez wprowadzenie w trybie interaktywnym swojego ulubionego smaku lodów. Następnie, w charakterze rekompensaty za swój zły uczynek, wprowadź instrukcję, która wypisze nazwę Twoich ulubionych lodów. 2. Utwórz i zapisz program, który wyświetla Twoje imię i (zanim zakończy swoje działanie) czeka, aż użytkownik naciśnie klawisz Enter. Następnie uruchom ten program przez dwukrotne kliknięcie jego ikony. 3. Napisz program, który wyświetla Twój ulubiony cytat. W kolejnym wierszu powinieneś podać nazwisko autora cytowanej wypowiedzi.

29

30

Rozdział 1. Wprowadzenie. Program Koniec gry

2 Typy, zmienne i proste operacje wejścia-wyjścia. Program Nieistotne fakty T

eraz, kiedy już zapoznałeś się z podstawami zapisywania i wykonywania programu, pora umocnić swoje pozycje i stworzyć coś więcej. W tym rozdziale poznasz różne sposoby klasyfikowania i przechowywania danych przez komputery i, co ważniejsze, dowiesz się, jak te dane wykorzystywać w swoich programach. Zobaczysz nawet, jak uzyskiwać informacje od użytkownika, aby Twoje programy stały się interaktywne. W szczególności dowiesz się, jak:  używać łańcuchów w potrójnych cudzysłowach i sekwencji specjalnych

w celu uzyskania większej kontroli nad tekstem,  sprawić, aby Twoje programy wykonywały operacje matematyczne,  przechowywać dane w pamięci komputera,  używać zmiennych w celu uzyskiwania dostępu do tych danych

i manipulowania nimi,  uzyskiwać dane wejściowe od użytkowników i tworzyć programy interaktywne.

Wprowadzenie do programu Nieistotne fakty Poprzez połączenie umiejętności zaprezentowanych w tym rozdziale utworzysz program Nieistotne fakty, którego działanie pokazano na rysunku 2.1.

32

Rozdział 2. Typy, zmienne i proste operacje wejścia-wyjścia. Program Nieistotne fakty

Rysunek 2.1. Ojej! Karol mógłby pomyśleć o diecie, zanim odwiedzi Słońce

Program pobiera trzy osobiste informacje od użytkownika: imię, wiek i wagę. Z tych prozaicznych danych program potrafi wyprodukować kilka zabawnych choć trywialnych faktów dotyczących tej osoby — na przykład, ile by ważyła na Księżycu. Chociaż może wydawać się, że jest to prosty program (i taki jest w istocie), bardziej Cię zainteresuje, kiedy sam go uruchomisz, ponieważ Ty podajesz dane wejściowe. Zwrócisz większą uwagę na wyniki, ponieważ zostaną dopasowane do Twojej osoby. Ta prawda odnosi się do wszystkich programów — od gier po aplikacje biznesowe.

Użycie cudzysłowów przy tworzeniu łańcuchów znaków Przykład łańcucha znaków, "Koniec gry", napotkałeś w poprzednim rozdziale. Ale łańcuchy mogą być o wiele dłuższe i bardziej skomplikowane. Być może będziesz chciał przekazać użytkownikowi kilka akapitów instrukcji. Albo mógłbyś chcieć sformatować swój tekst w bardzo specyficzny sposób. Odpowiednie użycie cudzysłowów może pomóc Ci w utworzeniu łańcuchów spełniających te wszystkie wymagania.

Prezentacja programu Koniec gry 2.0 Program Koniec gry 2.0 stanowi ulepszenie swojego poprzednika, programu Koniec gry, poprzez wyświetlenie atrakcyjniejszej wersji tego samego komunikatu, który informuje gracza, że jego (lub jej) gra komputerowa dobiegła końca. Sprawdź na rysunku 2.2, jak wygląda przykładowe uruchomienie programu.

Użycie cudzysłowów przy tworzeniu łańcuchów znaków

Rysunek 2.2. Teraz zrozumiałem — gra skończona

Ten program pokazuje, że prezentowanie tekstu na różne sposoby poprzez użycie cudzysłowów jest dość proste. Jego kod znajdziesz na stronie poświęconej książce (http:// www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 2.; nazwa pliku to koniec_gry2.py. # Koniec gry - wersja 2 # Demonstruje użycie cudzysłowów w łańcuchach znaków print("Program 'Koniec gry' 2.0") print("Taki sam", "komunikat", "jak przedtem,") print("tylko", "nieco", "większy.") print("Oto", end=" ") print("on...") print( """ _ __ | |/ / | ' / | < | . \ |_|\_\

____ / __ \ | | | | | | | | | |__| | \____/

_____ / ____| | | __ | | |_ | | |__| | \_____|

_ _ _____ | \ | | |_ _| | \| | | | | . ` | | | | |\ | _| |_ |_| \_| |_____|

______ _____ | ____| / ____| | |__ | | | __| | | | |____ | |____ |______| \_____|

_____ __ __ | __ \ \ \ / / | |__) | \ \_/ / | _ / \ / | | \ \ | | |_| \_\ |_|

""" ) input("\n\nAby zakończyć program, naciśnij klawisz Enter.")

33

34

Rozdział 2. Typy, zmienne i proste operacje wejścia-wyjścia. Program Nieistotne fakty

Używanie cudzysłowów wewnątrz łańcuchów Widziałeś, jak tworzy się proste łańcuchy poprzez ujęcie tekstu w cudzysłów. Do utworzenia wartości łańcuchowej możesz użyć pojedynczego (' ') albo podwójnego cudzysłowu (""). Komputerowi jest wszystko jedno. Więc zapis 'Koniec gry' reprezentuje taki sam łańcuch jak "Koniec gry". Ale spójrz na pierwsze wystąpienia łańcucha w programie: print("Program 'Koniec gry' 2.0")

W tej instrukcji występują obydwa rodzaje cudzysłowu. Popatrz jeszcze raz na rysunek 2.2. Widoczne są tylko znaki pojedynczego cudzysłowu, ponieważ stanowią one część łańcucha, tak jak na przykład litera K. Lecz znaki podwójnego cudzysłowu nie są częścią łańcucha. Odgrywają one rolę zewnętrznych ograniczników, które informują komputer, gdzie łańcuch się zaczyna i gdzie się kończy. Więc jeśli użyjesz podwójnego cudzysłowu do wyznaczenia granic swojego łańcucha, wewnątrz niego możesz użyć tyle pojedynczych cudzysłowów, ile tylko chcesz. I na odwrót, jeśli zamkniesz swój łańcuch w pojedynczym cudzysłowie, możesz wewnątrz tego łańcucha użyć dowolnej liczby podwójnych cudzysłowów. Gdy już użyjesz określonego rodzaju cudzysłowu jako ograniczników swojego łańcucha, nie możesz stosować cudzysłowów tego samego typu wewnątrz tego łańcucha. To ma swój sens, ponieważ kiedy komputer zobaczy drugie wystąpienie znaku rozpoczynającego cudzysłów, będzie sądził, że łańcuch się właśnie zakończył. Na przykład tekst "Dzięki słowom 'Houston, mamy problem.' Jim Lovell stał się jednym z najsławniejszych amerykańskich astronautów." jest prawidłowym łańcuchem. Lecz zapis "Dzięki słowom "Houston, mamy problem." Jim Lovell stał się jednym z najsławniejszych amerykańskich astronautów." nie jest prawidłowy, ponieważ komputer potraktuje

drugie wystąpienie znaku podwójnego cudzysłowu jako koniec łańcucha. Tak więc komputer widzi łańcuch "Dzięki słowom ", po którym pojawia się słowo Houston. A ponieważ komputer nie ma pojęcia, co oznacza Houston, będziesz miał w swoim programie paskudny błąd.

Wypisywanie wielu wartości Możesz wypisać wiele wartości za pomocą pojedynczego wywołania funkcji print() — wystarczy, że w nawiasach podasz listę wartości argumentów oddzielonych przecinkami. Ja wypisuję wiele wartości w wierszu print("Taki sam", "komunikat", "jak przedtem,")

Przekazuję do funkcji trzy argumenty: "Taki sam", "komunikat" i "jak przedtem,", co skutkuje wyświetleniem przez kod tekstu Taki sam komunikat jak przedtem,. Zwróć uwagę, że każda wartość jest wypisywana ze spacją w roli separatora. Jest to domyślne zachowanie funkcji print(). Kiedy masz do czynienia z listą argumentów, po każdym przecinku oddzielającym elementy listy możesz rozpocząć nowy wiersz. Kolejne trzy wiersze programu tworzą

Użycie cudzysłowów przy tworzeniu łańcuchów znaków

pojedynczą instrukcję, która wypisuje jeden wiersz tekstu tylko nieco większy. Rozpoczynam nowy wiersz po każdym separatorze w postaci przecinka: print("tylko", "nieco", "większy.")

Czasem przydaje się rozbicie listy argumentów na wiele wierszy, ponieważ może ono zwiększyć czytelność kodu.

Definiowanie łańcucha końcowego funkcji print Domyślnie funkcja print() wypisuje jako wartość końcową znak nowego wiersza. To oznacza, że kolejne wywołanie funkcji print() wyświetliłoby tekst w następnym wierszu. Na ogół jest to zgodne z Twoim oczekiwaniem, niemniej jednak masz możliwość zdefiniowania swojego własnego łańcucha, który zostanie wypisany na końcu tekstu. Możesz na przykład zdefiniować go tak, że kiedy wywołasz funkcję print(), ostatnim wypisanym znakiem będzie spacja (zamiast znaku nowego wiersza). To by oznaczało, że kolejna instrukcja print() rozpoczęłaby wypisywanie wartości bezpośrednio po tej spacji. Korzystam z tej funkcjonalności w następnych dwóch wierszach programu: print("Oto", end=" ") print("on...")

Ten kod wypisuje tekst „Oto on...” w jednym wierszu. Dzieje się tak dlatego, że w pierwszej instrukcji print() zdefiniowałem spację jako ostatni łańcuch do wypisania. Tak więc instrukcja wypisuje tekst „Oto ” (łącznie ze spacją po ostatnim „o”), ale wyprowadza znak nowego wiersza. Następna instrukcja print() rozpoczyna wypisywanie tekstu „on...” bezpośrednio po spacji, która pojawia się po ostatnim „o” w tekście „Oto”. Efekt ten uzyskuję przez zdefiniowanie spacji jako wartości parametru end funkcji print() za pomocą kodu end=" ". W swojej własnej instrukcji print() możesz zdefiniować łańcuch, który ma być wypisany jako ostatnia wartość, dokładnie tak, jak ja to zrobiłem, dodając przecinek, po nim nazwę parametru end, znak równości i sam łańcuch. Możliwość zdefiniowania swojego własnego łańcucha, który ma być wypisany przez instrukcję print() na końcu, daje Ci większą elastyczność w sposobie formatowania Twoich danych wyjściowych.

Wskazówka Nie martw się, jeśli jeszcze nie wiesz, co to jest parametr. Wszystkiego o parametrach i przekazywaniu ich wartości dowiesz się w rozdziale 6., w podrozdziale „Używanie parametrów i wartości zwrotnych”.

Tworzenie łańcuchów w potrójnych cudzysłowach Z pewnością najbardziej bajerancką częścią programu jest wypisywanie tekstu „Koniec gry” w postaci wielkich liter. Odpowiada za to następujący łańcuch:

35

36

Rozdział 2. Typy, zmienne i proste operacje wejścia-wyjścia. Program Nieistotne fakty """ _ __ | |/ / | ' / | < | . \ |_|\_\

____ / __ \ | | | | | | | | | |__| | \____/

_____ / ____| | | __ | | |_ | | |__| | \_____|

_ _ _____ | \ | | |_ _| | \| | | | | . ` | | | | |\ | _| |_ |_| \_| |_____|

______ | ____| | |__ | __| | |____ |______|

_____ / ____| | | | | | |____ \_____|

_____ __ __ | __ \ \ \ / / | |__) | \ \_/ / | _ / \ / | | \ \ | | |_| \_\ |_|

"""

Widzimy coś, co nazywa się łańcuchem w potrójnym cudzysłowie. Jest to łańcuch ujęty w parę ograniczników złożonych z trzech kolejnych znaków cudzysłowu. Tak jak przedtem, nie ma znaczenia, jakiego rodzaju cudzysłowów użyjesz, o ile każdy z trzech cudzysłowów będzie tego samego typu. Jak możesz zauważyć, łańcuchy w potrójnych cudzysłowach mogą obejmować wiele wierszy. Są wyświetlane w dokładnie tej postaci, w jakiej je wpisałeś w programie.

W świecie rzeczywistym Jeśli podobają Ci się litery utworzone z wielu znaków, które wystąpiły w programie Koniec gry 2.0, to z pewnością Ci się spodoba dziedzina sztuki o nazwie ASCII-Art. Są to zasadniczo rysunki składające się z samych tylko znaków klawiatury. Przy okazji wyjaśnię, że ASCII jest akronimem utworzonym od nazwy American Standard Code for Information Interchange (amerykański standardowy kod do wymiany informacji). Jest to kod, który zawiera 128 standardowych znaków. (Rodzaj sztuki reprezentowany przez ASCII-Art nie jest nowy i nie narodził się wraz z komputerem. W rzeczywistości pierwsze rysunki wykonane na maszynie do pisania datuje się na 1898 r.).

Używanie sekwencji specjalnych w łańcuchach znaków Sekwencje specjalne umożliwiają umieszczanie w łańcuchach znaków o szczególnym charakterze. Dają większą kontrolę nad wyświetlanym tekstem i elastyczność w jego tworzeniu. Sekwencje specjalne, które będziesz wykorzystywał, składają się ze znaku poprzedzonego lewym ukośnikiem. Wszystko to może wydawać się nieco tajemnicze, ale kiedy już zobaczysz kilka sekwencji specjalnych w działaniu, przekonasz się, jak łatwo ich używać.

Używanie sekwencji specjalnych w łańcuchach znaków

Prezentacja programu Zabawne podziękowania Oprócz poinformowania gracza, że gra została zakończona, program często wyświetla podziękowania, wymieniając wszystkie osoby, które tak ciężko pracowały, aby projekt stał się rzeczywistością. Program Zabawne podziękowania wykorzystuje sekwencje specjalne w celu osiągnięcia pewnych efektów, które byłyby niemożliwe do uzyskania w inny sposób. Wynik działania programu pokazuje rysunek 2.3.

Rysunek 2.3. Proszę ograniczyć aplauz

Kod wygląda na pierwszy rzut oka trochę zagadkowo, ale wkrótce zrozumiesz go w całości. Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 2.; nazwa pliku to zabawne_podziekowania.py. # Zabawne podziękowania # Demonstruje sekwencje specjalne print("\t\t\tZabawne podziękowania") print("\t\t\t \\ \\ \\ \\ \\ \\ \\ \\ \\ \\") print("\t\t\t napisał") print("\t\t\t Michael Dawson") print("\t\t\t \\ \\ \\ \\ \\ \\ \\") print("\nSpecjalne podziękowania należą się:") print("mojemu fryzjerowi,", end=" ") print("Henry\'emu \'Wielkiemu\', który nigdy nie mówi \"nie da się\".") # dzwonek systemowy print("\a") input("\n\nAby zakończyć program, naciśnij klawisz Enter.")

37

38

Rozdział 2. Typy, zmienne i proste operacje wejścia-wyjścia. Program Nieistotne fakty

Przesuwanie kursora tekstowego w prawo do najbliższego punktu tabulacji Czasem chcesz odsunąć jakiś tekst od lewego marginesu, od którego standardowo zaczyna się wypisywanie. W procesorze tekstu mógłbyś użyć klawisza Tab. W przypadku łańcuchów znaków możesz wykorzystać sekwencję specjalną zastępującą znak tabulacji \t. Dokładnie to samo zrobiłem w wierszu poniżej: print("\t\t\tZabawne podziękowania")

Użyłem sekwencji specjalnej \t trzy razy z rzędu. Więc kiedy program wyświetla łańcuch, wyprowadza trzy znaki tabulacji, a potem tekst Zabawne podziękowania. To sprawia, że tekst wygląda, jakby został wyświetlony niemal w środku okna konsoli. Sekwencje tabulacji dobrze się nadają do odsuwania tekstu od lewego marginesu, jak w tym programie, lecz są także doskonałym środkiem do ustawiania tekstu w kolumny.

Wypisywanie znaku lewego ukośnika Jeśli wybiegałeś myślą w przód, być może zastanawiałeś się, w jaki sposób wyświetlić lewy ukośnik, skoro komputer zawsze interpretuje go jako początek sekwencji specjalnej. Cóż, rozwiązanie jest dość proste — wystarczy wpisać dwa lewe ukośniki, jeden po drugim. Każdy z poniższych wierszy wypisuje trzy znaki tabulacji oraz pewną liczbę lewych ukośników (jako wynik zastosowania odpowiedniej liczby sekwencji \\) oddzielonych spacjami: print("\t\t\t \\ \\ \\ \\ \\ \\ \\ \\ \\ \\") print("\t\t\t \\ \\ \\ \\ \\ \\ \\")

Wstawianie znaku nowego wiersza Jedną z najprzydatniejszych sekwencji, jakie masz do dyspozycji, jest sekwencja nowego wiersza. Ma ona postać \n. Dzięki użyciu tej sekwencji możesz wstawiać w swoich łańcuchach znak nowego wiersza, mając na celu wyprowadzenie pustego wiersza w miejscach, gdzie to jest potrzebne. Możesz umieścić sekwencję nowego wiersza na samym początku łańcucha, aby oddzielić go od tekstu wypisanego poprzednio. To właśnie zrobiłem w wierszu: print("\nSpecjalne podziękowania należą się:")

Komputer po napotkaniu sekwencji \n wyprowadza pusty wiersz, a potem wypisuje tekst Specjalne podziękowania należą się:.

Wstawianie znaku cudzysłowu Wstawienie znaku cudzysłowu do łańcucha — nawet cudzysłowu tego samego typu co cudzysłów wyznaczający granice tego łańcucha — jest proste. Wystarczy użyć sekwencji \' w przypadku znaku pojedynczego cudzysłowu oraz \" w przypadku znaku podwójnego

Używanie sekwencji specjalnych w łańcuchach znaków

cudzysłowu. Sekwencje te znaczą „wstaw w tym miejscu znak cudzysłowu” i żadna z nich nie zostanie błędnie potraktowana przez komputer jako znacznik końca Twojego łańcucha. Właśnie tego sposobu użyłem, aby umieścić obydwa rodzaje cudzysłowów w jednym wierszu tekstu: print("Henry\'emu \'Wielkiemu\', który nigdy nie mówi \"nie da się\".")

Pierwszy i ostatni znak podwójnego cudzysłowu stanowią zewnętrzne ramy ograniczające łańcuch. Aby łatwiej zrozumieć zawartość łańcucha, przypatrzmy się jego poszczególnym częściom:  fragment \'Wielkiemu\' jest wyświetlany w postaci 'Wielkiemu';  każda sekwencja \' powoduje wyświetlenie znaku pojedynczego cudzysłowu;  fragment \"nie da się\" przyjmuje na wyjściu postać "nie da się";  obie sekwencje \" są wyświetlane jako znaki podwójnego cudzysłowu;  fragment Henry\'ego przyjmuje postać Henry'ego;  samotna sekwencja \' jest wyświetlana jako znak apostrofu.

Wywołanie sygnału dzwonka systemowego Uruchamiając ten program, od razu zauważysz coś nowego. Usłyszysz krótki dźwięk! Następna instrukcja w programie: print("\a")

wywołuje sygnał dzwonka systemowego Twojego komputera. W tym celu wykorzystuje sekwencję specjalną \a, która reprezentuje znak alarmu. Ile razy ją wypiszesz, tyle razy zadźwięczy dzwonek. Możesz użyć łańcucha zawierającego tylko tę sekwencję, tak jak ja zrobiłem, lub też umieścić ją wewnątrz dłuższego łańcucha. Możesz nawet użyć tej sekwencji wiele razy, aby sygnał dzwonka zabrzmiał wielokrotnie. Niektóre z sekwencji specjalnych działają zgodnie z założeniem tylko wtedy, gdy uruchamiasz swój program bezpośrednio z systemu operacyjnego, a nie poprzez IDLE. Dobrym przykładem jest tu sekwencja \a. Powiedzmy, że mam program, który po prostu wypisuje sekwencję specjalną \a. Jeśli uruchamiam go poprzez IDLE, na ekranie wyświetla mi się mały kwadratowy znaczek — to nie to, czego oczekiwałem. Ale jeśli uruchamiam ten sam program bezpośrednio z systemu Windows, poprzez podwójne kliknięcie ikony programu, zgodnie z moim zamierzeniem odzywa się dzwonek systemowy mojego komputera. Sekwencje specjalne okazują się nie takie złe, kiedy zobaczy się je w działaniu. A mogą być całkiem przydatne. W tabeli 2.1 znajduje się podsumowanie kilku najbardziej użytecznych.

39

40

Rozdział 2. Typy, zmienne i proste operacje wejścia-wyjścia. Program Nieistotne fakty Tabela 2.1. Wybrane sekwencje specjalne

Sekwencja

Opis

\\

Lewy ukośnik. Powoduje wyświetlenie jednego lewego ukośnika.

\'

Pojedynczy cudzysłów. Powoduje wyświetlenie znaku pojedynczego cudzysłowu.

\'

Podwójny cudzysłów. Powoduje wyświetlenie znaku podwójnego cudzysłowu.

\a

Alarm. Wywołuje sygnał dzwonka systemowego.

\n

Nowy wiersz. Przenosi kursor na początek następnego wiersza.

\t

Tabulator poziomy. Przesuwa kursor w prawo do najbliższego punktu tabulacji.

Konkatenacja i powielanie łańcuchów Dowiedziałeś się, jak można wstawiać do łańcucha znaki specjalne, ale są takie rzeczy, które możesz robić z całymi łańcuchami. Możesz na przykład połączyć dwa oddzielne łańcuch w jeden większy. A nawet możesz powielać pojedynczy łańcuch tyle razy, ile Ci się podoba.

Prezentacja programu Głupie łańcuchy Program Głupie łańcuchy wyświetla na ekranie szereg łańcuchów. Wyniki zostały pokazane na rysunku 2.4.

Rysunek 2.4. Łańcuchy na ekranie wyglądają inaczej niż w kodzie programu

Chociaż już zobaczyłeś łańcuchy wyświetlone na ekranie komputera, sposób ich utworzenia będzie dla Ciebie całkowitą nowością. Kod tego programu znajdziesz na

Konkatenacja i powielanie łańcuchów

stronie internetowej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 2.; nazwa pliku to glupie_lancuchy.py. # Głupie łańcuchy # Demonstruje konkatenację i powielanie łańcuchów print("Możesz dokonać konkatenacji dwóch " + "łańcuchów za pomocą operatora '+'.") print("\nTen łańcuch " + "może nie " + "sprawiać wiel" + "kiego wrażenia. " \ + "Ale " + "pewnie nie wiesz," + " że jest\n" + "to jeden napraw" \ + "d" + "ę" + " długi łańcuch, utworzony przez konkatenację " \ + "aż " + "dwudziestu dwu\n" + "różnych łańcuchów i rozbity na " \ + "sześć wierszy." + " Jesteś pod" + " wrażeniem tego faktu?\n" \ + "Dobrze, ten " + "jeden " + "długi" + " łańcuch właśnie się skończył!") print("\nJeśli jakiś łańcuch naprawdę Ci się podoba, możesz go powtórzyć.") print("Kto na przykład nie lubi lodów? Masz rację, nikt. Ale jeśli naprawdę ") print("je lubisz, powinieneś to wyrazić w adekwatny sposób:") print("Lody!" * 10) input("\n\nAby zakończyć program, naciśnij klawisz Enter.")

Konkatenacja łańcuchów Konkatenacja łańcuchów oznacza ich połączenie w celu utworzenia jednego nowego łańcucha. Prostego przykładu dostarcza pierwsza instrukcja print: print("Możesz dokonać konkatenacji dwóch " + "łańcuchów za pomocą operatora '+'.")

Operator + łączy dwa łańcuchy "Możesz dokonać konkatenacji dwóch " i "łańcuchów za pomocą operatora '+'." w jedną całość, tworząc nowy, dłuższy łańcuch. Jest to dość intuicyjna operacja. Jest to jakby dodawanie łańcuchów przy użyciu takiego samego symbolu, z jakiego się zawsze korzysta przy dodawaniu liczb. Kiedy łączy się dwa łańcuchy, ich właściwe wartości zostają ze sobą zespolone bez wstawiania między nie odstępu czy też innego separatora. Więc jeśli połączysz dwa łańcuchy "dobra" i "noc", otrzymasz "dobranoc", a nie "dobra noc". W większości przypadków będziesz chciał, aby łączone łańcuchy oddzielała spacja, więc nie zapomnij o jej wstawieniu. Kolejna instrukcja print pokazuje, że możesz łączyć łańcuchy bez żadnych ograniczeń: print("\nTen łańcuch " + "może nie " + "sprawiać wiel" + "kiego wrażenia. " \ + "Ale " + "pewnie nie wiesz," + " że jest\n" + "to jeden napraw" \ + "d" + "ę" + " długi łańcuch, utworzony przez konkatenację " \ + "aż " + "dwudziestu dwu\n" + "różnych łańcuchów i rozbity na " \ + "sześć wierszy." + " Jesteś pod" + " wrażeniem tego faktu?\n" \ + "Dobrze, ten " + "jeden " + "długi" + " łańcuch właśnie się skończył!")

Komputer wyświetla jeden długi łańcuch, który został utworzony przez konkatenację 22 osobnych łańcuchów.

41

42

Rozdział 2. Typy, zmienne i proste operacje wejścia-wyjścia. Program Nieistotne fakty

Używanie znaku kontynuacji wiersza Na ogół umieszczasz jedną instrukcję w każdym wierszu kodu. Ale nie jest to konieczne. Możesz rozciągnąć pojedynczą instrukcję na kilka wierszy. Wszystko, co musisz zrobić, to użyć znaku kontynuacji wiersza \ (który jest właśnie lewym ukośnikiem), tak jak ja zrobiłem w powyższym kodzie. Możesz wstawić go wszędzie, gdzie normalnie zastosowałbyś odstęp (choć nie wewnątrz łańcucha), by kontynuować pisanie instrukcji w następnym wierszu. Komputer będzie działał tak, jakby to był jeden długi wiersz kodu. Z punktu widzenia komputera długość wiersza programu jest nieistotna, ale jest ważna dla ludzi. Jeśli jakiś wiersz kodu wydaje Ci się zbyt długi lub uważasz, że byłby bardziej czytelny w postaci kilku wierszy, użyj znaku \, aby go podzielić na części.

Powielanie łańcuchów Kolejna nowa koncepcja zaprezentowana w omawianym programie została zilustrowana w poniższym wierszu: print("Lody!" * 10

W tym wierszu tworzony jest nowy łańcuch, "Lody!Lody!Lody!Lody!Lody!Lody! Lody!Lody!Lody!Lody!", który zostaje wyświetlony na ekranie. Notabene jest to łańcuch "Lody!" powtórzony 10 razy. Podobnie jak operator konkatenacji, operator powielania (*) jest dość intuicyjny. Jest to taki sam symbol, jaki jest używany do mnożenia liczb w komputerze, więc zastosowanie go do powielania łańcucha ma swój sens. To tak, jakbyś mnożył łańcuch. Aby powielić łańcuch, wystarczy umieścić między nim a liczbą powtórzeń operator *.

Operacje na liczbach Do tej pory używałeś łańcuchów do reprezentowania tekstu. To tylko jeden typ wartości. Komputery pozwalają na przedstawianie informacji także na inne sposoby. Jedną z najbardziej podstawowych i zarazem najważniejszych form informacji są liczby. Liczby są wykorzystywane w prawie każdym programie. Czy piszesz grę, np. kosmiczną strzelankę, czy pakiet do zarządzania domowymi finansami, musisz dysponować jakimś sposobem reprezentowania liczb. Jakby nie było, musisz się zajmować obsługą listy najlepszych wyników lub sprawdzaniem sald rachunków. Na szczęście Python oferuje kilka różnych typów liczb, które mogą zaspokoić potrzeby związane z programowaniem gier lub innych aplikacji.

Prezentacja programu Zadania tekstowe Kolejny program wykorzystuje te okropne zadania tekstowe. Jak się domyślasz, chodzi o ten ich rodzaj, który zdaje się zawsze dotyczyć dwóch pociągów wyruszających z różnych miast w tym samym czasie, które jadą naprzeciw siebie … i budzi na nowo koszmar

Operacje na liczbach

z gimnazjalnej algebry — pociągi niechybnie się zderzą. Nie obawiaj się jednak. Nie będziesz musiał rozwiązywać ani jednego zadania tekstowego, ani nawet wykonywać jakichkolwiek matematycznych obliczeń — całą pracę wykona komputer. Program Zadania tekstowe jest tylko zabawnym (mam nadzieję) sposobem eksploracji działań na liczbach. Sprawdź na rysunku 2.5, jak wygląda jego przykładowe uruchomienie.

Rysunek 2.5. W Pythonie możesz dodawać, odejmować, mnożyć, dzielić oraz prowadzić rejestr wagi ciężarnych hipopotamic

Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 2.; nazwa pliku to zadania_tekstowe.py. # Zadania tekstowe # Liczby i działania matematyczne print("Ciężarna hipopotamica ważąca print("ale potem zjada 25 kg paszy. input("Aby się dowiedzieć, naciśnij print("1500 - 45 + 25 =", 1500 - 45

1500 kg rodzi 45-kilogramowe młode,") Ile wynosi jej nowa waga?") klawisz Enter.") + 25)

print("Poszukiwacz przygód wraca z udanej wyprawy i kupuje każdemu ze swoich") print("6 towarzyszy 3 butelki piwa. Ile butelek zostało zakupionych?") input("Aby się dowiedzieć, naciśnij klawisz Enter.") print("6 * 3 =", 6 * 3) print("Należność za obiad w restauracji wynosi razem z napiwkiem 159 zł, a Ty") print("postanawiasz ze swoimi przyjaciółmi podzielić ją na 4 równe części. Ile") print("każde z Was będzie musiało zapłacić?") input("Aby się dowiedzieć, naciśnij klawisz Enter.") print("159 / 4 =", 159 / 4)

43

44

Rozdział 2. Typy, zmienne i proste operacje wejścia-wyjścia. Program Nieistotne fakty print("\nGrupa 4 piratów znajduje skrzynię, a w niej 107 złotych monet, i") print("postanawia podzielić zdobycz po równo. Ile monet otrzyma każdy z nich?") input("Aby się dowiedzieć, naciśnij klawisz Enter.") print("107 // 4 =", 107 // 4) print("\nTa sama grupa 4 piratów dzieli między siebie po równo 107 złotych") print("monet ze znalezionej skrzyni. Ile monet zostanie po podziale?") input("Aby się dowiedzieć, naciśnij klawisz Enter.") print("107 % 4 =", 107 % 4) input("\n\nAby zakończyć program, naciśnij klawisz Enter.")

Typy liczbowe W programie Zadania tekstowe używane są liczby. To oczywiste. Ale mniej oczywisty może być fakt, że w programie wykorzystano dwa różne typy liczb. Python umożliwia programistom wykorzystywanie kilku różnych typów liczb. Dwa typy używane w tym programie, prawdopodobnie występujące najczęściej, to liczby całkowite (ang. integers) oraz liczby zmiennoprzecinkowe (ang. floating-point numbers, floats). Liczby całkowite to liczby bez części ułamkowej. Można je opisać inaczej jako liczby, które można zapisać bez kropki dziesiętnej. Przykładowe liczby całkowite to 1, 27, -100 i 0. Liczby zmiennoprzecinkowe zawierają kropkę dziesiętną; ich przykładami są 2.376, -99.1 i 1.0.

Stosowanie operatorów matematycznych Dzięki operatorom matematycznym możesz przekształcić swój komputer w kosztowny kalkulator. Operatory te powinny wyglądać całkiem znajomo. Na przykład kod 1500 - 45 + 25 oznacza odjęcie wartości 45 od 1500, a potem dodanie liczby 25, zanim zostanie wyświetlony wynik 1480. Używając języka technicznego, powiemy, że obliczana jest wartość wyrażenia 1500 - 45 + 25, która wynosi 1480. Wyrażenie to nic innego jak ciąg wartości połączonych operatorami, który można uprościć do innej wartości. Kod 6 * 3 powoduje pomnożenie liczby 6 przez 3 i wyświetlenie wyniku 18. A kod 159 / 4 oznacza podzielenie liczby 159 przez 4 i wypisanie wyniku 39.75 w postaci liczby zmiennoprzecinkowej. Wszystkie omówione do tej pory operatory matematyczne są zapewne dobrze Ci znane — ale popatrz na kolejne obliczenie, 107 // 4. Użycie // jako operatora matematycznego jest dla Ciebie prawdopodobnie czymś nowym. Występujące w wyrażeniu dwa ukośniki (//) reprezentują dzielenie całkowite, w którym wynik jest zawsze liczbą całkowitą (ewentualna część ułamkowa jest ignorowana). Różni się ono od dzielenia zmiennoprzecinkowego z operatorem /, którego przykład poznałeś w poprzednim akapicie (przy którym część ułamkowa wyniku nie jest ignorowana). Tak więc wynik wyrażenia 107 // 4 to 26. Następne obliczenie, 107 % 4, może również sprawić, że podrapiesz się po głowie. Użyty w nim symbol % to operator modulo, który wyznacza resztę z dzielenia całkowitego. Tak więc wartością wyrażenia 107 % 4 jest liczba 3, czyli część ułamkowa wyniku dzielenia 107 / 4 pomnożona przez 4. Tabela 2.2 podsumowuje niektóre użyteczne operatory matematyczne.

Pojęcie zmiennych Tabela 2.2. Przydatne operatory matematyczne

Operator

Opis

Przykład użycia

Wartość wyrażenia

+

Dodawanie

7 + 3

10

-

Odejmowanie

7 - 3

4

*

Mnożenie

7 * 3

21

/

Dzielenie (zmiennoprzecinkowe)

7 / 3

2.3333333333333335

//

Dzielenie (całkowite)

7 // 3

2

%

Modulo

7 % 3

1

Zwróć uwagę na pozycję w tabeli 2.2 odnoszącą się do dzielenia zmiennoprzecinkowego. Wynika z niej, że 7 podzielone przez 2 równa się 2.3333333333333335. Choć wynik został obliczony z dość dobrym przybliżeniem, to nie jest jednak całkowicie dokładny. Trzeba o tym pamiętać, kiedy używa się liczb zmiennoprzecinkowych, chociaż takie przybliżenia są zupełnie wystarczające w większości zastosowań.

Wskazówka Moduł decimal zapewnia obsługę dokładnej dziesiętnej arytmetyki zmiennoprzecinkowej. Aby dowiedzieć się więcej, zajrzyj do dokumentacji Pythona.

Pojęcie zmiennych Dzięki zmiennym, które stanowią fundamentalny aspekt programowania, możesz przechowywać informacje oraz nimi manipulować. Python pozwala na tworzenie zmiennych w celu organizowania informacji i uzyskiwania do nich dostępu.

Prezentacja programu Pozdrawiacz Sprawdź na rysunku 2.6, jak wyglądają wyniki programu Pozdrawiacz. Na zrzucie ekranu przedstawionym na rysunku 2.6 ten program wygląda jak coś, co już kiedyś pisałeś. Ale wewnątrz kodu czai się nowa i potężna koncepcja zmiennych. Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 2.; nazwa pliku to pozdrawiacz.py. # Pozdrawiacz # Demonstruje użycie zmiennej name = "Ludwik" print(name) print("Cześć,", name) input("\n\nAby zakończyć program, naciśnij klawisz Enter.")

45

46

Rozdział 2. Typy, zmienne i proste operacje wejścia-wyjścia. Program Nieistotne fakty

Rysunek 2.6. Pozdrowienia dla wszystkich Ludwików na świecie

Tworzenie zmiennych Zmienna stanowi sposób na przypisanie informacji nazwy i przez to umożliwienie dostępu do niej. Nie musisz dokładnie wiedzieć, gdzie w pamięci komputera jakaś informacja jest przechowywana, albowiem możesz się do niej dostać dzięki użyciu zmiennej. Jest to tak, jakbyś dzwonił do swojego przyjaciela, wybierając numer jego telefonu komórkowego. Nie musisz wiedzieć, w jakim miejscu miasta przebywa Twój przyjaciel, aby się z nim skontaktować. Wystarczy, że naciśniesz przycisk, i już go masz. Lecz zanim użyjesz zmiennej, musisz ją utworzyć, tak jak w wierszu poniżej: name = "Ludwik"

Ten wiersz zawiera instrukcję przypisania. Tworzy ona zmienną o nazwie name i przypisuje jej wartość — poprzez tę zmienną odwołujemy się do łańcucha "Ludwik". Generalnie instrukcje przypisania służą do nadania zmiennej wartości. Jeśli jakaś zmienna jeszcze nie istnieje, co miało miejsce w przypadku zmiennej name, jest tworzona, a następnie zostaje jej przypisana wartość.

Pułapka Z technicznego punktu widzenia instrukcja przypisania zapamiętuje wartość znajdującą się po prawej stronie znaku równości w pamięci komputera, a zmienna po lewej stronie tylko odwołuje się do tej wartości (i nie przechowuje jej bezpośrednio). Dlatego pythonowi puryści powiedzieliby, że zmienna otrzymuje wartość, a nie że wartość zostaje jej przypisana. Ja jednak używam sformułowań „otrzymuje” i „zostaje jej przypisana” zamiennie w zależności od tego, co w danym kontekście wydaje się najbardziej klarowne. Dowiesz się więcej o implikacjach sytuacji, gdy zmienne odwołują się do wartości (zamiast je przechowywać), w rozdziale 5., w podrozdziale „Referencje współdzielone”

Pojęcie zmiennych

Wykorzystywanie zmiennych Kiedy zmienna zostaje utworzona, odwołuje się do pewnej wartości. Wygoda posługiwania się zmienną polega na tym, że może być ona używana dokładnie tak samo jak wartość, do której się odwołuje. Więc wykonanie instrukcji zawartej w wierszu: print(name)

skutkuje wyświetleniem łańcucha "Ludwik" dokładnie tak, jak wykonanie instrukcji print("Ludwik"). A wiersz print("Cześć,", name)

wyświetla wartość łańcucha "Cześć,", potem spację i wartość łańcucha "Ludwik". W tym przypadku używam zamiast łańcucha "Ludwik" zmiennej name, otrzymując taki sam wynik.

Nazwy zmiennych Jako dumny rodzic swojego programu wybierasz nazwy występujących w nim zmiennych. W przypadku tego programu zdecydowałem się nazwać swoją zmienną name (imię), ale równie dobrze mógłbym użyć nazwy osoba, facet lub alfa7345690876, a program wykonywałby się dokładnie tak samo. Istnieje kilka reguł, których należy przestrzegać, aby tworzyć prawidłowe nazwy zmiennych. Jeśli tylko utworzysz nieprawidłową nazwę, program Cię o tym powiadomi, zgłaszając błąd. Dwie najważniejsze reguły są następujące: 1. Nazwa zmiennej może zawierać tylko cyfry, litery i znaki podkreślenia. 2. Nazwa zmiennej nie może zaczynać się od cyfry. Oprócz reguł tworzenia prawidłowych nazw zmiennych istnieją pewne zalecenia, do których stosują się bardziej doświadczeni programiści przy tworzeniu dobrych nazw zmiennych — jeśli już jakiś czas programowałeś, poznałeś przepaść, jaka dzieli prawidłową nazwę zmiennej od dobrej. (Jedną radę dam Ci natychmiast: nigdy nie nazywaj zmiennej alfa7345690876).  Wybieraj nazwy opisowe. Nazwy zmiennych powinny być na tyle klarowne,

aby inny programista po spojrzeniu na nazwę miał dobre wyobrażenie o tym, co ona reprezentuje. Więc na przykład używaj nazwy wynik zamiast w. (Jedyny wyjątek od tej reguły dotyczy zmiennych używanych przez krótki okres. Programiści często nadają tym zmiennym krótkie nazwy, takie jak x. Ale to jest w porządku, bo poprzez użycie nazwy x programista daje jasno do zrozumienia, że zmienna reprezentuje chwilowe miejsce przechowywania wartości).  Bądź konsekwentny. Istnieją różne szkoły sposobu zapisywania

wielowyrazowych nazw zmiennych. Czy używać nazwy wysoki_wynik, czy też wysokiWynik? Ja używam stylu ze znakami podkreślenia. Lecz nie jest ważne, jaką metodę stosujesz, o ile jesteś konsekwentny.  Przestrzegaj tradycji języka. Pewne konwencje nazewnicze stały się już tradycją.

Na przykład w większości języków (łącznie z Pythonem) nazwy zmiennych

47

48

Rozdział 2. Typy, zmienne i proste operacje wejścia-wyjścia. Program Nieistotne fakty

rozpoczynają się od małej litery. Inną tradycją jest unikanie stosowania podkreślenia jako pierwszego znaku nazw zmiennych. Nazwy, które rozpoczynają się od znaku podkreślenia, mają w Pythonie specjalne znaczenie.  Zachowuj kontrolę nad długością nazw. Wydaje się to przeczyć pierwszemu

wskazaniu: wybieraj nazwy opisowe. Czy osobiste_sprawdzanie_salda_rachunku nie jest znakomitą nazwą zmiennej? Być może nie. Długie nazwy zmiennych mogą prowadzić do problemów. Mogą sprawić, że instrukcje będą mało czytelne. Poza tym im dłuższa nazwa zmiennej, tym większa możliwość pomyłki. Traktując to jako wskazówkę, staraj się utrzymywać długość nazw zmiennych poniżej 15 znaków.

Sztuczka Kod samodokumentujący jest pisany w taki sposób, żeby było łatwo zrozumieć, co się dzieje w programie niezależnie od ewentualnych komentarzy. Wybór dobrych nazw zmiennych jest znakomitym krokiem w kierunku tego rodzaju kodu.

Pobieranie danych wprowadzanych przez użytkownika Po docenieniu tego wszystkiego, co miał do zaoferowania program Pozdrawiacz, możesz wciąż myśleć: „No i co z tego”? Tak, mógłbyś napisać program, który robi dokładnie to samo, co Pozdrawiacz, bez zadawania sobie trudu tworzenia jakiś śmiesznych zmiennych. Lecz aby robić rzeczy o podstawowym znaczeniu, łącznie z pobieraniem i przechowywaniem danych wprowadzanych przez użytkownika oraz manipulowaniem nimi, potrzebujesz zmiennych. Popatrz na kolejny program, który wykorzystuje dane wejściowe do utworzenia spersonalizowanego pozdrowienia.

Prezentacja programu Osobisty pozdrawiacz Program Osobisty pozdrawiacz dodaje do programu Pozdrawiacz tylko jeden, ale za to bardzo fajny element — wprowadzanie danych przez użytkownika. Zamiast wykorzystywać wartość predefiniowaną, komputer pozwala użytkownikowi wprowadzić swoje imię, a potem używa go do powiedzenia mu „cześć”. Program ten został przedstawiony na rysunku 2.7. Pobieranie danych wprowadzanych przez użytkownika nie jest zbyt trudne. W rezultacie kod nie różni się aż tak bardzo. Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 2.; nazwa pliku to osobisty_pozdrawiacz.py.

Pobieranie danych wprowadzanych przez użytkownika

Rysunek 2.7. Teraz zmiennej name zostaje przypisany łańcuch znaków na podstawie tego, co wprowadzi użytkownik; może to być Robert # Osobisty pozdrawiacz # Demonstruje pobieranie danych wprowadzanych przez użytkownika name = input("Cześć. Jak masz na imię? ") print(name) print("Cześć,", name) input("\n\nAby zakończyć program, naciśnij klawisz Enter.")

Używanie instrukcji input() Jedynym wierszem, który się zmienił, jest instrukcja przypisania: name = input("Cześć. Jak masz na imię? ")

Lewa strona instrukcji jest dokładnie taka sama jak w programie Pozdrawiacz. Tak jak przedtem tworzona jest zmienna name i zostaje jej przypisana wartość. Lecz tym razem prawa strona instrukcji przypisania jest wywołaniem funkcji input(). Funkcja input() pobiera pewien tekst od użytkownika. Przyjmuje też argument w postaci łańcucha znaków, którego używa do poproszenia użytkownika o wprowadzenie tego tekstu. W tym przypadku argumentem, który przekazałem do funkcji input(), jest łańcuch "Cześć. Jak masz na imię? ". Jak możesz sprawdzić na rysunku 2.7, funkcja input() faktycznie używa tego łańcucha, aby zachęcić użytkownika do wprowadzenia swojego imienia. Funkcja input() czeka, aż użytkownik coś wprowadzi. Po naciśnięciu przez użytkownika klawisza Enter funkcja input() zwraca wszystko, co użytkownik wpisał, w postaci łańcucha. Ten łańcuch — wartość zwrotna wywołania funkcji — jest tym, co otrzymuje zmienna name. Aby sobie lepiej uzmysłowić, jak to działa, wyobraź

49

50

Rozdział 2. Typy, zmienne i proste operacje wejścia-wyjścia. Program Nieistotne fakty

sobie, że w instrukcji przypisania wywołanie funkcji input() zostaje zastąpione łańcuchem znaków wprowadzonym przez użytkownika. Oczywiście kod w programie nie ulega zmianie, ale wyobrażenie sobie wartości zwracanej przez funkcję zamiast jej wywołania pomaga w przyswojeniu sobie sposobu użycia wartości zwrotnych. Jeśli wywołania funkcji, argumenty i wartości zwrotne nie są nadal dla Ciebie całkowicie jasne, jest jeszcze jeden sposób ich zilustrowania: użycie funkcji input() jest jak zamówienie pizzy. Sama funkcja input() to jakby pizzeria. Dzwonisz do pizzerii, żeby złożyć zamówienie i wywołujesz funkcję input(), aby ją uruchomić. Kiedy dzwonisz do pizzerii, podajesz jakąś informację w rodzaju „pepperoni”. Kiedy wywołujesz funkcję input(), przekazujesz jej argument w rodzaju "Cześć. Jak masz na imię? ". Po zakończeniu Twojej telefonicznej rozmowy z pizzerią jej pracownicy dostarczają pizzę pepperoni pod drzwi Twojego domu. Analogicznie, kiedy wykonasz wywołanie funkcji input(), funkcja zwróci taki łańcuch znaków, jaki wprowadził użytkownik. Pozostała część programu Osobisty pozdrawiacz działa dokładnie tak samo jak program Pozdrawiacz. To, w jaki sposób zmienna name otrzymuje swoją wartość, jest komputerowi obojętne. Tak więc wiersz: print(name)

wyświetla wartość zmiennej name. Podczas gdy wiersz: print("Cześć,", name)

wyświetla łańcuch "Cześć,", za nim spację, a na końcu wartość zmiennej name. W tym momencie wiesz już wystarczająco dużo, aby zrozumieć ostatni wiersz w tych wszystkich konsolowych programach. Zadaniem ostatniego wiersza jest czekanie, aż użytkownik naciśnie klawisz Enter: input("\n\nAby zakończyć program, naciśnij klawisz Enter.")

I to dokładnie robi poprzez funkcję input(). Jako że nie dbam o to, co wprowadzi użytkownik — wystarczy, że naciśnie klawisz Enter — inaczej niż poprzednio, nie przypisuję wartości zwróconej przez funkcję input() do żadnej zmiennej. Może się komuś wydawać dziwne, że otrzymuję wartość zwrotną i nic z nią nie robię, ale taki jest mój wybór. Jeśli nie przypiszę wartości zwrotnej do zmiennej, komputer ją po prostu zignoruje. Więc jak tylko użytkownik naciśnie klawisz Enter, kończy się wywołanie funkcji input() oraz program, a okno konsoli się zamyka.

Używanie metod łańcucha Python ma bogaty zestaw narzędzi do obsługi łańcuchów. Jednym z typów tych narzędzi są metody łańcucha. Umożliwiają one tworzenie nowych łańcuchów ze starych. Możesz zrobić wszystko — od prostych czynności, takich jak utworzenie łańcucha, który jest tylko wersją oryginalnego łańcucha składającą się z samych wielkich liter, do złożonych operacji, takich jak utworzenie nowego łańcucha, który jest wynikiem całego ciągu zawiłych podstawień liter.

Używanie metod łańcucha

Prezentacja programu Manipulacje cytatami Według Marka Twaina „sztuka prorokowania jest bardzo trudna, szczególnie w odniesieniu do przyszłości”. Chociaż jest trudno przepowiedzieć dokładnie przyszłość, to wciąż zabawne jest czytanie przewidywań ekspertów dotyczących przyszłości techniki. Dobrym przykładem jest wypowiedź: „Myślę, że istnieje światowy rynek dla może pięciu komputerów”. Powiedział to w 1943 r. prezes IBM, Thomas Watson. Program Manipulacje cytatami, który napisałem, wypisuje powyższy cytat na różne sposoby, wykorzystując metody łańcucha. (Na szczęście mogłem napisać ten program, ponieważ przypadkiem posiadam komputer numer 3). Popatrz na przykładowe jego uruchomienie przedstawione na rysunku 2.8.

Rysunek 2.8. Ta nieco nietrafna prognoza została wyświetlona na różne sposoby za pomocą metod łańcucha

Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 2.; nazwa pliku to manipulacje_cytatami.py. # Manipulacje cytatami # Demonstruje metody łańcucha # Cytat z wypowiedzi prezesa IBM, Thomasa Watsona, z 1943 r. quote = "Myślę, że istnieje światowy rynek dla może pięciu komputerów." print("Oryginalny cytat w tłumaczeniu na język polski:") print(quote) print("\nDużymi literami:") print(quote.upper()) print("\nMałymi literami:")

51

52

Rozdział 2. Typy, zmienne i proste operacje wejścia-wyjścia. Program Nieistotne fakty print(quote.lower()) print("\nWszystkie wyrazy od dużej litery:") print(quote.title()) print("\nZ drobną zamianą:") print(quote.replace("pięciu", "milionów")) print("\nOryginalny cytat pozostał bez zmian:") print(quote) input("\n\nAby zakończyć program, naciśnij klawisz Enter.")

Tworzenie nowych łańcuchów za pomocą metod łańcucha Chociaż wchodzi tu w grę nowe pojęcie, kod jest nadal dość zrozumiały. Spójrz na poniższy wiersz: print(quote.upper)

Prawdopodobnie odgadłeś, co on robi — wyświetla wersję łańcucha quote zawierającą same duże litery. Robi to dzięki użyciu metody łańcucha upper(). Metoda łańcucha jest jakby zdolnością, którą łańcuch dysponuje. Tak więc dzięki swojej metodzie upper() łańcuch quote posiada zdolność tworzenia nowego łańcucha — swojej wersji, w której wszystkie małe litery są zastąpione dużymi. Kiedy ten nowy łańcuch utworzy, zwraca jego wartość, a omawiany wiersz kodu staje się równoważny następującemu: print("MYŚLĘ, ŻE ISTNIEJE ŚWIATOWY RYNEK DLA MOŻE PIĘCIU KOMPUTERÓW.")

Oczywiście właściwy wiersz kodu tak nie wygląda, ale możesz go sobie w ten sposób wyobrazić, co może Ci pomóc w zrozumieniu działania metody. Prawdopodobnie zwróciłeś uwagę na nawiasy występujące w wywołaniu tej metody. Powinno Ci to przypominać funkcje. Metody są podobne do funkcji. Główna różnica polega na tym, że wbudowana funkcja, taka jak input(), może być wywołana samodzielnie. Natomiast metoda łańcucha musi zostać wywołana w kontekście konkretnego łańcucha. To, co pokazuje poniższy wiersz, nie ma sensu: print(upper())

Uruchamiasz metodę, czyli wywołujesz ją, dopisując kolejno kropkę, nazwę metody i parę nawiasów po elemencie reprezentującym wartość łańcucha. Nawiasy nie są tylko na pokaz. Tak jak w przypadku funkcji możesz wewnątrz nich przekazywać argumenty. Metoda upper() nie przyjmuje żadnych argumentów, ale poznasz przykład metody łańcucha, która to robi, w postaci replace(). Wiersz: print(quote.lower())

Używanie metod łańcucha

wywołuje metodę lower() łańcucha quote w celu utworzenia i zwrócenia wersji tego łańcucha zawierającej same małe litery. Następnie ten nowy, złożony z małych liter łańcuch jest wypisywany. Wiersz: print(quote.title())

wyświetla wersję łańcucha quote, która przypomina niektóre nazwy wielowyrazowe. Metoda title() zwraca łańcuch, w którym każde słowo rozpoczyna się od dużej litery, a pozostałe litery są małe. Wiersz: print(quote.replace("pięciu", "milionów"))

wyświetla nowy łańcuch, w którym każde wystąpienie słowa "pięciu" w łańcuchu quote zostaje zamienione przez "milionów". Metoda replace() potrzebuje przynajmniej dwóch informacji: starego tekstu, który ma być zastąpiony, i nowego tekstu, który go zastąpi. Te dwa argumenty należy oddzielić przecinkiem. Możesz dodać opcjonalny trzeci argument, liczbę całkowitą, który informuje metodę, ile razy maksymalnie może wykonać zastąpienie. W końcu program wyświetla ponownie łańcuch quote: print("\nOryginalny cytat pozostał bez zmian:") print(quote)

Jak widać na rysunku 2.8, łańcuch quote nie uległ zmianie. Zapamiętaj, że metody łańcucha tworzą nowy łańcuch i nie wpływają na wartość oryginalnego. W tabeli 2.3 znajduje się podsumowanie metod łańcucha, które właśnie poznałeś, oraz kilku innych. Tabela 2.3. Przydatne metody łańcucha

Metoda

Opis

upper()

Zwraca wersję łańcucha, w której wszystkie małe litery zostały zamienione na duże.

lower()

Zwraca wersję łańcucha, w której wszystkie duże litery zostały zamienione na małe.

swapcase()

Zwraca nowy łańcuch po odwróceniu wielkości liter. Wszystkie małe litery zostały zamienione na duże, a duże na małe.

capitalize()

Zwraca nowy łańcuch, w którym pierwsza litera została zamieniona na dużą, a pozostałe litery są małe.

title()

Zwraca nowy łańcuch, w którym pierwsza litera każdego słowa została zamieniona na dużą, a wszystkie pozostałe są małe.

strip()

Zwraca łańcuch, w którym wszystkie białe znaki (tabulatory, spacje i znaki nowego wiersza) znajdujące się na początku i na końcu zostały usunięte.

replace(stary, nowy, [, max])

Zwraca łańcuch, w którym wszystkie wystąpienia łańcucha stary zostały zastąpione łańcuchem nowy. Opcjonalny parametr max ogranicza liczbę zamian.

53

54

Rozdział 2. Typy, zmienne i proste operacje wejścia-wyjścia. Program Nieistotne fakty

Stosowanie właściwych typów Do tej pory używałeś trzech różnych typów danych: łańcuchów, liczb całkowitych oraz liczb zmiennoprzecinkowych. Ważne jest, aby nie tylko wiedzieć, jakie typy danych są dostępne, ale także umieć z nich odpowiednio korzystać. W przeciwnym razie możesz pisać programy, które generują niezamierzone wyniki.

Prezentacja programu Uczestnik funduszu powierniczego — niepoprawna wersja Pomysł na następny program to utworzenie narzędzia dla tych lekkoduchów, którzy bawią się cały dzień, żyjąc z hojnego funduszu powierniczego. Program powinien obliczyć ogólną sumę miesięcznych wydatków na podstawie danych wprowadzonych przez użytkownika. Ta ogólna suma ma pomóc tym, którzy żyją na poziomie luksusu przekraczającym wszelkie rozsądne granice, utrzymać się w granicach budżetu, żeby nigdy nie musieli myśleć o podjęciu prawdziwej pracy. Ale jak mogłeś wywnioskować z tytułu, program Uczestnik funduszu powierniczego — niepoprawna wersja nie działa zgodnie z zamierzeniem programisty. Rysunek 2.9 pokazuje jego przykładowe uruchomienie.

Rysunek 2.9. Suma miesięczna powinna być wysoka, ale nie aż tak wysoka. Coś działa nieprawidłowo

Dobrze, program w oczywisty sposób nie działa prawidłowo. Zawiera jakiś błąd. Ale nie taki, który by spowodował awarię programu. Kiedy program generuje niezamierzone wyniki, ale nie kończy awaryjnie swojego działania, zawiera błąd logiczny. Na podstawie tego, co już wiesz, mógłbyś domyślić się, co się stało, patrząc na kod. Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 2.; nazwa pliku to fundusz_powierniczy_zly.py.

Stosowanie właściwych typów # Uczestnik funduszu powierniczego - niepoprawna wersja # Demonstruje błąd logiczny print( """ Uczestnik funduszu powierniczego Sumuje Twoje miesięczne wydatki, żeby Twój fundusz powierniczy się nie wyczerpał (bo wtedy byłbyś zmuszony do podjęcia prawdziwej pracy). Wprowadź swoje wymagane miesięczne koszty. Ponieważ jesteś bogaty, zignoruj grosze i swoje kwoty podaj w pełnych złotych. """ ) car = input("Serwis Mercedesa: ") rent = input("Apartament w Śródmieściu: ") jet = input("Wynajem prywatnego samolotu: ") gifts = input("Podarunki: ") food = input("Obiady w restauracjach: ") staff = input("Personel (służba domowa, kucharz, kierowca, asystent): ") guru = input("Osobisty guru i coach: ") games = input("Gry komputerowe: ") total = car + rent + jet + gifts + food + staff + guru + games print("\nOgółem:", total) input("\n\nAby zakończyć program, naciśnij klawisz Enter.")

Jeśli nie widzisz problemu od razu, to też nie dzieje się nic złego. Udzielę Ci jednak dalszych wskazówek. Jeszcze raz popatrz na dane wyjściowe na rysunku 2.9. Przyjrzyj się uważnie długiej liczbie, którą program wyświetlił jako sumę ogólną. Następnie przypatrz się wszystkim liczbom, które wprowadził użytkownik. Zauważyłeś jakiś związek? Dobrze, niezależnie od tego, czy go zauważyłeś, czy też nie, czytaj dalej.

Znajdowanie błędów logicznych Błędy logiczne mogą być najtrudniejsze do poprawienia. Ponieważ program nie kończy się awarią, nie możesz wykorzystać komunikatu o błędzie, który byłby dla Ciebie podpowiedzią. Musisz obserwować zachowanie programu i badać jego kod. W tym przypadku najwięcej informacji kryje się w danych wyjściowych programu. Olbrzymia liczba wyraźnie nie jest sumą wszystkich liczb, które wprowadził użytkownik. Ale patrząc na te liczby, możesz zauważyć, że wyświetlona suma jest konkatenacją tych wszystkich liczb. Jak się to mogło stać? Cóż, jak pamiętasz, funkcja input() zwraca łańcuch. Więc każda liczba wprowadzona przez użytkownika jest traktowana jak łańcuch znaków. To oznacza, że każda zmienna w tym programie ma przypisaną wartość łańcuchową. Tak więc w wierszu:

55

56

Rozdział 2. Typy, zmienne i proste operacje wejścia-wyjścia. Program Nieistotne fakty total = car + rent + jet + gifts + food + staff + guru + games

nie ma dodawania liczb. To konkatenacja łańcuchów! Teraz, kiedy już wiesz, na czym polega problem, jak go rozwiążesz? Te wszystkie wartości łańcuchowe muszą być jakoś przekształcone na liczby. Wtedy program będzie działał zgodnie z przeznaczeniem. Żeby tylko był jakiś sposób na wykonanie tego przekształcenia. Cóż, jak mogłeś zgadnąć, taki sposób istnieje.

W świecie rzeczywistym Symbol + może oznaczać działanie zarówno na parze łańcuchów, jak i na parze liczb całkowitych. Używanie tego samego operatora do wartości różnych typów jest nazywane przeciążaniem operatora. Chociaż słowo „przeciążanie” może się kojarzyć z czymś złym, ale w rzeczywistości może być dobrą rzeczą. Czy to, że łańcuchy są łączone przy użyciu znaku plusa, nie jest sensowne? Natychmiast rozumiesz, co on oznacza. Dobrze zaimplementowane przeciążanie operatorów może się przyczynić do tworzenia bardziej przejrzystego i eleganckiego kodu.

Konwersja wartości Dobrym rozwiązaniem problemu występującego w programie Uczestnik funduszu powierniczego — niepoprawna wersja jest przekształcenie wartości łańcuchowych zwróconych przez funkcję input() na wartości liczbowe. Ponieważ program operuje kwotami wyrażonymi w pełnych złotych, sensowne jest przeprowadzenie konwersji każdego łańcucha na liczbę całkowitą przed jej użyciem w obliczeniach.

Prezentacja programu Uczestnik funduszu powierniczego — poprawna wersja W programie Uczestnik funduszu powierniczego — poprawna wersja został poprawiony błąd logiczny, który występuje w programie Uczestnik funduszu powierniczego — niepoprawna wersja. Rzuć okiem na dane wyjściowe nowego programu przedstawione na rysunku 2.10. Teraz programowi udało się obliczyć poprawną sumę. Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 2.; nazwa pliku to uczestnik_funduszu_powierniczego_dobry.py. # Uczestnik funduszu powierniczego - poprawna wersja # Demonstruje konwersję typów print( """ Uczestnik funduszu powierniczego Sumuje Twoje miesięczne wydatki, żeby Twój fundusz powierniczy się nie wyczerpał (bo wtedy byłbyś zmuszony do podjęcia prawdziwej pracy).

Konwersja wartości

Rysunek 2.10. Aha, 151 300 miesięcznie wygląda rozsądniej Wprowadź swoje wymagane miesięczne koszty. Ponieważ jesteś bogaty, zignoruj grosze i swoje kwoty podaj w pełnych złotych. """ ) car = input("Serwis Mercedesa: ") car = int(car) rent = int(input("Apartament w Śródmieściu: ")) jet = int(input("Wynajem prywatnego samolotu: ")) gifts = int(input("Podarunki: ")) food = int(input("Obiady w restauracjach: ")) staff = int(input("Personel (służba domowa, kucharz, kierowca, asystent): ")) guru = int(input("Osobisty guru i coach: ")) games = int(input("Gry komputerowe: ")) total = car + rent + jet + gifts + food + staff + guru + games print("\nOgółem:", total) input("\n\nAby zakończyć program, naciśnij klawisz Enter.")

Konwersja łańcuchów na liczby całkowite Istnieje szereg funkcji, które przeprowadzają konwersję typów danych. Funkcja przekształcająca wartość na liczbę całkowitą została pokazana w następujących wierszach kodu: car = input("Serwis Mercedesa: ") car = int(car)

57

58

Rozdział 2. Typy, zmienne i proste operacje wejścia-wyjścia. Program Nieistotne fakty

Pierwszy wiersz pobiera dane wejściowe od użytkownika w postaci łańcucha znaków i przypisuje tę wartość zmiennej car. Drugi wiersz wykonuje konwersję. Funkcja int() pobiera łańcuch, do którego odwołuje się zmienna car, i zwraca jego interpretację w postaci liczby całkowitej. Następnie zmienna car otrzymuje nową wartość, która jest tą liczbą całkowitą. Siedem kolejnych wierszy programu pobiera pozostałe kategorie wydatków i przeprowadza ich konwersję: rent = int(input("Apartament w Śródmieściu: ")) jet = int(input("Wynajem prywatnego samolotu: ")) gifts = int(input("Podarunki: ")) food = int(input("Obiady w restauracjach: ")) staff = int(input("Personel (służba domowa, kucharz, kierowca, asystent): ")) guru = int(input("Osobisty guru i coach: ")) games = int(input("Gry komputerowe: "))

Zauważ, że przypisania są wykonywane w pojedynczych wierszach. Jest to możliwe dlatego, że wywołania dwóch funkcji input() i int() są zagnieżdżone. Zagnieżdżanie wywołań funkcji oznacza umieszczanie jednego wewnątrz drugiego. Działa to doskonale, o ile wartość zwrotna funkcji wewnętrznej może zostać wykorzystana jako wartość argumentu funkcji zewnętrznej. W tym przypadku wartością zwrotną funkcji input() jest łańcuch znaków, a typ łańcuchowy nadaje się doskonale do konwersji za pomocą funkcji int(). W instrukcji przypisującej wartość zmiennej rent funkcja input() pyta użytkownika, ile wynosi czynsz. Użytkownik wprowadza jakiś tekst, który zostaje zwrócony jako łańcuch. Następnie program wywołuje funkcję int() z tym łańcuchem w roli argumentu. Funkcja int() zwraca liczbę całkowitą reprezentowaną przez łańcuch. Wreszcie ta liczba całkowita zostaje przypisana zmiennej rent. Pozostałe sześć instrukcji przypisania działa w taki sam sposób. Istnieją inne funkcje, które przekształcają wartości do określonego typu. Kilka z nich wymieniono w tabeli 2.4. Tabela 2.4. Wybrane funkcje konwersji typów

Funkcja

Opis

Przykład

Zwraca

float(x)

Zwraca liczbę zmiennoprzecinkową po przeprowadzeniu konwersji argumentu x.

float("10.0")

10.0

int(x)

Zwraca liczbę całkowitą po przeprowadzeniu konwersji argumentu x.

int("10")

10

str(x)

Zwraca łańcuch znaków po przeprowadzeniu konwersji argumentu x.

str(10)

'10'

Używanie operatorów rozszerzonego przypisania Operatorów rozszerzonego przypisania jest niemało. Ale sama koncepcja jest prosta. Powiedzmy, że chcesz się dowiedzieć, ile użytkownik wydaje rocznie na żywność. Aby obliczyć i przypisać do zmiennej roczną sumę wydatków, mógłbyś użyć instrukcji:

Powrót do programu Nieistotne fakty food = food * 52

W tej instrukcji wartość zmiennej food jest mnożona przez 52, a następnie otrzymany wynik jest przypisywany z powrotem do zmiennej food. To samo można osiągnąć za pomocą następującego wiersza kodu: food *= 52

Operator *= jest przykładem operatora przypisania rozszerzonego. W tym wierszu mamy również do czynienia z mnożeniem wartości zmiennej food przez 52 i przypisaniem wyniku do tej samej zmiennej, ale instrukcja jest krótsza niż jej poprzednia wersja. Ponieważ przypisywanie zmiennej wyniku operacji wykonywanej na jej poprzedniej wartości jest czymś, co często występuje w programowaniu, tego typu operatory są dobrym sposobem na skrócenie kodu. Istnieją też inne operatory rozszerzonego przypisania. Tabela 2.5 zawiera podsumowanie kilku najbardziej użytecznych. Tabela 2.5. Przydatne operatory rozszerzonego przypisania

Operator

Przykład instrukcji

Instrukcja równoważna

*=

x *= 5

x = x * 5

/=

x /= 5

x = x / 5

%=

x %= 5

x = x % 5

+=

x += 5

x = x + 5

-=

x -= 5

x = x - 5

*=

x *= 5

x = x * 5

Powrót do programu Nieistotne fakty Teraz wiesz już wszystko, czego potrzebujesz do napisania programu do projektu Nieistotne fakty przedstawionego na początku tego rozdziału. Zaprezentuję ten program w nieco inny sposób niż pozostałe. Zamiast wylistowania kodu w całości omówię ten program fragment po fragmencie. Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 2.; nazwa pliku to nieistotne_fakty.py.

Utworzenie komentarzy początkowych Chociaż komentarze nie mają żadnego wpływu na działanie programu, stanowią ważną część każdego projektu. Jak zawsze rozpoczynam od kilku: # Nieistotne fakty # # Uzyskuje dane osobiste od użytkownika, a potem # wypisuje prawdziwe, lecz bezużyteczne informacje o nim

59

60

Rozdział 2. Typy, zmienne i proste operacje wejścia-wyjścia. Program Nieistotne fakty

Wskazówka Doświadczeni programiści wykorzystują także obszar komentarzy początkowych do wstawiania opisów wszystkich modyfikacji wprowadzanych w kodzie w przeciągu czasu. Dzięki temu cała historia programu jest udostępniona na samym jego początku. Ta praktyka jest szczególnie użyteczna, kiedy kilku programistów zajmuje się tym samym kodem.

Wczytanie danych wprowadzonych przez użytkownika Wykorzystując funkcję input(), program wczytuje imię użytkownika, jego wiek i wagę: name = input("Cześć! Jak masz na imię? ") age = input("Ile masz lat? ") age = int(age) weight = int(input("Dobrze, ostatnie pytanie. Ile kilogramów ważysz? "))

Pamiętaj, funkcja input() zawsze zwraca łańcuch. Ponieważ zmienne age i weight będą traktowane jak liczby, ich wartości muszą zostać przekształcone. W przypadku zmiennej age rozbiłem ten proces na dwa wiersze. Najpierw przypisałem do zmiennej łańcuch zwrócony przez funkcję input(). Następnie przekształciłem ten łańcuch na liczbę całkowitą i przypisałem ją ponownie do tej zmiennej. Natomiast w przypadku zmiennej weight mój kod wykonujący przypisanie zmieścił się w jednym wierszu dzięki zagnieżdżeniu wywołań funkcji. Zrealizowałem przypisania na dwa różne sposoby, aby przypomnieć Ci obydwa. Jednak w praktyce wybrałbym jedno podejście, aby być konsekwentnym.

Wyświetlanie imienia przy użyciu samych małych i samych dużych liter Poniższe wiersze kodu wyświetlają wartość zmiennej name w dwu wersjach: najpierw małymi literami, a następnie dużymi za pomocą metod łańcucha: print("\nJeśli poeta ee cummings wysłałby do Ciebie wiadomość e-mail,\nzwróciłby się do Ciebie", name.lower()) print("Ale jeśli byłby wściekły, nazwałby Cię", name.upper())

Nawiasem mówiąc, ee cummings był amerykańskim poetą awangardowym, który nie używał dużych liter. Więc gdyby żył i wysłał do Ciebie wiadomość e-mail, pisząc Twoje imię, użyłby prawdopodobnie samych małych liter. Ale gdyby był wściekły, zrobiłby zapewne wyjątek i nakrzyczałby na Ciebie poprzez użycie w swojej wiadomości dużych liter.

Pięciokrotne wypisanie imienia Program wyświetla imię użytkownika pięć razy z rzędu przy użyciu powielania łańcuchów:

Podsumowanie called = name * 5 print("\nJeśli małe dziecko próbowałoby zwrócić na siebie Twoją uwagę,",) print("Twoje imię przybrałoby formę:") print(called)

Zmiennej called zostaje przypisana powtórzona pięć razy wartość zmiennej name. Następnie zostaje wyświetlony komunikat, a po nim wartość zmiennej called.

Obliczanie liczby sekund W dwóch kolejnych wierszach programu zostaje obliczony i wyświetlony wiek użytkownika wyrażony w sekundach: seconds = age * 365 * 24 * 60 * 60 print("\nŻyjesz już ponad", seconds, "sekund.")

Ponieważ rok ma 365 dni, dzień 24 godziny, godzina 60 minut, a minuta 60 sekund, liczba lat przypisana do zmiennej age zostaje pomnożona przez iloczyn 365 * 24 * 60 * 60. Wynik zostaje przypisany do zmiennej second. Kolejny wiersz kodu wyświetla jego wartość.

Obliczanie wagi na Księżycu i na Słońcu Kolejne cztery wiersze kodu obliczają i wyświetlają wagę użytkownika na Księżycu i na Słońcu: moon_weight = weight / 6 print("\nCzy wiesz, że na Księżycu Twoja waga wynosiłaby", moon_weight, "kg?") sun_weight = weight * 27.1 print("Na Słońcu ważyłbyś (ważyłabyś)", sun_weight, "kg (ale niestety niedługo).

Ponieważ na Księżycu przyciąganie grawitacyjne jest sześciokrotnie mniejsze niż na Ziemi, zmiennej moon_weight zostaje przypisana wartość zmiennej weight podzielona przez 6. A ponieważ siła grawitacji na Słońcu jest 27.1 razy większa niż na Ziemi, mnożę wartość zmiennej weight przez 27.1 i przypisuję wynik do zmiennej sun_weight.

Czekanie na użytkownika Ostatnia instrukcja oznacza czekanie, aż użytkownik naciśnie klawisz Enter: input("\n\nAby zakończyć program, naciśnij klawisz Enter.")

Podsumowanie W tym rozdziale zobaczyłeś, jak tworzyć łańcuch przy użyciu pojedynczych, podwójnych i potrójnych cudzysłowów. Dowiedziałeś się, jak umieszczać w nich znaki niegraficzne za pomocą sekwencji specjalnych. Zobaczyłeś, jak łączyć i powielać łańcuchy znaków.

61

62

Rozdział 2. Typy, zmienne i proste operacje wejścia-wyjścia. Program Nieistotne fakty

Poznałeś dwa różne typy numeryczne — liczby całkowite i zmiennoprzecinkowe — i dowiedziałeś się, jak się nimi posługiwać. Wiesz także, jak przeprowadzać konwersje między łańcuchami i liczbami. Poznałeś zmienne i zobaczyłeś, jak ich można używać do przechowywania i pobierania informacji. Na koniec dowiedziałeś się, jak wczytywać dane wprowadzane przez użytkownika, aby sprawić, że programy będą interaktywne.

Sprawdź swoje umiejętności 1. Utwórz listę prawidłowych i nieprawidłowych nazw zmiennych. Przy każdej nazwie umieść wyjaśnienie, dlaczego jest prawidłowa albo nieprawidłowa. Następnie sporządź listę „dobrych” i „złych” prawidłowych nazw zmiennych. Do każdej z nich dołącz wyjaśnienie, dlaczego została dobrze albo źle wybrana. 2. Napisz program, który umożliwi użytkownikowi wprowadzenie nazw jego dwóch ulubionych przysmaków. Program powinien następnie wypisać nazwę nowego przysmaku utworzoną poprzez połączenie nazw podanych przez użytkownika. 3. Napisz program Kalkulator napiwku, w którym użytkownik wprowadza sumę ogólną z rachunku wystawionego przez restaurację. Program powinien potem wyświetlić dwie kwoty napiwku — w wysokości 15 i 20 procent. 4. Napisz program Sprzedawca samochodów, w którym użytkownik wprowadza podstawową cenę samochodu. Program powinien dodać szereg dodatkowych opłat, takich jak podatek, opłatę rejestracyjną, prowizję przygotowawczą dealera, opłatę za dostarczenie. Oblicz podatek i opłatę rejestracyjną jako pewien procent ceny podstawowej. Pozostałe opłaty powinny mieć stałe wartości. Wyświetl faktyczną cenę samochodu po doliczeniu wszystkich dodatków.

3 Rozgałęzianie kodu, pętle while, projektowanie programu. Gra Odgadnij moją liczbę D

o tej pory programy, które napisałeś, miały prostą sekwencyjną strukturę — wszystkie instrukcje są wykonywane raz, po kolei i przy każdym uruchomieniu. Gdybyś był ograniczony do tylko tego typu programowania, pisanie złożonych aplikacji byłoby bardzo trudne, jeśli nie niemożliwe. Ale w tym rozdziale dowiesz się, jak selektywnie wykonywać pewne porcje kodu oraz powtarzać fragmenty programu. W szczególności nauczysz się:  generować liczby losowe przy użyciu funkcji randint() i randrange(),  używać instrukcji if do warunkowego wykonywania kodu,  wykorzystywać klauzulę else do dokonywania wyboru opartego na warunku,  korzystać z klauzuli elif do dokonywania wyboru opartego na kilku warunkach,  używać pętli while do powtarzania fragmentów programu,  sporządzać szkice programów przy użyciu pseudokodu.

Wprowadzenie do gry Jaka to liczba? Program, który utworzysz w tym rozdziale, jest klasyczną grą polegającą na zgadywaniu liczb. Tych, których ta gra ominęła w dzieciństwie, informuję, na czym ona polega: komputer wybiera losowo liczbę z zakresu od 1 do 100, a gracz stara się ją odgadnąć w jak najmniejszej liczbie prób. Za każdym razem, gdy gracz wprowadza nową liczbę, komputer go informuje, czy liczba jest za duża, za mała, czy dokładnie taka jak trzeba.

64

Rozdział 3. Rozgałęzianie kodu, pętle while, projektowanie programu

Kiedy graczowi udaje się odgadnąć wygenerowaną przez komputer liczbę, gra się kończy. Rysunek 3.1 pokazuje grę Jaka to liczba? w działaniu.

Rysunek 3.1. Odgadnięta w tylko trzech próbach! Spróbuj to pobić

Generowanie liczb losowych Chociaż na ogół użytkownicy oczekują od swoich programów spójnych i przewidywalnych wyników, czasem nieprzewidywalność jest tym, co sprawia, że programy są ekscytujące — nagła zmiana w strategii komputerowego przeciwnika czy pozaziemska istota wypadająca z dowolnych drzwi. Liczby losowe mogą dostarczyć tego elementu przypadku i zaskoczenia, a Python udostępnia łatwy sposób ich generowania.

Pułapka Python generuje liczby losowe na podstawie wzoru matematycznego, więc nie są one prawdziwie losowe. Ta metoda nazywa się generowaniem liczb pseudolosowych i jest satysfakcjonująca w przypadku większości aplikacji (tylko nie próbuj przy jej zastosowaniu uruchamiać internetowego kasyna). Jeśli faktycznie potrzebujesz prawdziwie losowych liczb, odwiedź stronę http://www.fourmilab.ch/hotbits/. Ten portal generuje liczby losowe, bazując na naturalnym i nieprzewidywalnym procesie rozpadu radioaktywnego.

Prezentacja programu Rzut kośćmi Program Rzut kośćmi naśladuje rzucanie kostkami stanowiące istotny element szybkiej, hazardowej gry w kości. Lecz nie musisz nic wiedzieć o grze w kości, aby docenić ten program. Symuluje on rzut dwoma sześciennymi kostkami. W celu ustalenia liczby wyrzuconych oczek program używa funkcji, które generują liczby losowe. Na rysunku 3.2 można zobaczyć ten program w działaniu.

Generowanie liczb losowych

Rysunek 3.2. Ojej! Uzyskałem sumę 8 w moim pierwszym rzucie, co oznacza, że przegrywam

Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 3.; nazwa pliku to rzut_koscmi.py. # Rzut kośćmi # Demonstruje generowanie liczb losowych import random # generuj liczby losowe z zakresu 1 - 6 die1 = random.randint(1, 6) die2 = random.randrange(6) + 1 total = die1 + die2 print("Wyrzuciłeś", die1, "oraz", die2, "i uzyskałeś sumę", total) input("\n\nAby zakończyć program, naciśnij klawisz Enter.")

Import modułu random W pierwszym wierszu kodu powyższego programu wprowadziłem instrukcję import. Instrukcja ta pozwala Ci importować, czyli ładować moduły — w tym przypadku moduł random: import random

Moduły to pliki, które zawierają kod przeznaczony do wykorzystania w innych programach. Zazwyczaj grupują elementy kodu w kolekcje powiązane z jednym obszarem zadań. Moduł random zawiera funkcje związane z generowaniem liczb losowych i produkowaniem przypadkowych wyników.

65

66

Rozdział 3. Rozgałęzianie kodu, pętle while, projektowanie programu

Jeśli wyobrazisz sobie program jako projekt jakiejś konstrukcji, moduły możesz potraktować jak specjalistyczne zestawy narzędzi, które możesz wyciągnąć z warsztatu, gdy ich potrzebujesz. W tym jednak przypadku zamiast ściągać z półki piłę tarczową, zaimportowałem moduł random.

Używanie funkcji randint() Moduł random zawiera funkcję randint(), która generuje losową liczbę całkowitą. Program Rzut kośćmi uzyskuje dostęp do funkcji randint() poprzez następujące jej wywołanie: random.randint(1, 6)

Łatwo zauważyć, że program nie wywołuje funkcji randint() bezpośrednio. Zamiast tego program wywołuje tę funkcję przy użyciu polecenia random.randint(), uzyskując dostęp do funkcji randint() poprzez jej moduł random. W ogólności możesz wywoływać funkcję z zaimportowanego modułu przez podanie nazwy modułu, umieszczenie za nią kropki oraz samego wywołania funkcji. Ta metoda dostępu nazywa się notacją z kropką. Tym, którzy znają język angielski (albo się uczą tego języka), przypomina on dopełniacz saksoński. W języku angielskim „Mike’s Ferrari” oznacza, że chodzi to o Ferrari, którego właścicielem jest Mike. Wykorzystujące notację z kropką wyrażenie random.randint() oznacza funkcję randint(), która należy do modułu random. Notacja z kropką może być użyta w celu uzyskania dostępu do różnych elementów zaimportowanych modułów. Funkcja randint() wymaga podania dwóch argumentów w postaci liczb całkowitych i zwraca losową liczbę całkowitą o wartości mieszczącej się w przedziale wyznaczonym przez argumenty, do którego one same też należą. Tak więc przekazując do funkcji wartości 1 i 6, mam gwarancję, że otrzymam jako wartość zwrotną jedną z liczb: 1, 2, 3, 4, 5 lub 6. Jako że symuluję rzut kostką o sześciu ścianach, w pełni mnie to zadowala.

Używanie funkcji randrange() Moduł random zawiera również funkcję randrange(), która generuje losową liczbę całkowitą. Istnieje kilka sposobów wywołania funkcji randrange(), ale najprostszym jest użycie pojedynczego argumentu w postaci dodatniej liczby całkowitej. Wywołana w ten sposób funkcja zwraca losową liczbę całkowitą z przedziału od 0 do liczby użytej w jej wywołaniu, z włączeniem dolnej, lecz wyłączeniem górnej granicy tego przedziału. Tak więc wynikiem wywołania random.randrange(6) jest jedna z liczb: 0, 1, 2, 3, 4 lub 5. W porządku, a gdzie jest 6? Cóż, funkcja randrange() wybiera losowo jedną z grupy sześciu liczb, a lista liczb zaczyna się od 0. Właśnie dlatego dodałem do wyniku 1, aby otrzymać prawidłową wartość dla drugiej kostki: die2 = random.randrange(6) + 1

W rezultacie zmienna die2 otrzymuje jedną z wartości: 1, 2, 3, 4, 5 lub 6.

Używanie instrukcji if

Pułapka Użyłem w programie Rzut kośćmi obu funkcji — randint() i randrange() — żeby pokazać dwie różne funkcje służące do generowania liczb losowych. W ogólnym przypadku będziesz musiał wybrać funkcję, która będzie najlepiej pasowała do Twoich potrzeb.

Używanie instrukcji if Rozgałęzianie kodu stanowi fundamentalną część programowania komputerów. Zasadniczo oznacza ona podjęcie decyzji, czy pójść jedną ścieżką, czy drugą. Dzięki instrukcji if programy mogą wykonywać określony fragment kodu lub go omijać. Wszystko zależy od organizacji programu.

Prezentacja programu Hasło Program Hasło wykorzystuje instrukcję if do symulacji procedury logowania w systemie komputerowym o wysokim poziomie zabezpieczeń. Program udziela użytkownikowi dostępu tylko po wprowadzeniu prawidłowego hasła. Na rysunkach 3.3 i 3.4 pokazano kilka przykładowych uruchomień. Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 3.; nazwa pliku to haslo.py.

Rysunek 3.3. Ha, ha! Nigdy nie złamiesz tego hasła

67

68

Rozdział 3. Rozgałęzianie kodu, pętle while, projektowanie programu

Rysunek 3.4. Zgadłeś! Powinienem był wybrać lepsze hasło niż „sekret” # Hasło # Demonstruje instrukcję if print("Witaj w systemie firmy Bezpieczny Komputer SA") print("-- bezpieczeństwo to podstawa naszego działania\n") password = input("Wprowadź hasło: ") if password == "sekret": print("Dostęp został udzielony") input("\n\nAby zakończyć program, naciśnij klawisz Enter.")

W świecie rzeczywistym Chociaż program Hasło realizuje swoje zadanie, demonstrując instrukcję if, nie jest jednak dobrym przykładem tego, jak należy implementować bezpieczeństwo komputerów. W gruncie rzeczy każdy mógłby po prostu zbadać kod źródłowy i odkryć hasło „sekret”. Aby stworzyć system walidacji hasła, programista musiałby najprawdopodobniej wykorzystać jakąś formę kryptografii. Kryptografia — starożytna idea sięgająca tysiące lat wstecz — jest stosowana do kodowania informacji, tak aby tylko zamierzeni odbiorcy mogli je zrozumieć. Kryptografia jest samodzielną dziedziną nauki i niektórzy informatycy poświęcają jej całą swoją karierę.

Instrukcja if Kluczową rolę w programie Hasło odgrywa instrukcja if: if password == "sekret": print("Dostęp został udzielony")

Używanie instrukcji if

Do tego, jaka jest jej funkcja, możesz prawdopodobnie dojść sam, czytając kod. Jeśli hasło jest równe "sekret", wyświetlany jest tekst Dostęp został udzielony i program przechodzi do wykonywania następnej instrukcji. Gdy nie jest równe "sekret", program nie wyświetla tego komunikatu i przechodzi bezpośrednio do pierwszej instrukcji za instrukcją if.

Tworzenie warunków Wszystkie instrukcje if zawierają warunek. Warunek to takie wyrażenie, które jest albo prawdziwe, albo fałszywe. Warunki są Ci już dobrze znane. Występują dość często w życiu codziennym. W gruncie rzeczy prawie każde wypowiadane zdanie może być postrzegane jako warunek. Na przykład zdanie „Na zewnątrz jest 37 stopni” mogłoby być potraktowane jak warunek. Jest albo prawdziwe, albo fałszywe. Python ma swoje własne, wbudowane wartości reprezentujące prawdę i fałsz. True reprezentuje prawdę, a False reprezentuje fałsz. Warunek zawsze przyjmuje jedną z tych wartości. W programie Hasło wykorzystywany w instrukcji if warunek to password == "sekret". Oznacza on, że wartość zmiennej password jest równa "sekret" (czyli zmienna password odwołuje się do wartości "sekret"). Ten warunek przyjmuje wartość True lub False, w zależności od wartości zmiennej password. Jeśli wartość zmiennej password jest równa "sekret", warunek ma wartość True. W przeciwnym wypadku wartością warunku jest False.

Omówienie operatorów porównania Warunki są często tworzone przez porównywanie wartości. Wartości można porównywać przy użyciu operatorów porównania. Jeden z operatorów porównania miałeś okazję spotkać w programie Hasło. Jest to operator równości zapisywany jako ==.

Pułapka Operator równości tworzą dwa kolejne znaki równości. Użycie w warunku tylko jednego znaku równości skutkuje pojawieniem się błędu syntaktycznego, ponieważ pojedynczy znak równości reprezentuje operator przypisania. Tak więc wyrażenie password = "sekret" jest instrukcją przypisania, a password == "sekret" to warunek. Jego wartością jest albo True, albo False. Mimo że operator przypisania i operator równości wyglądają podobnie, są to dwie różne rzeczy.

Oprócz operatora równości istnieją inne operatory porównania. W tabeli 3.1 przedstawiono w skrócie kilka najbardziej użytecznych. Przy użyciu operatorów porównania można porównywać liczby całkowite z liczbami całkowitymi, liczby zmiennoprzecinkowe z liczbami zmiennoprzecinkowymi, a także liczby całkowite z liczbami zmiennoprzecinkowymi. Możesz nawet porównywać łańcuchy znaków — wynik jest oparty na porządku alfabetycznym. Na przykład "jabłko" < "pomarańcza", ponieważ zgodnie z porządkiem alfabetycznym łańcuch "jabłko" ma mniejszą wartość niż łańcuch "pomarańcza" (poprzedza go w słowniku).

69

70

Rozdział 3. Rozgałęzianie kodu, pętle while, projektowanie programu Tabela 3.1. Operatory porównania

Operator

Znaczenie

Przykładowy warunek

Przyjmuje wartość

==

równa się

5 == 5

True

!=

nie równa się

8 != 5

True

>

większe niż

3 > 10

False

<

mniejsze niż

5 < 8

True

>=

większe lub równe

5 >= 10

False

0 True True True True False

I program kończy się tak, jak powinien. Na rysunku 3.10 pokazano działanie poprawionego programu.

Rysunek 3.10. Teraz program działa poprawnie, unikając pętli nieskończonej. Los Twojego bohatera nie jest jednak taki szczęśliwy

Traktowanie wartości jako warunków Jeśli poprosiłbym Cię o obliczenie wartości wyrażenia 35 + 2, szybko odpowiedziałbyś, że wynik wynosi 37. Ale jeśli zapytałbym Cię, czy 37 oznacza prawdę, czy fałsz, odpowiedziałbyś pewnie „Hę?”. Ale koncepcja patrzenia na każdą wartość jako na prawdę lub fałsz jest w języku Python uprawniona. Każda wartość dowolnego typu może być traktowana w ten sposób. Tak więc każda z wartości 2749, 8.6, "banan", 0 czy "" może być zinterpretowana jako True albo False. Może się to podejście wydawać dziwaczne, ale nie ma w nim nic trudnego. Reguły pozwalające ustalić, czy jakaś wartość jest prawdą, czy fałszem, są proste. Co ważniejsze, interpretowanie wartości w ten sposób może przyczynić się do tworzenia bardziej eleganckich warunków.

Prezentacja programu Maitre D’ Jeśli nie zostałeś ostatnio zlekceważony w wykwintnej, francuskiej restauracji, mam program akurat dla Ciebie. Maitre D’ wita Cię w wytwornej jadłodajni, a potem pyta, jaką kwotę wsuniesz do kieszeni swojego gospodarza. Jeśli dasz mu zero złotych, zostaniesz

83

84

Rozdział 3. Rozgałęzianie kodu, pętle while, projektowanie programu

słusznie zignorowany. Jeśli dasz jakąś inną sumę, stolik na Ciebie czeka. Na rysunkach 3.11 i 3.12 pokazano ten program w działaniu.

Rysunek 3.11. Kiedy nie dasz maitre d’ napiwku, nie da się znaleźć wolnego stolika

Rysunek 3.12. Tym razem pieniądze pomogły wyleczyć maitre d’ z amnezji

Samo działanie programu może nie sprawić na Tobie dużego wrażenia. Przypomina coś, co już robiłeś. Różnica polega na tym, że w tym programie nie jest wykorzystywany żaden operator porównania. Zamiast tego wartość (kwota pieniędzy) jest traktowana jako warunek. Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 3.; nazwa pliku to maitre_d.py.

Traktowanie wartości jako warunków # Maitre d' # Demonstruje traktowanie wartości jako warunku print("Witaj w Chateau D' Smakosz") print("Wydaje się, że tego wieczoru mamy prawie komplet gości.\n") money = int(input("Ile złotych wsuniesz do kieszeni maitre d'? ")) if money: print("Och, przypomniałem sobie o wolnym stoliku. Proszę tędy.") else: print("Proszę zaczekać. To może trochę potrwać.") input("\n\nAby zakończyć program, naciśnij klawisz Enter.")

Interpretowanie dowolnej wartości jako prawdy lub fałszu Nowa koncepcja została zademonstrowana w wierszu: if money:

Zwróć uwagę, że wartość zmiennej money nie jest porównywana z jakąkolwiek inną wartością — money to warunek. Kiedy dochodzi do wyznaczania wartości liczby traktowanej jako warunek, 0 oznacza False, a dowolna inna wartość — True. Tak więc powyższy wiersz kodu jest równoważny następującemu: if money != 0:

Pierwsza wersja jest prostsza, bardziej elegancka i bardziej intuicyjna. Wygląda naturalnie i można by ją przetłumaczyć na „jeśli są pieniądze”. Reguły dotyczące interpretowania jakiejś wartości jako True lub False są proste. Główna zasada jest następująca: każda wartość pusta lub zerowa jest traktowana jako False, wszystkie inne wartości są interpretowane jako True. Tak więc 0 znaczy tyle, co False, ale wartość logiczna dowolnej innej liczby to True. Pustemu łańcuchowi znaków odpowiada wartość False, a każdemu innemu — wartość True. Jak widać, prawie każda wartość to True. Jedynie wartość pusta i zero są traktowane jako False. Przekonasz się, że sprawdzanie, czy jakaś wartość jest pusta, jest nagminnym zadaniem, więc ten sposób traktowania wartości może często występować w programach. Ostatnią rzeczą wartą zauważenia w kontekście naszego przykładu jest to, że jeśli wprowadzisz ujemną kwotę pieniędzy, maitre d’ też znajdzie dla Ciebie miejsce. Pamiętaj, w przypadku liczb tylko 0 to False. Więc wszystkim ujemnym liczbom odpowiada wartość logiczna True, identycznie jak dodatnim.

85

86

Rozdział 3. Rozgałęzianie kodu, pętle while, projektowanie programu

Tworzenie umyślnych pętli nieskończonych Wkrótce po przeczytaniu podrozdziału zatytułowanego „Unikanie pętli nieskończonych” możesz być bardziej niż trochę zaskoczony, widząc podrozdział o tworzeniu pętli nieskończonych. Czyż pętle nieskończone nie są zawsze wynikiem pomyłki? Cóż, jeśli pętla byłaby naprawdę nieskończona — to znaczy nie mogłaby się nigdy zakończyć — tak, byłby to błąd logiczny. Ale konstrukcje, które nazywam umyślnymi pętlami nieskończonymi, to pętle nieskończone z warunkiem wyjścia wbudowanym w ich ciało. Najlepszym sposobem na zrozumienie umyślnej pętli nieskończonej jest zapoznanie się z jej przykładem.

Prezentacja programu Wybredny licznik Program Wybredny licznik wypisuje liczby od 1 do 10, wykorzystując umyślną pętlę nieskończoną. Jest wybredny, ponieważ nie lubi liczby 5 i pomija ją. Na rysunku 3.13 pokazano działanie tego programu.

Rysunek 3.13. Liczba 5 zostaje pominięta za pomocą instrukcji continue, a pętla kończy się w wyniku użycia instrukcji break

Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 3.; nazwa pliku to wybredny_licznik.py. # Wybredny licznik # Demonstruje instrukcje break i continue count = 0 while True: count += 1 # zakończ pętlę, jeśli wartość zmiennej count jest większa niż 10

Tworzenie umyślnych pętli nieskończonych if count > 10: break # pomiń liczbę 5 if count == 5: continue print(count) input("\n\nAby zakończyć program, naciśnij klawisz Enter.")

Wykorzystanie instrukcji break do wyjścia z pętli Utworzyłem pętlę za pomocą konstrukcji: while True:

W sensie technicznym to oznacza, że pętla jest wykonywana bez końca, chyba że w ciele pętli znajduje się warunek wyjścia. Na szczęście wstawiłem taki jeden: # zakończ pętlę, jeśli wartość zmiennej count jest większa niż 10 if count > 10: break

Ponieważ wartość zmiennej count jest zwiększana o 1 za każdym razem, gdy rozpoczyna się wykonywanie ciała pętli, osiągnie w końcu 11. Kiedy to nastąpi, wykonywana jest instrukcja break, która w tym kontekście oznacza „przerwij wykonywanie pętli”, i pętla się kończy.

Wykorzystanie instrukcji continue do powrotu do początku pętli Tuż przed wyświetleniem wartości zmiennej count umieściłem następujące wiersze kodu: # pomiń liczbę 5 if count == 5: continue

Instrukcja continue oznacza „skocz z powrotem do początku pętli”. Na początku pętli sprawdzany jest warunek while i jeśli okaże się, że ma wartość True, nastąpi ponowne wejście do pętli. Więc w sytuacji, gdy licznik jest równy 5, program nie dociera do instrukcji print(count). Zamiast tego wraca natychmiast do początku pętli, więc liczba 5 nie zostanie nigdy wyświetlona.

Kiedy należy stosować instrukcje break i continue? Możesz używać instrukcji break i continue w dowolnej pętli, jaką utworzysz. Ich użycie nie ogranicza się do umyślnych pętli nieskończonych. Ale powinny być używane oszczędnie. Zarówno break, jak i continue mogą komuś (także i Tobie!) utrudnić śledzenie porządku, w jakim są wykonywane instrukcje pętli, oraz zrozumienie okoliczności, w jakich pętla

87

88

Rozdział 3. Rozgałęzianie kodu, pętle while, projektowanie programu

się kończy. Poza tym instrukcje break i continue nie są Ci właściwie potrzebne. Każda pętla, którą możesz napisać przy ich użyciu, może być napisana bez ich zastosowania. W Pythonie zdarzają się przypadki, gdy umyślna pętla nieskończona może być bardziej przejrzysta niż pętla tradycyjna. W takich sytuacji, gdy tworzenie pętli ze standardowym warunkiem jest naprawdę niezręczne, niektórzy programiści korzystają z umyślnych pętli nieskończonych.

Korzystanie z warunków złożonych Do tej pory spotkałeś tylko porównania, w których uczestniczą dokładnie dwie wartości. Nazywają się warunkami prostymi. Ale możesz się znaleźć w sytuacji, w której chciałbyś mieć więcej możliwości. Na szczęście możesz łączyć ze sobą proste warunki za pomocą operatorów logicznych. W taki sposób połączone te proste warunki stają się warunkami złożonymi. Wykorzystując warunki złożone, Twoje programy mogą podejmować decyzje oparte na porównywaniu wielu grup wartości.

Prezentacja programu Ekskluzywna sieć Ekskluzywne kluby to żadna przyjemność, chyba że jesteś ich członkiem. Utworzyłem więc program Ekskluzywna sieć. Symuluje on elitarną sieć komputerową, której członkowie to kilka wybranych osób. Lista członków składa się ze mnie i kilku aktualnie najlepszych na świecie projektantów gier (niezłe towarzystwo). Podobnie jak w przypadku systemów komputerowych ze świata realnego, każda osoba musi wprowadzić nazwę użytkownika i hasło. Członek musi wprowadzić zarówno swoją nazwę użytkownika, jak i hasło, bo inaczej nie będzie mógł się zalogować. Po udanym zalogowaniu członek jest osobiście pozdrawiany. Również jak w systemach ze świata rzeczywistego, każdy ma określony poziom uprawnień. Ponieważ nie jestem totalnym elitarystą, goście również mogą się logować. Mają jednak najniższy poziom uprawnień. Na rysunkach od 3.14 do 3.16 pokazano działanie tego programu. Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 3.; nazwa pliku to udzielony_odmowiony.py. # Ekskluzywna sieć # Demonstruje operatory logiczne i warunki złożone print("\tEkskluzywna Sieć Komputerowa") print("\t Tylko dla członków!\n") security = 0

Korzystanie z warunków złożonych

Rysunek 3.14. Jeśli nie jesteś członkiem ani gościem, nie możesz wejść do sieci

Rysunek 3.15. Gość może się zalogować, ale jego poziom uprawnień będzie dość niski

Rysunek 3.16. Wygląda na to, że jeden z kompanów dziś się zalogował

89

90

Rozdział 3. Rozgałęzianie kodu, pętle while, projektowanie programu username = "" while not username: username = input("Użytkownik: ") password = "" while not password: password = input("Hasło: ") if username == "M.Dawson" and password == "sekret": print("Cześć, Mike!") security = 5 elif username == "S.Meier" and password == "cywilizacja": print("Hej, Sid!") security = 3 elif username == "S.Miyamoto" and password == "mariobros": print("Co u Ciebie, Shigeru?") security = 3 elif username == "W.Wright" and password == "simsowie": print("Jak leci, Will?") security = 3 elif username == "gość" or password == "gość": print("Witaj, Gościu!") security = 1 else: print("Nieudana próba logowania. Nie jesteś taki wyjątkowy.\n") input("\n\nAby zakończyć program, naciśnij klawisz Enter.")

W świecie rzeczywistym Gdybyś faktycznie chciał zaimplementować sieć prywatną, nie umieszczałbyś nazw użytkowników i haseł bezpośrednio w swoim kodzie. Prawdopodobnie skorzystałbyś z pewnego typu systemu zarządzania bazą danych (ang. Database Management System — DBMS). Systemy zarządzania bazą danych umożliwiają organizowanie wzajemnie powiązanych informacji, dostęp do nich oraz ich aktualizację. Te systemy mają wielkie możliwości i mogłyby obsługiwać szybko i bezpiecznie tysiące lub nawet miliony par złożonych z nazwy użytkownika i hasła.

Operator logiczny not Chciałem uzyskać pewność, że użytkownik zapytany o swoją nazwę i hasło wprowadzi jakieś dane. Samo naciśnięcie klawisza Enter, którego efektem jest pusty łańcuch, nie może zostać zaakceptowane. Potrzebowałem pętli, która dotąd pyta o nazwę użytkownika, aż użytkownik coś wprowadzi: username = "" while not username: username = input("Użytkownik: ")

W warunku while użyłem operatora logicznego not. Funkcjonuje on tak samo jak słowo „nie”. W języku polskim umieszczenie przed czymś słowa „nie” tworzy nowe

Korzystanie z warunków złożonych

wyrażenie, które jest zaprzeczeniem oryginału. W Pythonie umieszczenie not przed warunkiem tworzy nowy warunek, którego wartość logiczna jest przeciwna do wartości logicznej oryginału. To oznacza, że warunek not username ma wartość True, kiedy wartością wyrażenia username jest False, oraz że warunek not username ma wartość False, kiedy wartością wyrażenia username jest True. Oto inny sposób przedstawienia działania operatora not: username True False

not username False True

Ponieważ w tym programie zmienna username została zainicjalizowana poprzez przypisanie pustego łańcucha, jej początkowa wartość logiczna to False. To sprawia, że wartością warunku not username jest True i pętla jest wykonywana po raz pierwszy. Następnie program przypisuje zmiennej username wartość wprowadzoną przez użytkownika. Jeśli użytkownik naciśnie tylko klawisz Enter, wartością zmiennej username będzie, jak przedtem, pusty łańcuch. Więc jak poprzednio, warunek not username będzie miał wartość True i pętla wykona się ponownie. Dlatego też dopóki użytkownik tylko naciska klawisz Enter, pętla nie przestaje się wykonywać i użytkownik jest nieustannie proszony o wprowadzenie swojej nazwy. Lecz kiedy użytkownik w końcu wprowadzi jakiś tekst, wartością zmiennej username stanie się coś innego niż pusty łańcuch. To sprawi, że warunek username przyjmie wartość True, a not username wartość False. W rezultacie pętla przestaje się wykonywać, dokładnie tak jak chciałem. Program w taki sam sposób obsługuje zmienną password.

Operator logiczny and Jeśli członek chce się zalogować do tej ekskluzywnej sieci, musi wprowadzić nazwę użytkownika i hasło, które są rozpoznawane łącznie. Jeśli na przykład Sid Meier chce się zalogować, musi wprowadzić tekst S.Meier jako nazwę użytkownika i cywilizacja jako hasło. Jeśli Sid nie wprowadzi obu elementów w dokładnie taki sposób, nie będzie mógł się zalogować. Kombinacja S.Meier i mariobros nie zadziała. Ani M.Dawson i cywilizacja. Zestawienie cywilizacja i S.Meier również zawiedzie. Program sprawdza, czy Sid wprowadził nazwę użytkownika S.Meier i hasło cywilizacja za pomocą następującego kodu: elif username == "S.Meier" and password == "cywilizacja":

Powyższy wiersz zawiera jeden warunek złożony utworzony z dwóch warunków prostych. Warunki proste to username == "S.Meier" oraz password == "cywilizacja". Są to takie same warunki, jakie już poznałeś, lecz zostały połączone operatorem logicznym and, tworząc większy, złożony warunek username == "S.Meier" and password == "cywilizacja". Ten warunek złożony, choć dłuższy od tych, do których się przyzwyczaiłeś, jest wciąż tylko warunkiem, co oznacza, że może mieć wartość True lub False. Więc kiedy warunek username == "S.Meier" and password == "cywilizacja"

91

92

Rozdział 3. Rozgałęzianie kodu, pętle while, projektowanie programu

ma wartość True, a kiedy False? Cóż, podobnie jak „i” w języku polskim, „and” oznacza wymaganie, aby były spełnione obydwa warunki składowe. Więc nasz warunek ma wartość True, jeśli zarówno username == "S.Meier", jak i password == "cywilizacja" mają wartość True; w przeciwnym wypadku jego wartość to False. Oto inny sposób przedstawienia, jak funkcjonuje operator and: username == "S.Meier"

password == "cywilizacja"

True True False False

True False True False

username == "S.Meier" and password == "cywilizacja" True False False False

Wskazówka Umieść operator and między dwoma warunkami, kiedy chcesz utworzyć nowy warunek, który jest spełniony (ma wartość True) wtedy i tylko wtedy, gdy są spełnione obydwa prostsze warunki.

Więc jeśli Sid wprowadzi S.Meier jako swoją nazwę użytkownika i cywilizacja jako hasło, warunek złożony będzie spełniony. Sid zostaje wtedy pozdrowiony i program przypisuje mu odpowiedni poziom uprawnień. Oczywiście oprócz Sida Meiera program obsługuje także innych użytkowników. Wykorzystując strukturę if-elif-else, sprawdza cztery różne pary danych złożone z nazwy użytkownika i hasła. Jeśli użytkownik wprowadzi parę, która zostanie rozpoznana, jest pozdrawiany w indywidualny sposób i zostaje mu przypisany określony poziom uprawnień. Jeśli członek sieci lub gość nie zaloguje się poprawnie, komputer wyświetli komunikat o „nieudanej próbie logowania” i informuje tę osobę, że nie jest taka wyjątkowa.

Operator logiczny or Goście także mogą korzystać z sieci, lecz z ograniczonym poziomem uprawnień. Aby ułatwić gościowi wypróbowanie sieci, wymaga się od niego, aby wprowadził słowo gość albo jako nazwę użytkownika, albo jako hasło. Obsługa logowania gościa jest zawarta w następujących wierszach kodu; elif username == "gość" or password == "gość": print("Witaj, Gościu!") security = 1

Warunek klauzuli elif postaci username == "gość" or password == "gość" w dużym stopniu przypomina pozostałe warunki, które były użyte w kodzie obsługi członków. Ale jest jedna poważna różnica. Warunek dotyczący gościa został utworzony przy użyciu operatora or. Warunek złożony utworzony za pomocą or ma wartość True, o ile przynajmniej jeden z warunków składowych ma wartość True. Operator or funkcjonuje tak jak „lub”

Projektowanie programów

w języku polskim i oznacza „którykolwiek”. Więc jeśli którykolwiek z warunków prostych ma wartość True, warunek złożony ma również wartość True. W tym szczególnym przypadku, jeśli warunek username == "gość" ma wartość True lub jeśli warunek password == "gość" ma wartość True, lub jeśli nawet obydwa mają wartość True, to warunek username == "gość" or password == "gość" przyjmuje wartość True; w przeciwnym wypadku jego wartością jest False. Oto inny sposób spojrzenia na działanie operatora or: username == "gość"

password == "gość"

True True False False

True False True False

username == "gość" or password == "gość" True True True False

Projektowanie programów Do tej pory wszystkie programy, które poznałeś, były dość proste. Pomysł sporządzania na papierze formalnego projektu któregokolwiek z nich wydaje się zapewne przesadą. Ale nie jest. Projektowanie programów, nawet tych małych, prawie zawsze przynosi efekt w postaci zaoszczędzonego czasu (a często pomaga uniknąć frustracji). Programowanie w dużym stopniu przypomina budowę. Więc wyobraź sobie kontrahenta budującego dla Ciebie dom bez wcześniejszego planu. Prawdziwy horror! Grozi Ci, że wylądujesz z domem, który ma 12 łazienek, nie ma żadnych okien, a drzwi frontowe zostały umieszczone na piętrze. Ponadto koszt jego budowy okaże się 10 razy wyższy od przewidywanego. Podobnie rzecz ma się z programowaniem. Bez projektu będziesz się szamotać w trakcie pisania programu, tracąc tylko czas. Możesz nawet skończyć z programem, który nie całkiem działa. Projektowanie programów jest aż tak ważne, że powstała cała dziedzina inżynierii oprogramowania poświęcona temu zadaniu. Lecz nawet początkujący programista może skorzystać z paru prostych narzędzi i technik projektowania.

Tworzenie algorytmów przy użyciu pseudokodu Algorytm to zestaw jasnych, łatwych do wykonania instrukcji służących do zrealizowania pewnego zadania. Algorytm jest czymś w rodzaju szkicu programu. Jest czymś, co zaprojektowałeś przed rozpoczęciem programowania, aby kierowało Twoimi krokami w trakcie tworzenia kodu. Algorytm nie jest tylko specyfikacją celu — jest konkretną listą kroków, jakie kolejno należy wykonać. Więc na przykład instrukcja „Zostań milionerem” tak naprawdę nie jest algorytmem. Bardziej przypomina sformułowanie zadania, choć nie byle jakiego. Więc napisałem algorytm „Zarób milion złotych”. Oto on:

93

94

Rozdział 3. Rozgałęzianie kodu, pętle while, projektowanie programu jeśli potrafisz wymyślić nowy i pożyteczny produkt wtedy masz już swój produkt w przeciwnym wypadku zmień opakowanie istniejącego produktu i potraktuj go jako swój produkt przygotuj reklamę informacyjną promującą Twój produkt pokaż reklamę informacyjną w telewizji bierz 100 zł za jednostkę swojego produktu sprzedaj 10 000 jednostek swojego produktu

O to właśnie chodzi. Mamy klarowny ciąg określonych kroków, które można wykonać, aby osiągnąć cel. Algorytmy są na ogół zapisywane w czymś, co nazywa się pseudokodem, i mój nie stanowi wyjątku. Pseudokod plasuje się gdzieś pomiędzy językiem naturalnym i językiem programowania. Każdy, kto zna język polski, może zrozumieć mój algorytm. Ale jednocześnie mój algorytm powinien sprawiać nieco wrażenie programu. Pierwsze cztery wiersze przypominają strukturę if-else i nie jest to przypadek.

Stopniowe udoskonalanie algorytmów Tak jak każdy szkic czy plan Twój algorytm może nie osiągnąć ostatecznej formy po pierwszym jego sporządzeniu. Często algorytmy wymagają wielu rewizji, zanim mogą zostać zaimplementowane w kodzie. Stopniowe udoskonalanie oznacza całościowy proces przerabiania algorytmów, tak aby nadawały się do implementacji. Zasadniczo oznacza to ich uszczegółowienie. Poprzez analizę każdego kroku algorytmu i rozbicie go na ciąg prostszych kroków sprawiasz, że algorytm coraz bardziej się zbliża do kodu programu. Stosując metodę stopniowego udoskonalania, kontynuujesz rozbijanie każdego kroku na mniejsze, dopóki nie uznasz, że cały algorytm może być dość łatwo przetłumaczony na program. Jako przykład weź pod uwagę następujący krok algorytmu Zarób milion złotych: przygotuj reklamę informacyjną promującą Twój produkt

Może to robić wrażenie zadania sformułowanego zbyt niejasno. W jaki sposób tworzy się reklamę informacyjną? Przy użyciu metody stopniowego udoskonalania pojedynczy krok może być rozbity na kilka drobniejszych. W efekcie otrzymujesz coś takiego: napisz scenariusz reklamy informacyjnej promującej Twój produkt wynajmij na jeden dzień studio telewizyjne zaangażuj ekipę produkcyjną wynajmij entuzjastyczną widownię nakręć reklamę

Jeśli uważasz, że każdy z tych pięciu kroków jest zrozumiały i możliwy do wykonania, ta część algorytmu została w pełni udoskonalona. Jeśli jakiś krok nadal jest dla Ciebie niejasny, przedstaw go jeszcze bardziej szczegółowo. Kontynuuj ten proces, a będziesz miał kompletny algorytm i milion złotych.

Powrót do gry Jaka to liczba?

Powrót do gry Jaka to liczba? Program Jaka to liczba? łączy w sobie wiele koncepcji, które poznałeś w tym rozdziale. Ale co ważniejsze, reprezentuje pierwszą kompletną grę, którą możesz popisać się przed swoimi przyjaciółmi, rodziną i osobami płci przeciwnej.

Projektowanie programu W celu zaprojektowania gry napisałem najpierw kilka wierszy pseudokodu: wybierz losowo jakąś liczbę dopóki gracz nie odgadł wybranej liczby daj graczowi szansę jej odgadnięcia pogratuluj graczowi

Jest to niezła pierwsza wersja, ale brakuje jej pewnych istotnych elementów. Po pierwsze, program musi poinformować użytkownika, czy podana przez niego liczba jest za duża, czy za mała. Po drugie, program powinien zarejestrować, ile razy gracz próbował odgadnąć wybraną liczbę, oraz poinformować go o liczbie wykonanych prób pod koniec gry.

Wskazówka Jest w porządku, jeśli Twój pierwszy projekt programu nie jest kompletny. Zaczynaj projektowanie od głównych idei, a potem wypełniaj luki, dopóki nie uznasz, że wszystko zostało zrobione.

Dobrze, oto udoskonalona wersja mojego algorytmu: przywitaj gracza i wyjaśnij zasady gry wybierz losowo liczbę z zakresu od 1 do 100 poproś gracza o odgadnięcie wybranej liczby ustaw liczbę prób odgadnięcia na 1 dopóki liczba wprowadzona przez gracza nie jest równa wybranej liczbie jeśli wprowadzona liczba jest większa niż wybrana liczba poinformuj gracza, że podana przez niego liczba jest za duża w przeciwnym wypadku poinformuj gracza, że podana przez niego liczba jest za mała pobierz od gracza nową liczbę zwiększ liczbę prób odgadnięcia o 1 pogratuluj graczowi odgadnięcia wybranej liczby poinformuj gracza, ile prób potrzebował na odgadnięcie wybranej liczby

Teraz jestem gotów do pisania programu. Przejrzyj kolejne kilka punktów i przekonaj się, jak prosto pseudokod może zostać przetłumaczony na instrukcje języka Python. Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 3.; nazwa pliku to jaka_to_liczba.py.

95

96

Rozdział 3. Rozgałęzianie kodu, pętle while, projektowanie programu

Tworzenie początkowego bloku komentarzy Podobnie jak wszystkie dobre programy, ten również rozpoczyna się od bloku komentarzy: # # # # # #

Jaka to liczba? Komputer wybiera losowo liczbę z zakresu od 1 do 100. Gracz próbuje ją odgadnąć, a komputer go informuje, czy podana przez niego liczba jest: za duża, za mała, prawidłowa.

Import modułu random Aby dostarczyć dobrej zabawy, program musi wygenerować liczbę losową. Zaimportowałem więc moduł random: import random

Objaśnienie gry Gra jest prosta, ale trochę wyjaśnień nie zaszkodzi: print("\tWitaj w grze 'Jaka to liczba'!") print("\nMam na myśli pewną liczbę z zakresu od 1 do 100.") print("Spróbuj ją odgadnąć w jak najmniejszej liczbie prób.\n")

Ustawienie wartości początkowych W następnej kolejności nadaję wszystkim zmiennym wartości początkowe: # ustaw wartości początkowe the_number = random.randint(1, 100) guess = int(input("Ta liczba to: ")) tries = 1

Zmienna the_number reprezentuje liczbę, którą gracz powinien odgadnąć. Przypisuję jej liczbę całkowitą wybraną losowo spośród liczb od 1 do 100 poprzez wywołanie funkcji random.randint(). Następnie funkcja input() odczytuje pierwszą odpowiedź gracza. Funkcja int() przekształca tę odpowiedź na liczbę całkowitą. Przypisuję tę liczbę do zmiennej guess. Zmiennej tries, która reprezentuje dotychczasową liczbę odpowiedzi gracza, przypisuję wartość 1.

Utworzenie pętli zgadywania Jest to rdzeń programu. Pętla jest wykonywana tak długo, aż gracz prawidłowo odgadnie liczbę wybraną przez komputer. W ciele pętli odpowiedź gracza jest porównywana z liczbą komputera. Jeśli liczba podana przez gracza jest większa od liczby wybranej przez komputer, wyświetlana jest informacja Za duża; w przeciwnym razie wyświetlany jest tekst Za mała. Gracz wprowadza kolejną odpowiedź i licznik prób odgadnięcia wybranej liczby jest inkrementowany.

Podsumowanie # pętla zgadywania while guess != the_number: if guess > the_number: print("Za duża...") else: print("Za mała...") guess = int(input("Ta liczba to: ")) tries += 1

Pogratulowanie graczowi Kiedy gracz odgadnie wybraną liczbę, wartość zmiennej guess jest równa wartości zmiennej the_number, co oznacza, że warunek pętli guess != the_number ma wartość False i pętla się kończy. W tym momencie graczowi należą się gratulacje: print("Odgadłeś! Ta liczba to", the_number) print("Do osiągnięcia sukcesu potrzebowałeś tylko", tries, "prób!\n")

Komputer potwierdza odgadnięcie przez gracza tajemnej liczby i informuje go, ile prób potrzebował do osiągnięcia sukcesu.

Czekanie, aż gracz zakończy program Jak zawsze, ostatni wiersz oznacza cierpliwe oczekiwanie na naciśnięcie przez gracza klawisza Enter: input("\n\nAby zakończyć program, naciśnij klawisz Enter.")

Podsumowanie W tym rozdziale zobaczyłeś, jak zmieniać przepływ programu. Dowiedziałeś się, że kluczem do zmieniania tego przepływu jest zdolność komputera do określania wartości warunków. Zobaczyłeś, jak tworzyć warunki proste i złożone. Poznałeś instrukcje if, if-else oraz if-elif-else, które umożliwiają programom podejmowanie decyzji. Zetknąłeś się z pętlą while, która przydaje się przy powtarzaniu fragmentów kodu. Dowiedziałeś się, jakie jest znaczenie projektowania programu. Zobaczyłeś, jak można projektować program poprzez tworzenie algorytmu w pseudokodzie. Dowiedziałeś się również, jak generować liczby losowe, aby wprowadzić do programów nieco ożywienia.

Sprawdź swoje umiejętności 1. Napisz program, który symuluje ciasteczko z wróżbą. Program powinien wyświetlić jedną z pięciu niepowtarzalnych przepowiedni, dokonując losowego wyboru przy każdym uruchomieniu.

97

98

Rozdział 3. Rozgałęzianie kodu, pętle while, projektowanie programu

2. Napisz program, który „rzuca” 100 razy monetą, a następnie podaje użytkownikowi liczbę orzełków i reszek. 3. Zmodyfikuj program Jaka to liczba? tak, aby gracz dysponował ograniczoną liczbą prób odgadnięcia wybranej przez komputer liczby. Gdy graczowi nie uda się odgadnąć tej liczby w wyznaczonej liczbie prób, program powinien wyświetlić odpowiedni komunikat z reprymendą. 4. Tym razem trudniejsze wyzwanie. Napisz pseudokod do programu, w którym gracz i komputer zamienią się rolami w grze z odgadywaniem liczby. To znaczy gracz wybiera losowo liczbę z przedziału od 1 do 100, a komputer ma ją odgadnąć. Zanim rozpoczniesz tworzenie algorytmu, pomyśl, w jaki sposób sam byś zgadywał. Jeśli wszystko się uda, spróbuj napisać kod gry.

4 Pętle for, łańcuchy znaków i krotki. Gra Wymieszane litery M

iałeś okazję się przekonać, jaki wspaniały sposób dostępu do informacji stanowią zmienne, ale tak jak Twoje programy stają się coraz dłuższe i bardziej skomplikowane, może rosnąć liczba używanych przez Ciebie zmiennych. Pamiętanie o nich wszystkich może stać się bardzo uciążliwe. Dlatego w tym rozdziale poznasz pojęcie sekwencji i spotkasz się z nowym typem danych o nazwie krotka, który umożliwi Ci organizowanie informacji w uporządkowane grupy, abyś mógł nimi łatwiej manipulować. Przekonasz się również, że typ, z którym się już spotkałeś — łańcuch znaków — jest w istocie także sekwencją. Poznasz nowy rodzaj pętli, który został skonstruowany specjalnie do pracy z sekwencjami. W szczególności nauczysz się, jak:  tworzyć pętle for, aby przechodzić przez sekwencje,  używać funkcji range() do tworzenia sekwencji liczb,  traktować łańcuchy znaków jako sekwencje,  używać krotek w celu wykorzystania potencjału sekwencji,  używać funkcji i operatorów działających na sekwencjach,  indeksować i wycinać sekwencje.

Wprowadzenie do programu Wymieszane litery Gra Wymieszane litery przedstawiona na rysunku 4.1 wykorzystuje wiele spośród nowych koncepcji, które poznasz w tym rozdziale.

100

Rozdział 4. Pętle for, łańcuchy znaków i krotki. Gra Wymieszane litery

Rysunek 4.1. Gra Wymieszane litery. Nie jest tak łatwo rozwiązać anagram

Ta gra odtwarza popularną łamigłówkę, jaką mógłbyś znaleźć w niedzielnym wydaniu gazety (musisz wiedzieć, że ludzie mieli zwyczaj czytać takie coś przed nastaniem ery internetu). Komputer wybiera losowo słowo z pewnej grupy, a potem tworzy jego pomieszaną wersję, w której litery występują w przypadkowej kolejności. Gracz, aby wygrać, musi odgadnąć oryginalne słowo.

Używanie pętli for W poprzednim rozdziale poznałeś jeden rodzaj pętli — pętlę while, która powtarza fragment kodu, bazując na warunku. Dopóki warunek jest spełniony, wykonywanie pewnego kodu jest powtarzane. W pętli for również ma miejsce powtarzanie kodu, lecz nie jest ono oparte na warunku. Zamiast tego pętla for powtarza fragment programu, bazując na sekwencji — uporządkowanej liście pewnych elementów. Jeśli kiedykolwiek sporządziłeś listę, powiedzmy, swoich dziesięciu najbardziej ulubionych filmów, to znaczy, że utworzyłeś sekwencję. Pętla for powtarza wykonywanie instrukcji tworzących jej ciało dla każdego elementu sekwencji po kolei. Kiedy zostanie osiągnięty koniec sekwencji, pętla się kończy. Jako przykład rozważ ponownie sekwencję tytułów filmów. Dzięki pętli for mógłbyś przejść przez tę sekwencję od tytułu do tytułu i każdy z nich wyświetlić. Lecz najlepszym sposobem na zrozumienie pętli for jest zobaczenie jej w działaniu.

Prezentacja programu Zwariowany łańcuch Ten program pobiera słowo od użytkownika i wypisuje kolejno tworzące go litery po jednej w wierszu. Popatrz na jego przykładowe uruchomienie przedstawione na rysunku 4.2.

Wprowadzenie do programu Wymieszane litery

Rysunek 4.2. Pętla for przechodzi kolejno przez wszystkie litery słowa wprowadzonego przez użytkownika

Ten prosty program stanowi dobry przykład użycia pętli for. Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 4.; nazwa pliku to zwariowany_lancuch.py. # Zwariowany łańcuch # Demonstruje użycie pętli for z łańcuchem znaków word = input("Wprowadź jakieś słowo: ") print("\nOto poszczególne litery Twojego słowa:") for letter in word: print(letter) input("\n\nAby zakończyć program, naciśnij klawisz Enter.")

Działanie pętli for Nową koncepcją w tym programie jest pętla for, która składa się z następujących dwóch krótkich wierszy: for letter in word: print(letter)

Kod jest dość jasny nawet dla kogoś, kto nic jeszcze nie wie o pętlach for, ale wyjaśnię dokładnie, jak działa. Wszystkie sekwencje składają się z elementów. Łańcuch to sekwencja, w której każdy element to jeden znak. W przypadku łańcucha "Pętla" pierwszym elementem jest litera "P", drugim "ę" itd. Pętla for maszeruje przez sekwencję od elementu do elementu (iteruje po elementach sekwencji). W moim programie pętla iteruje po znakach łańcucha "Pętla". Pętla for wykorzystuje zmienną, która udostępnia każdy kolejny znak łańcucha "Pętla".

101

102

Rozdział 4. Pętle for, łańcuchy znaków i krotki. Gra Wymieszane litery

Wtedy pętla może z każdym kolejnym elementem coś zrobić wewnątrz swojego ciała. W ciele mojej pętli wypisuję po prostu wartość zmiennej letter. Zmienna, której używasz do udostępniania poszczególnych elementów sekwencji, nie różni się od dowolnej innej zmiennej — a jeśli nie istniała przed pętlą, zostaje utworzona. Więc kiedy moja pętla się zaczyna, jest tworzona zmienna letter, a jej wartością staje się pierwszy znak łańcucha przypisanego do zmiennej word, którym jest litera "P". Następnie w ciele pętli instrukcja print wyświetla P. Po zakończeniu wykonywania ciała pętli sterowanie wraca do początku pętli i zmienna letter udostępnia kolejny znak łańcucha reprezentowanego przez zmienną word, którym jest "ę". Komputer wyświetla literę ę i pętla kontynuuje swoje działanie, dopóki nie zostaną wyświetlone wszystkie litery łańcucha "Pętla".

W świecie rzeczywistym Większość współczesnych języków oferuje jakąś formę pętli for. Te pętle są jednak zwykle bardziej restrykcyjne. Pętle na ogół udostępniają jedynie zmienną odgrywającą rolę licznika, której wartością musi być liczba. Wówczas licznik zmienia się o taką samą wartość przy każdym wykonaniu pętli. Możliwość bezpośredniej iteracji po elementach sekwencji sprawia, że pętla for w Pythonie jest bardziej elastyczna niż ten inny, bardziej tradycyjny typ pętli.

Tworzenie pętli for W celu utworzenia pętli for możesz posłużyć się przykładem z programu. Zacznij od słowa for; po nim wprowadź kolejno: nazwę zmiennej reprezentującej bieżący element sekwencji, słowo in, sekwencję, po której chcesz iterować, dwukropek i na koniec ciało pętli. I to wszystko.

Liczenie za pomocą pętli for Gdy piszesz program, może się okazać, że chcesz coś policzyć. Do liczenia na wszelkie możliwe sposoby możesz użyć funkcji Pythona range() w połączeniu z pętlą for.

Prezentacja programu Licznik Program Licznik nie zawiera nic specjalnego, ale pokazuje, jak można wykorzystać funkcję range() i pętlę for do liczenia w przód i w tył czy nawet z przeskakiwaniem liczb, jeśli tak Ci się spodoba. Rzuć okiem na rysunek 4.3, aby zobaczyć wyniki programu. Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 4.; nazwa pliku to licznik.py.

Liczenie za pomocą pętli for

Rysunek 4.3. Funkcja range() i pętla for umożliwiają liczenie do przodu, co pięć i do tyłu # Licznik # Demonstruje funkcję range() print("Liczenie:") for i in range(10): print(i, end=" ") print("\n\nLiczenie co pięć:") for i in range(0, 50, 5): print(i, end=" ") print("\n\nLiczenie do tyłu:") for i in range(10, 0, -1): print(i, end=" ") input("\n\nAby zakończyć program, naciśnij klawisz Enter.")

W świecie rzeczywistym Zmienne odgrywające rolę licznika lub używane do sterowania pętlą są tradycyjnie nazywane i, j lub k. Zwykle powinno się jednak tworzyć opisowe, zrozumiałe nazwy zmiennych. Możesz w to wierzyć lub nie, ale nazwy i, j i k są jasne dla doświadczonych programistów, którzy czytając Twój kod, wiedzą, że potrzebujesz tylko licznika na krótki użytek.

Liczenie do przodu Pierwsza pętla w programie liczy do przodu: for i in range(10): print(i, end=" ")

103

104

Rozdział 4. Pętle for, łańcuchy znaków i krotki. Gra Wymieszane litery

Ta pętla for działa tak samo jak pętla for, którą widziałeś w programie Zwariowany łańcuch. Ale tym razem sekwencja, po której iteruje pętla, jest generowana przez wartość zwrotną funkcji range(). Możesz sobie wyobrazić, że funkcja range() zwraca sekwencję liczb. Możesz więc sobie wyobrazić, że jeśli w wywołaniu przekażesz jej dodatnią liczbę całkowitą, zwróci sekwencję zaczynającą się od 0 i zawierającą kolejne liczby całkowite, aż do liczby stanowiącej argument wywołania z wyłączeniem tej ostatniej. Zatem możesz sobie wyobrazić, że kod range() zwraca sekwencję [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]. To powinno pomóc Ci w wizualizacji tego, co się dzieje w pętli: w pierwszej iteracji pętli zmienna i otrzymuje wartość 0, która jest następnie wypisywana, po czym w kolejnej iteracji i otrzymuje wartość 1, która też zostaje wypisana; ta sama procedura powtarza się w kolejnych iteracjach, aż do ostatniej, w której zmienna i otrzymuje wartość 9, jej nowa wartość zostaje wypisana i pętla się kończy.

Pułapka Chociaż wyobrażenie sobie wyniku funkcji range() jako sekwencji liczb całkowitych może być pomocne, to nie jest jednak to, co funkcja tworzy. W rzeczywistości funkcja zwraca w odpowiednim momencie kolejną liczbę z sekwencji. Wyobrażenie sobie całej sekwencji liczb zwróconej od razu w całości jest do naszych celów całkiem wystarczające. Jeśli chcesz się dowiedzieć więcej o wewnętrznych mechanizmach funkcji range(), zajrzyj do dokumentacji na stronie http://www.python.org.

Wskazówka Możesz utworzyć swoją własną sekwencję wartości, zwaną listą, poprzez zamknięcie tych wartości, oddzielonych przecinkami, w nawiasach. Ale nie przystępuj od razu do tworzenia list. Obiecuję, że wszystkiego o listach dowiesz się w rozdziale 5., „Listy i słowniki. Gra Szubienica”.

Liczenie co pięć Następna pętla liczy co pięć: for i in range(0, 50, 5): print(i, end=" ")

Jeśli wywołasz funkcję range() z trzema argumentami, zostaną one potraktowane jako punkt początkowy, punkt końcowy i liczba, wskazująca, co ile należy liczyć. Punkt początkowy oznacza zawsze pierwszą wartość w naszej wyimaginowanej sekwencji, podczas gdy punkt końcowy nigdy się w niej nie zawiera. Więc w tym przypadku nasza wyimaginowana sekwencja to [0, 5, 10, 15, 20, 25, 30, 35, 40, 45]. Zwróć uwagę, że sekwencja kończy się liczbą 45. Pamiętaj, że 50 to punkt końcowy, więc sekwencja go nie obejmuje. Jeśli chcesz do niej dołączyć liczbę 50, musisz wybrać punkt końcowy większy niż 50. Więc na przykład wywołanie o postaci range(0, 51, 5) byłoby dobrym na to sposobem.

Stosowanie funkcji i operatorów sekwencji do łańcuchów znaków

Liczenie do tyłu Ostatnia pętla w programie liczy do tyłu: for i in range(10, 0, -1): print(i, end=" ")

Robi tak, ponieważ ostatnim argumentem w wywołaniu funkcji range() jest -1. To sprawia, że funkcja przechodzi od punktu początkowego do końcowego dzięki dodawaniu w każdym kroku liczby –1 do poprzedniej wartości. To jest to samo co odejmowanie liczby 1. Możesz sobie wyobrazić, że wywołanie funkcji range() tworzy sekwencję [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]. Tak więc pętla liczy w dół od 10 do 1 i nie obejmuje liczby 0.

Sztuczka Nie istnieje żadne prawo, które nakazywałoby używanie zmiennej pętli wewnątrz pętli for. Mogłoby się zdarzyć, że chcesz powtórzyć pewną czynność określoną liczbę razy. Aby to zrobić, wystarczy, że utworzysz pętlę for i po prostu zignorujesz zmienną pętli w jej ciele. Powiedzmy na przykład, że chciałbym wyświetlić "Hej!" 10 razy. Wszystko, czego bym potrzebował, zawiera się w następujących dwóch wierszach kodu: for i in range(10): print("Hej!")

Stosowanie funkcji i operatorów sekwencji do łańcuchów znaków Jak się nieco wcześniej dowiedziałeś, łańcuchy stanowią jeden z typów sekwencji, składającej się z pojedynczych znaków. Python oferuje pewną liczbę użytecznych funkcji i operatorów, które działają na sekwencjach dowolnego rodzaju, nie wyłączając łańcuchów. Te operatory i funkcje mogą dostarczać podstawowe, niemniej ważne informacje o sekwencji, takie jak jej długość i to, czy zawiera określony element.

Prezentacja programu Analizator komunikatów Kolejny program analizuje dowolny komunikat, który wprowadzisz. Podaje długość wprowadzonego komunikatu oraz informację, czy zawiera on literę, która występuje w języku polskim najczęściej (literę „a”). Program wykonuje to zadanie za pomocą nowej funkcji i nowego operatora, które działają na sekwencjach. Uruchomienie tego programu pokazano na rysunku 4.4. Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 4.; nazwa pliku to analizator_komunikatow.py.

105

106

Rozdział 4. Pętle for, łańcuchy znaków i krotki. Gra Wymieszane litery

Rysunek 4.4. Ten program używa funkcji len() oraz operatora in do wyświetlenia pewnych informacji o Twoim komunikacie # Analizator komunikatów # Demonstruje funkcję len() i operator in message = input("Wprowadź komunikat: ") print("\nDługość Twojego komunikatu wynosi:", len(message)) print("\nNajczęściej używana litera w języku polskim, 'a',") if "a" in message: print("wystąpiła w Twoim komunikacie.") else: print("nie wystąpiła w Twoim komunikacie.") input("\n\nAby zakończyć program, naciśnij klawisz Enter.")

Użycie funkcji len() Po wczytaniu komunikatu użytkownika program wyświetla informację o długości komunikatu za pomocą instrukcji: print("\nDługość Twojego komunikatu wynosi:", len(message))

Można przekazać do funkcji len() dowolną sekwencję, a funkcja zwróci jej długość. Długość sekwencji to liczba jej elementów. Ponieważ komunikat przypisany do zmiennej message zawiera 11 znaków (licząc każdy znak, łącznie ze spacją i wykrzyknikiem), jego długość wynosi 11.

Indeksowanie łańcuchów

Użycie operatora in Litera „a” jest najczęściej używaną literą w języku polskim. Do sprawdzenia, czy „a” jest zawarte w komunikacie wprowadzonym przez użytkownika, program wykorzystuje kod zawarty w następujących wierszach: if "a" in message: print("wystąpiła w Twoim komunikacie.") else: print("nie wystąpiła w Twoim komunikacie.")

Warunek w instrukcji if ma postać "a" in message. Jeśli zmienna message zawiera znak "a", warunek jest prawdziwy. Jeśli zmienna message nie zawiera "a", jest fałszywy. W przykładowym uruchomieniu programu wartością zmiennej message jest łańcuch "Koniec gry!", który nie zawiera litery "a". Więc warunek "a" in message przyjął wartość False i komputer wypisał zawartość łańcucha "nie wystąpiła w Twoim komunikacie.". Gdyby warunek był fałszywy (gdyby na przykład wartością zmiennej message był łańcuch "Programowanie w Pythonie"), komputer wyświetliłby tekst wystąpiła w Twoim komunikacie. Jeśli jakiś element jest zawarty w sekwencji, nazywany jest składnikiem tej sekwencji. Operatora in możesz użyć we własnych programach do sprawdzenia, czy jakiś element jest składnikiem sekwencji. Wystarczy po elemencie, który chcesz zbadać, umieścić operator in, a po nim samą sekwencję. W ten sposób utworzysz warunek. Jeżeli ten element jest składnikiem sekwencji, warunek jest prawdziwy; w przeciwnym wypadku jest fałszywy.

Indeksowanie łańcuchów Dzięki użyciu pętli for możesz przemieszczać się wzdłuż całego łańcucha znak po znaku, zgodnie z ich kolejnością. Nazywa się to dostępem sekwencyjnym, który oznacza, że musisz przechodzić przez sekwencję element po elemencie. Dostęp sekwencyjny przypomina postępowanie ze stosem skrzynek tak ciężkich, że można podnieść tylko jedną naraz. Aby się dostać do skrzynki znajdującej się na samym dole stosu zawierającego pięć skrzynek, musiałbyś zdjąć górną skrzynkę, potem kolejną, po niej następną i jeszcze jedną, by w końcu dotrzeć do ostatniej. Czyż nie byłoby przyjemnie chwycić ostatnią skrzynkę bez oglądania się na wszystkie pozostałe? Ten rodzaj bezpośredniego dostępu nazywa się dostępem swobodnym. Pozwala on dostać się bezpośrednio do dowolnego elementu sekwencji. Na szczęście istnieje sposób na swobodny dostęp do elementów sekwencji. Nazywa się indeksowaniem. Poprzez indeksowanie specyfikujesz numer pozycji (czyli indeks) w sekwencji i uzyskujesz dostęp do elementu znajdującego się na tej pozycji. W przykładzie ze skrzynkami mógłbyś dostać się do dolnej skrzynki bezpośrednio poprzez zażądanie skrzynki numer pięć.

107

108

Rozdział 4. Pętle for, łańcuchy znaków i krotki. Gra Wymieszane litery

Prezentacja programu Dostęp swobodny Program Dostęp swobodny wykorzystuje indeksowanie sekwencji w celu uzyskania bezpośredniego dostępu do losowo wybranych znaków łańcucha. Wybiera losowo pozycję w łańcuchu "indeks" i wypisuje literę o tym numerze pozycji. Robi to 10 razy, aby uzyskać dobrą próbę losową pozycji. Na rysunku 4.5 pokazano ten program w działaniu.

Rysunek 4.5. Dzięki indeksowaniu można uzyskać bezpośredni dostęp do dowolnego znaku w łańcuchu

Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 4.; nazwa pliku to dostep_swobodny.py. # Dostęp swobodny # Demonstruje indeksowanie łańcucha znaków import random word = "indeks" print("Wartość zmiennej word to: ", word, "\n") high = len(word) low = -len(word) for i in range(10): position = random.randrange(low, high) print("word[", position, "]\t", word[position]) input("\n\nAby zakończyć program, naciśnij klawisz Enter.")

Indeksowanie łańcuchów

Używanie dodatnich numerów pozycji Jedną z pierwszych rzeczy, które robię w tym programie, jest przypisanie wartości łańcuchowej do zmiennej: word = "indeks"

Nie ma w tym nic nowego. Ale robiąc to, tworzę sekwencję (podobnie jak za każdym razem, gdy tworzę łańcuch), w której każdy znak zajmuje pozycję o określonym numerze. Pierwsza litera, „i”, zajmuje pozycję 0. (Pamiętaj, że komputery zwykle rozpoczynają liczenie od 0). Druga litera, „n”, zajmuje pozycję 1. Trzecia litera, „d”, pozycję 2 itd. Uzyskanie dostępu do pojedynczego znaku łańcucha jest proste. Aby uzyskać dostęp do litery zajmującej pozycję 0, wystarczy napisać word[0]. W przypadku dowolnej innej pozycji powinieneś podstawić jej numer w miejsce 0. Pomóc Ci w utrwaleniu tej koncepcji powinno przyjrzenie się fragmentowi mojej interaktywnej sesji: >>> >>> i >>> n >>> d >>> e >>> k >>> s

word = "indeks" print(word[0]) print(word[1]) print(word[2]) print(word[3]) print(word[4]) print(word[5])

Pułapka Ponieważ w łańcuchu "indeks" występuje sześć liter, mógłbyś pomyśleć, że ostatnia litera, „s”, zajmuje pozycję 6. Ale nie miałbyś racji. W tym łańcuchu nie ma pozycji 6, ponieważ komputer zaczyna liczenie od 0. Prawidłowe dodatnie pozycje to 0, 1, 2, 3, 4 i 5. Każda próba dostępu do pozycji 6 spowoduje wystąpienie błędu. Aby uzyskać potwierdzenie tego faktu, spójrz na poniższą sesję interaktywną: >>> word = "indeks" >>> print(word[6]) Traceback (most recent call last): File "", line 1, in print(word[6]) IndexError: string index out of range

Trochę niegrzecznie komputer komunikuje, że pozycja 6 nie istnieje. Więc zapamiętaj — ostatni element w sekwencji zajmuje pozycję o numerze mniejszym o jeden od jej długości.

109

110

Rozdział 4. Pętle for, łańcuchy znaków i krotki. Gra Wymieszane litery

Używanie ujemnych numerów pozycji Z wyjątkiem koncepcji, że pierwsza litera łańcucha zajmuje pozycję 0, a nie 1, korzystanie z dodatnich numerów pozycji wydaje się dość naturalne. Lecz istnieje również sposób dostępu do elementów sekwencji poprzez ujemne numery pozycji. Przy dodatnich numerach pozycji punktem odniesienia jest początek sekwencji. W przypadku łańcuchów to oznacza, że liczenie pozycji rozpoczyna się od pierwszego znaku. Lecz przy ujemnych numerach pozycji liczenie rozpoczyna się od końca. W przypadku łańcuchów oznacza to, że liczenie należy rozpocząć od ostatniego znaku i posuwać się do tyłu. Najlepszym sposobem na zrozumienie, jak funkcjonują ujemne numery pozycji, jest zobaczenie przykładu. Przyjrzyj się jeszcze jednej sesji interaktywnej w moim wykonaniu, wykorzystującej ponownie łańcuch "indeks": >>> >>> s >>> k >>> e >>> d >>> n >>> i

word = "indeks" print(word[-1]) print(word[-2]) print(word[-3]) print(word[-4]) print(word[-5]) print(word[-6])

Możesz zaobserwować w tej sesji, że wyrażenie word[-1] oznacza dostęp do ostatniej litery łańcucha "indeks", którą jest „s”. Kiedy używa się ujemnych numerów pozycji, –1 wskazuje ostatni element, indeks –2 — przedostatni element, indeks –3 — trzeci element od końca itd. Czasem sensowniejszy jest wybór końca sekwencji jako punktu odniesienia. W takich sytuacjach możesz wykorzystać ujemne numery pozycji. Rysunek 4.6 przedstawia w klarowny sposób łańcuch "indeks" rozbity na pojedyncze znaki, którym odpowiadają numery pozycji, zarówno dodatnie, jak i ujemne.

Rysunek 4.6. Możesz uzyskać dostęp do dowolnej litery łańcucha "indeks", wykorzystując dodatni lub ujemny numer pozycji

Dostęp do wybranego losowo elementu łańcucha Pora na powrót do programu Dostęp swobodny. Aby uzyskiwać dostęp do wybranej losowo litery z łańcucha "indeks", muszę generować liczby losowe. Dlatego pierwszą rzeczą, jaką zrobiłem w programie, był import modułu random: import random

Niemutowalność łańcuchów

Następnie potrzebowałem sposobu na wybieranie dowolnego prawidłowego numeru pozycji w łańcuchu przypisanym do zmiennej word — ujemnego lub dodatniego. Chciałem, aby mój program potrafił wygenerować liczbę losową z zakresu od –6 do 4 włącznie, ponieważ są to wszystkie możliwe wartości pozycji w łańcuchu word. Na szczęście funkcja random.randrange() może przyjąć jako argumenty dwa punkty końcowe i wygenerować liczbę losową mieszczącą się między nimi. Więc utworzyłem dwa punkty końcowe: high = len(word) low = -len(word)

Zmienna high otrzymuje wartość 6, ponieważ łańcuch "indeks" zawiera sześć znaków. Zmienna low otrzymuje ujemną wartość przeciwną do długości łańcucha word (wynika to z umieszczenia znaku minusa przed wartością liczbową). Tak więc zmiennej low zostaje przypisana wartość –6. Te dwie wartości reprezentują przedział, z którego chcę wybrać liczbę losową. Właściwie chcę wygenerować liczbę losową mieszczącą się w zakresie od –6 do 6, z włączeniem pierwszej, lecz wyłączeniem drugiej wartości. I właśnie dokładnie w ten sposób działa funkcja random.randrange(). Jeśli przekażesz jej dwa argumenty, zwróci liczbę losową z przedziału przez nie wyznaczonego, który zawiera swój dolny koniec, lecz nie zawiera górnego. Dlatego w moim przykładowym uruchomieniu wiersz: position = random.randrange(low, high)

generuje jedną z liczb: –6, –5, –4, –3, –2, –1, 0, 1, 2, 3, 4 lub 5. Dokładnie o to mi chodzi, ponieważ są to wszystkie możliwe prawidłowe numery pozycji odnoszące się do łańcucha "indeks". Na koniec utworzyłem pętlę for, która wykonuje się 10 razy. W ciele pętli program wybiera losowo numer pozycji oraz wyświetla jego wartość i odpowiadającą mu literę: for i in range(10): position = random.randrange(low, high) print("word[", position, "]\t", word[position])

Niemutowalność łańcuchów Sekwencje należą do dwóch kategorii — mogą być mutowalne lub niemutowalne. (Znów kolejne określenia z komputerowego żargonu). Mutowalny oznacza podlegający zmianom. Tak więc sekwencja, która jest mutowalna, jest taką sekwencją, która może się zmieniać. Niemutowalny oznacza niezmienny. Dlatego też sekwencja niemutowalna to taka sekwencja, która nie może się zmieniać. Łańcuchy znaków są sekwencjami niemutowalnymi, co oznacza, że nie mogą się zmieniać. Więc na przykład łańcuch "Koniec gry!" zawsze pozostanie łańcuchem "Koniec gry!". Nie można go zmienić. W rzeczywistości nie możesz zmienić jakiegokolwiek łańcucha, który utworzysz. Teraz mógłbyś pomyśleć na podstawie swojego doświadczenia z łańcuchami, że w tej kwestii

111

112

Rozdział 4. Pętle for, łańcuchy znaków i krotki. Gra Wymieszane litery

całkowicie się mylę. Mógłbyś nawet uruchomić sesję interaktywną, żeby udowodnić, że możesz zmienić łańcuch. Mogłaby ona przypominać coś takiego: >>> name = "Jaś" >>> print(name) Jaś >>> name = "Małgosia" >>> print(name) Małgosia

Mógłbyś to przedstawić jako dowód na to, że można zmienić łańcuch. Jakkolwiek by było, zmieniłeś łańcuch "Jaś" na "Małgosia". Lecz nie zmieniłeś ani jednego łańcucha w tej sesji. Utworzyłeś jedynie dwa różne łańcuchy. Najpierw utworzyłeś łańcuch "Jaś" i przypisałeś go do zmiennej name. Potem utworzyłeś kolejny łańcuch, "Małgosia", i przypisałeś go znów do zmiennej name. Otóż zarówno „Jaś”, jak i „Małgosia” to wspaniałe imiona, ale są to różne imiona i zawsze takimi pozostaną, tak jak są i zawsze będą różnymi łańcuchami. Popatrz na rysunek 4.7 przedstawiający w wizualnej postaci to, co zdarzyło się w trakcie sesji interaktywnej.

Rysunek 4.7. Najpierw do zmiennej imię zostaje przypisany łańcuch "Jaś", potem zmienna otrzymuje nową wartość w postaci łańcucha "Małgosia", ale same łańcuchy nie ulegają nigdy zmianie

Innym sposobem zobrazowania tej myśli jest wyobrażenie sobie, że łańcuchy zostały zapisane atramentem na karteczkach papieru. Można wyrzucić karteczkę z zapisanym na niej łańcuchem lub zamienić ją na inną karteczkę papieru z nowym łańcuchem, ale nie można zmienić samych słów, kiedy już zostały napisane. Mógłbyś pomyśleć, że robi się wiele hałasu o nic. Co z tego, że łańcuch jest niemutowalny? Lecz niemutowalność łańcucha ma swoje konsekwencje. Ponieważ nie można zmienić łańcucha, nie można przypisać do łańcucha nowego znaku poprzez indeksowanie. Oto sesja interaktywna, która powinna pokazać, co mam na myśli: >>> word = "gama" >>> word[0] = "l" Traceback (most recent call last): File "", line 1, in word[0] = "l" TypeError: 'str' object does not support item assignment

W tej sesji chciałem zamienić łańcuch "gama" na łańcuch "lama". Potrzebowałem tylko zamienić literę „g” na „l”. Toteż próbowałem przypisać "l" do pierwszej pozycji

Tworzenie nowego łańcucha

w łańcuchu, word[0]. Lecz jak widać, skończyło się to wygenerowaniem opasłego komunikatu o błędzie. Interpreter informuje mnie nawet, że łańcuchy nie obsługują przypisania do własnego elementu (nie można przypisać nowej wartości znakowi łańcucha). Ale to, że nie można zmieniać łańcucha, nie oznacza, iż nie można tworzyć nowych łańcuchów z już istniejących.

Tworzenie nowego łańcucha Już dowiedziałeś się, jak można dokonać konkatenacji dwóch łańcuchów za pomocą operatora +. Czasem możesz chcieć skonstruować nowy łańcuch znak po znaku. Ponieważ łańcuchy są niemutowalne, tym, co naprawdę będziesz robił, jest tworzenie nowego łańcucha za każdym razem, gdy będziesz używał operatora konkatenacji.

Prezentacja programu Bez samogłosek Kolejny program, Bez samogłosek, pobiera komunikat od użytkownika i wypisuje go z pominięciem wszystkich samogłosek. Na rysunku 4.8 pokazano ten program w działaniu.

Rysunek 4.8. Przy użyciu pętli for są tworzone nowe łańcuchy. Program pomija operację konkatenacji przy wszystkich samogłoskach

Program tworzy na podstawie oryginalnego komunikatu nowy łańcuch pozbawiony samogłosek. W rzeczywistości tworzy całą serię nowych łańcuchów. Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 4.; nazwa pliku to bez_samoglosek.py. # Bez samogłosek # Demonstruje tworzenie nowych łańcuchów przy użyciu pętli for

113

114

Rozdział 4. Pętle for, łańcuchy znaków i krotki. Gra Wymieszane litery message = input("Wprowadź komunikat: ") new_message = "" VOWELS = "aąeęioóuy" print() for letter in message: if letter.lower() not in VOWELS: new_message += letter print("Został utworzony nowy łańcuch: ", new_message) print("\nTwój komunikat bez samogłosek to:", new_message) input("\n\nAby zakończyć program, naciśnij klawisz Enter.")

Tworzenie stałych Po pobraniu komunikatu od użytkownika i utworzeniu pustego nowego komunikatu program tworzy łańcuch: VOWELS = "aąeęioóuy"

Do zmiennej VOWELS zostaje przypisany łańcuch wszystkich samogłosek. Prawdopodobnie zauważyłeś, że nazwa zmiennej składa się z samych dużych liter na przekór temu, czego się wcześniej dowiedziałeś — nazwy zmiennych są tradycyjnie pisane małymi literami. Cóż, nie odszedłem w tym przypadku od tradycji. W rzeczywistości nazwom zmiennych pisanym samymi dużymi literami przypisuje się specjalne znaczenie. Nazywają się stałymi i odwołują się do wartości, która zgodnie z zamysłem nie powinna się zmienić (ich wartość jest stała). Stałe są cenne dla programistów z dwóch względów. Po pierwsze, sprawiają, że programy są bardziej przejrzyste. W tym programie wykorzystuję nazwę zmiennej VOWELS wszędzie, gdzie potrzebuję użyć sekwencji wszystkich samogłosek, zamiast łańcucha "aąeęioóuy". Użycie nazwy zmiennej zamiast łańcucha sprawia, że kod jest bardziej zrozumiały. Kiedy widzisz nazwę zmiennej, rozumiesz, co ona oznacza, podczas gdy sam dziwnie wyglądający łańcuch mógłby Cię nieco dezorientować. Po drugie, stosowanie stałych pozwala uniknąć przepisywania wartości (oraz być może błędów wynikających z omyłek przy ich przepisywaniu). Stałe są szczególnie użyteczne, kiedy masz do czynienia z taką wartością, jak bardzo długa liczba lub łańcuch znaków. Wykorzystuj stałą w programach, w których tej samej, niezmiennej wartości musisz używać w wielu miejscach.

Pułapka Musisz być ostrożny, kiedy tworzysz stałe z wykorzystaniem nazwy zmiennej składającej się z samych dużych liter. Jeśli nawet powiesz sam sobie i innym programistom, że ta zmienna zawsze będzie się odnosić do tej samej wartości, to nie istnieje w Pythonie żaden mechanizm, który Cię powstrzyma przed zmianą jej wartości w Twoim programie. Ta praktyka nazewnicza jest po prostu konwencją. Więc jeśli już utworzysz zmienną o nazwie złożonej z samych dużych liter, pamiętaj o traktowaniu jej jako wartości niepodlegającej zmianie.

Tworzenie nowego łańcucha

W świecie rzeczywistym W niektórych językach programowania stałe są dokładnie tym, co oznaczają. Nie mogą zostać zmienione po ich zdefiniowaniu. Jest to najbezpieczniejszy sposób tworzenia i używania stałych. Jednak w Pythonie nie istnieje prosty sposób tworzenia własnych prawdziwych stałych.

Tworzenie nowych łańcuchów z już istniejących Prawdziwa praca programu wykonuje się w pętli. W trakcie wykonywania pętli program tworzy nowy komunikat niezawierający żadnych samogłosek. Przy każdym przebiegu komputer sprawdza kolejną literę w oryginalnym komunikacie. Jeśli nie jest to samogłoska, dodaje tę literę do nowo tworzonego komunikatu. Jeśli jest to samogłoska, program przechodzi do następnej litery. Wiesz już, że program nie może w sensie dosłownym dodać litery do łańcucha, więc dokładniej mówiąc, kiedy program napotyka znak, który nie jest samogłoską, dokonuje konkatenacji dotychczasowego nowego komunikatu z tym znakiem, by utworzyć w ten sposób nowy łańcuch. Kod, który to realizuje, wygląda następująco: for letter in message: if letter.lower() not in VOWELS: new_message += letter print("Został utworzony nowy łańcuch: ", new_message)

Pętla zawiera dwie nowe koncepcje, więc pozwól, że omówię obydwie. Po pierwsze, Python jest grymaśny, gdy ma do czynienia z łańcuchami i znakami. Litera "A" to nie to samo co "a". Ponieważ stałej VOWELS został przypisany łańcuch, który zawiera tylko małe litery, musiałem uzyskać pewność, że przy użyciu operatora in sprawdzam tylko małe litery. Właśnie dlatego użyłem metody letter.lower().

Sztuczka Kiedy porównujesz dwa łańcuchy, często chciałbyś zignorować różnice wynikające tylko z użycia małych lub dużych liter. Jeśli zapytasz gracza, czy chce kontynuować grę, łańcuch "Tak" jest równie dobrą odpowiedzią jak łańcuch "tak". Cóż, w tych przypadkach musisz pamiętać o zamianie w obu łańcuchach wszystkich dużych liter na małe (możesz postąpić odwrotnie — nie ma to żadnego znaczenia) przed ich porównaniem.

Oto przykład. Powiedzmy, że chcę porównać dwa łańcuchy dostępne poprzez zmienne name i winner, aby sprawdzić, czy są równe, i nie interesuje mnie zgodność wielkości liter. Mógłbym utworzyć warunek: name.lower() == winner.lower()

Ten warunek jest prawdziwy, jeśli łańcuchy name i winner zawierają ten sam ciąg znaków przy zignorowaniu wielkości liter. Więc łańcuchy "Marek" i "marek" są zgodne. Łańcuchy "MAREK" i "marek" — także. Nawet porównanie zgodności łańcuchów "MaReK" i "mArEk" daje pozytywny wynik.

115

116

Rozdział 4. Pętle for, łańcuchy znaków i krotki. Gra Wymieszane litery

Po drugie, mogłeś również zauważyć, że użyłem w programie operatora rozszerzonego przypisania (+=) do konkatenacji łańcuchów. Spotkałeś się z użyciem operatorów rozszerzonego przypisania w kontekście liczb, ale ich zastosowanie obejmuje także łańcuchy. Więc wiersz: new_message += letter

oznacza dokładnie to samo co: new_message = new_message + letter

Wycinanie łańcuchów Indeksowanie jest pożyteczną techniką, ale nie jesteś ograniczony do kopiowania za każdym razem tylko jednego elementu z sekwencji. Możesz wykonywać kopie ciągłych fragmentów sekwencji (zwanych wycinkami). Możesz skopiować (czyli wyciąć) jeden element (tak jak przy indeksowaniu) lub część sekwencji (jak na przykład środkowe trzy elementy). Możesz nawet utworzyć wycinek, który jest kopią całej sekwencji. Więc w przypadku łańcuchów możesz przechwycić wszystko: od pojedynczego znaku, poprzez grupę kolejnych znaków, do całego łańcucha.

Prezentacja programu Krajacz pizzy Program Krajacz pizzy pozwala Ci tworzyć wycinki łańcucha "pizza" w dowolny sposób. Jest to wspaniała, interaktywna metoda, która może Ci pomóc w zrozumieniu wycinania. Do Ciebie należy tylko wprowadzenie początkowej i końcowej pozycji wycinka, a program wyświetli wynik. Program został pokazany na rysunku 4.9. Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 4.; nazwa pliku to krajacz_pizzy.py. # Krajacz pizzy # Demonstruje tworzenie wycinków łańcucha word = "pizza" print( """ 'Ściągawka' tworzenia wycinków 0 1 2 3 4 5 +---+---+---+---+---+ | p | i | z | z | a | +---+---+---+---+---+ -5 -4 -3 -2 -1 """ )

Wycinanie łańcuchów

Rysunek 4.9. Świeże i gorące wycinki łańcucha "pizza" zostały utworzone zgodnie z Twoim życzeniem. Program dodatkowo oferuje „ściągawkę”, która pomoże Ci w wizualizacji sposobu tworzenia wycinka print("Wprowadź początkowy i końcowy indeks wycinka łańcucha 'pizza'.") print("Aby zakończyć tworzenie wycinków, w odpowiedzi na monit 'Początek:'\n" + "naciśnij klawisz Enter.") start = None while start != "": start = (input("\nPoczątek: ")) if start: start = int(start) finish = int(input("Koniec: ")) print("word[", start, ":", finish, "] to", end=" ") print(word[start:finish]) input("\n\nAby zakończyć program, naciśnij klawisz Enter.")

117

118

Rozdział 4. Pętle for, łańcuchy znaków i krotki. Gra Wymieszane litery

Wartość None Zanim przejdziesz do kodu dotyczącego tworzenia wycinków, popatrz na ten wiersz, który prezentuje nową koncepcję: start = None

W wierszu tym zmiennej start zostaje przypisana specjalna wartość o nazwie None. Jest ona przyjętym w Pythonie sposobem reprezentowania pojęcia „nic” oraz dobrym symbolem zastępczym. Traktowana jako warunek jest równoważna wartości False. Użyłem jej w tym miejscu programu, ponieważ chciałem zainicjalizować zmienną start przed użyciem jej w warunku pętli while.

Wycinanie Tworzenie wycinka jest podobne do indeksowania. Ale zamiast używać pojedynczego numeru pozycji, należy podać pozycję początkową i pozycję końcową. Każdy element położony między tymi dwoma punktami staje się częścią wycinka. Rysunek 4.10 pokazuje sposób postrzegania numerów punktów granicznych wycinka na przykładzie łańcucha "pizza". Zwróć uwagę, że jest to nieco inny system numeracji niż indeksowanie przedstawione na rysunku 4.6. Aby zdefiniować punkty graniczne wycinka, zamknij obydwa w nawiasach, oddzielając je dwukropkiem. Oto krótka sesja interaktywna, która powinna pokazać, co mam na myśli:

Rysunek 4.10. Przykład numerów punktów granicznych wycinania wykorzystujący łańcuch "pizza". Do zdefiniowania swojego wycinka możesz użyć dowolnej kombinacji dodatnich i ujemnych punktów granicznych >>> word = "pizza" >>> print(word[0:5]) pizza >>> print(word[1:3]) iz >>> print(word[-4:-2]) iz >>> print(word[-4:3]) iz

Wycinanie łańcuchów

Wyrażenie word[0:5] zwraca cały łańcuch, ponieważ wszystkie jego znaki znajdują się między tymi dwoma punktami granicznymi. Natomiast word[1:3] zwraca łańcuch "iz", ponieważ te dwa znaki znajdują się między punktami granicznymi. Podobnie jak w przypadku indeksowania, możesz używać ujemnych numerów. Wyrażenie word[-4:-2] także tworzy łańcuch "iz", ponieważ te właśnie znaki znajdują się między wskazanymi ujemnymi numerami pozycji. Możesz również dobierać mieszane, dodatnie i ujemne punkty graniczne. Działa to jak tworzenie dowolnego innego wycinka; znajdą się w nim elementy położone między tymi dwoma numerami pozycji. Tak więc wyrażenie word[-4:3] także tworzy łańcuch "iz", ponieważ te dwa znaki znajdują się między tymi dwoma punktami granicznymi.

Pułapka Jeśli spróbujesz utworzyć wycinek „niemożliwy”, w którym punkt początkowy jest większy niż punkt końcowy, taki jak word[2:1], nie spowodujesz błędu. Zamiast tego Python po cichu zwróci pustą sekwencję. W przypadku łańcuchów oznacza to, że otrzymasz pusty łańcuch. Więc zachowaj ostrożność, ponieważ nie jest to prawdopodobnie rodzaj wyniku, który chciałbyś uzyskać.

Tworzenie wycinków Wewnątrz swojej pętli, program Krajacz pizzy wypisuje składnię tworzenia wycinka na podstawie wprowadzonych przez użytkownika pozycji — początkowej i końcowej, używając następującego wiersza kodu: print("word[", start, ":", finish, "] to", end=" ")

Potem program wypisuje właściwy wycinek przy użyciu zmiennych start i finish: print(word[start:finish])

Korzystanie ze skrótów przy wycinaniu Chociaż można otrzymać każdy możliwy wycinek przez podanie dwóch numerów, istnieje kilka skrótów stosowanych przy wycinaniu, które możesz wykorzystać. Możesz opuścić punkt początkowy w specyfikacji wycinka, sprawiając, że będzie nim początek sekwencji. Więc zakładając, że do zmiennej word został przypisany łańcuch "pizza", wycinek word[:4] jest dokładnie takim samym wycinkiem jak word[0:4]. Możesz opuścić punkt końcowy, aby wycinek kończył się ostatnim elementem łańcucha word. Więc word[2:] jest skrótem wyrażenia word[2:5]. Możesz nawet opuścić obydwa numery, aby otrzymać wycinek, który będzie całą sekwencją. Tak więc word[:] jest skrótem wyrażenia word[0:5]. Na poparcie tego twierdzenia przedstawiam sesję interaktywną: >>> word = "pizza" >>>print(word[0:4]) pizz >>>print(word[:4])

119

120

Rozdział 4. Pętle for, łańcuchy znaków i krotki. Gra Wymieszane litery pizz >>>print(word[2:5]) zza >>>print(word[2:]) zza >>>print(word[0:5]) pizza >>> print(word[:]) Pizza

Sztuczka Jeśli istnieje jeden taki element wiedzy o skrótach stosowanych przy wycinaniu, który powinieneś zapamiętać, to jest nim fakt, że [:] zwraca całkowitą kopię sekwencji. Kiedy będziesz programował, może się okazać, że będziesz chciał wykonać kopię jakiejś sekwencji, a to jest szybki i efektywny sposób realizacji tego zadania.

Tworzenie krotek Krotki są typem sekwencji, podobnie jak łańcuchy. W odróżnieniu od łańcuchów, które mogą zawierać tylko znaki, krotki mogą zawierać elementy dowolnego typu. To oznacza, że możesz mieć krotkę, która przechowuje pewną liczbę najlepszych wyników uzyskanych w grze, lub taką, która przechowuje grupę nazwisk pracowników. Ale nie wszystkie elementy krotki muszą być tego samego typu. Gdybyś chciał, mógłbyś utworzyć krotkę, która zawiera zarówno łańcuchy znaków, jak i liczby. I nie musisz się zatrzymywać na samych łańcuchach czy liczbach. Możesz utworzyć krotkę, która zawiera sekwencję obrazów graficznych, plików dźwiękowych lub nawet grupę przybyszów z kosmosu (kiedy już dowiesz się, jak tworzyć takie rzeczy, co stanie się w trakcie lektury późniejszych rozdziałów). Wszystko, co możesz przypisać do zmiennej, możesz zgrupować i przechować jako sekwencję w krotce.

Prezentacja programu Inwentarz bohatera Program Inwentarz bohatera obsługuje rejestr wyposażenia bohatera z typowej gry z podziałem na role. Podobnie jak w większości gier, jakie kiedykolwiek powstały, bohater pochodzi z małej, mało znaczącej wioski. Jego ojciec został oczywiście zabity przez złego watażkę. (Co to za misja bez martwego ojca?). A teraz, kiedy bohater doszedł do pełnoletności, nadszedł dla niego czas szukania zemsty. W tym programie Inwentarz bohatera jest reprezentowany przez krotkę. Krotka zawiera łańcuchy znaków odpowiadające poszczególnym elementom dobytku bohatera. Bohater zaczyna od niczego, ale potem daję mu kilka przedmiotów. Rysunek 4.11 pokazuje skromne początki podróży naszego bohatera.

Wycinanie łańcuchów

Rysunek 4.11. Początkowo wykaz wyposażenia bohatera nie zawiera żadnych pozycji. Potem program tworzy nową krotkę z elementami w postaci łańcuchów znaków i nasz bohater zostaje wyekwipowany

Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 4.; nazwa pliku to inwentarz_bohatera.py. # Inwentarz bohatera # Demonstruje tworzenie krotek # utwórz pustą krotkę inventory = () # potraktuj krotkę jako warunek if not inventory: print("Nie posiadasz niczego.") input("\nAby kontynuować misję, naciśnij klawisz Enter.") # utwórz krotkę zawierającą pewne elementy inventory = ("miecz", "zbroja", "tarcza", "napój uzdrawiający") # wyświetl krotkę print("\nWykaz zawartości krotki:") print(inventory) # wyświetl każdy element krotki print("\nElementy Twojego wyposażenia:") for item in inventory: print(item) input("\n\nAby zakończyć program, naciśnij klawisz Enter.")

121

122

Rozdział 4. Pętle for, łańcuchy znaków i krotki. Gra Wymieszane litery

Tworzenie pustej krotki Aby utworzyć krotkę, wystarczy ująć w nawiasy sekwencję wartości oddzielonych przecinkami. Nawet para samotnych nawiasów oznacza prawidłową (chociaż pustą) krotkę. Utworzyłem pustą krotkę w pierwszej części programu, która ma odzwierciedlać fakt, że bohater nie posiada niczego: inventory = ()

Jest to aż tak proste. Więc w powyższym wierszu zmienna inventory otrzymuje wartość w postaci pustej krotki.

Traktowanie krotki jako warunku Kiedy poznawałeś warunki, dowiedziałeś się, że w Pythonie każdą wartość możesz traktować jako warunek. To oznacza, że jako warunek możesz potraktować także krotkę. I właśnie to zrobiłem w kolejnych wierszach kodu: if not inventory: print("Masz puste ręce.")

Jako warunek pusta krotka ma wartość False. To oznacza, że wyrażenie not inventory przyjmuje wartość True. Więc komputer wyświetla łańcuch znaków "Masz puste ręce.", dokładnie tak, jak powinien.

Tworzenie krotki zawierającej elementy Nieuzbrojony bohater jest nudnym bohaterem. Utworzyłem zatem nową krotkę z elementami w postaci łańcuchów reprezentujących użyteczne dla naszego bohatera przedmioty. Przypisałem tę nową krotkę do zmiennej inventory za pomocą następującej instrukcji: inventory = ("miecz", "zbroja", "tarcza", "napój uzdrawiający")

Każdy element krotki jest oddzielony od sąsiedniego przecinkiem. To sprawia, że pierwszym elementem zostaje łańcuch "miecz", drugim "zbroja", kolejnym "tarcza" i ostatnim "napój uzdrawiający". Więc każdy łańcuch stanowi w tej krotce pojedynczy element. Zwróć także uwagę, że krotka obejmuje kilka wierszy. Możesz zapisać krotkę w jednym wierszu lub, tak jak ja zrobiłem, rozciągnąć ją na wiele wierszy, pod warunkiem że każdy wiersz (z wyjątkiem ostatniego) zakończysz przecinkiem. Jest to jeden z kilku przypadków, w których Python pozwala rozbić instrukcję na wiele wierszy.

Wycinanie łańcuchów

Sztuczka Zapisuj krotki w wielu wierszach, aby Twoje programy stały się czytelniejsze. Jednak nie musisz umieszczać dokładnie jednego elementu w każdym wierszu. Zapisywanie kilku elementów w jednym wierszu też może okazać się sensowne. Pamiętaj tylko, żeby każdy wiersz kończył się przecinkiem oddzielającym elementy, a wszystko będzie dobrze.

Wypisywanie krotki Chociaż krotka może zawierać wiele elementów, możesz wypisać całą krotkę w taki sam sposób jak dowolną pojedynczą wartość. To właśnie zrobiłem w kolejnym wierszu kodu: print("\nWykaz zawartości krotki:") print(inventory)

Komputer wyświetla wszystkie elementy krotki, ujmując całość w nawiasy.

Przechodzenie w pętli przez elementy krotki Na koniec napisałem pętlę for, aby maszerować przez kolejne elementy krotki inventory i wyświetlać każdy z nich osobno: print("\nElementy Twojego wyposażenia:") for item in inventory: print(item)

Ta pętla wypisuje każdy element (każdy łańcuch) krotki inventory w oddzielnym wierszu. Wygląda ona dokładnie tak samo jak pętle, z którymi się spotkałeś przy obsłudze łańcuchów. W gruncie rzeczy możesz używać tego rodzaju pętli do przechodzenia przez elementy dowolnej sekwencji. Chociaż utworzyłem krotkę, w której wszystkie elementy są tego samego typu (w tym przypadku łańcuchy znaków), krotki nie muszą być wypełnione elementami tego samego typu. Pojedyncza krotka może na przykład zawierać i łańcuchy znaków, i liczby całkowite, i liczby zmiennoprzecinkowe.

Pułapka Inne języki programowania oferują struktury podobne do krotek. Niektóre noszą nazwę tablic lub wektorów. Jednak te inne języki ograniczają zwykle elementy tych sekwencji do jednego typu. Więc na przykład nie mógłbyś łączyć ze sobą łańcuchów i liczb. Chodzi tylko o to, byś miał świadomość, że te inne struktury nie oferują zwykle całej elastyczności charakteryzującej sekwencje Pythona.

Wykorzystywanie krotek Ponieważ krotki są po prostu jeszcze jednym rodzajem sekwencji, wszystko, czego się dowiedziałeś o sekwencjach na przykładzie łańcuchów, ma zastosowanie do krotek. Możesz pobrać długość krotki, wypisać jej wszystkie elementy za pomocą pętli for

123

124

Rozdział 4. Pętle for, łańcuchy znaków i krotki. Gra Wymieszane litery

oraz użyć operatora in do sprawdzenia, czy dany element jest w niej zawarty. Możesz także indeksować, wycinać i konkatenować krotki.

Prezentacja programu Inwentarz bohatera 2.0 Podróż naszego bohatera trwa dalej. W tym programie jego inwentarz jest liczony, sprawdzany, indeksowany i wycinany. Nasz bohater natrafi także na skrzynię zawierającą pewne przedmioty (reprezentowaną przez jeszcze jedną krotkę). Dzięki konkatenacji krotek wykaz dobytku naszego bohatera zostanie zastąpiony nowym, zawierającym jego obecne elementy plus skarb znaleziony w skrzyni. Na rysunku 4.12 zostało pokazane przykładowe uruchomienie programu.

Rysunek 4.12. Inwentarz bohatera jest krotką, co oznacza, że może być liczony, indeksowany, poddawany operacji wycinania, a nawet konkatenowany z inną krotką

Ponieważ ten program jest nieco długi, omówię kod fragment po fragmencie, nie pokazując od razu jego całości. Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 4.; nazwa pliku to inwentarz_bohatera2.py.

Początek programu Pierwsza część tego programu przypomina swoim działaniem poprzedni program, Inwentarz bohatera. Poniższe wiersze kodu tworzą krotkę i wypisują każdy jej element:

Wycinanie łańcuchów # Inwentarz bohatera 2.0 # Demonstruje krotki # utwórz krotkę zawierającą pewne elementy i wyświetl jej zawartość # za pomocą pętli for inventory = ("miecz", "zbroja", "tarcza", "napój uzdrawiający") print("Elementy Twojego inwentarza:") for item in inventory: print(item) input("\nAby kontynuować grę, naciśnij klawisz Enter.")

Stosowanie funkcji len() do krotek Funkcja len() działa w odniesieniu do krotek w dokładnie taki sam sposób jak w odniesieniu do łańcuchów znaków. Jeśli chcesz poznać długość krotki, umieść ją wewnątrz nawiasów. Funkcja zwraca liczbę elementów w krotce. Puste krotki, tak jak wszystkie puste sekwencje, mają długość równą 0. Poniższe wiersze kodu zawierają użycie funkcji len() w stosunku do krotki: # wyświetl długość krotki print("Twój dobytek zawiera", len(inventory), "elementy(-ów).") input("\nAby kontynuować grę, naciśnij klawisz Enter.")

Ponieważ ta krotka zawiera cztery elementy (cztery łańcuchy: "miecz", "zbroja", "tarcza" i "napój uzdrawiający"), wyświetlony zostaje komunikat Twój dobytek zawiera 4 elementy(-ów).

Pułapka Zwróć uwagę, że łańcuch "napój uzdrawiający" w krotce inventory jest liczony jako jeden element, mimo że zawiera dwa słowa.

Stosowanie operatora in do krotek Podobnie jak w przypadku łańcuchów, można używać operatora in w kontekście krotek do sprawdzenia przynależności elementu. I dokładnie jak przedtem operator in jest zwykle wykorzystywany do tworzenia warunku. Oto w jaki sposób go użyłem w poniższym kodzie: # sprawdź, czy element należy do krotki, za pomocą operatora in if "napój uzdrawiający" in inventory: print("Dożyjesz dnia, w którym stoczysz walkę.")

125

126

Rozdział 4. Pętle for, łańcuchy znaków i krotki. Gra Wymieszane litery

Warunek "healing potion" in inventory służy do sprawdzenia, czy cały łańcuch "healing potion" jest elementem krotki inventory. Ponieważ tak faktycznie jest, wyświetlany jest komunikat Dożyjesz dnia, w którym stoczysz walkę.

Indeksowanie krotek Indeksowanie krotek działa podobnie jak indeksowanie łańcuchów. Określasz numer pozycji, umieszczając go w nawiasach kwadratowych, aby uzyskać dostęp do konkretnego elementu. W poniższych wierszach kodu pozwalam użytkownikowi wybrać wartość indeksu, a potem komputer wyświetla odpowiadający jej element: # wyświetl jeden element wskazany przez indeks index = int(input("\nWprowadź indeks elementu inwentarza: ")) print("Pod indeksem", index, "znajduje się", inventory[index])

Ta krotka została pokazana na rysunku 4.13 wraz z wartościami indeksu.

Rysunek 4.13. Każdy łańcuch to pojedynczy element krotki

Wycinanie krotek Wycinanie działa dokładnie tak, jak to widziałeś w przypadku łańcuchów. Podajesz pozycję początkową i końcową. Wynikiem operacji jest krotka zawierająca wszystkie elementy znajdujące się między tymi dwoma pozycjami. Podobnie jak w przypadku programu Krajacz pizzy z wcześniejszej części tego rozdziału, pozwalam użytkownikowi wybrać numery pozycji początkowej i końcowej. Następnie, tak jak poprzednio, program wyświetla wycinek: # wyświetl wycinek start = int(input("\nWprowadź indeks wyznaczający początek wycinka: ")) finish = int(input("\nWprowadź indeks wyznaczający koniec wycinka: ")) print("inventory[", start, ":", finish, "] to", end=" ") print(inventory[start:finish]) input("\nAby kontynuować grę, naciśnij klawisz Enter.")

Wykorzystując tę krotkę jako przykład, na rysunku 4.14 przedstawiono w wizualny sposób, jak należy rozumieć tworzenie wycinków krotek.

Wycinanie łańcuchów

Rysunek 4.14. W przypadku krotek punkty graniczne wycinków są umiejscowione między elementami, tak jak to było w przypadku łańcuchów

Niemutowalność krotek Podobnie jak łańcuchy, krotki są niemutowalne. To oznacza, że krotki nie można zmienić. Oto sesja interaktywna, która ma dowieść słuszności mojej uwagi: >>> inventory = ("miecz", "zbroja", "tarcza", "napój uzdrawiający") >>> print(inventory) ('miecz', 'zbroja', 'tarcza', 'napój uzdrawiający') >>> inventory[0] = "topór" Traceback (most recent call last): File "", line 1, in inventory[0] = "topór" TypeError: 'tuple' object does not support item assignment

Chociaż nie możesz zmieniać krotek, podobnie jak łańcuchów, możesz tworzyć nowe krotki z już istniejących.

Konkatenacja krotek Krotki można konkatenować w ten sam sposób, jak konkatenuje się łańcuchy. Łączysz je po prostu razem za pomocą operatora konkatenacji: # dokonaj konkatenacji dwóch krotek chest = ("złoto", "klejnoty") print("Znajdujesz skrzynię, która zawiera:") print(chest) print("Dodajesz zawartość skrzyni do swojego inwentarza.") inventory += chest print("Twój aktualny inwentarz to:") print(inventory) input("\n\nAby zakończyć program, naciśnij klawisz Enter.")

Pierwszą rzeczą, którą zrobiłem, było utworzenie nowej krotki, chest, zawierającej dwa elementy w postaci łańcuchów "złoto" i "klejnoty". Następnie wyświetliłem krotkę chest, aby pokazać jej elementy. Potem użyłem operatora rozszerzonego przypisania, dokonując konkatenacji krotek inventory i chest i przypisując wynik z powrotem do zmiennej inventory. Nie zmodyfikowałem oryginalnej krotki

127

128

Rozdział 4. Pętle for, łańcuchy znaków i krotki. Gra Wymieszane litery

przypisanej do zmiennej inventory (ponieważ jest to niemożliwe, bo krotki są niemutowalne). Zamiast tego operator rozszerzonego przypisania utworzył całkowicie nową krotkę, zawierającą elementy krotek inventory i chest, która została przypisana do zmiennej inventory.

Powrót do gry Wymieszane litery Gra Wymieszane litery łączy kilka nowych koncepcji, które poznałeś w tym rozdziale. Możesz bez trudu zmodyfikować ten program tak, aby zawierał Twoją własną listę słów do odgadnięcia. Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 4.; nazwa pliku to wymieszane_litery.py.

Początek programu Po początkowych komentarzach importuję moduł random: # Wymieszane litery # Komputer wybiera losowo słowo, a potem miesza w nim litery # Gracz powinien odgadnąć pierwotne słowo import random

Następnie wykorzystałem krotkę do utworzenia sekwencji słów. Zwróć uwagę, że nazwa zmiennej WORDS zawiera same duże litery, z czego wynika, że będę ją traktował jako stałą. # utwórz sekwencję słów do wyboru WORDS = ("python", "anagram", "łatwy", "skomplikowany", "odpowiedź", "ksylofon")

W kolejnym kroku używam nowej funkcji, random.choise(), do wybrania w sposób losowy słowa z krotki WORDS: # wybierz losowo jedno słowo z sekwencji word = random.choice(WORDS)

Ta funkcja jest dla Ciebie nowa, ale jest dość prosta. Komputer wybiera losowo jeden element z dowolnej sekwencji, która zostanie mu wskazana. Kiedy komputer już dokona losowego wyboru słowa, przypisuje je do zmiennej word. Jest to słowo, które gracz będzie miał do odgadnięcia. Na koniec przypisuje wartość zmiennej word do zmiennej correct, której później użyję do sprawdzenia, czy gracz odgadł prawidłowo. # utwórz zmienną, by później użyć jej do sprawdzenia, czy odpowiedź jest poprawna correct = word

Powrót do gry Wymieszane litery

Projektowanie części programu, w której jest tworzony anagram Kolejny fragment kodu wykorzystuje nowe koncepcje wprowadzone w tym rozdziale i stanowi najbardziej interesującą część programu. Jest ten fragment, który faktycznie tworzy anagram na podstawie oryginalnego, wybranego losowo słowa. Lecz zanim zacząłem pisać kod, utworzyłem projekt tej części programu w pseudokodzie (tak, tak, naprawdę używamy tych wszystkich rzeczy, o których piszę). Oto moja pierwsza wersja algorytmu tworzenia anagramu wybranego słowa: utwórz pusty anagram dopóki wybrane słowo zawiera jakieś litery usuń wylosowaną literę z wybranego słowa dodaj wylosowaną literę do anagramu

Koncepcyjnie wygląda to dość dobrze, ale muszę przyjrzeć się swojej semantyce. Ponieważ łańcuchy znaków są niemutowalne, właściwie nie mogę „usunąć wylosowanej litery” z łańcucha wprowadzonego przez użytkownika. Lecz mogę utworzyć nowy łańcuch, który nie będzie zawierał wybranej losowo litery. A ponieważ nie mogę także „dodać wylosowanej litery” do łańcucha z anagramem, muszę utworzyć nowy łańcuch poprzez konkatenację cząstkowego anagramu w aktualnej postaci z „usuniętą” literą.

Tworzenie pustego łańcucha anagramu Sam początek algorytmu jest prosty: # utwórz 'pomieszaną' wersję słowa jumble =""

Program tworzy pusty łańcuch i przypisuje go do zmiennej jumble, która będzie stanowić odwołanie do ostatecznej wersji anagramu.

Skonfigurowanie pętli Procesem tworzenia anagramu steruje pętla while. Warunek pętli jest, jak widzisz, dosyć prosty: while word:c

Konfiguruję pętlę w ten sposób, aby jej wykonywanie było kontynuowane, dopóki wartość zmiennej word nie będzie równa pustemu łańcuchowi. Jest to doskonała metoda, ponieważ w trakcie każdego wykonania pętli komputer tworzy nową wersję łańcucha word z „usuniętą” jedną literą i przypisuje ją ponownie do zmiennej word. W końcu word stanie się pustym łańcuchem i tworzenie anagramu zostanie zakończone.

129

130

Rozdział 4. Pętle for, łańcuchy znaków i krotki. Gra Wymieszane litery

Generowanie losowej pozycji w łańcuchu word Pierwszy wiersz ciała pętli generuje losową pozycję w łańcuchu word na podstawie jego długości: position = random.randrange(len(word))

Więc litera word[position] jest tą literą, która ma być „usunięta” z łańcucha word i dodana do łańcucha jumble.

Tworzenie nowej wersji łańcucha jumble Następny wiersz pętli tworzy nową wersję łańcucha jumble. Nowy łańcuch jest równy swej poprzedniej wersji z dodaną na końcu literą word[position]. jumble += word[position]

Tworzenie nowej wersji łańcucha word Kolejny wiersz pętli to: word = word[:position] + word[(position + 1):]

Tworzy on nową wersję łańcucha word pozbawioną jednej litery, która w oryginalnym łańcuchu zajmowała pozycję position. Stosując wycinanie, komputer tworzy z zawartości łańcucha word dwa nowe łańcuchy. Pierwszy wycinek, word[:position], obejmuje wszystkie litery poprzedzające literę word[position] bez niej samej. Drugi wycinek, word[(position + 1):], zawiera wszystkie litery występujące w łańcuchu word po literze word[position]. Te dwa łańcuchy są łączone w jedną całość, która zostaje przypisana do zmiennej word, która teraz reprezentuje łańcuch równy swej poprzedniej wartości minus litera word[position].

Przywitanie gracza Po utworzeniu anagramu kolejny fragment programu wita gracza w grze i wyświetla słowo z wymieszanymi literami, które powinny zostać uporządkowane: # rozpocznij grę print( """ Witaj w grze 'Wymieszane litery'! Uporządkuj litery, aby odtworzyć prawidłowe słowo. (Aby zakończyć zgadywanie, naciśnij klawisz Enter bez podawania odpowiedzi.) """ ) print("Zgadnij, jakie to słowo:", jumble)

Podsumowanie

Uzyskanie odpowiedzi gracza Następnie komputer pobiera odpowiedź gracza. Komputer nie przestaje prosić gracza o podanie odpowiedzi, dopóki gracz nie wprowadzi poprawnego słowa lub nie naciśnie od razu klawisza Enter: guess = input("\nTwoja odpowiedź: ") while guess != correct and guess != "": print("Niestety, to nie to słowo.") guess = input("Twoja odpowiedź: ")

Pogratulowanie graczowi W tym miejscu programu gracz albo odgadł prawidłowo wybrane przez komputer słowo, albo zakończył grę. Jeśli gracz odgadł to słowo, komputer składa mu szczere gratulacje: if guess == correct: print("Zgadza się! Zgadłeś!\n")

Zakończenie gry Na koniec program dziękuje graczowi za uczestnictwo w grze i kończy pracę: print("Dziękuję za udział w grze.") input("\n\nAby zakończyć program, naciśnij klawisz Enter.")

Podsumowanie W tym rozdziale poznałeś koncepcję sekwencji. Zobaczyłeś, jak tworzyć sekwencję liczb za pomocą funkcji range(). Przekonałeś się, że łańcuchy są w rzeczywistości tylko sekwencjami znaków. Poznałeś krotki, które umożliwiają konfigurowanie sekwencji dowolnego typu. Zobaczyłeś, jak przechodzić przez elementy sekwencji za pomocą pętli for. Dowiedziałeś się, jak uzyskać informację o długości sekwencji i jak sprawdzić, czy element jest składnikiem sekwencji. Zobaczyłeś, jak kopiować fragmenty sekwencji poprzez indeksowanie i wycinanie. Dowiedziałeś się o niemutowalności i pewnych ograniczeniach, jakie na Ciebie nakłada. Lecz zobaczyłeś także, jak tworzyć nowe sekwencje z już istniejących poprzez konkatenację, pomimo tej niemutowalności. W końcu połączyłeś to wszystko razem, by utworzyć ambitną grę, polegającą na rozwiązywaniu anagramów.

131

132

Rozdział 4. Pętle for, łańcuchy znaków i krotki. Gra Wymieszane litery

Sprawdź swoje umiejętności 1. Napisz program, który liczy za użytkownika. Umożliw użytkownikowi wprowadzenie liczby początkowej, liczby końcowej i wielkości odstępu między kolejnymi liczbami. 2. Utwórz program, który wczytuje komunikat użytkownika, a następnie wypisuje go w odwrotnej kolejności. 3. Popraw program Wymieszane litery tak, żeby każdemu słowu towarzyszyła podpowiedź. Gracz powinien mieć możliwość zobaczenia podpowiedzi, jeśli utknie w martwym punkcie. Dodaj system punktacji, który nagradza graczy rozwiązujących anagram bez uciekania się do podpowiedzi. 4. Utwórz grę, w której komputer wybiera losowo słowo, które gracz musi odgadnąć. Komputer informuje gracza, ile liter znajduje się w wybranym słowie. Następnie gracz otrzymuje pięć szans na zadanie pytania, czy jakaś litera jest zawarta w tym słowie. Komputer może odpowiedzieć tylko „tak” lub „nie”. Potem gracz musi odgadnąć słowo.

5 Listy i słowniki. Gra Szubienica K

rotki stanowią wspaniały sposób pracy z sekwencjami dowolnego typu, ale ich niemutowalność może być ograniczająca. Na szczęście inny rodzaj sekwencji, lista, reprezentuje wszystkie możliwości krotki plus coś więcej — to dlatego, że listy są mutowalne. Elementy mogą być dodawane do listy bądź z niej usuwane. Można nawet posortować listę. Zostaniesz także zapoznany z innym typem — słownikami. Podczas gdy listy są związane z obsługą sekwencji informacji, słowniki działają na parach danych. Słowniki, podobnie jak ich odpowiedniki w realnym życiu, umożliwiają odszukiwanie jednej wartości za pomocą drugiej. W szczególności w tym rozdziale nauczysz się:  tworzyć, indeksować i wycinać listy,  dodawać i usuwać elementy listy,  używać metod listy do dodawania elementów na końcu listy i do sortowania listy,  wykorzystywać zagnieżdżone sekwencje do reprezentowania jeszcze bardziej

złożonych informacji,  używać słowników do obsługi par danych,  dodawać i usuwać pozycje słownika.

Wprowadzenie do gry Szubienica Projektem tego rozdziału jest gra w szubienicę. Komputer wybiera ukryte słowo, a zadaniem gracza jest próba jego stopniowego odgadnięcia, litera po literze. Za każdym razem, gdy litera podana przez gracza jest niepoprawna, komputer pokazuje nowy rysunek wieszanej postaci. Jeśli gracz nie odgadnie słowa w porę, ludzik z rysunku traci życie. Rysunki od 5.1 do 5.3 pokazują grę w pełnej krasie.

134

Rozdział 5. Listy i słowniki. Gra Szubienica

Rysunek 5.1. Gra Szubienica w toku. Hm… zastanawiam się, jakie to może być słowo

Rysunek 5.2. Zwyciężyłem w tej grze

Nie tylko będziesz mógł się cieszyć tą grą, ale zanim skończysz czytanie tego rozdziału, będziesz wiedział, jak stworzyć swoją własną wersję. Możesz zdefiniować spersonalizowaną grupę ukrytych słów, a nawet zmienić moją mało wyszukaną grafikę.

Korzystanie z list

Rysunek 5.3. Ta gra źle się skończyła, szczególnie dla małego ludzika utworzonego z tekstu

Korzystanie z list Listy są sekwencjami, dokładnie jak krotki — ale listy są mutowalne. Mogą być modyfikowane. Tak więc listy mają wszystkie możliwości krotek plus coś więcej. Listy funkcjonują tak jak krotki, więc wszystko, czego nauczyłeś się o krotkach, ma zastosowanie do list, co sprawia, że nauczenie się ich używania staje się pestką.

Prezentacja programu Inwentarz bohatera 3.0 Ten program został oparty na programie Inwentarz bohatera 2.0, przedstawionym w rozdziale 4., w punkcie „Tworzenie krotek”. Lecz zamiast stosować krotki do przechowywania inwentarza bohatera, ten program używa list. Pierwsza część programu Inwentarz bohatera 3.0 tworzy takie same wyniki jak wersja 2.0. W istocie kod jest prawie identyczny. Jedyna różnica polega na użyciu list zamiast krotek. Na rysunku 5.4 zostały ukazane wyniki pierwszej części programu. Jego druga część wykorzystuje mutowalność list i robi z sekwencjami pewne całkiem nowe rzeczy. Rysunek 5.5 pokazuje tę część programu w akcji.

Tworzenie listy W pierwszych wierszach tego programu tworzę nową listę, przypisuję ją do zmiennej inventory i wypisuję jej wszystkie elementy. Wszystko działa prawie tak samo jak w przypadku programu Inwentarz bohatera 2.0. Jedyna różnica polega na tym, że elementy otoczyłem nawiasami kwadratowymi, a nie zwykłymi, tworząc w ten sposób listę zamiast krotki. Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 5.; nazwa pliku to inwentarz_bohatera3.py.

135

136

Rozdział 5. Listy i słowniki. Gra Szubienica

Rysunek 5.4. Inwentarz bohatera jest teraz reprezentowany przez listę. Wyniki wyglądają prawie tak samo jak w programie Inwentarz bohatera 2.0, w którym inwentarz był reprezentowany przez krotkę

Rysunek 5.5. Ponieważ inwentarz bohatera jest reprezentowany przez listę, elementy mogą być dodawane, modyfikowane i usuwane # Inwentarz bohatera 3.0 # Demonstruje listy # utwórz listę zawierającą pewne elementy i wyświetl jej zawartość # za pomocą pętli for inventory = ["miecz", "zbroja", "tarcza", "napój uzdrawiający"] print("Elementy Twojego inwentarza:") for item in inventory: print(item)

Korzystanie z list

Stosowanie funkcji len() do list Poniższy kod jest dokładnie taki sam, jak odpowiadający mu kod w programie Inwentarz bohatera 2.0. Funkcja len() zastosowana do list działa tak samo jak w przypadku krotek. # wyświetl długość listy print("Twój dobytek zawiera", len(inventory), "elementy(-ów).") input("\nAby kontynuować grę, naciśnij klawisz Enter.")

Używanie operatora in z listami Ponownie kod tego fragmentu jest dokładnie taki sam jak w poprzedniej wersji programu. Operator in zastosowany do list działa tak samo jak w przypadku krotek. # sprawdź, czy element należy do listy, za pomocą operatora in if "napój uzdrawiający" in inventory: print("Dożyjesz dnia, w którym stoczysz walkę.")

Indeksowanie list Znowu kod jest taki sam jak w przypadku krotek. Indeksowanie list wygląda tak samo jak indeksowanie krotek — wystarczy, że podasz numer pozycji elementu, o który Ci chodzi, umieszczając go w nawiasach kwadratowych. # wyświetl jeden element wskazany przez indeks index = int(input("\nWprowadź indeks elementu inwentarza: ")) print("Pod indeksem", index, "znajduje się", inventory[index])

Tworzenie wycinków list Czy uwierzysz, że tworzenie wycinka listy jest dokładnie taką samą czynnością jak wycinanie fragmentu krotki? I tym razem wystarczy, że podasz w nawiasach kwadratowych dwa punkty graniczne oddzielone dwukropkiem: # wyświetl wycinek start = int(input("\nWprowadź indeks wyznaczający początek wycinka: ")) finish = int(input("Wprowadź indeks wyznaczający koniec wycinka: ")) print("inventory[", start, ":", finish, "] to", end=" ") print(inventory[start:finish]) input("\nAby kontynuować grę, naciśnij klawisz Enter.")

Konkatenacja list Konkatenacja list funkcjonuje w taki sam sposób jak konkatenacja krotek. Jedyna faktyczna różnica polega na tym, że utworzyłem listę (zamiast krotki) i przypisałem ją do zmiennej chest. Jest to mała, ale istotna różnica, ponieważ można tylko dokonywać konkatenacji sekwencji tego samego typu.

137

138

Rozdział 5. Listy i słowniki. Gra Szubienica # dokonaj konkatenacji dwóch list chest = ["złoto", "klejnoty"] print("Znajdujesz skrzynię, która zawiera:") print(chest) print("Dodajesz zawartość skrzyni do swojego inwentarza.") inventory += chest print("Twój aktualny inwentarz to:") print(inventory) input("\nAby kontynuować grę, naciśnij klawisz Enter.")

Mutowalność list W tym momencie możesz już czuć się nieco zmęczony formułą „działa dokładnie tak samo jak w przypadku krotek”. Jak dotąd, z wyjątkiem używania nawiasów kwadratowych zamiast zwykłych, listy nie wydają się niczym różnić od krotek. Ale istnieje między tymi dwoma strukturami jedna ogromna różnica. Listy są mutowalne. Mogą się zmieniać. W rezultacie istnieje wiele rzeczy, które można robić z listami, a których nie można robić z krotkami.

Przypisanie elementowi listy nowej wartości poprzez indeks Ponieważ listy są mutowalne, możesz przypisać istniejącemu elementowi nową wartość: # przypisz poprzez indeks print("Wymieniasz swój miecz na kuszę.") inventory[0] = "kusza" print("Twój aktualny inwentarz to:") print(inventory) input("\nAby kontynuować grę, naciśnij klawisz Enter.")

W następującym wierszu wziętym z powyższego kodu łańcuch "kusza" zostaje przypisany elementowi listy inventory o pozycji 0: inventory[0] = "kusza"

Nowy łańcuch zajmuje miejsce poprzedniej wartości (którą był łańcuch "miecz"). Wynik będziesz mógł zobaczyć, kiedy funkcja print wyświetli nową wersję listy inventory.

Pułapka Za pomocą indeksowania możesz przypisać istniejącemu elementowi listy nową wartość, ale nie możesz w ten sposób utworzyć nowego elementu. Próba przypisania wartości do nieistniejącego elementu spowoduje wystąpienie błędu.

Korzystanie z list

Przypisanie nowej wartości wycinkowi listy Oprócz przypisania nowej wartości pojedynczemu elementowi możesz także przypisać nową wartość wycinkowi. Ja przypisałem listę ["kula do wróżenia"] do wycinka inventory[4:6]: # przypisz poprzez wycinek print("Zużywasz swoje złoto i klejnoty na zakup kuli do wróżenia.") inventory[4:6] = ["kula do wróżenia"] print("Twój aktualny inwentarz to:") print(inventory) input("\nAby kontynuować grę, naciśnij klawisz Enter.")

Ta instrukcja przypisania zastępuje dwa elementy inventory[4] i inventory[5] łańcuchem "kula do wróżenia". Ponieważ przypisałem listę z jednym elementem do wycinka z dwoma elementami, długość listy zmniejszyła się o jeden.

Usunięcie elementu listy Możesz usunąć element z listy za pomocą del — po prostu wyznacz ten element, umieszczając po del jego określenie: # usuń element print("W wielkiej bitwie Twoja tarcza zostaje zniszczona.") del inventory[2] print("Twój aktualny inwentarz to:") print(inventory) input("\nAby kontynuować grę, naciśnij klawisz Enter.")

Kiedy ten kod zostanie wykonany, element, który zajmował pozycję numer 2, łańcuch "tarcza" zostanie usunięty z listy inventory. Usunięcie elementu nie tworzy w sekwencji

luki. Długość listy zmniejsza się o jeden, a wszystkie elementy, które usunięty element poprzedzał, zostają „przesunięte w lewo” o jedną pozycję. Więc w tym przypadku nadal istnieje element o pozycji 2; jest to ten sam element, który przedtem zajmował pozycję 3.

Usunięcie wycinka listy Możesz również usunąć z listy wycinek: # usuń wycinek print("Twoja kusza i zbroja zostały skradzione przez złodziei.") del inventory[:2] print("Twój aktualny inwentarz to:") print(inventory) input("\n\nAby zakończyć program, naciśnij klawisz Enter.")

Poniższy wiersz kodu usuwa wycinek inventory[:2], którym jest ["kusza", "zbroja"] z listy inventory: del inventory[:2]

139

140

Rozdział 5. Listy i słowniki. Gra Szubienica

Tak jak przy usuwaniu elementu, długość listy zostaje zmniejszona, a pozostałe w niej elementy tworzą nową ciągłą listę rozpoczynającą się od pozycji 0.

Korzystanie z metod listy Listy udostępniają metody, dzięki którym można nimi manipulować. Używając metod listy, możesz dodać element, usunąć element na podstawie jego wartości, posortować listę, a nawet odwrócić kolejność jej elementów.

Prezentacja programu Najlepsze wyniki Program Najlepsze wyniki wykorzystuje metody listy do tworzenia i utrzymywania listy najlepszych wyników użytkownika uzyskanych w grze komputerowej. Program używa prostego interfejsu opartego na menu. Użytkownik ma kilka możliwości wyboru. Może dodać nowy wynik, usunąć wynik, posortować wyniki lub zakończyć program. Rysunek 5.6 pokazuje ten program w działaniu.

Rysunek 5.6. Użytkownik obsługuje listę najlepszych wyników, wybierając pozycje z menu. Gros pracy wykonują jednak działające na zapleczu metody listy

Korzystanie z metod listy

Skonfigurowanie programu Kod odpowiedzialny za początkowe ustawienia jest dość prosty. Po początkowych komentarzach tworzę dwie zmienne. Zmienna scores reprezentuje listę, która ma zawierać wyniki. Na początek ustawiłem jej wartość jako pustą listę. Zmienna choice reprezentuje pozycję menu wybraną przez użytkownika. Zainicjalizowałem ją na None. Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 5.; nazwa pliku to najlepsze_wyniki.py. # Najlepsze wyniki # Demonstruje metody listy scores = [] choice = None

Wyświetlenie menu Pętla while stanowi rdzeń programu. Jej wykonywanie jest kontynuowane, dopóki użytkownik nie wprowadzi wartości 0. Pozostała część kodu pętli wyświetla menu i pobiera wybrany przez użytkownika numer pozycji: while choice != "0": print( """ Najlepsze wyniki 0 1 2 3 4 """ )

zakończ pokaż wyniki dodaj wynik usuń wynik posortuj wyniki

choice = input("Wybierasz: ") print()

Zakończenie programu Najpierw sprawdzam, czy użytkownik chce zakończyć program. Jeśli użytkownik wprowadzi 0, komputer mówi „Do widzenia.”: # zakończ program if choice == "0": print("Do widzenia.")

Jeśli użytkownik wprowadzi 0, warunek pętli while będzie fałszywy przy kolejnym sprawdzeniu. Pętla się zakończy, jak i cały program.

141

142

Rozdział 5. Listy i słowniki. Gra Szubienica

Wyświetlenie wyników Jeśli użytkownik wprowadzi 1, wykona się poniższy blok elif i komputer wyświetli wyniki: # wypisz tabelę najlepszych wyników elif choice == "1": print("Najlepsze wyniki") for score in scores: print(score)

Dodanie wyniku Jeśli użytkownik wprowadzi 2, komputer poprosi go o podanie nowego wyniku, który zostanie przypisany do zmiennej score. Ostatni wiersz kodu wykorzystuje metodę listy append(), aby umieścić wartość score na końcu listy scores. Długość listy wzrasta o jeden element. # dodaj wynik elif choice == "2": score = int(input("Jaki wynik uzyskałeś?: ")) scores.append(score)

Usunięcie wyniku Kiedy użytkownik wprowadzi 3, komputer pobiera od użytkownika wartość wyniku, który ma zostać usunięty. Jeśli wynik zawiera się na liście, komputer usuwa jego pierwsze wystąpienie. Jeśli wynik nie występuje na liście, użytkownik jest o tym informowany. # usuń wynik elif choice == "3": score = int(input("Który wynik usunąć?: ")) if score in scores: scores.remove(score) else: print(score, "nie ma na liście wyników.")

Kod najpierw sprawdza, czy wynik zawiera się na liście. Jeśli się zawiera, wywoływana jest metoda listy remove(). Metoda przegląda listę, zaczynając od pozycji 0, i szuka wartości przekazanej jej jako argument wywołania — w tym wypadku score. Kiedy metoda odnajduje pierwsze wystąpienie tej wartości, element jest usuwany z listy. Jeśli wartość w występuje na liście więcej niż raz, usuwane jest tylko pierwsze wystąpienie. Więc tylko pierwsze wystąpienie wyniku score zostanie usunięte. Jeśli metoda skutecznie usunie element z listy, staje się ona o jeden element krótsza. Zwróć też uwagę, w jaki sposób remove() różni się od del. Metoda remove() usuwa element nie na podstawie jego pozycji, lecz na podstawie jego wartości.

Korzystanie z metod listy

Pułapka Zachowaj ostrożność, kiedy będziesz korzystać z metody remove(). Jeśli spróbujesz usunąć wartość, która nie zawiera się na liście, wygenerujesz błąd. Oto bezpieczny sposób używania tej metody: if score in scores: scores.remove(score)

Sortowanie wyników Wyniki zawarte na liście występują w takiej samej kolejności, w jakiej zostały wprowadzone przez użytkownika. Aby posortować te wyniki, użytkownik musi jedynie wprowadzić liczbę 4: # posortuj wyniki elif choice == "4": scores.sort(reverse=True)

Metoda sort() porządkuje elementy listy. To doskonale, z wyjątkiem tego, że domyślnie sort() ustawia elementy w porządku rosnącym — zaczynając od wartości najmniejszych. Ale ja chciałbym, aby na początku były najlepsze wyniki. Na szczęście można nakazać metodzie sort() ustawienie elementów w porządku malejącym — z największymi wartościami na początku. Możesz to osiągnąć, nadając wartość True parametrowi metody o nazwie reverse. Właśnie to zrobiłem w swoim programie i w rezultacie wyniki są sortowane, począwszy od najwyższych wartości.

Wskazówka Jeśli chcesz posortować listę w porządku rosnącym (najpierw najmniejsze wartości), możesz po prostu wywołać metodę, nie nadając wartości żadnym parametrom. Więc jeśli chciałbym posortować listę o nazwie numbers w porządku rosnącym, mógłbym użyć następującego wiersza: numbers.sort()

Postępowanie po nieprawidłowym wyborze Jeśli użytkownik wprowadzi liczbę, która nie stanowi prawidłowego wyboru, przechwytuje ją klauzula else. Program powiadamia użytkownika, że jego wybór nie został zrozumiany. # nieznana opcja else: print("Niestety,", choice, "nie jest prawidłowym wyborem.")

Czekanie na decyzję użytkownika Po wprowadzeniu przez użytkownika wartości 0 następuje wyjście z pętli. Jak zawsze przed zakończeniem pracy, program czeka na decyzję użytkownika: input("\n\nAby zakończyć program, naciśnij klawisz Enter.")

143

144

Rozdział 5. Listy i słowniki. Gra Szubienica

Zobaczyłeś w działaniu kilka użytecznych metod listy. Podsumowanie tych metod (oraz paru innych) znajdziesz w tabeli 5.1: Tabela 5.1. Wybrane metody listy

Metoda

Opis

append(wartość)

Dodaje wartość na końcu listy.

sort()

Sortuje elementy, od najmniejszego do największego. Opcjonalnie można nadać wartość typu boolean parametrowi reverse. Jeśli będzie to wartość True, lista zostanie posortowana od największej do najmniejszej wartości.

reverse()

Odwraca porządek elementów listy.

count(wartość)

Zwraca liczbę wystąpień argumentu wartość.

index(wartość)

Zwraca numer pozycji pierwszego wystąpienia argumentu wartość.

insert(i, wartość)

Wstawia wartość na pozycji i.

pop(i)

Zwraca wartość zajmującą pozycję i oraz usuwa ją z listy. Przekazanie numeru pozycji i jest opcjonalne. Jeśli argument i nie zostanie podany, usuwany i zwracany jest ostatni element listy.

remove(wartość)

Usuwa z listy pierwsze wystąpienie argumentu wartość.

Kiedy należy używać krotek zamiast list? W tym momencie być może myślisz: „Po co w ogóle używać krotek?”. To prawda, że listy zawierają wszystkie możliwości krotek plus coś więcej. Ale nie śpiesz się tak z rezygnacją z krotek. W Twoim świecie programowania w języku Python jest dla nich miejsce. Istnieje kilka sytuacji, w których używanie krotek ma więcej sensu niż korzystanie z list.  Krotki są szybsze niż listy. Ponieważ komputer wie o krotkach, że nie mogą się

zmienić, może je przechowywać w taki sposób, aby ich używanie było szybsze niż korzystanie z list. W przypadku prostych programów ta różnica szybkości nie ma znaczenia, ale w bardziej złożonych aplikacjach operujących bardzo dużymi sekwencjami informacji mogłaby być istotna.  Niemutowalność krotek sprawia, że doskonale się nadają do tworzenia stałych,

ponieważ nie mogą się zmieniać. Użycie krotek może uczynić Twój kod bezpieczniejszym i bardziej przejrzystym.  Czasem użycie krotek jest wymagane. W pewnych przypadkach Python żąda

wartości niemutowalnych. W porządku, właściwie nie zetknąłeś się jeszcze z żadnym z tych przypadków, ale istnieje często występująca sytuacja, z którą się spotkasz, gdy będziesz w dalszej części tego rozdziału, w podrozdziale „Używanie słowników”, poznawał słowniki. Ponieważ słowniki wymagają stosowania typów niemutowalnych, przy tworzeniu niektórych ich rodzajów krotki będą miały zasadnicze znaczenie. Lecz ponieważ listy są tak elastyczne, prawdopodobnie ich użycie będzie dla Ciebie w większości przypadków korzystniejsze niż zastosowanie krotek.

Używanie sekwencji zagnieżdżonych

Używanie sekwencji zagnieżdżonych Przedtem powiedziałem, że listy i krotki mogą być sekwencjami dowolnych elementów. Jeśli to prawda, listy mogą zawierać inne listy lub też krotki, a krotki mogą zawierać inne krotki lub listy. Faktycznie mogą, a kiedy tak się dzieje, mamy do czynienia z sekwencjami zagnieżdżonymi. Sekwencje zagnieżdżone to sekwencje wewnątrz innych sekwencji. Są one świetnym sposobem organizowania bardziej złożonych kolekcji informacji. Chociaż ten termin wygląda na jeszcze jeden tajemniczy element komputerowego żargonu, założę się, że będziesz tworzył sekwencje zagnieżdżone i używał ich nieustannie. Pozwól, że posłużę się przykładem. Powiedzmy, że układasz listę świątecznych zakupów. Rozpoczynasz od sporządzenia listy imion i nazwisk. Pod każdym imieniem lub nazwiskiem umieszczasz listę kilku możliwych podarunków. Właśnie utworzyłeś sekwencję zagnieżdżoną — masz listę imion, a każde imię reprezentuje listę prezentów. I to wszystko, co na ten temat można powiedzieć.

Prezentacja programu Najlepsze wyniki 2.0 Poprzedni program, Najlepsze wyniki, operuje tylko wynikami. Ale większość list z najlepszymi wynikami przechowuje oprócz wyników nazwę gracza. Tak też jest w przypadku tej nowej wersji programu. Zawiera ona również kilka innych ulepszeń. Automatycznie sortuje wyniki i nawet ogranicza wielkość tej listy do czołowej piątki. Na rysunku 5.7 zostało pokazane jego przykładowe uruchomienie.

Rysunek 5.7. Nowa, poprawiona wersja programu Najlepsze wyniki przechowuje wraz z wynikiem nazwę gracza, wykorzystując sekwencje zagnieżdżone

145

146

Rozdział 5. Listy i słowniki. Gra Szubienica

Tworzenie sekwencji zagnieżdżonych Możesz utworzyć zagnieżdżoną listę lub krotkę w zwykły sposób — wypisz poszczególne elementy, oddzielając je przecinkami. Różnica związana z sekwencjami zagnieżdżonymi polega na tym, że jako elementy dołączasz całe listy lub krotki. Oto przykład: >>> nested = ["pierwszy", ("drugi", "trzeci"), ["czwarty", "piąty", "szósty"]] >>> print(nested) ['pierwszy', ('drugi', 'trzeci'), ['czwarty', 'piąty', 'szósty']]

Więc chociaż widzisz sześć łańcuchów, lista nested ma tylko trzy elementy. Pierwszym elementem jest łańcuch "pierwszy", drugim — krotka ("drugi", "trzeci"), a trzecim elementem jest lista ["czwarty", "piąty", "szósty"]. Mimo że możesz utworzyć listę lub krotkę zawierającą dowolną liczby list i krotek, użyteczne sekwencje zagnieżdżone mają często spójny schemat. Popatrz na kolejny przykład: >>> scores = [("Muniek", 1000), ("Lilka", 1500), ("Kajtek", 3000)] >>> print(scores) [('Muniek', 1000), ('Lilka', 1500), ('Kajtek', 3000)]

Lista scores ma trzy elementy. Każdy z jej elementów to krotka. Każda krotka ma dokładnie dwa elementy, łańcuch znaków i liczbę. Ta sekwencja, nawiasem mówiąc, reprezentuje tabelę najlepszych wyników z nazwami graczy i wynikami (tak jak powinna wyglądać rzeczywista tabela najlepszych wyników!). W tym szczególnym przypadku Muniek uzyskał wynik 1000; Lilka — 1500, a najlepszy wynik Kajtka to 3000.

Pułapka Chociaż możesz tworzyć zagnieżdżone sekwencje wewnątrz zagnieżdżonych sekwencji wielokrotnie, tak jak w poniższym przykładzie, nie jest to zwykle dobry pomysł. nested = ("głęboki", ("głębszy", ("najgłębszy", "wciąż najgłębszy")))

Sprawy mogą się szybko pogmatwać. Nawet doświadczeni programiści rzadko używają sekwencji o więcej niż jednym czy dwóch poziomach zagłębienia. W większości programów, które będziesz pisać, jeden poziom zagłębienia (tak jak w przypadku listy scores, którą przed chwilą widziałeś) jest naprawdę wszystkim, czego potrzebujesz.

Uzyskiwanie dostępu do elementów zagnieżdżonych Dostęp do elementów zagnieżdżonej sekwencji, tak jak w przypadku dowolnej innej sekwencji, uzyskujesz poprzez indeksowanie: >>> scores = [("Muniek", 1000), ("Lilka", 1500), ("Kajtek", 3000)] >>> print(scores[0]) ('Muniek', 1000) >>> print(scores[1]) ('Lilka', 1500) >>> print(scores[3]) ('Kajtek', 3000)

Używanie sekwencji zagnieżdżonych

Każdy element to krotka, więc taki właśnie otrzymujesz wynik po uzyskaniu dostępu do każdego z nich. Lecz co masz zrobić, jeśli chcesz uzyskać dostęp do jednego z elementów jednej z krotek? Jeden sposób polega na przypisaniu krotki do zmiennej i zastosowaniu indeksu, tak jak poniżej: >>> a_score = score[2] >>> print(a_score) ("Kajtek", 3000) >>> print(a_score[0]) Kajtek

Lecz istnieje bezpośredni sposób dostępu do łańcucha "Kajtek" poprzez samą zmienną scores: >>> print(scores[2][0]) Kajtek

Przez podanie dwóch indeksów w wyrażeniu scores[2][0] informujesz komputer, że ma pobrać z listy scores element zajmujący pozycję 2 (którym jest krotka ("Kajtek", 3000)), a następnie pobrać z niego element zajmujący pozycję 0 (którym jest łańcuch "Kajtek"). Możesz używać tego rodzaju wielokrotnego indeksowania do zagnieżdżonych sekwencji, aby się dostać bezpośrednio do zagnieżdżonego elementu.

Rozpakowanie sekwencji Jeśli wiesz, ile elementów zawiera sekwencja, możesz przypisać każdy z nich do jego własnej zmiennej w pojedynczym wierszu kodu: >>> name, score = ("Szymek", 175) >>> print(name) Szymek >>> print(score) 175

Nazywa się to rozpakowaniem i ma zastosowanie do dowolnego typu sekwencji. Pamiętaj tylko, aby użyć tyle samo zmiennych, ile elementów zawiera sekwencja, bo w przeciwnym wypadku wygenerujesz błąd.

Wstępne ustawienia programu Tak jak w oryginalnym programie Najlepsze wyniki, ustawiam wartości zmiennych i konfiguruję pętlę while. Tak jak przedtem, jeśli użytkownik wprowadzi 0, komputer wypisze zawartość łańcucha "Do widzenia.". Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 5.; nazwa pliku to najlepsze_wyniki2.py. # Najlepsze wyniki 2.0 # Demonstruje sekwencje zagnieżdżone

147

148

Rozdział 5. Listy i słowniki. Gra Szubienica scores = [] choice = None while choice != "0": print( """ Najlepsze wyniki 2.0 0 - zakończ 1 - wyświetl wyniki 2 - dodaj wynik """ ) choice = input("Wybierasz: ") print() # zakończ if choice == "0": print("Do widzenia.")

Wyświetlanie wyników poprzez dostęp do zagnieżdżonych krotek Jeśli użytkownik wprowadzi 1, komputer wybiera kolejno każdy element listy scores i rozpakowuje wynik i nazwę gracza do zmiennych score i name. Następnie komputer wyświetla ich wartości. # wyświetl tabelę najlepszych wyników elif choice == "1": print("Najlepsze wyniki\n") print("GRACZ\tWYNIK") for entry in scores: score, name = entry print(name, "\t", score)

Dodanie wyniku poprzez dołączenie do listy zagnieżdżonej krotki Jeśli użytkownik wprowadzi 2, komputer umożliwi mu wprowadzenie nowego wyniku i nazwy gracza. Z tych dwóch wartości komputer tworzy krotkę entry. Zdecydowałem się na przechowanie wyniku najpierw w tej krotce, ponieważ chciałem, aby wprowadzone dane zostały posortowane według wyniku, a potem nazwy. Następnie komputer dołącza tę nową pozycje z wynikiem do listy. Komputer sortuje listę i odwraca jej porządek, tak że najwyższe wyniki znajdują się na początku. Ostatnia instrukcja wycina i przypisuje listę, tak aby zostało zachowanych tylko pięć najwyższych wyników. # add a score elif choice == "2":

Referencje współdzielone name = input("Podaj nazwę gracza: ") score = int(input("Jaki wynik gracz uzyskał?: ")) entry = (score, name) scores.append(entry) scores.sort(reverse=True) scores = scores[:5] # zachowaj tylko 5 najlepszych wyników

Postępowanie po nieprawidłowym wyborze Jeśli użytkownik wprowadzi inną wartość niż 0, 1 lub 2, przechwyci ją klauzula else. Program informuje użytkownika, że jego wybór nie został zrozumiany. # nieznana opcja else: print("Niestety,", choice, "nie jest prawidłowym wyborem.")

Czekanie na decyzję użytkownika Po wprowadzeniu przez użytkownika wartości 0 następuje wyjście z pętli i program czeka na decyzję użytkownika dotyczącą jego zakończenia: input("\n\nAby zakończyć program, naciśnij klawisz Enter.")

Referencje współdzielone W rozdziale 2. dowiedziałeś się, że zmienna odwołuje się do wartości. Z technicznego punktu widzenia to oznacza, że zmienna nie przechowuje kopii wartości, lecz odwołuje się do miejsca w pamięci komputera, gdzie ta wartość jest przechowywana. Na przykład instrukcja language = "Python" przechowuje łańcuch "Python" gdzieś w pamięci Twojego komputera, a następnie tworzy zmienną language, która odwołuje się do tego miejsca w pamięci. Wizualne przedstawienie efektu działania tej instrukcji znajdziesz na rysunku 5.8.

Rysunek 5.8. Zmienna language odwołuje się do miejsca w pamięci, gdzie przechowywana jest wartość łańcucha "Python"

Mówienie, że zmienna language przechowuje łańcuch "Python", tak jak pojemnik firmy Tupperware przechowuje udko kurczaka, nie jest dokładne. W niektórych językach programowania mogłaby to być dobra analogia, ale nie w Pythonie. Lepszym sposobem wyobrażenia sobie tej kwestii byłoby coś takiego: zmienna odwołuje się do wartości w taki sam sposób jak nazwisko odwołuje się do osoby. Powiedzieć, że nazwisko „przechowuje” osobę byłoby czymś niewłaściwym (i głupim). Wykorzystując nazwisko osoby, możesz dotrzeć do niej samej. Używając nazwy zmiennej, możesz sięgnąć do jej wartości.

149

150

Rozdział 5. Listy i słowniki. Gra Szubienica

Więc jakie to wszystko ma znaczenie? Cóż, w przypadku wartości niemutowalnych, których używałeś, takich jak liczby, łańcuchy i krotki, nie znaczy to wiele. Ale ma swoje znaczenie w kontekście wartości mutowalnych, takich jak listy. Kiedy kilka zmiennych odwołuje się do tej samej wartości mutowalnej, współdzielą one tę samą referencję. Wszystkie odwołują się do jednej i tej samej kopii tej wartości. A zmiana tej wartości poprzez jedną z tych zmiennych skutkuje zmianą wartości ich wszystkich, ponieważ istnieje tylko jedno wspólne źródło tej wartości. Oto przykład pokazujący, jak to działa. Przypuśćmy, że urządzam szałowe party z udziałem moich przyjaciół oraz dygnitarzy z całego świata. (Hej, to moja książka. Mogę wymyślić taki przykład, jaki tylko chcę). Różni ludzie na przyjęciu nazywają mnie w różny sposób, mimo że jestem tylko jedną osobą. Powiedzmy, że kolega nazywa mnie „Mike”, dygnitarz — „Mr. Dawson”, a moja dziewczyna — supermodelka i zdobywczyni nagrody Pulitzera, która właśnie wróciła ze swojej podróży po świecie, mającej na celu zbieranie funduszy na walkę z analfabetyzmem (ponownie: moja książka — moja fikcyjna dziewczyna) — zwraca się do mnie „honey” (kochanie). Więc każde z tych trojga ludzi, odnosząc się do mnie, używa różnych nazw. Na tej samej zasadzie trzy zmienne mogłyby się odwoływać do tej samej listy. Oto początek sesji interaktywnej, która powinna Ci pokazać, co mam na myśli: >>> mike = ["spodnie khaki", "koszula frakowa", "marynarka"] >>> mr_dawson = mike >>> honey = mike >>> print(mike) ['spodnie khaki', 'koszula frakowa', 'marynarka'] >>> print(mr_dawson) ['spodnie khaki', 'koszula frakowa', 'marynarka'] >>> print(honey) ['spodnie khaki', 'koszula frakowa', 'marynarka']

Więc wszystkie trzy zmienne, mike, mr_dawson i honey, odwołują się do jednej i tej samej listy reprezentującej moją osobę (lub przynajmniej to, w co byłem ubrany na tym przyjęciu). Rysunek 5.9 pomaga w wyjaśnieniu tej koncepcji:

Rysunek 5.9. Wszystkie zmienne, mike, mr_dawson i honey, odwołują się do tej samej listy

To oznacza, że zmiana na liście dokonana przy użyciu jednej z tych trzech zmiennych dotyczy listy, do której wszystkie się odwołują. Wracając do przyjęcia, powiedzmy, że moja dziewczyna przyciąga moją uwagę, nazywając mnie „honey”. Prosi mnie, żebym zmienił moją marynarkę na czerwony sweter, który zrobiła na drutach (tak, robi także

Referencje współdzielone

na drutach). Ja oczywiście robię to, o co prosi. W mojej sesji interaktywnej mogłoby to zostać wyrażone następująco: >>> honey[2] = "czerwony sweter" >>> print(honey) ['spodnie khaki', 'koszula frakowa', 'czerwony sweter']

Wyniki są takie, jakich można by oczekiwać. Element zajmujący pozycję numer 2 na liście, do której odwołuje się zmienna honey, to już nie "marynarka", lecz "czerwony sweter". Więc gdyby na przyjęciu kolega chciał zwrócić na siebie moją uwagę, nazywając mnie „Mike”, lub dygnitarz chciał mnie przywołać, zwracając się do mnie „Mr. Dawson”, obydwaj by mnie zobaczyli w czerwonym swetrze, chociaż nikt z nich nie miał nic wspólnego z moją zmianą ubioru. To samo dotyczy Pythona. Jeśli nawet zmieniłem wartość elementu na pozycji numer 2 poprzez użycie zmiennej honey, ta zmiana została odzwierciedlona przez każdą zmienną, która odwołuje się do tej listy. Więc kontynuuję moją sesję interaktywną: >>> print(mike) ['spodnie khaki', 'koszula frakowa', 'czerwony sweter'] >>> print(mr_dawson) ['spodnie khaki', 'koszula frakowa', 'czerwony sweter']

Element zajmujący pozycję numer 2 listy, do której odwołują się zmienne mike i mr_dawson to "czerwony sweter". Nie może być inaczej, skoro istnieje tylko jedna lista. Więc morał, który płynie z tej historii, jest następujący: uważaj na współdzielone referencje, kiedy korzystasz z wartości mutowalnych. Jeśli zmienisz wartość poprzez jedną zmienną, zostanie zmieniona dla wszystkich. Możesz jednak uniknąć tego efektu, jeśli wykonasz kopię listy poprzez wycinanie. Na przykład: >>> mike = ["spodnie khaki", "koszula frakowa", "marynarka"] >>> honey = mike[:] >>> honey[2] = "czerwony sweter" >>> print(honey) ['spodnie khaki', 'koszula frakowa', 'czerwony sweter'] >>> print(mike) ["spodnie khaki", "koszula frakowa", "marynarka"]

Tym razem do zmiennej honey zostaje przypisana kopia listy mike. Zmienna honey nie odwołuje się do tej samej listy. Natomiast odwołuje się do jej kopii. Więc zmiana dotycząca listy honey nie ma żadnego wpływu na listę mike. To tak, jakbym został sklonowany. Teraz moja dziewczyna ubiera mojego klona w czerwony sweter, podczas gdy oryginał mojej osoby nosi nadal marynarkę. W porządku, to przyjęcie staje się dość niesamowite, gdy mój klon paraduje w czerwonym swetrze, który moja fikcyjna dziewczyna zrobiła dla mnie na drutach, więc myślę, iż nadeszła pora, aby zakończyć tę dziwaczną, choć użyteczną analogię.

151

152

Rozdział 5. Listy i słowniki. Gra Szubienica

Ostatnia rzecz, którą warto zapamiętać, to uwaga, że czasem efekt współdzielonej referencji będzie dla Ciebie pożądany, a czasem nie. Teraz, kiedy rozumiesz, na czym on polega, możesz go kontrolować.

Używanie słowników Do tej pory zdążyłeś sobie prawdopodobnie uświadomić, że programiści uwielbiają organizowanie informacji. Przekonałeś się, jak listy i krotki umożliwiają organizowanie różnych rzeczy w sekwencje. Słowniki również umożliwiają Ci organizowanie informacji, lecz w inny sposób. W przypadku słownika nie przechowujesz informacji w postaci sekwencji; zamiast tego przechowujesz je w postaci par. Przypomina to trochę prawdziwy słownik, w którym każda pozycja stanowi parę: słowo oraz jego definicję. Kiedy znajdujesz słowo, odczytujesz jego definicję. Słowniki w Pythonie funkcjonują w taki sam sposób: szukasz klucza i pobierasz jego wartość.

Wprowadzenie do programu Translator slangu komputerowego Świat zaawansowanych technologii stworzył wiele rzeczy, które wpływają na nasze życie, łącznie z samą kulturą. W wyniku rozwoju technologii narodziły się nowe słowa i pojęcia. Translator slangu komputerowego ma Ci pomóc zrozumieć wkraczających w Twoje życie entuzjastów postępu technicznego. Program tworzy słownik zawierający terminy używane przez maniaków komputerowych oraz ich definicje. Program umożliwia nie tylko odszukanie definicji terminu, ale również dodanie nowego wyrażenia, zmianę definicji oraz usunięcie terminu. Rysunek 5.10 ilustruje działanie programu.

Rysunek 5.10. Więc „uninstalled” oznacza zwolniony. Byłem całkowitym 404 w tej kwestii

Używanie słowników

Tworzenie słowników Pierwszą rzeczą, jaką zrobiłem w programie, było utworzenie słownika terminów i ich definicji. Terminy ze slangu komputerowego znajdują się po lewej, a ich definicje po prawej stronie. Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 5.; nazwa pliku to translator_slangu.py. # Translator slangu komputerowego # Demonstruje używanie słowników geek = {"404": "ignorant; od używanego w sieci WWW komunikatu o błędzie 404\n - nie znaleziono strony.", "Googling": "googlowanie; wyszukiwanie w internecie informacji dotyczących jakiejś osoby.", "Keyboard Plaque" : "(skojarzone z kamieniem nazębnym)zanieczyszczenia nagromadzone w klawiaturze komputera.", "Link Rot" : "(obumieranie linków) proces, w wyniku którego linki do stron WWW stają się nieaktualne.", "Percussive Maintenance" : "(konserwacja perkusyjna)naprawa urządzenia elektronicznego przez stuknięcie.", "Uninstalled" : "(odinstalowany) zwolniony z pracy; termin szczególnie popularny w okresie bańki internetowej."}

Ten kod tworzy słownik o nazwie geek. Składa się on z sześciu par zwanych elementami. Na przykład jednym z elementów jest "Keyboard Plaque" : "(skojarzone z kamieniem nazębnym)zanieczyszczenia nagromadzone w klawiaturze komputera.". Każdy element składa się z klucza i wartości. Klucze znajdują się po lewej stronie dwukropków. Wartości znajdują się po prawej stronie. Więc "Keyboard Plaque" to klucz, a jego wartość to "(skojarzone z kamieniem nazębnym)zanieczyszczenia nagromadzone w klawiaturze komputera.". Klucz jest dosłownie „kluczem” umożliwiającym dostęp do wartości. To oznacza, że mógłbyś użyć klucza "Keyboard Plaque", aby pobrać jego wartość "(skojarzone z kamieniem nazębnym)zanieczyszczenia nagromadzone w klawiaturze komputera.". Aby utworzyć swój własny słownik, postępuj według wzorca, którego ja użyłem. Wpisz klucz, za nim dwukropek i wartość klucza. Do oddzielenia poszczególnych par klucz-wartość użyj przecinków, a całość otocz nawiasami klamrowymi. Tak jak w przypadku krotek czy list, możesz umieścić całość w jednym wierszu lub zapisać każdą parę w oddzielnej linii, przechodząc do nowego wiersza po każdym przecinku.

Uzyskiwanie dostępu do wartości słownika Jedną z najczęstszych czynności, które będziesz wykonywał w stosunku do słownika, jest użycie klucza do pobrania wartości. Istnieje kilka różnych sposobów realizacji tego zadania. Przykład każdego z nich pokażę Ci w tym rozdziale przy użyciu interaktywnego interpretera.

153

154

Rozdział 5. Listy i słowniki. Gra Szubienica

Użycie klucza do pobrania wartości Najprostszy sposób pobrania wartości ze słownika to uzyskanie do niej dostępu za pomocą klucza. Aby uzyskać wartość klucza, umieść go po prostu w nawiasach kwadratowych po nazwie słownika. Oto sesja interaktywna, która ma Ci pokazać, co mam na myśli (przyjmijmy, że już wcześniej zdefiniowałem słownik geek): >>> geek["404"] 'ignorant; od używanego w sieci WWW komunikatu o błędzie 404\n - nie znaleziono strony.' >>> geek["Link Rot"] 'Link Rot" : "(obumieranie linków) proces, w wyniku którego linki do stron WWW stają się nieaktualne.'

Wygląda to podobnie do indeksowania sekwencji, ale istnieje ważna różnica. Kiedy indeksujesz sekwencję, używasz numeru pozycji. Kiedy szukasz wartości w słowniku, używasz klucza. Jest to jedyny bezpośredni sposób pobierania wartości ze słownika. Tak naprawdę słowniki w ogóle nie obsługują numerów pozycji. Czymś, co czasem zbija z tropu początkujących programistów, jest fakt, że w przypadku słownika wartość nie może zostać wykorzystana do uzyskania klucza. To tak, jakby próbować wykorzystać definicję do znalezienia słowa w rzeczywistym słowniku. Prawdziwe słowniki nie są po prostu przystosowane do tego rodzaju rzeczy i dotyczy to również słowników Pythona. Więc zapamiętaj — podajesz klucz i otrzymujesz wartość, nigdy na odwrót.

Pułapka Jeśli spróbujesz uzyskać wartość ze słownika poprzez bezpośredni dostęp za pomocą klucza, który nie istnieje, wygenerujesz błąd: >>> geek["Dancing Baloney"] Traceback (most recent call last): File "", line 1, in geek["Dancing Baloney"] KeyError: 'Dancing Baloney'

Ponieważ "Dancing Baloney" nie jest kluczem w tym słowniku, mamy efekt w postaci błędu. (Nawiasem mówiąc, termin „Dancing Baloney” — tańczące głupoty — oznacza animowaną grafikę i inne efekty wizualne, które nie stanowią istotnej wartości i są często używane przez projektantów stron WWW w celu zrobienia wrażenia na klientach).

Sprawdzanie klucza za pomocą operatora in przed pobraniem wartości Ponieważ użycie nieistniejącego klucza może doprowadzić do błędu, zazwyczaj najlepiej nie podejmować próby bezpośredniego dostępu do słownika bez przedsięwzięcia pewnych środków ostrożności. Jedną z rzeczy, które możesz zrobić, jest sprawdzenie, czy klucz istnieje, przed próbą pobrania jego wartości. Istnienie klucza można sprawdzić za pomocą operatora in:

Używanie słowników >>> if "Dancing Baloney" in geek: print("Wiem, co to jest Dancing Baloney.") else print("Nie mam pojęcia, co to jest Dancing Baloney.") Nie mam pojęcia, co to jest Dancing Baloney.

Ponieważ słownik nie zawiera klucza "Dancing Baloney", warunek "Dancing Baloney" in geek jest fałszywy. Więc komputer stwierdza, że nie wie, co to jest. Operatora in używa się do słowników w dużej mierze w taki sam sposób, jak był stosowany do list czy krotek. Wpisujesz wartość, którą chcesz sprawdzić, a po niej in i nazwę słownika. To tworzy warunek, który jest prawdziwy, jeśli klucz znajduje się w słowniku; w przeciwnym wypadku warunek jest fałszywy. Jest to przydatne przed podjęciem próby pobrania wartości. Lecz pamiętaj, operatorem in możesz sprawdzać tylko klucze; nie można w ten sposób sprawdzać wartości.

Użycie metody get() do pobrania wartości Istnieje także inny sposób pobierania wartości ze słownika. Można wykorzystać metodę słownika o nazwie get(). Ta metoda ma wbudowane zabezpieczenia służące do obsługi sytuacji, w których dochodzi do próby pobrania wartości nieistniejącego klucza. Jeśli klucz nie istnieje, metoda zwraca wartość domyślną, którą można zdefiniować. Spójrz na jeszcze jedną taką próbę: >>> print(geek.get("Dancing Baloney", "Nie mam pojęcia.")) Nie mam pojęcia.

Dzięki użyciu w powyższym przykładzie metody get() uzyskałem gwarancję otrzymania wartości zwrotnej. Jeśli ten termin nie występowałby w słowniku w roli klucza, otrzymałbym jego definicję. Ponieważ go rzeczywiście nie było, otrzymałem domyślną wartość zwrotną, którą zdefiniowałem, łańcuch "Nie mam pojęcia.". By użyć metody get(), wystarczy przy jej wywołaniu podać poszukiwany klucz, a następnie opcjonalną wartość domyślną. Jeśli klucz znajduje się w słowniku, pobierasz jego wartość. Jeśli klucza nie ma w słowniku, otrzymujesz wartość domyślną. Ale istnieje pewna szczególna sytuacja: jeśli nie podasz wartości domyślnej (to Twoja opcja), otrzymasz zwrotnie wartość None. Oto przykład, w którym nie podałem wartości domyślnej: >>> print(geek.get("Dancing Baloney")) None

Skonfigurowanie programu Pora na powrót do kodu programu Translator slangu komputerowego. Po utworzeniu słownika geek zaimplementowałem system menu, z jakim wcześniej się spotkałeś, tym razem obejmujący pięć możliwości wyboru. Tak jak przedtem, jeśli użytkownik wybiera opcję 0, komputer go żegna.

155

156

Rozdział 5. Listy i słowniki. Gra Szubienica choice = None while choice != "0": print( """ Translator slangu komputerowego 0 1 2 3 4 """ )

zakończ znajdź termin dodaj nowy termin zmień definicję terminu usuń termin

choice = input("Wybierasz: ") print() # wyjdź if choice == "0": print("Żegnaj.")

Pobranie wartości Jeśli użytkownik wprowadzi liczbę 1, w kolejnym fragmencie programu zostanie zapytany o termin do odszukania. Komputer sprawdza, czy ten termin znajduje się w słowniku. W przypadku pozytywnej odpowiedzi program realizuje dostęp do słownika, używając terminu jako klucza, pobiera definicję terminu i wyświetla ją. Jeśli terminu nie ma w słowniku, komputer informuje o tym użytkownika. # pobierz definicję elif choice == "1": term = input("Jaki termin mam przetłumaczyć?: ") if term in geek: definition = geek[term] print("\n", term, "oznacza", definition) else: print("\nNiestety, nie znam terminu", term)

Dodanie pary klucz-wartość Słowniki są mutowalne, więc można je modyfikować. Jeśli użytkownik wprowadzi liczbę 2, w kolejnym fragmencie programu do słownika zostanie dodany nowy termin: # dodaj parę termin-definicja elif choice == "2": term = input("Jaki termin mam dodać?: ") if term not in geek: definition = input("\nPodaj definicję tego terminu: ") geek[term] = definition

Używanie słowników print("\nTermin", term, "został dodany.") else: print("\nTen termin już istnieje! Spróbuj zmienić jego definicję.")

Komputer pyta użytkownika o nowy termin do dodania. Jeśli terminu nie ma jeszcze w słowniku, komputer wczytuje jego definicję i dodaje tak skompletowaną parę do słownika: geek[term] = definition

Powyższy kod tworzy nowy element w słowniku geek. Termin jest kluczem, a definicja jego wartością. W dokładnie taki sposób przypisuje się nowy element do słownika. Podajesz nazwę słownika, a po niej klucz w nawiasach kwadratowych, operator przypisania i wartość klucza. Napisałem ten program tak, aby komputer odmówił dodania terminu, jeśli znajduje się już w słowniku. Jest to zabezpieczenie, które stworzyłem w celu uzyskania pewności, że użytkownik nie nadpisze przypadkowo istniejącego wcześniej terminu. Jeśli użytkownik chce faktycznie zmienić definicję istniejącego terminu, powinien wybrać opcję 3.

Sztuczka Szczypta pesymizmu nie zaszkodzi, przynajmniej przy programowaniu. Jak widziałeś, założyłem, że użytkownik mógłby spróbować dodać nowy termin, nie zdając sobie sprawy z tego, że jest już on w słowniku. Jeżeli nie sprawdziłbym tego, użytkownik mógłby nieświadomie nadpisać termin. Kiedy będziesz pisać swoje własne programy, spróbuj pomyśleć o rzeczach, które mogłyby pójść w złą stronę, a potem staraj się upewnić, że Twój program będzie sobie z nimi radził. Więc bądź troszkę pesymistą.

Wymiana pary klucz-wartość Jeśli użytkownik wprowadzi liczbę 3, w kolejnym fragmencie programu nastąpi wymiana pary klucz-wartość: # zmiana definicji istniejącego terminu elif choice == "3": term = input("Jaki termin mam przedefiniować?: ") if term in geek: definition = input("Jaka jest nowa definicja?: ") geek[term] = definition print("\nTermin", term, "został przedefiniowany.") else: print("\nTen termin nie istnieje! Spróbuj go dodać.")

Aby wymienić parę klucz-wartość, użyłem dokładnie takiego samego wiersza kodu jak przy dodawaniu nowej pary: geek[term] = definition

Python zastępuje dotychczasową wartość (definicję) nową.

157

158

Rozdział 5. Listy i słowniki. Gra Szubienica

Pułapka Jeśli przypiszesz wartość do słownika przy użyciu klucza, który już istnieje, Python zastąpi dotychczasową wartość bez protestu. Musisz zatem uważać, ponieważ mógłbyś nadpisać wartość istniejącego klucza, nie zdając sobie z tego sprawy.

Usunięcie pary klucz-wartość Jeśli użytkownik wprowadzi liczbę 4, zostanie wykonany następujący blok elif: # usunięcie pary termin-definicja elif choice == "4": term = input("Jaki termin mam usunąć?: ") if term in geek: del geek[term] print("\nOK, usunąłem go", term) else: print("\nNie mogę tego zrobić! Terminu", term, "nie ma w słowniku.")

Program pyta użytkownika o termin ze slangu komputerowego, który to termin ma zostać usunięty. Następnie program sprawdza za pomocą operatora in, czy wprowadzony przez użytkownika termin faktycznie znajduje się w słowniku. Jeśli się w nim znajduje, właściwy element zostaje usunięty za pomocą instrukcji: del geek[term]

Oznacza to usunięcie elementu z kluczem term ze słownika geek. W taki sposób można usunąć dowolny element słownika. Wystarczy umieścić del przed wyrażeniem złożonym z nazwy słownika i ujętego w nawiasy kwadratowe klucza elementu, który ma zostać usunięty. Jeśli na początku okaże się, że żargonowego terminu nie ma w słowniku, wykonywana jest klauzula else i komputer powiadamia użytkownika o tym fakcie.

Pułapka Próba usunięcia elementu słownika poprzez klucz, który nie istnieje, spowoduje błąd. Upewnienie się, że klucz, którego chcesz użyć, istnieje, jest mądrym posunięciem.

Dokończenie programu Końcowa klauzula else informuje użytkownika, że wprowadził niepoprawną wartość opcji: # nieznana opcja else: print("\nNiestety,", choice, "to nieprawidłowy wybór.") input("\n\nAby zakończyć program, naciśnij klawisz Enter.")

Powrót do gry Szubienica

Wymagania dotyczące struktury słownika Jest kilka kwestii, o których powinieneś pamiętać, tworząc słowniki:  Słownik nie może zawierać wielu elementów z tym samym kluczem. Pomyśl znowu

o prawdziwym słowniku. Stałby się dość niezrozumiały, gdybyś mógł dodawać to samo słowo z całkowicie nowymi definicjami, ilekroć chciałbyś to zrobić.  Klucz musi być niemutowalny. Może być łańcuchem znaków, liczbą lub krotką,

co daje Ci mnóstwo możliwości. Klucz musi być niemutowalny, bo gdyby nie był taki, mógłbyś się później do niego wkraść i pozmieniać klucze, co prawdopodobnie skończyłoby się pojawieniem się dwóch identycznych kluczy. A dopiero co się dowiedziałeś, że jest to niedopuszczalne!  Wartości nie muszą być unikalne. Poza tym mogą być mutowalne

lub niemutowalne. Mogą być zupełnie dowolne. Ze słownikami można zrobić nawet jeszcze więcej rzeczy. Tabela 5.2 podsumowuje niektóre użyteczne metody, które mogą Ci pomóc wydobyć z tego typu danych więcej możliwości. Tabela 5.2. Wybrane metody słownika

Metoda

Opis

get(klucz, [wartość_ domyślna])

Zwraca wartość klucza. Jeśli klucz nie istnieje, zwracana jest opcjonalna wartość_domyślna. Jeśli klucz nie istnieje i wartość_ domyślna nie została określona, zwracana jest wartość None.

keys()

Zwraca widok wszystkich kluczy występujących w słowniku.

values()

Zwraca widok wszystkich wartości występujących w słowniku.

items()

Zwraca widok wszystkich elementów słownika. Każdy element to dwuskładnikowa krotka, której pierwszym składnikiem jest klucz, a drugim wartość klucza.

Pułapka Widoki słownika — zwracane przez metody keys(), values() i items() — są pod pewnymi względami podobne do list. Można po nich iterować za pomocą pętli for. Nie są to jednak listy. Nie mogą na przykład być indeksowane. W dodatku widoki są dynamiczne, co oznacza, że ich zawartość nie jest niezależna od związanych z nimi słowników. Więc zmiana w słowniku znajduje swoje odbicie w widokach tego słownika. Aby dowiedzieć się więcej o widokach, zajrzyj do dokumentacji zamieszczonej na oficjalnej stronie Pythona (www.python.org).

Powrót do gry Szubienica Łącząc to wszystko, czego się do tej pory nauczyłeś, możesz utworzyć grę Szubienica zaprezentowaną na początku tego rozdziału. Program jest dużo dłuższy od któregokolwiek z dotychczas poznanych, ale nie przerażaj się jego rozmiarem. Jego kod nie jest dużo

159

160

Rozdział 5. Listy i słowniki. Gra Szubienica

bardziej skomplikowany niż w przypadku innych projektów, jakie przeanalizowałeś. Największą część tego programu stanowi moja skromna twórczość z dziedziny ASCII-art — osiem wersji wieszanego ludzika z „patyczków”. Konkretna substancja tego programu zajmuje niewiele więcej niż pełny ekran kodu.

Skonfigurowanie programu Ale zacznijmy od początku. Jak zawsze rozpocząłem od otwierających komentarzy wyjaśniających program. Następnie zaimportowałem moduł random. Potrzebuję tego modułu do losowego wybrania słowa z sekwencji. Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 5.; nazwa pliku to szubienica.py. # # # # #

Szubienica Klasyczna gra w szubienicę. Komputer losowo wybiera słowo, a gracz próbuje odgadnąć jego poszczególne litery. Jeśli gracz nie odgadnie w porę całego słowa, mały ludzik zostaje powieszony.

# import modułów import random

Tworzenie stałych Chociaż ten kolejny fragment programu obejmuje kilka ekranów kodu, tworzę w nim tylko trzy stałe. Najpierw utworzyłem największą krotkę, z jaką się spotkałeś. To tak naprawdę tylko sekwencja ośmiu elementów, ale każdy element to łańcuch w potrójnym cudzysłowie, który obejmuje 12 wierszy. Każdy łańcuch przedstawia szubienicę, na której jest wieszany ludzik z patyków. Każdy kolejny łańcuch pokazuje coraz bardziej kompletną postać. Za każdym razem, gdy odpowiedź gracza jest niepoprawna, wyświetlany jest następny łańcuch. Po siedmiu nietrafnych odpowiedziach rysunek jest już kompletny, a ludzik zostaje nieboszczykiem. Jeśli ten ostatni łańcuch zostaje wyświetlony, gracz poniósł porażkę i gra się skończyła. Przypisałem tę krotkę do zmiennej HANGMAN, której nazwa zawiera same duże litery, ponieważ będę jej używał jako stałej. # stałe HANGMAN = ( """ -----| | | | | | | | | ----------

Powrót do gry Szubienica """, """ -----| | | O | | | | | | ---------""", """ -----| | | O | -+| | | | | ---------""", """ -----| | | O | /-+| | | | | ---------""", """ -----| | | O | /-+-/ | | | | | ---------""", """ -----| | | O

161

162

Rozdział 5. Listy i słowniki. Gra Szubienica | /-+-/ | | | | | | ---------""", """ -----| | | O | /-+-/ | | | | | | | | | ---------""", """ -----| | | O | /-+-/ | | | | | | | | | | | ---------""")

Następnie utworzyłem stałą reprezentującą maksymalną liczbę nieudanych prób odgadnięcia litery, które gracz może podjąć przed zakończeniem gry: MAX_WRONG = len(HANGMAN) - 1

Maksymalna liczba niepoprawnych prób odgadnięcia jest o jeden mniejsza niż długość krotki HANGMAN. Jest to podyktowane tym, że pierwszy rysunek pustej szubienicy jest wyświetlany, jeszcze zanim gracz podejmie swoją pierwszą próbę. Więc chociaż w krotce HANGMAN znajduje się osiem rysunków, gracz otrzymuje tylko siedem możliwości popełnienia błędu, zanim gra się zakończy. W końcu utworzyłem krotkę zawierającą wszystkie słowa, które może wybrać komputer do odgadnięcia przez gracza. Możesz śmiało zmodyfikować program i sporządzić swoją własną listę tych słów. WORDS = ("NADUŻYWANY", "MAŁŻ", "GUAM", "TAFTA", "PYTHON")

Powrót do gry Szubienica

Inicjalizacja zmiennych Następnie przeprowadziłem inicjalizację zmiennych. W celu losowego wybrania słowa z listy możliwych posłużyłem się funkcją random.choice(). Przypisałem to tajemne słowo do zmiennej word. # inicjalizacja zmiennych word = random.choice(WORDS)

# słowo do odgadnięcia

Utworzyłem jeszcze jeden łańcuch o nazwie so_far odzwierciedlający to, co już graczowi udało się w bieżącej turze gry odgadnąć. Początkowa postać łańcucha to ciąg kresek, z których każda odpowiada pojedynczej literze słowa. Kiedy gracz odgadnie prawidłowo jakąś literę, kreski zajmujące jej pozycje zostaną zastąpione samą literą. so_far = "-" * len(word)

# kreska zastępuje nieodgadniętą literę

Utworzyłem zmienną wrong i przypisałem jej liczbę 0. Zmienna wrong rejestruje liczbę nieudanych prób odgadnięcia litery dokonanych przez gracza. wrong = 0

# liczba nietrafionych liter

Utworzyłem też pustą listę, used, która ma zawierać wszystkie litery, których gracz użył w trakcie odgadywania: used = []

# litery już użyte w zgadywaniu

Utworzenie głównej pętli programu Utworzyłem pętlę, która jest wykonywana, dopóki gracz nie wprowadzi zbyt dużej liczby błędnych liter albo nie odgadnie wszystkich liter występujących w słowie: print("Witaj w grze 'Szubienica'.

Powodzenia!")

while wrong < MAX_WRONG and so_far != word: print(HANGMAN[wrong]) print("\nWykorzystałeś już następujące litery:\n", used) print("\nNa razie zagadkowe słowo wygląda tak:\n", so_far)

Następnie wyświetlam aktualną postać ludzika na podstawie liczby nieudanych prób odgadnięcia litery dokonanych przez gracza. Im więcej gracz takich prób wykonał, tym bliższy swojego końca jest ludzik. Potem wyświetlam listę liter, których gracz użył w tej grze. A następnie pokazuję, jak aktualnie wygląda częściowo odgadnięte słowo.

Pobranie litery wprowadzonej przez gracza Wczytuję literę wprowadzoną przez gracza i zamieniam ją na dużą, tak aby można ją było znaleźć w tajemnym słowie (które zostało zapisane przy użyciu samych dużych liter). Potem sprawdzam, czy gracz nie użył już tej litery. Jeśli gracz już przedtem podał tę literę, wymuszam na nim wprowadzenie nowego znaku, dopóki nie wprowadzi takiego, jakiego dotychczas nie użył. Kiedy gracz wprowadzi poprawną literę, przekształcam ją na dużą i dodaję do listy użytych liter.

163

164

Rozdział 5. Listy i słowniki. Gra Szubienica guess = input("\n\nWprowadź literę: ") guess = guess.upper() while guess in used: print("Już wykorzystałeś literę", guess) guess = input("Wprowadź literę: ") guess = guess.upper() used.append(guess)

Sprawdzenie, czy odgadywana litera znajduje się w słowie Następnie sprawdzam, czy litera występuje w tajemnym słowie. Jeśli tak, powiadamiam o tym gracza. Potem przystępuję do tworzenia nowej wersji łańcucha so_far, w którym ta nowa litera wystąpi we wszystkich miejscach, w których występuje w tajemnym słowie. if guess in word: print("\nTak!", guess, "znajduje się w zagadkowym słowie!") # utwórz nową wersję zmiennej so_far, aby zawierała odgadniętą literę new = "" for i in range(len(word)): if guess == word[i]: new += guess else: new += so_far[i] so_far = new

Jeśli litera zaproponowana przez gracza nie występuje w słowie, powiadamiam go o tym i zwiększam liczbę błędnych prób o jeden. else: print("\nNiestety,", guess, "nie występuje w zagadkowym słowie.") wrong += 1

Zakończenie gry W tym momencie gra została zakończona. Jeśli liczba nieudanych prób odgadnięcia litery osiągnęła wartość maksymalną, gracz przegrał, więc wyświetlam końcowy rysunek ludzika. W przeciwnym razie gratuluję graczowi. W każdym z tych przypadków informuję gracza, jakie to było słowo. if wrong == MAX_WRONG: print(HANGMAN[wrong]) print("\nZostałeś powieszony!") else: print("\nOdgadłeś!") print("\nZagadkowe słowo to", word) input("\n\nAby zakończyć program, naciśnij klawisz Enter.")

Podsumowanie

Podsumowanie W tym rozdziale dowiedziałeś się wszystkiego o listach i słownikach — dwóch nowych typach danych. Dowiedziałeś się, że listy są mutowalnymi sekwencjami. Zobaczyłeś, jak można dodawać, usuwać i sortować elementy listy, a nawet jak odwracać ich kolejność. Ale dowiedziałeś się również, że pomimo tego wszystkiego, co oferują listy, są pewne przypadki, w których mniej elastyczna krotka jest akurat lepszym (lub wymaganym) wyborem. Poznałeś również referencje współdzielone, które mogą występować przy mutowalnych typach danych, i zobaczyłeś, jak ich unikać, gdy to konieczne. Widziałeś, jak można tworzyć sekwencje zagnieżdżone i jak je można wykorzystywać do obsługi jeszcze ciekawszych struktur informacji, takich jak lista najlepszych wyników. Dowiedziałeś się także, jak tworzyć i modyfikować słowniki, które umożliwiają obsługę par danych.

Sprawdź swoje umiejętności 1. Utwórz program, który wypisuje listę słów w przypadkowej kolejności. Program powinien wypisać wszystkie słowa bez żadnych powtórzeń. 2. Napisz program Kreator postaci do gry z podziałem na role. Gracz powinien otrzymać pulę 30 punktów, którą może spożytkować na cztery atrybuty: siła, zdrowie, mądrość i zręczność. Gracz powinien mieć możliwość przeznaczania punktów z puli na dowolny atrybut, jak również możliwość odbierania punktów przypisanych do atrybutu i przekazywania ich z powrotem do puli. 3. Napisz program Kto jest twoim tatą?, który umożliwia użytkownikowi wprowadzenie imienia i nazwiska osoby płci męskiej i przedstawia imię i nazwisko jej ojca. (Możesz dla zabawy wykorzystać nazwiska celebrytów, osób fikcyjnych lub nawet postaci historycznych). Umożliw użytkownikowi dodawanie, wymianę i usuwanie par syn-ojciec. 4. Udoskonal program Kto jest moim tatą? poprzez dodanie opcji, która umożliwi użytkownikowi wprowadzenie imienia i (lub) nazwiska jakiejś osoby i uzyskanie odpowiedzi, kto jest jej dziadkiem. Twój program powinien nadal wykorzystywać tylko jeden słownik par syn-ojciec. Pamiętaj, aby włączyć do swojego słownika kilka pokoleń, tak aby możliwe było tego rodzaju dopasowanie.

165

166

Rozdział 5. Listy i słowniki. Gra Szubienica

6 Funkcje. Gra Kółko i krzyżyk K

ażdy program, który do tej pory pisałeś, był jednym, nieprzerwanym ciągiem instrukcji. Kiedy już jednak programy osiągną pewien rozmiar lub stopień złożoności, ten sposób pracy z nimi staje się trudny. Na szczęście istnieją metody rozbicia dużych programów na mniejsze, łatwe do opanowania kawałki kodu. W tym rozdziale poznasz jeden ze sposobów realizacji tego zadania poprzez tworzenie swoich własnych funkcji. W szczególności w tym rozdziale nauczysz się:  pisać swoje własne funkcje,  przyjmować w swoich funkcjach wartości z zewnątrz poprzez parametry,  zwracać ze swoich funkcji informacje poprzez wartości zwrotne,  wykorzystywać zmienne globalne i stałe,  tworzyć komputerowego przeciwnika w grze strategicznej.

Wprowadzenie do gry Kółko i krzyżyk Dzięki projektowi z tego rozdziału dowiesz się, jak utworzyć komputerowego przeciwnika przy użyciu szczypty sztucznej inteligencji (ang. artificial intelligence — AI). Gracz i komputer stają do ostatecznej rozgrywki między człowiekiem a maszyną w grze o wysokie stawki o nazwie Kółko i krzyżyk. Komputer prowadzi wspaniałą, choć niedoskonałą grę i wykazuje się podejściem, które sprawia, że każda rozgrywka dostarcza dobrej zabawy. Rysunki od 6.1 do 6.3 ilustrują przebieg gry.

168

Rozdział 6. Funkcje. Gra Kółko i krzyżyk

Rysunek 6.1. Komputerowi nie brakuje pewności siebie

Rysunek 6.2. Nie zauważyłem tego zagrożenia. Nawet po zastosowaniu prostych technik programowania komputer potrafi wykonywać dość dobre posunięcia

Rysunek 6.3. Znalazłem słabą stronę komputera i tym razem wygrałem

Tworzenie funkcji

Tworzenie funkcji Już miałeś okazję zobaczyć kilka funkcji wbudowanych w działaniu, w tym funkcje len() i range(). Jeśli Ci one nie wystarczają, Python umożliwi Ci utworzenie swoich własnych. Twoje funkcje działają dokładnie tak samo jak te, które są standardowo dostępne w języku. Uruchamiają się i wykonują zadanie, a potem zwracają sterowanie do programu. Tworzenie swoich własnych funkcji daje wiele korzyści. Jedną z największych jest możliwość rozbicia kodu na łatwe do ogarnięcia, niewielkie kawałki. Programy składające się z jednego, długiego ciągu instrukcji, niepodzielne pod względem logicznym są trudne do pisania, zrozumienia i konserwacji. Programy, które są zbudowane z funkcji, mogą być łatwiejsze w tworzeniu i użytkowaniu. Podobnie jak funkcje, z którymi już się spotkałeś, Twoje nowe funkcje powinny dobrze wykonywać jedno zadanie.

Prezentacja programu Instrukcja Na podstawie zrzutów ekranu z grą Kółko i krzyżyk, możesz zapewne stwierdzić, że komputerowy przeciwnik jest troszkę arogancki. Widać to całkiem jasno z instrukcji, jaką komputer wyświetla przed rozpoczęciem gry. Kodowi, który tworzy tę instrukcję, przyjrzysz się w kolejnym programie, o nazwie Instrukcja. Ten kod jest nieco inny od tego, czego mógłbyś oczekiwać. Jest tak dlatego, że utworzyłem funkcję do wyświetlania instrukcji. Tej samej funkcji użyłem w programie Instrukcja. Spójrz na rysunek 6.4, aby zobaczyć przykładowe uruchomienie programu.

Rysunek 6.4. Instrukcja jest za każdym razem wyświetlana za pomocą tylko jednego wiersza kodu — wywołania utworzonej przeze mnie funkcji

169

170

Rozdział 6. Funkcje. Gra Kółko i krzyżyk

Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 6.; nazwa pliku to instrukcja.py. # Instrukcja # Demonstruje funkcje tworzone przez programistę def instructions(): """Wyświetl instrukcję gry.""" print( """ Witaj w największym intelektualnym wyzwaniu wszech czasów, jakim jest gra 'Kółko i krzyżyk'. Będzie to ostateczna rozgrywka między Twoim ludzkim mózgiem a moim krzemowym procesorem. Swoje posunięcie wskażesz poprzez wprowadzenie liczby z zakresu 0 - 8. Liczba ta odpowiada pozycji na planszy zgodnie z poniższym schematem: 0 | 1 | 2 --------3 | 4 | 5 --------6 | 7 | 8 Przygotuj się, Człowieku. """ )

Ostateczna batalia niebawem się rozpocznie. \n

# main print("Oto instrukcja do gry 'Kółko i krzyżyk':") instructions() print("Ponownie ta sama instrukcja:") instructions() print("Prawdopodobnie teraz już zrozumiałeś tę grę.") input("\n\nAby zakończyć program, naciśnij klawisz Enter.")

Definiowanie funkcji Rozpocząłem definicję swojej nowej funkcji od pojedynczego wiersza: def instructions():

Wiersz ten informuje komputer, że blok kodu, który po nim wystąpi, ma zostać potraktowany w całości jako funkcja instructions(). Zasadniczo nadaję temu blokowi instrukcji nazwę. To oznacza, że ilekroć w tym programie wywołam funkcję instructions(), zostanie wykonany ten blok kodu. Ten wiersz i następujący po nim blok instrukcji stanowią definicję funkcji. Określają, co funkcja robi, ale jej nie uruchamiają. Kiedy komputer napotyka definicję funkcji, odnotowuje, że ta funkcja istnieje, więc może jej później użyć. Nie uruchomi tej funkcji, dopóki nie napotka w dalszej części programu odnoszącego się do niej wywołania.

Tworzenie funkcji

Aby zdefiniować swoją własną funkcję, naśladuj mój przykład. Zacznij od słowa def, po którym wpisz nazwę funkcji z parą nawiasów, następnie dwukropek i wcięty blok instrukcji. Przy wyborze nazwy funkcji przestrzegaj podstawowych reguł odnoszących się do nazywania zmiennych. Spróbuj także użyć nazwy, która odzwierciedla to, co funkcja tworzy, lub to, jaką czynność wykonuje.

Dokumentowanie funkcji Funkcje zawierają specjalny mechanizm, który pozwala na ich dokumentowanie za pomocą czegoś, co nazywa się łańcuchem dokumentacyjnym (ang. documentation string, docstring). Do funkcji instructions() utworzyłem następujący łańcuch dokumentacyjny: """Wyświetl instrukcję gry."""

Łańcuch dokumentacyjny jest na ogół łańcuchem w potrójnym cudzysłowie i jeśli takiego używasz, musi stanowić pierwszy wiersz bloku kodu Twojej funkcji. W przypadku prostych funkcji możesz zrobić to, co ja w tym programie — wpisać pojedyncze zdanie, które opisuje to, co funkcja robi. Funkcje działają równie dobrze bez łańcuchów dokumentacyjnych, ale ich używanie jest dobrym pomysłem. Pozwala Ci nabrać zwyczaju komentowania swojego kodu i zmusza Cię do opisania jednego, dobrze zdefiniowanego zadania funkcji. Łańcuch dokumentacyjny funkcji może także wyświetlić się w postaci interaktywnej dokumentacji, kiedy wpiszesz jego wywołanie w IDLE.

Wywołanie funkcji utworzonej przez programistę Wywołanie funkcji utworzonej przez programistę działa dokładnie tak samo jak wywołanie funkcji wbudowanej. Należy użyć nazwy funkcji i umieszczonej za nią pary nawiasów. Ja wywołałem swoją nową funkcję kilkakrotnie, za każdym razem za pomocą wiersza: instructions()

To wywołanie nakazuje komputerowi uruchomienie i wykonanie funkcji, którą wcześniej zdefiniowałem. Więc ilekroć tego wiersza używam, komputer wypisuje instrukcję gry.

Pojęcie abstrakcji Pisząc i wywołując funkcje, stosujesz w praktyce coś, co jest znane pod nazwą abstrakcji. Abstrakcja pozwala Ci myśleć o ogólnym obrazie bez troszczenia się o szczegóły. Więc i w tym programie mogę używać funkcji instructions(), nie martwiąc się o szczegóły związane z wyświetlaniem tekstu. Wszystko, co mam do zrobienia, to wywołanie funkcji w pojedynczym wierszu kodu, a ona wykonuje całe zadanie. Mógłbyś być zaskoczony tym, gdzie można spotkać się z abstrakcją, ale ludzie korzystają z niej nieustannie. Weź na przykład pod uwagę dwóch pracowników w lokalu fastfoodowym. Jeśli jeden mówi do drugiego, że właśnie załadował trójkę i podliczył ją, drugi pracownik wie, że ten pierwszy odebrał od klienta zamówienie, podszedł do

171

172

Rozdział 6. Funkcje. Gra Kółko i krzyżyk

podgrzewaczy, złapał hamburgera, podszedł do frytkownicy, napełnił największy kartonowy pojemnik frytkami, podszedł do saturatora, chwycił największy kubek, napełnił go wodą sodową, podał to wszystko klientowi, wziął od niego pieniądze i wydał mu resztę. Taka wersja konwersacji byłaby nie tylko nudna, ale również niepotrzebna. Obydwaj pracownicy wiedzą, co to znaczy załadować trójkę i ją podliczyć. Nie muszą zawracać sobie głowy wszystkimi szczegółami, ponieważ wykorzystują abstrakcję.

Używanie parametrów i wartości zwrotnych Jak już widziałeś przy omawianiu funkcji wbudowanych, możesz dostarczać do funkcji pewne wartości i otrzymywać z powrotem wartości od niej. Na przykład w przypadku funkcji len() dostarczasz sekwencję, a funkcja zwraca jej długość. Twoje własne funkcje mogą również otrzymywać i zwracać wartości. To umożliwia Twoim funkcjom komunikowanie się z resztą programu.

Prezentacja programu Pobierz i zwróć Utworzyłem w programie Pobierz i zwróć trzy funkcje, aby pokazać różne kombinacje pobierania i zwracania wartości. Jedna funkcja pobiera wartość. Następna zwraca pewną wartość. A ostatnia funkcja zarówno pobiera, jak i zwraca wartość. Przyjrzyj się rysunkowi 6.5, aby zobaczyć, co dokładnie się dzieje w wyniku uruchomienia programu.

Rysunek 6.5. Każda z funkcji wykorzystuje albo parametr, albo wartość zwrotną, albo obie wartości do komunikowania się z główną częścią programu

Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 6.; nazwa pliku to pobierz_i_zwroc.py. # Pobierz i zwróć # Demonstruje parametry i wartości zwrotne def display(message): print(message) def give_me_five():

Używanie parametrów i wartości zwrotnych five = 5 return five def ask_yes_no(question): """Zadaj pytanie, na które można odpowiedzieć tak lub nie.""" response = None while response not in ("t", "n"): response = input(question).lower() return response # main display("To wiadomość dla Ciebie.\n") number = give_me_five() print("Oto co otrzymałem z funkcji give_me_five():", number) answer = ask_yes_no("\nProszę wprowadzić 't' lub 'n': ") print("Dziękuję za wprowadzenie:", answer) input("\n\nAby zakończyć program, naciśnij klawisz Enter.")

Otrzymywanie informacji poprzez parametry Pierwsza funkcja, jaką zdefiniowałem, display(), otrzymuje wartość i wypisuje ją. Wartość tę otrzymuje poprzez swój parametr. Parametry są zasadniczo nazwami zmiennych występującymi wewnątrz nawiasów nagłówka funkcji: def display(message):

Parametry przechwytują wartości przesłane do funkcji z wywołania funkcji poprzez jego argumenty. Więc w tym przypadku, kiedy wywoływana jest funkcja display(), parametrowi message zostaje przypisana wartość dostarczona przez argument "To wiadomość dla Ciebie.\n". W głównej części programu wywołuję funkcję display() za pomocą instrukcji: display("To wiadomość dla Ciebie.\n")

W rezultacie parametr message otrzymuje wartość w postaci łańcucha "To wiadomość dla Ciebie.\n". Potem funkcja jest wykonywana. Parametr message, jak każdy inny parametr, istnieje wewnątrz funkcji jako zmienna. Tak więc wiersz kodu: print(message)

wyświetla łańcuch "To wiadomość dla Ciebie.\n". Jeśli nie przekazałbym wartości do parametru message, wygenerowałbym błąd. Funkcja display() wymaga podania dokładnie jednej wartości jako argumentu. Chociaż funkcja display() ma tylko jeden parametr, inne funkcje mogą posiadać ich wiele. Aby zdefiniować funkcję z wieloma parametrami, wymień wszystkie, oddzielając je przecinkami.

173

174

Rozdział 6. Funkcje. Gra Kółko i krzyżyk

Zwracanie informacji poprzez wartości zwrotne Kolejna funkcja, jaką napisałem, give_me_five(), zwraca wartość. Zwraca ją poprzez (możesz wierzyć lub nie) instrukcję return: return five

Kiedy wykonywany jest ten wiersz kodu, funkcja przekazuje wartość zmiennej five z powrotem do tej części programu, w której została wywołana, a następnie kończy swoje działanie. Funkcja zawsze kończy swoją pracę po napotkaniu instrukcji return. Przechwycenie wartości zwróconej przez funkcję i zrobienie coś z nią jest zadaniem tej części programu, która tę funkcję wywołała. Oto główna część programu, w której wywołałem funkcję: number = give_me_five() print("Oto co otrzymałem z funkcji give_me_five():", number)

Zastosowałem sposób przechwycenia wartości zwrotnej funkcji poprzez przypisanie wyniku wywołania funkcji do zmiennej number. Więc kiedy funkcja kończy swoje działanie, zmienna number otrzymuje wartość zwrotną funkcji give_me_five(), która jest równa 5. Następny wiersz kodu wypisuje wartość zmiennej number, aby pokazać, że zmienna otrzymała wartość zwrotną w prawidłowy sposób. Można przekazać z powrotem z funkcji więcej niż jedną wartość. Wystarczy wymienić wszystkie wartości, które mają być zwrócone, oddzielając je przecinkami.

Pułapka Pamiętaj, aby przewidzieć wystarczającą liczbę zmiennych do przechwycenia wszystkich wartości zwrotnych funkcji. Jeśli w przypisaniu nie użyjesz właściwej ich liczby, wygenerujesz błąd.

Pojęcie hermetyzacji Być może nie widzisz potrzeby korzystania z wartości zwrotnych w sytuacji, gdy używasz swoich własnych funkcji. Dlaczego nie wykorzystać samej zmiennej five po powrocie do głównej części programu? Ponieważ jest to niemożliwe. Zmienna five nie istnieje na zewnątrz funkcji give_me_five(). W gruncie rzeczy żadna zmienna, którą utworzysz w funkcji, nie wyłączając parametrów, nie jest bezpośrednio dostępna na zewnątrz niej samej. To dobra zasada, która nazywa się hermetyzacją (lub kapsułkowaniem, ang. encapsulation). Hermetyzacja prawdziwie pomaga oddzielić niezależny kod poprzez ukrycie, czyli zakapsułkowanie jego szczegółów. To dlatego używa się parametrów i wartości zwrotnych — do przekazywania tylko tych informacji, które muszą być wymieniane. Poza tym nie musisz śledzić zmiennych, które tworzysz wewnątrz funkcji, w pozostałej części programu. W sytuacji, gdy Twoje programy się rozrastają, to wielka korzyść. Hermetyzacja może bardzo przypominać abstrakcję. To dlatego, że te dwa pojęcia są ze sobą blisko związane. Hermetyzacja stanowi główny mechanizm abstrakcji. Abstrakcja chroni Cię przed koniecznością troszczenia się o szczegóły. Hermetyzacja ukrywa przed

Używanie parametrów i wartości zwrotnych

Tobą szczegóły. Jako przykład weź pod uwagę pilot do telewizora wyposażony w przyciski zwiększania i zmniejszania głośności. Kiedy używasz pilota do zmiany głośności stosujesz abstrakcję, ponieważ nie musisz wiedzieć, co się dzieje wewnątrz telewizora, żeby to działało. Teraz przypuśćmy, że pilot ma 10 poziomów głośności. Możesz uzyskać je wszystkie przy użyciu pilota, ale nie masz do nich bezpośredniego dostępu. To znaczy nie możesz bezpośrednio (poprzez numer) wybrać określonego poziomu głośności. Możesz tylko używać przycisków zwiększających lub zmniejszających głośność, aby w końcu uzyskać poziom, jakiego potrzebujesz. Sam poziom głośności (jego numer) został zakapsułkowany i nie jest dla Ciebie bezpośrednio dostępny.

Wskazówka Nie martw się, jeśli jeszcze nie zrozumiałeś całkowicie subtelnej różnicy między abstrakcją a hermetyzacją. Te pojęcia splatają się ze sobą, więc sprawa może być nieco złożona. Poza tym będziesz mógł zobaczyć je ponownie w działaniu, kiedy będziesz poznawał obiekty programowe i programowanie obiektowe w rozdziałach 8. i 9.

Otrzymywanie i zwracanie wartości w tej samej funkcji Ostatnia funkcja, jaką napisałem, ask_yes_no(), otrzymuje jedną wartość i zwraca drugą. Pobiera pytanie i zwraca odpowiedź użytkownika, "t" lub "n". Funkcja otrzymuje pytanie poprzez swój parametr: def ask_yes_no(question):

Parametr question otrzymuje wartość argumentu przekazanego do funkcji. W tym przypadku jest to łańcuch "\nProszę wprowadzić 't' or 'n': ". Kolejna część funkcji wykorzystuje ten łańcuch do poproszenia użytkownika o podanie odpowiedzi: response = None while response not in ("t", "n"): response = input(question).lower()

Pętla while powtarza zadawanie tego pytania, dopóki użytkownik nie wprowadzi t, T, n lub N. Funkcja zawsze przekształca to, co wprowadził użytkownik, na małe litery. W końcu, kiedy użytkownik wprowadzi prawidłową odpowiedź, funkcja przesyła łańcuch z powrotem do tej części programu, z której została wywołana: return response

i kończy swoje działanie. W głównej części programu wartość zwrotna zostaje przypisana do zmiennej answer i wyświetlona: answer = ask_yes_no("\nProszę wprowadzić 't' or 'n': ") print("Dziękuję za wprowadzenie:", answer)

175

176

Rozdział 6. Funkcje. Gra Kółko i krzyżyk

Ponowne wykorzystanie kodu Inną wspaniałą właściwością funkcji jest to, że mogą być łatwo ponownie wykorzystane w innych programach. Na przykład zadawanie użytkownikowi pytania, na które można odpowiedzieć „tak” lub „nie”, jest tak często występującą czynnością, że można by sięgnąć po funkcję ask_yes_no() i wykorzystać ją w innym programie bez tworzenia jakiegoś dodatkowego kodu. Ten typ działania nazywa się ponownym wykorzystaniem kodu. Więc pisanie dobrych funkcji nie tylko pozwala na zaoszczędzenie czasu i energii w bieżącym projekcie, ale może również zmniejszyć Twój wysiłek, który będziesz musiał włożyć w przyszłe projekty!

W świecie rzeczywistym Wymyślanie koła na nowo jest zawsze stratą czasu, więc ponowne użycie kodu poprzez wykorzystanie istniejącego oprogramowania i innych elementów projektu w nowych projektach to technika, którą biznes wziął sobie do serca. Ponowne wykorzystanie kodu pozwala:  zwiększyć wydajność przedsiębiorstwa — dzięki ponownemu wykorzystaniu kodu i innych elementów, które już istnieją, firmy mogą realizować swoje projekty przy mniejszym wysiłku;  poprawić jakość oprogramowania — jeśli przedsiębiorstwo już przetestowało jakąś część kodu, może używać tego kodu, wiedząc, że jest wolny od błędów;  zapewnić spójność produktów programowych — używając na przykład tego samego interfejsu użytkownika, firmy mogą tworzyć nowe oprogramowanie, z którym użytkownikom od razu będzie się wygodnie pracowało;  poprawić wydajność oprogramowania — jeśli przedsiębiorstwo dysponuje dobrym sposobem realizacji zadania poprzez oprogramowanie, ponowne jego użycie nie tylko zaoszczędza firmie trudu wymyślania koła na nowo, ale także chroni ją przed możliwością wynalezienia mniej efektywnego koła.

Jednym ze sposobów wykorzystania napisanych przez Ciebie funkcji jest przekopiowanie ich do Twojego nowego programu. Ale istnieje lepszy sposób. Możesz utworzyć swoje własne moduły i zaimportować (za pomocą instrukcji import) swoje funkcje do nowego programu, podobnie jak ja importowałem standardowe moduły Pythona i używałem ich funkcji. Jak tworzyć swoje własne moduły i importować napisany przez siebie kod do ponownego wykorzystania, dowiesz się w rozdziale 9., w podrozdziale „Tworzenie modułów”.

Wykorzystanie argumentów nazwanych i domyślnych wartości parametrów Przekazywanie wartości przez argumenty do parametrów umożliwia dostarczanie do funkcji informacji. Lecz do tej pory spotkałeś się tylko z najbardziej podstawowym sposobem realizacji tego zadania. Python pozwala na większą kontrolę i elastyczność

Wykorzystanie argumentów nazwanych i domyślnych wartości parametrów

co do sposobu przekazywania informacji dzięki domyślnym wartościom parametrów i argumentom nazwanym.

Prezentacja programu Życzenia urodzinowe Program Życzenia urodzinowe, którego przykładowe uruchomienie zostało przedstawione na rysunku 6.6, wysyła życzenia urodzinowe przy użyciu dwóch bardzo podobnych funkcji. Pierwsza funkcja wykorzystuje parametry takiego samego typu jak te, z którymi się spotkałeś w poprzednim podrozdziale, nazywane parametrami pozycyjnymi. Druga wersja funkcji używa domyślnych wartości parametrów. Najlepszym sposobem oceny różnicy między tymi rozwiązaniami jest zobaczenie ich przykładów w działaniu.

Rysunek 6.6. Funkcje mogą być wywoływane na różne sposoby z wykorzystaniem elastyczności, jaką oferują argumenty nazwane i domyślne wartości parametrów

Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 6.; nazwa pliku to zyczenia_urodzinowe.py. # Życzenia urodzinowe # Demonstruje argumenty nazwane i domyślne wartości parametrów # parametry pozycyjne def birthday1(name, age): print("Szczęśliwych urodzin,", name, "!", " Masz już", age, "lat.\n") # parametry z wartościami domyślnymi def birthday2(name = "Janusz", age = 5): print("Szczęśliwych urodzin,", name, "!", " Masz już", age, "lat.\n")

177

178

Rozdział 6. Funkcje. Gra Kółko i krzyżyk birthday1("Janusz", 5) birthday1(5, "Janusz") birthday1(name = "Janusz", age = 5) birthday1(age = 5, name = "Janusz") birthday2() birthday2(name = "Katarzyna") birthday2(age = 12) birthday2(name = "Katarzyna", age = 12) birthday2("Katarzyna", 12) input("\n\nAby zakończyć program, naciśnij klawisz Enter.")

Wykorzystywanie parametrów pozycyjnych i argumentów pozycyjnych Wpisując ciąg nazw zmiennych w nagłówku funkcji, tworzysz parametry pozycyjne: def birthday1(name, age):

Jeśli wywołujesz funkcję, podając jedynie ciąg wartości, tworzysz argumenty pozycyjne: birthday1("Janusz", 5)

Używanie parametrów pozycyjnych i argumentów pozycyjnych oznacza, że parametry otrzymują swoje wartości jedynie na podstawie pozycji przesłanych wartości. Pierwszy parametr otrzymuje pierwszą przesłaną wartość, drugi parametr otrzymuje drugą przesłaną wartość itd. W przypadku tego konkretnego wywołania funkcji to oznacza, że parametr name otrzymuje wartość "Janusz", a parametr age — wartość 5. Wynikiem tego jest komunikat Szczęśliwych urodzin, Janusz ! Masz już 5 lat. Jeśli zamienisz miejsca tych dwóch argumentów, parametry otrzymają inne wartości. Tak więc w wyniku wywołania: birthday1(5, "janusz")

parametr name otrzyma pierwszą wartość, 5, a parametr age drugą wartość, "Janusz". W rezultacie powstanie komunikat, który będzie zapewne nie taki, jakiego byś sobie życzył: Szczęśliwych urodzin, 5 ! Masz już Janusz lat. Ten sposób tworzenia i wywoływania funkcji już przedtem widziałeś. Lecz istnieją też inne sposoby tworzenia list parametrów i argumentów w programach.

Używanie parametrów pozycyjnych i argumentów nazwanych Parametry pozycyjne otrzymują przesłane do nich argumenty zgodnie z kolejnością, chyba że nakażesz funkcji inne postępowanie. Możesz zażądać, aby funkcja przypisała pewne wartości do określonych parametrów niezależnie od kolejności, poprzez użycie

Wykorzystanie argumentów nazwanych i domyślnych wartości parametrów

argumentów nazwanych. W przypadku argumentów nazwanych używasz faktycznych nazw parametrów z nagłówka funkcji do powiązania wartości z parametrem. Więc w wyniku wywołania tej samej funkcji birthday1() w postaci: birthday1(name = "Janusz", age = 5)

zmienna name otrzymuje wartość "Janusz", a zmienna age wartość 5 i funkcja wyświetla komunikat Szczęśliwych urodzin, Janusz ! Masz już 5 lat.. Nie robi to zbyt wielkiego wrażenia. Można by uzyskać ten sam wynik bez argumentów nazwanych — wystarczyłoby przesłanie tych wartości w tej kolejności. Ale urok argumentów nazwanych polega na tym, że ich kolejność nie ma znaczenia; to nazwy łączą wartości z parametrami. Więc wywołanie birthday1(age = 5, name = "Janusz")

także powoduje utworzenie komunikatu Szczęśliwych urodzin, Janusz ! Masz już 5 lat., mimo że wartości zostały wyszczególnione w odwrotnej kolejności. Argumenty nazwane pozwalają na przekazywanie wartości w dowolnym porządku. Ale największą korzyścią z ich stosowania jest klarowność kodu. Kiedy widzisz wywołanie funkcji przy użyciu argumentów nazwanych, uzyskujesz dużo lepszą świadomość tego, co te wartości reprezentują.

Pułapka Możesz łączyć argumenty nazwane i argumenty pozycyjne w tym samym wywołaniu funkcji, ale może się to okazać zdradliwe. Kiedy już użyjesz argumentu nazwanego, wszystkie pozostałe argumenty w wywołaniu muszą być także argumentami nazwanymi. Aby nie komplikować spraw, staraj się używać samych argumentów nazwanych albo samych argumentów pozycyjnych w swoich wywołaniach funkcji.

Używanie domyślnych wartości parametrów Na koniec dysponujesz opcją przypisania wartości domyślnych do swoich parametrów — wartości, które zostaną przypisane do parametrów, gdy nie zostanie do nich przekazana żadna wartość. Właśnie z tej możliwości skorzystałem, tworząc funkcję birthday2(). Wprowadziłem zmiany tylko w nagłówku: def birthday2(name = "Janusz", age = 5):

To oznacza, że jeśli do parametru name nie zostanie dostarczona żadna wartość, przyjmie on wartość "Janusz". I jeśli żadna wartość nie zostanie przekazana do parametru age, przyjmie on wartość 5. Więc wywołanie: birthday2()

nie wygeneruje błędu; zamiast tego do parametrów zostaną przypisane wartości domyślne i funkcja wyświetli komunikat Szczęśliwych urodzin Janusz ! Masz już 5 lat..

179

180

Rozdział 6. Funkcje. Gra Kółko i krzyżyk

Pułapka Kiedy już przypiszesz wartość domyślną do jednego z elementów listy parametrów, musisz przypisać wartości domyślne do wszystkich parametrów, które występują w tej liście po nim. Więc ten nagłówek funkcji jest całkowicie poprawny: def monkey_around(bananas = 100, barrel_of = "tak", uncle = "małpi"):

Lecz ten już nie: def monkey_around(bananas = 100, barrel_of, uncle):

Powyższy nagłówek wygeneruje błąd.

Na razie wszystko jest w porządku. Ale możesz sprawy nieco skomplikować poprzez zastąpienie wartości domyślnych wybranych lub wszystkich parametrów innymi wartościami. W wyniku wywołania: birthday2(name = "Katarzyna")

domyślna wartość parametru name zostaje zastąpiona przez łańcuch "Katarzyna", parametr age nadal przyjmuje swoją wartość domyślną 5 i wyświetlony zostaje komunikat Szczęśliwych urodzin Katarzyna ! Masz już 5 lat.. Po wywołaniu funkcji: birthday2(age = 12)

wartość domyślna parametru age zostaje zastąpiona liczbą 12. Parametr name przyjmuje swoją domyślną wartość "Janusz" i zostaje wyświetlony komunikat Szczęśliwych urodzin Janusz ! Masz już 12 lat.. W przypadku wywołania: birthday2(name = "Katarzyna", age = 12)

obie wartości domyślne zostają zastąpione. Parametr name otrzymuje wartość "Katarzyna", a parametr age wartość 12. Zostaje wyświetlony komunikat Szczęśliwych urodzin Katarzyna ! Masz już 12 lat.. A w wyniku wywołania: birthday2("Katarzyna", 12)

otrzymujesz dokładnie taki sam wynik jak przy poprzednim wywołaniu. Obie wartości domyślne zostają zastąpione. Parametr name otrzymuje wartość "Katarzyna", a parametr age wartość 12. I zostaje wyświetlony komunikat Szczęśliwych urodzin Katarzyna ! Masz już 12 lat..

Sztuczka Domyślne wartości parametrów są znakomitym rozwiązaniem w sytuacji, gdy masz do czynienia z funkcją, w której przy prawie każdym wywołaniu pewien parametr otrzymuje taką samą wartość. Aby oszczędzić programistom używającym Twojej funkcji trudu wpisywania tej wartości za każdym razem, mógłbyś zdefiniować dla tego parametru wartość domyślną.

Wykorzystanie zmiennych globalnych i stałych

Wykorzystanie zmiennych globalnych i stałych Dzięki magii hermetyzacji funkcje, z którymi się zapoznałeś, są całkowicie odizolowane i niezależne wzajemnie od siebie i od głównej części programu. Można do nich dostarczać informacje jedynie poprzez parametry, a jedynym sposobem wydobywania z nich informacji jest wykorzystanie wartości zwrotnych. Nie jest to cała prawda. Istnieje jeszcze jeden sposób współdzielenia informacji między różnymi częściami programu — wykorzystanie zmiennych globalnych.

Pojęcie zakresu Zakresy reprezentują różne obszary programu, które są wzajemnie oddzielone. Na przykład każda funkcja, którą definiujesz, ma swój własny zakres. To dlatego w przypadku funkcji, które poznałeś, zmienne jednej nie są dostępne dla drugiej. Przedstawienie wizualne naprawdę pomaga w skrystalizowaniu tego pojęcia, więc przyjrzyj się rysunkowi 6.7.

Rysunek 6.7. Ten prosty program zawiera trzy różne zakresy: po jednym dla każdej funkcji plus jeden zakres globalny

Na rysunku 6.7 pokazano program z trzema różnymi zakresami. Pierwszy jest zdefiniowany przez funkcję func1(), drugi jest zdefiniowany przez funkcję func2(), a trzeci to zakres globalny (który automatycznie występuje we wszystkich programach). W tym programie jesteś w zakresie globalnym, kiedy nie znajdujesz się wewnątrz jednej z funkcji. Zacieniony obszar na rysunku reprezentuje zakres globalny. Każda zmienna, którą tworzysz w zakresie globalnym, nazywa się zmienną globalną, podczas gdy każda zmienna utworzona wewnątrz funkcji jest nazywana zmienną lokalną (jest lokalna dla tej funkcji). Ponieważ zmienna variable1 została zdefiniowana wewnątrz funkcji func1(), jest zmienną lokalną, która istnieje tylko w zakresie funkcji func1(). Do zmiennej variable1

181

182

Rozdział 6. Funkcje. Gra Kółko i krzyżyk

nie można uzyskać dostępu z żadnego innego zakresu. Więc żadne polecenie w funkcji func2() nie może do niej sięgnąć i żadne polecenie w przestrzeni globalnej nie może uzyskać do niej dostępu ani zmodyfikować jej wartości. Dobrym sposobem na zapamiętanie, jak to działa, jest wyobrażenie sobie zakresów jako domów, a hermetyzacji jako przyciemniane okna nadające prywatność każdemu domowi. W rezultacie widzisz wszystko, co znajduje się wewnątrz domu, jeśli Ty sam jesteś w środku. Lecz jeśli jesteś na zewnątrz, nie widzisz, co znajduje się wewnątrz domu. Podobnie jest z funkcjami. Kiedy znajdujesz się w funkcji, masz dostęp do wszystkich jej zmiennych. Ale kiedy jesteś na zewnątrz funkcji, na przykład w zakresie globalnym, nie widzisz żadnej ze zmiennych występujących wewnątrz funkcji. Jeśli dwie zmienne znajdujące się wewnątrz dwóch oddzielnych funkcji mają tę samą nazwę, są to całkowicie inne zmienne, które nie mają ze sobą żadnego związku. Gdybym na przykład utworzył zmienną o nazwie variable2 wewnątrz funkcji func1(), to nie miałaby ona nic wspólnego ze zmienną o nazwie variable2 w funkcji func2(). Dzięki hermetyzacji istniałyby jakby w odrębnych światach i nie miałyby wzajemnie na siebie żadnego wpływu. Zmienne globalne stanowią jednak małą rysę na idei hermetyzacji, jak będziesz miał okazję zobaczyć.

Prezentacja programu Globalny zasięg Program Globalny zasięg pokazuje, jak można odczytywać czy nawet zmieniać wartości zmiennych globalnych z wnętrza funkcji. Na rysunku 6.8 przedstawiono wyniki wyświetlone przez program.

Rysunek 6.8. Możesz odczytać, przesłonić, a nawet zmienić wartość zmiennej globalnej z wnętrza funkcji

Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 6.; nazwa pliku to globalny_zasieg.py.

Wykorzystanie zmiennych globalnych i stałych # Globalny zasięg # Demonstruje zmienne globalne def read_global(): print("Wartość zmiennej value odczytana wewnątrz zakresu lokalnego", "\nfunkcji read_global() wynosi:", value) def shadow_global(): value = -10 print("Wartość zmiennej value odczytana wewnątrz zakresu lokalnego", "\nfunkcji shadow_global() wynosi:", value) def change_global(): global value value = -10 print("Wartość zmiennej value odczytana wewnątrz zakresu lokalnego", "\nfunkcji change_global() wynosi:", value) # główna część programu # value jest zmienną globalną, ponieważ jesteśmy teraz w zakresie globalnym value = 10 print("W zakresie globalnym wartość zmiennej value została ustawiona na:", value, "\n") read_global() print("Po powrocie do zakresu globalnego wartość zmiennej value nadal wynosi:", value, "\n") shadow_global() print("Po powrocie do zakresu globalnego wartość zmiennej value nadal wynosi:", value, "\n") change_global() print("Po powrocie do zakresu globalnego okazuje się, że wartość zmiennej value", "\nzmieniła się na:", value) input("\n\nAby zakończyć program, naciśnij klawisz Enter.")

Odczytywanie wartości zmiennej globalnej wewnątrz funkcji Chociaż do tej pory zdążyłeś się już zapewne dość dobrze oswoić z pojęciem hermetyzacji, chcę Cię nieco zaskoczyć — wartość zmiennej globalnej można odczytać z dowolnego zakresu w programie. Lecz nie obawiaj się, to wciąż pozostaje w zgodzie z koncepcją domów i przyciemnianych okien. Pamiętaj, przyciemniane okna chronią prywatność domów (czyli funkcji). Ale przyciemniane okna pozwalają także widzieć, co się dzieje na zewnątrz. Zawsze więc możesz wyjrzeć na zewnątrz funkcji, na zakres globalny, i zobaczyć wartość zmiennej globalnej. Właśnie to zrobiłem, gdy utworzyłem funkcję read_global(). Wyświetla ona bez problemu wartość zmiennej globalnej value. Chociaż możesz zawsze odczytać wartość zmiennej globalnej w dowolnej funkcji, nie możesz jej bezpośrednio zmienić (przynajmniej bez wyraźnego zażądania tego typu

183

184

Rozdział 6. Funkcje. Gra Kółko i krzyżyk

dostępu). Tak więc próba wykonania w funkcji read_global() czegoś takiego jak poniżej wygenerowałaby paskudny błąd: value += 1

W kontekście analogii z domami i przyciemnianymi szybami to oznacza, że widzisz zmienną z wnętrza funkcji przez zaciemnione okno, ale nie możesz jej dotknąć, ponieważ znajduje się na zewnątrz, za szybą. Więc chociaż możesz odczytać wartość zmiennej globalnej z wnętrza funkcji, nie możesz jej jednak zmienić bez zażądania specjalnego dostępu do niej.

Przesłonięcie zmiennej globalnej wewnątrz funkcji Jeśli nadasz zmiennej wewnątrz funkcji taką samą nazwę, jaką ma zmienna globalna, przesłonisz zmienną globalną. To znaczy, że ukryjesz ją za swoją nową zmienną. Mogłoby się wydawać, że mógłbyś przez to zmienić wartość zmiennej globalnej, lecz zmieniasz tylko wartość zmiennej lokalnej, którą utworzyłeś. Właśnie to zrobiłem w funkcji shadow_global(). Kiedy przypisałem -10 do zmiennej value za pomocą instrukcji: value = -10

nie zmieniłem globalnej wersji zmiennej value. Natomiast utworzyłem nową lokalną wersję zmiennej value wewnątrz funkcji, która otrzymała wartość -10. Możesz się przekonać, że tak się stało naprawdę, ponieważ po zakończeniu wykonywania funkcji główny program wypisuje wartość globalnej wersji zmiennej value za pomocą instrukcji: print("Po powrocie do zakresu globalnego wartość zmiennej value nadal wynosi:", value, "\n")

i wynosi ona nadal 10.

Pułapka Przesłanianie zmiennej globalnej wewnątrz funkcji nie jest dobrym pomysłem. Może doprowadzić do pomyłki. Mógłbyś myśleć, że używasz zmiennej globalnej, a tak by w istocie nie było. Pamiętaj o każdej zmiennej globalnej występującej w programie i nigdzie w kodzie nie używaj jej nazwy w innym znaczeniu.

Zmiana wartości zmiennej globalnej z wnętrza funkcji Aby uzyskać całkowity dostęp do zmiennej globalnej, użyj słowa kluczowego global, tak jak ja zrobiłem w funkcji change_global(): global value

W tym momencie funkcja ma całkowity dostęp do zmiennej value. Więc kiedy zmieniłem jej wartość za pomocą instrukcji: value = -10

Powrót do gry Kółko i krzyżyk

zmienna globalna value otrzymała wartość -10. Kiedy program wyświetla wartość value po ponownym powrocie do głównej części kodu za pomocą instrukcji: print("Po powrocie do zakresu globalnego okazuje się, że wartość zmiennej value", "\nzmieniła się na:", value)

wyświetlona zostaje liczba -10. Wartość zmiennej globalnej została zmieniona z wnętrza funkcji.

Kiedy należy używać zmiennych globalnych i stałych? To, że coś możesz robić, nie oznacza, że powinieneś to robić. Jest to dobre motto programowania. Czasem pewne rzeczy są technicznie możliwe do wykonania, ale nie są dobrymi pomysłami. Przykładem tego jest korzystanie ze zmiennych globalnych. Generalnie zmienne globalne powodują większe zagmatwanie programu, ponieważ śledzenie ich zmieniających się wartości może okazać się trudne. Powinieneś możliwie jak najbardziej ograniczać ich użycie. Z drugiej strony, stałe globalne (zmienne globalne, które traktujesz jako stałe), mogą przyczynić się do zmniejszenia zagmatwania programów. Powiedzmy na przykład, że piszesz aplikację biznesową, która oblicza czyjeś podatki. Jako dobry programista uwzględniłeś w swoim kodzie szereg różnych funkcji, z których wszystkie wykorzystują nieco tajemniczą wartość .28 jako stawkę podatku. Zamiast tego mógłbyś utworzyć stałą globalną o nazwie TAX_RATE i ustawić jej wartość na .28. Wtedy mógłbyś w każdej funkcji zastąpić liczbę .28 nazwą TAX_RATE. To daje dwie korzyści. Sprawia, że Twój kod staje się jaśniejszy i eliminuje trud związany z wprowadzaniem zmian (takich jak nowa stawka podatku).

Powrót do gry Kółko i krzyżyk Przedstawiona na początku tego rozdziału gra Kółko i krzyżyk jest jak dotychczas Twoim najambitniejszym projektem w tym rozdziale. Z pewnością masz już wszystkie umiejętności potrzebne do utworzenia tej gry, ale zamiast przystąpić od razu do pisania kodu, zamierzam przejść przez etap projektowania, aby pomóc Ci w uzyskaniu ogólnego obrazu i zrozumieniu zasad tworzenia większego programu.

Projektowanie gry Kółko i krzyżyk Jeśli do tej pory nie doszedłeś do tego wniosku, będę znów Cię nudził tą regułą: najważniejszą częścią programowania jest projektowanie programu. Bez mapy drogowej nigdy nie dojedziesz tam, gdzie chcesz (lub zajmie Ci to dużo więcej czasu, gdy będziesz podróżował trasą bogatą w piękne widoki).

185

186

Rozdział 6. Funkcje. Gra Kółko i krzyżyk

Zapisanie projektu w pseudokodzie Pora na powrót do Twojego ulubionego języka, który tak naprawdę nie jest językiem — pseudokodu. Ponieważ będę wykorzystywał funkcje do większości zadań w programie, mogę sobie pozwolić na przemyślenie go na dość ogólnym poziomie. Każdy wiersz pseudokodu powinien sugerować jedno wywołanie funkcji. Później będę tylko musiał napisać funkcje, które wynikają z projektu. Oto ten pseudokod: wyświetl instrukcję gry ustal, do kogo należy pierwszy ruch utwórz pustą planszę gry wyświetl planszę dopóki nikt nie wygrał ani nie padł remis jeśli ruch należy do człowieka odczytaj ruch człowieka zaktualizuj planszę przez zaznaczenie ruchu w przeciwnym wypadku wyznacz ruch komputera zaktualizuj planszę przez zaznaczenie ruchu wyświetl planszę zmień wykonawcę ruchu

Sposób reprezentowania danych W porządku, mam dobry projekt, ale jest on dosyć abstrakcyjny i raz po raz odwołuje się do różnych elementów, które w gruncie rzeczy nie zostały jeszcze w moim umyśle zdefiniowane. Koncepcję wykonywania ruchu postrzegam jako umieszczenie żetonu na planszy. Ale jaki konkretny sposób reprezentacji mam wybrać dla planszy? A jak przedstawić żeton? Albo ruch? Ponieważ zamierzam wyświetlać planszę do gry na ekranie, dlaczego miałbym nie użyć do reprezentowania żetonu pojedynczego znaku, "X" albo "O". Pusty żeton mógłby być po prostu spacją. Sama plansza powinna być listą, ponieważ ma się zmieniać po wykonaniu ruchu przez każdego z graczy. Plansza gry w kółko i krzyżyk obejmuje dziewięć pól, więc lista powinna zawierać dziewięć elementów. Każde pole planszy powinno odpowiadać pozycji na liście reprezentującej tę planszę. Rysunek 6.9 stanowi ilustrację mojej koncepcji.

Rysunek 6.9. Numer każdego pola odpowiada pozycji na liście reprezentującej planszę

Powrót do gry Kółko i krzyżyk

Więc każde pole czy też pozycja na planszy jest reprezentowana przez swój numer z zakresu od 0 do 8. To oznacza, że lista będzie miała dziewięć elementów zajmujących pozycje o numerach od 0 do 8. Ponieważ każdy ruch wskazuje pole, na którym należy położyć żeton, można go również przedstawić jako numer z zakresu od 0 do 8. Strony uczestniczące w grze (których rolę odgrywają gracz i komputer) również mogłyby być reprezentowane przez litery "X" i "O", identycznie jak żetony używane w grze. Także zmienna mająca reprezentować stronę, do której należy kolejka, miałaby wartość "X" albo "O".

Utworzenie listy funkcji Pseudokod podsuwa mi pomysły różnych funkcji, których będę potrzebował. Utworzyłem ich listę, zastanawiając się nad tym, co mogłyby robić, jakie mogłyby mieć parametry i jakie wartości mogłyby zwracać. Tabela 6.1 ukazuje wyniki moich wysiłków. Tabela 6.1. Funkcje występujące w grze Kółko i krzyżyk

Funkcja

Opis

display_instruct()

Wyświetla instrukcję gry.

ask_yes_no(question)

Zadaje pytanie, na które można odpowiedzieć „tak” lub „nie”. Otrzymuje pytanie. Zwraca "t" lub "n".

ask_number(question, low, high)

Prosi o podanie liczby z odpowiedniego zakresu. Otrzymuje pytanie, najmniejszą poprawną wartość liczby i największą poprawną wartość liczby. Zwraca liczbę z zakresu od low do high.

pieces()

Ustala, do kogo należy pierwszy ruch. Zwraca żeton komputera i żeton człowieka.

new_board()

Tworzy nową, pustą planszę gry. Zwraca planszę.

display_board(board)

Wyświetla planszę na ekranie. Otrzymuje planszę.

legal_moves(board)

Tworzy listę prawidłowych ruchów. Otrzymuje planszę. Zwraca listę prawidłowych ruchów.

winner(board)

Ustala zwycięzcę gry. Otrzymuje planszę. Zwraca żeton, "TIE" (remis) lub None.

human_move(board, human)

Odczytuje ruch człowieka podany przez gracza. Otrzymuje planszę i żeton człowieka. Zwraca ruch człowieka.

computer_move(board, computer, human)

Wyznacza ruch komputera. Otrzymuje planszę, żeton komputera i żeton człowieka. Zwraca ruch komputera.

next_turn(turn)

Zmienia stronę wykonującą ruch na podstawie bieżącej kolejki. Otrzymuje żeton. Zwraca żeton.

congrat_winner(the_winner, computer, human)

Gratuluje zwycięzcy lub ogłasza remis. Otrzymuje żeton zwycięzcy, żeton komputera i żeton człowieka.

187

188

Rozdział 6. Funkcje. Gra Kółko i krzyżyk

Skonfigurowanie programu Pierwszą czynnością, jaką wykonałem w trakcie pisania programu, było zdefiniowanie kilku stałych globalnych. Chodzi tu wartości, które będą wykorzystywane przez więcej niż jedną funkcję. Ich utworzenie sprawi, że funkcje będą bardziej przejrzyste, a jakiekolwiek zmiany dotyczące tych wartości łatwiejsze. Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 6.; nazwa pliku to kolko_i_krzyzyk.py. # Kółko i krzyżyk # Komputer gra w kółko i krzyżyk przeciwko człowiekowi # stałe globalne X = "X" O = "O" EMPTY = " " TIE = "TIE" NUM_SQUARES = 9

Stała X to krótka nazwa łańcucha "X" odgrywającego w grze rolę jednego z dwóch żetonów. Stała O reprezentuje "O" — drugi żeton wykorzystywany w grze. Stała EMPTY reprezentuje puste pole na planszy. Jej wartością jest spacja, ponieważ po wyświetleniu tego znaku pole wygląda tak, jakby było puste. Stała TIE reprezentuje wynik remisowy. A wartość stałej NUM_SQUARES jest równa liczbie pól na planszy.

Funkcja display_instruct() Funkcja ta wyświetla instrukcję gry. Miałeś okazję poznać ją już wcześniej: def display_instruct(): """Wyświetl instrukcję gry.""" print( """ Witaj w największym intelektualnym wyzwaniu wszech czasów, jakim jest gra 'Kółko i krzyżyk'. Będzie to ostateczna rozgrywka między Twoim ludzkim mózgiem a moim krzemowym procesorem. Swoje posunięcie wskażesz poprzez wprowadzenie liczby z zakresu 0 - 8. Liczba ta odpowiada pozycji na planszy zgodnie z poniższym schematem: 0 | 1 | 2 --------3 | 4 | 5 --------6 | 7 | 8 Przygotuj się, Człowieku. """ )

Ostateczna batalia niebawem się rozpocznie. \n

Jedyna rzecz, jaką zrobiłem, jest zmiana nazwy funkcji ze względu na zachowanie konsekwencji w programie.

Powrót do gry Kółko i krzyżyk

Funkcja ask_yes_no() Ta funkcja zadaje pytanie, na które można odpowiedzieć „tak” lub „nie”. Odbiera odpowiedź i zwraca wartość "t" lub "n". Z tą funkcją również spotkałeś się już wcześniej: def ask_yes_no(question): """Zadaj pytanie, na które można odpowiedzieć tak lub nie.""" response = None while response not in ("t", "n"): response = input(question).lower() return response

Funkcja ask_number() Ta funkcja prosi o podanie liczby z pewnego zakresu. W postaci argumentów wywołania otrzymuje treść pytania oraz najmniejszą i największą dopuszczalną wartość liczby. Zwraca liczbę z określonego zakresu. def ask_number(question, low, high): """Poproś o podanie liczby z odpowiedniego zakresu.""" response = None while response not in range(low, high): response = int(input(question)) return response

Funkcja pieces() Funkcja ta pyta gracza, czy chce wykonywać pierwszy ruch, i na podstawie jego decyzji zwraca żeton komputera oraz żeton człowieka. Zgodnie z tym, co dyktuje wielka tradycja gry w kółko i krzyżyk, pierwszy ruch należy do właściciela żetonu X. def pieces(): """Ustal, czy pierwszy ruch należy do gracza, czy do komputera.""" go_first = ask_yes_no("Czy chcesz mieć prawo do pierwszego ruchu? (t/n): ") if go_first == "t": print("\nWięc pierwszy ruch należy do Ciebie. Będzie Ci potrzebny.") human = X computer = O else: print("\nTwoja odwaga Cię zgubi... Ja wykonuję pierwszy ruch.") computer = X human = O return computer, human

Zwróć uwagę, że ta funkcja wywołuje inną z moich funkcji, ask_yes_no(). Wszystko jest w jak najlepszym porządku. Jedna funkcja może wywoływać drugą.

Funkcja new_board() Ta funkcja tworzy nową planszę (w postaci listy), której wszystkie elementy mają przypisaną wartość EMPTY, i zwraca ją:

189

190

Rozdział 6. Funkcje. Gra Kółko i krzyżyk def new_board(): """Utwórz nową planszę gry.""" board = [] for square in range(NUM_SQUARES): board.append(EMPTY) return board

Funkcja display_board() Ta funkcja wyświetla planszę przekazaną do niej jako argument. Ponieważ każdy element planszy jest albo spacją, albo znakiem "X", albo znakiem "O", funkcja może wyświetlić każdy z nich. Kilka innych znaków dostępnych na klawiaturze zostało użytych do narysowania przyzwoicie wyglądającej planszy do gry w kółko i krzyżyk. def display_board(board): """Wyświetl planszę gry na ekranie.""" print("\n\t", board[0], "|", board[1], "|", board[2]) print("\t", "---------") print("\t", board[3], "|", board[4], "|", board[5]) print("\t", "---------") print("\t", board[6], "|", board[7], "|", board[8], "\n")

Funkcja legal_moves() Ta funkcja otrzymuje planszę poprzez swój parametr i zwraca listę prawidłowych ruchów. Jest ona wykorzystywana przez inne funkcje. Funkcja human_move() używa jej do sprawdzenia, czy gracz wybrał prawidłowy ruch. Korzysta z niej także funkcja computer_move(), aby sprawić, że komputer będzie mógł rozpatrywać tylko prawidłowe ruchy w swoim procesie podejmowania decyzji. Prawidłowy ruch jest reprezentowany przez numer pustego pola. Gdyby na przykład pole centralne było otwarte, prawidłowym ruchem byłoby 4. Jeśli otwarte byłyby tylko pola narożne, lista prawidłowych ruchów wyglądałaby następująco: [0, 2, 6, 8]. (Jeśli nie jest to dla Ciebie jasne, zerknij na rysunek 6.9). Więc funkcja ta przegląda w pętli listę reprezentującą planszę. Ilekroć znajdzie puste pole, dodaje jego numer do listy prawidłowych ruchów. Na końcu zwraca gotową listę prawidłowych posunięć. def legal_moves(board): """Utwórz listę prawidłowych ruchów.""" moves = [] for square in range(NUM_SQUARES): if board[square] == EMPTY: moves.append(square) return moves

Powrót do gry Kółko i krzyżyk

Funkcja winner() Ta funkcja otrzymuje w wywołaniu planszę i zwraca wartość określającą zwycięzcę. Możliwe są cztery takie wartości. Funkcja zwraca X lub O, jeśli jeden z graczy zwyciężył w grze. Jeśli wszystkie pola zostały zapełnione i nikt nie wygrał, zostaje zwrócona wartość TIE. Wreszcie jeśli nikt nie wygrał oraz istnieje przynajmniej jedno puste pole, funkcja zwraca wartość None. Pierwszą rzeczą, jaką robię w tej funkcji, jest zdefiniowanie stałej o nazwie WAYS_TO_WIN, która reprezentuje osiem sposobów ustawienia trzech żetonów w jednym rzędzie. Każdy sposób na zwycięstwo jest reprezentowany przez krotkę. Każda krotka to ciąg trzech pozycji planszy tworzących jeden rząd, których zajęcie przez jedną ze stron gry daje zwycięstwo. Weźmy pod uwagę pierwszą krotkę w sekwencji: (0, 1, 2). Reprezentuje ona górny rząd planszy: pozycje 0, 1 i 2. Następna krotka (3, 4, 5) reprezentuje rząd środkowy. I tak dalej. def winner(board): """Ustal zwycięzcę WAYS_TO_WIN = ((0, (3, (6, (0, (1, (2, (0, (2,

gry.""" 1, 2), 4, 5), 7, 8), 3, 6), 4, 7), 5, 8), 4, 8), 4, 6))

Następnie wykorzystuję pętlę for do przejścia przez wszystkie możliwe sposoby uzyskania przez gracza zwycięstwa, aby sprawdzić, czy któryś z graczy nie ustawił trzech swoich żetonów w jednym rzędzie. Instrukcja if sprawdza, czy dane trzy pola tworzące jeden rząd zawierają taką samą wartość i czy nie są puste. Jeśli tak jest w istocie, oznacza to, że rząd zawiera albo trzy znaki X, albo trzy znaki O, a więc ktoś został zwycięzcą. Komputer przypisuje jeden z żetonów wygrywającej trójki do zmiennej winner, zwraca jej wartość i kończy wykonywanie funkcji. for row in WAYS_TO_WIN: if board[row[0]] == board[row[1]] == board[row[2]] != EMPTY: winner = board[row[0]] return winner

Jeśli żaden z graczy nie wygrał, wykonywanie funkcji jest kontynuowane. W następnej kolejności sprawdza ona, czy na planszy nie pozostały jakieś puste pola. Jeśli takich nie ma, w grze padł remis (ponieważ funkcja już wcześniej, wykonując pętlę for ustaliła, że nie ma zwycięzcy) i zostaje zwrócona wartość TIE. if EMPTY not in board: return TIE

Jeśli gra nie skończyła się remisem, funkcja wykonuje się dalej. Ostatecznie, jeśli żaden z graczy nie zwyciężył i gra nie zakończyła się remisem, to wynik gry pozostaje otwarty. Więc funkcja zwraca wartość None. return None

191

192

Rozdział 6. Funkcje. Gra Kółko i krzyżyk

Funkcja human_move() Ta kolejna funkcja otrzymuje w wywołaniu planszę i żeton człowieka. Zwraca numer pola, w którym gracz chce umieścić swój żeton. Najpierw funkcja pobiera listę wszystkich prawidłowych ruchów dla tej planszy. Następnie kontynuuje proszenie użytkownika o wprowadzenie numeru pola, w którym chce umieścić swój żeton, dopóki jego (lub jej) odpowiedź nie będzie się zawierać na liście prawidłowych ruchów. Po uzyskaniu prawidłowej odpowiedzi funkcja zwraca ten ruch (numer pola). def human_move(board, human): """Odczytaj ruch człowieka.""" legal = legal_moves(board) move = None while move not in legal: move = ask_number("Jaki będzie Twój ruch? (0 - 8):", 0, NUM_SQUARES) if move not in legal: print("\nTo pole jest już zajęte, niemądry Człowieku. Wybierz inne.\n") print("Znakomicie...") return move

Funkcja computer_move() Funkcja computer_move() otrzymuje w formie argumentów planszę, żeton komputera oraz żeton człowieka. Zwraca ruch komputera.

Sztuczka Jest to zdecydowanie najbardziej treściwa funkcja w programie. Wiedząc, że tak będzie, utworzyłem najpierw krótką, tymczasową funkcję, która wybiera losowe, niemniej prawidłowe posunięcie. Potrzebowałem czasu, aby przemyśleć tę funkcję, lecz nie chciałem spowalniać postępu całego projektu. Więc wstawiłem do programu funkcję tymczasową i uzyskałem działającą grę. Później wróciłem do tej kwestii i dodałem lepszą funkcję, która rzeczywiście wybiera posunięcia w racjonalny sposób. Dysponowałem tą elastycznością z powodu modularności projektu osiągniętej dzięki zastosowaniu w nim funkcji. Wiedziałem, że funkcja computer_move() była całkowicie niezależnym składnikiem i mogła być później zastąpiona bez problemu. W gruncie rzeczy mógłbym nawet wstawić nową funkcję właśnie teraz, taką, która wybiera jeszcze lepsze ruchy. (Wygląda to obecnie na strasznie trudne wyzwanie, prawda?).

Muszę być ostrożny w tym miejscu, ponieważ plansza (lista) jest mutowalna, a ja zmieniam ją w tej funkcji w trakcie szukania najlepszego ruchu komputera. Problem, jaki stąd wynika, polega na tym, że każda zmiana, jaką wprowadzę w planszy, znajdzie swoje odbicie w tej części programu, która wywołała tę funkcję. Jest to efekt referencji współdzielonych, o których dowiedziałeś się w rozdziale 5., w podrozdziale „Referencje współdzielone”. Zasadniczo istnieje tylko jeden egzemplarz tej listy i jakakolwiek zmiana, której dokonuję w tym miejscu, odnosi się do tego pojedynczego egzemplarza. Więc pierwszą rzeczą, jaką robię, jest wykonanie swojej własnej lokalnej kopii roboczej:

Powrót do gry Kółko i krzyżyk def computer_move(board, computer, human): """Spowoduj wykonanie ruchu przez komputer.""" # utwórz kopię roboczą, ponieważ funkcja będzie zmieniać listę board = board[:]

Wskazówka Zawsze, gdy wartość mutowalna zostaje przekazana do funkcji, musisz zachować ostrożność. Jeśli wiesz, że możesz zmienić tę wartość w trakcie jej wykorzystywania, utwórz jej kopię i używaj tej kopii.

Pułapka Mógłbyś pomyśleć, że wprowadzenie zmiany w planszy byłoby dobrym rozwiązaniem. Mógłbyś zmienić ją tak, aby zawierała nowy ruch komputera. W ten sposób nie musiałbyś przesyłać planszy z powrotem jako wartości zwrotnej. Tego typu bezpośrednia zmiana parametru mutowalnego jest traktowana jako tworzenie skutku ubocznego (ang. side effect). Nie wszystkie skutki uboczne są złe, ale ten ich rodzaj jest na ogół źle widziany (nawet w tej chwili marszczą mi się brwi, gdy tylko o tym pomyślę). Najlepiej komunikować się z pozostałą częścią programu poprzez wartości zwrotne; w ten sposób wyraźnie widać, jakie dokładnie informacje są przekazywane z powrotem.

W porządku, oto podstawowa strategia, jaką wymyśliłem dla komputera: 1. Jeśli istnieje ruch, który pozwala komputerowi wygrać w tej kolejce, komputer powinien wybrać ten ruch. 2. Jeśli istnieje ruch, który pozwoli człowiekowi wygrać w następnej kolejce, komputer powinien go zablokować. 3. W przeciwnym razie komputer powinien wybrać najlepsze puste pole jako swój ruch. Najlepszym polem jest środek planszy. Kolejne najlepsze pola to rogi. Potem przychodzi kolej na pozostałe pola. Więc w kolejnym fragmencie kodu definiuję krotkę, która ma reprezentować najlepsze pola w odpowiedniej kolejności: # najlepsze pozycje do zajęcia według kolejności BEST_MOVES = (4, 0, 2, 6, 8, 1, 3, 5, 7) print("Wybieram pole numer", end=" ")

Następnie tworzę listę wszystkich prawidłowych ruchów. Działając w pętli, umieszczam na próbę żeton komputera w każdym pustym polu, którego numer pobrałem z listy prawidłowych ruchów, i sprawdzam, czy ten ruch nie daje komputerowi zwycięstwa. Jeśli tak się dzieje, jest to ruch, który należy wykonać. W takim przypadku funkcja zwraca ten ruch i kończy swoje działanie. W przeciwnym razie wycofuję ten ruch i przechodzę do następnego elementu listy. # jeśli komputer może wygrać, wykonaj ten ruch for move in legal_moves(board):

193

194

Rozdział 6. Funkcje. Gra Kółko i krzyżyk board[move] = computer if winner(board) == computer: print(move) return move # ten ruch został sprawdzony, wycofaj go board[move] = EMPTY

Dotarcie do tego miejsca w funkcji oznacza, że komputer nie może wygrać w swoim kolejnym posunięciu. Więc sprawdzam, czy w swoim następnym ruchu może zwyciężyć gracz. Kod pobiera w pętli kolejne elementy listy prawidłowych ruchów, umieszczając żeton człowieka w każdym po kolei pustym polu i sprawdzając możliwość zwycięstwa człowieka. Jeśli okaże się, że testowany ruch daje człowiekowi zwycięstwo, komputer powinien go wykonać jako pierwszy, blokując tę możliwość. W tym przypadku funkcja zwraca ten ruch i kończy swoje działanie. W przeciwnym razie wycofuję ten ruch i sprawdzam kolejny prawidłowy ruch z listy. # jeśli człowiek może wygrać, zablokuj ten ruch for move in legal_moves(board): board[move] = human if winner(board) == human: print(move) return move # ten ruch został sprawdzony, wycofaj go board[move] = EMPTY

Z tego, że dotarłem do tego miejsca funkcji, wynika, że żadna strona nie może wygrać w swoim następnym ruchu. Więc przeglądam listę najlepszych posunięć i wybieram z niej pierwsze prawidłowe. Komputer pobiera w pętli kolejne elementy krotki BEST_MOVES i gdy tylko znajdzie taki, który jest prawidłowym ruchem, zwraca jego wartość. # ponieważ nikt nie może wygrać w następnym ruchu, wybierz najlepsze wolne pole for move in BEST_MOVES: if move in legal_moves(board): print(move) return move

W świecie rzeczywistym Program Kółko i krzyżyk rozpatruje tylko następny możliwy ruch w grze. Programy, które obsługują poważne gry strategiczne, takie jak szachy, analizują konsekwencje poszczególnych ruchów dużo głębiej, rozpatrując wiele poziomów posunięć i kontrposunięć. Dzisiejsze komputery mogą sprawdzić olbrzymią liczbę pozycji w grze. Wyspecjalizowane maszyny, takie jak komputer do gry w szachy firmy IBM, o nazwie Deep Blue, który pokonał mistrza świata Garriego Kasparowa, mogą sprawdzić ich dużo więcej. Deep Blue potrafi zbadać ponad 200 000 000 pozycji na szachownicy w ciągu sekundy. Wygląda to całkiem imponująco, dopóki nie uświadomisz sobie, że ogólna liczba pozycji na szachownicy została w całościowej analizie szachów oceniona na ponad 100 000 000 000 000 000 000 000 000 000 000 000 000 000 000 000 000, co oznacza, że przejrzenie tych wszystkich możliwych pozycji zajęłoby komputerowi Deep Blue ponad 1 585 489 599 188 229 lat. (nawiasem mówiąc, wiek wszechświata jest szacowany na jedyne 15 000 000 000 lat).

Powrót do gry Kółko i krzyżyk

Funkcja next_turn() Funkcja ta otrzymuje jako argument żeton bieżącej kolejki i zwraca żeton następnej kolejki. Żeton kolejki reprezentuje stronę, do której należy ruch, i przyjmuje wartość X lub O. def next_turn(turn): """Zmień wykonawcę ruchu.""" if turn == X: return O else: return X

Powyższa funkcja jest wykorzystywana do zmiany autora kolejnego ruchu po tym, jak jeden z graczy wykonał swój ruch.

Funkcja congrat_winner Ta funkcja otrzymuje jako argumenty wywołania żetony: zwycięzcy gry, komputera i człowieka. Jest wywoływana tylko wtedy, gdy gra zostaje zakończona, więc parametrowi the_winner zostanie przekazana wartość X lub O, jeśli jeden z graczy zwyciężył w grze, albo TIE, jeśli gra zakończyła się remisem. def congrat_winner(the_winner, computer, human): """Pogratuluj zwycięzcy.""" if the_winner != TIE: print(the_winner, "jest zwycięzcą!\n") else: print("Remis!\n") if the_winner == computer: print("Jak przewidywałem, Człowieku, jeszcze raz zostałem triumfatorem. \n" \ "Dowód na to, że komputery przewyższają ludzi pod każdym względem.") elif the_winner == human: print("No nie! To niemożliwe! Jakoś udało Ci się mnie zwieść, Człowieku. \n" \ "Ale to się nigdy nie powtórzy! Ja, komputer, przyrzekam Ci to!") elif the_winner == TIE: print("Miałeś mnóstwo szczęścia, Człowieku, i jakoś udało Ci się ze mną " \ "zremisować. \nŚwiętuj ten dzień... bo to najlepszy wynik, jaki możesz " \ "kiedykolwiek osiągnąć.")

Funkcja main() Główną część programu umieściłem w jej własnej funkcji, zamiast pozostawić ją na poziomie globalnym. To hermetyzuje także i główny kod. Z wyjątkiem sytuacji, gdy piszesz prosty, krótki program, hermetyzacja nawet głównej jego części jest zwykle dobrym pomysłem. Jeśli umieścisz swój główny kod w funkcji takiej jak ta, nie musisz nazywać jej main()1. 1

W tłumaczeniu na język polski: główna — przyp. tłum.

195

196

Rozdział 6. Funkcje. Gra Kółko i krzyżyk

W tej nazwie nie ma nic magicznego. Ale ponieważ jest to dość często spotykana praktyka, dobrze się do niej stosować. W porządku, oto kod głównej części programu. Jak możesz się przekonać, prawie dokładnie, wiersz w wiersz, odpowiada on pseudokodowi, który napisałem wcześniej: def main(): display_instruct() computer, human = pieces() turn = X board = new_board() display_board(board) while not winner(board): if turn == human: move = human_move(board, human) board[move] = human else: move = computer_move(board, computer, human) board[move] = computer display_board(board) turn = next_turn(turn) the_winner = winner(board) congrat_winner(the_winner, computer, human)

Rozpoczęcie programu Kolejny wiersz kodu wywołuje główną funkcję (która z kolei wywołuje pozostałe funkcje) z poziomu globalnego: # rozpocznij program main() input("\n\nAby zakończyć grę, naciśnij klawisz Enter.")

Podsumowanie W tym rozdziale nauczyłeś się pisać swoje własne funkcje. Zobaczyłeś następnie, jak przyjmować i zwracać wartości w swoich funkcjach. Dowiedziałeś się o zakresach i zobaczyłeś, jak można uzyskiwać z wnętrza funkcji dostęp do zmiennych globalnych i zmieniać ich wartość. Nauczyłeś się również ograniczać wykorzystywanie zmiennych globalnych, lecz zobaczyłeś też, jak korzystać ze stałych globalnych, jeśli jest to konieczne. Otarłeś się nawet troszeczkę o pewne koncepcje z dziedziny sztucznej inteligencji, tworząc komputerowego przeciwnika w grze strategicznej.

Podsumowanie

Sprawdź swoje umiejętności 1. Popraw funkcję ask_number() w taki sposób, aby mogła być wywoływana z wartością kroku. Spraw, aby domyślną wartością kroku była liczba 1. 2. Zmodyfikuj projekt Jaka to liczba? z rozdziału 3. przez użycie w nim funkcji ask_number(). 3. Zmodyfikuj nową wersję gry Jaka to liczba?, którą utworzyłeś w ramach poprzedniego zadania, tak aby kod programu znalazł się w funkcji o nazwie main(). Nie zapomnij o wywołaniu funkcji main() w celu uruchomienia gry. 4. Napisz nową funkcję computer_move() do gry Kółko i krzyżyk, aby zlikwidować lukę w strategii komputera. Sprawdź, czy potrafisz utworzyć przeciwnika, który będzie nie do pobicia.

197

198

Rozdział 6. Funkcje. Gra Kółko i krzyżyk

7 Pliki i wyjątki. Gra Turniej wiedzy Z

mienne stanowią znakomity sposób przechowywania informacji i uzyskiwania do nich dostępu w trakcie wykonywania programu, lecz często będziesz chciał zapisać dane w taki sposób, aby można je było później odzyskać. W tym rozdziale dowiesz się, jak wykorzystywać pliki do tego rodzaju trwałego przechowywania danych. Dowiesz się również, jak obsługiwać błędy, jakie Twój kod może wygenerować. W szczególności nauczysz się:  odczytywać dane z plików tekstowych,  zapisywać informacje w plikach tekstowych,  odczytywać i zapisywać bardziej złożone dane z wykorzystaniem plików,  przechwytywać i obsługiwać błędy podczas wykonywania programu.

Wprowadzenie do programu Turniej wiedzy Gra Turniej wiedzy sprawdza wiedzę gracza za pomocą serii pytań wielokrotnego wyboru. Gra udostępnia pytania w formie pojedynczego „odcinka”. Odcinek, który utworzyłem w celu zademonstrowania programu, dotyczy mafii i nazywa się „Odcinkiem nie do odrzucenia”. Wszystkie występujące w nim pytania odnoszą się w jakiś sposób do mafii (choć czasem w nieco pośredni sposób). Znakomitą stroną tej gry jest to, że pytania określonego odcinka są przechowywane w odrębnym pliku, niezależnie od kodu gry. W ten sposób łatwo jest wykorzystywać w grze różne odcinki. Oznacza to nawet więcej — każdy, kto dysponuje edytorem tekstu (takim jak Notatnik na maszynach z systemem Windows), może tworzyć swoje własne odcinki konkursu wiedzy dotyczące dowolnie wybranego tematu, od anime do zoologii. Rysunek 7.1 pokazuje grę (i mój odcinek) w działaniu.

200

Rozdział 7. Pliki i wyjątki. Gra Turniej wiedzy

Rysunek 7.1. Gracz ma zawsze do wyboru cztery kuszące odpowiedzi, ale tylko jedna z nich jest poprawna

Odczytywanie danych z plików tekstowych Odczytywanie łańcuchów znaków ze zwykłych plików tekstowych — plików, które składają się z samych znaków ASCII — jest proste. (Chociaż istnieją różne typy plików tekstowych, gdy używam terminu „plik tekstowy”, mam na myśli zwykły plik tekstowy). Pliki tekstowe stanowią dobry wybór w sytuacji, gdy chcesz przechowywać proste informacje z jakichś powodów. Plik tekstowy na maszynie z systemem Windows jest takim samym plikiem tekstowym na komputerze Mac oraz takim samym plikiem tekstowym w systemie Unix. Po drugie, pliki tekstowe są łatwe w użyciu. Większość systemów operacyjnych jest wyposażona w podstawowe narzędzia służące do ich przeglądania i edytowania.

Prezentacja programu Odczytaj to Program Odczytaj to demonstruje kilka sposobów odczytywania łańcuchów znaków z pliku tekstowego. Program pokazuje, jak można odczytać cokolwiek, zaczynając od pojedynczego znaku, a kończąc na całym pliku. Przedstawia również różne sposoby odczytywania danych po jednym wierszu na raz. Został zilustrowany na rysunku 7.2. Program odczytuje dane z prostego pliku tekstowego, który utworzyłem w swoim systemie przy użyciu edytora tekstu. Ten plik możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 7.; nazwa pliku to odczytaj_to.txt. Oto zawartość pliku: Wiersz 1 To jest wiersz 2 Ten tekst tworzy wiersz 3

Odczytywanie danych z plików tekstowych

Rysunek 7.2. Ten plik jest odczytywany przy użyciu kilku różnych technik

Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 7.; nazwa pliku to odczytaj_to.py. # Odczytaj to # Demonstruje odczytywanie danych z pliku tekstowego print("Otwarcie i zamknięcie pliku.") text_file = open("odczytaj_to.txt", "r") text_file.close() print("\nOdczytywanie znaków z pliku.") text_file = open("odczytaj_to.txt", "r") print(text_file.read(1)) print(text_file.read(7)) text_file.close() print("\nOdczytanie całego pliku za jednym razem.") text_file = open("odczytaj_to.txt", "r") whole_thing = text_file.read() print(whole_thing)

201

202

Rozdział 7. Pliki i wyjątki. Gra Turniej wiedzy text_file.close() print("\nOdczytywanie znaków z wiersza.") text_file = open("odczytaj_to.txt", "r") print(text_file.readline(1)) print(text_file.readline(7)) text_file.close() print("\nOdczytywanie po jednym wierszu na raz.") text_file = open("odczytaj_to.txt", "r") print(text_file.readline()) print(text_file.readline()) print(text_file.readline()) text_file.close() print("\nWczytanie całego pliku do listy.") text_file = open("odczytaj_to.txt", "r") lines = text_file.readlines() print(lines) print(len(lines)) for line in lines: print(line) text_file.close() print("\nPobieranie zawartości pliku wiersz po wierszu przy użyciu pętli.") text_file = open("odczytaj_to.txt", "r") for line in text_file: print(line) text_file.close() input("\n\nAby zakończyć program, naciśnij klawisz Enter.")

Pokażę Ci dokładnie, jak ten kod działa, poprzez sesję interaktywną.

Otwarcie i zamknięcie pliku Zanim będziesz mógł odczytywać dane z pliku (lub zapisywać je do pliku), musisz go otworzyć. To pierwsza czynność, jaką wykonałem w programie Odczytaj to: >>> text_file = open("odczytaj_to.txt", "r")

Używam funkcji open() do otwarcia pliku i przypisania wyniku operacji do zmiennej text_file. W wywołaniu funkcji podaje dwa argumenty w postaci łańcuchów znaków: nazwę pliku i tryb dostępu. Argument z nazwą pliku "odczytaj_to.txt" jest dość oczywisty. Ponieważ nie zawarłem w nim żadnej informacji o ścieżce dostępu, Python szuka pliku w katalogu bieżącym. Mogę uzyskać dostęp do pliku znajdującego się w dowolnym katalogu poprzez podanie właściwej informacji o ścieżce. Następnie podaję "r" jako tryb dostępu, informując Pythona, że chcę otworzyć ten plik do odczytu. Można otworzyć plik do odczytu, do zapisu lub do odczytu i zapisu.

Odczytywanie danych z plików tekstowych

W tabeli 7.1 znajduje się opis wybranych prawidłowych trybów dostępu do pliku tekstowego. Tabela 7.1. Wybrane tryby dostępu do pliku tekstowego

Tryb

Opis

"r"

Odczyt danych z pliku tekstowego. Jeśli plik nie istnieje, Python zasygnalizuje błąd.

"w"

Zapis danych do pliku tekstowego. Jeśli plik już istnieje, jego zawartość zostaje zastąpiona przez nowe dane. Jeśli nie istnieje, zostaje utworzony.

"a"

Dopisanie danych na końcu pliku tekstowego. Jeśli plik istnieje, nowe dane zostają do niego dopisane. Jeśli plik nie istnieje, jest tworzony.

"r+"

Odczyt i zapis danych z (do) pliku tekstowego. Jeśli plik nie istnieje, Python zasygnalizuje błąd.

"w+"

Zapis i odczyt danych do (z) pliku tekstowego. Jeśli plik istnieje, jego zawartość zostanie zastąpiona nowymi danymi. Jeśli nie istnieje, zostanie utworzony.

"a+"

Dopisywanie i odczyt danych do (z) pliku tekstowego. Jeśli plik istnieje, nowe dane są dopisywane na jego końcu. Jeśli plik nie istnieje, zostanie utworzony.

Po otwarciu pliku mam do niego dostęp poprzez zmienną text_file, która reprezentuje obiekt pliku. Istnieje wiele użytecznych metod obiektu pliku, które mogę wywoływać, lecz najprostsza z nich jest metoda close(), która zamyka plik, uniemożliwiając ewentualne dalsze operacje odczytu czy zapisu, dopóki plik nie zostanie otwarty ponownie. Właśnie to robię w swoim programie w następnej kolejności: >>> text_file.close()

Cokolwiek zrobisz z plikiem, jego zamknięcie jest dobrą praktyką w programowaniu.

Odczytywanie znaków z pliku Żeby mieć z pliku jakiś pożytek, musisz coś zrobić z jego zawartością między jego otwarciem i zamknięciem. Więc w następnej kolejności otwieram plik i odczytuję jego zawartość za pomocą metody obiektu pliku read(). Metoda read() umożliwia Ci odczytanie z pliku określonej liczby znaków, które zostają zwrócone w postaci łańcucha. Po ponownym otwarciu pliku odczytuję z niego dokładnie jeden znak i wyświetlam go: >>> text_file = open("odczytaj_to.txt", "r") >>> print(text_file.read(1)) W

Muszę jedynie podać w nawiasach liczbę znaków. Następnie odczytuję i wyświetlam kolejne siedem znaków: >>> print(text_file.read(7)) iersz 1

203

204

Rozdział 7. Pliki i wyjątki. Gra Turniej wiedzy

Zauważ, że odczytuję siedem znaków następujących po literze "W". Python pamięta, w którym miejscu zakończyłem poprzedni odczyt. To tak, jakby komputer umieszczał w pliku zakładkę — każda kolejna operacja read() rozpoczyna swoje działanie w miejscu, w którym zakończyła się poprzednia. Kiedy odczytasz wszystkie znaki do końca pliku, kolejne odczyty zwrócą pusty łańcuch. Aby rozpocząć odczytywanie z powrotem od początku pliku, możesz go zamknąć i otworzyć ponownie. Właśnie to zrobiłem w następnej kolejności: >>> text_file.close() >>> text_file = open("odczytaj_to.txt", "r")

Jeśli nie podasz liczby znaków do odczytania, Python zwróci cały plik jako jeden łańcuch znaków. Następnie więc odczytuję całą zawartość pliku, przypisuję zwrócony łańcuch znaków do zmiennej i wyświetlam jej wartość: >>> whole_thing = text_file.read() >>> print(whole_thing) Wiersz 1 To jest wiersz 2 Ten tekst tworzy wiersz 3

Jeśli plik jest dość mały, odczytanie go od razu w całości może mieć sens. Ponieważ odczytałem zawartość całego pliku, jakiekolwiek kolejne odczyty zwrócą jedynie pusty łańcuch. Więc zamykam plik ponownie: >>> text_file.close(

Odczytywanie znaków z wiersza Często będziesz chciał przetwarzać dane pliku tekstowego po jednym wierszu. Metoda readline() umożliwia odczytywanie znaków z bieżącego wiersza. Przekazujesz tylko liczbę znaków, które chcesz odczytać z bieżącego wiersza, a metoda zwraca je w postaci łańcucha. Jeśli nie przekażesz liczby znaków, metoda odczyta wszystkie znaki od pozycji bieżącej do końca wiersza. Gdy już odczytasz wszystkie znaki wiersza, wierszem bieżącym staje się kolejny wiersz pliku. Po ponownym otwarciu pliku odczytuję pierwszy znak bieżącego wiersza: >>> text_file = open("odczytaj_to.txt", "r") >>> print(text_file.readline(1)) W

Następnie odczytuję kolejne siedem znaków z bieżącego wiersza: >>> print(text_file.readline(7)) iersz 1 >>> text_file.close()

W tym momencie może się wydawać, że metoda readline() niczym się nie różni od read(), lecz readline() odczytuje znaki tylko z bieżącego wiersza, podczas gdy metoda read() odczytuje znaki z całego pliku. Z tego powodu metoda readline() jest zwykle wywoływana w celu odczytania jednego wiersza tekstu na raz:

Odczytywanie danych z plików tekstowych >>> text_file = open("odczytaj_to.txt", "r") >>> print(text_file.readline()) Wiersz 1 >>> print(text_file.readline()) To jest wiersz 2 >>> print(text_file.readline()) Ten tekst tworzy wiersz 3 >>> text_file.close()

Zwróć uwagę, że po każdym wyświetlonym wierszu pojawia się pusta linia. To dlatego, że każdy wiersz w pliku tekstowym kończy się znakiem nowej linii ("\n").

Wczytywanie wszystkich wierszy do listy Inny rodzaj obsługi pojedynczych wierszy tekstu udostępnia metoda readlines(), która wczytuje zawartość pliku tekstowego do listy, w której każdy wiersz pliku staje się jako łańcuch znaków elementem listy. Jako następną wywołuję metodę readlines(): >>> text_file = open("odczytaj_to.txt", "r") >>> lines = text_file.readlines()

Zmienna lines odwołuje się teraz do listy, której elementami są wszystkie wiersze zawarte w pliku tekstowym: >>> print(lines) ['Wiersz 1\n', 'To jest wiersz 2\n', 'Ten tekst tworzy wiersz 3\n']

Lista lines nie różni się od innych list. Możesz znaleźć jej długość, a nawet przetwarzać jej elementy w pętli: >>> print(len(lines)) 3 >>> for line in lines: print(line) Wiersz 1 To jest wiersz 2 Ten tekst tworzy wiersz 3 >>> text_file.close()

Odczytywanie zawartości pliku w pętli Można także zbudować pętlę opartą bezpośrednio na wierszach pliku, wykorzystując w tym celu instrukcję for: >>> text_file = open("odczytaj_to.txt", "r") >>> for line in text_file: print(line)

205

206

Rozdział 7. Pliki i wyjątki. Gra Turniej wiedzy Wiersz 1 To jest wiersz 2 Ten tekst tworzy wiersz 3 >>> text_file.close()

Jak widzisz, zmienna pętli (w tym kodzie line) otrzymuje, jako swoją wartość, każdy po kolei wiersz pliku. Przy pierwszej iteracji pętli „pobiera” pierwszy wiersz, przy drugiej iteracji — drugi wiersz itd. Ta technika jest najbardziej eleganckim rozwiązaniem w sytuacji, gdy chcesz przetwarzać zawartość pliku po jednym wierszu na raz.

Zapisywanie danych do pliku tekstowego Aby pliki tekstowe mogły stanowić realną formę przechowywania danych, musisz mieć możliwość umieszczania w nich informacji. W przypadku Pythona zapisywanie łańcuchów znaków do plików tekstowych jest również prostą sprawą. Zademonstruję dwa podstawowe sposoby realizacji tego zadania.

Prezentacja programu Zapisz to Program Zapisz to tworzy plik tekstowy o takiej samej zawartości, jaką miał plik odczytaj_to.txt, który wykorzystałem w programie Odczytaj to. Właściwie program tworzy i wyświetla ten nowy plik dwukrotnie, używając za każdym razem innej metody zapisu danych do pliku. Rysunek 7.3 pokazuje wynik działania tego programu. Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 7.; nazwa pliku to zapisz_to.py.

Rysunek 7.3. Ten sam plik jest tworzony dwukrotnie, za każdym razem za pomocą innej metody obiektu pliku

Zapisywanie danych do pliku tekstowego

Zapisywanie łańcuchów znaków do pliku Tak jak poprzednio, w celu wykorzystania pliku muszę go otworzyć we właściwym trybie. Więc pierwszą rzeczą, jaką robię w programie, jest otwarcie pliku w trybie zapisu: # Zapisz to # Demonstruje zapisywanie danych do pliku tekstowego print("Utworzenie pliku tekstowego za pomocą metody write().") text_file = open("zapisz_to.txt", "w")

Pojawia się plik zapisz_to.txt jako pusty plik tekstowy oczekujący na dane, które program będzie w nim zapisywał. Gdyby plik zapisz_to.txt istniał już wcześniej, zostałby zastąpiony całkowicie nowym, pustym plikiem, a jego pierwotna zawartość zostałaby usunięta. Korzystam teraz z metody write() obiektu pliku, która zapisuje łańcuch znaków do pliku: text_file.write("Wiersz 1\n") text_file.write("To jest wiersz 2\n") text_file.write("Ten tekst tworzy wiersz 3\n")

Metoda write() nie wstawia automatycznie znaku nowego wiersza na końcu łańcucha, który zapisuje. Musisz sam wstawić znaki nowego wiersza tam, gdzie są one potrzebne. Jeśli opuściłbyś trzy znaki nowej linii występujące w powyższych wierszach kodu, program zapisałby w pliku jeden długi wiersz. Nie musisz również każdego łańcucha, jaki zapisujesz do pliku, kończyć na znaku nowego wiersza. Aby osiągnąć taki sam ostateczny wynik, mógłbym równie dobrze wszystkie trzy poprzednio zapisane łańcuchy skleić razem, aby utworzyć jeden długi łańcuch, "Wiersz 1\nTo jest wiersz 2\nTen tekst tworzy wiersz 3\n", i zapisać go do pliku poprzez jednokrotne wywołanie metody write(). W końcu zamykam plik: text_file.close()

Następnie w celu udowodnienia, że zapisy zostały wykonane zgodnie z oczekiwaniem, odczytuję i wyświetlam całą zawartość pliku: print("\nOdczytanie zawartości nowo utworzonego pliku.") text_file = open("zapisz_to.txt", "r") print(text_file.read()) text_file.close()

Zapisywanie listy łańcuchów do pliku Następnie tworzę od nowa ten sam plik przy użyciu metody writelines() obiektu pliku. Podobnie jak jej odpowiedniczka readlines(), metoda writelines() obsługuje listę łańcuchów, lecz zamiast wczytywać zawartość pliku tekstowego do listy, zapisuje listę łańcuchów do pliku.

207

208

Rozdział 7. Pliki i wyjątki. Gra Turniej wiedzy

Najpierw otwieram plik do zapisu: print("\nUtworzenie pliku tekstowego za pomocą metody writelines().") text_file = open("zapisz_to.txt", "w")

Otwieram ten sam plik, zapisz_to.txt, co oznacza, że wymazuję całkowicie istniejący plik i zaczynam od nowego, pustego. Następnie tworzę listę łańcuchów znaków, które mają być po kolei zapisane do pliku: lines = ["Wiersz 1\n", "To jest wiersz 2\n", "Ten tekst tworzy wiersz 3\n"]

Tak jak poprzednio, wstawiłem znaki nowego wiersza tam, gdzie według mnie powinny się znajdować w pliku tekstowym. Następnie zapisuję całą listę łańcuchów do pliku za pomocą metody writelines(): text_file.writelines(lines)

Po czym zamykam plik: text_file.close()

Na koniec wyświetlam zawartość pliku, aby pokazać, że nowy plik jest dokładnie taki sam jak jego poprzednia wersja: print("\nOdczytanie zawartości nowo utworzonego pliku.") text_file = open("zapisz_to.txt", "r") print(text_file.read()) text_file.close() input("\n\nAby zakończyć program, naciśnij klawisz Enter.")

Poznałeś wiele metod obiektu pliku służących do odczytywania i zapisywania danych. Ich podsumowanie znajdziesz w tabeli 7.2. Tabela 7.2. Wybrane metody obiektu pliku

Metoda

Opis

close()

Zamyka plik. Odczytywanie danych z zamkniętego pliku oraz zapisywanie do niego jest niemożliwe, dopóki nie zostanie ponownie otwarty.

read([rozmiar])

Odczytuje z pliku wskazaną przez argument rozmiar liczbę znaków i zwraca je w postaci łańcucha. Jeśli rozmiar nie jest określony, metoda zwraca wszystkie znaki od pozycji bieżącej do końca pliku.

readline([rozmiar])

Odczytuje z bieżącego wiersza pliku wskazaną przez argument rozmiar liczbę znaków i zwraca je w postaci łańcucha. Jeśli rozmiar nie jest określony, metoda zwraca wszystkie znaki od pozycji bieżącej do końca wiersza.

readlines()

Odczytuje wszystkie wiersze pliku i zwraca je jako elementy listy.

write(dane)

Zapisuje łańcuch dane do pliku.

writelines(dane)

Zapisuje łańcuchy będące elementami listy dane do pliku.

Przechowywanie złożonych struktur danych w plikach

Przechowywanie złożonych struktur danych w plikach Pliki tekstowe są wygodne w użyciu, ponieważ można je odczytywać i manipulować nimi za pomocą dowolnego edytora tekstu, lecz ich zastosowanie ogranicza się do przechowywania ciągów znaków. Czasem możesz chcieć przechowywać bardziej złożone informacje, na przykład takie jak lista lub słownik. Mógłbyś spróbować przekształcić zawartość tych struktur danych na znaki i zapisać ją w pliku tekstowym, ale Python oferuje dużo lepszy sposób. Możesz zapisać bardziej złożone dane w pliku za pomocą pojedynczego wiersza kodu. Możesz nawet przechowywać w pojedynczym pliku prostą bazę danych, która działa jak słownik.

Prezentacja programu Zamarynuj to Marynowanie (ang. pickling) oznacza konserwowanie — i takie właśnie znaczenie ma ten termin w Pythonie. Możesz zamarynować złożoną strukturę danych, taką jak lista czy słownik, i zapisać ją w całości w pliku. Najlepsze ze wszystkiego jest to, że Twoje ręce nie będą pachnieć octem, kiedy zakończysz swoją pracę. Program Zamarynuj to marynuje, przechowuje w pliku binarnym i pobiera trzy listy łańcuchów. Robi to dwukrotnie. Najpierw program marynuje, przechowuje i pobiera te trzy listy sekwencyjnie, co w dużym stopniu przypomina sposób obsługi znaków w pliku tekstowym, z którym się wcześniej spotkałeś. Następnie program przechowuje i pobiera te same listy z zastosowaniem dostępu swobodnego. Wynik działania programu został pokazany na rysunku 7.4. Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 7.; nazwa pliku to zamarynuj_to.py.

Rysunek 7.4. Każda lista jest zapisywana do pliku i odczytywana z pliku w całości

209

210

Rozdział 7. Pliki i wyjątki. Gra Turniej wiedzy

Marynowanie danych i zapisywanie ich do pliku Pierwszą rzeczą, jaką robię w programie, jest zaimportowanie dwóch nowych modułów: # Zamarynuj to # Demonstruje marynowanie i odkładanie danych na półkę import pickle, shelve

Moduł pickle umożliwia marynowanie i przechowywanie w pliku bardziej złożonych danych. Moduł shelve umożliwia magazynowanie zamarynowanych obiektów w pliku i zapewnia do nich dostęp swobodny. Marynowanie jest proste. Zamiast zapisywać do pliku znaki, możesz zapisać w nim zamarynowany obiekt. Zamarynowane obiekty są przechowywane w plikach w dużej mierze tak samo jak znaki; możesz składować i pobierać je sekwencyjnie. Najpierw tworzę trzy listy, które zamierzam zamarynować za pomocą modułu pickle i zapisać do pliku. print("Marynowanie list.") variety = ["łagodny", "pikantny", "kwaszony"] shape = ["cały", "krojony wzdłuż", "w plasterkach"] brand = ["Dawtona", "Klimex", "Vortumnus"]

Następnie otwieram nowy plik, aby przechować w nim zamarynowane listy. f = open("pikle1.dat", "wb")

Zamarynowane obiekty muszą być przechowywane w pliku binarnym — nie mogą zostać zapisane w pliku tekstowym. Więc otwieram nowy plik binarny o nazwie pikle1.dat do zapisu poprzez przekazanie łańcucha "wb" wyznaczającego tryb dostępu do pliku. W tabeli 7.3 znajduje się wykaz użytecznych trybów dostępu do pliku binarnego. Tabela 7.3. Wybrane tryby dostępu do pliku binarnego

Tryb

Opis

"rb"

Odczyt danych z pliku binarnego. Jeśli plik nie istnieje, Python zasygnalizuje błąd.

"wb"

Zapis danych do pliku binarnego. Jeśli plik już istnieje, jego zawartość zostaje zastąpiona przez nowe dane. Jeśli nie istnieje, zostaje utworzony.

"ab"

Dopisanie danych na końcu pliku binarnego. Jeśli plik istnieje, nowe dane zostają do niego dopisane. Jeśli plik nie istnieje, jest tworzony.

"rb+"

Odczyt i zapis danych z (do) pliku binarnego. Jeśli plik nie istnieje, Python zasygnalizuje błąd.

"wb+"

Zapis i odczyt danych do (z) pliku binarnego. Jeśli plik istnieje, jego zawartość zostanie zastąpiona nowymi danymi. Jeśli nie istnieje, zostanie utworzony.

"ab+"

Dopisywanie i odczyt danych do (z) pliku binarnego. Jeśli plik istnieje, nowe dane są dopisywane na jego końcu. Jeśli plik nie istnieje, zostanie utworzony.

Następnie marynuję i magazynuję trzy listy, variety, shape i brand, w pliku pikle1.dat przy użyciu funkcji pickle.dump(). Funkcja wymaga podania dwóch argumentów: danych do zamarynowania i pliku do ich przechowywania.

Przechowywanie złożonych struktur danych w plikach pickle.dump(variety, f) pickle.dump(shape, f) pickle.dump(brand, f) f.close()

Więc ten kod marynuje listę, do której odwołuje się zmienna variety, i zapisuje całość jako jeden obiekt do pliku pikle1.dat. Następnie program marynuje listę, do której odwołuje się zmienna shape, i zapisuje całość jako jeden obiekt do pliku. Po czym program marynuje listę, do której odwołuje się zmienna brand, i zapisuje całość jako jeden obiekt do pliku. Na koniec program zamyka plik. Można marynować różne obiekty, w tym:  liczby,  łańcuchy znaków,  krotki,  listy,  słowniki.

Odczytywanie danych z pliku i ich odmarynowanie Następnie pobieram z pliku i odmarynowuję te trzy listy za pomocą funkcji pickle.load(). Funkcja ta przyjmuje jeden argument — plik, z którego ma załadować kolejny zamarynowany obiekt. print("\nOdmarynowanie list.") f = open("pikle1.dat", "rb") variety = pickle.load(f) shape = pickle.load(f) brand = pickle.load(f)

Program odczytuje pierwszy zamarynowany obiekt w pliku, odmarynowuje go, tworząc listę ["łagodny", "pikantny", "kwaszony"], i przypisuje tę listę do zmiennej variety. Następnie program odczytuje kolejny zamarynowany obiekt w pliku, odmarynowuje go, tworząc listę ["cały", "krojony wzdłuż", "w plasterkach"], i przypisuje tę listę do zmiennej shape. Na koniec program odczytuje ostatni zamarynowany obiekt w pliku, odmarynowuje go, tworząc listę ["Dawtona", "Klimex", "Vortumnus"], i przypisuje tę listę do zmiennej brand. W końcu wyświetlam odmarynowane listy, aby udowodnić, że cały ten proces przebiegł prawidłowo: print(variety) print(shape) print(brand) f.close()

W tabeli 7.4 zostały opisane wybrane funkcje modułu pickle.

211

212

Rozdział 7. Pliki i wyjątki. Gra Turniej wiedzy Tabela 7.4. Wybrane funkcje modułu pickle

Funkcja

Opis

dump(obiekt, plik, [, bin])

Zapisuje zamarynowaną wersję obiektu obiekt do pliku plik. Jeśli bin ma wartość True, obiekt zostaje zapisany w formacie binarnym. A jeśli bin ma wartość False, obiekt zostaje zapisany w mniej wydajnym, ale czytelniejszym dla człowieka formacie binarnym. Wartość domyślna parametru bin jest równa False.

load(plik)

Odmarynowuje i zwraca kolejny zamarynowany obiekt z pliku plik.

Wykorzystanie półki do przechowywania zamarynowanych danych Teraz posuwam się o krok dalej w stosunku do koncepcji marynowania poprzez umieszczenie tych list „na półce” (ang. shelving), w pojedynczym pliku. Korzystając z modułu shelve, tworzę półkę (ang. shelf), która funkcjonuje jak słownik umożliwiający bezpośredni (swobodny) dostęp do list: Najpierw tworzę półkę o nazwie s: print("\nOdkładanie list na półkę.") s = shelve.open("pikle2.dat")

Funkcja shelve.open() przypomina w dużej mierze funkcję otwierania pliku open(). Funkcja shelve.open() operuje jednak plikiem, który przechowuje zamarynowane obiekty, a nie znaki. W tym przypadku przypisałem półkę stanowiącą wynik wykonania funkcji do zmiennej s, która teraz funkcjonuje jako słownik, którego zawartość jest przechowywana w pliku lub grupie plików.

Wskazówka Gdy wywołasz funkcję shelve.open(), Python może dodać rozszerzenie do podanej przez Ciebie nazwy pliku. Python może również utworzyć dodatkowe pliki do obsługi nowo utworzonej półki.

Funkcja shelve.open() wymaga podania jednego argumentu — nazwy pliku. Przyjmuje także opcjonalny tryb dostępu. Jeśli nie podasz trybu dostępu (tak jak ja), przyjmie on wartość domyślną "c". Tabela 7.5 zawiera szczegóły dotyczące trybów dostępu obsługiwanych przez tę funkcję. Tabela 7.5. Tryby dostępu obsługiwane przez moduł shelve

Tryb

Opis

"c"

Otwórz plik do odczytu i zapisu. Jeśli plik nie istnieje, zostaje utworzony.

"n"

Utwórz nowy plik do odczytu i zapisu. Jeśli plik już istnieje, jego zawartość zostaje nadpisana.

"r"

Odczytuj dane z pliku. Jeśli plik nie istnieje, Python zasygnalizuje błąd.

"w"

Zapisuj dane do pliku. Jeśli plik nie istnieje, Python zasygnalizuje błąd.

Przechowywanie złożonych struktur danych w plikach

Następnie dodaję do półki trzy listy:s s["odmiana"] = ["łagodny", "pikantny", "kwaszony"] s["kształt"] = ["cały", "krojony wzdłuż", "w plasterkach"] s["marka"] = ["Dawtona", "Klimex", "Vortumnus"]

Półka s funkcjonuje jak słownik. Tak więc klucz "odmiana" tworzy parę z zawartością ["łagodny", "pikantny", "kwaszony"]. Kluczowi "kształt" odpowiada wartość ["cały", "krojony wzdłuż", "w plasterkach"], a klucz "marka" tworzy parę z wartością ["Dawtona", "Klimex", "Vortumnus"]. Jedną z ważnych rzeczy, na które należy zwrócić uwagę, jest to, że klucz półki może być tylko łańcuchem znaków. Na koniec wywołuję metodę sync() półki: s.sync()

# upewnij się, że dane zostały zapisane

Python zapisuje zmiany, które powinny się znaleźć w pliku półki, do bufora, a potem okresowo zapisuje zawartość bufora do pliku. Aby mieć pewność, że zawartość pliku odzwierciedla zmiany dokonane w półce, możesz wywołać jej metodę sync(). Plik półki jest również aktualizowany wtedy, gdy zamykasz go za pomocą metody close().

Wskazówka Chociaż mógłbyś zasymulować półkę poprzez zamarynowanie słownika, to jednak moduł shelve wykorzystuje pamięć efektywniej. Więc jeśli potrzebujesz swobodnego dostępu do zamarynowanych obiektów, utwórz półkę.

Wykorzystanie półki do przywrócenia zamarynowanych danych Ponieważ półka funkcjonuje jak słownik, możesz uzyskiwać bezpośredni dostęp do zamarynowanych obiektów dzięki podaniu klucza. Aby to udowodnić, realizuję dostęp do zamarynowanych list z półki s w odwrotnej kolejności. print("\nPobieranie list z pliku półki:") print("marka -", s["marka"]) print("kształt -", s["kształt"]) print("odmiana -", s["odmiana"])

Na koniec zamykam plik: s.close() input("\n\nAby zakończyć program, naciśnij klawisz Enter.")

W świecie rzeczywistym Zamarynowanie i odmarynowanie to dobre sposoby magazynowania i pobierania z powrotem ustrukturyzowanych informacji, lecz bardziej złożone informacje mogą wymagać nawet silniejszych i elastyczniejszych środków. Dwie popularne metody magazynowania i pobierania bardziej skomplikowanych struktur informacji to bazy danych i pliki XML, a Python zawiera moduły, które mogą współpracować z każdą z nich. Aby dowiedzieć się na ten temat więcej, odwiedź stronę języka Python http://www.python.org.

213

214

Rozdział 7. Pliki i wyjątki. Gra Turniej wiedzy

Obsługa wyjątków Kiedy Python napotyka błąd, zatrzymuje bieżący program i wyświetla komunikat o błędzie. Ujmując to bardziej precyzyjnie, zgłasza wyjątek, wskazując, że zdarzyło się coś niezwykłego. Jeśli z tym wyjątkiem nic się nie robi, Python przerywa to, co wykonuje w danej chwili, i wyświetla komunikat o błędzie podający szczegóły wyjątku. Oto prosty przykład zgłoszenia przez Python wyjątku: >>> num = float("Hej!") Traceback (most recent call last): File "", line 1, in num = float("Hej!") ValueError: could not convert string to float: Hej!

W tej sesji interaktywnej Python próbuje przekształcić łańcuch "Hej!" na liczbę zmiennoprzecinkową. Ponieważ okazuje się to niemożliwe, Python zgłasza wyjątek i wyświetla stosowne szczegóły. Wykorzystując funkcjonalność obsługi wyjątków oferowaną przez Pythona, możesz przechwytywać i obsługiwać wyjątki, tak aby Twój program nie kończył się w nagły i nieoczekiwany sposób (nawet jeśli użytkownik wprowadzi "Hej!" wtedy, gdy prosisz o podanie liczby). Możesz przynajmniej sprawić, aby Twój program zakończył się zgrabnie i elegancko.

Prezentacja programu Obsłuż to Program Obsłuż to otwiera się na wyjątki wynikające z danych wprowadzonych przez użytkownika, a potem celowo generuje kilka własnych wyjątków. Lecz zamiast przerwać swoje działanie, program wykonuje się do końca. Jest tak, ponieważ program obsługuje wyjątki, które są zgłaszane. Rysunek 7.5 pokazuje program w działaniu. Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 7.; nazwa pliku to obsluz_to.py.

Rysunek 7.5. Chociaż program nie może dokonać konwersji łańcucha "Hej!" na liczbę, nie przerywa swojego działania, kiedy zostają zgłoszone wyjątki

Obsługa wyjątków

Użycie instrukcji try z klauzulą except Najbardziej elementarnym sposobem obsługi (wyłapywania) wyjątków jest użycie instrukcji try z klauzulą except. Dzięki użyciu instrukcji try wydzielasz pewien fragment kodu, który mógłby potencjalnie wywołać wyjątek. Następnie tworzysz klauzulę except z blokiem instrukcji, które są wykonywane tylko w przypadku zgłoszenia wyjątku. Pierwszą czynnością, jaką wykonuję w programie Obsłuż to, jest poproszenie użytkownika o podanie liczby. Pobieram łańcuch znaków wprowadzony przez użytkownika i usiłuję dokonać konwersji tego łańcucha na liczbę zmiennoprzecinkową. Do obsługi wszystkich wyjątków, jakie mogłyby zostać zgłoszone w trakcie tej operacji, wykorzystuję try i except. # Obsłuż to # Demonstruje obsługę wyjątków # try/except try: num = float(input("Wprowadź liczbę: ")) except: print("Wystąpił jakiś błąd!")

Jeśli wywołanie funkcji float() powoduje zgłoszenie wyjątku (w następstwie wprowadzenia przez użytkownika łańcucha nienadającego się do konwersji, takiego jak na przykład "Hej!"), wyjątek zostaje wyłapany i użytkownik jest informowany, że Wystąpił jakiś błąd!. Jeśli nie został zgłoszony żaden wyjątek, liczba wprowadzona przez użytkownika zostaje przypisana do zmiennej num i program omija klauzulę except, kontynuując wykonywanie reszty kodu.

Specyfikacja typu wyjątku Różne rodzaje błędów skutkują różnymi typami wyjątków. Na przykład próba dokonania konwersji łańcucha "Hej!" za pomocą funkcji float() powoduje zgłoszenie wyjątku ValueError (błąd wartości), ponieważ znaki w łańcuchu mają niewłaściwą wartość (nie są cyframi). Istnieje ponad dwadzieścia typów wyjątków, lecz w tabeli 7.6 zostało wymienionych tylko kilka najczęściej występujących. Klauzula except umożliwia dokładne określenie typu wyjątków, jaki będzie obsługiwać. Aby określić jeden typ wyjątków, wystarczy podać jego specyfikację po słowie except. # specyfikacja typu wyjątku try: num = float(input("\nWprowadź liczbę: ")) except ValueError: print("To nie była liczba!")

W tej sytuacji funkcja print zostanie wykonana tylko wtedy, kiedy zostanie zgłoszony wyjątek ValueError. Dzięki temu mogę być konkretniejszy i wyświetlić komunikat To nie była liczba!. Jeśli jednak wewnątrz instrukcji try zostanie zgłoszony wyjątek jakiegoś innego typu, klauzula except go nie wyłapie i wykonywanie programu zostanie przerwane.

215

216

Rozdział 7. Pliki i wyjątki. Gra Turniej wiedzy Tabela 7.6. Wybrane typy wyjątków

Typ wyjątku

Opis

IOError

Zgłaszany w przypadku wystąpienia błędu operacji wejścia-wyjścia, takiego jak próba otwarcia nieistniejącego pliku w trybie odczytu.

IndexError

Zgłaszany przy indeksowaniu sekwencji, gdy numer indeksu wskazuje na nieistniejący element.

KeyError

Zgłaszany, gdy nie zostanie znaleziony klucz słownika.

NameError

Zgłaszany, gdy nie zostanie znaleziona nazwa (na przykład zmiennej lub funkcji).

SyntaxError

Zgłaszany, gdy zostanie wykryty błąd składni.

TypeError

Zgłaszany, gdy wbudowana operacja lub funkcja zostanie zastosowana do obiektu nieodpowiedniego typu.

ValueError

Zgłaszany, gdy wbudowana operacja lub funkcja otrzyma argument, który ma właściwy typ, ale nieodpowiednią wartość.

ZeroDivisionError

Zgłaszany, gdy drugi argument operacji dzielenia lub modulo jest równy zeru.

Specyfikowanie typów wyjątków pozwalające na obsługę każdego przypadku indywidualnie stanowi dobrą praktykę w programowaniu. Wyłapywanie wszystkich wyjątków w taki sposób, jak to zrobiłem w pierwszej klauzuli except programu, jest właściwie niebezpieczne. Na ogół powinieneś unikać uniwersalnych rozwiązań tego typu.

Wskazówka Kiedy powinno się stosować obsługę wyjątków? Każde miejsce zewnętrznej interakcji z Twoim programem jest warte rozważenia pod kątem wyjątków. Dobrym pomysłem jest obsługa wyjątków przy otwieraniu pliku do odczytu, nawet jeśli uważasz, że plik już istnieje. Możesz także zdefiniować obsługę wyjątków, gdy próbujesz przeprowadzać konwersję danych pochodzących z zewnętrznego źródła, takiego jak użytkownik.

Sztuczka Więc powiedzmy, że chcesz zdefiniować obsługę wyjątku, lecz nie jesteś całkiem pewien, jak się nazywa jego typ. Oto łatwy sposób na dowiedzenie się tego — wystarczy wygenerować ten wyjątek. Jeśli na przykład wiesz, że potrzebujesz obsługi wyjątku dzielenia przez zero, ale nie pamiętasz dokładnie, jak się nazywa odpowiedni typ wyjątku, skorzystaj z interpretera i podziel jakąś liczbę przez 0: >>> 1/0 Traceback (most recent call last): File "", line 1, in 1/0 ZeroDivisionError: int division or modulo by zero

Dzięki tej sesji interaktywnej mogę zobaczyć, że wyjątek ma nazwę ZeroDivisionError. Na szczęście interpreter nie jest nieśmiały i informuje Cię dokładnie, jaki typ wyjątku wywołałeś.

Obsługa wyjątków

Obsługa wielu typów wyjątków Pojedynczy fragment kodu może wygenerować różne typy wyjątków. Na szczęście możesz zdefiniować obsługę wielu typów wyjątków. Jednym ze sposobów realizacji tego zadania jest wymienienie ich w pojedynczej klauzuli except w postaci zamkniętej w nawiasach grupy elementów oddzielonych przecinkami: # obsłuż kilka typów wyjątków print() for value in (None, "Hej!"): try: print("Próba konwersji:", value, "-->", end=" ") print(float(value)) except (TypeError, ValueError): print("Wystąpił jakiś błąd!")

Ten kod próbuje dokonać konwersji dwóch różnych wartości na liczbę zmiennoprzecinkową. Obie próby się nie udają, lecz każda z nich generuje wyjątek innego typu. Wywołanie float(None) powoduje wyjątek typu TypeError, ponieważ funkcja może tylko dokonywać konwersji łańcuchów znaków i liczb. Wywołanie float("Hej!") powoduje powstanie wyjątku typu ValueError, ponieważ mimo że "Hej!" jest łańcuchem, znaki w nim zawarte mają złą wartość (nie są cyframi). Dzięki klauzuli except każdy z tych typów wyjątków jest obsługiwany. Innym sposobem wyłapywania wielu wyjątków jest zastosowanie kilku klauzul except. Po pojedynczej instrukcji try możesz użyć tyle klauzul except, ile chcesz: print() for value in (None, "Hej!"): try: print("Próba konwersji:", value, "-->", end=" ") print(float(value)) except TypeError: print("Możliwa jest tylko konwersja łańcucha lub liczby!") except ValueError: print("Możliwa jest tylko konwersja łańcucha cyfr!")

Teraz każdy typ wyjątku ma swój własny blok instrukcji. Więc kiedy zmienna value ma wartość None, zostaje wygenerowany wyjątek typu TypeError i wyświetlony łańcuch "Możliwa jest tylko konwersja łańcucha lub liczby!". Kiedy wartością zmiennej value jest "Hej!", zostaje zgłoszony wyjątek ValueError i wyświetlony łańcuch "Możliwa jest tylko konwersja łańcucha cyfr!". Użycie wielu klauzul except umożliwia Ci zdefiniowanie zindywidualizowanych reakcji na różne typy wyjątków wywołanych z tego samego bloku try. W tym przypadku dzięki indywidualnej obsłudze każdego z typów wyjątku podaję bardziej konkretny komunikat o błędzie.

217

218

Rozdział 7. Pliki i wyjątki. Gra Turniej wiedzy

Wykorzystanie argumentu wyjątku Kiedy zdarza się wyjątek, może mu towarzyszyć związana z nim wartość — jego argument. Tym argumentem jest zwykle oficjalny komunikat od Pythona z opisem wyjątku. Możesz otrzymać argument, jeśli po typie wyjątku podasz nazwę zmiennej poprzedzoną słowem kluczowym as. W tym przypadku otrzymuję argument wyjątku w zmiennej e i wypisuję jej wartość razem z moim zwykłym komunikatem o błędzie: # pobierz argument wyjątku try: num = float(input("\nWprowadź liczbę: ")) except ValueError as e: print("To nie była liczba! Lub wyrażając to na sposób Pythona...") print(e)

Dodanie klauzuli else Po wszystkich klauzulach except możesz dodać w instrukcji try pojedynczą klauzulę else. Instrukcje bloku else są wykonywane tylko wtedy, gdy w bloku try nie zostanie zgłoszony żaden wyjątek. # try/except/else try: num = float(input("\nWprowadź liczbę: ")) except ValueError: print("To nie była liczba!") else: print("Wprowadziłeś liczbę", num) input("\n\nAby zakończyć program, naciśnij klawisz Enter.")

W powyższym kodzie wartość zmiennej num jest wyświetlana w bloku else tylko wtedy, gdy instrukcja przypisania w bloku try nie zgłosi wyjątku. Jest to doskonałe rozwiązanie, ponieważ wartość zmiennej num zostanie wyświetlona tylko wówczas, gdy instrukcja przypisania zakończyła się sukcesem i zmienna istnieje.

Powrót do gry Turniej wiedzy Po opanowaniu podstaw plików i wyjątków nadszedł czas na zajęcie się grą Turniej wiedzy zaprezentowaną na początku rozdziału. Jedną z fajnych cech tego programu jest to, że odczytuje dane ze zwykłego pliku tekstowego, co umożliwia tworzenie własnych odcinków gry kwizowej z wykorzystaniem edytora tekstu i szczypty kreatywności. Jak zobaczysz w kodzie, plik tekstowy, z którego program odczytuje dane, kwiz.txt, musi znajdować się w tym samym katalogu co plik programu. Aby utworzyć swój własny odcinek z pełnym zestawem pytań, musisz tylko zastąpić ten plik takim, który będzie zawierać Twoje własne dzieło.

Powrót do gry Turniej wiedzy

Objaśnienie struktury pliku z danymi Zanim zacznę omawiać sam kod gry, powinieneś zrozumieć dokładnie strukturę pliku kwiz.txt. Pierwszy wiersz pliku zajmuje tytuł odcinka. Reszta pliku składa się z ośmiowierszowych bloków odpowiadających poszczególnym pytaniom. Możesz w nim umieścić tyle bloków (a tym samym pytań), ile Ci się podoba. Oto ogólne przedstawienie struktury bloku:

A oto początek pliku, jaki utworzyłem do tej gry: Odcinek nie do odrzucenia Plany na resztę życia Za działalność mafijną skazano Cię na "kopę lat z hakiem" w więzieniu. /To oznacza, że za kratkami spędzisz: co najmniej 15 lat co najmniej 25 lat co najmniej 50 lat co najmniej 60 lat 4 Kopa to sześćdziesiąt sztuk. Ojciec Chrzestny chce Cię bliżej poznać Powiedzmy, że masz audiencję u Ojca Chrzestnego soulu. Jak wypadałoby się /do niego zwrócić? Mr. Richard Mr. Domino Mr. Brown Mr. Checker 3 James Brown jest nazywany „ojcem chrzestnym” soulu.

Aby zaoszczędzić miejsca w książce, pokazuję tylko pierwsze 17 wierszy pliku obejmujące zakres dwóch pytań. Możesz obejrzeć zawartość całego pliku kwiz.txt, który znajduje się na stronie WWW książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 7. Pamiętaj, pierwszy wiersz w pliku, Odcinek nie do odrzucenia, jest w tej grze tytułem odcinka. Następne osiem wierszy dotyczy pierwszego pytania. A kolejne osiem odnosi się do pytania drugiego. Tak więc wiersz Plany na resztę życia to kategoria pierwszego pytania. Kategoria jest sprytnym sposobem wprowadzenia w tematykę pytania. Kolejny wiersz, Za działalność mafijną skazano Cię na "kopę lat z hakiem" w więzieniu. /To oznacza, że za kratkami spędzisz:, to pierwsze pytanie w tej grze. Następne cztery wiersze, co najmniej 15 lat, co najmniej 25 lat, co najmniej 50 lat i co najmniej 60 lat, to cztery możliwe odpowiedzi, spośród których gracz dokona wyboru.

219

220

Rozdział 7. Pliki i wyjątki. Gra Turniej wiedzy

Kolejny wiersz, 4, to numer poprawnej odpowiedzi. Więc w tym przypadku poprawną odpowiedzią na pytanie jest ostatnia odpowiedź, co najmniej 60 lat. Kolejny wiersz, Kopa to sześćdziesiąt sztuk., wyjaśnia, dlaczego poprawna odpowiedź jest poprawna. Ten sam schemat odnosi się do reszty pytań. Ważną rzeczą, która wymaga uwagi, jest to, że w dwóch z tych wierszy umieściłem znak prawego ukośnika (/). Ma on reprezentować nowy wiersz, ponieważ Python nie zawija automatycznie tekstu, kiedy go wypisuje. Kiedy program odczytuje wiersz z pliku, zastępuje wszystkie prawe ukośniki znakiem nowego wiersza. Zobaczysz dokładnie, jak program to robi, kiedy będę po kolei omawiał kod.

Funkcja open_file Pierwszą czynnością, jaką wykonuję w programie, jest zdefiniowanie funkcji open_file(), która w wywołaniu otrzymuje nazwę pliku i tryb (obydwa argumenty to łańcuchy znaków) i zwraca odpowiadający im obiekt pliku. Używam instrukcji try z klauzulą except do obsługi wyjątku IOError generowanego przez błędy wejścia-wyjścia, które wystąpiłyby na przykład, gdyby plik nie istniał. Wyłapanie wyjątku oznacza, że wystąpił problem z otwarciem pliku z kwizem. Jeśli się to zdarzy, kontynuacja programu nie ma sensu, więc wyświetlam odpowiedni komunikat i wywołuję funkcję sys.exit(). Funkcja ta zgłasza wyjątek, który skutkuje zakończeniem programu. Powinieneś używać funkcji sys.exit() tylko jako środka ostatecznego — w sytuacji, gdy musisz zakończyć program. Zauważ, że aby wywołać funkcję sys.exit(), musiałem zaimportować moduł sys. # Turniej wiedzy # Gra sprawdzająca wiedzę ogólną, odczytująca dane ze zwykłego pliku tekstowego import sys def open_file(file_name, mode): """Otwórz plik.""" try: the_file = open(file_name, mode) except IOError as e: print("Nie można otworzyć pliku", file_name, "Program zostanie zakończony.\n", e) input("\n\nAby zakończyć program, naciśnij klawisz Enter.") sys.exit() else: return the_file

Funkcja next_line() Następnie definiuję funkcję next_line(), która otrzymuje obiekt pliku i zwraca kolejny wiersz tekstu zawartego w pliku:

Powrót do gry Turniej wiedzy def next_line(the_file): """Zwróć kolejny wiersz pliku kwiz po sformatowaniu go.""" line = the_file.readline() line = line.replace("/", "\n") return line

Stosuję jednak do tego wiersza jeden mały element formatowania przed jego zwróceniem. Wszystkie prawe ukośniki zastępuję znakami nowego wiersza. Robię to, ponieważ Python nie zawija automatycznie wypisywanego tekstu bez dzielenia wyrazów. Moja procedura daje twórcy pliku tekstowego z kwizem pewną kontrolę nad formatowaniem. Może on lub ona wskazać miejsca, gdzie powinny wystąpić przejścia do nowego wiersza, tak aby słowa nie były dzielone między wiersze. Przyjrzyj się zawartości pliku kwiz.txt i danym wyjściowym gry Turniej wiedzy, aby zobaczyć to w działaniu. Spróbuj usunąć prawe ukośniki z pliku tekstowego i sprawdzić, jaki będzie tego efekt.

Funkcja next_block() Funkcja next_block() odczytuje kolejny blok wierszy dotyczący jednego pytania. Pobiera obiekt pliku i zwraca cztery łańcuchy znaków oraz listę łańcuchów znaków. Zwraca łańcuchy reprezentujące kategorię, pytanie, poprawną odpowiedź oraz wyjaśnienie, jak również listę czterech łańcuchów reprezentujących możliwe odpowiedzi na pytanie. def next_block(the_file): """Zwróć kolejny blok danych z pliku kwiz.""" category = next_line(the_file) question = next_line(the_file) answers = [] for i in range(4): answers.append(next_line(the_file)) correct = next_line(the_file) if correct: correct = correct[0] explanation = next_line(the_file) return category, question, answers, correct, explanation

Kiedy zostanie osiągnięty koniec pliku, odczyt wiersza zwróci pusty łańcuch. Więc kiedy program dotrze do końca pliku kwiz.txt, zmienna category otrzyma pusty łańcuch. Sprawdzam kategorię w funkcji main() programu. Kiedy staje się pustym łańcuchem, następuje koniec gry.

Funkcja welcome() Funkcja welcome() wita gracza i zapowiada tytuł odcinka. Funkcja otrzymuje tytuł odcinka w postaci łańcucha i wyświetla go razem z komunikatem powitalnym.

221

222

Rozdział 7. Pliki i wyjątki. Gra Turniej wiedzy def welcome(title): """Przywitaj gracza i pobierz jego nazwę.""" print("\t\t Witaj w turnieju wiedzy!\n") print("\t\t", title, "\n")

Zainicjowanie gry Następnie tworzę funkcję main(), która mieści w sobie główną pętlę gry. W pierwszej części funkcji inicjuję grę poprzez otwarcie pliku z kwizem, pobranie tytułu odcinka (pierwszy wiersz pliku), przywitanie gracza i ustawienie wyniku gracza na 0. def main(): trivia_file = open_file("kwiz.txt", "r") title = next_line(trivia_file) welcome(title) score = 0

Zadanie pytania Po czym wczytuję pierwszy blok wierszy dotyczących pierwszego pytania do zmiennych. Następnie uruchamiam pętlę while, która będzie kontynuować zadawanie pytań, dopóki zmienna category nie będzie reprezentować pustego łańcucha. Pusty łańcuch jako wartość zmiennej category oznacza, że został osiągnięty koniec pliku z kwizem i nie nastąpi wejście do ciała pętli. Zadaję pytanie, wyświetlając kategorię pytania, samo pytanie i cztery możliwe odpowiedzi. # pobierz pierwszy blok category, question, answers, correct, explanation = next_block(trivia_file) while category: # zadaj pytanie print(category) print(question) for i in range(4): print("\t", i + 1, "-", answers[i])

Pobranie odpowiedzi Następnie pobieram odpowiedź gracza: # uzyskaj odpowiedź answer = input("Jaka jest Twoja odpowiedź?: ")

Sprawdzenie odpowiedzi Potem porównuję odpowiedź gracza z odpowiedzią poprawną. Jeśli są zgodne, gracz otrzymuje gratulacje i jego (lub jej) wynik jest zwiększany o jeden. Jeśli się okażą niezgodne, gracz zostaje poinformowany o tym, że dokonał niewłaściwego wyboru. W obydwu przypadkach wyświetlam potem wyjaśnienie, które uzasadnia słuszność prawidłowej odpowiedzi. W końcu wyświetlam aktualny wynik gracza.

Podsumowanie # sprawdź odpowiedź if answer == correct: print("\nOdpowiedź prawidłowa!", end=" ") score += 1 else: print("\nOdpowiedź niepoprawna.", end=" ") print(explanation) print("Wynik:", score, "\n\n")

Pobranie następnego pytania Potem wywołuję funkcję next_block() i pobieram zestaw łańcuchów znaków dotyczących kolejnego pytania. Jeśli nie ma już więcej pytań, zmienna category otrzyma pusty łańcuch i wykonywanie pętli zostanie przerwane. # pobierz kolejny blok category, question, answers, correct, explanation = next_block(trivia_file)

Zakończenie gry Po zakończeniu pętli zamykam plik z pytaniami i wyświetlam wynik gracza: trivia_file.close() print("To było ostatnie pytanie!") print("Twój końcowy wynik wynosi", score)

Uruchomienie funkcji main() Ostatnie wiersze kodu uruchamiają funkcję main() i zamykają okno programu: main() input("\n\nAby zakończyć program, naciśnij klawisz Enter.")

Podsumowanie W tym rozdziale poznałeś pliki i wyjątki. Dowiedziałeś się, jak odczytywać dane z plików tekstowych. Zobaczyłeś, jak się odczytuje pojedynczy znak albo cały plik na raz. Poznałeś kilka różnych sposobów odczytywania zawartości pliku tekstowego po jednym pełnym wierszu na raz, które prawdopodobnie jest najczęściej stosowane. Dowiedziałeś się także, jak zapisywać dane do plików tekstowych — od pojedynczego znaku do listy łańcuchów — po czym dowiedziałeś się, jak zapisywać w plikach bardziej skomplikowane dane poprzez ich marynowanie oraz jak zarządzać grupą zamarynowanych obiektów zapisanych w pojedynczym pliku binarnym przy użyciu półki. Potem zobaczyłeś, jak można obsługiwać wyjątki zgłoszone w trakcie wykonywania programu. Zobaczyłeś, jak wyłapywać konkretne wyjątki i jak pisać kod do ich obsługi. Na koniec dowiedziałeś się, jak połączyć obsługę plików i wyjątków poprzez konstrukcję programu gry kwizowej, który umożliwia każdemu posiadaczowi edytora tekstu tworzenie swoich własnych odcinków kwizu.

223

224

Rozdział 7. Pliki i wyjątki. Gra Turniej wiedzy

Sprawdź swoje umiejętności 1. Ulepsz grę Turniej wiedzy w taki sposób, aby każde pytanie miało przypisaną indywidualnie określoną wartość punktową. Wynik uzyskany przez gracza powinien stanowić sumę wartości punktowych wszystkich pytań, na które gracz odpowiedział poprawnie. 2. Ulepsz grę Turniej wiedzy w taki sposób, aby w pliku była utrzymywana lista najlepszych wyników. Program powinien rejestrować nazwę gracza i jego wynik, jeśli ten mieści się na liście. Przechowuj najlepsze wyniki przy użyciu „zamarynowanego” obiektu. 3. Zmień sposób implementacji obsługi listy najlepszych wyników, którą utworzyłeś w poprzednim zadaniu. Tym razem do przechowywania listy użyj zwykłego pliku tekstowego. 4. Utwórz odcinek gry kwizowej, która testuje wiedzę gracza o obsłudze plików i wyjątków.

8 Obiekty programowe. Program Opiekun zwierzaka P

rogramowanie obiektowe (ang. object-oriented programming — OOP) jest odmiennym sposobem myślenia o programowaniu. Jest to nowoczesna metodologia, która została chętnie przyjęta przez branżę producentów oprogramowania i jest stosowana przy tworzeniu większości nowego komercyjnego oprogramowania. Podstawowym elementem konstrukcyjnym w programowaniu OOP jest obiekt programowy — często nazywany po prostu obiektem. W tym rozdziale postawisz swoje pierwsze kroki na drodze do zrozumienia metodologii OOP, kiedy będziesz poznawać obiekty. W szczególności nauczysz się:  tworzyć klasy definiujące obiekty,  pisać metody i tworzyć atrybuty do obiektów,  tworzyć obiekty jako instancje klas,  ograniczać dostęp do atrybutów obiektu.

Wprowadzenie do programu Opiekun zwierzaka Program Opiekun zwierzaka powierza użytkownikowi opiekę nad jego wirtualnym pupilem. Użytkownik nadaje zwierzakowi imię i jest całkowicie odpowiedzialny za utrzymywanie go w stanie szczęśliwości, co nie jest wcale łatwym zadaniem. Użytkownik musi karmić zwierzaka i bawić się z nim, aby podtrzymywać jego dobry nastrój. Użytkownik może słuchać zwierzaka, aby dowiedzieć się o jego samopoczuciu, które może zmieniać się od bycia szczęśliwym do wściekłości. Program został przedstawiony na rysunkach od 8.1 do 8.3.

226

Rozdział 8. Obiekty programowe. Program Opiekun zwierzaka

Rysunek 8.1. Musisz nazwać swojego zwierzaka

Rysunek 8.2. Jeśli nie nakarmisz swojego zwierzaka lub nie pobawisz się z nim, jego nastrój się pogorszy

Chociaż możliwe byłoby napisanie tego programu bez wykorzystania obiektów programowych, utworzyłem zwierzaka jako obiekt. Ostatecznie takie podejście sprawia, że program staje się łatwiejszy w obsłudze i modyfikacji. Poza tym umożliwia ono bezbolesne skalowanie. Kiedy już się utworzy jednego zwierzaka, utworzenie kolejnych dziesięciu i zarządzanie nimi nie wymaga wielkiego trudu. Czy daleko stąd do farmy zwierzaków? (Przekonasz się, że nie, kiedy spróbujesz rozwiązać zadania zamieszczone na końcu rozdziału).

Podstawy programowania obiektowego

Rysunek 8.3. Ale dzięki właściwej opiece, Twojemu zwierzakowi wróci jego pierwotny, pogodny nastrój

Podstawy programowania obiektowego Istnieje dość powszechna opinia, że programowanie OOP jest skomplikowane, ale ja uważam, że jest właściwie prostsze niż niektóre z koncepcji, które już poznałeś. W gruncie rzeczy programowanie OOP umożliwia Ci przedstawianie w swoich programach różnych rzeczy w sposób bardziej zbliżony do świata realnego. Tym, co często chcesz odwzorować w swoich programach — poczynając od rachunku bieżącego, a kończąc na statku kosmicznym z obcej planety — są obiekty wzięte z prawdziwego życia. Programowanie obiektowe pozwala Ci przedstawiać te obiekty z prawdziwego życia jako obiekty programowe. Podobnie jak obiekty ze świata rzeczywistego, obiekty programowe łączą w sobie cechy (zwane w terminologii OOP atrybutami) i zachowania (nazywane w języku OOP metodami). Gdybyś na przykład miał utworzyć obiekt statku kosmicznego obcych przybyszy, jego atrybuty obejmowałyby jego położenie i poziom energii, podczas gdy metody mogłyby obejmować jego zdolność do poruszania się lub rażenia celów za pomocą posiadanej broni. Obiekty są tworzone (czyli konkretyzowane w języku OOP) na podstawie definicji zwanej klasą (która jest elementem programowym mogącym zawierać definicje atrybutów i metod). Klasy to jakby plany czy wzorce. Klasa nie jest obiektem, lecz projektem obiektu. I jak budowniczy może wznieść wiele domów na podstawie tego samego planu, tak programista może utworzyć wiele obiektów na podstawie tej samej klasy. W rezultacie każdy obiekt (zwany również instancją lub egzemplarzem) skonkretyzowany na podstawie tej samej klasy będzie miał podobną strukturę. Więc jeśli dysponujesz klasą rachunku bieżącego, mógłbyś jej użyć do utworzenia wielu obiektów rachunku bieżącego. I te różne obiekty miałyby taką samą podstawową strukturę. Każdy z nich mógłby na przykład mieć atrybut o nazwie saldo.

227

228

Rozdział 8. Obiekty programowe. Program Opiekun zwierzaka

Ale tak jak możesz wybrać dwa domy wybudowane na podstawie tego samego planu i pomalować je w różny sposób, możesz również mieć dwa obiekty tej samej klasy i nadać każdemu z nich jego własny, unikalny zestaw wartości atrybutów. Więc mógłbyś mieć jeden obiekt rachunku bieżącego z atrybutem salda o wartości 100, a drugi z atrybutem salda o wartości 1 000 000.

Wskazówka Nie martw się, jeśli cała ta terminologia OOP nie jest jeszcze dla Ciebie całkowicie jasna. Chciałem Ci tylko przedstawić w ogólnym zarysie pojęcie obiektów. Podobnie jak w przypadku wszystkich nowych koncepcji z zakresu programowania, czytanie o nich nie wystarczy. Lecz po zapoznaniu się z pewną ilością prawdziwego kodu w języku Python, który definiuje klasy i tworzy obiekty (i po utworzeniu pewnej ilości własnego kodu) wkrótce zrozumiesz, o co chodzi w OOP.

Tworzenie klas, metod i obiektów Aby skonstruować obiekt, musisz najpierw mieć plan, czyli klasę. Klasy prawie zawsze zawierają metody reprezentujące czynności, które obiekt może wykonywać. Można co prawda utworzyć klasę bez żadnych metod, ale nie byłoby to zbyt interesujące.

Prezentacja programu Prosty zwierzak Program Prosty zwierzak zawiera Twój pierwszy przykład klasy napisanej w Pythonie. Definiuję w nim wyjątkowo prosty typ zwierzaka, który potrafi tylko jedno — mówić „cześć”. Chociaż tego rodzaju zwierzak mógłby się wydawać zbyt prosty, przynajmniej jest uprzejmy. Wynik działania tego programu został przedstawiony na rysunku 8.4.

Rysunek 8.4. Kiedy program wywołuje metodę talk() obiektu klasy Critter, zwierzak pozdrawia świat

Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 8.; nazwa pliku to prosty_zwierzak.py. Sam program jest dość krótki. # Prosty zwierzak # Demonstruje podstawową klasę i obiekt

Tworzenie klas, metod i obiektów class Critter(object): """Wirtualny pupil""" def talk(self): print("Cześć! Jestem egzemplarzem klasy Critter.") # część główna crit = Critter() crit.talk() input("\n\nAby zakończyć program, naciśnij klawisz Enter.")

Definiowanie klasy Program rozpoczyna się od definicji klasy, czyli projektu mojego pierwszego zwierzaka. Pierwszym wierszem tej definicji jest nagłówek klasy: class Critter(object):

Użyłem słowa kluczowego class, po którym wpisałem wybraną przez siebie nazwę klasy: Critter. Zauważ, że moja nazwa klasy rozpoczyna się od dużej litery. Python tego nie wymaga, ale jest to standardowa konwencja, więc i Ty powinieneś rozpoczynać wszystkie swoje nazwy klas od dużej litery. Następnie poleciłem Pythonowi, aby oparł moją klasę na podstawowym, wbudowanym typie o nazwie object. Możesz oprzeć nową klasę na typie object lub na dowolnej uprzednio zdefiniowanej klasie, lecz jest to temat z rozdziału 9., „Programowanie obiektowe. Gra Blackjack”. W tym rozdziale klasą bazową wszystkich moich klas jest typ object. Następny wiersz to łańcuch dokumentacyjny klasy. Dobry łańcuch dokumentacyjny opisuje rodzaj obiektów, do tworzenia których klasa może być wykorzystywana. Mój łańcuch dokumentacyjny jest dość prosty: """Wirtualny pupil"""

Definiowanie metody Ostatnia część kodu klasy definiuje metodę. Bardzo to przypomina definicję funkcji: def talk(self): print("Cześć! Jestem egzemplarzem klasy Critter.")

Właściwie możesz myśleć o metodach jako o funkcjach związanych z obiektem. (Spotkałeś się z tym już wcześniej na przykładzie metod łańcucha lub listy). Metoda talk() wyświetla łańcuch "Cześć! Jestem egzemplarzem klasy Critter.". Zwróć uwagę, że metoda talk() ma jeden parametr, self, (którego akurat nie używa). Każda metoda instancji — metoda, którą ma każdy obiekt klasy — musi mieć specjalny pierwszy parametr noszący zgodnie z konwencją nazwę self. Ten parametr dostarcza metodzie sposobu odwoływania się do samego obiektu. Na razie nie martw się o parametr self, zobaczysz go w działaniu w nieco dalszej części tego rozdziału.

229

230

Rozdział 8. Obiekty programowe. Program Opiekun zwierzaka

Pułapka Jeśli utworzysz metodę instancji bez jakichkolwiek parametrów, wygenerujesz błąd przy jej wywołaniu. Pamiętaj, wszystkie metody instancji muszą zawierać specjalny pierwszy parametr noszący zgodnie z konwencją nazwę self.

Konkretyzacja obiektu Po napisaniu kodu klasy konkretyzacja nowego obiektu zajęła mi tylko jeden wiersz: crit = Critter()

W tym wierszu tworzony jest nowiutki obiekt klasy Critter, który zostaje przypisany do zmiennej crit. Zwróć uwagę na nawiasy występujące po nazwie klasy Critter w instrukcji przypisania. Ich użycie przy tworzeniu nowego obiektu ma kluczowe znaczenie. Możesz przypisać nowo skonkretyzowany obiekt do zmiennej o dowolnej nazwie. Nazwa ta nie musi być oparta na nazwie klasy. Powinieneś jednak unikać na ogół używania jako nazwy obiektu pisanej małą literą nazwy klasy, ponieważ może to prowadzić do zamieszania.

Wywoływanie metody Mój nowy obiekt posiada metodę o nazwie talk(). Metoda ta jest podobna do dowolnej innej metody spośród tych, z jakimi już się spotkałeś. Zasadniczo jest to funkcja, która należy do obiektu. Mogę wywoływać tę metodę tak samo jak każdą inną przy użyciu notacji z kropką: crit.talk()

Wiersz ten wywołuje metodę talk() obiektu klasy Critter przypisanego do zmiennej crit. Metoda ta po prostu wyświetla łańcuch znaków "Cześć! Jestem egzemplarzem klasy Critter.".

Używanie konstruktorów Zobaczyłeś, jak można tworzyć metody takie jak talk(), lecz możesz napisać specjalną metodę, zwaną konstruktorem, która jest wywoływana automatycznie zaraz po utworzeniu nowego obiektu. Metoda odgrywająca rolę konstruktora jest niezwykle pożyteczna. Prawdę mówiąc, będziesz często pisać jedną taką metodę do każdej tworzonej przez siebie klasy. Konstruktor jest często wykorzystywany do nadania wartości początkowych atrybutom obiektu, chociaż w niżej przedstawionym programie nie użyję jej w tym celu.

Używanie konstruktorów

Prezentacja programu Zwierzak z konstruktorem Program Zwierzak z konstruktorem definiuje nową klasę Critter, która zawiera prosty konstruktor. Program pokazuje również, jak łatwo można tworzyć wiele obiektów na podstawie tej samej klasy. Na rysunku 8.5 pokazano przykładowe uruchomienie programu.

Rysunek 8.5. Zostają utworzone dwa oddzielne zwierzaki. Każdy z nich mówi „cześć”

Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 8.; nazwa pliku to zwierzak_z_konstruktorem.py. # Zwierzak z konstruktorem # Demonstruje konstruktory class Critter(object): """Wirtualny pupil""" def __init__(self): print("Urodził się nowy zwierzak!") def talk(self): print("\nCześć! Jestem egzemplarzem klasy Critter.") # część główna crit1 = Critter() crit2 = Critter() crit1.talk() crit2.talk() input("\n\nAby zakończyć program, naciśnij klawisz Enter.")

231

232

Rozdział 8. Obiekty programowe. Program Opiekun zwierzaka

Tworzenie konstruktora Pierwszym nowym fragmentem kodu w definicji klasy jest konstruktor (zwany również metodą inicjalizacji): def __init__(self): print("Urodził się nowy zwierzak!")

Zwykle sam tworzysz nazwy swoich metod, ale w tym miejscu użyłem szczególnej nazwy metody, rozpoznawanej przez Python. Nadając metodzie nazwę __init__, poinformowałem Pythona, że jest to mój konstruktor. Jako konstruktor metoda __init__ jest wywoływana automatycznie przez każdy nowo tworzony obiekt klasy Critter natychmiast po zaistnieniu obiektu. Jak widać w drugim wierszu kodu metody, oznacza to, że każdy nowo utworzony obiekt klasy Critter automatycznie ogłasza światu swoje powstanie poprzez wyświetlenie łańcucha "Urodził się nowy zwierzak!".

Wskazówka Python posiada kolekcję wbudowanych „metod specjalnych”, których nazwy rozpoczynają się od dwóch znaków podkreślenia i tak samo się kończą, tak jak w przypadku konstruktora __init__.

Tworzenie wielu obiektów Po napisaniu kodu klasy utworzenie wielu obiektów to pestka. W głównej części programu tworzę dwa: # część główna crit1 = Critter() crit2 = Critter()

W rezultacie zostają utworzone dwa obiekty. Każdy z nich zaraz po konkretyzacji wyświetla łańcuch "Urodził się nowy zwierzak!" poprzez swoją metodę — konstruktora. Każdy obiekt to osobny, w pełni ukształtowany zwierzak. Aby to udowodnić, wywołuję ich metody talk(): crit1.talk() crit2.talk()

Mimo że te dwa wiersze kodu wypisują dokładnie takie same łańcuchy znaków, "\nCześć! Jestem egzemplarzem klasy Critter.", każdy z nich jest wynikiem działania

innego obiektu.

Wykorzystywanie atrybutów Możesz sprawić, że atrybuty obiektu zostaną automatycznie utworzone i zainicjalizowane tuż po ich utworzeniu, poprzez wykorzystanie konstruktora. Jest to duża wygoda i coś, co często będziesz stosował.

Wykorzystywanie atrybutów

Prezentacja programu Zwierzak z atrybutem Program Zwierzak z atrybutem tworzy nowy typ obiektu z atrybutem name (nazwa). Klasa Critter zawiera konstruktor, który tworzy i inicjalizuje atrybut name. Program wykorzystuje nowy atrybut do personalizacji pozdrowienia artykułowanego przez zwierzaka. Rysunek 8.6 pokazuje program w działaniu.

Rysunek 8.6. Tym razem każdy obiekt klasy Critter ma atrybut name, który wykorzystuje, mówiąc „cześć”

Oto kod tego programu: # Zwierzak z atrybutem # Demonstruje tworzenie atrybutów obiektu i uzyskiwanie do nich dostępu class Critter(object): """Wirtualny pupil""" def __init__(self, name): print("Urodził się nowy zwierzak!") self.name = name def __str__(self): rep = "Obiekt klasy Critter\n" rep += "name: " + self.name + "\n" return rep def talk(self): print("Cześć! Jestem", self.name, "\n") # część główna crit1 = Critter("Reksio") crit1.talk() crit2 = Critter("Pucek")

233

234

Rozdział 8. Obiekty programowe. Program Opiekun zwierzaka crit2.talk() print("Wyświetlenie obiektu crit1:") print(crit1) print("Bezpośrednie wyświetlenie wartości atrybutu crit1.name:") print(crit1.name) input("\n\nAby zakończyć program, naciśnij klawisz Enter.")

Inicjalizacja atrybutów Konstruktor w tym programie wyświetla komunikat "Urodził się nowy zwierzak!" zupełnie tak samo jak konstruktor w programie Zwierzak z konstruktorem, ale kolejny wiersz metody robi coś nowego. Tworzy atrybut name dla nowego obiektu i nadaje mu wartość parametru name. Więc wykonanie znajdującej się w głównej części programu instrukcji: crit1 = Crittter("Reksio")

skutkuje utworzeniem nowego obiektu klasy Critter z atrybutem name, któremu została nadana wartość "Reksio". Na koniec obiekt zostaje przypisany do zmiennej crit1. Żebyś mógł dokładnie zrozumieć, jak to wszystko działa, wyjawię, czym jest tajemniczy parametr self. Jako pierwszy parametr każdej metody, self automatycznie otrzymuje jako swoją wartość referencję do obiektu wywołującego metodę. To oznacza, że dzięki parametrowi self metoda może sięgnąć do wywołującego ją obiektu i uzyskać dostęp do jego atrybutów i metod (a nawet utworzyć nowe atrybuty dla tego obiektu).

Wskazówka Możesz nadać pierwszemu parametrowi w nagłówku metody inną nazwę niż self, ale nie powinieneś tego robić. Nazwa self jest charakterystyczna dla Pythona i inni programiści będą jej oczekiwać.

Wróćmy więc do konstruktora. Parametr self automatycznie otrzymuje referencję do nowego obiektu klasy Critter, podczas gdy parametr name otrzymuje wartość "Reksio". Następnie wiersz: self.name = name

tworzy atrybut name dla obiektu i nadaje mu wartość parametru name, którą jest łańcuch "Reksio". Z kolei instrukcja przypisania w głównej części programu przypisuje ten nowy obiekt do zmiennej crit1. To oznacza, że zmienna crit1 odwołuje się do nowego obiektu z jego własnym atrybutem o nazwie name i wartości "Reksio". Tak więc zwierzak został utworzony ze swoim własnym imieniem! Wiersz głównej części programu: crit2 = Critter("Pucek")

Wykorzystywanie atrybutów

uruchamia taki sam podstawowy łańcuch zdarzeń. Lecz tym razem nowy obiekt klasy Critter zostaje utworzony ze swoim własnym atrybutem name ustawionym na "Pucek". A obiekt zostaje przypisany do zmiennej crit2.

Uzyskiwanie dostępu do atrybutów Atrybuty nie miałyby żadnego sensu, gdyby nie można było ich używać, więc napisałem bardziej spersonalizowaną metodę talk(), która wykorzystuje atrybut name obiektu klasy Critter. Teraz, kiedy zwierzak mówi „cześć”, przedstawia się swoim imieniem. Zmusiłem mojego pierwszego zwierzaka do powiedzenia „cześć” poprzez wywołanie metody talk w wierszu: crit1.talk()

Metoda talk() otrzymuje poprzez swój parametr self przesłaną automatycznie referencję do obiektu: def talk(self):

Następnie funkcja print wyświetla tekst Cześć! Jestem Reksio, uzyskując dostęp do atrybutu name obiektu poprzez odwołanie self.name: print("Cześć! Jestem", self.name, "\n")

Te same podstawowe zdarzenia zachodzą, kiedy potem wywołuję tę metodę dla mojego drugiego obiektu: crit2.talk()

Lecz tym razem metoda talk() wyświetla tekst Cześć! Jestem Pucek, ponieważ wartością atrybutu name obiektu crit2 jest łańcuch "Pucek". Domyślnie możesz uzyskiwać dostęp do atrybutów obiektu oraz modyfikować ich wartość z zewnątrz jego klasy. W głównej części programu skorzystałem z bezpośredniego dostępu do atrybutu name obiektu crit1: print(crit1.name)

Powyższy wiersz kodu wyświetla łańcuch "Reksio". Na ogół, aby uzyskać dostęp do atrybutu obiektu z zewnątrz klasy tego obiektu, można użyć notacji z kropką. Wpisz nazwę zmiennej, po niej kropkę, a po kropce nazwę atrybutu.

Wskazówka Zwykle starasz się unikać korzystania z bezpośredniego dostępu do atrybutów obiektu poza definicją jego klasy. Dowiesz się o tym więcej w dalszej części tego rozdziału, w podrozdziale „Hermetyzacja obiektów”.

235

236

Rozdział 8. Obiekty programowe. Program Opiekun zwierzaka

Wyświetlanie obiektu Standardowo, gdybym chciał wyświetlić obiekt za pomocą kodu print(crit1), Python zwróciłby coś, co wygląda dość zagadkowo:

Dowiaduję się, że wyświetliłem obiekt klasy Critter w głównej części swojego programu, ale nie otrzymuję żadnych użytecznych informacji o samym obiekcie. Istnieje jednak sposób na zmianę tego stanu rzeczy. Poprzez dołączenie metody specjalnej __str__() do definicji klasy możesz utworzyć reprezentację każdego z jej obiektów w postaci łańcucha znaków, który zostanie wypisany, ilekroć wystąpi polecenie wyświetlenia obiektu. Jako reprezentacja obiektu zostanie wyświetlony łańcuch znaków zwrócony przez metodę __str__(), bez względu na jego zawartość. Metoda __str__(), którą napisałem, zwraca łańcuch, który zawiera wartość atrybutu name obiektu. Więc kiedy wykonywany jest wiersz: print(crit1)

pojawia się poniższy, bardziej użyteczny tekst: Obiekt klasy Critter name: Reksio

Sztuczka Nawet jeśli w ogóle nie zamierzasz wyświetlać obiektu w swoim programie, utworzenie metody __str__() nadal nie jest złym pomysłem. Może się okazać, że możliwość zobaczenia wartości atrybutów obiektu pomoże Ci zrozumieć działanie programu (lub brak spodziewanego działania).

Wykorzystanie atrybutów klasy i metod statycznych Dzięki atrybutom różne obiekty tej samej klasy mogą zawierać swoje własne, unikalne wartości. Mógłbyś na przykład posiadać 10 różnych, biegających wokół zwierzaków, z których każdy ma swoje własne imię. Ale możesz mieć pewne informacje, które nie odnoszą się do poszczególnych obiektów, lecz do całej klasy. Mógłbyś, powiedzmy, chcieć śledzić na bieżąco liczbę utworzonych przez siebie zwierzaków. Można by było dodać do każdego obiektu klasy Critter atrybut o nazwie total. Ale wówczas, ilekroć byłby tworzony nowy obiekt, musiałbyś aktualizować wartość atrybutu total każdego istniejącego obiektu. Byłoby to prawdziwym utrapieniem. Na szczęście Python daje możliwość utworzenia pojedynczej wartości, która będzie związana z samą klasą, zwanej atrybutem klasy. Jeśli postrzegamy klasę jako plan, to atrybut klasy możemy sobie wyobrazić jako samoprzylepną karteczkę przyklejoną do tego planu. Istnieje tylko jeden jej egzemplarz, bez względu na to, ile rzeczy wytworzysz na podstawie planu.

Wykorzystanie atrybutów klasy i metod statycznych

Mogłoby się również okazać, że potrzebujesz metody, która jest związana z klasą; w tej sytuacji Python oferuje metodę statyczną. Ponieważ metody statyczne są związane z klasą, często wykorzystuje się w nich atrybuty klasy.

Prezentacja programu Zwierzak z klasą Nie, program Zwierzak z klasą nie dotyczy zwierzaka, który chodził do prywatnej szkoły przygotowującej do życia w wielkim świecie i szydzi z innych zwierzaków, które nie wiedzą, jak posługiwać się widelcem. Ten program wiąże się ze stosowaniem atrybutów i metod, które należą raczej do klasy niż do konkretnego obiektu. Program definiuje atrybut klasy, który rejestruje ogólną liczbę skonkretyzowanych obiektów klasy Critter. Klasa zawiera również metodę statyczną, która wyświetla tę liczbę. Rysunek 8.7 pokazuje wynik działania programu.

Rysunek 8.7. Zwierzaki rodzą się na prawo i lewo! Program śledzi je wszystkie poprzez pojedynczy atrybut klasy, którego wartość wyświetla za pomocą metody statycznej

Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 8.; nazwa pliku to zwierzak_z_klasa.py. # Zwierzak z klasą # Demonstruje atrybuty klasy i metody statyczne class Critter(object): """Wirtualny pupil""" total = 0 @staticmethod def status(): print("\nOgólna liczba zwierzaków wynosi", Critter.total)

237

238

Rozdział 8. Obiekty programowe. Program Opiekun zwierzaka def __init__(self, name): print("Urodził się zwierzak!") self.name = name Critter.total += 1 #część główna print("Uzyskanie dostępu do atrybutu klasy Critter.total:", end=" ") print(Critter.total) print("\nTworzenie zwierzaków.") crit1 = Critter("zwierzak 1") crit2 = Critter("zwierzak 2") crit3 = Critter("zwierzak 3") Critter.status() print("\nUzyskanie dostępu do atrybutu klasy poprzez obiekt:", end= " ") print(crit1.total) input("\n\nAby zakończyć program, naciśnij klawisz Enter.")

Tworzenie atrybutu klasy Drugi wiersz mojej definicji klasy: total = 0

tworzy atrybut klasy o nazwie total i przypisuje mu wartość 0. Każda instrukcja przypisania tego rodzaju — nowa zmienna otrzymuje wartość poza kodem metody — tworzy atrybut klasy. Instrukcja przypisania jest wykonywana tylko raz, kiedy Python po raz pierwszy napotyka definicję klasy. To oznacza, że atrybut klasy już istnieje, zanim zostanie utworzony choć jeden obiekt. Więc możesz wykorzystywać atrybut klasy nawet w sytuacji, gdy żadne jej obiekty nie zostały powołane do istnienia.

Uzyskiwanie dostępu do atrybutu klasy Uzyskiwanie dostępu do atrybutu klasy jest proste. Korzystam z dostępu do nowego atrybutu klasy w kilku różnych miejscach programu. W głównej części programu wypisuję jego wartość za pomocą instrukcji print Critter.total

W metodzie statycznej status() wyświetlam wartość atrybutu klasy Critter za pomocą wiersza kodu: print("\nOgólna liczba zwierzaków wynosi", Critter.total)

W konstruktorze zwiększam wartość tego atrybutu klasy, wykorzystując instrukcję: Critter.total += 1

Wykorzystanie atrybutów klasy i metod statycznych

Dzięki tej instrukcji za każdym razem, gdy konkretyzowany jest nowy obiekt, wartość atrybutu jest zwiększana o 1. Generalnie w celu uzyskania dostępu do atrybutu klasy stosuj notację z kropką. Wpisz nazwę klasy, umieść po niej kropkę, a po kropce podaj nazwę atrybutu. Wreszcie możesz uzyskać dostęp do atrybutu klasy poprzez obiekt tej klasy. Właśnie to zrobiłem w głównej części programu, w następującym wierszu: print(crit1.total)

Powyższa instrukcja wyświetla wartość atrybutu klasy: total (a nie atrybutu samego obiektu). Można odczytać wartość atrybutu klasy, wykorzystując dowolny obiekt, który należy do tej klasy. Więc mógłbym użyć instrukcji print(crit2.total) lub print(crit3.total) i otrzymałbym w tym przypadku taki sam wynik.

Pułapka Chociaż możesz wykorzystać obiekt określonej klasy do uzyskania dostępu do atrybutu klasy, nie możesz przypisać nowej wartości do atrybutu klasy poprzez obiekt. Jeśli chcesz zmienić wartość atrybutu klasy, uzyskaj do niego dostęp poprzez nazwę klasy.

Tworzenie metody statycznej Pierwszą metodą w klasie jest status(): def status(): print("\nOgólna liczba zwierzaków wynosi", Critter.total)

Napisałem tę definicję jako część tworzenia metody statycznej. Zwróć uwagę, że definicja nie zawiera zmiennej self w swojej liście parametrów. Jest tak dlatego, że podobnie jak wszystkie metody statyczne ma być wywoływana poprzez klasę, a nie poprzez obiekt. Więc do metody nie zostanie przekazana referencja do obiektu i tym samym nie potrzebuje ona parametru w rodzaju self służącego do przyjęcia takiej referencji. Metody statyczne mogą oczywiście mieć parametry, lecz w tym konkretnym przypadku nie potrzebowałem żadnego. Chociaż powyższa definicja tworzy metodę o nazwie status, bezpośrednio przed nią wstawiłem dekorator —możesz sobie to wyobrazić jako dekorowanie czy też modyfikowanie funkcji lub metody. Ten dekorator ostatecznie tworzy metodę statyczną o tej samej nazwie: @staticmethod

Dzięki tym trzem wierszom kodu klasa dysponuje metodą statyczną status(), która wyświetla ogólną liczbę obiektów klasy Critter poprzez wypisanie wartości atrybutu klasy o nazwie total. Tworząc swoją własną metodę statyczną, postępuj zgodnie z moim przykładem. Zacznij od dekoratora @staticmethod i umieść po nim definicję metody klasy. A ponieważ jest to metoda dla całej klasy, nie uwzględnisz w niej parametru self, który jest niezbędny tylko w metodach obiektu.

239

240

Rozdział 8. Obiekty programowe. Program Opiekun zwierzaka

Wywoływanie metody statycznej Wywoływanie metody statycznej jest proste. W pierwszym wierszu głównej części programu wywołuję metodę statyczną: Critter.status()

Jak mógłbyś się domyślić, zostanie wyświetlone 0, ponieważ nie zostały utworzone żadne obiekty. Ale zwróć uwagę, że mogę wywołać tę metodę nawet wówczas, gdy nie istnieje ani jeden obiekt. Ponieważ metody statyczne są wywoływane poprzez samą klasę, nie muszą istnieć żadne obiekty tej klasy, zanim będziesz mógł te metody wywołać. Następnie tworzę trzy obiekty. Potem ponownie wywołuję metodę status(), która wyświetla komunikat stwierdzający, że istnieją trzy zwierzaki. Wszystko działa jak należy, ponieważ w trakcie wykonywania kodu konstruktora przy tworzeniu każdego z tych obiektów wartość atrybutu klasy total jest zwiększana o 1.

Hermetyzacja obiektów Po raz pierwszy zetknąłeś się z koncepcją hermetyzacji w kontekście funkcji w podrozdziale „Pojęcie hermetyzacji” w rozdziale 6. Zobaczyłeś, że funkcje są poddane hermetyzacji i ukrywają szczegóły swoich wewnętrznych mechanizmów przed tą częścią programu, która je wywołuje (zwaną klientem funkcji). Dowiedziałeś się, że klient dobrze zdefiniowanej funkcji komunikuje się z nią jedynie poprzez jej parametry i wartości zwrotne. Na ogół obiekty powinny być traktowane w taki sam sposób. Klienty powinny komunikować się z obiektami poprzez parametry metod i ich wartości zwrotne. Generalnie kod klienta powinien unikać bezpośredniej zmiany wartości atrybutu obiektu. Jak zawsze, najlepiej pomaga konkretny przykład. Załóżmy, że masz do czynienia z obiektem klasy Checking_Account (rachunek bieżący) z atrybutem balance (saldo). Powiedzmy, że Twój program musi obsługiwać wypłaty z kont, które zmniejszają wartość atrybutu balance obiektu o pewną kwotę. Aby dokonać wypłaty, kod klienta mógłby po prostu odjąć określoną liczbę od wartości atrybutu balance. Taki bezpośredni dostęp jest łatwy dla klienta, ale może spowodować problemy. Kod klienta może odjąć taką liczbę, że saldo balance stanie się ujemne, co mogłoby być uważane za nie do zaakceptowania (szczególnie przez bank). O wiele lepiej jest mieć do dyspozycji metodę o nazwie withdraw() (wypłać), która pozwoli klientowi na zgłoszenie żądania wypłaty poprzez przekazanie jej kwoty do metody. Wtedy sam obiekt może obsłużyć to żądanie. Jeśli kwota okaże się zbyt duża, obiekt może sobie z tym poradzić, prawdopodobnie odrzucając transakcję. Obiekt chroni swoje bezpieczeństwo, umożliwiając jedynie pośredni dostęp do swoich atrybutów poprzez metody.

Używanie atrybutów i metod prywatnych

Używanie atrybutów i metod prywatnych Domyślnie wszystkie atrybuty i metody obiektu są publiczne, co oznacza, że klient może mieć do nich bezpośredni dostęp bądź je wywoływać. Aby wesprzeć hermetyzację, możesz zdefiniować atrybut lub metodę jako prywatne, co oznaczałoby, że tylko inne metody tego samego obiektu mogłyby łatwo uzyskać dostęp do takiego atrybutu lub wywołać taką metodę.

Prezentacja programu Prywatny zwierzak Program Prywatny zwierzak konkretyzuje obiekt, który ma zarówno prywatne, jak i publiczne atrybuty i metody. Na rysunku 8.8 pokazano jego przykładowe uruchomienie.

Rysunek 8.8. Dostęp do atrybutu prywatnego i do metody prywatnej został uzyskany w sposób pośredni

Omówię kod tego programu, dzieląc go na fragmenty, ale jego całość możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 8.; nazwa pliku to prywatny_zwierzak.py.

Tworzenie atrybutów prywatnych Aby ograniczyć bezpośredni dostęp klientów do atrybutów obiektów, możesz użyć atrybutów prywatnych. W konstruktorze tworzę dwa atrybuty, jeden publiczny i jeden prywatny: # Prywatny zwierzak # Demonstruje zmienne i metody prywatne class Critter(object): """Wirtualny pupil""" def __init__(self, name, mood):

241

242

Rozdział 8. Obiekty programowe. Program Opiekun zwierzaka print("Urodził się nowy zwierzak!") self.name = name # atrybut publiczny self.__mood = mood # atrybut prywatny

Dwa znaki podkreślenia, które rozpoczynają nazwę drugiego atrybutu, stanowią dla Pythona informację, że jest to atrybut prywatny. Jeśli chcesz utworzyć swój własny atrybut prywatny, wystarczy, że jego nazwę rozpoczniesz od dwóch znaków podkreślenia.

Wskazówka Możesz utworzyć prywatny atrybut klasy poprzez umieszczenie na początku jego nazwy dwóch znaków podkreślenia.

Dostęp do atrybutów prywatnych Dostęp do prywatnego atrybutu obiektu z wnętrza definicji klasy obiektu nie podlega dyskusji. (Pamiętaj, że atrybuty prywatne zostały wymyślone po to, aby odwieść programistę od używania bezpośredniego dostępu do atrybutów w kodzie klienckim). Ja wykorzystuję dostęp do atrybutu prywatnego w metodzie talk(): def talk(self): print("\nJestem", self.name) print("W tej chwili czuję się", self.__mood, "\n")

Ta metoda wyświetla wartość prywatnego atrybutu obiektu, który reprezentuje stan nastroju zwierzaka. Gdybym spróbował uzyskać dostęp do tego atrybutu spoza definicji klasy Critter, miałbym kłopot. Oto sesja interaktywna, która powinna pokazać, co mam na myśli: >>> crit = Critter(name = "Reksio", mood = "szczęśliwy") Urodził się nowy zwierzak! >>> print(crit.mood) Traceback (most recent call last): File "", line 1, in print(crit.mood) AttributeError: 'Critter' object has no attribute 'mood'

Poprzez zgłoszenie wyjątku AttributeError Python informuje, że obiekt crit nie ma atrybutu mood. Jeśli uważasz, że możesz przechytrzyć Pythona przez dodanie dwóch początkowych znaków podkreślenia, jesteś w błędzie. To właśnie wypróbowałem w następnej części mojej sesji interaktywnej: >>> print(crit.__mood) Traceback (most recent call last): File "", line 1, in print(crit.__mood) AttributeError: 'Critter' object has no attribute '__mood'

To także skutkuje zgłoszeniem wyjątku AttributeError. Python znów twierdzi, że atrybut nie istnieje. Czy to oznacza, że wartość atrybutu prywatnego jest całkowicie niedostępna na zewnątrz definicji jego klasy? Cóż, nie jest to prawda. Python ukrywa

Używanie atrybutów i metod prywatnych

atrybut za specjalną konwencją nazewniczą, chociaż dostęp do niego jest wciąż technicznie możliwy. I właśnie to zrobiłem w kolejnej części mojej sesji interaktywnej: >>> print(crit._Critter__mood) szczęśliwy

Powyższa instrukcja wyświetla wartość tego nieuchwytnego atrybutu prywatnego, którego wartością jest w tym przypadku łańcuch "szczęśliwy". Skoro dostęp do atrybutów prywatnych jest technicznie możliwy, możesz pomyśleć: do czego są one potrzebne? Cóż, w języku Python prywatność stanowi wskaźnik, że atrybut lub metoda są przeznaczone tylko do wewnętrznego użytku obiektu. Dodatkowo pomaga ona zapobiec nieumyślnemu dostępowi do takiego atrybutu czy metody.

Wskazówka Nigdy nie powinieneś próbować bezpośredniego dostępu do prywatnych atrybutów lub metod obiektu z zewnątrz definicji jego klasy.

Tworzenie metod prywatnych Możesz utworzyć metodę prywatną w ten sam prosty sposób, w jaki tworzy się prywatny atrybut: poprzez umieszczenie na początku jego nazwy dwóch znaków podkreślenia. To właśnie w kolejnej definicji metody zawartej w tej klasie: def __private_method(self): print("To jest metoda prywatna.")

Jest to metoda prywatna, lecz każda z pozostałych metod klasy ma do niej łatwy dostęp. Podobnie jak w przypadku atrybutów prywatnych, z dostępu do metod prywatnych powinny z założenia korzystać własne metody obiektu.

Dostęp do metod prywatnych Tak jak w przypadku atrybutów prywatnych, uzyskiwanie dostępu do prywatnych metod obiektu w obrębie definicji jego klasy jest proste. W metodzie public_method() wykorzystuję dostęp do prywatnej metody klasy: def public_method(self): print("To jest metoda publiczna.") self.__private_method()

Metoda ta wyświetla łańcuch "To jest metoda publiczna", a potem wywołuję metodę prywatną obiektu. Podobnie jak prywatne atrybuty, metody prywatne nie są przeznaczone do tego, aby klienty korzystały z bezpośredniego dostępu do nich. Wracając do mojej sesji interaktywnej, próbuję uzyskać dostęp do prywatnej metody obiektu crit: >>> crit.private_method() Traceback (most recent call last): File "", line 1, in

243

244

Rozdział 8. Obiekty programowe. Program Opiekun zwierzaka crit.private_method() AttributeError: 'Critter' object has no attribute 'private_method'

Ta próba powoduje zgłoszenie dobrze Ci znanego wyjątku AttributeError. Python stwierdza, że obiekt crit nie zawiera metody o podanej nazwie. Python ukrywa metodę przez zastosowanie takiej samej, specjalnej konwencji nazewniczej. Jeśli spróbuję ponownie, dodając dwa początkowe znaki podkreślenia do nazwy metody, zderzę się z tym samym komunikatem o błędzie: >>> crit.__private_method() Traceback (most recent call last): File "", line 1, in crit.__private_method() AttributeError: 'Critter' object has no attribute '__private_method'

Jednak dostęp do metod prywatnych z dowolnego miejsca w programie jest, podobnie jak w przypadku atrybutów prywatnych, technicznie możliwy. Udowadnia to końcowa część mojej sesji interaktywnej: >>> crit._Critter__private_method() To jest metoda prywatna.

Lecz jak już pewnie wiesz, klient nigdy nie powinien próbować bezpośredniego dostępu do prywatnych metod obiektu.

Wskazówka Możesz utworzyć prywatną metodę statyczną, rozpoczynając nazwę metody dwoma znakami podkreślenia.

Respektowanie prywatności obiektu W głównej części programu zachowuję się jak należy i nie próbuję dobierać się do prywatnych atrybutów czy metod. Tworzę natomiast obiekt i wywołuję jego dwie metody publiczne: # część główna crit = Critter(name = "Reksio", mood = "szczęśliwy") crit.talk() crit.public_method() input("\n\nAby zakończyć program, naciśnij klawisz Enter.")

Metoda __init__() obiektu klasy Critter, która jest automatycznie wywoływana natychmiast po utworzeniu obiektu, oznajmia światu, że urodził się nowy zwierzak. Metoda talk() obiektu crit informuje nas o samopoczuciu zwierzaka. Metoda public_method() obiektu crit wyświetla łańcuch "To jest metoda publiczna.", a następnie wywołuje metodę prywatną obiektu crit, która wypisuje łańcuch "To jest metoda prywatna.". Wreszcie program kończy działanie.

Kontrolowanie dostępu do atrybutów

Kiedy należy stosować prywatność? Czy więc teraz, gdy wiesz, jak stosować prywatność, powinieneś każdy atrybut w każdej klasie definiować jako prywatny, aby chronić go przed złym światem zewnętrznym? No nie. Prywatność jest podobna do dobrej przyprawy — używana z umiarem może bardzo poprawić jakość Twojego kodu. Jeśli nie chcesz, aby klient wywoływał jakąś metodę, zdefiniuj ją jako prywatną. Jeśli krytyczne znaczenie ma to, aby klient nigdy nie korzystał z bezpośredniego dostępu do pewnego atrybutu, możesz uczynić go prywatnym. Rozpowszechniona wśród wielu programistów Pythona filozofia polega na zaufaniu, że klienty będą korzystać z metod obiektu i nie będą bezpośrednio zmieniać jego atrybutów.

Wskazówka Gdy piszesz klasę:  twórz metody, aby zmniejszyć potrzebę korzystania z bezpośredniego dostępu do atrybutów obiektu przez klienty;  stosuj prywatność do tych atrybutów i metod, których rola w funkcjonowaniu obiektów jest czysto wewnętrzna. Kiedy używasz obiektu:  minimalizuj bezpośrednie odczytywanie atrybutów obiektu;  unikaj bezpośredniego zmieniania wartości atrybutów obiektu;  nigdy nie staraj się uzyskać bezpośredniego dostępu do prywatnych atrybutów i metod obiektu.

Kontrolowanie dostępu do atrybutów Czasem zamiast odmawiać dostępu do atrybutu, możesz chcieć go tylko ograniczyć. Możesz na przykład chcieć, aby kod klienta mógł odczytywać wartość jakiegoś atrybutu, ale nie mógł jej zmieniać. Python udostępnia kilka narzędzi pomagających zrealizować tego rodzaju wymaganie, a wśród nich właściwości. Umożliwiają one precyzyjne zarządzanie dostępem do atrybutu i zmianami jego wartości.

Prezentacja programu Zwierzak z właściwością Program Zwierzak z właściwością pozwala kodowi klienta na odczytywanie wartości atrybutu obiektu klasy Critter, który odwołuje się do nazwy zwierzaka, lecz nakłada ograniczenia, kiedy kod klienta usiłuje zmienić wartość, do której odwołuje się atrybut. Jeśli kod klienta próbuje przypisać pusty łańcuch, program wyświetla odpowiedni komunikat i nie dopuszcza do zmiany. Na rysunku 8.9 pokazano efekt działania tego programu.

245

246

Rozdział 8. Obiekty programowe. Program Opiekun zwierzaka

Rysunek 8.9. Właściwość kontroluje dostęp do atrybutu obiektu klasy Critter dotyczącego jego nazwy

Omówię kod tego programu, dzieląc go na fragmenty, ale jego całość możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 8.; nazwa pliku to zwierzak_z_wlasciwoscia.py.

Tworzenie właściwości Jednym ze sposobów kontroli dostępu do prywatnego atrybutu jest utworzenie właściwości — obiektu z metodami, które umożliwiają pośredni dostęp do atrybutów i często nakładają pewien rodzaj ograniczenia na ten dostęp. Po napisaniu konstruktora klasy Critter tworzę właściwość o nazwie name, aby umożliwić pośredni dostęp do atrybutu prywatnego __name: # Zwierzak z właściwością # Demonstruje właściwości class Critter(object): """Wirtualny pupil""" def __init__(self, name): print("Urodził się nowy zwierzak!") self.__name = name @property def name(self): return self.__name

Tworzę tę właściwość poprzez napisanie metody zwracającej wartość, do której chcę zapewnić pośredni dostęp (w tym przypadku wartość prywatnego atrybutu __name), oraz poprzedzam definicję metody dekoratorem @property. Właściwość ma taką samą nazwę jak metoda — w tym przypadku name. Teraz mogę wykorzystać właściwość name

Kontrolowanie dostępu do atrybutów

dowolnego obiektu klasy Critter do pobrania wartości jego prywatnego atrybutu __name wewnątrz lub na zewnątrz definicji klasy, przy użyciu dobrze Ci znanej notacji z kropką. (Przykłady wykorzystania tej własności poznasz w następnym punkcie, „Dostęp do właściwości”). Aby utworzyć swoją własną właściwość, napisz metodę zwracającą wartość, do której chcesz zapewnić pośredni dostęp, i poprzedź jej definicję dekoratorem @property. Właściwość będzie mieć taką samą nazwę jak metoda. Jeśli umożliwiasz dostęp do atrybutu prywatnego, powinieneś zastosować konwencję, która polega na nadaniu właściwości nazwy utworzonej z nazwy tego atrybutu przez pominięcie początkowych znaków podkreślenia, tak jak to zrobiłem w swoim programie. Tworząc właściwość, możesz zapewnić dostęp w trybie odczytu do prywatnego atrybutu. Właściwość może jednak zapewnić dostęp do zapisu, a nawet może nałożyć pewne ograniczenia na ten dostęp. Umożliwiam dostęp w trybie zapisu, z pewnymi ograniczeniami, do prywatnego atrybutu __name poprzez właściwość name: @name.setter def name(self, new_name): if new_name == "": print("Pusty łańcuch znaków nie może być imieniem zwierzaka.") else: self.__name = new_name print("Zmiana imienia się powiodła.")

Rozpoczynam ten kod od dekoratora @name.setter. Poprzez użycie atrybutu setter właściwości name sygnalizuję, że następująca po dekoratorze definicja metody określi sposób ustawiania wartości tej właściwości. Swój własny dekorator służący do ustawiania wartości właściwości możesz utworzyć, naśladując mój przykład: rozpocznij od symbolu @ i wpisz po nim nazwę właściwości, kropkę (.) i słowo setter. Po dekoratorze definiuję metodę o nazwie name, która jest wywoływana, gdy kod klienta próbuje ustawić nową wartość przy użyciu właściwości. Łatwo zauważysz, że ta metoda została nazwana name, tak samo jak sama właściwość; tak być musi. Kiedy się ustala metodę typu setter w ten sposób, musi ona mieć taką samą nazwę jak odpowiadająca jej właściwość. W tej metodzie parametr new_name otrzymuje wartość reprezentującą nowe imię zwierzaka. Jeśli ta wartość okaże się pustym łańcuchem znaków, atrybut __name pozostaje niezmieniony i wyświetlany jest komunikat mówiący o tym, że próba zmiany imienia się nie powiodła. W przeciwnym wypadku metoda ustawia wartość prywatnego atrybutu __name na new_name i wyświetla komunikat stwierdzający, że zmiana imienia się udała. Więc kiedy będziesz tworzyć metodę do ustawiania wartości właściwości, musisz pamiętać, że jej definicja musi zawierać parametr przeznaczony do przyjęcia tej nowej wartości, tak jak w tym przykładzie.

247

248

Rozdział 8. Obiekty programowe. Program Opiekun zwierzaka

Dostęp do właściwości Dzięki utworzeniu właściwości name mogę pobrać imię zwierzaka, używając notacji z kropką. Demonstruje to kolejna część programu: def talk(self): print("\nCześć! Jestem", self.name) # część główna crit = Critter("Reksio") crit.talk()

Wyrażenie self.name wykorzystuje dostęp do właściwości name i wywołuje pośrednio metodę, która zwraca wartość atrybutu __name. W tym akurat przypadku jest nią łańcuch "Reksio". Ale właściwość name obiektu mogę wykorzystywać nie tylko wewnątrz definicji jego klasy, lecz mogę jej używać także poza tą definicją, co też robię w następnym fragmencie kodu: print("\nImię mojego zwierzaka to:", end= " ") print(crit.name)

Chociaż ten kod znajduje się na zewnątrz klasy Critter, dzieją się zasadniczo te same rzeczy — wyrażenie crit.name realizuje dostęp do właściwości name obiektu klasy Critter i pośrednio wywołuje metodę, która zwraca wartość __name. Także w tym przypadku jest nią łańcuch "Reksio". Następnie próbuję zmienić imię zwierzaka: print("\nPróbuję zmienić imię mojego zwierzaka na Pucek...") crit.name = "Pucek"

Instrukcja przypisania crit.name = "Pucek" wykorzystuje dostęp do właściwości name obiektu i pośrednio wywołuje metodę, która się stara ustawić wartość atrybutu __name. W tym przypadku do parametru new_name tej metody zostaje przekazana referencja do łańcucha "Pucek", a ponieważ "Pucek" nie jest pustym łańcuchem, atrybut __name obiektu przyjmuje wartość "Pucek" i zostaje wyświetlony komunikat "Zmiana imienia się powiodła.". Następnie wyświetlam imię zwierzaka, wykorzystując właściwość name: print("\nImię mojego zwierzaka to:", end= " ") print(crit.name)

Program wyświetla komunikat Imię mojego zwierzaka to: Pucek. Po czym próbuję zmienić imię zwierzaka na pusty łańcuch znaków: print("\nPróbuję zmienić imię mojego zwierzaka na pusty łańcuch znaków...") crit.name = ""

Tak jak poprzednio przypisanie wykorzystuje dostęp do właściwości name obiektu i pośrednio wywołuje metodę, która próbuje ustawić wartość atrybutu __name. W tym przypadku do metody poprzez parametr new_name jest przekazywana referencja do pustego łańcucha znaków. W efekcie metoda wyświetla komunikat Pusty łańcuch znaków nie może być imieniem zwierzaka i atrybut __name obiektu pozostaje niezmieniony.

Powrót do programu Opiekun zwierzaka

Na koniec, aby udowodnić, że imię zwierzaka nie zostało zastąpione pustym łańcuchem znaków, wyświetlam je po raz ostatni: print("Imię mojego zwierzaka to:", end= " ") print(crit.name) input("\n\nAby zakończyć program, naciśnij klawisz Enter.")

Powyższy kod wyświetla komunikat wskazujący, że zwierzak ma nadal na imię Pucek.

Powrót do programu Opiekun zwierzaka Finalny program Opiekun zwierzaka łączy w sobie części klas, które poznawałeś w trakcie lektury całego tego rozdziału. Zawiera on również system menu, którego już używałeś, a który pozwala użytkownikowi na interakcję z jego własnym zwierzakiem. Omówię kod tego programu, dzieląc go na fragmenty, ale jego całość możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 8.; nazwa pliku to opiekun_zwierzaka.py.

Klasa Critter Klasa Critter stanowi projekt obiektu, który reprezentuje zwierzaka należącego do użytkownika. Klasa ta nie jest skomplikowana, a większość jej zawartości powinna Ci się wydawać znajoma. Jest to jednak dość długi kawałek kodu, więc zmaganie się z nim fragment po fragmencie jest sensownym podejściem.

Konstruktor Konstruktor klasy inicjalizuje trzy publiczne atrybuty obiektu klasy Critter: name (imię), hunger (głód) i boredom (nuda). Zauważ, że zarówno hunger, jak i boredom mają wartość domyślną 0, co pozwala zwierzakowi być na początku w bardzo dobrym nastroju. # Opiekun zwierzaka # Wirtualny pupil, którym należy się opiekować class Critter(object): """Wirtualny pupil""" def __init__(self, name, hunger = 0, boredom = 0): self.name = name self.hunger = hunger self.boredom = boredom

Jako programista Pythona przyjmuję w tej metodzie luźniejsze podejście i pozostawiam atrybuty z ich domyślnym publicznym statusem. Zamierzam utworzyć wszystkie metody, jakich (jak podejrzewam) klient będzie potrzebował, co powinno zachęcić klienta do interakcji z obiektem klasy Critter tylko za pośrednictwem tych metod.

249

250

Rozdział 8. Obiekty programowe. Program Opiekun zwierzaka

Metoda __pass_time() Metoda __pass_time() jest prywatną metodą, która zwiększa poziom głodu i nudy zwierzaka. Jest wywoływana na końcu każdej metody, w której zwierzak coś robi (je, bawi się lub mówi), aby zasymulować upływ czasu. Zdefiniowałem tę metodę jako prywatną, ponieważ powinna być wywoływana tylko przez inną metodę klasy. Zgodnie z moim założeniem czas mija dla zwierzaka tylko wtedy, gdy ten coś robi (na przykład je, bawi się lub mówi). def __pass_time(self): self.hunger += 1 self.boredom += 1

Właściwość mood Właściwość mood reprezentuje nastrój zwierzaka. Przedstawiona niżej metoda oblicza poziom tego nastroju. Sumuje ona wartości atrybutów hunger i boredom obiektu klasy Critter oraz na podstawie tej sumy zwraca łańcuch reprezentujący nastrój — "szczęśliwy", "zadowolony", "podenerwowany" lub "wściekły". We właściwości mood interesujące jest to, że nie umożliwia ona dostępu do atrybutu prywatnego. Jest tak, ponieważ łańcuch znaków, który reprezentuje nastrój zwierzaka nie jest przechowywany jako część obiektu klasy Critter, lecz jest tworzony na bieżąco. Właściwość mood udostępnia po prostu łańcuch zwracany przez metodę. @property def mood(self): unhappiness = self.hunger + self.boredom if unhappiness < 5: m = "szczęśliwy" elif 5 10: v = 10 else: v = None return v

Metoda ta zwraca liczbę z zakresu od 1 do 10, która reprezentuje wartość karty w blackjacku. Pierwsza część obliczenia jest wykonywana w wyrażeniu BJ_Card.RANKS.index(self.rank) + 1. Wyrażenie to pobiera wartość atrybutu rank obiektu (powiedzmy "6") i wyznacza odpowiadający jej numer indeksu na liście BJ_Card.RANKS przy użyciu metody listy o nazwie index() (w przypadku wartości atrybutu równej "6" będzie to liczba 5). Na koniec do wyniku zostaje dodana liczba 1, ponieważ komputer rozpoczyna indeksowanie od 0 (to sprawia, że wartość punktowa obliczona dla karty o randze "6" jest poprawna i wynosi 6). Ponieważ jednak dla atrybutów rank o wartościach "J", "Q" i "K" otrzymujemy liczby większe niż 10, wartość atrybutu value, która jest większa od 10, jest ustawiana na 10. Jeśli wartość atrybutu face_up obiektu jest równa False, cały ten proces zostaje ominięty i jest zwracana wartość None.

Klasa BJ_Deck Klasa BJ_Deck jest wykorzystywana do tworzenia talii kart blackjacka. Klasa jest prawie taka sama jak jej klasa bazowa, karty.Deck. Jedyna różnica sprowadza się do przesłonięcia metody populate() klasy karty.Deck w taki sposób, aby nowy obiekt klasy BJ_Deck otrzymał pełną listę obiektów klasy BJ_Card: class BJ_Deck(karty.Deck):

281

282

Rozdział 9. Programowanie obiektowe. Gra Blackjack """ Talia kart do blackjacka. """ def populate(self): for suit in BJ_Card.SUITS: for rank in BJ_Card.RANKS: self.cards.append(BJ_Card(rank, suit))

Klasa BJ_Hand Klasa BJ_Hand oparta na klasie karty.Hand jest wykorzystywana do tworzenia obiektów reprezentujących układy kart blackjacka zwane rękami. Przesłaniam konstruktor klasy cards.Hand i dodaję atrybut name, który ma reprezentować nazwę właściciela ręki: class BJ_Hand(karty.Hand): """ Ręka w blackjacku. """ def __init__(self, name): super(BJ_Hand, self).__init__() self.name = name

Następnie przesłaniam odziedziczoną metodę __str__(), aby wyświetlać sumaryczną wartość punktową ręki: def __str__(self): rep = self.name + ":\t" + super(BJ_Hand, self).__str__() if self.total: rep += "(" + str(self.total) + ")" return rep

Tworzę konkatenację atrybutu name obiektu z łańcuchem znaków zwróconym przez metodę karty.Hand__str__() wywołaną na rzecz tego obiektu. Następnie, jeśli wartość właściwości total obiektu jest różna od None, dołączam do tworzonej konkatenacji łańcuchową reprezentację tejże wartości. Na koniec zwracam tak utworzony łańcuch znaków. Następnie tworzę właściwość o nazwie total, która reprezentuje sumaryczną wartość punktową ręki kart blackjacka. Jeśli ręka blackjacka zawiera zakrytą kartę, wartość jej właściwości total wynosi None. W przeciwnym razie wartość ta zostaje obliczona poprzez zsumowanie wartości punktowych wszystkich kart ręki. @property def total(self): # jeśli karta w ręce ma wartość None, to i wartość sumy wynosi None for card in self.cards: if not card.value: return None # zsumuj wartości kart, traktuj każdego asa jako 1 t = 0 for card in self.cards: t += card.value # ustal, czy ręka zawiera asa contains_ace = False

Powrót do gry Blackjack for card in self.cards: if card.value == BJ_Card.ACE_VALUE: contains_ace = True # jeśli ręka zawiera asa, a suma jest wystarczająco niska, # potraktuj asa jako 11 if contains_ace and t 21

Zwróć uwagę, że w tej metodzie zwracam bezpośrednio wynik warunku self.total > 21 zamiast przypisania go do zmiennej, którą następnie miałbym zwrócić. Możesz tworzyć tego rodzaju instrukcje return z dowolnym warunkiem (a właściwie wyrażeniem) i często to daje w efekcie bardziej elegancką metodę. Ten rodzaj metody, który zwraca albo True, albo False, występuje dość często. Często jest używany (tak jak w tym przypadku) do reprezentowania dwóch możliwych stanów obiektu, takich jak na przykład „włączony” lub „wyłączony”. Metody tego typu mają prawie zawsze nazwę (o ile używamy angielskich nazw metod), która zaczyna się od słowa „is” (jest), jak przykładowo is_on().

Klasa BJ_Player Klasa BJ_Player, która jest pochodną klasy BJ_Hand, jest używana do reprezentowania graczy w blackjacku: class BJ_Player(BJ_Hand): """ Gracz w blackjacku. """ def is_hitting(self): response = gry.ask_yes_no("\n" + self.name + ", chcesz dobrać kartę? (T/N): ") return response == "t" def bust(self): print(self.name, "ma furę.") self.lose()

283

284

Rozdział 9. Programowanie obiektowe. Gra Blackjack def lose(self): print(self.name, "przegrywa.") def win(self): print(self.name, "wygrywa.") def push(self): print(self.name, "remisuje.")

Pierwsza metoda, is_hitting(), zwraca wartość True, jeśli gracz chce dobrać jeszcze jedną kartę, i zwraca wartość False w przeciwnym wypadku. Metoda bust() oznajmia, że gracz ma furę, i wywołuje metodę lose() obiektu. Metoda lose() ogłasza przegraną gracza, metoda win() — jego zwycięstwo, a metoda push() ogłasza remis. Metody bust(), lose(), win() oraz push() są tak proste, że mógłbyś się zastanawiać, dlaczego istnieją. Umieściłem je w klasie, ponieważ tworzą wspaniałą strukturę szkieletową do obsługi bardziej skomplikowanych kwestii, które powstają, kiedy gracze mają możliwość stawiania pieniędzy (a będą ją mieć, gdy wykonasz jedno z zadań zamieszczonych na końcu rozdziału).

Klasa BJ_Dealer Klasa BJ_Dealer, która jest klasą pochodną klasy BJ_Hand, jest wykorzystywana do reprezentowania rozdającego karty w blackjacku: class BJ_Dealer(BJ_Hand): """ Rozdający w blackjacku. """ def is_hitting(self): return self.total < 17 def bust(self): print(self.name, "ma furę.") def flip_first_card(self): first_card = self.cards[0] first_card.flip()

Pierwsza metoda, is_hitting(), rozstrzyga, czy rozdający bierze dodatkowe karty. Ponieważ rozdający musi dobrać kartę w sytuacji, gdy suma punktów jego ręki nie przekracza liczby 17, metoda zwraca wartość True, jeśli właściwość total obiektu ma mniejszą wartość niż 17; w przeciwnym wypadku zwraca wartość False. Metoda bust() ogłasza, że rozdający ma furę. Metoda flip_first_card() odwraca pierwszą kartę rozdającego.

Klasa BJ_Game Klasa BJ_Game jest używana do utworzenia pojedynczego obiektu, który reprezentuję grę w blackjacka. Klasa zawiera kod głównej pętli gry w swojej metodzie play(). Jednak mechanizm gry jest na tyle skomplikowany, że kilka jego elementów utworzyłem poza tą

Powrót do gry Blackjack

metodą, w tym metodę __additional_cards, która zajmuje się wydawaniem graczowi dodatkowych kart, oraz właściwość still_playing(), która zwraca listę wszystkich graczy nadal biorących udział w rozgrywce.

Metoda __init__() Konstruktor otrzymuje listę nazw i dla każdej nazwy tworzy obiekt gracza. Metoda tworzy także rozdającego i talię. class BJ_Game(object): """ Gra w blackjacka. """ def __init__(self, names): self.players = [] for name in names: player = BJ_Player(name) self.players.append(player) self.dealer = BJ_Dealer("Rozdający") self.deck = BJ_Deck() self.deck.populate() self.deck.shuffle()

Właściwość still_playing Właściwość still_playing zwraca listę graczy, którzy nadal biorą udział w grze (którzy w tej rundzie gry nie mieli fury): @property def still_playing(self): sp = [] for player in self.players: if not player.is_busted(): sp.append(player) return sp

Metoda __additional_cards() Metoda __additional_cards() rozdaje dodatkowe karty wszystkim graczom i rozdającemu. Metoda otrzymuje obiekt poprzez swój parametr player, który może być albo obiektem klasy BJ_Player, albo obiektem klasy BJ_Dealer. Metoda kontynuuje swoje działanie, dopóki metoda is_busted() obiektu zwraca wartość False, a jego metoda is_hitting() zwraca wartość True. Gdy metoda is_busted() obiektu zwróci wartość True, zostanie wywołana jego metoda bust(). def __additional_cards(self, player): while not player.is_busted() and player.is_hitting(): self.deck.deal([player]) print(player) if player.is_busted(): player.bust()

285

286

Rozdział 9. Programowanie obiektowe. Gra Blackjack

W przypadku dwóch wywołań metod działa polimorfizm. Wywołanie metody player.is_hitting() funkcjonuje jednakowo dobrze, czy parametr player odwołuje się do obiektu klasy BJ_Player, czy też do obiektu klasy BJ_Dealer. Metoda __additional_cards()

nie musi w ogóle wiedzieć, z jakim typem obiektu współpracuje. To samo dotyczy wiersza player.bust(). Ponieważ obie klasy, BJ_Player i BJ_Dealer, definiują swoje własne metody bust(), wykonanie tego wiersza daje w każdym przypadku pożądany wynik.

Metoda play() Metoda play() jest tą częścią programu, w której została zdefiniowana główna pętla gry, i charakteryzuje ją uderzające podobieństwo do pseudokodu, który wcześniej zaprezentowałem. def play(self): # rozdaj każdemu początkowe dwie karty self.deck.deal(self.players + [self.dealer], per_hand = 2) self.dealer.flip_first_card() # ukryj pierwszą kartę rozdającego for player in self.players: print(player) print(self.dealer) # rozdaj graczom dodatkowe karty for player in self.players: self.__additional_cards(player) self.dealer.flip_first_card()

# odsłoń pierwszą kartę rozdającego

if not self.still_playing: # ponieważ wszyscy gracze dostali furę, pokaż tylko rękę rozdającego print(self.dealer) else: # daj dodatkowe karty rozdającemu print(self.dealer) self.__additional_cards(self.dealer) if self.dealer.is_busted(): # wygrywa każdy, kto jeszcze pozostaje w grze for player in self.still_playing: player.win() else: # porównaj punkty każdego gracza pozostającego w grze z punktami rozdającego for player in self.still_playing: if player.total > self.dealer.total: player.win() elif player.total < self.dealer.total: player.lose() else: player.push() # usuń karty wszystkich graczy for player in self.players:

Podsumowanie player.clear() self.dealer.clear()

Wszystkim graczom i rozdającemu zostają rozdane po dwie początkowe karty. Pierwsza karta rozdającego jest odwrócona w celu ukrycia jej wartości. Następnie zostają wyświetlone wszystkie ręce. Potem każdy gracz dostaje dodatkowe karty, dopóki o nie prosi i dopóki nie uzbierał fury. Jeśli wszyscy gracze uzbierali furę, pierwsza karta rozdającego zostaje odwrócona, a jego ręka wyświetlona. W innym przypadku gra jest kontynuowana. Rozdający dobiera karty, dopóki suma punktów jego ręki jest mniejsza niż 17. Jeśli rozdający uzbiera furę, wszyscy gracze, którzy pozostali w grze, wygrywają. W przeciwnym razie wartość punktowa ręki każdego gracza pozostającego w grze jest porównywana z sumą punktów ręki rozdającego. Jeśli suma punktów uzbieranych przez gracza jest większa niż u rozdającego, gracz wygrywa. Jeśli jest mniejsza, gracz przegrywa. Jeśli obie sumy są równe, gracz remisuje.

Funkcja main() Funkcja main() wczytuje nazwy wszystkich graczy, wstawia je do listy i tworzy obiekt klasy BJ_Game, używając tej listy jako argumentu. Następnie funkcja wywołuje metodę play() obiektu i będzie to działanie kontynuować, dopóki gracze nie będą już chcieli grać. def main(): print("\t\tWitaj w grze 'Blackjack'!\n") names = [] number = gry.ask_number("Podaj liczbę graczy (1 - 7): ", low = 1, high = 8) for i in range(number): name = input("Wprowadź nazwę gracza: ") names.append(name) print() game = BJ_Game(names) again = None while again != "n": game.play() again = gry.ask_yes_no("\nCzy chcesz zagrać ponownie?: ") main() input("\n\nAby zakończyć program, naciśnij klawisz Enter.")

Podsumowanie W tym rozdziale zostałeś wprowadzony w świat programowania obiektowego. Zobaczyłeś, jak można przesyłać komunikaty między obiektami. Dowiedziałeś się, jak łączyć ze sobą obiekty w celu tworzenia bardziej złożonych obiektów. Zostałeś

287

288

Rozdział 9. Programowanie obiektowe. Gra Blackjack

wprowadzony w koncepcję dziedziczenia, procesu tworzenia nowych klas na podstawie już istniejących. Zobaczyłeś, jak można rozszerzyć klasę pochodną poprzez dodanie nowych metod. Zobaczyłeś również, jak można przesłonić odziedziczone metody. Dowiedziałeś się, jak pisać i importować własne moduły. Pokazałem Ci przykład, jak można stworzyć zarys swoich klas przed rozpoczęciem realizacji projektu. Na koniec zobaczyłeś, jak te wszystkie koncepcje występują razem przy tworzeniu kasynowej gry karcianej dla wielu graczy.

Sprawdź swoje umiejętności 1. Dodaj do gry Blackjack pewne bardzo potrzebne elementy kontroli błędów. Przed rozpoczęciem nowej rundy upewnij się, że talia zawiera wystarczającą liczbę kart. Jeśli tak nie jest, ponownie dołącz do niej komplet kart i potasuj jej zawartość. Znajdź też inne miejsca, w których mógłbyś dodać sprawdzanie błędów, i utwórz niezbędne zabezpieczenia. 2. Napisz jednokartową wersję gry w wojnę, w której każdy otrzymuje jedną kartę i gracz z najwyższą kartą wygrywa. 3. Ulepsz projekt Blackjack, umożliwiając graczom stawianie pieniędzy. Śledź budżety wszystkich graczy i usuwaj każdego gracza, któremu skończą się pieniądze. 4. Stwórz prostą grę przygodową wykorzystującą obiekty, w której gracz może podróżować pomiędzy różnymi, wzajemnie połączonymi miejscami.

10 Tworzenie interfejsów GUI. Gra Mad Lib D

o tej pory wszystkie programy, z którymi się spotkałeś, wykorzystywały do interakcji z użytkownikiem stary, zwykły tryb tekstowy. Lecz istnieją bardziej wyszukane metody prezentowania i przyjmowania informacji. Graficzny interfejs użytkownika (ang. graphical user interface — GUI) udostępnia wizualny sposób interakcji użytkownika z komputerem. Wszystkie najpopularniejsze systemy operacyjne do użytku domowego wykorzystują interfejs GUI, sprawiając, że interakcje użytkownika stają się prostsze i bardziej konsekwentne. W tym rozdziale dowiesz się, jak tworzyć interfejsy GUI. W szczególności nauczysz się:  posługiwać się zestawem narzędzi do tworzenia interfejsów GUI,  tworzyć i wypełniać ramki,  tworzyć i wykorzystywać przyciski,  tworzyć i wykorzystywać pola wejściowe i tekstowe,  tworzyć i wykorzystywać pola wyboru,  tworzyć i wykorzystywać przyciski opcji.

Wprowadzenie do programu Mad Lib Projekt rozdziału, program Mad Lib, prosi użytkownika o pomoc w tworzeniu opowiadania. Użytkownik podaje imię osoby, rzeczownik w liczbie mnogiej i czasownik. Użytkownik może również wybrać przymiotniki spośród kilku podanych oraz jedną nazwę części ciała. Program pobiera te wszystkie informacje i wykorzystuje je do stworzenia opowiadania. Program został przedstawiony na rysunkach od 10.1 do 10.3. Jak widzisz, program Mad Lib używa interfejsu GUI do interakcji z użytkownikiem.

290

Rozdział 10. Tworzenie interfejsów GUI. Gra Mad Lib

Rysunek 10.1. Przyjemny interfejs GUI czeka na kreatywność użytkownika

Rysunek 10.2. Użytkownik wprowadził wszystkie niezbędne informacje

Rysunek 10.3. Po kliknięciu przycisku Kliknij, aby wyświetlić opowiadanie, w polu tekstowym zostaje wyświetlone literackie arcydzieło

Przyjrzenie się interfejsowi GUI

Przyjrzenie się interfejsowi GUI Zanim opiszę, jak się programuje interfejs GUI, chciałbym zdefiniować wszystkie jego elementy, z którymi się zetkniesz w tym rozdziale. Rysunek 10.4 również pokazuje program Mad Lib, z tym że teraz różne elementy interfejsu zostały oznaczone nazwami.

Rysunek 10.4. Nauczysz się tworzyć te wszystkie elementy GUI

Aby tworzyć interfejs GUI w Pythonie, musisz skorzystać z zestawu narzędzi GUI (ang. GUI toolkit). Istnieje ich wiele do wyboru, lecz w tym rozdziale używam popularnego, niezależnego od platformy zestawu narzędzi Tkinter.

Wskazówka Jeśli używasz innego systemu operacyjnego niż Windows, będziesz być może potrzebował pobrać i zainstalować dodatkowe oprogramowanie, by móc korzystać z zestawu narzędzi Tkinter. Aby dowiedzieć się na ten temat więcej, odwiedź w serwisie internetowym Pythona stronę poświęconą interfejsowi Tkinter o adresie http://docs.python.org/3/library/tkinter.html.

Elementy GUI tworzy się poprzez konkretyzację obiektów klas z modułu tkinter, który jest częścią zestawu narzędzi Tkinter. W tabeli 10.1 opisuję wszystkie elementy GUI z rysunku 10.4 i podaję odpowiadające im klasy z modułu tkinter.

Wskazówka Nie ma potrzeby zapamiętywania tych wszystkich klas modułu tkinter. Chciałem Ci tylko zaprezentować ogólny przegląd klas, o których się będziesz uczyć w tym rozdziale.

291

292

Rozdział 10. Tworzenie interfejsów GUI. Gra Mad Lib Tabela 10.1. Wybrane elementy interfejsu GUI

Element

tkinter

Opis klasy

Ramka

Frame

Stanowi pojemnik na inne elementy interfejsu GUI.

Etykieta

Label

Wyświetla nieedytowalny tekst lub ikony.

Przycisk

Button

Wykonuje pewne działanie po uaktywnieniu go przez użytkownika.

Pole wejściowe

Entry

Przyjmuje i wyświetla jeden wiersz tekstu.

Pole tekstowe

Text

Przyjmuje i wyświetla tekst złożony z wielu wierszy.

Pole wyboru

Checkbutton

Pozwala użytkownikowi na wybranie lub niewybranie opcji.

Przycisk opcji

Radiobutton

Występując w grupie, pozwala użytkownikowi na wybranie jednej z wielu opcji.

Programowanie sterowane zdarzeniami Programy obsługujące GUI są tradycyjnie sterowane zdarzeniami (ang. event-driven), co oznacza, że reagują na różne działania niezależnie od porządku, w jakim te działania występują. Programowanie sterowane zdarzeniami to nieco odmienny sposób myślenia o tworzeniu kodu. Ale nie martw się, ponieważ jeśli kiedykolwiek przedtem korzystałeś z interfejsu GUI (takiego jak przeglądarka internetowa), to już pracowałeś w systemie sterowanym zdarzeniami. Aby lepiej zrozumieć programowanie sterowane zdarzeniami, pomyśl o finalnym projekcie z tego rozdziału, Mad Lib. Jeśli miałbyś napisać podobny program przy swoich aktualnych umiejętnościach kodowania w Pythonie, prawdopodobnie zadałbyś użytkownikowi serię pytań przy użyciu funkcji input(). Zapytałbyś o imię osoby, następnie poprosiłbyś o wprowadzenie rzeczownika w liczbie mnogiej, potem czasownika itd. W rezultacie użytkownik musiałby podać każdą informacje w ustalonej kolejności. Lecz gdybyś napisał program sterowany zdarzeniami, który, powiedzmy, wykorzystywałby interfejs GUI, użytkownik mógłby wprowadzać informacje w dowolnym porządku. Również wybór czasu, w którym program rzeczywiście generuje opowiadanie, zależałby od decyzji użytkownika. Gdy piszesz program sterowany zdarzeniami, wiążesz (kojarzysz) zdarzenia (rzeczy dotyczące obiektów programu, które mogą się wydarzyć) z procedurami obsługi zdarzeń (ang. event handlers; kod który jest wykonywany, gdy wystąpi określone zdarzenie). Szukając konkretnego przykładu, pomyśl ponownie o projekcie Mad Lib. Kiedy użytkownik klika przycisk Kliknij, aby wyświetlić opowiadanie (zdarzenie), program wywołuje metodę, która wyświetla opowiadanie (procedurę obsługi zdarzenia). Aby to mogło mieć miejsce, muszę skojarzyć kliknięcie przycisku z metodą wyświetlającą opowiadanie (wiążę te dwie rzeczy ze sobą). Definiując wszystkie swoje obiekty, zdarzenia i procedury obsługi zdarzeń, ustalasz sposób działania swojego programu. Następnie uruchamiasz program poprzez wejście w pętlę obsługi zdarzeń, w której program oczekuje na wystąpienie opisanych przez

Zastosowanie okna głównego

Ciebie zdarzeń. Gdy któreś ze zdarzeń faktycznie wystąpią, program je obsłuży w zaplanowany przez Ciebie sposób. Nie martw się, jeśli ten nieco inny sposób podejścia do programowania, nie jest jeszcze dla Ciebie całkowicie zrozumiały. Po zapoznaniu się z kilkoma działającymi przykładami będziesz wiedział, jak obmyślać swoje własne programy sterowane zdarzeniami.

Zastosowanie okna głównego Podstawą programu wykorzystującego interfejs GUI jest jego okno główne (ang. root window), w oparciu o które możesz dodawać pozostałe elementy GUI. Jeśli wyobrazisz sobie GUI jako drzewo, to okno główne będzie jego korzeniem. Twoje drzewo może się rozgałęziać we wszystkich kierunkach, lecz każda jego część bezpośrednio lub pośrednio jest zakotwiczona w korzeniu.

Prezentacja programu Prosty interfejs GUI Program Prosty interfejs GUI tworzy chyba najprostszy możliwy interfejs GUI — pojedyncze okno. Rysunek 10.5 pokazuje wynik uruchomienia tego programu.

Rysunek 10.5. Program tworzy tylko samotne, puste okno. Cóż, od czegoś musisz zacząć

Pułapka Uruchomienie programu wykorzystującego Tkinter bezpośrednio z IDLE spowoduje zamrożenie albo Twojego programu, albo IDLE. Najprostszym rozwiązaniem jest uruchomienie używającego Tkintera programu w sposób bezpośredni. W systemie Windows możesz to zrobić poprzez dwukrotne kliknięcie ikony programu.

Oprócz okna przedstawionego na rysunku 10.5 program Prosty interfejs GUI może wygenerować jeszcze jedno okno (zależy to od Twojego systemu operacyjnego) — znajome okno konsoli przedstawione na rysunku 10.6.

293

294

Rozdział 10. Tworzenie interfejsów GUI. Gra Mad Lib

Rysunek 10.6. Program wykorzystujący GUI może także wygenerować okno konsoli

Sztuczka Chociaż możesz uruchomić program wykorzystujący Tkinter poprzez dwukrotne kliknięcie jego ikony, będziesz miał problem, jeśli w programie wystąpi błąd — okno konsoli zostanie zamknięte, zanim zdążysz przeczytać komunikat o błędzie. W systemie Windows możesz utworzyć plik wsadowy, który uruchamia Twój program i wstrzymuje swoje działanie po zakończeniu programu, sprawiając, że okno konsoli pozostaje otwarte, abyś mógł zobaczyć wszystkie komunikaty o błędach. Jeśli na przykład Twoim programem jest prosty_gui.py, wystarczy, że utworzysz plik wsadowy złożony z dwóch wierszy: prosty_gui.py pause

Następnie uruchom plik wsadowy, klikając dwukrotnie jego ikonę. Aby utworzyć plik wsadowy:  Otwórz edytor tekstu taki jak Notatnik (lecz nie Word ani Wordpad).  Wpisz swój tekst.  Zapisz plik z rozszerzeniem .bat (jak na przykład prosty_gui.bat) i upewnij się, że po .bat nie ma rozszerzenia .txt. Utworzyłem pliki wsadowe do wszystkich programów w tym rozdziale. Możesz je znaleźć na stronie internetowej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 10. razem z programami Pythona.

Chociaż mógłbyś pomyśleć, że to okno konsoli towarzyszące Twojemu skądinąd nienagannemu interfejsowi GUI psuje tylko doskonałe wrażenie, jakie on wywołuje, nie bądź taki skory do pozbycia się go. Okno konsoli może dostarczyć cennej informacji zwrotnej, jeśli (i kiedy) Twój korzystający z Tkintera program wygeneruje błędy. Pamiętaj także, aby nie zamykać okna konsoli, ponieważ to spowoduje równoczesne zamknięcie programu z interfejsem GUI.

Zastosowanie okna głównego

Sztuczka Kiedy Twoje oprogramowanie GUI działa już bez zarzutu, mógłbyś chcieć zapobiec pojawianiu się towarzyszącego mu okna konsoli. Na maszynie z systemem Windows najłatwiej tego dokonać poprzez zmianę rozszerzenia nazwy Twojego programu z .py na .pyw.

Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 10.; nazwa pliku to prosty_gui.py.

Import modułu tkinter W końcu nadszedł czas ubrudzić sobie ręce pisaniem jakiegoś kodu! Pierwszą moją czynnością w programie Prosty interfejs GUI jest zaimportowanie modułu tkinter: # Prosty interfejs GUI # Demonstruje tworzenie okna from tkinter import *

Powyższy kod importuje całą zawartość modułu tkinter bezpośrednio do zakresu globalnego programu. Zazwyczaj unika się robienia podobnych rzeczy; jednak kilka modułów, takich jak tkinter, zostało tak zaprojektowanych, że muszą być importowane w ten sposób. Zobaczysz, jak się to przydaje w następnym wierszu kodu.

Utworzenie okna głównego Aby utworzyć okno główne, konkretyzuję obiekt klasy Tk modułu tkinter: # utwórz okno główne root = Tk()

Zwróć uwagę, że nie musiałem poprzedzić nazwy klasy, Tk, przedrostkiem w postaci nazwy modułu, tkinter. Właściwie mogę teraz korzystać z bezpośredniego dostępu do dowolnej części modułu tkinter bez potrzeby używania nazwy modułu. Ponieważ większość programów wykorzystujących tkinter, zawiera wiele odwołań do klas i stałych zdefiniowanych w module, zaoszczędza to wiele pisania i sprawia, że kod jest łatwiejszy do czytania.

Pułapka W programie wykorzystującym Tkinter możesz utworzyć tylko jedno okno główne. Jeśli utworzysz ich więcej, niechybnie zamrozisz swój program, ponieważ utworzone okna główne będą walczyć o przejęcie sterowania.

295

296

Rozdział 10. Tworzenie interfejsów GUI. Gra Mad Lib

Modyfikowanie okna głównego Następnie modyfikuję okno główne, używając paru jego metod: # zmodyfikuj okno root.title("Prosty interfejs GUI") root.geometry("225x100")

Metoda title() ustawia tytuł okna głównego. Nie musisz nic robić oprócz przekazania, w postaci łańcucha znaków, tytułu, który chcesz wyświetlić. Ustawiłem tytuł tak, aby na pasku tytułu okna pojawił się tekst Prosty interfejs GUI. Metoda geometry() ustawia rozmiar okna głównego wyrażony w pikselach. Metoda pobiera łańcuch znaków (a nie liczby całkowite), który reprezentuje szerokość i wysokość okna rozdzielone znakiem "x". Ustawiam szerokość okna na 225, a jego wysokość na 100.

Wejście w pętlę zdarzeń okna głównego W końcu uruchamiam pętlę zdarzeń okna poprzez wywołanie metody mainloop() obiektu root: # uruchom pętlę zdarzeń root.mainloop()

W rezultacie okno pozostaje otwarte, gotowe do obsługi zdarzeń. Ponieważ nie zdefiniowałem żadnych zdarzeń, okno nie ma wiele pracy. Ale jest to pełnowartościowe okno, którego rozmiar można zmieniać, które można zminimalizować lub zamknąć. Możesz je przetestować poprzez dwukrotne kliknięcie ikony pliku wsadowego.

Używanie etykiet Elementy GUI są nazywane widżetami (ang. widgets = window gadgets). Zapewne najprostszym widżetem jest widżet Label, który reprezentuje nieedytowalny tekst lub ikony (albo obie te rzeczy na raz). Widżet Label stanowi etykietę do części interfejsu GUI. Jest często używany do opisywania innych widżetów. W przeciwieństwie do innych widżetów etykiety nie są interaktywne. Użytkownik nie może kliknąć etykiety (w porządku, użytkownik może to zrobić, lecz etykieta na to nie zareaguje). Mimo to etykiety mają duże znaczenie i użyjesz przynajmniej jednej za każdym razem, gdy będziesz tworzył interfejs GUI.

Prezentacja programu Metkownica Program Metkownica tworzy okno główne i dodaje do niego etykietę. Widżet Label po prostu oznajmia, że jest etykietą. Efekt uruchomienia programu przedstawiono na rysunku 10.7.

Zastosowanie okna głównego

Rysunek 10.7. Etykieta może dostarczyć informacji o interfejsie GUI

Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 10.; nazwa pliku to metkownica.py.

Rozpoczęcie programu Najpierw inicjuję program Metkownica poprzez import modułu tkinter i utworzenie okna głównego: # Metkownica # Demonstruje etykietę from tkinter import * # utwórz okno główne root = Tk() root.title("Metkownica") root.geometry("200x50")

Utworzenie ramki Widżet Frame (ramka) może przechowywać inne widżety (takie jak widżety Label). Ramka przypomina korkowe tworzywo tablicy ogłoszeń; możesz jej używać jako podłoża, na którym można umieszczać inne rzeczy. Więc tworzę teraz nową ramkę: # utwórz w oknie ramkę jako pojemnik na inne widżety app = Frame(root)

Za każdym razem, gdy tworzysz nowy widżet musisz przekazać do jego konstruktora obiekt nadrzędny (ang. master; obiekt, który będzie zawierał ten widżet). W tym przypadku przekazuję obiekt root do konstruktora klasy Frame. W rezultacie nowa ramka zostaje umieszczona wewnątrz okna głównego. Następnie wywołuję metodę grid() nowego obiektu: app.grid()

Metodę grid() posiadają wszystkie widżety. Jest ona związana z menedżerem układów (ang. layout manager), który umożliwia rozplanowanie położenia widżetów. Aby nie komplikować sprawy, odkładam omówienie menedżerów układów na nieco dalszą część tego rozdziału.

297

298

Rozdział 10. Tworzenie interfejsów GUI. Gra Mad Lib

Utworzenie etykiety Tworzę widżet Label poprzez konkretyzację obiektu klasy Label: # utwórz w ramce etykietę lbl = Label(app, text = "Jestem etykietą!")

Dzięki przekazaniu obiektu app do konstruktora obiektu klasy Label sprawiam, że ramka, do której odwołuje się zmienna app, staje się obiektem nadrzędnym widżetu Label. W rezultacie etykieta zostaje umieszczona w ramce. Widżety mają opcje, które można ustawiać. Wiele z tych opcji wpływa na wygląd widżetu. Przekazując łańcuch "Jestem etykietą!" do parametru text, sprawiam, że ten łańcuch staje się wartością opcji text widżetu. W rezultacie, kiedy etykieta zostaje wyświetlona, pojawia się tekst Jestem etykietą!. Następnie wywołuję metodę grid() obiektu: lbl.grid()

To zapewnia widoczność etykiety.

Wejście w pętlę zdarzeń okna głównego Ostatnią, lecz nie mniej ważną czynnością, jaką wykonuję, jest wywołanie pętli zdarzeń okna w celu uruchomienia interfejsu GUI: # uruchom pętlę zdarzeń okna root.mainloop()

Używanie przycisków Widżet Button może zostać uaktywniony przez użytkownika w celu wykonanie pewnej czynności. Ponieważ już wiesz, jak tworzyć etykiety, nauczenie się tworzenia przycisków będzie dość proste.

Prezentacja programu Leniwe przyciski W programie Leniwe przyciski tworzę kilka przycisków, które nic nie robią po ich uaktywnieniu. To jak zainstalowanie nowego sprzętu oświetleniowego bez podłączenia do niego przewodów. Sprzęt został zamontowany, lecz jeszcze nie funkcjonuje. Na rysunku 10.8 przedstawiono ten program. Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 10.; nazwa pliku to leniwe_przyciski.py.

Używanie przycisków

Rysunek 10.8. Możesz klikać te leniwe przyciski, ile tylko chcesz; i tak nic nie zrobią

Rozpoczęcie programu Najpierw inicjuję program poprzez zaimportowanie modułu tkinter oraz okna głównego i ramki: # Leniwe przyciski # Demonstruje tworzenie przycisków from tkinter import * # utwórz okno główne root = Tk() root.title("Leniwe przyciski") root.geometry("200x85") # utwórz w oknie ramkę do pomieszczenia innych widżetów app = Frame(root) app.grid()

Utworzenie przycisków Widżet Button tworzy się poprzez konkretyzację obiektu klasy Button. To właśnie zrobiłem w kolejnych wierszach kodu: # utwórz w ramce przycisk bttn1 = Button(app, text = "Nic nie robię!") bttn1.grid()

W powyższych wierszach kodu zostaje utworzony nowy przycisk z tekstem Nic nie robię!. Obiektem nadrzędnym przycisku jest ramka, którą utworzyłem

wcześniej, co oznacza, że przycisk zostaje w niej umieszczony. Moduł tkinter oferuje elastyczność, gdy chodzi o tworzenie, definiowanie i zmienianie widżetów. Możesz utworzyć widżet i ustawić wszystkie jego opcje w jednym wierszu kodu (tak jak ja to zrobiłem), ale możesz też ustawić lub zmienić opcje widżetu później, po jego utworzeniu. Pokażę Ci, co mam na myśli, na przykładzie następnego przycisku. Najpierw tworzę nowy przycisk: # utwórz w ramce drugi przycisk bttn2 = Button(app) bttn2.grid()

299

300

Rozdział 10. Tworzenie interfejsów GUI. Gra Mad Lib

Zauważ, że jedyną wartością, jaką przekazuję do konstruktora obiektu, jest app, obiekt nadrzędny przycisku. Więc wszystko, co zrobiłem, to dodanie do ramki pustego przycisku. Mogę jednak to naprawić. Mogę zmodyfikować widżet po jego utworzeniu, wykorzystując metodę configure() obiektu: bttn2.configure(text = "Ja również!")

W tym wierszu ustawiam opcję text przycisku poprzez przypisanie łańcucha "Ja również!", co skutkuje umieszczeniem na przycisku tekstu Ja również!. Możesz wykorzystać metodę configure() widżetu do każdej jego opcji (i do każdego typu widżetu). Możesz nawet użyć tej metody do zmiany wartości opcji, którą już ustawiłeś. Następnie tworzę trzeci przycisk: # utwórz w ramce trzeci przycisk bttn3 = Button(app) bttn3.grid()

Potem ustawiam opcję text przycisku przy użyciu innego interfejsu: bttn3["text"]= "To samo mnie dotyczy!"

Uzyskuję dostęp do opcji text poprzez interfejs przypominający słownik. Ustawiam wartość opcji text w postaci łańcucha "To samo mnie dotyczy!", co powoduje umieszczenie tekstu To samo mnie dotyczy! na przycisku. Kiedy ustawiasz wartość opcji, stosując ten słownikowy typ dostępu, kluczem opcji jest jej nazwa w postaci łańcucha znaków.

Wejście w pętlę zdarzeń okna głównego Jak zawsze, wywołuję pętlę zdarzeń okna głównego w celu uruchomienia interfejsu GUI: # uruchom pętlę zdarzeń okna głównego root.mainloop()

Tworzenie interfejsu GUI przy użyciu klasy Jak już dowiedziałeś się z innych rozdziałów, zorganizowanie kodu w klasy może znacznie ułatwić Twoje życie programisty. Pisanie większych programów wykorzystujących GUI poprzez definiowanie swoich własnych klas często przynosi korzyści. Więc teraz pokażę Ci, jak napisać program korzystający z GUI poprzez zorganizowanie kodu za pomocą klasy.

Prezentacja programu Leniwe przyciski 2 Program Leniwe przyciski 2 jest prostym programem Leniwe przyciski napisanym na nowo przy użyciu klasy. Od strony użytkownika program wygląda dokładnie tak samo,

Tworzenie interfejsu GUI przy użyciu klasy

lecz za kulisami dokonałem pewnej restrukturyzacji. Na rysunku 10.9 pokazano tak dobrze Ci znany program w działaniu.

Rysunek 10.9. To jakieś déjà vu. Program wygląda tak samo jak jego poprzednik, mimo że „pod maską” dokonano znaczących zmian

Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 10.; nazwa pliku to leniwe_przyciski2.py.

Import modułu tkinter Mimo że w strukturze programu zaszły znaczące zmiany, importowanie modułu GUI wygląda wciąż tak samo: # Leniwe przyciski 2 # Demonstruje użycie klasy w programie wykorzystującym Tkinter from tkinter import *

Zdefiniowanie klasy Application Następnie tworzę nową klasę, Application, na bazie klasy Frame: class Application(Frame): """ Aplikacja oparta na GUI z trzema przyciskami. """

Zamiast konkretyzować obiekt klasy Frame, tworzę ostatecznie obiekt klasy Application mający pomieścić wszystkie przyciski. To działa, ponieważ obiekt klasy Application jest tylko wyspecjalizowanym typem obiektu klasy Frame.

Zdefiniowanie konstruktora Potem definiuję w klasie Application konstruktor: def __init__(self, master): """ Inicjalizuj ramkę. """ super(Application, self).__init__(master) self.grid() self.create_widgets()

301

302

Rozdział 10. Tworzenie interfejsów GUI. Gra Mad Lib

W pierwszej kolejności wywołuję konstruktor nadklasy. Przekazuję do niego obiekt nadrzędny obiektu klasy Application, żeby został prawidłowo ustawiony jako obiekt odgrywający tę rolę. Na koniec wywołuję metodę create_widgets() obiektu klasy Application, którą zdefiniuję w następnym kroku.

Zdefiniowanie metody służącej do utworzenia widżetów Definiuję metodę, która tworzy wszystkie trzy przyciski, create_widgets(): def create_widgets(self): """ Utwórz trzy przyciski, które nic nie robią. """ # utwórz pierwszy przycisk self.bttn1 = Button(self, text = "Nic nie robię!") self.bttn1.grid() # utwórz drugi przycisk self.bttn2 = Button(self) self.bttn2.grid() self.bttn2.configure(text = "Ja również!") # utwórz trzeci przycisk self.bttn3 = Button(self) self.bttn3.grid() self.bttn3["text"] = "To samo mnie dotyczy!"

Kod wygląda dość podobnie do kodu, który tworzy przyciski w pierwotnym programie Leniwe przyciski. Istotna różnica polega na tym, że bttn1, bttn2 i bttn3 są teraz atrybutami obiektu klasy Application. Inną ważną różnicą jest to, że używam zmiennej self do przekazania obiektu nadrzędnego przycisków, żeby ich obiektem nadrzędnym został wcześniej utworzony obiekt klasy Application.

Tworzenie obiektu klasy Application W głównej części kodu tworzę okno główne oraz nadaję mu tytuł i właściwe rozmiary: # część główna root = Tk() root.title("Leniwe przyciski 2") root.geometry("210x85")

Potem konkretyzuję obiekt klasy Application z oknem głównym w roli obiektu nadrzędnego. Konstruktor obiektu klasy Application wywołuje metodę create_widgets() tego obiektu. Tworzy ona wtedy trzy przyciski z obiektem klasy Application jako ich obiektem nadrzędnym. Na koniec wywołuję pętlę zdarzeń okna głównego w celu uruchomienia interfejsu GUI i podtrzymywania jego pracy: root.mainloop()

Wiązanie widżetów z procedurami obsługi zdarzeń

Wiązanie widżetów z procedurami obsługi zdarzeń Programy z interfejsem GUI poznane do tej pory przez Ciebie nie robią zbyt wiele. Przyczyna tego tkwi w braku kodu skojarzonego z uaktywnieniem zawartych w nich widżetów. Przypominam, że widżety są podobne do sprzętu oświetleniowego, który został zainstalowany, ale do którego nie podłączono przewodów z prądem. Teraz nadszedł czas, aby ten prąd popłynął; w przypadku programowania GUI, przyszła pora na napisanie procedur obsługi zdarzeń i powiązanie ich z samymi zdarzeniami.

Prezentacja programu Licznik kliknięć Program Licznik kliknięć zawiera przycisk, który coś robi — wyświetla liczbę wskazującą, ile razy został kliknięty przez użytkownika. W sensie technicznym aktualizowaniem licznika kliknięć i zmienianiem tekstu wyświetlanego na przycisku zajmuje się procedura obsługi zdarzeń przycisku. Program został zademonstrowany na rysunku 10.10.

Rysunek 10.10. Procedura obsługi zdarzeń przycisku aktualizuje liczbę wskazującą, ile razy przycisk został kliknięty

Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 10.; nazwa pliku to licznik_klikniec.py.

Rozpoczęcie programu W swoim pierwszym kroku tradycyjnie importuję moduł GUI: # Licznik kliknięć # Demonstruje powiązanie zdarzenia z procedurą obsługi zdarzeń from tkinter import *

Potem rozpoczynam definiowanie klasy Application: class Application(Frame): """ Aplikacja z GUI, która zlicza kliknięcia przycisku. """ def __init__(self, master): """ Inicjalizuj ramkę. """ super(Application, self).__init__(master) self.grid() self.bttn_clicks = 0 # liczba kliknięć przycisku self.create_widget()

303

304

Rozdział 10. Tworzenie interfejsów GUI. Gra Mad Lib

Większość tego kodu widziałeś już przedtem. Nowym jego elementem jest wiersz self.bttn_clicks = 0, który tworzy atrybut obiektu, który ma rejestrować liczbę kliknięć

przycisku przez użytkownika.

Dowiązanie procedury obsługi zdarzeń Przy użyciu metody create_widget() tworzę pojedynczy przycisk: def create_widget(self): """ Utwórz przycisk, który wyświetla liczbę kliknięć. """ self.bttn = Button(self) self.bttn["text"]= "Liczba kliknięć: 0" self.bttn["command"] = self.update_count self.bttn.grid()

Ustawiam jako wartość opcji command widżetu Button metodę update_count(). Dzięki temu, kiedy użytkownik kliknie przycisk, zostanie wywołana ta metoda. Z perspektywy technicznej to, co zrobiłem, jest powiązaniem zdarzenia (kliknięcia widżetu Button) z jego procedurą obsługi (metodą update_count()). W ogólności, aby powiązać uaktywnienie widgetu z procedurą obsługi zdarzeń, należy ustawić opcję command tego widżetu.

Utworzenie procedury obsługi zdarzeń Następnie piszę metodę update_count(), która obsługuje zdarzenie kliknięcia przycisku: def update_count(self): """ Zwiększ licznik kliknięć i wyświetl jego nową wartość. """ self.bttn_clicks += 1 self.bttn["text"] = "Liczba kliknięć: " + str(self.bttn_clicks)

Ta metoda zwiększa całkowitą liczbę kliknięć przycisku, a następnie zmienia tekst na przycisku tak, aby odzwierciedlał nową wartość tej liczby. To wszystko, czego potrzeba, aby przycisk zrobił coś pożytecznego (lub prawie pożytecznego).

Dokończenie programu Do tej pory główna część kodu powinna Ci się już wydawać całkiem znajoma: # część główna root = Tk() root.title("Licznik kliknięć") root.geometry("200x50") app = Application(root) root.mainloop()

Używanie widżetów Text i Entry oraz menedżera układu Grid

Tworzę okno główne i ustawiam jego tytuł i rozmiary. Potem konkretyzuję nowy obiekt klasy Application z oknem głównym jako obiektem nadrzędnym. Na końcu uruchamiam pętlę zdarzeń okna głównego, aby interfejs GUI ożył na ekranie komputera.

Używanie widżetów Text i Entry oraz menedżera układu Grid W programowaniu z wykorzystaniem GUI wystąpią przypadki, gdy będziesz chciał, aby użytkownik wprowadził jakiś tekst. Innym razem będziesz chciał wyświetlić tekst użytkownikowi. W obu tych sytuacjach będziesz mógł skorzystać z widżetów obsługujących tekst. Zapoznam Cię z dwoma ich rodzajami. Widżet Entry dobrze się nadaje do obsługi jednego wiersza tekstu, podczas gdy widżet Text wspaniale się spisuje przy wielowierszowych blokach tekstu. Możesz odczytywać zawartość widżetów obu tych typów, aby pobierać dane wprowadzone przez użytkownika. Możesz również wstawiać do nich tekst, aby dostarczać użytkownikowi informacji zwrotnych. Kiedy już wrzucisz do ramki pewną liczbę widżetów, będziesz potrzebować sposobu na ich zorganizowanie. Jak do tej pory, używałem menedżera układu Grid, lecz tylko w maksymalnie ograniczony sposób. Menedżer układu Grid oferuje Ci dużo więcej kontroli nad wyglądem Twojego interfejsu GUI. Menedżer pozwala Ci umieszczać widżety w określonych pozycjach dzięki traktowaniu ramki jako siatki.

Prezentacja programu Długowieczność Program Długowieczność ujawnia sekret zapewniający dożycie sędziwego wieku 100 lat, jeśli użytkownik wprowadzi tajne hasło (niezwykle bezpieczne hasło „sekret”). Użytkownik wprowadza hasło w polu wejściowym, a następnie klika przycisk Akceptuj. Jeśli hasło okazuje się prawidłowe, program wyświetla klucz do długowieczności w polu tekstowym. Program został zademonstrowany na rysunku 10.11 i 10.12.

Rysunek 10.11. Jeśli użytkownikowi nie uda się wprowadzić prawidłowego hasła, program grzecznie odmawia wyjawienia swojego sekretu

305

306

Rozdział 10. Tworzenie interfejsów GUI. Gra Mad Lib

Rysunek 10.12. Po podaniu poprawnego hasła program dzieli się swoją bezcenną wiedzą o sposobie na długie życie

Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 10.; nazwa pliku to dlugowiecznosc.py.

Rozpoczęcie programu Rozpoczynam program całkiem podobnie jak kilka ostatnich: # Długowieczność # Demonstruje widżety Text i Entry oraz menedżer układu Grid from tkinter import * class Application(Frame): """ Aplikacja z GUI, która może ujawnić sekret długowieczności. """ def __init__(self, master): """ Inicjalizuj ramkę. """ super(Application, self).__init__(master) self.grid() self.create_widgets()

Importuję moduł tkinter i rozpoczynam definiowanie klasy Application. W konstruktorze inicjalizuję nowy obiekt klasy Application, upewniam się, że obiekt będzie widoczny, oraz wywołuję jego metodę create_widgets().

Umiejscowienie widżetu za pomocą menedżera układu Grid Następnie uruchamiam metodę create_widgets() i tworzę etykietę, która zawiera instrukcję dla użytkownika: def create_widgets(self): """ Utwórz widżety typu Button, Text i Entry . """ # utwórz etykietę z instrukcją self.inst_lbl = Label(self, text = "Wprowadź hasło do sekretu długowieczności")

Używanie widżetów Text i Entry oraz menedżera układu Grid

Jak dotąd nic nowego. Lecz w kolejnym wierszu wykorzystuję menedżer układu Grid, aby dokładnie określić położenie tej etykiety: self.inst_lbl.grid(row = 0, column = 0, columnspan = 2, sticky = W)

Metoda grid() obiektu widżetu może pobierać wartości wielu różnych parametrów, lecz ja wykorzystuje tylko cztery z nich: row, column, columnspan i sticky. Parametry row i column przyjmują jako swoje wartości liczby całkowite i definiują miejsce ulokowania obiektu w obrębie jego nadrzędnego widżetu. W tym programie możesz sobie wyobrazić ramkę w oknie głównym jako siatkę (ang. grid) podzieloną na wiersze i kolumny. Przecięcie dowolnego wiersza i dowolnej kolumny wyznacza komórkę, w której możesz umieścić widżet. Na rysunku 10.13 przedstawiłem rozmieszczenie dziewięciu widżetów Button w dziewięciu różnych komórkach, podając numery wierszy i kolumn.

Rysunek 10.13. Każdy przycisk został umieszczony w oddzielnej komórce w zależności od numeru wiersza i kolumny

W przypadku mojego widżetu Label przekazuję 0 do parametru row oraz 0 do parametru column, co sprawia, że etykieta zostaje umieszczona w lewym górnym rogu ramki. Jeśli widżet jest bardzo szeroki (tak jak widżet Label z długim wierszem instrukcji w tym programie), możesz chcieć mu umożliwić zajęcie więcej niż jednej komórki, aby Twoje pozostałe widżety były prawidłowo rozmieszczone. Parametr columnspan pozwala na rozciągnięcie widżetu na więcej niż jedną kolumnę. Przekazuję do tego parametru wartość 2, aby pozwolić, by długa etykieta objęła dwie kolumny. To oznacza, że etykieta zajmuje dwie komórki, jedną w wierszu 0 i kolumnie 0, a drugą w wierszu 0 i kolumnie 1. (Możesz również skorzystać z parametru rowspan, aby widżet mógł obejmować więcej niż jeden wiersz). Nawet po ustaleniu, jaką komórkę (lub komórki) widżet zajmuje, masz jeszcze możliwość regulacji położenia widżetu wewnątrz komórki (lub komórek) poprzez użycie parametru sticky, który jako swoje wartości przyjmuje symbole stron świata: N, S, E i W (północ, południe, wschód i zachód). Widżet zostaje przesunięty do tego kwadrantu komórki (lub komórek), który odpowiada danemu kierunkowi. Ponieważ przekazuję W do parametru sticky obiektu klasy Label, etykieta zostaje przesunięta na zachód (w lewo). Innym sposobem wyrażenia tego jest stwierdzenie, że etykieta została wyrównana do lewej.

307

308

Rozdział 10. Tworzenie interfejsów GUI. Gra Mad Lib

Następnie tworzę etykietę, która pojawia się w kolejnym wierszu i jest także wyrównana do lewej. # utwórz etykietę do hasła self.pw_lbl = Label(self, text = "Hasło: ") self.pw_lbl.grid(row = 1, column = 0, sticky = W)

Tworzenie widżetu Entry Następnie tworzę widżet nowego typu, Entry: # utwórz widżet Entry do przyjęcia hasła self.pw_ent = Entry(self)

Ten kod tworzy pole wejściowe, w którym użytkownik może wprowadzić hasło. Pozycjonuję widżet Entry w taki sposób, aby znalazł się w komórce sąsiadującej z prawej strony z etykietą hasła: self.pw_ent.grid(row = 1, column = 1, sticky = W)

Potem tworzę przycisk, który umożliwi użytkownikowi zaakceptowanie swojego hasła: # utwórz przycisk 'Akceptuj' self.submit_bttn = Button(self, text = "Akceptuj", command = self.reveal)

Wiążę uaktywnienie przycisku z metodą reveal(), która odsłania sekret długowieczności, o ile użytkownik wprowadził prawidłowe hasło. Umieszczam przycisk w następnym wierszu, całkiem na lewo: self.submit_bttn.grid(row = 2, column = 0, sticky = W)

Utworzenie widżetu Text Następnie tworzę widżet nowego typu, Text: # utwórz widżet Text do wyświetlenia komunikatu self.secret_txt = Text(self, width = 35, height = 5, wrap = WORD)

Przekazuję wartości do parametrów width i height w celu ustalenia wymiarów pola tekstowego. Dodatkowo przekazuję wartość do parametru wrap, który decyduje o sposobie zawijania tekstu w polu. Wartości, jakie może przyjąć ten parametr, to WORD, CHAR i NONE. Wartość WORD, której używam w tym widżecie Text, sprawia, że po osiągnięciu prawej krawędzi pola tekstowego są zawijane całe słowa. Wartość CHAR powoduje zawijanie znaków, co oznacza, że po osiągnięciu prawego brzegu pola tekstowego kolejny znak pojawia się po prostu w następnym wierszu. Wartość NONE oznacza brak zawijania. W rezultacie możesz wpisywać tekst tylko w pierwszym wierszu pola tekstowego1.

1

O ile nie używasz klawisza Enter — przyp. tłum.

Używanie widżetów Text i Entry oraz menedżera układu Grid

Potem ustawiam pole tekstowe w taki sposób, aby pojawiło się w nowym wierszu i objęło dwie kolumny: self.secret_txt.grid(row = 3, column = 0, columnspan = 2, sticky = W)

Pobieranie i wstawianie tekstu w widżetach xEntry i Text Następnie piszę metodę reveal(), która sprawdza, czy użytkownik wprowadził prawidłowe hasło. Jeśli test daje wynik pozytywny, metoda wyświetla tajemnicę długiego życia. W przeciwnym wypadku użytkownik zostaje poinformowany, że hasło było nieprawidłowe. Pierwszą moją czynnością jest pobranie tekstu z widżetu Entry poprzez wywołanie metody get(): def reveal(self): """ Wyświetl komunikat zależny od poprawności hasła. """ contents = self.pw_ent.get()

Metoda get() zwraca tekst zawarty w widżecie. Metodę get() mają zarówno obiekty klasy Entry, jak i Text. Sprawdzam, czy ten tekst jest równy wartości łańcucha "sekret". Jeśli jest to prawda, nadaję zmiennej message wartość łańcucha znaków opisującego sekret dożycia 100 lat. W przeciwnym wypadku wartością zmiennej message zostaje łańcuch znaków informujący użytkownika, że wprowadził nieprawidłowe hasło. if contents == "sekret": message = "Oto tajemny przepis na dożycie 100 lat: dożyj 99 lat, " \ "a potem bądź BARDZO ostrożny." else: message = "To nie jest poprawne hasło, więc nie mogę się z Tobą " \ "podzielić swoim sekretem."

Teraz, gdy mam już łańcuch znaków, który chcę pokazać użytkownikowi, muszę wstawić go do widżetu Text. Najpierw usuwam jakikolwiek tekst, który już się znajduje w widżecie Text, poprzez wywołanie metody delete() widżetu: self.secret_txt.delete(0.0, END)

Metoda delete() usuwa tekst z widżetów tekstowych. Metoda może pobrać pojedynczy indeks albo punkt początkowy i końcowy. Przekazujesz do niej liczby zmiennoprzecinkowe reprezentujące pary złożone z numeru wiersza i numeru kolumny, w których cyfra znajdująca się na lewo od kropki dziesiętnej określa numer wiersza, a cyfra na prawo od kropki dziesiętnej wskazuje numer kolumny. Na przykład w powyższym wierszu kodu przekazuję wartość 0.0 jako punkt początkowy, co ma oznaczać, że metoda powinna usunąć tekst, począwszy od pozycji o numerze wiersza 0 i numerze kolumny 0 (czyli od absolutnego początku) pola tekstowego.

309

310

Rozdział 10. Tworzenie interfejsów GUI. Gra Mad Lib

Moduł tkinter udostępnia kilka stałych, które mogą być pomocne w korzystaniu z tego typu metody, takich jak stała END oznaczająca koniec tekstu. Więc w powyższym wierszu kodu usuwam wszystko od pierwszej pozycji pola tekstowego do końca. Obydwa widżety, Text i Entry, posiadają metodę delete(). Następnie do widżetu Text wstawiam łańcuch znaków, który chcę wyświetlić: self.secret_txt.insert(0.0, message)

Metoda insert() służy do wstawienia łańcucha znaków do widżetu tekstowego. Metoda pobiera pozycję wstawienia i łańcuch znaków. W powyższym wierszu kodu przekazuję wartość 0.0 jako pozycję wstawienia, co oznacza, że metoda powinna rozpocząć wstawianie znaków łańcucha od wiersza 0 i kolumny 0. Jako drugą wartość przekazuję message, aby w polu tekstowym pojawił się odpowiedni komunikat. Obydwa typy widżetów, Text i Entry, dysponują metodą insert().

Pułapka Metoda insert() nie zastępuje tekstu w widżecie tekstowym — po prostu go wstawia. Jeśli chcesz zastąpić istniejący tekst nowym, wywołaj najpierw metodę delete() widżetu.

Dokończenie programu Aby sfinalizować program, tworzę okno główne i ustawiam jego tytuł i rozmiary. Następnie tworzę nowy obiekt klasy Application z oknem głównym jako obiektem nadrzędnym. Na koniec inicjuję działanie aplikacji poprzez uruchomienie pętli okna głównego. # część główna root = Tk() root.title("Długowieczność") root.geometry("300x150") app = Application(root) root.mainloop()

Wykorzystanie pól wyboru Pola wyboru umożliwiają użytkownikowi wybór dowolnej liczby opcji z określonej grupy. Chociaż mechanizm ten oferuje użytkownikowi dużą elastyczność, w istocie rzeczy daje programiście większą kontrolę poprzez ograniczenie możliwości wyboru użytkownika do określonej listy opcji.

Prezentacja programu Wybór filmów Program Wybór filmów umożliwia użytkownikowi wybranie ulubionych gatunków filmowych z listy trzech propozycji: komedia, dramat i romans. Ponieważ program

Wykorzystanie pól wyboru

wykorzystuje pola wyboru, użytkownik może wybrać tyle (choć jest to bardzo niewiele) gatunków, ile mu się podoba. Program wyświetla wynik wyborów użytkownika w polu tekstowym (rysunek 10.14).

Rysunek 10.14. Wyniki wyboru użytkownika pojawiają się w polu tekstowym

Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 10.; nazwa pliku to wybor_filmow.py.

Rozpoczęcie programu Rozpoczynam program Wybór filmów od importu zawartości modułu tkinter i przystąpienia do tworzenia definicji klasy Application: # Wybór filmów # Demonstruje pola wyboru from tkinter import * class Application(Frame): """ Aplikacja z GUI służąca do wyboru ulubionych gatunków filmów. """ def __init__(self, master): super(Application, self).__init__(master) self.grid() self.create_widgets()

Dopuszczenie, aby jedyną referencję do widżetu utrzymywał jego obiekt nadrzędny Następnie tworzę etykietę, która opisuje program: def create_widgets(self): """ Utwórz widżety służące do wyboru gatunków filmów. """ # utwórz etykietę z opisem

311

312

Rozdział 10. Tworzenie interfejsów GUI. Gra Mad Lib Label(self, text = "Wybierz swoje ulubione gatunki filmów." ).grid(row = 0, column = 0, sticky = W)

Istnieje jedna ważna różnica pomiędzy tą etykietą a innymi etykietami, które tworzyłem: nie przypisuję nowo utworzonego obiektu klasy Label do zmiennej. Zwykle byłby to duży błąd, sprawiający, że obiekt stałby się bezużyteczny, ponieważ nie byłby w żaden sposób połączony z programem. Ale dzięki modułowi tkinter obiekt Label jest połączony z programem, podobnie jak wszystkie elementy GUI, poprzez swój obiekt nadrzędny. To oznacza, że jeśli wiem, że nie potrzebuję bezpośredniego dostępu do widżetu, nie muszę przypisywać tego obiektu do zmiennej. Główną korzyścią, jaka wynika z takiego podejścia, jest krótszy i bardziej przejrzysty kod. Do tej pory byłem dość konserwatywny i zawsze przypisywałem nowo utworzony widżet do zmiennej. Lecz w tym przypadku wiem, że nie będę potrzebował dostępu do tej etykiety, więc nie przypisuję obiektu klasy Label do zmiennej. Zamiast tego pozwalam, aby jego obiekt nadrzędny utrzymywał jedyną referencję do niego. Następnie tworzę kolejną etykietę w bardzo podobny sposób: # utwórz etykietę z instrukcją Label(self, text = "Zaznacz wszystkie, które chciałbyś wybrać:" ).grid(row = 1, column = 0, sticky = W)

Etykieta ta zawiera instrukcję informującą użytkownika, że może wybrać tyle gatunków filmów, ile uważa za stosowne.

Utworzenie pól wyboru Następnie tworzę pola wyboru, po jednym do każdego gatunku filmów. Najpierw biorę się do pola wyboru komedii. Każde pole wyboru potrzebuje specjalnego, związanego z nim obiektu, który automatycznie odzwierciedla stan pola wyboru. Ten specjalny obiekt musi być instancją klasy BooleanVar z modułu tkinter. Więc przed utworzeniem pola wyboru komedii konkretyzuję obiekt klasy BooleanVar i przypisuję go do nowego atrybutu obiektu Application, likes_comedy: # utwórz pole wyboru komedii self.likes_comedy = BooleanVar()

W świecie rzeczywistym Zmienna typu Boolean to specjalny rodzaj zmiennej, której wartością może być tylko prawda albo fałsz. Programiści często nazywają ją po prostu zmienną Boolean. Termin ten jest zawsze pisany dużą literą, ponieważ został utworzony od nazwiska angielskiego matematyka George’a Boole’a.

Wykorzystanie pól wyboru

Następnie tworzę samo pole wyboru: Checkbutton(self, text = "komedia", variable = self.likes_comedy, command = self.update_text ).grid(row = 2, column = 0, sticky = W)

Powyższy kod tworzy nowe pole wyboru z tekstem komedia. Poprzez przekazanie atrybutu self.likes_comedy do parametru variable kojarzę stan pola wyboru (zaznaczone albo niezaznaczone) z atrybutem likes_comedy. Poprzez przekazanie metody self.update_text() do parametru command wiążę uaktywnienie pola wyboru z metodą update_text(). To oznacza, że ilekroć użytkownik zaznacza lub kasuje zaznaczenie pola wyboru, tylekroć zostaje wywołana metoda update_text(). W końcu umieszczam pole wyboru w następnym wierszu, całkiem z lewej strony. Zwróć uwagę, że nie przypisuję nowo utworzonego obiektu Checkbutton do zmiennej. Wszystko jest dobrze, ponieważ tak naprawdę interesuje mnie tylko stan pola, do którego mam dostęp poprzez atrybut likes_comedy. Kolejne dwa pola wyboru tworzę w taki sam sposób: # utwórz pole wyboru dramatu filmowego self.likes_drama = BooleanVar() Checkbutton(self, text = "dramat", variable = self.likes_drama, command = self.update_text ).grid(row = 3, column = 0, sticky = W) # utwórz pole wyboru romansu self.likes_romance = BooleanVar() Checkbutton(self, text = "romans", variable = self.likes_romance, command = self.update_text ).grid(row = 4, column = 0, sticky = W)

Więc za każdym razem, gdy użytkownik zaznacza pola wyboru dramatu i romansu lub gdy anuluje ich zaznaczenie, wywoływana jest metoda update_text(). I jeśli nawet nie przypisuję powstałych obiektów klasy Checkbutton do żadnych zmiennych, zawsze mogę sprawdzić stan pola wyboru dramatu poprzez atrybut likes_drama oraz zawsze mogę sprawdzić stan pola wyboru romansu poprzez atrybut likes_romance. W końcu tworzę pole tekstowe, które wykorzystuję do pokazania wyników wyborów użytkownika: # utwórz pole tekstowe do wyświetlenia wyników self.results_txt = Text(self, width = 40, height = 5, wrap = WORD) self.results_txt.grid(row = 5, column = 0, columnspan = 3)

313

314

Rozdział 10. Tworzenie interfejsów GUI. Gra Mad Lib

Odczytywanie stanu pola wyboru Następnie piszę metodę update_text(), która aktualizuje zawartość pola tekstowego w taki sposób, aby odzwierciedlała zaznaczone przez użytkownika pola wyboru: def update_text(self): """ Zaktualizuj pole tekstowe i wyświetl ulubione gatunki filmów użytkownika.""" likes = "" if self.likes_comedy.get(): likes += "Lubisz filmy komediowe.\n" if self.likes_drama.get(): likes += "Lubisz dramaty filmowe.\n" if self.likes_romance.get(): likes += "Lubisz filmy romantyczne." self.results_txt.delete(0.0, END) self.results_txt.insert(0.0, likes)

Nie można uzyskać bezpośredniego dostępu do wartości obiektu BooleanVar. Dlatego musisz wywołać metodę get() tego obiektu. W powyższym kodzie wykorzystuję metodę get() obiektu BooleanVar, do którego odwołuje się atrybut likes_comedy, aby uzyskać wartość obiektu. Jeśli tą wartością jest prawda, co oznacza, że pole wyboru komedii zostało zaznaczone, dodaję łańcuch "Lubisz filmy komediowe.\n" do łańcucha konstruowanego w celu wyświetlenia go w polu tekstowym. Wykonuję podobne operacje na podstawie stanu pól wyboru dramatu i romansu. Na koniec usuwam cały tekst znajdujący się w polu tekstowym, a następnie wstawiam nowy łańcuch, likes, który właśnie zbudowałem.

Dokończenie programu Kończę program znajomą główną częścią. Tworzę okno główne i nowy obiekt klasy Application z oknem głównym jako obiektem nadrzędnym. Następnie uruchamiam pętlę zdarzeń okna. # część główna root = Tk() root.title("Wybór filmów") app = Application(root) root.mainloop()

Wykorzystanie przycisków opcji Przyciski opcji są bardzo podobne do pól wyboru z tym wyjątkiem, że umożliwiają jednoczesny wybór tylko jednego przycisku z grupy. Jest to znakomite w sytuacji, gdy chcesz, aby użytkownik dokonał wyboru jednej opcji z grupy możliwych do wybrania.

Wykorzystanie przycisków opcji

Ponieważ przyciski opcji mają tak dużo wspólnego z polami wyboru, nauczenie się ich stosowania jest dosyć proste.

Prezentacja programu Wybór filmów 2 Program Wybór filmów 2 jest podobny do programu Wybór filmów. Użytkownikowi zostają przedstawione trzy różne gatunki filmów, spośród których powinien dokonać wyboru. Różnica polega na tym, że program Wybór filmów 2 wykorzystuje przyciski opcji zamiast pól wyboru, więc użytkownik może wybrać tylko jeden gatunek filmu. Jest to doskonała okoliczność, ponieważ program prosi użytkownika o wybranie ulubionego gatunku filmu. Program został zademonstrowany na rysunku 10.15.

Rysunek 10.15. Użytkownik może wybrać tylko jeden gatunek filmu

Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 10.; nazwa pliku to wybor_filmow2.py.

Rozpoczęcie programu Rozpoczynam program od importu zawartości modułu tkinter: # Wybór filmów 2 # Demonstruje przyciski opcji from tkinter import *

Następnie piszę kod klasy Application. Definiuję jej konstruktor, który inicjalizuje nowy obiekt klasy Application: class Application(Frame): """ Aplikacja z GUI służąca do wyboru ulubionego gatunku filmowego. """ def __init__(self, master): """ Inicjalizuj ramkę. """

315

316

Rozdział 10. Tworzenie interfejsów GUI. Gra Mad Lib super(Application, self).__init__(master) self.grid() self.create_widgets()

Następnie tworzę etykiety zawierające instrukcje dla użytkowników: def create_widgets(self): """ Utwórz widżety umożliwiające wybór gatunku filmowego. """ # utwórz etykietę z opisem Label(self, text = "Wybierz swój ulubiony gatunek filmu" ).grid(row = 0, column = 0, sticky = W) # utwórz etykietę z instrukcją Label(self, text = "Wybierz jeden gatunek:" ).grid(row = 1, column = 0, sticky = W)

Utworzenie przycisków opcji Ponieważ tylko jeden przycisk opcji w grupie może jednocześnie zostać wybrany, nie istnieje potrzeba, aby każdy przycisk opcji miał własną zmienną stanu, jak to było wymagane w przypadku pól wyboru. Natomiast grupa przycisków opcji współdzieli jeden specjalny obiekt odzwierciedlający to, który z przycisków opcji został wybrany. Ten obiekt może być instancją klasy StringVar z modułu tkinter, która umożliwia przechowywanie i pobieranie łańcucha znaków. Więc przed utworzeniem samych przycisków opcji tworzę pojedynczy obiekt klasy StringVar, który ma być współużytkowany przez przyciski opcji, przypisuję go do atrybutu favorite i ustawiam jego wartość początkową na None przy użyciu metody set() obiektu: # utwórz zmienną, która ma reprezentować pojedynczy, ulubiony gatunek filmu self.favorite = StringVar() self.favorite.set(None)

Następnie tworzę przycisk opcji komedia: # utwórz przycisk opcji do wyboru komedii Radiobutton(self, text = "komedia", variable = self.favorite, value = "komedia.", command = self.update_text ).grid(row = 2, column = 0, sticky = W)

Opcja variable przycisku definiuje specjalną zmienną związaną z tym przyciskiem, podczas gdy opcja value definiuje wartość, która ma być przechowywana przez zmienną specjalną, gdy przycisk jest wybrany. Więc poprzez ustawienie opcji variable tego przycisku na self.favorite, a jego opcji value na "komedia." deklaruję, że wtedy, gdy przycisk jest zaznaczony, obiekt klasy StringVar, do którego odwołuje się atrybut self.favorite, powinien przechowywać łańcuch "komedia.".

Wykorzystanie przycisków opcji

Następnie tworzę dwa pozostałe przyciski opcji: # utwórz przycisk opcji do wyboru dramatu Radiobutton(self, text = "dramat", variable = self.favorite, value = "dramat.", command = self.update_text ).grid(row = 3, column = 0, sticky = W) # utwórz przycisk opcji do wyboru romansu Radiobutton(self, text = "romans", variable = self.favorite, value = "romans.", command = self.update_text ).grid(row = 4, column = 0, sticky = W)

Poprzez ustawienie opcji variable przycisku dramat na self.favorite, a jego opcji value na "dramat." deklaruję, że wtedy, gdy ten przycisk jest wybrany, obiekt klasy StringVar, do którego odwołuje się atrybut self.favorite, powinien przechowywać łańcuch "dramat.". A poprzez ustawienie opcji variable przycisku romans na self.favorite, a jego opcji value na "romans." deklaruję, że wtedy, gdy ten przycisk jest wybrany, obiekt klasy StringVar, do którego odwołuje się atrybut self.favorite, powinien przechowywać łańcuch "romans.". Następnie tworzę pole tekstowe służące do wyświetlania wyników wyboru użytkownika: # utwórz pole tekstowe do wyświetlania wyników self.results_txt = Text(self, width = 40, height = 5, wrap = WORD) self.results_txt.grid(row = 5, column = 0, columnspan = 3)

Pobranie wartości z grupy przycisków opcji Pobranie wartości z grupy przycisków opcji jest proste i sprowadza się do wywołania metody get() obiektu StringVar, który jest wykorzystywany wspólnie przez wszystkie przyciski: def update_text(self): """ Zaktualizuj pole tekstowe i wyświetl ulubiony gatunek filmu użytkownika. """ message = "Twoim ulubionym gatunkiem filmu jest " message += self.favorite.get()

Kiedy jest zaznaczony przycisk opcji komedia, metoda self.favorite.get() zwraca łańcuch "komedia."; gdy jest wybrany przycisk opcji dramat, metoda self.favorite.get() zwraca łańcuch "dramat."; a kiedy jest zaznaczony przycisk opcji romans, metoda self.favorite.get() zwraca łańcuch "romans.".

317

318

Rozdział 10. Tworzenie interfejsów GUI. Gra Mad Lib

Następnie usuwam wszelki tekst, jaki może znajdować się w polu tekstowym, i wstawiam świeżo utworzony łańcuch znaków, który oznajmia, jaki jest ulubiony gatunek filmu użytkownika: self.results_txt.delete(0.0, END) self.results_txt.insert(0.0, message)

Dokończenie programu Finalizuję program poprzez utworzenie okna głównego i nowego obiektu klasy Application. Następnie inicjuję pętlę zdarzeń okna głównego w celu uruchomienia interfejsu GUI. # część główna root = Tk() root.title("Wybór filmów 2") app = Application(root) root.mainloop()

Powrót do programu Mad Lib Teraz, gdy już widziałeś dość dużo różnych widżetów wykorzystywanych osobno, nadszedł czas, aby połączyć je w jeden większy interfejs GUI. Nie wprowadzam żadnych nowych koncepcji w programie Mad Lib, więc nie komentuję zanadto kodu. Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 10.; nazwa pliku to mad_lib.py.

Import modułu tkinter Jak już do tej pory zapewne wiesz, musisz zaimportować zawartość modułu tkinter przed jej wykorzystaniem: # Mad Lib # Tworzy opowiadanie oparte na szczegółach wprowadzonych przez użytkownika from tkinter import *

Konstruktor klasy Application Podobnie jak wszystkie poprzednie konstruktory klasy Application także i ten inicjalizuje nowo utworzony obiekt klasy Application i wywołuje metodę create_widgets(): class Application(Frame): """ Aplikacja z GUI, która tworzy opowiadanie oparte na danych wprowadzonych przez użytkownika. """

Powrót do programu Mad Lib def __init__(self, master): """ Inicjalizuj ramkę. """ super(Application, self).__init__(master) self.grid() self.create_widgets()

Metoda create_widgets() klasy Application Ta klasa tworzy wszystkie widżety interfejsu GUI. Jedyną nową rzeczą, jaką robię, jest utworzenie wszystkich trzech przycisków opcji w pętli poprzez przejście przez listę łańcuchów znaków, które zostają przypisane do opcji text i value każdego kolejnego przycisku: def create_widgets(self): """ Utwórz widżety potrzebne do pobrania informacji podanych przez użytkownika i wyświetlenia opowiadania. """ # utwórz etykietę z instrukcją Label(self, text = "Wprowadź informacje do nowego opowiadania" ).grid(row = 0, column = 0, columnspan = 2, sticky = W) # utwórz etykietę i pole znakowe służące do wpisania imienia osoby Label(self, text = "Osoba: " ).grid(row = 1, column = 0, sticky = W) self.person_ent = Entry(self) self.person_ent.grid(row = 1, column = 1, sticky = W) # utwórz etykietę i pole znakowe do wpisania rzeczownika w liczbie mnogiej Label(self, text = "Rzeczownik w liczbie mnogiej:" ).grid(row = 2, column = 0, sticky = W) self.noun_ent = Entry(self) self.noun_ent.grid(row = 2, column = 1, sticky = W) # utwórz etykietę i pole znakowe służące do wpisania czasownika Label(self, text = "Czasownik:" ).grid(row = 3, column = 0, sticky = W) self.verb_ent = Entry(self) self.verb_ent.grid(row = 3, column = 1, sticky = W) # utwórz etykietę do pól wyboru przymiotników Label(self, text = "Przymiotnik(i):" ).grid(row = 4, column = 0, sticky = W) # utwórz pole wyboru przymiotnika 'naglące' self.is_itchy = BooleanVar() Checkbutton(self, text = "naglące",

319

320

Rozdział 10. Tworzenie interfejsów GUI. Gra Mad Lib variable = self.is_itchy ).grid(row = 4, column = 1, sticky = W) # utwórz pole wyboru przymiotnika 'radosne' self.is_joyous = BooleanVar() Checkbutton(self, text = "radosne", variable = self.is_joyous ).grid(row = 4, column = 2, sticky = W) # utwórz pole wyboru przymiotnika 'elektryzujące' self.is_electric = BooleanVar() Checkbutton(self, text = "elektryzujące", variable = self.is_electric ).grid(row = 4, column = 3, sticky = W) # utwórz etykietę do przycisków opcji odnoszących się do części ciała Label(self, text = "Część ciała:" ).grid(row = 5, column = 0, sticky = W) # utwórz zmienną na pojedynczą część ciała self.body_part = StringVar() self.body_part.set(None) # utwórz przyciski opcji odnoszące się do części ciała body_parts = ["pępek", "duży palec u nogi", "rdzeń przedłużony"] column = 1 for part in body_parts: Radiobutton(self, text = part, variable = self.body_part, value = part ).grid(row = 5, column = column, sticky = W) column += 1 # utwórz przycisk akceptacji danych Button(self, text = "Kliknij, aby wyświetlić opowiadanie", command = self.tell_story ).grid(row = 6, column = 0, sticky = W) self.story_txt = Text(self, width = 75, height = 10, wrap = WORD) self.story_txt.grid(row = 7, column = 0, columnspan = 4)

Metoda tell_story() klasy Application W tej metodzie pobieram wartości wprowadzone przez użytkownika i wykorzystuję je do utworzenia jednego, długiego łańcucha reprezentującego opowiadanie. Następnie usuwam wszelki tekst znajdujący się w polu tekstowym oraz wstawiam ten nowy łańcuch znaków, aby pokazać użytkownikowi opowiadanie, które utworzył.

Powrót do programu Mad Lib def tell_story(self): """ Wpisz w pole tekstowe nowe opowiadanie oparte na danych użytkownika. """ # pobierz wartości z interfejsu GUI person = self.person_ent.get() noun = self.noun_ent.get() verb = self.verb_ent.get() adjectives = "" if self.is_itchy.get(): adjectives += "naglące, " if self.is_joyous.get(): adjectives += "radosne, " if self.is_electric.get(): adjectives += "elektryzujące, " body_part = self.body_part.get() # create the story story = "Sławny badacz i odkrywca " story += person story += " o mało co nie zrezygnował z życiowej misji poszukiwania " story += "zaginionego miasta, które zamieszkiwały " story += noun story += ", gdy pewnego dnia " story += noun story += " znalazły " story += person + "a. " story += "Silne, " story += adjectives story += "osobliwe uczucie owładnęło badaczem. " story += "Po tak długim czasie poszukiwanie wreszcie się zakończyło. W oku " story += person + "a pojawiła się łza, która spadła na jego " story += body_part + ". " story += "A wtedy " story += noun story += " szybko pożarły " story += person + "a. " story += "Jaki morał płynie z tego opowiadania? Pomyśl, zanim zaczniesz coś " story += verb story += "." # wyświetl opowiadanie self.story_txt.delete(0.0, END) self.story_txt.insert(0.0, story)

Główna część programu Widziałeś już przedtem ten tekst niemało razy. Tworzę okno główne oraz instancję klasy Application. Potem uruchamiam cały interfejs GUI poprzez wywołanie metody mainloop() obiektu root. # część główna root = Tk() root.title("Mad Lib") app = Application(root) root.mainloop()

321

322

Rozdział 10. Tworzenie interfejsów GUI. Gra Mad Lib

Podsumowanie W tym rozdziale poznałeś tworzenie interfejsów GUI. Najpierw dowiedziałeś się o programowaniu sterowanym zdarzeniami, nowym sposobie spojrzenia na pisanie kodu. Potem poznałeś szereg widżetów GUI, a wśród nich ramki, przyciski, pola wejściowe, pola tekstowe, pola wyboru oraz przyciski opcji. Zobaczyłeś, jak przystosowywać widżety do swoich potrzeb. Zobaczyłeś także, jak organizować je w ramce przy użyciu menedżera układu Grid. Dowiedziałeś się, jak wiązać zdarzenia z procedurami ich obsługi, aby widżety coś robiły po ich uaktywnieniu. Na koniec zobaczyłeś, jak łączyć poszczególne elementy w dość skomplikowany interfejs GUI, tworząc zabawny program Mad Lib.

Sprawdź swoje umiejętności 1. Napisz swoją własną wersję programu Mad Lib, wykorzystując inny układ widżetów. 2. Napisz wersję programu Jaka to liczba?, stanowiącego projekt rozdziału 3., która wykorzystuje interfejs GUI. 3. Utwórz program z interfejsem GUI o nazwie Złóż zamówienie!, który przedstawia użytkownikowi proste restauracyjne menu, które wymienia pozycje i ich ceny. Pozwól użytkownikowi na wybranie różnych pozycji, a potem pokaż mu końcowy rachunek.

11 Grafika. Gra Pizza Panic W

iększość programów, z którymi dotychczas się spotkałeś, skupiała się na prezentacji tekstu, lecz dzisiaj ludzie oczekują od programów bogatej, wizualnej treści, niezależnie od ich zastosowania. W tym rozdziale dowiesz się, jak używać grafiki za pomocą kilku multimedialnych modułów zaprojektowanych do pisania gier w języku Python. W szczególności nauczysz się:  tworzyć okno graficzne,  tworzyć duszki i manipulować nimi,  wyświetlać tekst w oknie graficznym,  sprawdzać, czy nie dochodzi do kolizji między duszkami,  obsługiwać dane wejściowe z myszy,  sterować komputerowym przeciwnikiem.

Wprowadzenie do gry Pizza Panic Główny projekt tego rozdziału, gra Pizza Panic, obejmuje szalonego szefa kuchni, głęboką patelnię i znaczną liczbę latających pizz. Oto scenariusz gry: wyprowadzony z równowagi przez o jednego za dużo wybrednych gości szef kuchni w lokalnej pizzerii wyszedł na dach i z wściekłością ciska pizze, skazując je na zmarnowanie. Oczywiście pizze muszą zostać uratowane. Przy użyciu myszy gracz steruje patelnią, manewrując nią w taki sposób, aby złapać spadające pizze. Zdobycz punktowa gracza zwiększy się wraz z każdą złapaną pizzą, ale kiedy choć tylko jedna spadnie na ziemię, gra się kończy. Na rysunkach 11.1 i 11.2 pokazałem tę grę w akcji.

324

Rozdział 11. Grafika. Gra Pizza Panic

Rysunek 11.1. Gracz musi łapać spadające pizze

Rysunek 11.2. Kiedy graczowi nie udaje się złapać jakiejś pizzy, gra się kończy

Wprowadzenie do pakietów pygame i livewires

Wprowadzenie do pakietów pygame i livewires Pakiety (ang. packages) pygame i livewires to zestawy modułów, które dają programistom Pythona dostęp do szerokiego wachlarza klas multimediów. Za pomocą tych klas możesz tworzyć programy z grafiką, efektami dźwiękowymi, muzyką i animacją. Te pakiety umożliwiają także obsługę danych wejściowych z rozmaitych urządzeń, w tym z myszy i klawiatury. Dzięki nim nie będziesz musiał się troszczyć o niskopoziomowe szczegóły dotyczące sprzętu, takie jak rodzaj karty graficznej posiadanej przez gracza lub to, czy w ogóle ma jakąś kartę graficzną. Będziesz za to mógł się skoncentrować na logice programu i zabrać się szybko do pisania gier. Pakiet pygame stanowi tajną broń w Twoim arsenale obsługi mediów. Napisany przez Pete’a Shinnersa pakiet umożliwia pisanie w Pythonie imponujących programów multimedialnych. Ponieważ pakiet ma tak wielkie możliwości, może nieco przytłaczać niedoświadczonego programistę. Pakiet livewires napisany przez grupę edukatorów w Zjednoczonym Królestwie został zaprojektowany w celu wykorzystania potencjału pakietu pygame przy jednoczesnej redukcji jego złożoności dla programisty. Pakiet livewires udostępnia prostszy sposób na rozpoczęcie programowania gier z grafiką i dźwiękiem. I jeśli nawet nie będziesz bezpośrednio korzystać z dostępu do pakietu pygame, będzie on nadal obecny, pracując ciężko na zapleczu. Zanim będziesz mógł uruchomić programy prezentowane w tym rozdziale, musisz zainstalować zarówno pygame, jak i livewires. Na szczęście wersje obydwu pakietów znajdują się na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze Software. Postępuj zgodnie z instrukcjami instalacji, które towarzyszą tym pakietom.

Pułapka Chociaż zachęcam Cię do odwiedzenia witryny organizacji LiveWires o adresie http://www.livewires.org.uk, musisz mieć na uwadze, że pakiet livewires dostępny na stronie internetowej tej książki (http://www.helion.pl/ksiazki/ pytdk3.htm) jest zmodyfikowaną wersją pakietu utworzonego przez LiveWires. Zaktualizowałem ten pakiet, aby uczynić go jeszcze prostszym w użyciu dla programistów. I nie martw się — zmodyfikowaną wersję dokumentacji zamieściłem w dodatku B.

Jeśli chcesz się dowiedzieć więcej o pakiecie pygame, odwiedź stronę tego pakietu o adresie http://www.pygame.org.

Tworzenie okna graficznego Zanim zaczniesz wyświetlać jakąkolwiek grafikę, musisz utworzyć okno graficzne — puste tło, na którym można wyświetlać tekst i obrazy.

325

326

Rozdział 11. Grafika. Gra Pizza Panic

Prezentacja programu Nowe okno graficzne Utworzenie okna graficznego za pomocą pakietu livewires to pestka. Program Nowe okno graficzne tworzy puste okno graficzne w zaledwie paru wierszach kodu. Na rysunku 11.3 został pokazany wynik uruchomienia programu.

Rysunek 11.3. Moje pierwsze okno graficzne. Niby nic wielkiego, ale jest moje

Pułapka Tak jak w przypadku programu używającego zestawu narzędzi Tkinter, tworząc nowe okno, nie powinieneś uruchamiać programu wykorzystującego livewires w środowisku IDLE. Jeśli używasz systemu Windows, utwórz plik wsadowy, który uruchamia Twój program w języku Python, a następnie wstrzymuje swoje działanie. Aby sprawdzić, co zawiera taki plik wsadowy, zajrzyj do punktu „Prezentacja programu Prosty interfejs GUI” w rozdziale 10.

Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 11.; nazwa pliku to nowe_okno_graficzne.py.

Tworzenie okna graficznego

Import modułu games Pakiet livewires składa się z kilku ważnych modułów, w tym modułu games, który zawiera grupę obiektów i klas służących do programowania gier. Możesz zaimportować konkretny moduł pakietu przy użyciu instrukcji from. W tym celu użyj słowa kluczowego from, następnie podaj nazwę pakietu, słowo kluczowe import oraz nazwę modułu (lub listę nazw modułów oddzielonych przecinkami). Pierwszą moją czynnością w programie jest import modułu games pakietu livewires: # Nowe okno graficzne # Demonstruje tworzenie okna graficznego from livewires import games

W rezultacie mogę używać modułu games tak jak każdego innego modułu, który zaimportowałem. Aby poznać moduł games w ogólnym zarysie, zajrzyj do tabeli 11.1, która zawiera listę użytecznych obiektów modułu, oraz do tabeli 11.2, zawierającej wykaz przydatnych klas. Tabela 11.1. Przydatne obiekty modułu games

Obiekt

Opis

screen

Umożliwia dostęp do ekranu graficznego — obszaru, w którym mogą istnieć obiekty graficzne

mouse

Umożliwia dostęp do myszy.

keyboard

Umożliwia dostęp do klawiatury.

Tabela 11.2. Przydatne klasy modułu games

Klasa

Opis

Sprite

Służy do tworzenia obiektów graficznych, które mogą zostać wyświetlone na ekranie graficznym.

Text

Podklasa klasy Sprite. Służy do tworzenia obiektów tekstowych wyświetlanych na ekranie graficznym

Message

Podklasa klasy Text. Służy do tworzenia obiektów tekstowych wyświetlanych na ekranie graficznym, które znikają po określonym czasie.

Inicjalizacja ekranu graficznego Następnie inicjalizuję ekran graficzny: games.init(screen_width = 640, screen_height = 480, fps = 50)

Kiedy wywołujesz funkcję games.init(), tworzysz nowy ekran graficzny. Szerokość ekranu jest określona przez wartość parametru screen_width, podczas gdy (tu proszę o werble) wysokość ekranu jest określona przez wartość parametru screen_height. Rozmiary ekranu są mierzone w pikselach — pojedynczych punktach obszaru grafiki.

327

328

Rozdział 11. Grafika. Gra Pizza Panic

Wartość parametru fps (ang. frames per second — liczba ramek na sekundę) decyduje, ile razy w ciągu każdej sekundy ekran będzie aktualizowany.

Uruchomienie głównej pętli Ostatni wiersz programu to: games.screen.mainloop()

Słowo screen oznacza obiekt z modułu games, który reprezentuje ekran graficzny. Metoda mainloop() jest wołem roboczym obiektu screen i aktualizuje okno graficzne, wyświetlając wszystko na nowo fps razy na sekundę. Tak więc ostatni wiersz programu utrzymuje otwarte okno graficzne i aktualizuje ekran 50 razy na sekundę. Przyjrzyj się kilku właściwościom obiektu screen wymienionym w tabeli 11.3. Listę przydatnych metod obiektu screen znajdziesz w tabeli 11.4. Tabela 11.3. Przydatne właściwości obiektu screen

Właściwość

Opis

width

Szerokość ekranu.

height

Wysokość ekranu.

fps

Częstotliwość aktualizacji ekranu wyrażona w ramkach na sekundę.

background

Obraz stanowiący tło ekranu.

all objects

Lista wszystkich duszków na ekranie.

event_grab

Właściwość typu Boolean, której wartość decyduje, czy sygnały wejściowe są przechwytywane przez ekran. Wartość True oznacza przechwytywanie sygnałów wejściowych przez ekran. Wartość False oznacza brak przechwytywania.

Tabela 11.4. Przydatne metody obiektu screen

Metoda

Opis

add(sprite)

Dodaje sprite, obiekt klasy Sprite (lub obiekt klasy pochodnej klasy Sprite) do ekranu graficznego.

clear()

Usuwa wszystkie duszki z ekranu graficznego.

mainloop()

Uruchamia główną pętlę ekranu graficznego.

quit()

Zamyka okno graficzne.

Ustawienie obrazu tła Pusty ekran pasuje całkiem dobrze, jeśli Twoim celem jest utworzenie najnudniejszego programu na świecie. Na szczęście obiekt screen ma właściwość służącą do ustawienia obrazu tła.

Ustawienie obrazu tła

Prezentacja programu Obraz tła Program Obraz tła jest tylko modyfikacją programu Nowe okno graficzne. Do ekranu graficznego dodałem obraz tła pokazany na rysunku 11.4.

Rysunek 11.4. Dzięki wykorzystaniu właściwości background obiektu screen dodaję do okna graficznego obraz tła

Aby utworzyć program Obraz tła, do programu Nowe okno graficzne dodaję dwa wiersze, umieszczając je bezpośrednio przed wywołaniem metody mainloop(). Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 11.; nazwa pliku to obraz_tla.py. # Obraz tła # Demonstruje ustawienie obrazu tła ekranu graficznego from livewires import games games.init(screen_width = 640, screen_height = 480, fps = 50) wall_image = games.load_image("sciana.jpg", transparent = False) games.screen.background = wall_image games.screen.mainloop()

329

330

Rozdział 11. Grafika. Gra Pizza Panic

Załadowanie obrazu Zanim będziesz mógł zrobić cokolwiek z obrazem, ustawiając go na przykład jako tło ekranu graficznego, musisz załadować ten obraz do pamięci, aby utworzyć obiekt obrazu. Ładuję obraz poprzez dodanie bezpośrednio po inicjalizacji okna graficznego następującego wiersza kodu: wall_image = games.load_image("sciana.jpg", transparent = False)

Wywołuję w nim funkcję load_image() z modułu games, która ładuje obraz przechowywany w pliku sciana.jpg do pamięci operacyjnej i przypisuje utworzony w ten sposób obiekt obrazu do zmiennej wall_image.

Pułapka Upewnij się, że każdemu plikowi, do którego Twój program w języku Python ma mieć dostęp, towarzyszyła prawidłowa informacja o ścieżce, o czym dowiedziałeś się w rozdziale 7., w punkcie „Otwarcie i zamknięcie pliku”. Najprostszym rozwiązaniem w zarządzaniu plikami, które stosuję w tym przypadku, jest umieszczenie plików z obrazem w tym samym folderze, w którym się znajduje ładujący je program. Jeśli będziesz naśladować ten sposób, nie będziesz w ogóle musiał się martwić o informacje dotyczące ścieżek dostępu.

Funkcja load_image() przyjmuje dwa argumenty: łańcuch reprezentujący nazwę pliku z obrazem oraz wartość True albo False poprzez parametr transparent. Dokładnie wyjaśnię, co oznacza transparent, nieco później w tym rozdziale. Jak na razie wystarczy, że zapamiętasz tę regułę: zawsze ładuj obraz tła, stosując przypisanie transparent = False. Zapewne zauważysz, że jako tło ładuję w tym programie obraz typu JPEG. Nie jesteś jednak ograniczony do obrazów JPEG, gdy korzystasz z funkcji load_image(). Działa ona tak samo dobrze z wieloma innymi typami plików zawierających obraz, w tym BMP, GIF, PNG, PCX oraz TGA.

Ustawienie tła Aby ustawić obiekt obrazu jako tło ekranu, musisz jedynie użyć właściwości background obiektu screen. Bezpośrednio po załadowaniu obrazu dodaję następujący wiersz kodu: games.screen.background = wall_image

W ten sposób ustawiam jako tło ekranu obiekt obrazu wall_image. Kiedy program napotyka wywołanie metody mainloop(), utrzymuje otwarte okno graficzne z jego nowym obrazem tła, aby każdy mógł go zobaczyć.

Układ współrzędnych ekranu graficznego

Układ współrzędnych ekranu graficznego Do tej pory utworzyłem już kilka ekranów graficznych, które za każdym razem miały szerokość 640 i wysokość 480, ale poza tym nie powiedziałem o nich zbyt wiele. Teraz przyjrzymy się dokładniej ekranowi i jego układowi współrzędnych. Można sobie wyobrazić ekran graficzny jako siatkę 640 kolumn i 480 wierszy. Każde przecięcie kolumny i wiersza wyznacza miejsce na ekranie, pojedynczy punkt, czyli piksel. Gdy masz na myśli określony punkt ekranu, podajesz jego dwie współrzędne: x, która reprezentuje kolumnę, oraz y, która reprezentuje wiersz. Początkiem układu współrzędnych jest lewy górny róg ekranu. Zarówno współrzędna x tego punktu, jak i współrzędna y mają wartość 0, co się zapisuje w postaci pary (0,0). W miarę przesuwania się w prawo wzrastają wartości współrzędnej x. Gdy przesuwasz się w dół ekranu, wzrastają wartości współrzędnej y. W ten sposób punkt w prawym dolnym rogu ekranu otrzymuje współrzędne (639, 479). Układ współrzędnych ekranu graficznego został przedstawiony wizualnie na rysunku 11.5.

Rysunek 11.5. Położenie punktów na ekranie graficznym określasz za pomocą pary współrzędnych x i y

Obiekty graficzne, takie jak obrazek pizzy lub czerwonego koloru tekst „Koniec gry”, możesz umieszczać na ekranie, wykorzystując układ współrzędnych. Środek obiektu graficznego zostaje umieszczony w punkcie o podanych współrzędnych. Zobaczysz dokładnie, jak to działa, na przykładzie następnego programu.

331

332

Rozdział 11. Grafika. Gra Pizza Panic

Wyświetlanie duszka Obrazy tła mogą przyozdobić gładki ekran graficzny, ale nawet oszałamiające tło pozostaje nadal tylko obrazem statycznym. Ekran graficzny z samym tylko obrazem tła przypomina pustą scenę. Potrzebni są jacyś aktorzy. Niech pojawi się na nim duszek. Duszek (ang. sprite) to obiekt graficzny z obrazem, który rzeczywiście potrafi ożywić programy. Duszki są wykorzystywane w grach, oprogramowaniu służącemu rozrywce, prezentacjach oraz w całej sieci WWW. Właściwie już widziałeś przykłady duszków w grze Pizza Panic. Szalony kucharz, patelnia, pizze — to wszystko duszki.

W świecie rzeczywistym Duszki mają zastosowanie nie tylko w grach. Istnieje wiele przypadków dotyczących oprogramowania, które nie służy rozrywce, gdzie są one używane… lub nadużywane. Zapewne znacie najsławniejszego duszka w historii oprogramowania aplikacji, asystenta pakietu Microsoft Office o nazwie Clippy — animowany spinacz, który miał z założenia udzielać użytecznych wskazówek użytkownikom. Wielu ludzi jednak uważało, że Clippy jest natrętny i denerwujący. Jeden z większych portali internetowych zamieścił nawet artykuł zatytułowany Kill Clippy! (zabijcie Clippy’ego). W końcu Microsoft przejrzał na oczy. Począwszy od pakietu Office 2007, Clippy jest już niedostępny. Więc chociaż grafika może sprawić, że programy staną się bardziej interesujące, pamiętaj o tym, aby wykorzystywać moce duszków tylko w dobrym celu, a nigdy w złym.

Chociaż byłoby zabawne i ciekawe widzieć chmarę duszków, które fruwają dokoła i wpadają na siebie, rozpocznę od postawienia pierwszego kroku — wyświetlenia pojedynczego, nieruchomego duszka.

Prezentacja programu Duszek pizzy W programie Duszek pizzy tak jak poprzednio tworzę okno graficzne i ustawiam obraz tła. Jednak po wykonaniu tego kroku, tworzę duszka pizzy w samym środku ekranu. Wynik uruchomienia tego programu został pokazany na rysunku 11.6. Program Duszek pizzy jest tylko modyfikacją programu Obraz tła. Aby mógł pojawić się na ekranie nowy duszek, dodaję tylko trzy wiersze kodu bezpośrednio przed wywołaniem metody mainloop(). Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 11.; nazwa pliku to duszek_pizzy.py. # Duszek pizzy # Demonstruje tworzenie duszka from livewires import games games.init(screen_width = 640, screen_height = 480, fps = 50) wall_image = games.load_image("sciana.jpg", transparent = False) games.screen.background = wall_image

Wyświetlanie duszka

Rysunek 11.6. Obrazek pizzy nie jest częścią tła, lecz niezależnym obiektem klasy Sprite pizza_image = games.load_image("pizza.bmp") pizza = games.Sprite(image = pizza_image, x = 320, y = 240) games.screen.add(pizza) games.screen.mainloop()

Załadowanie obrazu potrzebnego do utworzenia obiektu klasy Sprite Najpierw ładuję obrazek pizzy do pamięci operacyjnej, aby utworzyć obiekt obrazu: pizza_image = games.load_image("pizza.bmp")

Zwróć uwagę na jedną małą różnicę w stosunku do sposobu, w jaki załadowałem obraz tła. Tym razem w wywołaniu funkcji nie uwzględniłem nadania wartości parametrowi transparent. Jego wartością domyślną jest True, więc obraz został załadowany z włączoną przezroczystością. Jeśli obraz został załadowany z włączoną przezroczystością, jest wyświetlany na ekranie graficznym w taki sposób, że przez jego przezroczyste części widać obraz tła. Jest to wspaniała rzecz w przypadku duszków o nieregularnym kształcie, które nie są idealnymi prostokątami, oraz w przypadku duszków z „dziurami” w środku, takich jak, powiedzmy, duszek sera szwajcarskiego.

333

334

Rozdział 11. Grafika. Gra Pizza Panic

Te części obrazu, które są przezroczyste, są zdefiniowane przez swój kolor. Jeśli obraz zostaje załadowany z włączoną przezroczystością, kolorem przezroczystym staje się kolor punktu położonego w lewym górnym rogu obrazu. Przez wszystkie części obrazu, które mają ten przezroczysty kolor, będzie przezierać tło ekranu. Na rysunku 11.7 pokazuję duszka sera szwajcarskiego na jednolitym białym tle umożliwiającym wykorzystanie przezroczystości.

Rysunek 11.7. Serowy duszek przedstawiony na tle jednolitego koloru umożliwiający wykorzystanie przezroczystości

Jeśli załaduję ten obraz sera szwajcarskiego z włączoną przezroczystością, każdy jego fragment, który ma czysto biały kolor (kolor wzięty z piksela znajdującego się w lewym górnym rogu ekranu), stanie się przezroczysty, kiedy duszek zostanie wyświetlony na ekranie graficznym. Przez te przezroczyste części duszka będzie widoczny obraz tła. Na rysunku 11.8 pokazuję, jak obraz wygląda po załadowaniu z włączoną i wyłączoną przezroczystością.

Rysunek 11.8. Obrazek z lewej strony został załadowany z włączoną przezroczystością. Po prawej stronie znajduje się ten sam obrazek, lecz załadowany z wyłączoną przezroczystością

Wyświetlanie duszka

Przyjmij jako ogólną regułę: musisz utworzyć swój obraz duszka na tle jednolitego koloru, który nie jest używany w żadnej innej części obrazu.

Pułapka Upewnij się, że Twój obraz duszka też nie zawiera koloru, którego używasz do uzyskania przezroczystości. W przeciwnym razie te części duszka staną się również przezroczyste, co sprawi, że Twój duszek będzie wyglądał tak, jakby zawierał małe dziurki lub pęknięcia, ponieważ będzie prześwitywał przez nie obraz tła.

Utworzenie duszka Następnie tworzę duszka pizzy: pizza = games.Sprite(image = pizza_image, x = 320, y = 240)

Zostaje utworzony nowy obiekt klasy Sprite z obrazem pizzy i współrzędnymi x i y o wartościach (320, 240), które ustawiają obraz w samym środku ekranu. Następnie nowy obraz zostaje przypisany do zmiennej pizza. Gdy tworzysz obiekt klasy Sprite, powinieneś przekazać do jej konstruktora przynajmniej obraz, współrzędną x oraz współrzędną y.

Dodanie obiektu klasy Sprite do ekranu Po utworzeniu duszka musisz dodać go do ekranu, aby mógł być widoczny i aktualizowany, i dokładnie to robię w kolejnym wierszu kodu: games.screen.add(pizza)

Metoda add() po prostu dodaje duszka do ekranu graficznego.

Sztuczka Aby tworzyć grafikę do swoich gier, nie musisz być artystą. Jak możesz zauważyć w tym rozdziale, nadrabiam mój całkowity brak artystycznych zdolności elementem nowoczesnej techniki — moim aparatem cyfrowym. Jeśli masz dostęp do aparatu cyfrowego, możesz tworzyć wspaniałe obrazy do swoich projektów. Oto jak w rzeczywistości tworzyłem grafikę do gry Pizza Panic. Ceglany mur to tył domu mojego przyjaciela. Jeśli chodzi o pizzę, to pewnego wieczoru zamówiłem jej dostawę. A szefem kuchni jest mój bardzo dzielny kolega, Dave. Chociaż jest to wspaniała technika, ważną rzeczą do zapamiętania jest fakt, że kiedy zrobisz zdjęcie osoby lub obiektu, niekoniecznie zostajesz właścicielem obrazu — oczywiście, pewne rzeczy są chronione znakiem firmowym lub prawem autorskim. Wykorzystanie aparatu cyfrowego jest jednak wspaniałym sposobem uzyskiwania obrazów o ogólnej treści i nadawania programom wyjątkowego, fotorealistycznego stylu.

Tabela 11.5 zawiera listę przydatnych właściwości klasy Sprite, a tabela 11.6 — listę użytecznych metod tej klasy.

335

336

Rozdział 11. Grafika. Gra Pizza Panic Tabela 11.5. Przydatne właściwości klasy Sprite

Właściwość

Opis

angle

Orientacja w stopniach.

x

Współrzędna x.

y

Współrzędna y.

dx

Prędkość zmiany współrzędnej x.

dy

Prędkość zmiany współrzędnej y.

left

Współrzędna x lewej krawędzi duszka.

right

Współrzędna x prawej krawędzi duszka.

top

Współrzędna y górnej krawędzi duszka.

bottom

Współrzędna y dolnej krawędzi duszka.

image

Obiekt obrazu duszka.

overlapping_sprites

Lista innych obiektów, które zachodzą na duszka.

is_collideable

Ustala, czy duszek podlega kolizjom, czy nie. Wartość True oznacza, że duszek wystąpi w kolizjach. Wartość False oznacza, że duszek nie pojawi się w kolizjach.

Tabela 11.6. Przydatne metody klasy Sprite

Metoda

Opis

update()

Aktualizuje duszka. Wywoływana automatycznie w każdym cyklu pętli mainloop().

destroy()

Usuwa duszka z ekranu.

Wyświetlanie tekstu Czy chcesz pokazać liczby przy prezentacji wyników sprzedaży, czy też liczbę unicestwionych kosmitów, są sytuacje, w których chciałbyś wyświetlić tekst na ekranie graficznym. Moduł games zawiera klasę, która to właśnie umożliwia, o stosownej nazwie Text.

Prezentacja programu Wysoki wynik Wyświetlenie tekstu na ekranie graficznym sprowadza się do utworzenia obiektu klasy Text. Program Wysoki wynik jest modyfikacją programu Obraz tła. Wyświetlam wynik gry w prawym górnym rogu ekranu, tak jak w wielu klasycznych grach arkadowych. Efekt uruchomienia programu został pokazany na rysunku 11.9.

Wyświetlanie tekstu

Rysunek 11.9. Imponująco wysoki wynik został wyświetlony po konkretyzacji obiektu klasy Text

Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 11.; nazwa pliku to wysoki_wynik.py. # Wysoki wynik # Demonstruje wyświetlanie tekstu na ekranie graficznym from livewires import games, color games.init(screen_width = 640, screen_height = 480, fps = 50) wall_image = games.load_image("sciana.jpg", transparent = False) games.screen.background = wall_image score = games.Text(value = 1756521, size = 60, color = color.black, x = 550, y = 30) games.screen.add(score) games.screen.mainloop()

337

338

Rozdział 11. Grafika. Gra Pizza Panic

Import modułu color Pakiet livewires zawiera jeszcze jeden moduł, color, w którym zdefiniowano zbiór stałych reprezentujących różne kolory. Te kolory mogą zostać użyte w pewnych obiektach graficznych, w tym w dowolnych obiektach klasy Text i Message. Kompletną listę predefiniowanych kolorów znajdziesz w dokumentacji pakietu livewires, w dodatku B. Aby móc dokonać wyboru z grupy dostępnych kolorów, importuję moduł color, zastępując instrukcję import znajdującą się na początku programu wierszem kodu: from livewires import games, color

Obydwa moduły, color i game, są ładowane z pakietu livewires.

Utworzenie obiektu klasy Text Obiekt klasy Text reprezentuje tekst na ekranie graficznym. Tuż przed wywołaniem metody mainloop() tworzę obiekt klasy Text i przypisuję go do zmiennej score: score = games.Text(value = 1756521, size = 60, color = color.black, x = 550, y = 30)

Do konstruktora obiektu klasy Text powinno się przekazać co najmniej wartość, która ma zostać wyświetlona w postaci tekstu, rozmiar czcionki, kolor, współrzędną x oraz współrzędną y. Jako wartość parametru value przekazuję liczbę całkowitą 1756521, co ma spowodować wyświetlenie ciągu tworzących ją znaków. (Obiekt klasy Text zostanie wyświetlony jako łańcuch znaków reprezentujący dowolną wartość, jaka zostanie przekazana do value). Parametrowi size, który reprezentuje wysokość tekstu w pikselach, nadaję wartość 60, aby czcionka była ładna i duża — pasująca do wyniku. Parametrowi color nadaję wartość stałej color.black z modułu color, aby sprawić, że tekst będzie — zgadłeś — czarny. Nadaję parametrowi x wartość 550, a parametrowi y wartość 30, umieszczając w ten sposób środek obiektu w punkcie o współrzędnych (550, 30). Oznacza to umiejscowienie tekstu w prawym górnym rogu okna graficznego.

Dodanie obiektu klasy Text do ekranu W kolejnym wierszu kodu dodaję nowy obiekt do ekranu, aby został wyświetlony: games.screen.add(score)

Natychmiast po wywołaniu metody mainloop() okno graficzne zostanie wyświetlone wraz z obiektem score. Ponieważ klasa Text jest podklasą klasy Sprite, dziedziczy wszystkie jej właściwości, atrybuty i metody. Tabela 11.7 zawiera dwie dodatkowe właściwości klasy Text w niej zdefiniowane.

Wyświetlanie komunikatu Tabela 11.7. Dodatkowe właściwości klasy Text

Właściwość

Opis

value

Wartość, która ma być wyświetlona w postaci tekstu.

color

Kolor tekstu.

Wyświetlanie komunikatu Mógłbyś chcieć wyświetlać jakiś tekst na ekranie jedynie przez krótki czas. Mógłbyś zechcieć pokazać komunikat o treści „Wszystkie rekordy zostały zaktualizowane” lub „Siódma fala ataku została zakończona!”. Klasa Message modułu games nadaje się doskonale do tworzenia tego rodzaju tymczasowych komunikatów.

Prezentacja programu Wygrałeś Program Wygrałeś to zmodyfikowana wersja programu Obraz tła. Konkretyzuję obiekt klasy Message tuż przed wywołaniem metody mainloop(), aby wyświetlić tekst „Wygrałeś!” dużymi, czerwonymi literami. Komunikat jest wyświetlany przez mniej więcej pięć sekund, a następnie program kończy działanie. Program ten został zilustrowany na rysunku 11.10.

Rysunek 11.10. Och, ten smak zwycięstwa

339

340

Rozdział 11. Grafika. Gra Pizza Panic

Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 11.; nazwa pliku to wygrales.py. # Wygrałeś # Demonstruje wyświetlanie komunikatu from livewires import games, color games.init(screen_width = 640, screen_height = 480, fps = 50) wall_image = games.load_image("sciana.jpg", transparent = False) games.screen.background = wall_image won_message = games.Message(value = "Wygrałeś!", size = 100, color = color.red, x = games.screen.width/2, y = games.screen.height/2, lifetime = 250, after_death = games.screen.quit) games.screen.add(won_message) games.screen.mainloop()

Import modułu color Obiekty klasy Message podobnie jak obiekty klasy Text mają właściwość color. Aby móc dokonać wyboru z grupy dostępnych kolorów, importuję moduł color, zastępując instrukcję import znajdującą się na początku programu wierszem kodu: from livewires import games, color

Utworzenie obiektu klasy Message Komunikaty są tworzone na podstawie klasy Message modułu games. Komunikat jest specjalnym rodzajem obiektu klasy Text, który dokonuje samozniszczenia po ustalonym czasie. Komunikat może też określać metodę lub funkcję, która ma zostać wykonana po samolikwidacji obiektu. Konstruktor klasy Message przyjmuje wszystkie wartości, z którymi się spotkałeś przy tworzeniu obiektów klasy Text, oraz dwie dodatkowe: lifetime i after_death. Parametr lifetime przyjmuje wartość w postaci liczby całkowitej, która określa czas wyświetlania komunikatu mierzony w cyklach pętli mainloop(). Poprzez parametr after_death można przekazać funkcję lub metodę, która ma zostać wykonana po samolikwidacji obiektu klasy Message. Wartością domyślną parametru after_death jest None, więc przekazanie wartości parametru nie jest wymagane.

Wyświetlanie komunikatu

Tworzę ten obiekt klasy Message tuż przed wywołaniem metody mainloop(): won_message = games.Message(value = "Wygrałeś!", size = 100, color = color.red, x = games.screen.width/2, y = games.screen.height/2, lifetime = 250, after_death = games.screen.quit)

Zostaje w ten sposób utworzony komunikat „Wygrałeś!” wyświetlany dużymi, czerwonymi literami w środku ekranu przez mniej więcej pięć sekund, po czym program kończy działanie. Powyższy kod konkretyzuje nowy obiekt klasy Message z atrybutem lifetime o wartości 250. To oznacza, że obiekt będzie istniał przez jakieś pięć sekund, gdyż pętla mainloop() wykonuje się z prędkością 50 ramek na sekundę. Po pięciu sekundach zostaje wywołana metoda games.screen.quit(), ponieważ właśnie jej nazwę przekazałem poprzez parametr after_death. W tym momencie ekran i wszystkie związane z nim obiekty są niszczone i program kończy pracę.

Wskazówka Pamiętaj, aby jako parametr after_death przekazywać samą nazwę funkcji lub metody, która ma zostać wywołana po zniknięciu obiektu klasy Message. Nie dołączaj do nazwy nawiasów.

Wykorzystanie danych o szerokości i wysokości ekranu Obiekt screen ma właściwość width, która reprezentuje szerokość ekranu graficznego, oraz właściwość height reprezentującą jego wysokość. Czasem klarowniejsze jest użycie tych właściwości zamiast liczb całkowitych w formie literału w celu określenia miejsca na ekranie. Wykorzystuję te właściwości, kiedy przekazuję wartości dotyczące położenia nowego obiektu klasy Message za pomocą przypisań x = games.screen.width/2 i y = games.screen.height/2. Ustawiając wartość współrzędnej x jako połowę szerokości ekranu, a wartość współrzędnej y jako połowę jego wysokości, umieszczam obiekt dokładnie w środku ekranu. Można użyć tej techniki do umieszczenia obiektu w środku ekranu graficznego niezależnie od jego faktycznej szerokości i wysokości.

Dodanie obiektu klasy Message do ekranu W kolejnym wierszu kodu dodaję nowy obiekt do ekranu, aby mógł zostać wyświetlony: games.screen.add(won_message)

341

342

Rozdział 11. Grafika. Gra Pizza Panic

Klasa Message jest podklasą klasy Text. Oznacza to, że klasa Message dziedziczy wszystkie właściwości, atrybuty i metody klasy Text. W tabeli 11.8 zostały wymienione dwa dodatkowe atrybuty klasy Message. Tabela 11.8. Dodatkowe atrybuty klasy Message

Atrybuty

Opis

lifetime

Liczba cykli pętli mainloop(), zanim obiekt ulegnie samolikwidacji. Wartość 0 oznacza, że obiekt ma nie ulec samozniszczeniu. Jest to wartość domyślna.

after_death

Funkcja lub metoda, jaka ma zostać uruchomiona po samolikwidacji obiektu. Wartością domyślną jest None.

Przemieszczanie duszków Ruch obrazów stanowi istotę większości gier — dotyczy to zresztą większości form rozrywki. W przypadku duszków przejście od statyczności do ruchu jest proste. Obiekty klasy Sprite mają właściwości, które umożliwiają im poruszanie się po ekranie graficznym.

Prezentacja programu Pizza w ruchu Ten nowy program jest modyfikacją programu Duszek pizzy. W tym programie pizza porusza się w dół i w prawo. Muszę tylko zmienić kilka wierszy kodu, aby sprawić, by pizza się poruszała. W tym tkwi potęga duszków. Program został przedstawiony na rysunku 11.11.

Rysunek 11.11. Pizza porusza się w prawo, w dół, w kierunku wskazywanym przez strzałkę

Przemieszczanie duszków

Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 11.; nazwa pliku to pizza_w_ruchu.py. # Pizza w ruchu # Demonstruje określanie parametrów prędkości ruchu duszka from livewires import games games.init(screen_width = 640, screen_height = 480, fps = 50) wall_image = games.load_image("sciana.jpg", transparent = False) games.screen.background = wall_image pizza_image = games.load_image("pizza.bmp") the_pizza = games.Sprite(image = pizza_image, x = games.screen.width/2, y = games.screen.height/2, dx = 1, dy = 1) games.screen.add(the_pizza) games.screen.mainloop()

Ustawienie wartości prędkości ruchu pizzy Wystarczy, że zmodyfikuję kod, który tworzy nowego duszka, przez dostarczenie dodatkowych wartości parametrów dx i dy do konstruktora: the_pizza = games.Sprite(image = pizza_image, x = games.screen.width/2, y = games.screen.height/2, dx = 1, dy = 1)

Każdy obiekt oparty na klasie Sprite ma właściwości dx i dy określające prędkość obiektu odpowiednio w kierunku osi x i osi y. (Można tu dodać, że litera „d” reprezentuję symbol „delta” oznaczający zmianę). Tak więc dx to zmiana współrzędnej x obiektu, a dy to zmiana współrzędnej y obiektu po każdej kolejnej aktualizacji ekranu (obiektu screen) przez mainloop(). Dodatnia wartość dx powoduje przesunięcie duszka w prawo, a wartość ujemna przesuwa go w lewo. Dodatnia wartość dy powoduje przesunięcie duszka w dół, a wartość ujemna — w górę Kiedy wrócimy do programu Duszek pizzy, okaże się, że nie przekazałem żadnych wartości do parametrów dx i dy. Mimo że duszek w tamtym programie zawierał właściwości dx i dy, obie miały wartość domyślną równą 0. Ponieważ przekazuję 1 do parametru dx oraz 1 do parametru dy, za każdym razem, gdy okno jest aktualizowane przez mainloop(), zarówno współrzędna x pizzy, jak i jej współrzędna y są zwiększane o 1, co powoduje przesunięcie pizzy w prawo w dół.

343

344

Rozdział 11. Grafika. Gra Pizza Panic

Radzenie sobie z granicami ekranu Obserwując działanie programu Pizza w ruchu odpowiednio długo, możesz zauważyć, że kiedy pizza dociera do granicy ekranu, nie zaprzestaje swojego ruchu. Tak naprawdę znika z pola widzenia. Zawsze, gdy wprowadzasz duszka w ruch, powinieneś stworzyć mechanizm obsługujący granice okna graficznego. Masz kilka możliwości wyboru. Ruch duszka mógłby być po prostu zatrzymany po osiągnięciu brzegu ekranu. Duszek mógłby stracić życie, powiedzmy, na skutek gwałtownej eksplozji. Mógłby odbić się od brzegu jak olbrzymia gumowa piłka. Mógłby nawet przewinąć się wokół ekranu, znikając na jednym jego brzegu i pojawiając się ponownie po przeciwnej stronie. Co wydaje się najsensowniejsze w przypadku pizzy? Oczywiście odbicie się.

Program Sprężysta pizza Mówiąc, że duszek „odbija się” od brzegów okna graficznego, mam na myśli to, że kiedy dotrze do granicy ekranu, składowa jego prędkości, która powodowała ruch w kierunku tej granicy, powinna zmienić swoją wartość na przeciwną. Więc jeśli sprężysta pizza dotrze do górnego lub dolnego brzegu ekranu, powinna zmienić wartość swojej właściwości dy na przeciwną. Kiedy dotrze do jednego z boków ekranu, na przeciwną powinna zmienić się wartość właściwości dx obiektu. Działanie programu Sprężysta pizza zostało zilustrowane na rysunku 11.12.

Rysunek 11.12. Chociaż nie możesz tego stwierdzić na podstawie samego zrzutu ekranu, pizza odbija się od brzegów ekranu i porusza się wzdłuż ścieżki wskazanej przez linię ze strzałką

Radzenie sobie z granicami ekranu

Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 11.; nazwa pliku to sprezysta_pizza.py.

Rozpoczęcie programu Zaczynam tak jak w przypadku dowolnego innego programu graficznego: # Sprężysta pizza # Demonstruje postępowanie po osiągnięciu granic ekranu from livewires import games games.init(screen_width = 640, screen_height = 480, fps = 50)

Jak poprzednio, w powyższych wierszach kodu uzyskuję dostęp do modułu games oraz tworzę ekran graficzny.

Utworzenie nowej klasy pochodnej na bazie klasy Sprite Po raz pierwszy chcę, aby duszek robił coś, do czego nie został zaprogramowany — odbijał się od brzegów ekranu. Więc muszę na bazie klasy Sprite utworzyć nową klasę. Ponieważ potrzebuję odbijającej się pizzy, nadaję nowej klasie nazwę Pizza: class Pizza(games.Sprite): """ Sprężysta pizza. """

Przesłonięcie metody update() Aby przekształcić ruchomą pizzę w taką, która odbija się od brzegów ekranu, muszę dodać tylko jedną metodę. Za każdym razem, gdy okno graficzne jest aktualizowane przez mainloop(), zachodzą następujące dwa zdarzenia:  położenie każdego duszka zostaje zaktualizowane na podstawie jego

właściwości dx i dy,  wywoływana jest metoda update() każdego duszka.

Każdy obiekt klasy Sprite ma metodę update(); domyślnie nie robi ona niczego. Więc poprzez przesłonięcie tej metody w klasie Pizza uzyskuję doskonałe miejsce do wstawienia kodu, który obsłuży sprawdzanie granic ekranu. def update(self): """ Po osiągnięciu brzegu ekranu zmień wartość składowej prędkości na przeciwną. """ if self.right > games.screen.width or self.left < 0: self.dx = -self.dx if self.bottom > games.screen.height or self.top < 0: self.dy = -self.dy

345

346

Rozdział 11. Grafika. Gra Pizza Panic

W metodzie update() sprawdzam, czy duszek nie wychodzi w żadnym kierunku poza granice ekranu. Jeżeli tak się dzieje, zmieniam wartość odpowiedzialnej za to składowej prędkości na przeciwną. Jeżeli właściwość right obiektu, która reprezentuje współrzędną x jego prawego brzegu, jest większa niż games.screen.width, pizza ma właśnie przekroczyć prawą krawędź ekranu i zapaść się w nicość. Jeśli zaś właściwość left obiektu, która reprezentuje współrzędną x jego lewego brzegu, jest mniejsza niż 0, pizza opuszcza ekran z jego lewej strony. W każdym przypadku po prostu zmieniam na przeciwną wartość dx poziomej prędkości pizzy, aby spowodować jej „odbicie się” od granicy ekranu. Jeśli właściwość bottom obiektu, która reprezentuje współrzędną y jego dolnego brzegu, jest większa niż games.screen.height, pizza znajduje się na granicy przekroczenia dolnej krawędzi ekranu i zniknięcia. Jeśli zaś właściwość top obiektu, która reprezentuje współrzędną y jego lewego górnego brzegu, jest mniejsza niż 0, pizza opuszcza ekran po przekroczeniu jego górnej krawędzi. W każdym przypadku po prostu zmieniam na przeciwną wartość dy pionowej składowej prędkości pizzy, aby spowodować jej „odbicie się” od granicy ekranu.

Dokończenie programu Ponieważ definiuję w programie klasę, pomyślałem, że zorganizuję pozostałą część kodu w funkcję: def main(): wall_image = games.load_image("sciana.jpg", transparent = False) games.screen.background = wall_image pizza_image = games.load_image("pizza.bmp") the_pizza = Pizza(image = pizza_image, x = games.screen.width/2, y = games.screen.height/2, dx = 1, dy = 1) games.screen.add(the_pizza) games.screen.mainloop() # wystartuj! main()

Gros tego kodu już przedtem widziałeś. Jedyna ważna różnica polega na tym, że utworzyłem obiekt na bazie mojej nowej klasy Pizza, zamiast posłużyć się klasą Sprite. Dzięki temu metoda update() obiektu sprawdza przekroczenie granic ekranu i odwraca kierunek prędkości, aby w razie potrzeby zapewnić odbicie się pizzy, która ma być sprężysta!

Obsługa danych wejściowych z myszy

Obsługa danych wejściowych z myszy Chociaż widziałeś dużo z tego, co ma do zaoferowania pakiet livewires, nie poznałeś jeszcze głównego składnika interaktywności — obsługi danych wejściowych użytkownika. Jednym z najczęściej występujących sposobów uzyskiwania danych wejściowych od użytkownika jest wykorzystanie w tym celu myszy. Pakiet livewires oferuje obiekt, który pozwala to zrealizować.

Prezentacja programu Patelnia w ruchu Obiekt mouse z modułu games daje Ci dostęp do myszy. Obiekt ma właściwości, które sprawiają, że odczytywanie położenia myszy na ekranie graficznym staje się bułką z masłem. Wykorzystując te właściwości, tworzę program Patelnia w ruchu, umożliwiający użytkownikowi przeciąganie duszka patelni w poprzek ekranu poprzez wykonywanie ruchów myszą. Efekt działania tego programu został pokazany na rysunku 11.13.

Rysunek 11.13. Duszek patelni wędruje po całym ekranie graficznym, podążając za ruchem myszy

Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 11.; nazwa pliku to patelnia_w_ruchu.py.

347

348

Rozdział 11. Grafika. Gra Pizza Panic

Rozpoczęcie programu Poniższy kod powinien wyglądać wybitnie znajomo: # Patelnia w ruchu # Demonstruje obsługę danych wejściowych z myszy from livewires import games games.init(screen_width = 640, screen_height = 480, fps = 50)

Tak jak przedtem, importuję moduł games i inicjalizuję ekran graficzny. Funkcja init() tworzy również obiekt mouse, którego użyję do odczytywania pozycji myszy.

Odczytywanie współrzędnych x i y myszy Następnie tworzę klasę Pan reprezentującą duszka patelni: class Pan(games.Sprite): """" Patelnia sterowana myszą. """ def update(self): """ Przejdź do współrzędnych myszy. """ self.x = games.mouse.x self.y = games.mouse.y

Podobnie jak obiekt klasy Sprite, obiekt mouse posiada właściwość x, reprezentującą jego współrzędną x, oraz właściwość y, reprezentującą jego współrzędną y. Za ich pomocą mogę odczytać aktualne położenie myszy na ekranie graficznym. W metodzie update() przypisuję właściwości x obiektu klasy Pan wartość właściwości x obiektu mouse. To powoduje przesunięcie patelni do aktualnego położenia wskaźnika myszy. Następnie piszę funkcję main() zawierającą typ kodu, z jakim już wcześniej się spotkałeś, która ustawia obraz tła i tworzy obiekty duszków: def main(): wall_image = games.load_image("sciana.jpg", transparent = False) games.screen.background = wall_image pan_image = games.load_image("patelnia.bmp") the_pan = Pan(image = pan_image, x = games.mouse.x, y = games.mouse.y) games.screen.add(the_pan)

Dzięki przekazaniu wartości games.mouse.x do parametru x oraz wartości games.mouse.y do parametru y początkowe położenie obiektu klasy Pan odpowiada współrzędnym myszy.

Obsługa danych wejściowych z myszy

Ustawienie widoczności wskaźnika myszy Następnie wykorzystuję w funkcji main()właściwość is_visible obiektu mouse do ustawienia widoczności wskaźnika myszy. games.mouse.is_visible = False

Ustawienie tej właściwości na True oznacza, że wskaźnik myszy będzie widoczny, natomiast ustawienie jej na False oznacza, że wskaźnik myszy będzie niewidoczny. Ponieważ nie chcę widzieć wskaźnika na wierzchu obrazka patelni, nadaję tej właściwości wartość False.

Przechwytywanie sygnałów wejściowych przez okno graficzne Następnie w funkcji main() wykorzystuję właściwość event_grab obiektu screen, aby wszystkie sygnały wejściowe były przechwytywane przez ekran graficzny: games.screen.even_grab = True

Ustawienie tej właściwości na True oznacza, że obsługa wszystkich sygnałów wejściowych zostanie skoncentrowana w ekranie graficznym. Korzyść z tego polega na tym, że mysz wraz z wszystkimi zdarzeniami nie opuści okna graficznego. Ustawienie tej właściwości na False oznacza, że kierowanie wszystkich sygnałów wejściowych do ekranu graficznego nie ma zastosowania i wskaźnik myszy może opuścić okno graficzne.

Wskazówka Jeśli przechwytujesz wszystkie sygnały wejściowe, kierując je do ekranu graficznego, nie będziesz mógł zamknąć okna graficznego za pomocą myszy. Zawsze jednak możesz zamknąć okno przez naciśnięcie klawisza Esc.

Dokończenie programu Wreszcie kończę funkcję main() tak jak przedtem i wywołuję metodę mainloop(), aby zapewnić aktualizowanie całej zawartości ekranu. games.screen.mainloop() # wystartuj! main()

Podsumowanie kilku użytecznych właściwości obiektu mouse znajdziesz w tabeli 11.9.

349

350

Rozdział 11. Grafika. Gra Pizza Panic Tabela 11.9. Przydatne właściwości obiektu mouse

Właściwość

Opis

x

Współrzędna x wskaźnika myszy.

y

Współrzędna y wskaźnika myszy.

is_visible

Wartość typu Boolean służąca do ustawienia widoczności wskaźnika myszy. True oznacza widoczność wskaźnika, False — brak widoczności. Wartością domyślną jest True.

Wykrywanie kolizji W większości gier, gdy zderzają się dwie rzeczy, efekt tego jest wyraźny. Może to być tak prosty przypadek jak zderzenie się postaci dwuwymiarowej z granicą, której nie może przekroczyć, lub tak spektakularny jak rozgrywająca się w trzech wymiarach scena, w której asteroida przebija kadłub ogromnego statku bazy. Tak czy owak istnieje potrzeba wykrycia momentu kolizji obiektów.

Prezentacja programu Nieuchwytna pizza Program Nieuchwytna pizza stanowi rozszerzenie programu Patelnia w ruchu. W programie Nieuchwytna pizza użytkownik steruje patelnią za pomocą myszy, tak jak w programie Patelnia w ruchu. Tym razem jednak na ekranie pojawia się duszek pizzy. Użytkownik może przesuwać patelnię w kierunku pizzy, ale jeśli tylko uda mu się jej dosięgnąć, wyślizgująca się pizza przenosi się w nowe, przypadkowo wybrane miejsce ekranu. Na rysunkach 11.14 i 11.15 przedstawiłem program w działaniu. Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 11.; nazwa pliku to nieuchwytna_pizza.py.

Rozpoczęcie programu Początkowy fragment kodu został wzięty z programu Patelnia w ruchu z jednym małym dodatkiem: # Nieuchwytna pizza # Demonstruje wykrywanie kolizji duszków from livewires import games import random games.init(screen_width = 640, screen_height = 480, fps = 50)

Jedyną nową czynnością, jaką wykonuję, jest import naszego starego znajomego — modułu random. Pozwoli mi to na wygenerowanie nowego, przypadkowego położenia duszka pizzy po kolizji.

Wykrywanie kolizji

Rysunek 11.14. Gracz prawie dosięga pizzy

Rysunek 11.15. Nieuchwytnej pizzy znowu udaje się wymknąć

351

352

Rozdział 11. Grafika. Gra Pizza Panic

Wykrywanie kolizji Tworzę nową klasę Pan poprzez dodanie niewielkiej ilości kodu służącego do wykrywania kolizji: class Pan(games.Sprite): """" Patelnia sterowana za pomocą myszy. """ def update(self): """ Przesuń do pozycji myszy. """ self.x = games.mouse.x self.y = games.mouse.y self.check_collide() def check_collide(self): """ Sprawdź, czy nie doszło do kolizji z pizzą. """ for pizza in self.overlapping_sprites: pizza.handle_collide()

W ostatnim wierszu metody update() wywołuję metodę check_collide() klasy Pan. Metoda check_collide() przetwarza w pętli listę wszystkich obiektów zachodzących na obiekt klasy Pan, dostępną poprzez właściwość overlapping_sprites tego ostatniego obiektu. W każdym obiekcie, którego obraz zachodzi na obraz patelni, wywoływana jest jego własna metoda handle_collide(). Zasadniczo obiekt patelni zleca każdemu z obiektów, które na niego zachodzą, obsługę wynikającej stąd kolizji.

Obsługa kolizji Następnie tworzę nową klasę o nazwie Pizza: class Pizza(games.Sprite): """" Nieuchwytna pizza. """ def handle_collide(self): """ Przemieść się w przypadkowe miejsce ekranu. """ self.x = random.randrange(games.screen.width) self.y = random.randrange(games.screen.height)

Piszę tylko jedną metodę, handle_collide(), która generuje losowe współrzędne punktu na ekranie, i przenosi obiekt klasy Pizza w to nowe miejsce.

Dokończenie programu A oto funkcja main(): def main(): wall_image = games.load_image("sciana.jpg", transparent = False) games.screen.background = wall_image pizza_image = games.load_image("pizza.bmp") pizza_x = random.randrange(games.screen.width) pizza_y = random.randrange(games.screen.height) the_pizza = Pizza(image = pizza_image, x = pizza_x, y = pizza_y)

Powrót do gry Pizza Panic games.screen.add(the_pizza) pan_image = games.load_image("patelnia.bmp") the_pan = Pan(image = pan_image, x = games.mouse.x, y = games.mouse.y) games.screen.add(the_pan) games.mouse.is_visible = False games.screen.event_grab = True games.screen.mainloop() # wystartuj! main()

Jak zawsze ustawiam obraz tła. Następnie tworzę dwa obiekty: obiekt klasy Pizza oraz obiekt klasy Pan. Generuję losową parę współrzędnych ekranowych dla pizzy i umieszczam patelnię w miejscu wyznaczonym przez współrzędne myszy. Ustawiam wskaźnik myszy tak, aby był niewidoczny, i konfiguruję przechwytywanie wszystkich sygnałów wejściowych przez okno gry. Potem wywołuję metodę mainloop(). W końcu wszystko uruchamiam poprzez wywołanie funkcji main().

Powrót do gry Pizza Panic Teraz, kiedy masz już wyobrażenie o tym, czego może dokonać multimedialny pakiet livewires, nadszedł czas na stworzenie gry Pizza Panic przedstawionej na początku rozdziału. Duża część kodu gry może być bezpośrednio przeniesiona z programów przykładowych. Wprowadzę również jednak kilka nowych koncepcji. Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 11.; nazwa pliku to pizza_panic.py.

Rozpoczęcie programu Tak jak we wszystkich programach w tym rozdziale, zaczynam od importu modułów oraz inicjalizacji ekranu graficznego: # Pizza Panic # Gracz musi złapać lecące w dół pizze, zanim spadną na ziemię from livewires import games, color import random games.init(screen_width = 640, screen_height = 480, fps = 50)

Aby móc tworzyć jakąś grafikę, muszę zaimportować moduł games, podczas gdy moduł color daje mi dostęp do palety predefiniowanych kolorów. Importuję moduł

353

354

Rozdział 11. Grafika. Gra Pizza Panic random, aby oszalały kucharz bardziej przypominał w swoim dokonywaniu wyborów żywą osobę. W końcu wywołuję funkcję init() modułu games w celu inicjalizacji ekranu graficznego i uzyskania dostępu do obiektu mouse modułu games.

Klasa Pan Klasa Pan to projekt duszka patelni, którym gracz steruje za pomocą myszy. Patelnia może się jednak poruszać tylko w prawo lub w lewo. Omówię całą klasę po kawałku.

Załadowanie obrazu patelni Na początku tworzenia tej klasy robię coś trochę odmiennego — ładuję obraz duszka i przypisuję go do zmiennej klasy o nazwie image. Robię tak, ponieważ gra Pizza Panic zawiera kilka klas, a załadowanie obrazu w definicji odpowiadającej mu klasy jest bardziej przejrzyste niż załadowanie wszystkich obrazów w funkcji main() programu. class Pan(games.Sprite): """ Patelnia sterowana przez gracza służąca do łapania spadających pizz. """ image = games.load_image("patelnia.bmp")

Metoda __init__() Następnie piszę kod konstruktora służącego do inicjalizacji nowego obiektu klasy Pan: def __init__(self): """ Initialize Pan object and create Text object for score. """ super(Pan, self).__init__(image = Pan.image, x = games.mouse.x, bottom = games.screen.height) self.score = games.Text(value = 0, size = 25, color = color.black, top = 5, right = games.screen.width - 10) games.screen.add(self.score)

Używam funkcji super(), aby zapewnić sobie wywołanie metody __init__() klasy Sprite. Następnie definiuję atrybut score — obiekt klasy Text reprezentujący wynik gracza, którego wartością początkową jest 0. Oczywiście pamiętam, aby dodać nowy obiekt klasy Text do ekranu, by mógł być wyświetlony.

Metoda update() Ta metoda obsługuje ruch patelni gracza: def update(self): """ Zmień pozycję na wyznaczoną przez współrzędną x myszy. """ self.x = games.mouse.x if self.left < 0: self.left = 0

Powrót do gry Pizza Panic if self.right > games.screen.width: self.right = games.screen.width self.check_catch()

Metoda przypisuje współrzędną x myszy do współrzędnej x obiektu klasy Pan, umożliwiając graczowi przesuwanie patelni w lewo i w prawo za pomocą myszy. Następnie wykorzystuję właściwość left obiektu, aby sprawdzić, czy współrzędna x jego lewego brzegu jest mniejsza od 0, co oznaczałoby, że część patelni znajduje się poza lewą krawędzią okna graficznego. Jeśli tak jest w istocie, ustawiam współrzędną x lewego brzegu (właściwość left obiektu) na 0, aby patelnia została wyświetlona przy lewej krawędzi okna. Potem wykorzystuję właściwość right obiektu, aby sprawdzić, czy współrzędna x jego prawego brzegu jest większa od wartości games.screen.width, co oznaczałoby, że część patelni znajduje się poza prawą krawędzią okna graficznego. Jeśli tak się dzieje, przypisuję współrzędnej x prawego brzegu (właściwości right obiektu) wartość games.screen.width, aby patelnia została wyświetlona przy prawej krawędzi okna. Na koniec wywołuję metodę check_catch() obiektu.

Metoda check_catch() Metoda ta sprawdza, czy gracz złapał jakieś spadające pizze: def check_catch(self): """ Sprawdź, czy nie zostały złapane jakieś pizze. """ for pizza in self.overlapping_sprites: self.score.value += 10 self.score.right = games.screen.width - 10 pizza.handle_caught()

Dla każdego obiektu, który zachodzi na patelnię, metoda zwiększa liczbę punktów uzyskanych przez gracza o 10. Potem zapewnia, że prawy brzeg obiektu klasy Text reprezentującego wynik jest zawsze oddalony o 10 pikseli od prawej krawędzi ekranu, bez względu na to, ile cyfr ten wynik zawiera. Na koniec omawiana metoda wywołuje metodę handle_caught() zachodzącego na patelnię duszka.

Klasa Pizza Ta klasa reprezentuje spadające pizze, które gracz musi łapać: class Pizza(games.Sprite): """ Pizza, która spada na ziemię. """ image = games.load_image("pizza.bmp") speed = 1

Definiuję dwie zmienne klasy: image, reprezentującą obraz pizzy, oraz speed, reprezentującą szybkość spadania pizzy. Ustawiam wartość zmiennej speed na 1,

355

356

Rozdział 11. Grafika. Gra Pizza Panic

aby pizze spadały w dość wolnym tempie. Obydwu zmiennych klasy używam w konstruktorze klasy Pizza, o czym wkrótce się przekonasz.

Metoda __init__() Ta metoda inicjalizuje nowy obiekt klasy Pizza: def __init__(self, x, y = 90): """ Inicjalizuj obiekt klasy Pizza. """ super(Pizza, self).__init__(image = Pizza.image, x = x, y = y, dy = Pizza.speed)

Wszystko, co robię w tej metodzie, to wywołanie konstruktora klasy nadrzędnej klasy Pizza. Zwróć uwagę, że ustawiam wartość domyślną parametru y na 90, co lokuje każdą

nową pizzę dokładnie na wysokości klatki piersiowej kucharza.

Metoda update() Ta metoda obsługuje sprawdzanie, czy obiekt nie dotarł do granicy ekranu: def update(self): """ Sprawdź, czy dolny brzeg pizzy dosięgnął dołu ekranu. """ if self.bottom > games.screen.height: self.end_game() self.destroy()

Cała praca tej metody polega na sprawdzeniu, czy pizza dotarła do dolnej krawędzi ekranu. Jeśli okazuje się, że tak jest w istocie, omawiana metoda wywołuje metodę end_game obiektu, a następnie obiekt sam się usuwa z ekranu.

Metoda handle_caught() Warto przypomnieć, że ta metoda jest wywoływana przez obiekt klasy Pan wtedy, gdy zderza się z nim obiekt klasy Pizza: def handle_caught(self): """ Destroy self if caught. """ self.destroy()

Kiedy pizza zderza się z patelnią, uważa się, że pizza została „złapana” i po prostu przestaje istnieć. Więc obiekt klasy Pizza wywołuje swoją własną metodę destroy() i pizza dosłownie znika.

Metoda end_game() Ta metoda kończy grę. Jest wywoływana, kiedy pizza dociera do dolnej krawędzi ekranu. def end_game(self): """ Zakończ grę. """ end_message = games.Message(value = "Koniec gry", size = 90, color = color.red, x = games.screen.width/2,

Powrót do gry Pizza Panic y = games.screen.height/2, lifetime = 5 * games.screen.fps, after_death = games.screen.quit) games.screen.add(end_message)

Powyższy kod tworzy obiekt klasy Message, który oznajmia koniec gry. Po mniej więcej pięciu sekundach komunikat znika i okno graficzne się zamyka, kończąc grę.

Pułapka Metoda end_game() jest wywoływana zawsze, gdy pizza dociera do dolnej krawędzi ekranu. Ponieważ jednak komunikat „Koniec gry” jest wyświetlany przez mniej więcej pięć sekund, istnieje możliwość, że inna pizza dotrze do dolnej krawędzi ekranu, zanim okno graficzne zdąży się zamknąć, powodując w ten sposób zwielokrotnienie komunikatów „Koniec gry”. W rozdziale 12. zobaczysz, jak utworzyć obiekt reprezentujący samą grę, który mógłby śledzić, czy gra się skończyła, czy też nie, i zapobiegać czemuś takiemu jak wielokrotne tworzenie komunikatów „Koniec gry”.

Klasa Chef Klasa Chef jest wykorzystywana do utworzenia szalonego szefa kuchni, który zrzuca pizze z dachu restauracji. class Chef(games.Sprite): """ Szef kuchni, który porusza się w lewo i w prawo, zrzucając pizze. """ image = games.load_image("kucharz.bmp")

Definiuję atrybut klasy, image, reprezentujący obraz kucharza.

Metoda __init__() Oto kod konstruktora: def __init__(self, y = 55, speed = 2, odds_change = 200): """ Initialize the Chef object. """ super(Chef, self).__init__(image = Chef.image, x = games.screen.width / 2, y = y, dx = speed) self.odds_change = odds_change self.time_til_drop = 0

Najpierw wywołuję konstruktora klasy nadrzędnej klasy Chef. Jako parametr image przekazuję atrybut klasy Chef.image. Jako x przekazuję wartość, która ustawia kucharza w samym środku ekranu. W przypadku parametru y wartość domyślna 55 sytuuje szefa kuchni dokładnie na szczycie ceglanej ściany. Jako parametr dx zostaje przekazana wartość speed, która ustala poziomą prędkość kucharza, gdy ten się przemieszcza wzdłuż dachu. Wartość domyślna wynosi 2.

357

358

Rozdział 11. Grafika. Gra Pizza Panic

Metoda tworzy również dwa atrybuty obiektu: odds_change i time_til_drop. Atrybut odds_change jest liczbą całkowitą, która reprezentuje prawdopodobieństwo, że kucharz zmieni kierunek swojego ruchu. Jeśli na przykład wartość odds_change wynosi 200, istnieje przy każdym ruchu kucharza 1 szansa na 200, że zmieni się jego kierunek. Zobaczysz, jak to działa w metodzie update() tej klasy. Atrybut time_til_drop jest liczbą całkowitą, która reprezentuje ilość czasu wyrażoną w cyklach pętli mainloop(), jaka pozostaje do zrzucenia przez kucharza następnej pizzy. Nadaję mu na początku wartość 0, co oznacza, że gdy tylko obiekt kucharza zostanie powołany do życia, powinien od razu zrzucić pizzę. Jak funkcjonuje atrybut time_til_drop, zobaczysz w metodzie check_drop().

Metoda update() Ta metoda definiuje reguły, które określają, w jaki sposób kucharz decyduje o przesuwaniu się w jedną lub w drugą stronę wzdłuż krawędzi dachu: def update(self): """ Ustal, czy kierunek ruchu musi zostać zmieniony na przeciwny. """ if self.left < 0 or self.right > games.screen.width: self.dx = -self.dx elif random.randrange(self.odds_change) == 0: self.dx = -self.dx self.check_drop()

Kucharz przesuwa się wzdłuż krawędzi dachu w jednym kierunku, dopóki albo nie dotrze do brzegu ekranu, albo nie „zadecyduje” w sposób losowy o zmianie kierunku. Na początku tej metody sprawdzam, czy szef kuchni nie przesunął się poza lewą lub prawą krawędź okna graficznego. Jeśli tak się stało, kierunek ruchu kucharza zostaje odwrócony za pomocą kodu self.dx = -self.dx. W przeciwnym wypadku kucharz ma jedną szansę zmiany kierunku ruchu na taką liczbę możliwości, jaką ustala atrybut odd_change. Niezależnie od tego, czy kucharz zmienia kierunek ruchu, czy też nie, ostatnią czynnością wykonywaną przez omawianą metodę jest wywołanie metody check_drop() obiektu klasy Chef.

Metoda check_drop() Ta metoda jest wywoływana w każdym cyklu pętli mainloop(), ale to nie oznacza, że 50 razy na sekundę jest zrzucana nowa pizza! def check_drop(self): """ Zmniejsz licznik odliczający czas lub zrzuć pizzę i zresetuj odliczanie. """ if self.time_til_drop > 0: self.time_til_drop -= 1 else: new_pizza = Pizza(x = self.x) games.screen.add(new_pizza)

Powrót do gry Pizza Panic # ustaw margines na mniej więcej 30% wysokości pizzy, niezależnie od prędkości pizzy self.time_til_drop = int(new_pizza.height * 1.3 / Pizza.speed) + 1

Atrybut time_til_drop reprezentuje mechanizm odliczania wykorzystywany przez naszego kucharza. Jeśli wartość time_til_drop jest większa od 0, zostaje od niej odjęta liczba 1. W przeciwnym wypadku tworzony jest nowy obiekt klasy Pizza, a wartość atrybutu time_til_drop zostaje przywrócona do stanu początkowego. Tworząc nową pizzę, przekazuje do konstruktora klasy Pizza współrzędną x obiektu klasy Chef, żeby nowa pizza pojawiała się na ekranie w tym samym miejscu co duszek kucharza. Nowa wartość atrybutu time_til_drop jest obliczana w taki sposób, aby nowa pizza była zrzucana wtedy, gdy jej odległość od poprzedniej wyniesie około 30% wysokości pizzy, niezależnie od tego, z jaką szybkością pizze spadają.

Funkcja main() Funkcja main() tworzy obiekty i uruchamia grę: def main(): """ Uruchom grę. """ wall_image = games.load_image("sciana.jpg", transparent = False) games.screen.background = wall_image the_chef = Chef() games.screen.add(the_chef) the_pan = Pan() games.screen.add(the_pan) games.mouse.is_visible = False games.screen.event_grab = True games.screen.mainloop() # wystartuj! main()

Najpierw ustawiam ceglaną ścianę jako tło. Tworzę kucharza i patelnię. Następnie ustawiam niewidoczność wskaźnika myszy i przechwytywanie wszystkich sygnałów wejściowych, które sprawia, że wskaźnik myszy nie może opuścić okna graficznego. Wywołuję metodę mainloop() w celu rozpoczęcia gry. W końcu wywołuję funkcję main(), aby to wszystko uruchomić!

359

360

Rozdział 11. Grafika. Gra Pizza Panic

Podsumowanie W tym rozdziale zobaczyłeś, jak można wykorzystać multimedialny pakiet livewires, aby do programów dodać grafikę. Dowiedziałeś się, jak utworzyć nowe okno graficzne i jak ustawić dla tego okna obraz tła. Zobaczyłeś, jak można wyświetlać tekst w oknie graficznym. Poznałeś duszka, specjalny obiekt graficzny z obrazem. W szczególności zobaczyłeś, jak można umiejscawiać i zmieniać położenie duszka na ekranie graficznym. Zobaczyłeś również, jak można sprawdzać, czy między duszkami występują kolizje. Dowiedziałeś się, jak pobierać dane wejściowe z myszy. Na koniec zobaczyłeś, jak to wszystko można poskładać w jedną całość w postaci szybkiej gry wideo z udziałem sterowanego przez komputer przeciwnika.

Sprawdź swoje umiejętności 1. Ulepsz grę Pizza Panic poprzez zwiększanie jej trudności w miarę postępu gry. Pomyśl o różnych sposobach osiągnięcia tego celu. Mógłbyś zwiększać prędkość pizz oraz prędkość ruchu kucharza. Mógłbyś podnieść poziom, na którym porusza się patelnia. Mógłbyś nawet zwiększyć liczbę szalonych kucharzy ciskających pizze. 2. Napisz grę, w której gracz steruje postacią, która musi unikać spadających szczątków. Gracz steruje tą postacią za pomocą myszy, a obiekty spadają z nieba. 3. Utwórz prostą grę Pong dla jednego gracza, w której gracz steruje paletką, a piłeczka odbija się od trzech ścian. Jeśli piłeczka przejdzie obok paletki gracza, następuje koniec gry.

12 Dźwięk, animacja i rozwijanie programu. Gra Astrocrash W

tym rozdziale poszerzysz swoje umiejętności tworzenia programów multimedialnych o obsługę dźwięku i ruchomych obrazów. Zobaczysz również, jak można pisać duży program etapami. W szczególności nauczysz się:  odczytywać dane z klawiatury,  odtwarzać pliki dźwiękowe,  odtwarzać pliki muzyczne,  tworzyć animacje,  rozwijać program poprzez pisanie coraz bardziej kompletnych jego wersji.

Wprowadzenie do gry Astrocrash Projekt przedstawiony w tym rozdziale, gra Astrocrash, to moja wersja klasycznej gry arkadowej Asteroids. W grze Astrocrash gracz steruje statkiem kosmicznym w ruchomym polu śmiertelnie groźnych asteroidów. Statek może się obracać i wykonywać ruch do przodu oraz, co najważniejsze, może wystrzeliwać pociski w kierunku asteroidów, aby je zniszczyć. Gracz ma jednak trochę roboty do wykonania, ponieważ asteroidy dużego i średniego rozmiaru rozpadają się na dwie mniejsze asteroidy, gdy zostaną zniszczone. I w tej samej chwili, gdy graczowi uda się unicestwić wszystkie asteroidy, pojawia się ich nowa, większa fala. Liczba punktów uzyskanych przez gracza zwiększa się wraz z każdą zniszczoną asteroidą, lecz jeśli tylko statek gracza zderzy się z unoszącą się w przestrzeni kosmiczną skałą, gra się kończy. Rysunki 12.1 i 12.2 pokazują tę grę w toku.

362

Rozdział 12. Dźwięk, animacja i rozwijanie programu. Gra Astrocrash

Rysunek 12.1. Gracz steruje statkiem kosmicznym i niszczy asteroidy w celu zwiększenia swojego wyniku punktowego. (Obraz mgławicy należy do domeny publicznej. Dzięki uprzejmości: NASA, The Hubble Heritage Team–AURA/STScl)

Rysunek 12.2. Jeśli asteroida uderzy w statek gracza, gra się kończy

Odczyt klawiatury

Odczyt klawiatury Już wiesz, jak pobierać łańcuchy znaków od użytkownika przy użyciu funkcji input(), ale odczyt klawiatury w celu identyfikacji pojedynczych naciśnięć klawiszy to inna kwestia. Na szczęście istnieje nowy obiekt z modułu games, który to właśnie umożliwia.

Prezentacja programu Odczytaj klawisz Program Odczytaj klawisz wyświetla statek kosmiczny na tle mgławicy. Użytkownik może przemieszczać statek po całym ekranie za pomocą naciśnięć różnych klawiszy. Kiedy użytkownik naciska klawisz W, statek porusza się do góry. Gdy użytkownik naciska klawisz S, statek porusza się w dół. Kiedy użytkownik naciska klawisz A, statek porusza się w lewo. Kiedy użytkownik naciska klawisz D, statek porusza się w prawo. Użytkownik może również nacisnąć wiele klawiszy jednocześnie dla uzyskania łącznego efektu. Na przykład, gdy użytkownik naciska jednocześnie klawisze W i D, statek porusza się po przekątnej — w prawo, do góry. Program został zilustrowany na rysunku 12.3.

Rysunek 12.3. Statek porusza się po ekranie dzięki naciśnięciom klawiszy

Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 12.; nazwa pliku to odczytaj_klawisz.py.

363

364

Rozdział 12. Dźwięk, animacja i rozwijanie programu. Gra Astrocrash

Rozpoczęcie programu Tak jak w przypadku wszystkich programów wykorzystujących pakiet livewires rozpoczynam od importu potrzebnych modułów i wywołania funkcji inicjalizującej ekran graficzny: # Odczytaj klawisz # Demonstruje odczytywanie klawiatury from livewires import games games.init(screen_width = 640, screen_height = 480, fps = 50)

Testowanie stanu klawiszy Następnie piszę kod klasy reprezentującej statek kosmiczny. W metodzie update() sprawdzam, czy zostały naciśnięte określone klawisze, i zgodnie z wynikiem tych testów zmieniam pozycję statku. class Ship(games.Sprite): """ Poruszający się statek kosmiczny. """ def update(self): """ Kieruj ruchem statku na podstawie wciśniętych klawiszy. """ if games.keyboard.is_pressed(games.K_w): self.y -= 1 if games.keyboard.is_pressed(games.K_s): self.y += 1 if games.keyboard.is_pressed(games.K_a): self.x -= 1 if games.keyboard.is_pressed(games.K_d): self.x += 1

Wykorzystuję nowy obiekt z modułu games o nazwie keyboard. Możesz użyć tego obiektu do sprawdzenia, czy określone klawisze zostały naciśnięte. Wywołuję metodę is_pressed() obiektu, która zwraca wartość True, jeśli testowany klawisz jest naciśnięty, i wartość False w przeciwnym wypadku. Używam metody is_pressed w ciągu instrukcji if, aby sprawdzić, czy którykolwiek z czterech klawiszy — W, S, A lub D — nie jest naciśnięty. Jeśli jest naciśnięty klawisz W, zmniejszam o 1 wartość właściwości y obiektu klasy Ship, przesuwając duszka w górę ekranu o jeden piksel. Jeśli jest naciśnięty klawisz S, zwiększam wartość właściwości y obiektu o 1, przesuwając duszka w dół ekranu. Jeśli jest naciśnięty klawisz A, zmniejszam o 1 wartość właściwości x obiektu, przesuwając duszka w lewo. Jeśli jest naciśnięty klawisz S, zwiększam wartość właściwości x obiektu o 1, przesuwając duszka w prawo. Ponieważ wielokrotne wywołania metody is_pressed() mogą odczytywać jednoczesne naciśnięcia klawiszy, użytkownik może przyciskać wiele klawiszy naraz dla uzyskania łącznego efektu. Jeśli na przykład użytkownik przytrzymuje w tym samym czasie wciśnięte klawisze D i S, statek porusza się w dół i w prawo, ponieważ za każdym razem, gdy wykonywana jest metoda update(), wartość 1 zostaje dodana zarówno do współrzędnej x, jak i współrzędnej y obiektu klasy Ship.

Obracanie duszka

Moduł games zawiera zbiór stałych reprezentujących klawisze, których możesz używać w roli argumentu w wywołaniu metody is_pressed(). W tym programie wykorzystuję stałą games.K_w reprezentującą klawisz W, stałą games.K_s odpowiadającą klawiszowi S, stałą games.K_a oznaczającą klawisz A oraz stałą games.K_d identyfikującą klawisz D. Schemat nadawania nazw tym stałym jest dość intuicyjny. Oto szybki sposób na wydedukowanie nazwy większości stałych reprezentujących klawisze:  wszystkie stałe klawiatury zaczynają się od games.K_;  w przypadku klawiszy alfabetycznych dodaj literę z klawisza, po zamianie

na małą, na końcu nazwy stałej; na przykład stała reprezentująca klawisz A to games.K_a;  w przypadku klawiszy numerycznych dodaj cyfrę z klawisza na końcu nazwy

stałej; na przykład stała reprezentująca klawisz 1 to games.K_1;  w przypadku pozostałych klawiszy często możesz na końcu nazwy stałej dodać

ich nazwę pisaną samymi dużymi literami; na przykład stała reprezentująca klawisz spacji to games.K_SPACE. Kompletną listę stałych klawiatury znajdziesz w dokumentacji pakietu livewires, w dodatku B.

Dokończenie programu Na koniec piszę znajomą funkcję main(). Ładuję obraz tła przedstawiający mgławicę, tworzę statek, umieszczając go w środku ekranu, oraz wszystko uruchamiam poprzez wywołanie metody mainloop(). def main(): nebula_image = games.load_image("mglawica.jpg", transparent = False) games.screen.background = nebula_image ship_image = games.load_image("statek.bmp") the_ship = Ship(image = ship_image, x = games.screen.width/2, y = games.screen.height/2) games.screen.add(the_ship) games.screen.mainloop() main()

Obracanie duszka W rozdziale 11. dowiedziałeś się, jak przemieszczać duszki po ekranie, ale pakiet livewires umożliwia również ich obracanie. Duszka można obracać, wykorzystując jedną z jego właściwości.

365

366

Rozdział 12. Dźwięk, animacja i rozwijanie programu. Gra Astrocrash

Prezentacja programu Obróć duszka W programie Obróć duszka, użytkownik może obracać statek kosmiczny przy użyciu klawiatury. Kiedy użytkownik naciska klawisz strzałki w górę, statek obraca się w kierunku zgodnym z ruchem wskazówki zegara. Jeśli użytkownik naciśnie klawisz strzałki w lewo, statek obróci się w kierunku przeciwnym do ruchu wskazówki zegara. Jeśli naciśnie klawisz 1, kąt położenia statku zmieni się skokowo na 0 stopni. Jeśli użytkownik naciśnie klawisz 2, statek zmieni swoją orientację na 90 stopni. Jeśli naciśnie klawisz 3, statek przyjmie położenie kątowe 180 stopni. Jeśli użytkownik naciśnie klawisz 4, kąt położenia zmieni się skokowo na 270 stopni. Program został przedstawiony na rysunku 12.4.

Rysunek 12.4. Statek kosmiczny może się obracać zgodnie z ruchem wskazówki zegara lub w kierunku przeciwnym do ruchu wskazówki zegara. Może też przeskoczyć do położenia pod z góry ustalonym kątem

Pułapka Program Obróć duszka sprawdza, czy są wciśnięte klawisze cyfr znajdujące się u góry klawiatury, powyżej klawiszy z literami, lecz nie sprawdza stanu klawiszy klawiatury numerycznej.

Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 12.; nazwa pliku to obroc_duszka.py.

Obracanie duszka # Obróć duszka # Demonstruje obracanie duszka from livewires import games games.init(screen_width = 640, screen_height = 480, fps = 50) class Ship(games.Sprite): """ Obracający się statek kosmiczny. """ def update(self): """ Obróć w zależności od naciśniętych klawiszy. """ if games.keyboard.is_pressed(games.K_RIGHT): self.angle += 1 if games.keyboard.is_pressed(games.K_LEFT): self.angle -= 1 if games.keyboard.is_pressed(games.K_1): self.angle = 0 if games.keyboard.is_pressed(games.K_2): self.angle = 90 if games.keyboard.is_pressed(games.K_3): self.angle = 180 if games.keyboard.is_pressed(games.K_4): self.angle = 270 def main(): nebula_image = games.load_image("mglawica.jpg", transparent = False) games.screen.background = nebula_image ship_image = games.load_image("statek.bmp") the_ship = Ship(image = ship_image, x = games.screen.width/2, y = games.screen.height/2) games.screen.add(the_ship) games.screen.mainloop() main()

Wykorzystanie właściwości angle obiektu klasy Sprite Nowym elementem w programie jest właściwość angle, która reprezentuje orientację kątową duszka wyrażoną w stopniach. Możesz zwiększać lub zmniejszać wartość tej właściwości przez dodawanie lub odejmowanie przyrostów, lecz możesz też po prostu przypisać jej nową wartość w celu zmiany kąta położenia duszka. W metodzie update() najpierw sprawdzam, czy wciśnięty jest klawisz strzałki w prawo. Jeśli tak jest, dodaję jedynkę do wartości właściwości angle obiektu, co powoduje obrócenie duszka o jeden stopień w kierunku ruchu wskazówki zegara. Następnie sprawdzam, czy wciśnięty jest klawisz strzałki w lewo. Jeśli jest tak w istocie, odejmuję jedynkę od wartości tej właściwości, powodując obrót duszka o jeden stopień w kierunku przeciwnym do ruchu wskazówki zegara.

367

368

Rozdział 12. Dźwięk, animacja i rozwijanie programu. Gra Astrocrash

Kolejny zestaw wierszy kodu obraca statek bezpośrednio do określonego kąta położenia poprzez przypisanie nowej wartości do właściwości angle. Kiedy użytkownik naciska klawisz 1, kod przypisuje 0 do właściwości angle i duszek przeskakuje do położenia pod kątem 0 stopni (jest to jego początkowa orientacja). Kiedy użytkownik naciska klawisz 2, kod przypisuje do właściwości angle wartość 90 i duszek przeskakuje do położenia pod kątem 90 stopni. Gdy użytkownik naciska klawisz 3, kod przypisuje do właściwości angle wartość 180 i duszek przeskakuje do położenia pod kątem 180 stopni. Wreszcie, gdy użytkownik naciska klawisz 4, kod przypisuje do właściwości angle wartość 270 i duszek przeskakuje do położenia pod kątem 270 stopni.

Tworzenie animacji Przemieszczanie duszków i ich obracanie sprawia, że gra staje się bardziej ekscytująca, lecz dopiero animacja wnosi w nią prawdziwe życie. Na szczęście moduł games zawiera klasę do obsługi animacji, stosownie nazwaną Animation.

Prezentacja programu Eksplozja Program Eksplozja tworzy animację wybuchu w środku ekranu graficznego. Animacja jest odtwarzana w sposób ciągły, żebyś mógł się jej dobrze przyjrzeć. Kiedy skończysz już podziwiać ten bombowy efekt, możesz zakończyć program przez zamknięcie okna graficznego. Na rysunku 12.5 prezentuję migawkę programu w akcji.

Rysunek 12.5. Chociaż trudno to stwierdzić na podstawie nieruchomego obrazu, w centrum okna graficznego wykonywana jest animacja wybuchu

Przegląd obrazów eksplozji

Przegląd obrazów eksplozji Animacja to sekwencja obrazów (zwanych także ramkami — ang. frames) wyświetlanych jeden po drugim. Utworzyłem sekwencję dziewięciu obrazów, które, kiedy są wyświetlane jeden po drugim, tworzą wrażenie ognistej eksplozji. Wszystkie dziewięć obrazków pokazałem na rysunku 12.6.

Rysunek 12.6. Gdy te dziewięć ramek zostanie wyświetlonych w szybkim tempie jedna po drugiej, otrzymamy wrażenie eksplozji

Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 12.; nazwa pliku to eksplozja.py.

Rozpoczęcie programu Jak zawsze, na początku programu importuję potrzebne moduły i wywołuję funkcję inicjalizującą ekran graficzny: # Eksplozja # Demonstruje tworzenie animacji from livewires import games games.init(screen_width = 640, screen_height = 480, fps = 50)

369

370

Rozdział 12. Dźwięk, animacja i rozwijanie programu. Gra Astrocrash

Potem ustawiam tło ekranu graficznego: nebula_image = games.load_image("mglawica.jpg", transparent = 0) games.screen.background = nebula_image

Utworzenie listy plików z obrazami Konstruktor klasy Animation pobiera listę nazw plików zawierających obrazy lub listę obiektów obrazu reprezentującą sekwencję obrazów do wyświetlenia. Więc w następnej kolejności tworzę listę nazw plików z obrazami, które zostały pokazane na rysunku 12.6: explosion_files = ["eksplozja1.bmp", "eksplozja2.bmp", "eksplozja3.bmp", "eksplozja4.bmp", "eksplozja5.bmp", "eksplozja6.bmp", "eksplozja7.bmp", "eksplozja8.bmp", "eksplozja9.bmp"]

Utworzenie obiektu klasy Animation W końcu tworzę obiekt klasy Animation i dodaję go do ekranu: explosion = games.Animation(images = explosion_files, x = games.screen.width/2, y = games.screen.height/2, n_repeats = 0, repeat_interval = 5) games.screen.add(explosion)

Animation jest klasą pochodną klasy Sprite, więc dziedziczy wszystkie jej atrybuty, właściwości i metody. Podobnie jak w przypadku wszystkich duszków, możesz podać współrzędne x i y, aby zdefiniować umiejscowienie animacji. W powyższym kodzie przekazuję współrzędne do konstruktora klasy, tak aby animacja była utworzona w środku ekranu. Animacja tym się różni od duszka, że występuje w niej lista obrazów, która jest przetwarzana cyklicznie. Więc musisz dostarczyć listę nazw plików graficznych w postaci łańcuchów znaków albo listę obiektów obrazu reprezentujących obrazy, które mają być wyświetlane. Ja dostarczam listę explosion_files z łańcuchami reprezentującymi nazwy plików graficznych poprzez parametr images. Atrybut n_repeats obiektu określa, ile razy animacja (jako sekwencja jej wszystkich obrazów) zostanie wyświetlona. Wartość domyślna atrybutu n_repeats wynosi 0. Ponieważ przekazuję 0 do n_repeats, cykl animacji eksplozji będzie powtarzany bez końca (lub przynajmniej do momentu zamknięcia okna graficznego). Atrybut repeat_interval obiektu reprezentuje opóźnienie między następującymi po sobie obrazami. Większa liczba oznacza większe opóźnienie między ramkami, skutkujące

Wykorzystywanie dźwięku i muzyki

wolniejszą animacją. Mniejsza liczba reprezentuje mniejszą zwłokę, generując szybszą animację. Ja przekazuję do atrybutu repeat_interval wartość 5, aby uzyskać prędkość, jaką uważam za właściwą do wygenerowania przekonującej eksplozji. Ostatnią, lecz równie ważną czynnością jest uruchomienie programu poprzez wywołanie metody mainloop() obiektu screen: games.screen.mainloop()

Wykorzystywanie dźwięku i muzyki Dźwięk i muzyka dodają nowy, oddziałujący na zmysły wymiar do programów. Ładowanie, odtwarzanie, powtarzanie w pętli i zatrzymywanie dźwięku i muzyki są łatwe do wykonania za pomocą modułu games. Chociaż ludzie mogliby się spierać na temat różnicy między dźwiękiem a muzyką, na gruncie pakietu livewires nie ma żadnej takiej dyskusji — istnieje w nim wyraźne rozróżnienie między tymi dwoma elementami.

Prezentacja programu Dźwięk i muzyka Program Dźwięk i muzyka umożliwia użytkownikowi odtwarzanie, powtarzanie w pętli i zatrzymywanie efektu dźwiękowego wystrzelonego pocisku oraz tematu muzycznego z gry Astrocrash. Użytkownik może nawet odtwarzać obydwa te elementy jednocześnie. Rysunek 12.7 pokazuje uruchomiony program (lecz niestety nie jest w stanie wygenerować dźwięku).

Rysunek 12.7. Program umożliwia użytkownikowi odtworzenie dźwięku i fragmentu muzyki

371

372

Rozdział 12. Dźwięk, animacja i rozwijanie programu. Gra Astrocrash

Wskazówka Kiedy uruchomisz ten program, będzie Ci potrzebna interakcja z oknem konsoli. Powinieneś umieścić okno konsoli w takiej pozycji, aby nie było ono zakryte przez okno graficzne. Możesz zignorować lub nawet zminimalizować utworzone przez program okno graficzne.

Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 12.; nazwa pliku to dzwiek_i_muzyka.py.

Praca z dźwiękami Możesz utworzyć obiekt dźwiękowy do użytku programu poprzez załadowanie pliku typu WAV. Format WAV znakomicie się nadaje do zapisu efektów dźwiękowych, ponieważ może zostać użyty do zakodowania wszystkiego, co zarejestrujesz za pomocą mikrofonu.

Załadowanie dźwięku Program rozpoczynam jak zawsze: # Dźwięk i muzyka # Demonstruje odtwarzanie plików dźwiękowych i muzycznych from livewires import games games.init(screen_width = 640, screen_height = 480, fps = 50)

Potem ładuję plik WAV, wykorzystując funkcję load_sound() modułu games. # załaduj plik dźwiękowy missile_sound = games.load_sound("pocisk.wav")

Funkcja przyjmuje łańcuch znaków reprezentujący nazwę pliku dźwiękowego, który ma zostać załadowany. Ładuję plik pocisk.wav i przypisuję nowo utworzony obiekt dźwiękowy do zmiennej missile_sound.

Pułapka Za pomocą funkcji load_sound() można ładować tylko pliki WAV.

Następnie ładuję plik muzyczny: # załaduj plik muzyczny games.music.load("temat.mid")

Omówienie sposobu obsługi muzyki odłożę do momentu, gdy skończę demonstrowanie dźwięków.

Wykorzystywanie dźwięku i muzyki

Odtworzenie dźwięku Następnie tworzę menu, z czym po raz pierwszy spotkałeś się w rozdziale 5.: choice = None while choice != "0": print( """ Dźwięk i muzyka 0 1 2 3 4 5 6 """ )

zakończ odtwórz dźwięk pocisku odtwarzaj cyklicznie dźwięk pocisku zatrzymaj odtwarzanie dźwięku pocisku odtwórz temat muzyczny odtwarzaj cyklicznie temat muzyczny zatrzymaj odtwarzanie tematu muzycznego

choice = input("Wybieram: ") print() # wyjdź if choice == "0": print("Żegnaj!")

Jeśli użytkownik wprowadzi 0, program pożegna użytkownika i zakończy działanie. Poniższy kod obsługuje przypadek, w którym użytkownik wprowadza 1: # odtwórz dźwięk pocisku elif choice == "1": missile_sound.play() print("Odtworzenie dźwięku pocisku.")

Aby odtworzyć dźwięk jeden raz, wywołuję metodę play() obiektu dźwiękowego. Kiedy dźwięk jest odtwarzany, zajmuje jeden z ośmiu dostępnych kanałów dźwiękowych. Aby odtworzyć dźwięk, potrzeba przynajmniej jednego otwartego kanału dźwiękowego. Kiedy zajętych jest wszystkich osiem kanałów, wywołanie metody play() obiektu dźwiękowego nie przyniesie żadnego efektu. Jeśli wywołasz metodę play() obiektu dźwiękowego, który jest już odtwarzany, rozpocznie się odtwarzanie tego dźwięku na innym kanale, jeśli taki jest dostępny.

Cykliczne odtwarzanie dźwięku Możesz odtwarzać dźwięk cyklicznie poprzez przekazanie liczby dodatkowych odtworzeń, jakie mają mieć miejsce, do metody play() obiektu. Jeśli na przykład przekażesz do play() liczbę 3, odpowiedni dźwięk zostanie odtworzony cztery razy (początkowe odtworzenie plus trzy powtórzenia). Możesz cyklicznie odtwarzać dźwięk bez końca po przekazaniu wartości -1 do metody play(). Poniższy kod obsługuje przypadek, gdy użytkownik wprowadza 2:

373

374

Rozdział 12. Dźwięk, animacja i rozwijanie programu. Gra Astrocrash # odtwarzaj cyklicznie dźwięk pocisku elif choice == "2": loop = int(input("Ile razy powtórzyć odtwarzanie? (-1 = bez końca): ")) missile_sound.play(loop) print("Cykliczne odtwarzanie dźwięku pocisku.")

W tym fragmencie kodu pobieram liczbę wskazującą, ile dodatkowo razy użytkownik chce usłyszeć odgłos pocisku, a następnie przekazuję tę wartość do metody play() obiektu dźwiękowego.

Zatrzymanie odtwarzania dźwięku Odtwarzanie dźwięku przez obiekt dźwiękowy zatrzymuje się poprzez wywołanie metody stop(). Zatrzymuje ona ten konkretny dźwięk na wszystkich kanałach, na których jest odtwarzany. Jeśli wywołasz metodę stop() obiektu dźwiękowego, który w danej chwili nic nie odtwarza, przekonasz się, że pakiet livewires jest tolerancyjny i nie poskarży się poprzez zgłoszenie błędu. Jeśli użytkownik wprowadzi 3, poniższy kod zatrzyma odtwarzanie odgłosu pocisku (jeśli takowy jest właśnie odtwarzany): # zatrzymaj odtwarzanie dźwięku pocisku elif choice == "3": missile_sound.stop() print("Zatrzymanie odtwarzania dźwięku pocisku.")

Praca z muzyką W pakiecie livewires muzyka jest obsługiwana nieco inaczej niż dźwięk. Istnieje tylko jeden kanał muzyczny, więc w danym momencie jako bieżący plik muzyczny może zostać wyznaczony tylko jeden plik. Kanał muzyczny jest jednak bardziej elastyczny niż kanały dźwiękowe. Akceptuje on wiele różnych typów plików dźwiękowych, takich jak WAV, MP3, OGG i MIDI. Wreszcie, ponieważ istnieje tylko jeden kanał muzyczny, nie tworzy się nowego obiektu dla każdego pliku muzycznego. Zamiast tego masz dostęp do zestawu funkcji służących do ładowania, odtwarzania i zatrzymywania muzyki.

Załadowanie muzyki Widziałeś kod odpowiedzialny za załadowanie pliku muzycznego w podpunkcie „Załadowanie dźwięku” punktu „Praca z dźwiękami”. Kod skorzystał z dostępu do obiektu music modułu games. To dzięki obiektowi music możesz załadować, odtworzyć i zatrzymać pojedynczą ścieżkę muzyczną. Kod, którego użyłem do załadowania ścieżki muzycznej, games.music.load("temat.mid"), ustawia bieżącą muzykę na plik temat.mid typu MIDI. Muzykę ładuje się poprzez wywołanie metody games.music.load() i przekazanie do niej nazwy pliku muzycznego w postaci łańcucha znaków. Masz dostępną tylko jedną ścieżkę muzyczną. Więc jeśli załadujesz nowy plik muzyczny, zastąpi on muzykę bieżącą.

Wykorzystywanie dźwięku i muzyki

Odtworzenie muzyki Poniższy kod obsługuje przypadek, gdy użytkownik wprowadzi 4: # odtwórz temat muzyczny elif choice == "4": games.music.play() print("Odtworzenie tematu muzycznego.")

W rezultacie komputer odtwarza plik muzyczny, który załadowałem, temat.mid. Jeśli nie przekażesz żadnych wartości do metody games.music.play(), muzyka jest odtwarzana tylko raz.

Cykliczne odtwarzanie muzyki Możesz odtwarzać muzykę cyklicznie, tyle razy, ile chcesz, po przekazaniu do metody play() liczby dodatkowych odtworzeń. Jeśli na przykład przekażesz wartość 3 do metody games.music.play(), muzyka zostanie odtworzona cztery razy (odtworzenie początkowe plus trzy powtórzenia). Możesz cyklicznie odtwarzać muzykę bez końca po przekazaniu wartości -1 do metody. Poniższy kod obsługuje przypadek, w którym użytkownik wprowadza 5: # odtwarzaj cyklicznie temat muzyczny elif choice == "5": loop = int(input("Ile razy powtórzyć odtwarzanie? (-1 = bez końca): ")) games.music.play(loop) print("Cykliczne odtwarzanie tematu muzycznego.")

W tym fragmencie kodu wczytuję liczbę dodatkowych odtworzeń tematu muzycznego, jakiej użytkownik chce posłuchać, a następnie przekazuję tę wartość do metody play().

Zatrzymanie odtwarzania muzyki Jeśli użytkownik wprowadzi opcję 6, poniższy kod zatrzyma odtwarzanie muzyki (jeśli faktycznie jest wykonywane): # zatrzymaj odtwarzanie tematu muzycznego elif choice == "6": games.music.stop() print("Zatrzymanie odtwarzania tematu muzycznego.")

Możesz spowodować zatrzymanie odtwarzania bieżącej muzyki poprzez wywołanie metody games.music.stop(), co właśnie w tym miejscu kodu robię. Jeśli wywołasz tę metodę, kiedy nie jest odtwarzana żadna muzyka, moduł livewires okazuje się wyrozumiały i nie generuje błędu.

Dokończenie programu Wreszcie kończę program obsługą nieprawidłowego wyboru i oczekiwaniem na decyzję użytkownika:

375

376

Rozdział 12. Dźwięk, animacja i rozwijanie programu. Gra Astrocrash # nieprzewidziany wybór else: print("\nNiestety,", choice, "nie jest prawidłowym wyborem.") input("\n\nAby zakończyć program, naciśnij klawisz Enter.")

Planowanie gry Astrocrash Pora na powrót do projektu rozdziału — gry Astrocrash. Zamierzam pisać stopniowo coraz kompletniejsze wersję gry, aż osiągnę postać końcową, lecz nadal odczuwam potrzebę wymienienia kilku szczegółów programu, w tym głównych elementów gry, kilku niezbędnych klas i wymaganych zasobów multimedialnych.

Elementy gry Chociaż moja gra jest oparta na klasycznej grze wideo, którą dobrze znam (a poznawałem ją etapami, drogą prób i błędów), wypisanie listy jej elementów jest nadal dobrym pomysłem:  statek kosmiczny powinien obracać się i inicjować (lub przyśpieszać) ruch

do przodu w reakcji na klawisze naciśnięte przez gracza;  statek powinien wystrzeliwać pociski po naciśnięciu przez gracza

odpowiedniego klawisza;  asteroidy powinny przelatywać przez ekran z różnymi prędkościami; mniejsze

asteroidy powinny mieć generalnie wyższe prędkości niż większe;  statek, wszystkie pociski i asteroidy powinny „przewijać się” przez brzegi ekranu —

jeśli wyjdą poza granicę ekranu, powinny ukazać się po przeciwnej stronie;  jeśli pocisk uderzy w dowolny inny obiekt na ekranie, powinien zniszczyć ten

obiekt i sam siebie w efektownej, ognistej eksplozji;  jeśli statek uderzy w dowolny inny obiekt na ekranie, powinien zniszczyć ten

obiekt i sam siebie w efektownej, ognistej eksplozji;  jeśli statek zostaje zniszczony, gra się kończy;  jeśli zostaje zniszczona duża asteroida, powinny utworzyć się dwie asteroidy

średniej wielkości; jeśli zostaje zniszczona asteroida średniego rozmiaru, powinny powstać dwie małe asteroidy; jeśli zostaje zniszczona mała asteroida, nie powstają już żadne nowe;  za każdym razem, gdy gracz zniszczy asteroidę, jego dorobek punktowy powinien

się zwiększyć; mniejsze asteroidy powinny być warte więcej punktów niż większe;  liczba punktów uzyskanych przez gracza powinna być wyświetlana w prawym

górnym rogu ekranu;  kiedy tylko wszystkie asteroidy zostaną zniszczone, powinna zostać utworzona

nowa, większa fala asteroidów. Pomijam kilka elementów oryginału, aby zachować prostotę gry.

Utworzenie asteroidów

Klasy potrzebne w grze Następnie sporządzam listę klas, które, jak sądzę, będą mi potrzebne:  Ship,  Missile,  Asteroid,  Explosion.

Już trochę wiem o tych klasach. Ship, Missile i Asteroid powinny być klasami pochodnymi klasy games.Sprite, podczas gdy Explosion powinna być klasą pochodną klasy games.Animation. Wiem też, że ta lista może ulec zmianie, kiedy teorię będę zamieniał w praktykę i gdy będę pisał kod gry.

Zasoby gry Ponieważ gra zawiera dźwięk, muzykę, duszki i animację, wiem, że muszę utworzyć pewną liczbę plików multimedialnych. Oto lista, jaką udało mi się stworzyć:  plik graficzny reprezentujący statek kosmiczny,  plik graficzny reprezentujący pociski,  trzy pliki graficzne, po jednym dla każdego rozmiaru asteroidy,  seria plików graficznych do animacji eksplozji,  plik dźwiękowy imitujący rozpędzanie statku,  plik dźwiękowy z odgłosem wystrzeliwania pocisku,  plik dźwiękowy imitujący eksplozję obiektu,  plik z tematem muzycznym.

Utworzenie asteroidów Ponieważ w grze mają występować śmiercionośne asteroidy, pomyślałem, że zacznę od nich. Choć wydaje się, że jest to dla mnie najlepszy wybór pierwszego kroku, w przypadku innego programisty może być inaczej — i jest to w porządku. Mógłbyś oczywiście wybrać inny pierwszy krok, taki jak umieszczenie na ekranie statku kosmicznego gracza. Nie istnieje jeden właściwy pierwszy krok. Najważniejszą rzeczą jest zdefiniowanie i wykonanie programów „na jeden kęs”, które, bazując jeden na drugim, wypracowują ścieżkę do kompletnego projektu.

Program Astrocrash01 Program Astrocrash01 tworzy okno graficzne, ustawia tło w postaci mgławicy i tworzy osiem asteroid w losowo wybranych miejscach. Prędkość każdej asteroidy jest również

377

378

Rozdział 12. Dźwięk, animacja i rozwijanie programu. Gra Astrocrash

obliczana z uwzględnieniem losowości, lecz mniejsze asteroidy mogą się poruszać szybciej niż większe. Na rysunku 12.8 pokazuję program w akcji.

Rysunek 12.8. Pole poruszających się asteroid stanowi podstawę gry

Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 12.; nazwa pliku to astrocrash01.py.

Rozpoczęcie programu Program rozpoczyna się jak większość pozostałych: # Astrocrash01 # Tworzy poruszające się po ekranie asteroidy import random from livewires import games games.init(screen_width = 640, screen_height = 480, fps = 50)

Importuję moduł random, aby wygenerować współrzędne x i y dla asteroid.

Utworzenie asteroidów

Klasa Asteroid Klasa Asteroid jest wykorzystywana do tworzenia poruszających się asteroid: class Asteroid(games.Sprite): """ Asteroida przelatująca przez ekran. """ SMALL = 1 MEDIUM = 2 LARGE = 3 images = {SMALL : games.load_image("asteroida_mala.bmp"), MEDIUM : games.load_image("asteroida_sred.bmp"), LARGE : games.load_image("asteroida_duza.bmp") } SPEED = 2

Pierwszą moją czynnością jest zdefiniowanie stałych klasowych reprezentujących trzy różne wielkości asteroid: SMALL (mała), MEDIUM (średniego rozmiaru) i LARGE (duża). Następnie tworzę słownik z rozmiarami i odpowiadającymi im obiektami obrazów asteroid. W ten sposób mogę wykorzystać stałą reprezentującą rozmiar do znalezienia odpowiedniego obiektu obrazu. Na koniec tworzę stałą klasową o nazwie SPEED, której użyję jako podstawy do obliczenia ulosowionej prędkości każdej asteroidy.

Metoda __init__() W następnej kolejności zajmuję się zdefiniowaniem konstruktora: def __init__(self, x, y, size): """ Inicjalizuj duszka asteroidy. """ super(Asteroid, self).__init__( image = Asteroid.images[size], x = x, y = y, dx = random.choice([1, -1]) * Asteroid.SPEED * random.random()/size, dy = random.choice([1, -1]) * Asteroid.SPEED * random.random()/size) self.size = size

Wartość przekazana poprzez parametr size reprezentuje wielkość asteroidy i powinna być równa jednej ze stałych rozmiaru: Asteroid.SMALL, Asteroid.MEDIUM lub Asteroid.LARGE. Na podstawie wartości size pobierany jest odpowiedni obraz nowej asteroidy, który następnie zostaje przekazany do konstruktora klasy Sprite (ponieważ Sprite jest klasą nadrzędną klasy Asteroid). Do konstruktora klasy Sprite zostają również przekazane wartości x i y reprezentujące położenie kosmicznej skały, przekazane wcześniej do konstruktora klasy Asteroid. Konstruktor klasy Asteroid generuje losowe wartości składowych prędkości nowego obiektu i przekazuje je do konstruktora klasy Sprite. Składowe prędkości mają wartości losowe, ale mniejsze asteroidy mogą się potencjalnie poruszać szybciej niż większe. W końcu konstruktor klasy Asteroid tworzy i inicjalizuje atrybut size obiektu.

379

380

Rozdział 12. Dźwięk, animacja i rozwijanie programu. Gra Astrocrash

Metoda update() Metoda update() utrzymuje asteroidę w grze poprzez przeniesienie jej na przeciwległy brzeg ekranu: def update(self): """ Przenieś asteroidę na przeciwległy brzeg ekranu. """ if self.top > games.screen.height: self.bottom = 0 if self.bottom < 0: self.top = games.screen.height if self.left > games.screen.width: self.right = 0 if self.right < 0: self.left = games.screen.width

Funkcja main() Na koniec funkcja main() ustawia tło w postaci mgławicy oraz tworzy osiem asteroid w przypadkowych miejscach ekranu: def main(): # ustaw tło nebula_image = games.load_image("mglawica.jpg") games.screen.background = nebula_image # utwórz 8 asteroid for i in range(8): x = random.randrange(games.screen.width) y = random.randrange(games.screen.height) size = random.choice([Asteroid.SMALL, Asteroid.MEDIUM, Asteroid.LARGE]) new_asteroid = Asteroid(x = x, y = y, size = size) games.screen.add(new_asteroid) games.screen.mainloop() # wystartuj! main()

Obracanie statku Aby wykonać swoje następne zadanie, wprowadzam statek kosmiczny gracza. Moim skromnym celem jest umożliwienie użytkownikowi obracania statku za pomocą klawiszy strzałek. Do pozostałych funkcji statku zamierzam zabrać się później.

Obracanie statku

Program Astrocrash02 Program Astrocrash02 stanowi rozszerzenie programu Astrocrash01. W nowej wersji tworzę w środku ekranu statek, który gracz może obracać. Jeśli gracz naciska klawisz strzałki w prawo, statek obraca się zgodnie z ruchem wskazówek zegara. Jeśli zaś gracz naciska klawisz strzałki w lewo, statek obraca się w kierunku przeciwnym do ruchu wskazówek zegara. Na rysunku 12.9 pokazuję ten program w działaniu.

Rysunek 11.9. Statek kosmiczny gracz stanowi teraz część akcji

Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 12.; nazwa pliku to astrocrash02.py.

Klasa Ship Moim głównym zadaniem jest napisanie kodu klasy Ship reprezentującej statek kosmiczny gracza: class Ship(games.Sprite): """ Statek kosmiczny gracza. """ image = games.load_image("statek.bmp") ROTATION_STEP = 3

381

382

Rozdział 12. Dźwięk, animacja i rozwijanie programu. Gra Astrocrash def update(self): """ Obróć statek zgodnie z naciśniętym klawiszem. """ if games.keyboard.is_pressed(games.K_LEFT): self.angle -= Ship.ROTATION_STEP if games.keyboard.is_pressed(games.K_RIGHT): self.angle += Ship.ROTATION_STEP

Ta klasa jest podobna do klasy występującej w programie Obróć duszka z wcześniejszej części tego rozdziału, lecz istnieje kilka różnic. Po pierwsze, ładuję obraz statku i przypisuję uzyskany w ten sposób obiekt obrazu do zmiennej klasowej o nazwie image. Po drugie, wykorzystuję stałą klasową, ROTATION_STEP, do reprezentowania liczby stopni, o jaką statek się obraca.

Konkretyzacja obiektu klasy Ship Moją ostatnią czynnością w tej nowej wersji gry jest konkretyzacja obiektu klasy Ship oraz dodanie go do ekranu. Tworzę nowy statek w funkcji main(): # utwórz statek the_ship = Ship(image = Ship.image, x = games.screen.width/2, y = games.screen.height/2) games.screen.add(the_ship)

Poruszanie statku W następnej wersji programu wprawiam statek w ruch. Gracz może nacisnąć strzałkę w górę, aby włączyć silnik statku. Dzięki temu na statek oddziałuje siła ciągu, pchając go w kierunku, jaki wskazuje przód statku. Ponieważ brak jest tarcia, statek kontynuuje poruszanie się, nie tracąc prędkości nadanej mu na początku przez gracza.

Program Astrocrash03 Kiedy gracz włącza silnik statku, program Astrocrash03 zmienia prędkość statku w sposób zależny od położenia kątowego statku (czemu towarzyszy odpowiedni efekt dźwiękowy). Program został zilustrowany na rysunku 12.10. Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 12.; nazwa pliku to astrocrash03.py.

Import modułu math Moją pierwszą czynnością jest umieszczenie na początku programu instrukcji importującej nowy moduł: import math, random

Poruszanie statku

Rysunek 12.10. Teraz statek może się poruszać po ekranie

Moduł math zawiera znaczną liczbę funkcji i stałych matematycznych, ale niech Cię to nie przeraża. W tym programie użyję tylko kilku z nich.

Dodanie do klasy Ship zmiennej i stałej klasowej Tworzę stałą klasową, VELOCITY_STEP, którą wykorzystam do zmiany prędkości statku: VELOCITY_STEP = .03

Użycie większej liczby spowodowałoby szybsze przyśpieszanie statku, podczas gdy mniejsza liczba sprawiłaby, że statek przyśpieszałby wolniej. Dodaję również nową zmienną klasową, sound, mającą reprezentować dźwięk towarzyszący przyśpieszaniu statku: sound = games.load_sound("przyspieszenie.wav")

Modyfikacja metody update() klasy Ship Następnie dodaję nowy kod na końcu metody update() klasy Ship, aby sprawić, by statek się poruszał. Sprawdzam, czy gracz naciska klawisz strzałki w górę. Jeśli ma to miejsce, odtwarzam dźwięk przyśpieszającego statku:

383

384

Rozdział 12. Dźwięk, animacja i rozwijanie programu. Gra Astrocrash # zastosuj siłę ciągu przy naciśniętym klawiszu strzałki w górę if games.keyboard.is_pressed(games.K_UP): Ship.sound.play()

Poza tym, gdy gracz naciska klawisz strzałki w górę, muszę zmieniać składowe prędkości statku (właściwości dx i dy obiektu klasy Ship). Więc jak, mając dany kąt położenia statku, mogę obliczyć wartość, o jaką powinienem zmienić każdą ze składowych prędkości? Odpowiedź daje trygonometria. Poczekaj, nie zamykaj z trzaskiem tej książki i nie uciekaj, gdzie Cię nogi poniosą, wykrzykując coś bez ładu i składu. Jak obiecałem, do tych obliczeń wykorzystam tylko dwie funkcję matematyczne w paru wierszach kodu. Aby rozpocząć ten proces, obliczam kąt położenia statku po zamianie stopni na radiany: # zmień składowe prędkości w zależności od kąta położenia statku angle = self.angle * math.pi / 180 # zamień na radiany

Radian to tylko miara obrotu, podobnie jak stopień. Moduł math w języku Python wymaga, aby miary kątów były wyrażone w radianach (podczas gdy pakiet livewires używa stopni), więc z tego powodu muszę dokonać konwersji. W obliczeniu wykorzystuję stałą pi modułu math, która reprezentuje liczbę π. Kiedy już mam kąt położenia statku wyrażony w radianach, mogę obliczyć, o jaką wartość powinienem zmienić każdą ze składowych prędkości, wykorzystując funkcje sin() i cos() obliczające sinus i cosinus kąta. W poniższych wierszach zostają obliczone nowe wartości właściwości dx i dy obiektu: self.dx += Ship.VELOCITY_STEP * math.sin(angle) self.dy += Ship.VELOCITY_STEP * -math.cos(angle)

Zasadniczo math.sin(angle) reprezentuje procent siły ciągu powodujący zmianę prędkości statku w kierunku osi x, podczas gdy -math.cos(angle) reprezentuje procent siły ciągu zmieniający prędkość statku w kierunku osi y. Pozostaje tylko zająć się granicami ekranu. Korzystam z tej samej strategii, której używałem w przypadku asteroid: statek wychodzący poza krawędź ekranu powinien wrócić po przeciwnej stronie. Prawdę mówiąc, kopiuję kod z metody update() klasy Asteroid i wklejam go na końcu metody update() w klasie Ship: # przenieś statek na przeciwległy brzeg ekranu if self.top > games.screen.height: self.bottom = 0 if self.bottom < 0: self.top = games.screen.height if self.left > games.screen.width: self.right = 0 if self.right < 0: self.left = games.screen.width

Wystrzeliwanie pocisków

Chociaż jest to skuteczne, kopiowanie i wklejanie dużych fragmentów kodu to zwykle oznaka słabości projektu. Wrócę do tego kodu później, aby znaleźć bardziej eleganckie rozwiązanie.

Pułapka Powtarzające się, duże porcje kodu powodują rozdęcie programów i sprawiają, że stają się one trudniejsze do konserwacji. Kiedy widzisz powtarzający się kod, to często pora na wprowadzenie nowej funkcji lub klasy. Pomyśl, jak mógłbyś skonsolidować kod w jednym miejscu i wywoływać go z innych części programu, w których powtarzający się kod aktualnie występuje.

Wystrzeliwanie pocisków Następnie umożliwię statkowi wystrzeliwanie pocisków. Kiedy gracz naciska klawisz spacji, wystrzeliwany jest pocisk z działa statku, który leci w kierunku wskazywanym przez przód statku. Pocisk powinien niszczyć wszystko, w co uderza, ale aby nie komplikować spraw, odkładam frajdę niszczenia do jednej z późniejszych wersji programu.

Program Astrocrash04 Program Astrocrash04 pozwala graczowi na wystrzeliwanie pocisków poprzez naciśnięcie klawisza spacji, lecz jest z tym pewien problem. Jeśli gracz przytrzymuje naciśnięty klawisz spacji, ze statku wylatuje strumień pocisków w tempie około 50 na sekundę. Muszę ograniczyć tempo wystrzeliwania pocisków, lecz zostawiam ten problem do następnej wersji gry. Na rysunku 12.11 przedstawiłem program Astrocrash04 z pełnym realizmem. Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 12.; nazwa pliku to astrocrash04.py.

Modyfikacja metody update() klasy Ship Modyfikuję metodę update() klasy Ship poprzez dodanie kodu, dzięki któremu statek może wystrzeliwać pociski. Jeśli gracz naciśnie klawisz spacji, tworzony jest nowy pocisk: # wystrzel pocisk, jeśli jest naciśnięty klawisz spacji if games.keyboard.is_pressed(games.K_SPACE) or True: new_missile = Missile(self.x, self.y, self.angle) games.screen.add(new_missile)

Oczywiście, aby skonkretyzować nowy obiekt przy użyciu wyrażenia Missile(self.x, self.y, self.angle), muszę napisać taką drobną rzecz… jak klasa Missile.

385

386

Rozdział 12. Dźwięk, animacja i rozwijanie programu. Gra Astrocrash

Rysunek 12.11. Tempo wystrzeliwania pocisków jest zbyt duże

Klasa Missile Piszę kod klasy Missile mającej reprezentować pociski wystrzeliwane przez statek. Zaczynam od utworzenia zmiennych i stałych klasowych: class Missile(games.Sprite): """ Pocisk wystrzelony przez statek gracza. """ image = games.load_image("pocisk.bmp") sound = games.load_sound("pocisk.wav") BUFFER = 40 VELOCITY_FACTOR = 7 LIFETIME = 40

Zmienna image ma reprezentować pocisk — pełne, czerwone kółko. Zmienna sound reprezentuje efekt dźwiękowy wystrzeliwania pocisku. Stała BUFFER definiuje odległość miejsca utworzenia nowego pocisku od statku (żeby pocisk nie został utworzony na wierzchu statku). Stała VELOCITY_FACTOR wpływa na szybkość lotu pocisku. Wreszcie stała LIFETIME określa długość czasu istnienia pocisku przed jego zniknięciem (żeby pocisk nie latał bez końca po ekranie).

Metoda __init__() Rozpoczynam kod konstruktora klasy od następujących wierszy: def __init__(self, ship_x, ship_y, ship_angle): """ Inicjalizuj duszka pocisku. """

Wystrzeliwanie pocisków

Możesz być zaskoczony tym, że konstruktor obiektu pocisku wymaga podania wartości współrzędnych x i y statku oraz jego kąta położenia, które są przyjmowane przez parametry ship_x, ship_y oraz ship_angle. Metoda potrzebuje tych wartości, aby ustalić dwie rzeczy: dokładne miejsce pierwszego pojawienia się pocisku oraz składowe jego prędkości. To, gdzie tworzony jest pocisk, zależy od miejsca statku, a to, jak pocisk leci, zależy od jego położenia kątowego. Następnie odtwarzam efekt dźwiękowy wystrzeliwania pocisku: Missile.sound.play()

Potem wykonuję trochę obliczeń, aby określić początkowe położenie nowego pocisku: # zamień na radiany angle = ship_angle * math.pi / 180 # oblicz pozycję początkową pocisku buffer_x = Missile.BUFFER * math.sin(angle) buffer_y = Missile.BUFFER * -math.cos(angle) x = ship_x + buffer_x y = ship_y + buffer_y

Uzyskuję miarę kąta położenia statku wyrażoną w radianach. Następnie obliczam współrzędne początkowe pocisku, x i y, na podstawie kąta położenia statku i wartości stałej Missile.BUFFER. Uzyskane wartości x i y umieszczają pocisk dokładnie przed działem statku. Następnie obliczam składowe prędkości pocisku. Stosuję ten sam typ obliczeń, którego użyłem w klasie Ship: # oblicz składowe prędkości pocisku dx = Missile.VELOCITY_FACTOR * math.sin(angle) dy = Missile.VELOCITY_FACTOR * -math.cos(angle)

Wywołuję konstruktor klasy Sprite na rzecz bieżącego obiektu: # utwórz pocisk super(Missile, self).__init__(image = Missile.image, x = x, y = y, dx = dx, dy = dy)

W końcu dodaję do obiektu klasy Missile atrybut lifetime, żeby obiekt nie pojawiał się na ekranie bez końca. self.lifetime = Missile.LIFETIME

Metoda update() Następnie piszę kod metody update(). Oto jego pierwsza część: def update(self): """ obsługuj ruch pocisku. """ # jeśli wyczerpał się czas życia pocisku, zniszcz go self.lifetime -= 1 if self.lifetime == 0: self.destroy()

387

388

Rozdział 12. Dźwięk, animacja i rozwijanie programu. Gra Astrocrash

Powyższy kod odlicza czas życia pocisku. Zmniejszana jest wartość atrybutu lifetime. Kiedy osiągnie 0, obiekt klasy Missile dokonuje samozniszczenia. W drugiej części metody update() zawarłem znajomy kod przenoszący pocisk

na przeciwległy brzeg ekranu: # przenieś pocisk na przeciwległy brzeg ekranu if self.top > games.screen.height: self.bottom = 0 if self.bottom < 0: self.top = games.screen.height if self.left > games.screen.width: self.right = 0 if self.right < 0: self.left = games.screen.width

Widzę, że powyższy kod został już w moim programie trzy razy powtórzony. Zdecydowanie będę go później konsolidował.

Regulowanie tempa wystrzeliwania pocisków Jak widziałeś w poprzednim programie, statek może wystrzelić około 50 pocisków w ciągu sekundy. Nawet dla gracza, który chce wygrać, jest to trochę za dużo. Więc w tej kolejnej wersji gry nakładam ograniczenie na tempo wystrzeliwania pocisków.

Program Astrocrash05 Program Astrocrash05 ogranicza tempo wystrzeliwania pocisków poprzez utworzenie mechanizmu odliczania, który wymusza zwłokę pomiędzy wystrzałami. Kiedy odliczanie się kończy, gracz może wystrzelić kolejny pocisk. Program został zilustrowany na rysunku 12.12. Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 12.; nazwa pliku to astrocrash05.py.

Dodanie stałej klasowej do klasy Ship Moim pierwszym krokiem w wymuszeniu zwłoki między wystrzeliwaniem pocisków jest dodanie do klasy Ship stałej klasowej: MISSILE_DELAY = 25

Stała MISSILE_DELAY reprezentuje czas zwłoki, jaki gracz musi odczekiwać między wystrzeliwaniem pocisków. Wykorzystuję ją do ponownego ustawiania odliczania, które zmusza gracza do czekania.

Regulowanie tempa wystrzeliwania pocisków

Rysunek 12.12. Teraz statek wystrzeliwuje pociski w rozsądniejszym tempie

Tworzenie konstruktora klasy Ship Następnie tworzę konstruktor dla klasy: def __init__(self, x, y): """ Inicjalizuj duszka statku. """ super(Ship, self).__init__(image = Ship.image, x = x, y = y) self.missile_wait = 0

Metoda przyjmuje wartości reprezentujące współrzędne x i y nowego statku i przekazuje je dalej do klasy nadrzędnej klasy Ship, games.Sprite. W kolejnym wierszu dodaję do nowego obiektu atrybut o nazwie missile_wait. Wykorzystuję go do odliczania czasu zwłoki, zanim gracz będzie mógł wystrzelić kolejny pocisk.

Modyfikacja metody update() klasy Ship Dodaję do metody update() klasy Ship kod, który zmniejsza wartość atrybutu missile_wait obiektu w ramach odliczania do 0. # jeśli czekasz, aż statek będzie mógł wystrzelić następny pocisk, # zmniejsz czas oczekiwania if self.missile_wait > 0: self.missile_wait -= 1

Następnie zmieniam kod z poprzedniej wersji gry obsługujący wystrzeliwanie pocisków na poniższe wiersze:

389

390

Rozdział 12. Dźwięk, animacja i rozwijanie programu. Gra Astrocrash # wystrzel pocisk, jeśli klawisz spacji jest naciśnięty i skończył się # czas oczekiwania if games.keyboard.is_pressed(games.K_SPACE) and self.missile_wait == 0: new_missile = Missile(self.x, self.y, self.angle) games.screen.add(new_missile) self.missile_wait = Ship.MISSILE_DELAY

Teraz, kiedy gracz naciśnie klawisz spacji, zanim statek wystrzeli nowy pocisk, musi się zakończyć odliczanie (wartość missile_wait musi być równa 0). Natychmiast po wystrzeleniu pocisku ustawiam atrybut missile_wait z powrotem na wartość MISSILE_DELAY, aby rozpocząć na nowo odliczanie.

Obsługa kolizji Jak dotąd gracz może przemieszczać statek po polu asteroid, a nawet wystrzeliwać pociski, ale żaden z obiektów nie wchodzi w interakcję z innymi. Zmieniam ten stan rzeczy w kolejnej wersji gry. Kiedy pocisk zderza się z dowolnym innym obiektem, niszczy zarówno ten obiekt, jak i samego siebie. Ta sama zasada obowiązuje w przypadku kolizji statku kosmicznego z innym obiektem. Asteroidy będą w tym układzie pasywne, ponieważ nie chcę, aby zachodzące na siebie asteroidy niszczyły się wzajemnie.

Program Astrocrash06 Program Astrocrash06 realizuje całe niezbędne wykrywanie kolizji dzięki wykorzystaniu właściwości overlapping_sprites klasy Sprite. Muszę też obsługiwać niszczenie asteroid w specjalny sposób, ponieważ kiedy są niszczone asteroidy dużej i średniej wielkości, tworzone są w miejsce każdej z nich dwie nowe, lecz mniejsze.

Pułapka Ponieważ asteroidy są początkowo generowane w losowo wybranych miejscach, istnieje możliwość, że któraś z nich zostanie utworzona na wierzchu statku kosmicznego gracza, niszcząc statek już w momencie rozpoczęcia programu. Muszę tymczasowo pogodzić się z tą niedogodnością, ale będę musiał rozwiązać ten problem w ostatecznej wersji gry.

Program w działaniu został pokazany na rysunku 12.13. Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 12.; nazwa pliku to astrocrash06.py.

Modyfikacja metody update() klasy Missile Na końcu metody update() klasy Missile dodaję następujący kod: # sprawdź, czy pocisk zachodzi na jakiś inny obiekt if self.overlapping_sprites:

Obsługa kolizji

Rysunek 12.13. Teraz pociski statku kosmicznego niszczą asteroidy, ale uważaj — asteroidy mogą zniszczyć statek for sprite in self.overlapping_sprites: sprite.die() self.die()

Jeśli pocisk zachodzi na jakieś inne obiekty, zarówno w kontekście tych innych obiektów, jak i pocisku, jest wywoływana metoda die(). Jest to nowa metoda, którą dodam do klas Asteroid, Ship i Missile.

Dodanie metody die() do klasy Missile W klasie Missile, podobnie jak w każdej innej klasie w tej wersji gry, jest potrzebna metoda die(). Metoda jest prawie tak prosta, jak tylko to możliwe: def die(self): """ Zniszcz pocisk. """ self.destroy()

Kiedy zostaje wywołana metoda die() obiektu klasy Missile, obiekt sam się niszczy.

391

392

Rozdział 12. Dźwięk, animacja i rozwijanie programu. Gra Astrocrash

Modyfikacja metody update() klasy Ship Na końcu metody update() klasy Ship dodaję następujący kod: # sprawdź, czy statek nie zachodzi na jakiś inny obiekt if self.overlapping_sprites: for sprite in self.overlapping_sprites: sprite.die() self.die()

Jeśli statek zachodzi na jakieś inne obiekty, zarówno w kontekście tych innych obiektów, jak i statku jest wywoływana metoda die(). Zwróć uwagę, że dokładnie taki sam kod pojawia się również w metodzie update() klasy Missile. Jak już wcześniej wspomniałem, kiedy widzisz zdublowany kod, powinieneś pomyśleć o tym, jak go skonsolidować. W następnej wersji gry pozbędę się zarówno tego, jak i innych fragmentów redundantnego kodu.

Dodanie metody die() do klasy Ship Ta metoda jest identyczna jak metoda die() w klasie Missile: def die(self): """ Zniszcz statek. """ self.destroy()

Gdy zostaje wywołana metoda die() obiektu klasy Ship, obiekt sam się niszczy.

Dodanie stałej klasowej do klasy Asteroid Do klasy Asteroid dodaję jedną stałą klasową: SPAWN = 2

Stała SPAWN określa liczbę nowych asteroid, jakie powstają po zniszczeniu asteroidy macierzystej.

Dodanie metody die() do klasy Asteroid def die(self): """ Zniszcz asteroidę. """ # jeśli nie jest to mała asteroida, zastąp ją dwoma mniejszymi if self.size != Asteroid.SMALL: for i in range(Asteroid.SPAWN): new_asteroid = Asteroid(x = self.x, y = self.y, size = self.size - 1) games.screen.add(new_asteroid) self.destroy()

Dodanie efektów eksplozji

Utrudnienie, jakie dodaję w tym miejscu, polega na tym, że metoda die() klasy Asteroid zawiera potencjał tworzenia nowych obiektów tej klasy. Metoda sprawdza, czy niszczona asteroida nie należy do kategorii małych asteroid. Jeśli do niej nie należy, zostają utworzone dwie nowe asteroidy, o jeden rozmiar mniejsze, w miejscu aktualnego położenia asteroidy macierzystej. Czy nowe asteroidy zostały utworzone, czy też nie, wcześniej istniejąca asteroida sama się niszczy i metoda się kończy.

Dodanie efektów eksplozji W poprzedniej wersji gry gracz mógł niszczyć asteroidy, strzelając w nie pociskami, lecz ich destrukcji brakowało nieco wyrazu. Więc w następnym kroku dodaję do gry eksplozje.

Program Astrocrash07 W programie Astrocrash07 piszę nową klasę, opartą na klasie games.Animation, obsługującą animowane eksplozje. Wykonuję też pewną pracę niewidoczną dla użytkownika, konsolidując redundantny kod. Nawet jeśli gracz nie doceni tych dodatkowych zmian, to i tak są one ważne. Na rysunku 12.14 pokazuję nowy program w akcji.

Rysunek 12.14. Teraz wszystkim przypadkom destrukcji w grze towarzyszą potężne eksplozje

393

394

Rozdział 12. Dźwięk, animacja i rozwijanie programu. Gra Astrocrash

Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 12.; nazwa pliku to astrocrash07.py.

Klasa Wrapper Rozpoczynam od pracy na zapleczu. Tworzę nową klasę, Wrapper, opartą na klasie games.Sprite.

Metoda update() Klasa Wrapper zawiera metodę update(), która po przekroczeniu przez obiekt krawędzi ekranu automatycznie przenosi go na krawędź przeciwległą, tak że jego tor ruchu jakby „owija” ekran: class Wrapper(games.Sprite): """ Duszek, którego tor lotu owija się wokół ekranu. """ def update(self): """ Przenieś duszka na przeciwległy brzeg ekranu. """ if self.top > games.screen.height: self.bottom = 0 if self.bottom < 0: self.top = games.screen.height if self.left > games.screen.width: self.right = 0 if self.right < 0: self.left = games.screen.width

Powyższy kod widziałeś już kilkakrotnie. Powoduje owinięcie toru lotu duszka wokół ekranu. Kiedy teraz oprę pozostałe klasy występujące w tej grze na klasie Wrapper, jej metoda update() może utrzymywać instancje tych pozostałych klas w obrębie ekranu — a kod może istnieć tylko w jednym miejscu!

Metoda die() Kod klasy kończę metodą die(), która niszczy obiekt: def die(self): """ Zniszcz się. """ self.destroy()

Klasa Collider Następnie biorę się za inny redundantny kod. Zauważyłem, że zarówno klasa Ship, jak i Missile dzielą takie same instrukcje obsługujące kolizje, więc postanowiłem utworzyć

Dodanie efektów eksplozji

nową klasę, Collider (opartą na klasie Wrapper), reprezentującą obiekty, których tor lotu owija się wokół ekranu i które mogą się zderzać z innymi obiektami.

Metoda update() Oto metoda update() obsługująca kolizje: def update(self): """ Sprawdź, czy duszki nie zachodzą na siebie. """ super(Collider, self).update() if self.overlapping_sprites: for sprite in self.overlapping_sprites: sprite.die() self.die()

Moją pierwszą czynnością w metodzie update() klasy Collider jest wywołanie metody update() jej nadklasy (czyli metody update() klasy Wrapper) w celu utrzymania obiektu w obrębie ekranu. Potem sprawdzam, czy nie występują kolizje. Jeśli nasz obiekt zachodzi na jakiekolwiek inne obiekty, wywołuje metodę die() tych innych obiektów, a potem jego własną metodę die().

Metoda die() Następnie tworzę metodę dla opisywanej klasy, ponieważ wszystkie obiekty klasy Collider robią to samo, kiedy kończą swoje istnienie — tworzą eksplozję i niszczą się same. def die(self): """ Zniszcz się i pozostaw po sobie eksplozję. """ new_explosion = Explosion(x = self.x, y = self.y) games.screen.add(new_explosion) self.destroy()

W tej metodzie tworzę obiekt klasy Explosion. To nowa klasa, której obiektami są animacje eksplozji. Wkrótce ujrzysz tę klasę w pełni jej blasku.

Modyfikacja klasy Asteroid Modyfikuję klasę Asteroid w taki sposób, aby była oparta na klasie Wrapper: class Asteroid(Wrapper):

Teraz klasa Asteroid dziedziczy metodę update() po klasie Wrapper i dlatego wycinam jej własną metodę update(). Redundantny kod zaczyna znikać! Jedyną rzeczą, jaką jeszcze robię w tej klasie, jest zmiana ostatniego wiersza jej metody die(). Wywołanie self.die() zastępuję wierszem: super(Asteroid, self).die()

Odtąd zawsze, kiedy zmienię metodę die() klasy Wrapper, klasa Asteroid automatycznie zbierze wynikające z tego korzyści.

395

396

Rozdział 12. Dźwięk, animacja i rozwijanie programu. Gra Astrocrash

Modyfikacja klasy Ship Modyfikuję klasę Ship tak, aby była oparta na klasie Collider: class Ship(Collider):

W metodzie update() klasy Ship dodaję wiersz: super(Ship, self).update()

Mogę teraz wyciąć kilka dalszych fragmentów redundantnego kodu. Ponieważ kolizje obsługuje metoda update() klasy Collider, wycinam kod wykrywania kolizji z metody update() klasy Ship. A ponieważ metoda update() klasy Collider wywołuje metodę update() klasy Wrapper, z metody update() klasy Ship wycinam także kod obsługujący powracanie obiektu na ekran. Wycinam również z klasy Ship metodę die(), ponieważ dziedziczy ją ona po klasie Collider.

Modyfikacja klasy Missile W trakcie modyfikacji klasy Missile, zmieniam nagłówek klasy w taki sposób, aby klasa była oparta na klasie Collider: class Missile(Collider):

W metodzie update() klasy Missile dodaję wiersz super(Missile, self).update()

Podobnie jak w przypadku klasy Ship, mogę teraz wyciąć z klasy Missile redundantny kod. Ponieważ kolizje obsługuje metoda update() klasy Collider, wycinam kod wykrywania kolizji z metody update() klasy Missile. A ponieważ metoda update() klasy Collider wywołuje metodę update() klasy Wrapper, z metody update() klasy Missile wycinam także kod obsługujący powracanie obiektu na ekran. Wycinam również z klasy Missile metodę die(), ponieważ dziedziczy ją ona po klasie Collider.

Wskazówka Aby pomóc sobie w zrozumieniu tych wszystkich zmian, jakie opisuję, zajrzyj do kompletnego kodu wszystkich wersji programu Astrocrash zamieszczonego na stronie internetowej tej książki www.courseptr.com/downloads.

Klasa Explosion Ponieważ chcę tworzyć animowane eksplozje, napisałem klasę Explosion opartą na klasie games.Animation. class Explosion(games.Animation): """ Animacja eksplozji. """ sound = games.load_sound("eksplozja.wav") images = ["eksplozja1.bmp", "eksplozja2.bmp",

Dodanie poziomów gry, rejestracji wyników oraz tematu muzycznego "eksplozja3.bmp", "eksplozja4.bmp", "eksplozja5.bmp", "eksplozja6.bmp", "eksplozja7.bmp", "eksplozja8.bmp", "eksplozja9.bmp"]

Definiuję zmienną klasową sound reprezentującą efekt dźwiękowy towarzyszący eksplozji. Definiuję także zmienną klasową images, przypisując do niej listę nazw plików zawierających dziewięć ramek służących do animacji eksplozji. Następnie piszę konstruktor klasy Explosion. def __init__(self, x, y): super(Explosion, self).__init__(images = Explosion.images, x = x, y = y, repeat_interval = 4, n_repeats = 1, is_collideable = False) Explosion.sound.play()

Konstruktor klasy Explosion przyjmuje wartości reprezentujące współrzędne ekranowe eksplozji poprzez parametry x i y. Kiedy wywołuję konstruktor nadklasy (games.Animation), przekazuję te wartości znów jako parametry x i y, aby animacja została odtworzona dokładnie tam, gdzie chcę. Do konstruktora klasy nadrzędnej przekazuję również poprzez parametr images listę nazw plików graficznych, Explosion.images. Parametrowi n_repeats nadaję wartość 1, aby animacja była odtwarzana tylko raz. A parametrowi repat_interval nadaję wartość 4, aby szybkość animacji była taka jak należy. Parametrowi is_collideable nadaję wartość False, aby mogące się przydarzyć zachodzenie innych duszków na animację eksplozji nie liczyło się jako kolizja. Na koniec odtwarzam efekt dźwiękowy eksplozji za pomocą wywołania metody Explosion.sound.play().

Sztuczka Pamiętaj, że do konstruktora klasy games.Animation możesz przekazać albo listę nazw plików, albo listę obiektów obrazu reprezentującą ramki animacji.

Dodanie poziomów gry, rejestracji wyników oraz tematu muzycznego Do gry trzeba dodać jeszcze kilka elementów, aby mogła być postrzegana jako kompletna. Moim ostatnim posunięciem jest dodanie poziomów gry — co oznacza, że wtedy, gdy gracz zniszczy wszystkie asteroidy znajdujące się na ekranie, pojawia się nowa, liczniejsza grupa tych obiektów. Dodaję również funkcjonalność rejestracji dorobku punktowego gracza oraz wzmagający napięcie temat muzyczny, aby było można w pełni przeżywać grę.

397

398

Rozdział 12. Dźwięk, animacja i rozwijanie programu. Gra Astrocrash

Program Astrocrash08 Oprócz poziomów gry, rejestracji zdobytych punktów i tematu muzycznego dodaję trochę kodu, którego efekty mogą być mniej oczywiste dla gracza, lecz który ma pomimo to istotne znaczenie dla kompletności programu. Na rysunku 12.15 pokazuję moją ostateczną wersję tej gry.

Rysunek 12.15. Ostateczny szlif umożliwia kontynuowanie gry tak długo, jak na to pozwalają umiejętności gracza

Kod tego programu możesz znaleźć na stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze rozdziału 12.; nazwa pliku to astrocrash08.py.

Import modułu color Pierwszy dodatek jest dosyć prosty. Z pakietu livewires oprócz modułu games importuję moduł color: from livewires import games, color

Potrzebuję modułu color, aby komunikat „Koniec gry” mógł zostać wyświetlony w ładnym, jaskrawoczerwonym kolorze.

Dodanie poziomów gry, rejestracji wyników oraz tematu muzycznego

Klasa Game Pod koniec programu dodaję klasę Game — nową klasę, służącą do utworzenia obiektu reprezentującego samą grę. Tworzenie obiektu mającego reprezentować grę może się z początku wydawać nieco dziwnym pomysłem, ale nabierze ono sensu, gdy się nad tym dłużej zastanowisz. Sama gra mogłaby z pewnością stanowić obiekt z takimi metodami jak play(), służąca do rozpoczęcia gry, advance(), umożliwiająca podniesienie gry na kolejny poziom, oraz end(), pozwalająca zakończyć grę. Decyzja projektowa o reprezentowaniu gry przez obiekt ułatwia innym obiektom przesyłanie do gry komunikatów. Na przykład w sytuacji, gdy na aktualnym poziomie gry zostaje zniszczona ostatnia asteroida, mogłaby przesłać do gry komunikat z żądaniem przejścia do następnego poziomu. Albo wtedy, gdy zostaje zniszczony statek, mógłby przesłać do gry komunikat, że powinna się zakończyć. Kiedy będę omawiał klasę Game, zapewne zauważysz, że duża część kodu zawartego w funkcji main() została włączona do tej klasy.

Metoda __init__() Pierwszą rzeczą, jaką robię w klasie Game, jest zdefiniowanie konstruktora: class Game(object): """ Sama gra. """ def __init__(self): """ Inicjalizuj obiekt klasy Game. """ # ustaw poziom self.level = 0 # załaduj dźwięk na podniesienie poziomu self.sound = games.load_sound("poziom.wav") # utwórz wynik punktowy self.score = games.Text(value = 0, size = 30, color = color.white, top = 5, right = games.screen.width - 10, is_collideable = False) games.screen.add(self.score) # utwórz statek kosmiczny gracza self.ship = Ship(game = self, x = games.screen.width/2, y = games.screen.height/2) games.screen.add(self.ship)

Atrybut level reprezentuje aktualny numer poziomu gry. Atrybut sound odpowiada za efekt dźwiękowy towarzyszący podniesieniu poziomu gry. Atrybut score reprezentuje wynik punktowy gry — to obiekt klasy Text, który ukazuje się w prawym górnym rogu ekranu. Właściwość is_collideable tego obiektu ma wartość False, co oznacza, że wynik

399

400

Rozdział 12. Dźwięk, animacja i rozwijanie programu. Gra Astrocrash

nie występuje w żadnych kolizjach — więc statek gracza nie zderzy się z wynikiem i nie dojdzie do eksplozji! W końcu ship to atrybut reprezentujący statek kosmiczny gracza.

Metoda play() Następnie definiuję metodę play(), która rozpoczyna grę. def play(self): """ Przeprowadź grę. """ # rozpocznij odtwarzanie tematu muzycznego games.music.load("temat.mid") games.music.play(-1) # załaduj i ustaw tło nebula_image = games.load_image("mglawica.jpg") games.screen.background = nebula_image # przejdź do poziomu 1 self.advance() # rozpocznij grę games.screen.mainloop()

Metoda ta ładuje temat muzyczny i odtwarza go w niekończącej się pętli. Ładuje obraz mgławicy i ustawia go jako tło. Następnie wywołuje własną metodę obiektu klasy Game o nazwie advance(), która podnosi grę na kolejny poziom. (Wszystkiego o metodzie advance() dowiesz się w następnym podpunkcie). Na koniec metoda play() wywołuje metodę games.screen.mainloop(), aby całą grę wprowadzić w ruch!

Metoda advance() Metoda advance() podnosi grę na kolejny poziom. Zwiększa numer poziomu, tworzy nową falę asteroid, wyświetla krótko na ekranie numer poziomu oraz odtwarza dźwięk obwieszczający zmianę poziomu gry. Moja pierwsza czynność w tej metodzie jest dość prosta — zwiększam numer poziomu: def advance(self): """ Przejdź do następnego poziomu gry. """ self.level += 1

Następnie przechodzę do najciekawszej części metody — utworzenia nowej fali asteroid. Każdy poziom rozpoczyna się od liczby asteroid równej jego numerowi. Więc pierwszy poziom rozpoczyna się od jednej asteroidy, drugi — od dwóch itd. Mimo że utworzenie grupy asteroid jest proste, muszę uzyskać pewność, że żadna nowa asteroida nie zostanie utworzona na wierzchu statku kosmicznego. Inaczej statek wybuchnie w momencie rozpoczęcia nowego poziomu gry. # wielkość obszaru ochronnego wokół statku przy tworzeniu asteroid BUFFER = 150 # utwórz nowe asteroidy

Dodanie poziomów gry, rejestracji wyników oraz tematu muzycznego for i in range(self.level): # oblicz współrzędne x i y zapewniające minimum odległości od statku # określ minimalną odległość wzdłuż osi x oraz wzdłuż osi y x_min = random.randrange(BUFFER) y_min = BUFFER - x_min # wyznacz odległość wzdłuż osi x oraz wzdłuż osi y # z zachowaniem odległości minimalnej x_distance = random.randrange(x_min, games.screen.width - x_min) y_distance = random.randrange(y_min, games.screen.height - y_min) # oblicz położenie na podstawie odległości x = self.ship.x + x_distance y = self.ship.y + y_distance # jeśli to konieczne, przeskocz między krawędziami ekranu x %= games.screen.width y %= games.screen.height # utwórz asteroidę new_asteroid = Asteroid(game = self, x = x, y = y, size = Asteroid.LARGE) games.screen.add(new_asteroid)

Stała BUFFER reprezentuje wielkość bezpiecznej strefy, jaką chcę mieć wokół statku. Następnie uruchamiam pętlę. W trakcie każdej iteracji tworzę nową asteroidę w bezpiecznej odległości od statku. Wartość zmiennej x_min wyznacza minimalną odległość miejsca utworzenia nowej asteroidy od statku obliczoną wzdłuż osi x, podczas gdy zmienna y_min reprezentuje minimalną odległość miejsca utworzenia nowej asteroidy od statku, obliczoną wzdłuż osi y. Wprowadzam zmienność poprzez wykorzystanie modułu random, ale suma wartości x_min i y_min zawsze jest równa stałej BUFFER. Wartość zmiennej x_distance to odległość miejsca utworzenia nowej asteroidy wzdłuż osi x. Jest losowo wybraną liczbą, która jednak daje pewność, że nowa asteroida zostanie utworzona w odległości od statku co najmniej równej x_min. Natomiast zmienna y_distance reprezentuje odległość miejsca utworzenia nowej asteroidy obliczoną wzdłuż osi y. Jest losowo wybraną liczbą, która jednak daje pewność, że nowa asteroida zostanie utworzona w odległości od statku co najmniej równej y_min. Wartość zmiennej x to współrzędna x nowej asteroidy. Obliczam ją poprzez dodanie liczby x_distance do wartości współrzędnej x statku kosmicznego. Potem upewniam się, że wartość x nie usytuuje asteroidy poza ekranem, wymuszając „obieganie” ekranu1 za pomocą operatora modulo. Z kolei wartość zmiennej y to współrzędna y nowej asteroidy. Obliczam ją poprzez dodanie liczby y_distance do wartości współrzędnej y statku kosmicznego. Potem upewniam się, że wartość y nie umieszcza asteroidy poza ekranem, 1

Można sobie wyobrazić ekran jako powierzchnię walca powstałego przez sklejenie jego prawej i lewej krawędzi — przyp. tłum.

401

402

Rozdział 12. Dźwięk, animacja i rozwijanie programu. Gra Astrocrash

wymuszając „obieganie” ekranu2 za pomocą operatora modulo. Następnie wykorzystuję tak obliczone wartości x i y do utworzenia nowiuteńkiej asteroidy. Zauważ, że w konstruktorze klasy Asteroid pojawił się nowy parametr, game. Zapamiętaj, że ponieważ każda asteroida musi mieć możliwość wywołania metody obiektu klasy Game, każdy obiekt klasy Asteroid musi zawierać referencję do obiektu klasy Game. Więc przekazuję obiekt self do parametru game, który zostanie wykorzystany przez konstruktor klasy Asteroid do zdefiniowania atrybutu reprezentującego grę. Moje ostatnie czynności w metodzie advance() to wyświetlenie nowego numeru poziomu oraz odtworzenie dźwięku obwieszczającego podwyższenie poziomu gry: # wyświetl numer poziomu level_message = games.Message(value = "Poziom " + str(self.level), size = 40, color = color.yellow, x = games.screen.width/2, y = games.screen.width/10, lifetime = 3 * games.screen.fps, is_collideable = False) games.screen.add(level_message) # odtwórz dźwięk przejścia do nowego poziomu (nie dotyczy pierwszego poziomu) if self.level > 1: self.sound.play()

Metoda end() Metoda end() wyświetla dużymi, czerwonymi literami komunikat „Koniec gry” na środku ekranu przez mniej więcej pięć sekund. Potem gra się kończy i ekran graficzny zostaje zamknięty. def end(self): """ Zakończ grę. """ # pokazuj komunikat 'Koniec gry' przez 5 sekund end_message = games.Message(value = "Koniec gry", size = 90, color = color.red, x = games.screen.width/2, y = games.screen.height/2, lifetime = 5 * games.screen.fps, after_death = games.screen.quit, is_collideable = False) games.screen.add(end_message)

2

Tym razem można sobie wyobrazić ekran jako powierzchnię walca powstałego przez sklejenie jego dolnej i górnej krawędzi — przyp. tłum.

Dodanie poziomów gry, rejestracji wyników oraz tematu muzycznego

Dodanie do klasy Asteroid zmiennej i stałej klasowej Dokonuję w klasie Asteroid kilku zmian związanych z dodaniem poziomów gry i rejestrowaniem wyniku. Dodaję stałą klasową POINTS: POINTS = 30

Stała ta będzie odgrywać rolę wartości bazowej służącej do wyznaczenia liczby punktów określających wartość asteroidy. Rzeczywista wartość punktowa będzie modyfikowana zgodnie z wielkością asteroidy — mniejsze asteroidy będą warte więcej punktów niż większe. Aby móc zmieniać poziom gry, program musi rozpoznać moment, w którym wszystkie asteroidy występujące na aktualnym poziomie gry zostały zniszczone. Dlatego też śledzę i rejestruję całkowitą liczbę asteroid za pomocą nowej zmiennej klasowej, total, którą definiuję w początkowej części kodu klasy: total = 0

Modyfikacja konstruktora klasy Asteroid W konstruktorze dodaję wiersz zwiększający wartość zmiennej Asteroid.total: Asteroid.total += 1

Chcę teraz, aby każda asteroida miała możliwość przesyłania komunikatu do obiektu klasy Game, więc dostarczam do każdego obiektu klasy Asteroid referencję do obiektu klasy Game. W konstruktorze klasy Asteroid przyjmuję obiekt klasy Game poprzez utworzenie nowego parametru: def __init__(self, game, x, y, size):

Parametr game przyjmuje obiekt klasy Game, który następnie wykorzystuję do utworzenia atrybutu w nowym obiekcie klasy Asteroid: self.game = game

Tak więc każdy nowy obiekt klasy Asteroid ma atrybut game, który jest referencją do samej gry. Dzięki temu atrybutowi obiekt klasy Asteroid może wywołać metodę obiektu klasy Game, taką jak advance().

Modyfikacja metody die() klasy Asteroid Wprowadzam kilka dodatków do metody die() w klasie Asteroid. Najpierw zmniejszam wartość zmiennej Asteroid.total: Asteroid.total -= 1

Następnie zwiększam wynik punktowy na podstawie wartości stałej Asteroid.POINTS oraz rozmiaru asteroidy (mniejsze asteroidy są warte więcej punktów niż mniejsze). Chcę mieć również pewność, że wynik będzie zawsze wyrównywany do prawej strony, więc ustawiam na nowo właściwość right obiektu score na 10 pikseli od prawej krawędzi ekranu.

403

404

Rozdział 12. Dźwięk, animacja i rozwijanie programu. Gra Astrocrash self.game.score.value += int(Asteroid.POINTS / self.size) self.game.score.right = games.screen.width - 10

Kiedy tworzę każdą z dwóch nowych asteroid, muszę przekazać referencję do obiektu klasy Game, czego dokonuję poprzez zmodyfikowanie pierwszego wiersza wywołania konstruktora klasy Asteroid: new_asteroid = Asteroid(game = self.game,

Pod koniec metody die() klasy Asteroid badam wartość zmiennej Asteroid.total, aby sprawdzić, czy wszystkie asteroidy zostały zniszczone. Jeśli rzeczywiście tak jest, ostatnia asteroida wywołuje metodę advance() obiektu klasy Game, która przenosi grę na następny poziom oraz tworzy nową grupę asteroid. # jeśli wszystkie asteroidy zostały zniszczone, przejdź do następnego poziomu if Asteroid.total == 0: self.game.advance()

Dodanie stałej klasowej do klasy Ship Wprowadzam kilka dodatków do klasy Ship. Tworzę stałą klasową VELOCITY_MAX, którą wykorzystuję do ograniczenia maksymalnej prędkości statku kosmicznego gracza: VELOCITY_MAX = 3

Modyfikacja konstruktora klasy Ship Podobnie jak obiekt klasy Asteroid, obiekt klasy Ship musi mieć dostęp do obiektu klasy Game, aby mógł wywołać jego metodę. Tak jak to zrobiłem w przypadku klasy Asteroid, modyfikuję konstruktor klasy Ship: def __init__(self, game, x, y):

Nowy parametr, game, przyjmuje jako swoją wartość obiekt klasy Game, której potem używam do utworzenia atrybutu obiektu klasy Ship: self.game = game

Więc każdy obiekt klasy Ship ma atrybut game, który jest referencją do samej gry. Dzięki temu atrybutowi obiekt klasy Ship może wywołać metodę obiektu klasy Game, taką jak end().

Modyfikacja metody update() klasy Ship W metodzie update() klasy Ship, ograniczam poszczególne składowe prędkości obiektu tej klasy, dx i dy, wykorzystując stałą klasową VELOCITY_MAX: # ogranicz prędkość w każdym kierunku self.dx = min(max(self.dx, -Ship.VELOCITY_MAX), Ship.VELOCITY_MAX) self.dy = min(max(self.dy, -Ship.VELOCITY_MAX), Ship.VELOCITY_MAX)

Podsumowanie

Powyższy kod daje pewność, że dx i dy nigdy nie będą miały wartości mniejszej niż Ship.VELOCITY_MAX oraz większej niż Ship.VELOCITY_MAX. Aby to osiągnąć, skorzystałem z funkcji min() i max(). Funkcja min() zwraca wartość minimalną dwóch liczb, podczas gdy funkcja max() zwraca wartość maksymalną dwóch liczb. Ograniczam prędkość statku, aby uniknąć potencjalnych problemów, łącznie z wpadaniem statku na swoje własne pociski.

Dodanie do klasy Ship metody die() Kiedy statek gracza zostaje zniszczony, gra się kończy. Dodaję do klasy Ship metodę die(), która wywołuje metodę end() obiektu klasy Game w celu zakończenia gry. def die(self): """ Zniszcz statek i zakończ grę. """ self.game.end() super(Ship, self).die()

Funkcja main() Teraz, skoro mam klasę Game, funkcja main() staje się całkiem krótka. Wszystko, co mam w tej funkcji do zrobienia, to utworzenie obiektu klasy Game oraz wywołanie metody play() tego obiektu, aby uruchomić grę. def main(): astrocrash = Game() astrocrash.play() # wystartuj! main()

Podsumowanie W tym rozdziale rozszerzyłeś swoją wiedzę o programowaniu multimedialnym na obszar dźwięku, muzyki i animacji. Dowiedziałeś się, jak ładować i odtwarzać pliki dźwiękowe i muzyczne oraz jak zatrzymywać ich odtwarzanie. Zobaczyłeś również, jak tworzy się animacje. Nauczyłeś się również techniki tworzenia dużych programów poprzez pisanie coraz bardziej kompletnych, roboczych wersji finalnego produktu. Zobaczyłeś, jak można każdorazowo stawiać sobie jeden nowy cel do realizacji, budując w ten sposób swoją drogę do pełnego programu. Na koniec zobaczyłeś, jak wszystkie te nowe informacje i techniki zostały wykorzystane przy tworzeniu rozgrywanej w szybkim tempie gry akcji z efektami dźwiękowymi, animacją i własną muzyką.

405

406

Rozdział 12. Dźwięk, animacja i rozwijanie programu. Gra Astrocrash

Sprawdź swoje umiejętności 1. Ulepsz grę Astrocrash poprzez utworzenie nowego rodzaju śmiercionośnego gruzu kosmicznego. Nadaj temu nowemu typowi kosmicznych śmieci pewną cechę, która odróżni je od asteroid. Na przykład zniszczenie takiego obiektu mogłoby wymagać dwóch uderzeń pocisku. 2. Napisz wersję gry Simon Says, w której gracz ma za zadanie powtarzanie coraz bardziej skomplikowanych, przypadkowych sekwencji kolorów i dźwięków przy użyciu klawiatury. 3. Napisz własną wersję innej klasycznej gry wideo, takiej jak Space Invaders lub Pac-Man. 4. Wykreuj swoje własne programistyczne wyzwanie, lecz co najważniejsze, nigdy nie rezygnuj z mobilizowania się do dalszej nauki.

A Opis pakietu livewires S

trona internetowa tej książki jest dostępna pod adresem http://www.helion.pl/ksiazki/pytdk3.htm. Możesz z niej pobrać cały kod źródłowy, pliki pomocnicze oraz pakiety oprogramowania opisane w tym tekście.

Pliki archiwów Dostępne są dwa pliki do pobrania:  py3e_source.zip — zawiera kod źródłowy i pliki pomocnicze do każdego

kompletnego programu zaprezentowanego w tej książce;  py3e_software.zip — zawiera pliki wszystkich pakietów oprogramowania

opisanych w tej książce, łącznie z instalatorem Pythona 3.1.1 dla systemu Windows. Tabela A.1 opisuje zawartość archiwum py3e_source.zip, podczas gdy tabela A.2 wymienia szczegółowo zawartość archiwum py3e_software.zip. Tabela A.1. Zawartość archiwum py3e_source.zip

Nazwa folderu

Opis

rozdzial01

Kod źródłowy do rozdziału 1.

rozdzial02

Kod źródłowy do rozdziału 2.

rozdzial03

Kod źródłowy do rozdziału 3.

rozdzial04

Kod źródłowy do rozdziału 4.

rozdzial05

Kod źródłowy do rozdziału 5.

rozdzial06

Kod źródłowy do rozdziału 6.

rozdzial07

Kod źródłowy i pliki z danymi do rozdziału 7.

rozdzial08

Kod źródłowy do rozdziału 8.

rozdzial09

Kod źródłowy do rozdziału 9.

408

Dodatek A. Strona internetowa książki Tabela A.1. Zawartość archiwum py3e_source.zip (ciąg dalszy)

Nazwa folderu

Opis

rozdzial10

Pliki z kodem źródłowym i odpowiadające im pliki wsadowe Windows do rozdziału 10.

rozdzial11

Pliki z kodem źródłowym, odpowiadające im pliki wsadowe Windows oraz pliki multimedialne do rozdziału 11.

rozdzial12

Pliki z kodem źródłowym, odpowiadające im pliki wsadowe Windows oraz pliki multimedialne do rozdziału 12.

Tabela A.2. Zawartość archiwum py3e_software.zip

Nazwa folderu

Opis

python

Instalator Windows Pythona 3.1.1.

pygame

Instalator Windows pakietu pygame 1.9.1 do Pythona 3.1.x.

livewires

Pakiet silnika gier livewires.

Wskazówka Instalator Windows pakietu pygame zawarty w pliku py3e_software.zip jest kompatybilny z Pythonem 3.1.x, co oznacza zgodność z każdą podwersją Pythona 3.1 (od Pythona 3.1.0 do Pythona 3.1.9).

B Opis pakietu livewires T

en dodatek zawiera prawie wszystko, co chciałbyś wiedzieć o zmodyfikowanej wersji pakietu livewires, a o co bałbyś się zapytać. Opuszczam pewne fragmenty dla uproszczenia — jeśli potrzebna Ci jest maksymalnie dokładna „dokumentacja”, zawsze możesz zajrzeć do samego kodu źródłowego modułów pakietu.

Pakiet livewires Tabela B.1. Moduły pakietu livewires

Moduł

Opis

games

Definiuje funkcje i klasy, które ułatwiają tworzenie gier.

color

Przechowuje zestaw stałych reprezentujących kolory.

Klasy modułu games Moduł games zawiera grupę klas i funkcji pomocnych przy programowaniu gier. Tabela B.2 zawiera opis klas. Tabela B.2. Klasy modułu games

Klasa

Opis

Screen

Obiekt tej klasy reprezentuje ekran graficzny.

Sprite

Obiekt tej klasy zawiera obraz oraz może zostać wyświetlony na ekranie graficznym.

Text

Obiekt tej klasy reprezentuje tekst na ekranie graficznym. Text to podklasa klasy Sprite.

Message

Obiekt tej klasy reprezentuje komunikat wyświetlany na ekranie graficznym; komunikat ten znika po ustalonym czasie. Message jest podklasą klasy Text.

410

Dodatek B. Opis pakietu livewires Tabela B.2. Klasy modułu games (ciąg dalszy)

Klasa

Opis

Animation

Obiekt tej klasy reprezentuje serię obrazów pokazywanych na ekranie graficznym jeden po drugim. Animation to podklasa klasy Sprite.

Mouse

Obiekt tej klasy daje dostęp do myszy.

Keyboard

Obiekt tej klasy daje dostęp do klawiatury.

Music

Obiekt tej klasy daje dostęp do kanału muzycznego.

Klasa Screen Obiekt klasy Screen reprezentuje ekran graficzny. Funkcja games.init() tworzy obiekt klasy Screen o nazwie screen, który reprezentuje ekran graficzny. Generalnie powinieneś używać obiektu screen, zamiast konkretyzować własny obiekt klasy Screen. W tabeli B.3 zostały opisane właściwości klasy Screen, podczas gdy tabela B.4 wyszczególnia metody tej klasy. Tabela B.3. Właściwości klasy Screen

Właściwość

Opis

width

Szerokość ekranu.

height

Wysokość ekranu.

fps

Liczba wskazująca, ile razy na sekundę ekran jest aktualizowany.

background

Obraz tła ekranu.

all_objects

Lista wszystkich duszków na ekranie.

event_grab

Wartość typu Boolean, która ustala, czy dane wejściowe są przechwytywane na ekran. True oznacza przechwytywanie danych wejściowych, False — brak przechwytywania.

Tabela B.4. Metody klasy Screen

Metoda

Opis

get_width()

Zwraca szerokość ekranu.

get_height()

Zwraca wysokość ekranu.

get_fps()

Zwraca liczbę, która wskazuje, ile razy na sekundę ekran jest aktualizowany.

get_background()

Zwraca obraz tła ekranu.

set_background (new_background)

Ustawia new_background jako obraz tła ekranu.

get_all_objects()

Zwraca listę wszystkich duszków na ekranie.

get_event_grab()

Zwraca stan przechwytywania danych wejściowych przez ekran. True jeśli dane wejściowe są przechwytywane. False jeśli nie są przechwytywane.

Klasy modułu games Tabela B.4. Metody klasy Screen (ciąg dalszy)

Metoda

Opis

set_event_grab (new_status)

Ustawia stan przechwytywania danych wejściowych przez ekran na new_status. Wartość True oznacza, że dane wejściowe mają być przechwytywane, a wartość False wyłącza przechwytywanie danych wejściowych.

add(sprite)

Dodaje sprite, obiekt klasy Sprite (lub obiekt podklasy klasy Sprite) do ekranu graficznego.

remove(sprite)

Usuwa sprite, obiekt klasy Sprite (lub obiekt podklasy klasy Sprite) z ekranu graficznego.

clear()

Usuwa wszystkie duszki z ekranu graficznego.

mainloop()

Uruchamia główną pętlę ekranu graficznego.

quit()

Zamyka okno graficzne.

Klasa Sprite Obiekt klasy Sprite ma obraz i może być wyświetlany na ekranie graficznym. W tabeli B.5 zostały opisane właściwości klasy Sprite, podczas gdy w tabeli B.6 wyszczególniono metody tej klasy. Tabela B.5. Właściwości klasy Sprite

Właściwość

Opis

image

Obiekt obrazu duszka.

width

Szerokość obrazu duszka.

height

Wysokość obrazu duszka.

angle

Położenie kątowe w stopniach.

x

Współrzędna x.

y

Współrzędna y.

position

Pozycja duszka. Dwuelementowa krotka, która zawiera współrzędną x oraz współrzędną y obiektu.

top

Współrzędna y górnego brzegu duszka.

bottom

Współrzędna y dolnego brzegu duszka.

left

Współrzędna x lewego brzegu duszka.

right

Współrzędna x prawego brzegu duszka.

dx

Prędkość wzdłuż osi x.

dy

Prędkość wzdłuż osi y.

velocity

Prędkość duszka. Dwuelementowa krotka, która zawiera prędkość obiektu wzdłuż osi x oraz prędkość obiektu wzdłuż osi y.

overlapping_sprites

Lista innych obiektów, które zachodzą na duszka.

411

412

Dodatek B. Opis pakietu livewires Tabela B.5. Właściwości klasy Sprite (ciąg dalszy)

Właściwość

Opis

is_collideable

Ustala, czy duszek może, czy nie może wchodzić w kolizje. Wartość True oznacza, że ewentualne kolizje duszka są rejestrowane, a wartość False oznacza, że duszek nie może pojawić się w kolizjach.

interval

Ustala interwał metody tick() obiektu.

Tabela B.6. Metody klasy Sprite

Metoda

Opis

__init__(image [, angle] [, x] [, y] [, top] [, bottom] [, left] [, right] [, dx] [, dy] [, interval] [, is_collideable])

Inicjalizuje nowego duszka. Parametr image nie ma wartości domyślnej, więc niezbędne jest jej przekazanie. Parametry angle, x, y, dx i dy mają wartość domyślną 0. Parametry top, bottom, left i right mają wartość domyślną None. Jeśli do parametrów nie zostanie przekazana żadna wartość, odpowiadającym im właściwościom nie zostanie przypisana wartość przy inicjalizacji. Parametr interval ma wartość domyślną 1. Parametr is_collideable ma wartość domyślną True.

get_image()

Zwraca obiekt obrazu duszka.

set_image(new_image)

Ustawia obiekt obrazu duszka na new_image.

get_height()

Zwraca wysokość obrazu duszka.

get_width()

Zwraca szerokość obrazu duszka.

get_angle()

Zwraca aktualny kąt położenia duszka w stopniach.

set_angle(new_angle)

Ustawia kąt położenia duszka na new_angle.

get_x()

Zwraca współrzędną x duszka.

set_x(new_x)

Ustawia współrzędną x duszka na new_x.

get_y()

Zwraca współrzędną y duszka.

set_y(new_y)

Ustawia współrzędną y duszka na new_y.

get_position()

Zwraca współrzędne x i y duszka jako dwuelementową krotkę.

set_position(new_position)

Ustawia współrzędne x i y duszka zgodnie z dwuelementową krotką new_position.

get_top()

Zwraca współrzędną y górnego brzegu duszka.

set_top(new_top)

Ustawia współrzędną y górnego brzegu duszka na new_top.

get_bottom()

Zwraca współrzędną y dolnego brzegu duszka.

set_bottom(new_bottom)

Ustawia współrzędną y dolnego brzegu duszka na new_bottom.

get_left()

Zwraca współrzędną x lewego brzegu obiektu.

set_left(new_left)

Ustawia współrzędną x lewego brzegu duszka na new_left.

get_right()

Zwraca współrzędną x prawego brzegu obiektu.

Klasy modułu games Tabela B.6. Metody klasy Sprite (ciąg dalszy)

Metoda

Opis

set_right(new_right)

Ustawia współrzędną x prawego brzegu duszka na new_right.

get_dx()

Zwraca prędkość duszka w kierunku osi x.

set_dx(new_dx)

Ustawia prędkość duszka w kierunku osi x na new_dx.

get_dy()

Zwraca prędkość duszka w kierunku osi y.

set_dy(new_dy)

Ustawia prędkość duszka w kierunku osi y na new_dy.

get_velocity()

Zwraca prędkość duszka w kierunku osi x oraz prędkość duszka w kierunku osi y jako dwuelementową krotkę.

set_velocity(new_velocity)

Ustawia prędkość duszka w kierunku osi x oraz prędkość duszka w kierunku osi y zgodnie z dwuelementową krotką new_velocity.

get_overlapping_sprites()

Zwraca listę wszystkich duszków z właściwością collideable ustawioną na True, które zachodzą na obiekt.

overlaps(other)

Zwraca True, jeśli obiekt other zachodzi na obiekt self, i False w przeciwnym wypadku.

get_is_collideable()

Zwraca status duszka w odniesieniu do kolizji. Wartość True oznacza, że ewentualne kolizje duszka są rejestrowane, a wartość False oznacza, że duszek nie może pojawić się w kolizjach.

set_is_collideable (new_status)

Ustawia status duszka w odniesieniu do kolizji na new_status. Wartość True oznacza, że ewentualne kolizje duszka będą rejestrowane, a wartość False oznacza, że duszek nie może pojawić się w kolizjach.

get_interval()

Zwraca interwał metody tick() duszka.

set_interval(new_interval)

Ustawia interwał metody tick() duszka na new_interval.

update()

Aktualizuje duszka. Domyślnie nic nie robi. Wywoływana automatycznie w każdym cyklu pętli mainloop(). Możesz przesłonić tę metodę w podklasie klasy Sprite.

tick()

Jest wykonywana co tyle cykli pętli mainloop(), ile wskazuje właściwość interval obiektu. Domyślnie nic nie robi. Możesz przesłonić tę metodę w podklasie klasy Sprite.

destroy()

Usuwa duszka z ekranu.

Klasa Text Text jest podklasą klasy Sprite. Obiekt klasy Text reprezentuje tekst na ekranie graficznym. Oczywiście klasa Text dziedziczy atrybuty, właściwości i metody klasy Sprite. W tabeli B.7 zostały opisane dodatkowe właściwości klasy Text, podczas gdy

w tabeli B.8 wyszczególniono dodatkowe metody tej klasy.

413

414

Dodatek B. Opis pakietu livewires Tabela B.7. Właściwości klasy Text

Właściwość

Opis

value

Wartość wyświetlana w postaci tekstu.

size

Rozmiar czcionki.

color

Kolor tekstu. Może zostać ustawiony przy użyciu wartości z modułu color.

Klasa Text używa właściwości value, size i color do tworzenia obiektu graficznego reprezentującego wyświetlany tekst. Tabela B.8. Metody klasy Text

Metoda

Opis

__init__(value, size, color [, angle] [, x] [, y] [, top] [, bottom] [, left] [, right] [, dx] [, dy] [, interval] [, is_collideable])

Inicjalizuje nowy obiekt. Parametr value to wartość, która ma być wyświetlana jako tekst. Parametr size to rozmiar czcionki. Parametr color to kolor tekstu. Parametry angle, x, y, dx i dy mają wartość domyślną 0. Parametry top, bottom, left i right mają wartość domyślną None. Jeśli do parametrów nie zostanie przekazana żadna wartość, odpowiadającym im właściwościom nie zostanie przypisana wartość przy inicjalizacji. Parametr interval ma wartość domyślną 1. Parametr is_collideable ma wartość domyślną True.

get_value()

Zwraca wartość wyświetlaną jako tekst.

set_value(new_value)

Ustawia wartość wyświetlaną jako tekst na new_value.

get_size()

Zwraca rozmiar czcionki tekstu.

set_size(new_size)

Ustawia rozmiar czcionki tekstu na new_size.

get_color()

Zwraca kolor tekstu.

set_color(new_color)

Ustawia kolor tekstu na new_color. Można do tego użyć wartości z modułu color.

Klasa Message Message to podklasa klasy Text. Obiekt klasy Message reprezentuje komunikat na ekranie graficznym, który znika po określonym czasie. Obiekt klasy Message może również

zawierać specyfikację zdarzenia, które ma wystąpić po zniknięciu obiektu. Klasa Message dziedziczy atrybuty, właściwości i metody klasy Text. Obiekt klasy Message ma jednak nowy atrybut, ustawiany poprzez parametr after_death — kod, jaki ma zostać wykonany po zniknięciu obiektu, reprezentowany na przykład przez nazwę funkcji lub metody. Jego wartość domyślna to None. W klasie Message została zdefiniowana nowa metoda __init__(): __init__(value, size, color [, angle] [, x] [, y] [, top] [, bottom] [, left] [, right] [, dx] [, dy] [, lifetime] [, is_collideable] [, after_death]). Metoda ta inicjalizuje nowy obiekt. Parametr value to wartość, która ma być wyświetlana jako tekst. Parametr size

Klasy modułu games

to rozmiar czcionki. Parametr color to kolor tekstu. Parametry angle, x, y, dx i dy mają wartość domyślną 0. Parametry top, bottom, left i right mają wartość domyślną None. Jeśli do parametrów nie zostanie przekazana żadna wartość, odpowiadającym im właściwościom nie zostanie przypisana wartość przy inicjalizacji. Parametr lifetime reprezentuje czas wyświetlania komunikatu na ekranie wyrażony w cyklach pętli mainloop(), po upływie którego obiekt ulegnie samozniszczeniu. Jeśli nadasz parametrowi wartość 0, obiekt nie podlega samolikwidacji. Parametr is_collideable ma wartość domyślną True, a wartością domyślną parametru after_death jest None.

Wskazówka Wartość parametru lifetime zostaje po prostu przypisana do właściwości interval obiektu klasy Message. Obiekt klasy Message nie ma atrybutu ani właściwości lifetime.

Klasa Animation Klasa Animation jest podklasą klasy Sprite. Obiekt klasy Animation reprezentuje serię obrazów wyświetlanych jeden po drugim. Klasa Animation dziedziczy atrybuty, właściwości i metody klasy Sprite. W klasie Animation zostały zdefiniowane dodatkowe atrybuty, opisane w tabeli B.9. Tabela B.9. Atrybuty klasy Animation

Atrybut

Opis

images

Lista obiektów obrazu.

n_repeats

Liczba wskazująca, ile razy powinien zostać powtórzony całkowity cykl animacji. Ustawiany za pośrednictwem parametru n_repeats. Wartość parametru równa 0 (domyślna) oznacza powtarzanie bez końca.

W klasie Animation została zdefiniowana nowa metoda __init__(): __init__(images [, angle] [, x] [, y] [, top] [, bottom] [, left] [, right] [, dx] [, dy] [, repeat_interval] [, n_repeats] [, is_collideable]). Metoda ta inicjalizuje nowy obiekt. Poprzez parametr images mogą zostać przekazane albo obiekty obrazów, albo nazwy plików w postaci łańcuchów znaków, na podstawie których zostaną utworzone obiekty obrazów. Parametry angle, x, y, dx i dy mają wartość domyślną 0. Parametry top, bottom, left i right mają wartość domyślną None. Jeśli do parametrów nie zostanie przekazana żadna wartość, odpowiadającym im właściwościom nie zostanie przypisana wartość przy inicjalizacji. Parametr repeat_interval ustala interwał metody tick() obiektu, a więc tym samym szybkość animacji. Jego wartość domyślna to 1. Wartością domyślną parametru n_repeats jest 0, a parametru is_collideable — True.

415

416

Dodatek B. Opis pakietu livewires

Wskazówka Wartość parametru repeat_interval zostaje po prostu przypisana do właściwości interval obiektu klasy Animation. Obiekt klasy Animation nie ma atrybutu ani właściwości repeat_interval.

Klasa Mouse Obiekt klasy Mouse obsługuje dostęp do myszy. Funkcja games.init() tworzy obiekt klasy Mouse, dostępny poprzez zmienną mouse, w celu wykorzystania go przy odczytywaniu pozycji myszy lub sprawdzaniu, czy przyciski myszy są naciśnięte. Ogólnie rzecz biorąc, powinieneś używać obiektu mouse, zamiast konkretyzować swój własny obiekt na podstawie klasy Mouse. W tabeli B.10 zostały opisane właściwości klasy Mouse, podczas gdy w tabeli B.11 wyszczególniono metody tej klasy. Tabela B.10. Właściwości klasy Mouse

Właściwość

Opis

x

Współrzędna x wskaźnika myszy.

y

Współrzędna y wskaźnika myszy.

position

Pozycja wskaźnika myszy. Dwuelementowa krotka zawierająca współrzędne x i y wskaźnika myszy.

is_visible

Wartość typu Boolean dotycząca widoczności wskaźnika myszy. Wartość True oznacza, że wskaźnik jest widoczny, a wartość False oznacza, że wskaźnik nie jest widoczny. Wartością domyślną jest True.

Tabela B.11. Metody klasy Mouse

Metoda

Opis

get_x()

Zwraca współrzędną x wskaźnika myszy.

set_x(new_x)

Ustawia współrzędną x wskaźnika myszy na new_x.

get_y()

Zwraca współrzędną y wskaźnika myszy.

set_y(new_y)

Ustawia współrzędną y wskaźnika myszy na new_y.

get_position()

Zwraca współrzędne x i y wskaźnika myszy jako dwuelementową krotkę.

set_position(new_position)

Ustawia współrzędne x i y wskaźnika myszy zgodnie z zawartością dwuelementowej krotki.

set_is_visible(new_visibility)

Ustawia widoczność wskaźnika myszy. Jeśli wartością parametru new_visibility jest True wskaźnik będzie widoczny; jeśli wartość new_visibility wynosi False, wskaźnik będzie niewidoczny.

is_pressed(button_number)

Sprawdza, czy przycisk jest naciśnięty. Zwraca wartość True, jeśli przycisk button_number jest naciśnięty; w przeciwnym wypadku zwraca wartość False.

Funkcje modułu games

Klasa Keyboard Obiekt klasy Keyboard obsługuje dostęp do klawiatury. Funkcja games.init() tworzy obiekt klasy Keyboard, dostępny poprzez zmienną keyboard, w celu wykorzystania go przy sprawdzaniu, czy określone klawisze są naciśnięte. Na ogół powinieneś używać obiektu keyboard, zamiast konkretyzować swój własny obiekt na podstawie klasy Keyboard. Klasa zawiera jedną metodę, is_pressed(key), która zwraca wartość True, jeśli sprawdzany klawisz, key, jest naciśnięty, a False w sytuacji przeciwnej. W module games zdefiniowano stałe reprezentujące klawisze, które możesz wykorzystać w roli argumentu tej metody. Lista tych stałych została zaprezentowana w tym dodatku, w podrozdziale „Stałe modułu games”

Klasa Music Obiekt klasy Music umożliwia dostęp do pojedynczego kanału muzycznego, pozwalając na ładowanie, odtwarzanie oraz zatrzymywanie odtwarzania pliku muzycznego. Na ogół powinieneś wykorzystywać obiekt music, zamiast konkretyzować własny obiekt klasy Music. Kanał muzyczny akceptuje wiele różnych typów plików, w tym WAV, MP3, OGG oraz MIDI. Metody klasy Music zostały wymienione w tabeli B.12. Tabela B.12. Metody klasy Music

Metoda

Opis

load(filename)

Ładuje plik filename do kanału muzycznego, zastępując nim aktualnie załadowany plik muzyczny.

play([loop])

Odtwarza muzykę załadowaną do kanału muzycznego i dodatkowo powtarza to odtwarzanie tyle razy, ile wskazuje parametr loop. Wartość -1 oznacza powtarzanie bez końca. Wartość domyślna parametru loop to 0.

fadeout(millisec)

Stopniowo wycisza aktualnie odtwarzaną muzykę w ciągu millisec milisekund.

stop()

Zatrzymuje odtwarzanie muzyki na kanale muzycznym.

Funkcje modułu games W module games zostały zdefiniowane funkcje służące do pracy z obrazami i dźwiękiem, opisane w tabeli B.13.

417

418

Dodatek B. Opis pakietu livewires Tabela B.13. Funkcje modułu games

Funkcja

Opis

init([screen_width,] [screen_height,] [fps])

Inicjalizuje ekran graficzny o szerokości screen_width i wysokości screen_height, który jest aktualizowany fps razy na sekundę. Tworzy obiekt klasy games.Screen, który umożliwia dostęp do ekranu gry. Ponadto po imporcie modułu games, zostają utworzone: obiekt mouse klasy games.Mouse, który umożliwia dostęp do myszy gracza, obiekt keyboard klasy games.Keyboard, który umożliwia dostęp do klawiatury gracza, oraz obiekt music klasy games.Music, który umożliwia dostęp do pojedynczego kanału muzycznego.

load_image(filename [, transparent])

Zwraca obiekt obrazu załadowany z pliku o nazwie podanej w postaci łańcucha znaków filename i ustawia przezroczystość obrazu, jeśli wartością parametru transparent jest True. Wartość domyślna parametru transparent to True.

scale_image(image, x_scale [,y_scale])

Zwraca nowy obiekt obrazu przeskalowany w kierunku osi x współczynnikiem x_scale, a w kierunku osi y współczynnikiem y_scale. Jeśli do y_scale nie zostanie przekazana żadna wartość, obraz zostanie przeskalowany współczynnikiem x_scale w obu kierunkach. Oryginalny obraz image pozostaje niezmieniony.

load_sound(filename)

Zwraca obiekt dźwiękowy utworzony na podstawie pliku WAV o nazwie zawartej w łańcuchu filename.

Obiekt dźwiękowy zwrócony przez funkcję load_sound() ma do dyspozycji kilka metod, które zostały wymienione w tabeli B.14. Tabela B.14. Metody obiektu dźwiękowego

Metoda

Opis

play([loop])

Odtwarza dźwięk tyle razy, ile wskazuje parametr loop, oprócz początkowego odtworzenia. Wartość -1 oznacza powtarzanie odtwarzania w nieskończoność. Wartość domyślna parametru loop to 0.

fadeout(millisec)

Stopniowo wycisza dźwięk w ciągu millisec milisekund.

stop()

Zatrzymuje odtwarzanie dźwięku na wszystkich kanałach.

Stałe modułu games W module games zdefiniowano listę stałych reprezentujących klawisze klawiatury komputera. Kompletna ich lista została podana w tabeli B.15.

Stałe modułu games Tabela B.15. Stałe modułu games reprezentujące klawisze

Stała

Klawisz

K_BACKSPACE

Backspace

K_TAB

Tab

K_RETURN

Enter

K_PAUSE

Pause

K_ESCAPE

Esc

K_SPACE

Klawisz spacji

K_EXCLAIM

Wykrzyknik

K_QUOTEDBL

Cudzysłów podwójny

K_HASH

Znak kratki (#)

K_DOLLAR

Znak dolara

K_AMPERSAND

Znak etki (&)

K_QUOTE

Cudzysłów pojedynczy

K_LEFTPAREN

Lewy nawias okrągły

K_RIGHTPAREN

Prawy nawias okrągły

K_ASTERISK

Asterysk (*)

K_PLUS

Znak plusa

K_COMMA

Przecinek

K_MINUS

Znak minusa

K_PERIOD

Kropka

K_SLASH

Prawy ukośnik

K_0

0

K_1

1

K_2

2

K_3

3

K_4

4

K_5

5

K_6

6

K_7

7

K_8

8

K_9

9

K_COLON

Dwukropek

K_SEMICOLON

Średnik

K_LESS

Znak mniejszości

K_EQUALS

Znak równości

K_GREATER

Znak większości

419

420

Dodatek B. Opis pakietu livewires Tabela B.15. Stałe modułu games reprezentujące klawisze (ciąg dalszy)

Stała

Klawisz

K_QUESTION

Pytajnik

K_AT

Znak „at” (@)

K_LEFTBRACKET

Lewy nawias kwadratowy

K_BACKSLASH

Lewy ukośnik

K_RIGHTBRACKET

Prawy nawias kwadratowy

K_CARET

Kareta (^)

K_UNDERSCORE

Znak podkreślenia

K_a

A

K_b

B

K_c

C

K_d

D

K_e

E

K_f

F

K_g

G

K_h

H

K_i

I

K_j

J

K_k

K

K_l

L

K_m

M

K_n

N

K_o

O

K_p

P

K_q

Q

K_r

R

K_s

S

K_t

T

K_u

U

K_v

V

K_w

W

K_x

X

K_y

Y

K_z

Z

K_DELETE

Delete

K_KP0

0 (klawiatura numeryczna)

Stałe modułu games Tabela B.15. Stałe modułu games reprezentujące klawisze (ciąg dalszy)

Stała

Klawisz

K_KP1

1 (klawiatura numeryczna)

K_KP2

2 (klawiatura numeryczna)

K_KP3

3 (klawiatura numeryczna)

K_KP4

4 (klawiatura numeryczna)

K_KP5

5 (klawiatura numeryczna)

K_KP6

6 (klawiatura numeryczna)

K_KP7

7 (klawiatura numeryczna)

K_KP8

8 (klawiatura numeryczna)

K_KP9

9 (klawiatura numeryczna)

K_KP_PERIOD

Kropka (kl. num.)

K_KP_DEVIDE

Znak dzielenia (kl. num.)

K_KP_MULTIPLY

Znak mnożenia (kl. num.)

K_KP_MINUS

Minus (kl. num.)

K_KP_PLUS

Plus (kl. num.)

K_KP_ENTER

Enter (kl. num.)

K_KP_EQUALS

Znak równości (kl. num.)

K_UP

Strzałka w górę

K_DOWN

Strzałka w dół

K_RIGHT

Strzałka w prawo

K_LEFT

Strzałka w lewo

K_INSERT

Insert

K_HOME

Home

K_END

End

K_PAGEUP

Page Up

K_PAGEDOWN

Page Down

K_F1

F1

K_F2

F2

K_F3

F3

K_F4

F4

K_F5

F5

K_F6

F6

K_F7

F7

K_F8

F8

K_F9

F9

K_F10

F10

421

422

Dodatek B. Opis pakietu livewires Tabela B.15. Stałe modułu games reprezentujące klawisze (ciąg dalszy)

Stała

Klawisz

K_F11

F11

K_F12

F12

K_NUMLOCK

Num Lock

K_CAPSLOCK

Caps Lock

K_SCROLLOCK

Scroll Lock

K_RSHIFT

Prawy Shift

K_LSHIFT

Lewy Shift

K_RCTRL

Prawy Ctrl

K_LCTRL

Lewy Ctrl

K_RALT

Prawy Alt

K_LALT

Lewy Alt

K_RSUPER

Prawy Windows

K_LSUPER

Lewy Windows

K_HELP

Help

K_PRINT

Print Screen

K_BREAK

Break

Stałe modułu color Moduł color udostępnia pewne stałe, których możesz użyć wszędzie, gdzie moduł games wymaga specyfikacji koloru. Te stałe są następujące:  red  green  blue  black  white  dark_red  dark_green  dark_blue  dark_grey  gray  light_gray  yellow  brown  pink  purple

Skorowidz A abstrakcja, 171 aktualizacja zmiennej, 79 algorytm, 93 anagram, 129 animacja, 361, 368, 370 argument, 23 nazwany, 176, 178 pozycyjny, 178 wyjątku, 218 ASCII-Art, 36 atrybut, 227, 232 atrybut klasy, 236, 238 Animation, 415 Message, 342 atrybuty prywatne, 241

B blok kodu, 70 błąd, 23 błąd logiczny, 54, 55

C ciało pętli, 78 cudzysłów, 32 podwójny, 34 pojedynczy, 34 potrójny, 35

D DBMS, Database Management System, 90 definiowanie funkcji, 170 klasy, 229 konstruktora, 301 metody, 229 dekorator, 239 dodanie obiektu do ekranu, 335, 338, 341 pary klucz-wartość, 156 dokumentowanie funkcji, 171 domyślne wartości parametrów, 177, 179 dostęp do atrybutów, 235, 238, 245 do atrybutów prywatnych, 242 do elementów zagnieżdżonych, 146 do elementu łańcucha, 110 do kodu źródłowego, 19 do metod prywatnych, 243 do pliku binarnego, 210 do pliku tekstowego, 203 do wartości słownika, 153

do właściwości, 248 do zagnieżdżonych krotek, 148 do zamarynowanych obiektów, 213 do znaku łańcucha, 109 sekwencyjny, 107 swobodny, 107 duszek, sprite, 332, 335 dziedziczenie, 263, 265 dzielenie całkowite, 44 zmiennoprzecinkowe, 44 dzwonek systemowy, 39 dźwięk, 361, 371

E ekran, 341, 344 ekran graficzny, 327, 331 element, 153 elementy interfejsu GUI, 292 etykieta, 296, 298

F fałsz, 85 funkcja, 22 __additional_cards(), 285 __init__(), 285, 354–357, 379, 399 __pass_time(), 250 advance(), 400

424

Python dla każdego. Podstawy programowania

funkcja append(), 142 ask_number(), 189 ask_yes_no(), 189 capitalize(), 53 change_global(), 184 check_catch(), 355 check_drop(), 358 close(), 208 computer_move(), 192 congrat_winner, 195 count(), 144 create_widgets(), 318, 319 die(), 391–395, 403 display(), 173 display_board(), 190 display_instruct(), 188 dump(), 212 eat(), 250 end(), 402 end_game(), 356 get(), 155, 159 human_move(), 192 handle_caught(), 356 index(), 144 init(), 348 input(), 49, 60 insert(), 144 instructions(), 170 int(), 58 is_pressed(), 364 items(), 159 keys(), 159 legal_moves(), 190 len(), 106, 125, 137 load(), 212 lower(), 53 main(), 195, 222, 287, 359, 405 mainloop(), 322, 349, 365 new_board(), 189 next_block(), 221, 223 next_line(), 220

next_turn(), 195 open_file, 220 pieces(), 189 play(), 251, 286 pop(), 144 print(), 22, 34, 35 randint(), 66 random.randrange(), 111 randrange(), 66 range(), 102, 103 read_global(), 184 read(), 208 readline(), 204, 208 readlines(), 205, 208 remove(), 142 replace(), 53 reverse(), 144 shelve.open(), 212 sort(), 143 strip(), 53 swapcase(), 53 talk(), 250 tell_story(), 321 title(), 53 update(), 345, 354, 380–395, 404 upper(), 52 values(), 159 welcome(), 221 winner(), 191 write(), 207 writelines(), 207 funkcje konwersji typów, 58 modułu games, 417 modułu pickle, 212

G generowanie błędu, 23 liczb losowych, 64 losowej pozycji, 130

gra Astrocrash, 361, 376 Blackjack, 255, 277 Jaka to liczba, 63, 95 Kółko i krzyżyk, 167, 185 Mad Lib, 289, 323 Pizza Panic, 323, 353 Szubienica, 133, 159 Turniej wiedzy, 199, 218 Wymieszane litery, 99, 128 graficzny interfejs użytkownika, GUI, 15, 289, 300 granice ekranu, 344 grupowanie obiektów, 262 GUI, Graphical User Interface, 16, 289, 300 GUI toolkit, 291

H hermetyzacja, 174 hermetyzacja obiektów, 240

I IDLE, 20 import modułu, 65, 96, 276 indeksowanie krotek, 126 list, 137 łańcuchów, 107 inicjalizacja atrybutów, 234 ekranu graficznego, 327 zmiennych, 163 instalacja w systemie Windows, 19 instrukcja, 23 break, 87 continue, 87

Skorowidz

if, 67, 68, 71 przypisania, 46 try, 215

J języki obiektowe, 17 wysokiego poziomu, 17

K klasa, 227, 229 Animation, 370, 415 Application, 301 Asteroid, 379, 392, 403 BJ_Card, 281 BJ_Dealer, 284 BJ_Deck, 281 BJ_Game, 284 BJ_Hand, 282 BJ_Player, 283 Card, 259 Chef, 357 Collider, 394 Critter, 249 Explosion, 396 Game, 399 Hand, 261 Keyboard, 417 Label, 312 Message, 340, 414 Missile, 386, 390, 396 Mouse, 416 Music, 417 Pan, 354 Pizza, 355 Screen, 410 Ship, 381–396, 404 Sprite, 333–336, 342, 367, 411 Text, 338, 413 Wrapper, 394

klasy bazowe, 265, 270 modułu games, 327, 409 pochodne, 266, 272 programu Blackjack, 279 klauzula elif, 73, 75 else, 71, 73, 218 except, 215 klawisz, 419–422 Enter, 61 Tab, 38 klient funkcji, 240 klucz, 110, 154 kod, 23 kod samodokumentujący, 48 kolizje, 350, 352, 390 kombinacje obiektów, 259 komentarz, 27, 59, 96 komunikat, 256, 339 konfiguracja w innych systemach, 20 w systemie Windows, 19 konfigurowanie programu, 188 konkatenacja krotek, 127 list, 137 łańcuchów, 41 konkretyzacja obiektu, 230 konstruktor, 230, 232, 249 konwersja łańcuchów na liczby, 57 wartości, 56 kończenie programu, 28, 97 krotka, 99, 120 funkcja len(), 125 indeksowanie, 126 jako warunek, 122 konkatenacja, 127 niemutowalność, 127

operator in, 125 wycinanie, 126 wypisywanie, 123 zawierająca elementy, 122 kryptografia, 68

L liczby całkowite, 44, 57 losowe, 64 pseudolosowe, 64 zmiennoprzecinkowe, 44 liczenie co pięć, 104 do przodu, 103 do tyłu, 105 lista, 133 funkcja len(), 137 indeksowanie, 137 konkatenacja, 137 mutowalność, 138 operator in, 137 przypisanie wartości elementowi, 138 przypisanie wartości wycinkowi, 139 usuwanie elementu, 139 usuwanie wycinka, 139 lista mailingowa, 18 literał, 23

Ł ładowanie dźwięku, 372 muzyki, 374 obrazu, 330, 333 łańcuch, 23, 28, 105–115 łańcuch dokumentacyjny, 171

425

426

Python dla każdego. Podstawy programowania

M marynowanie, 209, 210 menedżer układu Grid, 305, 306 metoda, Patrz funkcja metody, 229 instancji, 229, 230 klasy bazowej, 271 klasy Mouse, 416 klasy Music, 417 klasy Screen, 410 klasy Sprite, 412, 413 klasy Text, 414 listy, 140, 144 łańcucha, 52, 53 obiektu dźwiękowego, 418 obiektu pliku, 208 obiektu screen, 328 prywatne, 241, 243 słownika, 159 statyczne, 236, 239 moduł, 274 color, 338, 398, 422 decimal, 45 games, 327, 353, 418–422 karty, 277 math, 382 pakietu livewires, 409 pickle, 211 random, 65, 96 shelve, 212 tkinter, 295, 297, 301, 318 modyfikowanie metod, 269 okna głównego, 296 mutowalność, 111 mutowalność list, 138 muzyka, 371, 374 mysza, 348

N nadklasa, superclass, 271 nazwa zmiennej, 47 niemutowalność krotek, 127 łańcuchów, 111 notacja z kropką, 66 numery pozycji dodatnie, 109 ujemne, 110

O obiekt, 230 klasy Card, 261 klasy Ship, 382 klasy Sprite, 342 klasy Text, 338 modułu games, 327 mouse, 350 screen, 328, 341 obiekty graficzne, 331 programowe, 225 obliczanie liczby sekund, 61 wagi, 61 obsługa danych wejściowych, 347 kolizji, 352, 390 wyjątków, 214–217 zdarzeń, 292, 303 odbieranie komunikatów, 256 odczyt klawiatury, 363 odczytywanie danych, 200 wartości zmiennej, 183 z pliku, 203, 211 z wiersza, 204 zawartości pliku, 205

odmarynowanie, 211 odtwarzanie dźwięku, 373, 374 muzyki, 375 okno główne, root window, 293 graficzne, 325 konsoli, 16 Python Shell, 21 OOP, object-oriented programming, 17, 225 opcja Edit with IDLE, 27 operacje na liczbach, 42 operator in, 107, 125, 137, 154 logiczny and, 91 logiczny not, 90 logiczny or, 92 modulo, 44 operatory matematyczne, 44 porównania, 69, 70 operatory rozszerzonego przypisania, 59 sekwencji, 105 otrzymywanie informacji, 173 wartości, 175 otwieranie pliku, 202

P pakiet livewires, 325, 347, 384, 398, 407 pygame, 325 para klucz-wartość, 156–158 parametr, 173 lifetime, 415 pozycyjny, 177 self, 230

Skorowidz

pętla for, 100–102 nieskończona, 79 nieskończona umyślna, 86 while, 76, 78, 129 zdarzeń, 292, 296, 298, 300 piksel, 327 plik analizator_komunikatow. py, 105 astrocrash01.py, 378 astrocrash06.py, 390 dlugowiecznosc.py, 306 dostep_swobodny.py, 108 duszek_pizzy.py, 332 dzwiek_i_muzyka.py, 372 eksplozja.py, 369 fundusz_powierniczy_ zly.py, 54 globalny_zasieg.py, 182 glupie_lancuchy.py, 41 gra_w_karty.py, 259 gry.py, 275 haslo.py, 67 instrukcja.py, 170 inwentarz_bohatera.py, 121 inwentarz_bohatera2.py, 124 inwentarz_bohatera3.py, 135 karty.py, 277 komputer_nastrojow.py, 74 koniec_gry2.py, 33 krajacz_pizzy.py, 116 kwiz.txt, 219 leniwe_przyciski2.py, 301 licznik.py, 102

licznik_klikniec.py, 303 maitre_d.py, 84 manipulacje_cytatami.py, 51 metkownica.py, 297 najlepsze_wyniki.py, 141 najlepsze_wyniki2.py, 147 nieuchwytna_pizza.py, 350 nowe_okno_graficzne.py, 326 obraz_tla.py, 329 obroc_duszka.py, 366 odczytaj_klawisz.py, 363 odczytaj_to.txt, 200 opiekun_zwierzaka.py, 249 osobisty_pozdrawiacz.py, 48 patelnia_w_ruchu.py, 347 pizza_panic.py, 353 pizza_w_ruchu.py, 343 pobierz_i_zwroc.py, 172 pozdrawiacz.py, 45 przegrana_bitwadobry.py, 82 przegrana_bitwa-zly.py, 80 py3e_software.zip, 407 py3e_source.zip, 407 rzut_koscmi.py, 65 sprezysta_pizza.py, 345 szubienica.py, 160 translator_slangu.py, 153 trzylatek.py, 77 uczestnik_funduszu_ powierniczego_ dobry.py, 56 udzielony_odmowiony. py, 71, 88

wybor_filmow.py, 311 wybor_filmow2.py, 315 wybredny_licznik.py, 86 plik wygrales.py, 340 wymieszane_litery.py, 128 wysoki_wynik.py, 337 zabawne_podziekowania. py, 37 zadania_tekstowe.py, 43 zwariowany_lancuch.py, 101 zwierzak_z_klasa.py, 237 zwierzak_ z_konstruktorem.py, 231 zyczenia_urodzinowe.py, 177 pliki dźwiękowe, 361 muzyczne, 361 tekstowe, 200, 209 z obrazami, 370 pobieranie danych, 48 wartości, 156 podświetlanie składni, 24 pola wyboru, 310, 312, 314 polimorfizm, 273 ponowne wykorzystanie kodu, 176 powielanie łańcuchów, 40, 42 powrót do początku pętli, 87 poziomy gry, 397 półka, shelf, 212 prawda, 85 prędkość, 343, 384 procedura obsługi zdarzeń, 292, 303 program Analizator komunikatów, 105 Astrocrash01, 377

427

428

Python dla każdego. Podstawy programowania

program Astrocrash02, 381 Astrocrash03, 382 Astrocrash04, 385 Astrocrash05, 388 Astrocrash06, 390 Astrocrash07, 393 Astrocrash08, 398 Długowieczność, 305 Dostęp swobodny, 108 Duszek pizzy, 332 Dźwięk i muzyka, 371 Ekskluzywna sieć, 88 Eksplozja, 368 Globalny zasięg, 182 Głupie łańcuchy, 40 Gra w karty, 259 Gra w karty 2.0, 264 Gra w karty 3.0, 269 Hasło, 67 Instrukcja, 169 Inwentarz bohatera, 120 Inwentarz bohatera 2.0, 124 Inwentarz bohatera 3.0, 135 Komputer nastrojów, 73 Koniec gry, 15 Kółko i krzyżyk, 194 Krajacz pizzy, 116 Leniwe przyciski, 298 Leniwe przyciski 2, 300 Licznik, 102 Licznik kliknięć, 303 Mad Lib, 289, 318 Maitre D’, 83 Manipulacje cytatami, 51 Metkownica, 296 Najlepsze wyniki, 140 Najlepsze wyniki 2.0, 145 Nieistotne fakty, 31 Nieuchwytna pizza, 350

Nowe okno graficzne, 326 Obraz tła, 329 Obróć duszka, 366 Obsłuż to, 214 Odczytaj klawisz, 363 Odczytaj to, 200 Opiekun zwierzaka, 225, 249 Osobisty pozdrawiacz, 48 Patelnia w ruchu, 347 Pizza w ruchu, 342 Pobierz i zwróć, 172 Pogromca Obcych, 257 Pozdrawiacz, 45 Prosta gra, 274 Prosty interfejs GUI, 293 Prywatny zwierzak, 241 Przegrana bitwa, 80 Rzut kośćmi, 64 Sprężysta pizza, 344 Symulator trzylatka, 77 Translator slangu komputerowego, 152 Turniej wiedzy, 199 Uczestnik funduszu powierniczego, 54 Udzielony-odmówiony, 71 Wybór filmów, 311 program Wybór filmów 2, 315 Wybredny licznik, 86 Wygrałeś, 339 Wymieszane litery, 99 Wysoki wynik, 336 Zabawne podziękowania, 37 Zadania tekstowe, 42 Zamarynuj to, 209 Zapisz to, 206

Zwariowany łańcuch, 100 Zwierzak z atrybutem, 233 Zwierzak z klasą, 237 Zwierzak z konstruktorem, 231 Zwierzak z właściwością, 245 Życzenia urodzinowe, 177 programowanie obiektowe, OOP, 17, 225 sterowane zdarzeniami, 292 w trybie interaktywnym, 21 w trybie skryptowym, 24 projektowanie klas, 279 programów, 93 prywatność obiektu, 244 przechwytywanie sygnałów wejściowych, 349 przeciążanie operatora, 56 przekazywanie wartości, 23 przemieszczanie duszków, 342 przesłanianie metod, 269, 345 przesłonięcie zmiennej globalnej, 184 przesuwanie kursora, 38 przesyłanie komunikatu, 258 przycisk, 298, 299 przyciski opcji, 315–317 pseudokod, 93, 95, 186, 280 punkt tabulacji, 38 puste krotki, 122 pusty wiersz, 28

Skorowidz

R radian, 384 ramka, frame, 297, 369 referencja do widżetu, 312 referencje współdzielone, 145, 151 rejestracja wyników, 397 reprezentowanie danych, 186 rodzaje cudzysłowu, 34 rozgałęzianie kodu, 63, 77 rozpakowanie sekwencji, 147 rozszerzanie klasy, 263 rozszerzanie klasy pochodnej, 266 rozszerzenie .bat, 294 .py, 25

S sekwencja niemutowalna, 111 sekwencje specjalne, 36, 40 zagnieżdżone, 145, 146 składowe prędkości, 384 słownik, 133, 152, 159 sortowanie, 143 specyfikacja typu wyjątku, 215 sprawdzanie klucza, 154 wartości zmiennej, 79 stała, 114, 160, 181 stała klasowa, 383, 388, 392, 403 stałe modułu color, 422 games, 418–422 stan klawiszy, 364 pola wyboru, 314

struktura słownika, 159 sygnał dzwonka systemowego, 39 wejściowy, 349 system zarządzania bazą danych, 90

Ś śledzenie programu, 81 środowisko programowania, 20

T tekst, 336 testowanie stanu klawiszy, 364 Tkinter, 293 tło, 330 tryb interaktywny, 21, 26 skryptowy, 24 tryby dostępu do pliku, 203, 210, 212 tworzenie algorytmów, 93 anagramu, 129 animacji, 368 atrybutów prywatnych, 241 atrybutu klasy, 238 duszka, 335 etykiety, 298 funkcji, 169 interfejsu GUI, 289, 291, 300 klasy, 229, 259 klasy bazowej, 265, 270 klasy pochodnej, 345 kombinacji obiektów, 259

komentarzy, 59, 96 konstruktora, 232 krotek, 120 listy, 135 łańcuchów, 32, 35, 52 menu, 251 metod, 229 metod prywatnych, 243 metod statycznych, 239 modułów, 273, 274 nowego łańcucha, 113, 115, 130 obiektu, 230, 302 okna głównego, 295 okna graficznego, 325 pętli, 96, 102, 163 pól wyboru, 312 procedury obsługi zdarzeń, 304 przycisków, 299 przycisków opcji, 316 pustego łańcucha, 129 pustej krotki, 122 ramki, 297 sekwencji zagnieżdżonych, 146 skutku ubocznego, 193 słowników, 153 stałych, 114, 160 warunków, 69 widżetów, 302, 308 wielu obiektów, 232 właściwości, 246 wycinków, 118, 137 zmiennych, 46 typ danych, 54 typy liczbowe, 44 sekwencji, 105 wyjątków, 216

429

430

Python dla każdego. Podstawy programowania

U układ współrzędnych, 331 umiejscowienie widżetu, 306 umieszczanie na półce, 212 UML, Unified Modeling Language, 258 umyślne pętle nieskończone, 86 uruchamianie programu, 24 ustawienie tła, 330 usuwanie elementu listy, 139 pary klucz-wartość, 158 wycinka listy, 139 używanie cudzysłowów, 32 domyślnych wartości parametrów, 179 etykiet, 296 klas pochodnych, 272 klucza, 154 komentarza, 27 używanie konstruktorów, 230 krotek, 123, 144 list, 144 metod łańcucha, 50 parametrów zwrotnych, 172 przycisków, 298 sekwencji specjalnych, 36 sekwencji zagnieżdżonych, 145 słowników, 152 stałych, 185 widżetów, 305 zmiennych globalnych, 181, 185 znaku kontynuacji wiersza, 42

W wartości mutowalne, 151 początkowe, 96 zwrotne, 23, 172, 174 wartość False, 82, 85 None, 118 True, 82, 85 wartownik, 79 warunek, 69 prosty, 88 złożony, 88 wcięcie, 70 wczytywanie wierszy, 205 wiązanie widżetów, 303 widoczność wskaźnika myszy, 349 widżet, 296 Button, 298, 299 Entry, 305, 308 Frame, 297 Label, 298 Text, 305, 308 właściwości klasy Mouse, 416 klasy Screen, 410 klasy Sprite, 336, 411 klasy Text, 339, 414 obiektu mouse, 350 obiektu screen, 328 właściwość, 245, 246 angle, 367 bottom, 346 mood, 250 still_playing, 285 wprowadzanie danych, 60 współrzędne myszy, 348 punktu, 331

wstawianie tekstu, 309 znaku cudzysłowu, 38 znaku nowego wiersza, 38 wycinanie krotek, 126 łańcuchów, 116 wycinek, 118 wycinki list, 137 wyjątek, 214 wyjątek AttributeError, 242 wyjście z pętli, 87 wykrywanie kolizji, 350, 352 wypisywanie krotki, 123 wartości, 34 znaku lewego ukośnika, 38 wyrażenie, 44 wysyłanie komunikatów, 256 wyświetlanie duszka, 332 komunikatu, 339 menu, 141 obiektu, 236 tekstu, 336 wartości zmiennej, 60 wyników, 142, 148 wywołanie funkcji, 171 wywoływanie metody, 230 klasy bazowej, 271 statycznej, 240

Z zagnieżdżanie wywołań funkcji, 58 zakres, 181 zamykanie pliku, 202

Skorowidz

zapisywanie do pliku, 206, 210 łańcuchów, 207 programu, 24 zdarzenia, 292 złożone struktury danych, 209 zmiana parametru mutowalnego, 193

wartości zmiennej globalnej, 184 zmienna, 45, 46 globalna, 181 lokalna, 181 typu Boolean, 313 znak cudzysłowu, 38 kontynuacji wiersza, 42 kratki, 27

lewego ukośnika, 38 nowego wiersza, 38 tabulacji, 38 zachęty, 21 Zunifikowany Język Modelowania, UML, 258 zwracanie informacji, 174 wartości, 175

431

View more...

Comments

Copyright ©2017 KUPDF Inc.
SUPPORT KUPDF