Professional Documents
Culture Documents
Python Dla Każdego. Podstawy Programowania. Wydanie III PDF
Python Dla Każdego. Podstawy Programowania. Wydanie III PDF
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
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.
P
ragramowanie polega zasadniczo na spowodowaniu, 7,e
Nie jest to za bardzo techniczna definicja, ale mimo to
�
�")r
� tter coś zrobił.
okładna. Dzięki
poznaniu Pythona potrafisz utworzyć program- pro �Miewielkie narzędzie czy
też produkt biznesowy wyposażony w pełni profes�t�iczny interfejs u7,ytkownika
(ang. graphical user interface- GUl). Będzie ca��wój - coś, co sam wykonałeś
- 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,
wkraczasz na drogę programowania �),{ython. Z rozdziału tego dowiesz się:
• co to jest Python i co w nim �Vego wspaniałego;
• jak zainstalować Pytho �a iwoim komputerze;
i_
jak wypisywać tekst �nie;
�
•
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<;dnc? Masz do tego odpowiednie narz<;dzic. Python daje Ci
moc i elastyczność.
�
�
Kod Pythona działa wszędzie("\
��
Python pracuje na każdym sprzęcie, od pal o superkomputera Cray. I jeśli
przypadkiem nie masz superkompu � pokoju, możesz nadal używać Pythona
s
na maszynach z systemem Window · tosh lub Linux. A to tylko początek listy.
Programy napisane w Pythonie ezałeżne od platformy, co oznacza, że
niezależnie od systemu opera �eg6l, z którego korzystałeś przy tworzeniu swojego
i.._
programu, b<;dzic on działa �wolnym innym komputerze, na którym zainstalowano
� �z program na swoim PC, będziesz mógł przesłać pocztą
Pythona. Więc jeśli napi4z._
elektroniczną jego k � jemu przyjacielowi, który używa Linuksa lub swojej cioci,
która posiada M· rogram będzie działał (o ile Twój przyjaciel i ciocia mają Pythona
zainstalowanego na oich komputerach).
przyjazność i otwartość. Tylko takie podejście ma sens, skoro sam język jest tak
przystępny dla początkujących.
�
1. Pobierz instalator Pythona d �
poświęconej książce htt :/
instalacyjny znajduje s·
�
�u Windows dostępny na stronie
.helion.pl!ksiazki!pytdk3.htm. Plik
p�folderze Python folderu Software i ma nazw<;
python-3-l.msiA...\
:
�
Wskazówka
Stronę poświęconą tej książce można znaleźć pod adresem
http:jjwww.helion.pljksiazkijpytdk3.htm. Zawiera ona kod każdego kompletnego
programu prezentowanego na stronach tej książki razem ze wszystkimi niezbędnymi
pomocniczymi plikami i instalatorami oprogramowania. Szczegółowy opis tego,
co jest udostępnione do pobrania, można znaleźć w dodatku A, "Strona
internetowa książki".
20 Rozdział 1. Wprowadzenie. Program Koniec gry
ld Python31
pyt h on
for
lc:\Python31\
windows
< Back
Konfiguracja Pythona
w innych systemach o e
(}i'
acyjnych
Python może pracować w dosłownie d si· tkach innych systemów operacyjnych.
Wi<;c jeśli używasz czegoś innego � indows, nic omieszkaj odwiedzić oficjalnej
��p:�www.python.org, aby pobrać najnowszą wersję
strony internetowej Python
języka dla Twojej maszyny�gląda strona główna Pythona, możesz sprawdzić
na rysunku 1.3. �
l �::n : k
Wprowadzenie do IDLE
Standardowo w skład Pythona wchodzi zintegrowane środowisko programowania o nazwie
IDLE. Środowisko programowania to zestaw narzędzi, które ułatwiają pisanie programów.
Możesz je traktować jak procesor tekstu, którego możesz używać do tworzenia swoich
programów. Ale jest ono czymś wi<;ccj niż miejscem do tworzenia, zapisywania na dysku
i edytowania Twojego kodu - udostępnia tryb interaktywny oraz tryb skryptowy.
Wprowadzenie do IDLE 21
Rysunek 1.3. Odwiedź stronę główną Pythona, aby pobrać jego najnowszą wersję
i przeczytać masę informacji o języku
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 23
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.
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 "<pyshell#0>", line 1, in <module>
primt("Koniec gry")
NameError: name 'primt' is not defined
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.
26 Rozdział 1. Wprowadzenie. Program 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
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.
To Twój stary przyjaciel — funkcja print. Ten wiersz, dokładnie tak samo
jak w trybie interaktywnym, wyświetla komunikat Koniec gry.
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.
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.
print("tylko",
"nieco",
"większy.")
print(
"""
_ __ ____ _ _ _____ ______ _____
| |/ / / __ \ | \ | | |_ _| | ____| / ____|
| ' / | | | | | \| | | | | |__ | |
| < | | | | | . ` | | | | __| | |
| . \ | |__| | | |\ | _| |_ | |____ | |____
|_|\_\ \____/ |_| \_| |_____| |______| \_____|
_____ _____ __ __
/ ____| | __ \ \ \ / /
| | __ | |__) | \ \_/ /
| | |_ | | _ / \ /
| |__| | | | \ \ | |
\_____| |_| \_\ |_|
"""
)
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.
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”.
"""
_ __ ____ _ _ _____ ______ _____
| |/ / / __ \ | \ | | |_ _| | ____| / ____|
| ' / | | | | | \| | | | | |__ | |
| < | | | | | . ` | | | | __| | |
| . \ | |__| | | |\ | _| |_ | |____ | |____
|_|\_\ \____/ |_| \_| |_____| |______| \_____|
_____ _____ __ __
/ ____| | __ \ \ \ / /
| | __ | |__) | \ \_/ /
| | |_ | | _ / \ /
| |__| | | | \ \ | |
\_____| |_| \_\ |_|
"""
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.).
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 \\ \\ \\ \\ \\ \\ \\")
# dzwonek systemowy
print("\a")
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.
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ł!")
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.
42 Rozdział 2. Typy, zmienne i proste operacje wejścia-wyjścia. Program Nieistotne fakty
Powielanie łańcuchów
Kolejna nowa koncepcja zaprezentowana w omawianym programie została zilustrowana
w poniższym wierszu:
print("Lody!" * 10
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.
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.
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)
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.
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.
name = "Ludwik"
print(name)
print("Cześć,", name)
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 47
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)
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
48 Rozdział 2. Typy, zmienne i proste operacje wejścia-wyjścia. Program Nieistotne fakty
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.
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
print(name)
print("Cześć,", name)
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ź
50 Rozdział 2. Typy, zmienne i proste operacje wejścia-wyjścia. Program Nieistotne fakty
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.
print("\nDużymi literami:")
print(quote.upper())
print("\nMałymi literami:")
52 Rozdział 2. Typy, zmienne i proste operacje wejścia-wyjścia. Program Nieistotne fakty
print(quote.lower())
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 53
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 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.
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 55
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).
"""
)
print("\nOgółem:", total)
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.
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.
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 57
"""
)
print("\nOgółem:", total)
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
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.
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.
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.
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ść.
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.
62 Rozdział 2. Typy, zmienne i proste operacje wejścia-wyjścia. Program Nieistotne fakty
Kiedy graczowi udaje się odgadnąć wygenerowaną przez komputer liczbę, gra się kończy.
Rysunek 3.1 pokazuje grę Jaka to liczba? w działaniu.
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.
import random
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.
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.
Rysunek 3.4. Zgadłeś! Powinienem był wybrać lepsze hasło niż „sekret”
# Hasło
# Demonstruje instrukcję if
if password == "sekret":
print("Dostęp został udzielony")
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 69
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.
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.
Pułapka
Python nie pozwala na wykonywanie pewnych porównań. Obiekty różnych typów,
które nie mają ustalonej definicji porządku, nie mogą być porównywane przy użyciu
operatorów <, <=, > czy też >=. Na przykład Python nie pozwoli Ci użyć tych
operatorów do porównywania łańcuchów z liczbami. Śmiało! Spróbuj użyć w swoim
programie warunku 10 > "pięć" — najwyżej wygenerujesz sążnisty komunikat
o błędzie.
Wskazówka
W obrębie społeczności Pythona toczy się zawzięta dyskusja na temat tego,
czy do tworzenia wcięć należy używać tabulatorów, czy spacji (a jeśli spacji, to
w jakiej liczbie). Jest to naprawdę kwestia indywidualnego stylu. Lecz istnieją dwie
wskazówki, do których warto się stosować. Po pierwsze, bądź konsekwentny. Jeśli
wcinasz bloki za pomocą dwóch spacji, używaj dwóch spacji zawsze. Po drugie,
nie łącz spacji i tabulatorów. Jeśli nawet uda Ci się poustawiać w jednej linii bloki
kodu przy użyciu kombinacji obu tych znaków, może to doprowadzić do dużego
bólu głowy w przyszłości. Do powszechnie stosowanych stylów wcięć należą:
pojedynczy znak tabulacji, dwie spacje i (styl, którego używa twórca języka
Python) cztery spacje. Wybór należy do Ciebie.
Rysunek 3.5. Po wprowadzeniu poprawnego hasła użytkownik uzyskuje, tak jak poprzednio,
dostęp do systemu
if password == "sekret":
print("Dostęp został udzielony")
else:
print("Odmowa dostępu")
Klauzula else
W stosunku do programu Hasło wprowadziłem tylko jedną zmianę. Do instrukcji if
dodałem klauzulę else:
if password == "sekret":
print("Dostęp został udzielony")
else:
print("Odmowa dostępu")
Jeśli wartość zmiennej password jest równa "sekret", program, dokładnie tak jak
poprzednio, wypisuje komunikat Dostęp został udzielony. Lecz w przeciwnym
wypadku wyświetla tekst Odmowa dostępu dzięki klauzuli else.
W instrukcji if z klauzulą else zostanie wykonany dokładnie jeden z bloków kodu.
Jeśli warunek jest spełniony (jego wartością jest True), wykonywany jest blok kodu
znajdujący się bezpośrednio za nim. Jeśli warunek nie jest spełniony (przyjmuje wartość
False), wykonywany jest blok umieszczony bezpośrednio za klauzulą else.
Klauzulę else tworzy się poprzez umieszczenie bezpośrednio za blokiem if kolejno:
słowa else, dwukropka i bloku instrukcji. Klauzula else musi być umieszczona w tym
samym bloku kodu co odpowiadająca jej klauzula if. To oznacza, że else i if muszą
mieć takie samo wcięcie.
import random
mood = random.randint(1, 3)
if mood == 1:
# szczęśliwy
print( \
"""
-----------
| |
| O O |
| < |
| |
| . . |
| `...` |
-----------
""")
elif mood == 2:
# obojętny
print( \
Używanie klauzuli elif 75
"""
-----------
| |
| O O |
| < |
| |
| ------ |
| |
-----------
""")
elif mood == 3:
# smutny
print( \
"""
-----------
| |
| O O |
| < |
| |
| .'. |
| ' ' |
-----------
""")
else:
print("Nieprawidłowa wartość nastroju! (Musisz być naprawdę w złym humorze).")
print("...dzisiaj.")
Klauzula elif
Instrukcja if z klauzulami elif może zawierać ciąg warunków do sprawdzenia przez
program. W programie Komputer nastrojów wierszami zawierającymi różne warunki są:
if mood == 1:
elif mood == 2:
elif mood == 3:
Warto zauważyć, że pierwszy warunek zapisuje się przy użyciu if, ale pozostałe
warunki sprawdza się przy użyciu klauzul elif (skrót od „else if”). W instrukcji if można
umieścić dowolną liczbę klauzul elif.
Dzięki wyizolowaniu tych warunków można zobaczyć cel całej struktury: sprawdzenie,
która z trzech różnych wartości została przypisana do zmiennej mood. Najpierw program
sprawdza, czy wartość zmiennej mood jest równa 1. Jeśli jest równa, wyświetlana jest
uśmiechnięta buzia. Jeśli nie jest, program przechodzi do następnego warunku i sprawdza,
czy wartość zmiennej mood jest równa 2. Jeśli ten warunek jest spełniony, wyświetlana jest
twarz, która nie wyraża żadnych emocji. Jeśli nie jest, program sprawdza, czy wartość
zmiennej mood jest równa 3. Jeśli tak jest, wyświetlane jest smutne oblicze.
76 Rozdział 3. Rozgałęzianie kodu, pętle while, projektowanie programu
Pułapka
Ważną cechą instrukcji if zawierającej klauzule elif jest fakt, że gdy tylko
okaże się, że jakiś warunek ma wartość True, komputer wykona odpowiadający
mu blok kodu i nastąpi wyjście z instrukcji. To oznacza, że zostanie wykonany
co najwyżej jeden blok kodu, nawet gdyby kilka warunków miało wartość True.
W programie Komputer nastrojów nie ma to dużego znaczenia. Wartość zmiennej
mood może być równa tylko jednej liczbie, więc tylko jeden z warunków może
zostać spełniony. Ale ważne, by mieć świadomość takiego zachowania, ponieważ
istnieje możliwość tworzenia instrukcji, w których może być jednocześnie spełniony
więcej niż jeden warunek. W takim przypadku wykonywany jest tylko blok kodu
związany z pierwszym spełnionym warunkiem.
Jeśli żaden z wcześniejszych warunków dotyczących wartości zmiennej mood nie okaże
się spełniony, wykonywany jest blok kończącej instrukcję klauzuli else i na ekranie
pojawia się komunikat Nieprawidłowa wartość nastroju! (Musisz być naprawdę w złym
humorze). Nie powinno się to nigdy zdarzyć, ponieważ zmienna mood zawsze przyjmuje
jedną z wartości: 1, 2 lub 3. Ale wstawiłem tę klauzulę tylko na wszelki wypadek.
Nie musiałem jednak tego robić, ponieważ końcowa klauzula else jest opcjonalna.
Pułapka
Chociaż stosowanie końcowej klauzuli else nie jest konieczne, jest jednak
dobrym pomysłem. Działa na zasadzie ostatniej instancji, na wypadek, gdyby
żaden z warunków zawartych w instrukcji nie był spełniony. Nawet jeśli uważasz,
że zawsze będzie spełniony przynajmniej jeden z Twoich warunków, to i tak
możesz użyć końcowej klauzuli else do wyłapania przypadku „niemożliwego”,
tak jak ja to zrobiłem.
Rysunek 3.8. Jeśli kiedykolwiek miałeś pod opieką trzylatka, ten dialog powinien Ci
przywrócić ciepłe wspomnienia
response = ""
while response != "Dlatego.":
response = input("Dlaczego?\n")
Pętla while
Pętla w programie Symulator trzylatka składa się tylko z dwóch wierszy:
while response != "Dlatego.":
response = input("Dlaczego?\n")
Jeśli format pętli while wydaje się znajomy, nie dzieje się to bez przyczyny.
Charakteryzuje ją uderzające podobieństwo do jej kuzynki — instrukcji if. Jedyna
różnica polega na tym, że słowo if jest zastąpione przez while. A te podobieństwa
nie są powierzchowne. W obu typach instrukcji, jeśli warunek jest spełniony, blok kodu
(w przypadku pętli nazywany czasem ciałem pętli) jest wykonywany. Lecz w instrukcji
while komputer sprawdza warunek i wykonuje blok kodu raz po raz, dopóki warunek nie
okaże się fałszywy. Właśnie dlatego nazywamy ją pętlą.
W tym przypadku ciałem pętli jest tylko jedna instrukcja response =
input("Dlaczego?\n"), której wykonywanie jest kontynuowane, dopóki użytkownik
nie wprowadzi odpowiedzi Dlatego.. W tym momencie warunek response != "Dlatego."
staje się fałszywy, a pętla się litościwie kończy. Wtedy program wykonuje następną
instrukcję, print("Aha, już wiem.").
Tworzenie pętli while 79
Pułapka
Jeśli zmienna (wartownik) nie będzie miała przypisanej wartości w momencie
określania wartości warunku, Twój program wygeneruje błąd.
health = 10
trolls = 0
damage = 3
while health != 0:
trolls += 1
health -= damage
Unikanie pętli nieskończonych 81
Rysunek 3.9. Wydaje się, że Twój bohater jest nieśmiertelny. Jedynym sposobem
zakończenia programu było zatrzymanie procesu
Śledzenie programu
Wygląda to tak, jakby program zawierał błąd logiczny. Dobrym sposobem na odnalezienie
tego rodzaju błędu jest prześledzenie wykonania programu. Śledzenie polega na tym,
że symuluje się pracę programu i robi się dokładnie to, co program, podążając śladem
każdej instrukcji oraz rejestrując wartości przypisywane zmiennym. W ten sposób
możesz przejrzeć program krok po kroku, dokładnie zrozumieć, co się dzieje w każdym
jego miejscu, i wykryć okoliczności, które w ukryty sposób przyczyniły się do pojawienia
się błędu w Twoim kodzie.
Najprostszym sposobem śledzenia programu jest stara metoda z ołówkiem i kartką
papieru. Utworzyłem kolumny, po jednej dla każdej zmiennej i warunku. Więc na
początku moja kartka wygląda następująco:
health trolls damage health != 0
82 Rozdział 3. Rozgałęzianie kodu, pętle while, projektowanie programu
Po kilku kolejnych przejściach przez pętlę mój ślad wygląda jak poniżej:
health trolls damage health != 0
10 0 3 True
7 1 3 True
4 2 3 True
1 3 3 True
-2 4 3 True
-5 5 3 True
-8 6 3 True
Zaprzestałem dalszego tworzenia śladu, ponieważ stało się widoczne, że znalazłem się
w pętli, która się nigdy nie zakończy. Ponieważ wartość zmiennej health jest ujemna
(i nie jest równa 0) w ostatnich trzech wierszach śladu, warunek zachowuje wartość True.
Problem polega na tym, że wartość zmiennej health nigdy nie stanie się równa 0. Jej
wartość będzie oddalać się od zera w kierunku ujemnym po każdym wykonaniu pętli.
W rezultacie warunek nigdy nie przyjmie wartości False i pętla nigdy się nie zakończy.
Tworzenie warunków,
które mogą przyjąć wartość False
Oprócz upewnienia się, że wartości występujące w warunku pętli while się zmieniają,
powinieneś mieć pewność, że warunek osiągnie w końcu wartość False; w przeciwnym
razie nadal będziesz mieć do czynienia z pętlą nieskończoną. W przypadku programu
Przegrana bitwa naprawienie błędu jest łatwe. Wystarczy, że wiersz zawierający warunek
przybierze postać:
while health > 0:
I program kończy się tak, jak powinien. Na rysunku 3.10 pokazano działanie
poprawionego 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
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 85
# Maitre d'
# Demonstruje traktowanie wartości jako warunku
if money:
print("Och, przypomniałem sobie o wolnym stoliku. Proszę tędy.")
else:
print("Proszę zaczekać. To może trochę potrwać.")
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:
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 87
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.
security = 0
Korzystanie z warunków złożonych 89
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
username = ""
while not username:
username = input("Użytkownik: ")
password = ""
while not password:
password = input("Hasło: ")
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.
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 91
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" username == "S.Meier" and
password == "cywilizacja"
True True True
True False False
False True False
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
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.
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? 95
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.
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")
# pętla zgadywania
while guess != the_number:
if guess > the_number:
print("Za duża...")
else:
print("Za mała...")
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")
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.
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.
Rysunek 4.1. Gra Wymieszane litery. Nie jest tak łatwo rozwiązać anagram
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: ")
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".
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.
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=" ")
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=" ")
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=" ")
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!")
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
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 107
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.")
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ęć.
108 Rozdział 4. Pętle for, łańcuchy znaków i krotki. Gra Wymieszane litery
Rysunek 4.5. Dzięki indeksowaniu można uzyskać bezpośredni dostęp do dowolnego znaku
w łańcuchu
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])
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:
>>> word = "indeks"
>>> print(word[0])
i
>>> print(word[1])
n
>>> print(word[2])
d
>>> print(word[3])
e
>>> print(word[4])
k
>>> print(word[5])
s
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 "<pyshell#1>", line 1, in <module>
print(word[6])
IndexError: string index out of range
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
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
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 "<pyshell#1>", line 1, in <module>
word[0] = "l"
TypeError: 'str' object does not support item assignment
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.
print()
for letter in message:
if letter.lower() not in VOWELS:
new_message += letter
print("Został utworzony nowy łańcuch: ", new_message)
Tworzenie stałych
Po pobraniu komunikatu od użytkownika i utworzeniu pustego nowego komunikatu
program tworzy łańcuch:
VOWELS = "aąeęioóuy"
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 115
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.
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.
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.
116 Rozdział 4. Pętle for, łańcuchy znaków i krotki. Gra Wymieszane litery
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.
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 117
start = None
while start != "":
start = (input("\nPoczątek: "))
if start:
start = int(start)
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:
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])
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.
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
# wyświetl krotkę
print("\nWykaz zawartości krotki:")
print(inventory)
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.
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)
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
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.
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 125
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.
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])
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])
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 "<pyshell#2>", line 1, in <module>
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)
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
128 Rozdział 4. Pętle for, łańcuchy znaków i krotki. Gra Wymieszane litery
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
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 129
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ą.
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.
130 Rozdział 4. Pętle for, łańcuchy znaków i krotki. Gra Wymieszane litery
Więc litera word[position] jest tą literą, która ma być „usunięta” z łańcucha word
i dodana do łańcucha jumble.
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'!
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.")
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.
132 Rozdział 4. Pętle for, łańcuchy znaków i krotki. Gra Wymieszane litery
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.
Rysunek 5.1. Gra Szubienica w toku. Hm… zastanawiam się, jakie to może być słowo
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 135
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ą.
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.
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ę
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])
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.
138 Rozdział 5. Listy i słowniki. Gra Szubienica
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.
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 139
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.
Poniższy wiersz kodu usuwa wycinek inventory[:2], którym jest ["kusza", "zbroja"]
z listy inventory:
del inventory[:2]
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.
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 141
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 - zakończ
1 - pokaż wyniki
2 - dodaj wynik
3 - usuń wynik
4 - posortuj wyniki
"""
)
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.
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 143
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)
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()
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.
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
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
scores = []
choice = None
while choice != "0":
print(
"""
Najlepsze wyniki 2.0
0 - zakończ
1 - wyświetl wyniki
2 - dodaj wynik
"""
)
# 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 149
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.
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 151
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ę.
152 Rozdział 5. Listy i słowniki. Gra Szubienica
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ść.
Rysunek 5.10. Więc „uninstalled” oznacza zwolniony. Byłem całkowitym 404 w tej kwestii
Używanie słowników 153
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.
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 "<pyshell#1>", line 1, in <module>
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).
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.
156 Rozdział 5. Listy i słowniki. Gra Szubienica
choice = None
while choice != "0":
print(
"""
Translator slangu komputerowego
0 - zakończ
1 - znajdź termin
2 - dodaj nowy termin
3 - zmień definicję terminu
4 - usuń termin
"""
)
# 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)
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ą.
Aby wymienić parę klucz-wartość, użyłem dokładnie takiego samego wiersza kodu
jak przy dodawaniu nowej pary:
geek[term] = definition
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.
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.")
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).
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 161
""",
"""
------
| |
| O
|
|
|
|
|
|
----------
""",
"""
------
| |
| O
| -+-
|
|
|
|
|
----------
""",
"""
------
| |
| O
| /-+-
|
|
|
|
|
----------
""",
"""
------
| |
| O
| /-+-/
|
|
|
|
|
----------
""",
"""
------
| |
| O
162 Rozdział 5. Listy i słowniki. Gra Szubienica
| /-+-/
| |
|
|
|
|
----------
""",
"""
------
| |
| O
| /-+-/
| |
| |
| |
| |
|
----------
""",
"""
------
| |
| O
| /-+-/
| |
| |
| | |
| | |
|
----------
""")
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 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
used.append(guess)
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ś!")
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.
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.
Rysunek 6.2. Nie zauważyłem tego zagrożenia. Nawet po zastosowaniu prostych technik
programowania komputer potrafi wykonywać dość dobre posunięcia
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.
Rysunek 6.4. Instrukcja jest za każdym razem wyświetlana za pomocą tylko jednego
wiersza kodu — wywołania utworzonej przeze mnie funkcji
170 Rozdział 6. Funkcje. Gra Kółko i krzyżyk
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.
0 | 1 | 2
---------
3 | 4 | 5
---------
6 | 7 | 8
# 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ę.")
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 171
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."""
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
172 Rozdział 6. Funkcje. Gra Kółko i krzyżyk
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
def display(message):
print(message)
def give_me_five():
Używanie parametrów i wartości zwrotnych 173
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)
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)
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 175
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.
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
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.
# parametry pozycyjne
def birthday1(name, age):
print("Szczęśliwych urodzin,", name, "!", " Masz już", age, "lat.\n")
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)
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.
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.
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..
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.
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 181
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.
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
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ć.
Rysunek 6.8. Możesz odczytać, przesłonić, a nawet zmienić wartość zmiennej globalnej
z wnętrza funkcji
# 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)
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)
dostępu). Tak więc próba wykonania w funkcji read_global() czegoś takiego jak poniżej
wygenerowałaby paskudny błąd:
value += 1
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")
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.
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.
Rysunek 6.9. Numer każdego pola odpowiada pozycji na liście reprezentującej planszę
Powrót do gry Kółko i krzyżyk 187
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".
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.
0 | 1 | 2
---------
3 | 4 | 5
---------
6 | 7 | 8
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 189
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
Funkcja new_board()
Ta funkcja tworzy nową planszę (w postaci listy), której wszystkie elementy mają
przypisaną wartość EMPTY, i zwraca ją:
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 191
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ę gry."""
WAYS_TO_WIN = ((0, 1, 2),
(3, 4, 5),
(6, 7, 8),
(0, 3, 6),
(1, 4, 7),
(2, 5, 8),
(0, 4, 8),
(2, 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
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 193
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.
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 195
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
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.")
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.
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)
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 197
Rysunek 7.2. Ten plik jest odczytywany przy użyciu kilku różnych technik
text_file.close()
W tabeli 7.1 znajduje się opis wybranych prawidłowych trybów dostępu do pliku
tekstowego.
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(
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 205
>>> print(text_file.readline())
To jest wiersz 2
>>> print(text_file.readline())
Ten tekst tworzy wiersz 3
>>> text_file.close()
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
>>> text_file.close()
Wiersz 1
To jest wiersz 2
>>> 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.
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")
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)
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()
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 209
Rysunek 7.4. Każda lista jest zapisywana do pliku i odczytywana z pliku w całości
210 Rozdział 7. Pliki i wyjątki. Gra Turniej wiedzy
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 211
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.
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.
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ę.
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.
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 "<pyshell#1>", line 1, in <module>
num = float("Hej!")
ValueError: could not convert string to float: Hej!
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 215
# try/except
try:
num = float(input("Wprowadź liczbę: "))
except:
print("Wystąpił jakiś błąd!")
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.
216 Rozdział 7. Pliki i wyjątki. Gra Turniej wiedzy
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 "<pyshell#0>", line 1, in <module>
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 217
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.
218 Rozdział 7. Pliki i wyjątki. Gra Turniej wiedzy
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.
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
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 221
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)
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.
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 223
# sprawdź odpowiedź
if answer == correct:
print("\nOdpowiedź prawidłowa!", end=" ")
score += 1
else:
print("\nOdpowiedź niepoprawna.", end=" ")
print(explanation)
print("Wynik:", score, "\n\n")
Zakończenie gry
Po zakończeniu pętli zamykam plik z pytaniami i wyświetlam wynik gracza:
trivia_file.close()
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.
224 Rozdział 7. Pliki i wyjątki. Gra Turniej wiedzy
Rysunek 8.2. Jeśli nie nakarmisz swojego zwierzaka lub nie pobawisz się z nim,
jego nastrój się pogorszy
Rysunek 8.3. Ale dzięki właściwej opiece, Twojemu zwierzakowi wróci jego pierwotny,
pogodny nastrój
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.
Rysunek 8.4. Kiedy program wywołuje metodę talk() obiektu klasy Critter,
zwierzak pozdrawia świat
class Critter(object):
"""Wirtualny pupil"""
def talk(self):
print("Cześć! Jestem egzemplarzem klasy Critter.")
# część główna
crit = Critter()
crit.talk()
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.")
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 231
Rysunek 8.5. Zostają utworzone dwa oddzielne zwierzaki. Każdy z nich mówi „cześć”
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()
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__.
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 233
Rysunek 8.6. Tym razem każdy obiekt klasy Critter ma atrybut name,
który wykorzystuje, mówiąc „cześć”
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")
234 Rozdział 8. Obiekty programowe. Program Opiekun zwierzaka
crit2.talk()
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ć.
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 235
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.
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")
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”.
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:
<__main__.Critter object at 0x00A0BA90>
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).
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.
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
class Critter(object):
"""Wirtualny pupil"""
total = 0
@staticmethod
def status():
print("\nOgólna liczba zwierzaków wynosi", Critter.total)
238 Rozdział 8. Obiekty programowe. Program Opiekun zwierzaka
#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()
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.
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.
240 Rozdział 8. Obiekty programowe. Program Opiekun zwierzaka
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 241
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.
class Critter(object):
"""Wirtualny pupil"""
def __init__(self, name, mood):
242 Rozdział 8. Obiekty programowe. Program Opiekun zwierzaka
Wskazówka
Możesz utworzyć prywatny atrybut klasy poprzez umieszczenie na początku jego
nazwy dwóch znaków podkreślenia.
Wskazówka
Nigdy nie powinieneś próbować bezpośredniego dostępu do prywatnych atrybutów
lub metod obiektu z zewnątrz definicji jego klasy.
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.
crit.private_method()
AttributeError: 'Critter' object has no attribute 'private_method'
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.
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.
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
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.")
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"
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
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 <= unhappiness <= 10:
m = "zadowolony"
elif 11 <= unhappiness <= 15:
m = "podenerwowany"
else:
m = "wściekły"
return m
Metoda talk()
Metoda talk() oznajmia światu, jaki jest nastrój zwierzaka, wykorzystując dostęp
do właściwości mood obiektu klasy Critter. Następnie wywołuje ona metodę prywatną
__pass_time().
def talk(self):
print("Nazywam się", self.name, "i jestem", self.mood, "teraz.\n")
self.__pass_time()
Metoda eat()
Metoda eat() zmniejsza poziom głodu zwierzaka o liczbę przekazaną poprzez parametr
food. Jeśli nie zostanie przekazana żadna wartość, parametr food otrzymuje domyślną
Powrót do programu Opiekun zwierzaka 251
wartość 4. Poziom głodu zwierzaka jest utrzymywany pod kontrolą i nie może spaść
poniżej wartości 0. Na koniec metoda wywołuje metodę prywatną __pass_time().
def eat(self, food = 4):
print("Mniam, mniam. Dziękuję.")
self.hunger -= food
if self.hunger < 0:
self.hunger = 0
self.__pass_time()
Metoda play()
Metoda play() zmniejsza poziom nudy zwierzaka o liczbę przekazaną poprzez parametr
fun. Jeśli nie zostanie przekazana żadna wartość, parametr fun otrzymuje domyślną
wartość 4. Poziom nudy zwierzaka jest utrzymywany pod kontrolą i nie może spaść
poniżej wartości 0. Na koniec metoda wywołuje metodę prywatną __pass_time().
def play(self, fun = 4):
print("Hura!")
self.boredom -= fun
if self.boredom < 0:
self.boredom = 0
self.__pass_time()
Utworzenie zwierzaka
Kod głównej części programu umieściłem w osobnej funkcji, main(). Na początku
programu pobieram nazwę zwierzaka od użytkownika. Następnie konkretyzuję nowy
obiekt klasy Critter. Ponieważ nie przekazuję wartości do parametrów hunger i boredom,
początkowa wartość obu atrybutów wynosi 0, a zwierzak rozpoczyna swe życie szczęśliwy
i zadowolony.
def main():
crit_name = input("Jak chcesz nazwać swojego zwierzaka?: ")
crit = Critter(crit_name)
0 - zakończ
1 - słuchaj swojego zwierzaka
2 - nakarm swojego zwierzaka
3 - pobaw się ze swoim zwierzakiem
""")
# wyjdź z pętli
if choice == "0":
print("Do widzenia.")
# nieznany wybór
else:
print("\nNiestety,", choice, "nie jest prawidłowym wyborem.")
Uruchomienie programu
Kolejny wiersz kodu wywołuje funkcję main() i rozpoczyna program. W ostatnim wierszu
program oczekuje na decyzję użytkownika przed zakończeniem swojego działania.
main()
input("\n\nAby zakończyć program, naciśnij klawisz Enter.")
Podsumowanie
W tym rozdziale wprowadziłem Cię w inny sposób programowania z użyciem obiektów
programowych. Dowiedziałeś się, że obiekty programowe mogą łączyć w sobie funkcje
i dane (w terminologii OOP — metody i atrybuty) i pod wieloma względami naśladują
obiekty świata realnego. Zobaczyłeś, jak pisać klasy stanowiące projekty obiektów.
Dowiedziałeś się o specjalnej metodzie zwanej konstruktorem, która jest wywoływana
automatycznie, gdy konkretyzowany jest nowy obiekt. Zobaczyłeś, jak tworzyć
i inicjalizować atrybuty obiektu za pomocą konstruktora. Dowiedziałeś się, jak tworzyć
elementy wspólne dla całej klasy, takie jak atrybuty klasy i metody statyczne. Potem
dowiedziałeś się, na czym polega hermetyzacja obiektów. Poznałeś sposoby pomagające
Podsumowanie 253
Następnie gracz otrzymuje szansę dobrania dodatkowych kart. Każdy z graczy może
jednorazowo dobrać jedną kartę i powtarzać tę czynność tak długo, jak chce. Lecz kiedy
suma punktów gracza przekroczy 21 (jest to tak zwana „fura”), gracz przegrywa. Jeśli
każdy z graczy dostanie furę, komputer odsłania swoją pierwszą kartę i runda się kończy.
W przeciwnym wypadku gra toczy się dalej. Komputer musi dobierać dodatkowe karty,
dopóki suma jego punktów jest mniejsza niż 17. Jeśli komputer dostanie furę, wszyscy
gracze, którzy sami jej nie dostali, zostają zwycięzcami. W przeciwnym razie suma
punktów każdego z graczy pozostających w grze jest porównywana z sumą uzyskaną
przez komputer. Jeśli suma punktów uzyskana przez gracza jest większa, gracz wygrywa.
Jeśli jest mniejsza, przegrywa. Jeśli obie sumy są jednakowe, gracz remisuje
z komputerem. Gra została pokazana na rysunku 9.1.
Rysunek 9.2. Opis walki jest rezultatem wymiany komunikatu między obiektami
Rysunek 9.3. Obiekt klasy Player reprezentowany przez zmienną hero wysyła komunikat
do obiektu invader klasy Alien
258 Rozdział 9. Programowanie obiektowe. Gra Blackjack
W świecie rzeczywistym
Schemat, który utworzyłem, aby pokazać dwa obiekty wymieniające między sobą
komunikat, jest dość prosty. Ale w przypadku wielu obiektów i wielu zachodzących
między nimi relacji, podobne schematy mogą się stać skomplikowane.
W rzeczywistości istnieją rozmaite formalne metody służące do odwzorowywania
projektów programowych. Jedną z najpopularniejszych jest Zunifikowany Język
Modelowania (ang. Unified Modeling Language — UML) — język notacyjny,
który jest szczególnie użyteczny przy wizualizacji systemów obiektowych.
class Player(object):
""" Gracz w grze strzelance. """
def blast(self, enemy):
print("Gracz razi wroga.\n")
enemy.die()
class Alien(object):
""" Obcy w grze strzelance. """
def die(self):
print("Obcy z trudem łapie oddech, 'To już koniec. Ale wielki koniec... \n" \
"Tak, już robi się ciemno. Powiedz moim dwóm milionom larw, że je
kochałem... \n" \
"Żegnaj, okrutny Wszechświecie.'")
# main
print("\t\tŚmierć Obcego\n")
hero = Player()
invader = Alien()
hero.blast(invader)
Przesyłanie komunikatu
Zanim sprawisz, że jeden obiekt prześle komunikat do drugiego, musisz najpierw
dysponować dwoma obiektami! Więc tworzę dwa obiekty w głównej części programu.
Najpierw tworzę obiekt klasy Player i przypisuję go do zmiennej hero. Następnie tworzę
obiekt klasy Alien i przypisuję go do zmiennej invader.
Od następnego wiersza kod staje się interesujący. Poprzez wyrażenie
hero.blast(invader) wywołuję metodę blast() obiektu hero i przekazuję invader —
obiekt klasy Alien — jako argument. Po zbadaniu definicji metody blast() widzisz,
że metoda przyjmuje ten obiekt poprzez swój parametr enemy (wróg). Więc kiedy
Tworzenie kombinacji obiektów 259
wykonywana jest metoda blast(), parametr enemy zawiera referencję do obiektu klasy
Alien. Po wyświetleniu komunikatu metoda blast() wywołuje metodę die() obiektu
klasy Alien poprzez wyrażenie enemy.die(). W zasadzie obiekt klasy Player przesyła
komunikat do obiektu klasy Alien poprzez wywołanie jego metody die().
Odebranie komunikatu
Obiekt klasy Alien odbiera komunikat od obiektu klasy Player w tym sensie, że
wywoływana jest jego metoda die(). Wówczas metoda die() obiektu klasy Alien
wyświetla melodramatyczne pożegnanie.
Rysunek 9.4. Każdy obiekt klasy Hand (ręka) jest kolekcją obiektów klasy Card (karta)
# Gra w karty
# Demonstruje tworzenie kombinacji obiektów
class Card(object):
""" Karta do gry. """
RANKS = ["A", "2", "3", "4", "5", "6", "7",
"8", "9", "10", "J", "Q", "K"]
SUITS = ["c", "d", "h", "s"]
def __str__(self):
rep = self.rank + self.suit
return rep
Każdy obiekt klasy Card ma atrybut rank, który reprezentuje rangę karty. Możliwe
jej wartości zostały wyszczególnione w atrybucie klasy o nazwie RANKS. Symbol "A"
reprezentuje asa, symbole od "2" do "10" reprezentują odpowiadające im wartości
liczbowe, symbol "J" reprezentuje waleta (ang. jack), "Q" — damę (ang. queen),
a "K" — króla (ang. king).
Każda karta ma również atrybut suit, który reprezentuje kolor karty. Możliwe
wartości tego atrybutu zostały wymienione w atrybucie klasy o nazwie SUITS. Litera "c"
reprezentuje trefle (ang. clubs), "d" oznacza kara (ang. diamonds), "h" symbolizuje kiery
(ang. hearts), a "s" reprezentuje piki (ang. spades). Więc obiekt z atrybutem rank
o wartości "A" i atrybutem suit o wartości "d" reprezentuje asa karo.
Metoda specjalna __str__() zwraca po prostu konkatenację atrybutów rank i suit,
aby obiekt mógł zostać wyświetlony.
Tworzenie kombinacji obiektów 261
def __str__(self):
if self.cards:
rep = ""
for card in self.cards:
rep += str(card) + " "
else:
rep = "<pusta>"
return rep
def clear(self):
self.cards = []
Nowy obiekt klasy Hand zawiera atrybut cards, który został zamierzony jako lista
obiektów klasy Card. Więc każdy pojedynczy obiekt klasy Hand ma atrybut, który jest listą
być może wielu innych obiektów.
Metoda specjalna __str__() zwraca łańcuch znaków, który reprezentuje całą rękę
(karty w ręku gracza). Metoda ta iteruje po wszystkich obiektach klasy Card zawartych
w obiekcie klasy Hand i dołącza (poprzez konkatenację) do tworzonego łańcucha znaków
ich łańcuchowe reprezentacje. Jeśli obiekt klasy Hand nie zawiera żadnych obiektów klasy
Card, zwracany jest łańcuch "<pusta>".
Metoda clear() kasuje listę kart poprzez przypisanie pustej listy do atrybutu cards
obiektu.
Metoda add() dodaje obiekt do atrybutu cards.
Metoda give() usuwa obiekt karty z obiektu klasy Hand i dołącza go do innego
obiektu klasy Hand poprzez wywołanie metody add() tego drugiego obiektu. Mówiąc
inaczej, pierwszy obiekt klasy Hand wysyła do drugiego obiektu tej samej klasy komunikat
z poleceniem dodania obiektu klasy Card.
Obiekt klasy Card utworzony jako pierwszy ma atrybut rank o wartości równej "A"
oraz atrybut suit o wartości "c". Kiedy wyświetlam ten obiekt, na ekranie pojawia się
tekst Ac. Taki sam schemat ma zastosowanie do pozostałych obiektów.
Ponieważ atrybut cards obiektu to pusta lista, wykonanie na obiekcie operacji print
skutkuje wyświetleniem tekstu <pusta>.
Następnie dodaję do obiektu my_hand pięć obiektów klasy Card i wypisuję go
ponownie:
my_hand.add(card1)
my_hand.add(card2)
my_hand.add(card3)
my_hand.add(card4)
my_hand.add(card5)
print("\nWyświetlam zawartość mojej ręki po dodaniu 5 kart:")
print(my_hand)
Jak mógłbyś się spodziewać, obiekt your_hand zostaje wyświetlony jako Ac 2c,
podczas gdy my_hand pojawia się na ekranie jako 3c 4c 5c.
W końcu wywołuję metodę clear() obiektu my_hand i wyświetlam go po raz ostatni:
my_hand.clear()
print("\nMoja ręka po usunięciu z niej kart:")
print(my_hand)
Wykorzystanie dziedziczenia
do tworzenia nowych klas
Jednym z kluczowych elementów programowania obiektowego jest dziedziczenie, które
umożliwia oparcie nowej klasy na już istniejącej. Dzięki temu nowa klasa automatycznie
otrzymuje (czyli dziedziczy) wszystkie metody i atrybuty istniejącej klasy — jest to jakby
skorzystanie za darmo z całej pracy włożonej w pisanie klasy bazowej!
Pułapka
W Pythonie jest możliwe utworzenie nowej klasy, która bezpośrednio dziedziczy
po więcej niż jednej klasie. Jest to nazywane dziedziczeniem wielokrotnym.
Jednak takie podejście wprowadza szereg komplikacji. Więc zapewne będzie
najlepiej, jeśli jako początkujący programista będziesz unikać stosowania
dziedziczenia wielokrotnego.
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 9.; nazwa pliku to gra_w_karty2.py.
Rozszerzanie klasy poprzez dziedziczenie 265
class Card(object):
""" Karta do gry. """
RANKS = ["A", "2", "3", "4", "5", "6", "7",
"8", "9", "10", "J", "Q", "K"]
SUITS = ["c", "d", "h", "s"]
def __str__(self):
rep = self.rank + self.suit
return rep
class Hand(object):
""" Ręka - karty do gry w ręku gracza. """
def __init__(self):
self.cards = []
def __str__(self):
if self.cards:
rep = ""
for card in self.cards:
rep += str(card) + "\t"
else:
rep = "<pusta>"
return rep
def clear(self):
self.cards = []
Klasa Hand nazywa się klasą bazową, ponieważ klasa Deck jest na niej oparta. Klasa
Deck jest uważana za klasę pochodną, ponieważ część swojej definicji czerpie z klasy Hand.
W wyniku tej relacji klasa Deck dziedziczy wszystkie metody klasy Hand. Więc nawet
gdybym nie zdefiniował ani jednej nowej metody w tej klasie, obiekty klasy Deck miałyby
wciąż wszystkie metody zdefiniowane w klasie Hand:
__init__(),
__str__(),
clear(),
add(),
give().
Jeśli to pomoże, na potrzeby tego prostego przykładu możesz sobie nawet wyobrazić,
że skopiowałeś wszystkie metody klasy Hand i wkleiłeś je prosto do klasy Deck dzięki
dziedziczeniu.
def shuffle(self):
import random
random.shuffle(self.cards)
Więc oprócz wszystkich metod, jakie klasa Deck dziedziczy, ma ona następujące nowe
metody:
populate(),
shuffle(),
deal().
Rozszerzanie klasy poprzez dziedziczenie 267
Z perspektywy kodu klienckiego każda metoda jest tak samo ważna jak pozostałe —
niezależnie od tego, czy została odziedziczona po klasie Hand, czy też zdefiniowana
w klasie Deck. A wszystkie metody obiektu klasy Deck są wywoływane w taki sam sposób,
przy użyciu notacji z kropką.
Przyglądając się tej klasie, zauważysz, że nie definiuję w niej konstruktora. Lecz
klasa Deck dziedziczy konstruktor klasy Hand, więc ta właśnie metoda jest automatycznie
wywoływana na rzecz nowo utworzonego obiektu klasy Deck. W rezultacie nowy
obiekt klasy Deck uzyskuje atrybut cards, który zostaje zainicjalizowany jako pusta lista,
zupełnie tak samo, jakby to miało miejsce w przypadku każdego nowo utworzonego
obiektu klasy Hand. Na koniec nowy obiekt zostaje przypisany do zmiennej deck1.
Teraz wyposażony w nową (lecz pustą) talię wyświetlam ją:
print("Utworzyłem nową talię.")
print("Talia:")
print(deck1)
Nie zdefiniowałem także w klasie Deck metody specjalnej __str__(), ale tak jak
poprzednio, klasa Deck dziedziczy tę metodę po klasie Hand. Ponieważ talia nie zawiera
kart, kod wyświetla tekst <pusta>. Jak dotąd talia wydaje się całkowicie przypominać
rękę. Tak się dzieje, ponieważ talia jest wyspecjalizowanym typem ręki. Pamiętaj,
że talia może wykonywać wszystkie czynności ręki plus coś więcej.
Pusta talia jest mało interesująca, więc wywołuję metodę populate() obiektu,
która umieszcza w talii tradycyjne 52 karty:
deck1.populate()
Wreszcie zrobiłem z talią coś, czego nie mógłbym zrobić z ręką. To dlatego,
że metoda populate() jest nową metodą, którą zdefiniowałem w klasie Deck. Metoda
populate() tworzy w pętli 52 możliwe kombinacje wartości zawartych na listach
Card.SUITS i Card.RANKS (każda karta z prawdziwej talii jest w ten sposób reprezentowana).
Dla każdej kombinacji metoda tworzy nowy obiekt klasy Card, który dodaje do talii.
Następnie wyświetlam talię:
print("\nDodałem do talii komplet kart.")
print("Talia:")
print(deck1)
Tym razem zostają wyświetlone wszystkie 52 karty! Ale jeśli przyjrzysz się uważnie,
dostrzeżesz, że ich porządek jest oczywisty. Aby było ciekawiej, tasuję tę talię:
deck1.shuffle()
268 Rozdział 9. Programowanie obiektowe. Gra Blackjack
Następnie tworzę dwa obiekty klasy Hand i umieszczam je na liście, którą przypisuję
do zmiennej hands:
my_hand = Hand()
your_hand = Hand()
hands = [my_hand, your_hand]
Metoda deal() jest nową metodą, którą definiuję w klasie Deck. Przyjmuje ona dwa
argumenty: listę rąk i liczbę kart, jaką należy rozdać każdej ręce. Metoda daje każdej ręce
po jednej karcie z talii. Jeśli w talii brakuje kart, wyświetla komunikat Nie mogę dalej
rozdawać. Zabrakło kart!. Metoda powtarza ten proces tyle razy, ile wynosi liczba kart,
która ma zostać przekazana każdej ręce. Więc wykonanie powyższego wiersza kodu
skutkuje przekazaniem każdej ręce (my_hand i your_hand) pięciu kart z talii deck1.
Aby zobaczyć wyniki rozdania,x wyświetlam jeszcze raz obie ręce i talię:
print("\nRozdałem sobie i Tobie po 5 kart.")
print("Moja ręka:")
print(my_hand)
print("Twoja ręka:")
print(your_hand)
print("Talia:")
print(deck1)
Patrząc na wyświetlone dane, możesz sprawdzić, że każda ręka ma 5 kart, a talia liczy
ich tylko 42.
Na koniec doprowadzam talię z powrotem do jej stanu początkowego poprzez
usunięcie z niej kart:
deck1.clear()
print("\nUsunąłem zawartość talii.")
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 9.; nazwa pliku to gra_w_karty3.py.
class Card(object):
""" Karta do gry. """
RANKS = ["A", "2", "3", "4", "5", "6", "7",
"8", "9", "10", "J", "Q", "K"]
SUITS = ["c", "d", "h", "s"]
def __str__(self):
rep = self.rank + self.suit
return rep
Przebiega to tak samo jak w poprzednich programach i zostaje wyświetlony tekst Ac.
Kolejną moją czynnością jest wyświetlenie obiektu klasy Unprintable_Card:
print("\nWyświetlenie obiektu klasy Unprintable_Card:")
print(card2)
Mimo że obiekt ma atrybut rank z ustawioną wartością "A" oraz atrybut suit
z ustawioną wartością "d", operacja print na obiekcie powoduje wyświetlenie tekstu
<utajniona>, ponieważ klasa Unprintable_Card przesłania odziedziczoną metodę
__str__() swoją własną, która zawsze zwraca łańcuch "<utajniona>".
Kolejne dwa wiersze kodu wyświetlają obiekt klasy Positionable_Card:
print("\nWyświetlenie obiektu klasy Positionable_Card:")
print(card3)
Ponieważ atrybut face_up obiektu ma wartość True, metoda __str__() tego obiektu
wywołuje metodę __str__() klasy Card i zostaje wyświetlony tekst Ah.
Następnie wywołuję metodę flip() obiektu klasy Positionable_Card:
Polimorfizm 273
Tym razem zostaje wyświetlony tekst XX, ponieważ atrybut face_up obiektu ma
wartość False.
Polimorfizm
Polimorfizm to zdolność do traktowania różnego typu rzeczy tak samo i sprawienia,
aby każda z nich reagowała na swój własny sposób. W kontekście programowania
obiektowego polimorfizm oznacza, że możesz wysłać ten sam komunikat do obiektów
różnych klas powiązanych poprzez dziedziczenie oraz osiągnąć różne, odpowiednie dla
konkretnego obiektu wyniki. Kiedy na przykład wywołasz metodę __str__() obiektu
klasy Unprintable_Card, która jest klasą pochodną klasy Card, otrzymasz inny wynik niż
wtedy, kiedy wywołasz metodę __str__() obiektu klasy Card. Efekt tego polimorficznego
zachowania polega na tym, że możesz wyświetlić obiekt, jeśli nawet nie wiesz, czy jest on
obiektem klasy Unprintable_Card, czy też obiektem klasy Card. Niezależnie od klasy
obiektu, gdy jest on wyświetlany, zostaje wywołana metoda __str__() i na ekranie
pojawia się właściwa jego reprezentacja w postaci łańcucha znaków.
Tworzenie modułów
Po raz pierwszy dowiedziałeś się o modułach w rozdziale 3., w punkcie „Import
modułu random”, w którym się zetknąłeś z modułem random. Lecz potężnym aspektem
programowania w Pythonie jest możliwość tworzenia, używania, a nawet współdzielenia
swoich własnych modułów. Tworzenie własnych modułów przynosi znaczące korzyści.
Po pierwsze, tworząc własne moduły, możesz wykorzystywać kod wielokrotnie,
co może Ci zaoszczędzić czasu i wysiłków. Mógłbyś na przykład wykorzystać ponownie
klasy Card, Hand i Deck, z którymi się do tej pory spotkałeś, do utworzenia wielu różnych
typów gier w karty bez potrzeby wymyślania za każdym razem na nowo podstawowej
funkcjonalności karty, talii i ręki.
Po drugie, dzięki rozbiciu programu na moduły logiczne możesz radzić sobie łatwiej
z dużymi programami. Do tej pory programy, z którymi miałeś do czynienia, zawierały
się w jednym pliku. Ponieważ były dość krótkie, nie stanowiło to wielkiego problemu.
Lecz wyobraź sobie program, który składa się z tysięcy (czy nawet dziesiątek tysięcy)
274 Rozdział 9. Programowanie obiektowe. Gra Blackjack
wierszy. Praca nad programem tego rozmiaru zapisanym w jednym, ogromnym pliku
byłaby prawdziwą zmorą (nawiasem mówiąc, profesjonalne projekty mogą z łatwością
osiągać takie rozmiary). Rozbicie kodu na moduły ułatwia członkom zespołu
zajmującego się inżynierią oprogramowania podzielenie między siebie pracy
nad oddzielnymi częściami projektu.
Po trzecie, tworząc moduły, możesz się dzielić swoim talentem z innymi.
Jeśli stworzysz jakiś użyteczny moduł, możesz przesłać go pocztą elektroniczną
do przyjaciela, który może go wykorzystać na bardzo podobnej zasadzie jak dowolny
wbudowany moduł Pythona.
Pisanie modułów
W zwykłym przypadku pokazałbym Ci kod kolejnego programu Prosta gra, lecz w tym
punkcie omówię napisany przeze mnie moduł, który wykorzystuje ta Prosta gra.
Moduł tworzy się w taki sam sposób, jak się pisze dowolny inny program w Pythonie.
Gdy jednak tworzysz moduł, powinieneś zbudować kolekcję powiązanych ze sobą
Tworzenie modułów 275
class Player(object):
""" Uczestnik gry. """
def __init__(self, name, score = 0):
self.name = name
self.score = score
def __str__(self):
rep = self.name + ":\t" + str(self.score)
return rep
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
if __name__ == "__main__":
print("Uruchomiłeś ten moduł bezpośrednio (zamiast go zaimportować).")
input("\n\nAby zakończyć program, naciśnij klawisz Enter.")
Ten moduł został nazwany gry, ponieważ został zapisany w pliku o nazwie gry.py.
Nazwy modułów utworzonych przez programistę (używane w instrukcji import) są
oparte na nazwach plików, które je zawierają.
Większość kodu modułu jest prosta. Klasa Player (gracz) definiuje obiekt z dwoma
atrybutami, name (nazwa) i score (wynik), których wartości są ustawiane w konstruktorze.
Istnieje jeszcze tylko jedna metoda, __str__(), która zwraca łańcuchową reprezentację
obiektu, aby obiekty mogły być wyświetlane.
Kolejne dwie funkcje, ask_yes_no() i ask_number() poznałeś już wcześniej,
w rozdziale 6., w punktach „Funkcja ask_yes_no()” i „Funkcja ask_number()”.
W następnej części programu wprowadzam nową koncepcję związaną z modułami.
Warunek instrukcji if postaci __name__ == "__main__" jest prawdziwy, jeśli program
276 Rozdział 9. Programowanie obiektowe. Gra Blackjack
został uruchomiony bezpośrednio. Natomiast jest fałszywy, jeśli plik został zaimportowany
jako moduł. Więc gdy plik gry.py jest uruchamiany bezpośrednio, zostaje wyświetlony
komunikat informujący użytkownika, że plik powinien być zaimportowany, a nie
uruchomiony bezpośrednio.
Import modułów
Teraz, kiedy już się zapoznałeś z modułem gry, przedstawię kod programu Prosta gra.
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 9.; nazwa pliku to prosta_gra.py.
# Prosta gra
# Demonstruje import modułów
Moduł utworzony przez programistę importuje się w taki sam sposób jak moduł
wbudowany, za pomocą instrukcji import. W rzeczywistości importuję moduł gry razem
ze znanym Ci modułem random w tej samej instrukcji import.
Pułapka
Jeśli moduł utworzony przez programistę nie znajduje się w tym samym katalogu
co program, który go importuje, Python nie potrafi go znaleźć. Są sposoby obejścia
tego ograniczenia. Można nawet zainstalować moduł utworzony przez programistę
w taki sposób, aby był dostępny tak jak moduły wbudowane w całym systemie,
ale wymaga to specjalnej procedury instalacyjnej, której omówienie wykracza
poza zakres tej książki. Więc na razie musisz się upewnić, że każdy moduł, jaki
chcesz zaimportować, znajduje się w tym samym katalogu co programy, które
go importują.
again = None
while again != "n":
players = []
num = gry.ask_number(question = "Podaj liczbę graczy (2 - 5): ", low = 2, high = 5)
Następnie pobieram nazwę każdego z graczy i generuję losowo jego wynik mieszczący się
w zakresie od 1 do 100 poprzez wywołanie funkcji randrange() z modułu random. Potem
tworzę obiekt gracza przy użyciu nazwy gracza i jego wyniku. Ponieważ klasa Player
została zdefiniowana w module gry, znów używam notacji z kropką i przed nazwą klasy
umieszczam nazwę modułu. Następnie dołączam ten nowy obiekt gracza do listy graczy:
for i in range(num):
name = input("Nazwa gracza: ")
score = random.randrange(100) + 1
player = gry.Player(name, score)
players.append(player)
Na koniec pytam, czy gracze chcą wziąć udział w jeszcze jednej rundzie gry.
Do uzyskania odpowiedzi wykorzystuję funkcję ask_yes_no() z modułu gry.
again = gry.ask_yes_no("\nCzy chcesz zagrać ponownie? (t/n): ")
Moduł karty
W celu napisania gry Blackjack utworzyłem ostateczny moduł karty oparty
na programach Gra w karty. Klasy Hand i Deck są dokładnie takie same jak w programie
Gra w karty 2.0. Nowa klasa Card reprezentuje taką samą funkcjonalność jak klasa
Positionable_Card z programu Gra w karty 3.0. Kod programu możesz znaleźć na
stronie internetowej tej książki (http://www.helion.pl/ksiazki/pytdk3.htm), w folderze
rozdziału 9.; nazwa pliku to karty.py.
# Moduł karty
# Podstawowe klasy do gry w karty
class Card(object):
""" Karta do gry. """
RANKS = ["A", "2", "3", "4", "5", "6", "7",
"8", "9", "10", "J", "Q", "K"]
SUITS = ["c", "d", "h", "s"]
278 Rozdział 9. Programowanie obiektowe. Gra Blackjack
def __str__(self):
if self.is_face_up:
rep = self.rank + self.suit
else:
rep = "XX"
return rep
def flip(self):
self.is_face_up = not self.is_face_up
class Hand(object):
""" Ręka - wszystkie karty trzymane przez gracza. """
def __init__(self):
self.cards = []
def __str__(self):
if self.cards:
rep = ""
for card in self.cards:
rep += str(card) + "\t"
else:
rep = "<pusta>"
return rep
def clear(self):
self.cards = []
class Deck(Hand):
""" Talia kart. """
def populate(self):
for suit in Card.SUITS:
for rank in Card.RANKS:
self.add(Card(rank, suit))
def shuffle(self):
import random
random.shuffle(self.cards)
if self.cards:
top_card = self.cards[0]
self.give(top_card, hand)
else:
print("Nie mogę dalej rozdawać. Zabrakło kart!")
if __name__ == "__main__":
print("To moduł zawierający klasy do gry w karty.")
input("\n\nAby zakończyć program, naciśnij klawisz Enter.")
Projektowanie klas
Zanim rozpoczniesz kodowanie projektu zawierającego wiele klas, pomocne może się
okazać sporządzenie na papierze ich planu. Mógłbyś zrobić ich listę i dołączyć do każdej
klasy krótki opis. Tabela 9.1 pokazuje moją pierwszą próbę sporządzenia takiej listy
do gry Blackjack.
Powinieneś spróbować ująć w niej wszystkie klasy, których Twoim zdaniem będziesz
potrzebował, ale nie martw się tym, że Twoje opisy klas nie są kompletne, ponieważ
nigdy takie nie będą (moje też takie nie są). Lecz przygotowanie takiej listy powinno
pomóc Ci uzyskać dobry przegląd typów obiektów, nad którymi będziesz pracować
w swoim projekcie.
Oprócz słownych opisów swoich klas mógłby Ci się przydać rysunek drzewa
hierarchii klas, by przedstawić w wizualny sposób powiązania między Twoimi klasami.
To właśnie zrobiłem na rysunku 9.8.
280 Rozdział 9. Programowanie obiektowe. Gra Blackjack
# Blackjack
# Od 1 do 7 graczy współzawodniczy z rozdającym
Klasa BJ_Card
Klasa BJ_Card rozszerza definicję tego, czym jest karta, dziedzicząc po klasie karty.Card.
W klasie BJ_Card tworzę nową właściwość, value, reprezentującą wartość karty wyrażoną
w punktach:
class BJ_Card(karty.Card):
""" Karta do blackjacka. """
ACE_VALUE = 1
@property
def value(self):
if self.is_face_up:
v = BJ_Card.RANKS.index(self.rank) + 1
if v > 10:
v = 10
else:
v = None
return v
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):
282 Rozdział 9. Programowanie obiektowe. Gra Blackjack
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
return t
Pierwsza część tej metody sprawdza, czy wartość właściwości value jakiejś karty
należącej do ręki blackjacka nie jest równa None (co oznaczałoby, że karta jest zakryta).
Jeśli ma to miejsce, metoda zwraca None. Następna część metody po prostu sumuje
wartości punktowe wszystkich kart ręki. Kolejna część ustala, czy ręka zawiera asa.
Jeśli go zawiera, ostatnia część metody decyduje, czy wartość punktowa karty powinna
wynosić 11, czy też 1.
Ostatnia metoda w klasie BJ_Hand to is_busted(). Zwraca ona wartość True,
jeśli wartość właściwości total obiektu jest większa od 21. W przeciwnym wypadku
zwraca wartość False.
def is_busted(self):
return self.total > 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()
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()
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 285
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()
286 Rozdział 9. Programowanie obiektowe. Gra Blackjack
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)
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()
player.clear()
self.dealer.clear()
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ś
288 Rozdział 9. Programowanie obiektowe. Gra Blackjack
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.
292 Rozdział 10. Tworzenie interfejsów GUI. Gra Mad Lib
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.
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.
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.
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.
296 Rozdział 10. Tworzenie interfejsów GUI. Gra Mad Lib
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.
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.
Rozpoczęcie programu
Najpierw inicjuję program Metkownica poprzez import modułu tkinter i utworzenie
okna głównego:
# Metkownica
# Demonstruje etykietę
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()
Utworzenie etykiety
Tworzę widżet Label poprzez konkretyzację obiektu klasy Label:
# utwórz w ramce etykietę
lbl = Label(app, text = "Jestem etykietą!")
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.
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
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()
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ż!")
Rysunek 10.9. To jakieś déjà vu. Program wygląda tak samo jak jego poprzednik,
mimo że „pod maską” dokonano znaczących zmian
Zdefiniowanie konstruktora
Potem definiuję w klasie Application konstruktor:
def __init__(self, master):
""" Inicjalizuj ramkę. """
super(Application, self).__init__(master)
self.grid()
self.create_widgets()
302 Rozdział 10. Tworzenie interfejsów GUI. Gra Mad Lib
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.
Rozpoczęcie programu
W swoim pierwszym kroku tradycyjnie importuję moduł GUI:
# Licznik kliknięć
# Demonstruje powiązanie zdarzenia z procedurą obsługi zdarzeń
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.
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.
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 305
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.
Rysunek 10.11. Jeśli użytkownikowi nie uda się wprowadzić prawidłowego hasła,
program grzecznie odmawia wyjawienia swojego sekretu
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
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
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()
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 307
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.
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)
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)
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 309
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)
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.
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)
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()
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).
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
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()
Label(self,
text = "Wybierz swoje ulubione gatunki filmów."
).grid(row = 0, column = 0, sticky = W)
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 313
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)
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)
314 Rozdział 10. Tworzenie interfejsów GUI. Gra Mad Lib
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()
Ponieważ przyciski opcji mają tak dużo wspólnego z polami wyboru, nauczenie się ich
stosowania jest dosyć proste.
Rozpoczęcie programu
Rozpoczynam program od importu zawartości modułu tkinter:
# Wybór filmów 2
# Demonstruje przyciski opcji
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ę. """
316 Rozdział 10. Tworzenie interfejsów GUI. Gra Mad Lib
super(Application, self).__init__(master)
self.grid()
self.create_widgets()
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()
variable = self.is_itchy
).grid(row = 4, column = 1, sticky = W)
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()
# wyświetl opowiadanie
self.story_txt.delete(0.0, END)
self.story_txt.insert(0.0, story)
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.
Rysunek 11.2. Kiedy graczowi nie udaje się złapać jakiejś pizzy, gra się kończy
Wprowadzenie do pakietów pygame i livewires 325
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.
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.
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.
Wartość parametru fps (ang. frames per second — liczba ramek na sekundę) decyduje, ile
razy w ciągu każdej sekundy ekran będzie aktualizowany.
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.
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.
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
games.screen.mainloop()
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)
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.
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
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.
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.
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()
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.
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ą.
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)
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.
336 Rozdział 11. Grafika. Gra Pizza Panic
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.
games.screen.mainloop()
338 Rozdział 11. Grafika. Gra Pizza Panic
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.
games.screen.mainloop()
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)
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.
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.
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.
Rysunek 11.11. Pizza porusza się w prawo, w dół, w kierunku wskazywanym przez strzałkę
Przemieszczanie duszków 343
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()
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 345
Rozpoczęcie programu
Zaczynam tak jak w przypadku dowolnego innego programu graficznego:
# Sprężysta pizza
# Demonstruje postępowanie po osiągnięciu granic ekranu
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
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 347
Rozpoczęcie programu
Poniższy kod powinien wyglądać wybitnie znajomo:
# Patelnia w ruchu
# Demonstruje obsługę danych wejściowych z myszy
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.
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)
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()
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.
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
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 351
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()
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)
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 353
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().
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ę
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ł
354 Rozdział 11. Grafika. Gra Pizza Panic
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.
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)
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 355
self.check_catch()
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,
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)
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 357
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")
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.
358 Rozdział 11. Grafika. Gra Pizza Panic
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 359
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ć!
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.
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 363
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.
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
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 365
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.
366 Rozdział 12. Dźwięk, animacja i rozwijanie programu. Gra Astrocrash
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.
# Obróć duszka
# Demonstruje obracanie duszka
from livewires import games
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()
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.
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
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 371
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.
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
Pułapka
Za pomocą funkcji load_sound() można ładować tylko pliki WAV.
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 - zakończ
1 - odtwórz dźwięk pocisku
2 - odtwarzaj cyklicznie dźwięk pocisku
3 - zatrzymaj odtwarzanie dźwięku pocisku
4 - odtwórz temat muzyczny
5 - odtwarzaj cyklicznie temat muzyczny
6 - zatrzymaj odtwarzanie tematu muzycznego
"""
)
# wyjdź
if choice == "0":
print("Żegnaj!")
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.
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.
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 375
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.")
Dokończenie programu
Wreszcie kończę program obsługą nieprawidłowego wyboru i oczekiwaniem na decyzję
użytkownika:
376 Rozdział 12. Dźwięk, animacja i rozwijanie programu. Gra Astrocrash
# nieprzewidziany wybór
else:
print("\nNiestety,", choice, "nie jest prawidłowym wyborem.")
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 377
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ż
378 Rozdział 12. Dźwięk, animacja i rozwijanie programu. Gra Astrocrash
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
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
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
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.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 381
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.
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
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
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.
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.
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)
if self.bottom < 0:
self.top = games.screen.height
if self.right < 0:
self.left = games.screen.width
Wystrzeliwanie pocisków 385
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.
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
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 387
Potem wykonuję trochę obliczeń, aby określić początkowe położenie nowego pocisku:
# zamień na radiany
angle = ship_angle * math.pi / 180
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()
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.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ł.
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.
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 389
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.
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.
Kiedy zostaje wywołana metoda die() obiektu klasy Missile, obiekt sam się niszczy.
392 Rozdział 12. Dźwięk, animacja i rozwijanie programu. Gra Astrocrash
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.
Gdy zostaje wywołana metoda die() obiektu klasy Ship, obiekt sam się niszczy.
Stała SPAWN określa liczbę nowych asteroid, jakie powstają po zniszczeniu asteroidy
macierzystej.
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.
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.
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.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 395
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()
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.
Odtąd zawsze, kiedy zmienię metodę die() klasy Wrapper, klasa Asteroid
automatycznie zbierze wynikające z tego korzyści.
396 Rozdział 12. Dźwięk, animacja i rozwijanie programu. Gra Astrocrash
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.
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 397
"eksplozja3.bmp",
"eksplozja4.bmp",
"eksplozja5.bmp",
"eksplozja6.bmp",
"eksplozja7.bmp",
"eksplozja8.bmp",
"eksplozja9.bmp"]
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.
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.
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 399
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
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
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)
# przejdź do poziomu 1
self.advance()
# rozpocznij grę
games.screen.mainloop()
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
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
# 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.
402 Rozdział 12. Dźwięk, animacja i rozwijanie programu. Gra Astrocrash
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 403
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):
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().
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()
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().
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.
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ą.
406 Rozdział 12. Dźwięk, animacja i rozwijanie programu. Gra Astrocrash
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.
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
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.
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
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.
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.
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.
414 Dodatek B. Opis pakietu livewires
Klasa Text używa właściwości value, size i color do tworzenia obiektu graficznego
reprezentującego wyświetlany tekst.
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 415
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.
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.
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.
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.
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.
A D do właściwości, 248
do zagnieżdżonych
abstrakcja, 171 DBMS, Database krotek, 148
aktualizacja Management System, 90 do zamarynowanych
zmiennej, 79 definiowanie obiektów, 213
algorytm, 93 funkcji, 170 do znaku łańcucha, 109
anagram, 129 klasy, 229 sekwencyjny, 107
animacja, 361, 368, 370 konstruktora, 301 swobodny, 107
argument, 23 metody, 229 duszek, sprite, 332, 335
nazwany, 176, 178 dekorator, 239 dziedziczenie, 263, 265
pozycyjny, 178 dodanie dzielenie
wyjątku, 218 obiektu do ekranu, 335, całkowite, 44
ASCII-Art, 36 338, 341 zmiennoprzecinkowe, 44
atrybut, 227, 232 pary klucz-wartość, 156 dzwonek systemowy, 39
atrybut klasy, 236, 238 dokumentowanie funkcji, dźwięk, 361, 371
Animation, 415 171
Message, 342 domyślne wartości E
atrybuty prywatne, 241 parametrów, 177, 179
dostęp ekran, 341, 344
B do atrybutów, 235, 238, ekran graficzny, 327, 331
245 element, 153
blok kodu, 70 do atrybutów elementy interfejsu GUI, 292
błąd, 23 prywatnych, 242 etykieta, 296, 298
błąd logiczny, 54, 55 do elementów
zagnieżdżonych, 146 F
C do elementu łańcucha,
110 fałsz, 85
ciało pętli, 78 do kodu źródłowego, 19 funkcja, 22
cudzysłów, 32 do metod prywatnych, __additional_cards(),
podwójny, 34 243 285
pojedynczy, 34 do pliku binarnego, 210 __init__(), 285, 354–357,
potrójny, 35 do pliku tekstowego, 203 379, 399
do wartości słownika, __pass_time(), 250
153 advance(), 400
424 Python dla każdego. Podstawy programowania
M N odmarynowanie, 211
odtwarzanie
marynowanie, 209, 210 nadklasa, superclass, 271 dźwięku, 373, 374
menedżer układu Grid, 305, nazwa zmiennej, 47 muzyki, 375
306 niemutowalność okno
metoda, Patrz funkcja krotek, 127 główne, root window, 293
metody, 229 łańcuchów, 111 graficzne, 325
instancji, 229, 230 notacja z kropką, 66 konsoli, 16
klasy bazowej, 271 numery pozycji Python Shell, 21
klasy Mouse, 416 dodatnie, 109 OOP, object-oriented
klasy Music, 417 ujemne, 110 programming, 17, 225
klasy Screen, 410 opcja Edit with IDLE, 27
klasy Sprite, 412, 413 O operacje na liczbach, 42
klasy Text, 414 operator
listy, 140, 144 obiekt, 230 in, 107, 125, 137, 154
łańcucha, 52, 53 klasy Card, 261 logiczny and, 91
obiektu dźwiękowego, klasy Ship, 382 logiczny not, 90
418 klasy Sprite, 342 logiczny or, 92
obiektu pliku, 208 klasy Text, 338 modulo, 44
obiektu screen, 328 modułu games, 327 operatory
prywatne, 241, 243 mouse, 350 matematyczne, 44
słownika, 159 screen, 328, 341 porównania, 69, 70
statyczne, 236, 239 obiekty operatory
moduł, 274 graficzne, 331 rozszerzonego
color, 338, 398, 422 programowe, 225 przypisania, 59
decimal, 45 obliczanie sekwencji, 105
games, 327, 353, liczby sekund, 61 otrzymywanie
418–422 wagi, 61 informacji, 173
karty, 277 obsługa wartości, 175
math, 382 danych wejściowych, 347 otwieranie pliku, 202
pakietu livewires, 409 kolizji, 352, 390
pickle, 211 wyjątków, 214–217
P
random, 65, 96 zdarzeń, 292, 303
shelve, 212 odbieranie komunikatów, pakiet
tkinter, 295, 297, 301, 256 livewires, 325, 347, 384,
318 odczyt klawiatury, 363 398, 407
modyfikowanie odczytywanie pygame, 325
metod, 269 danych, 200 para klucz-wartość, 156–158
okna głównego, 296 wartości zmiennej, 183 parametr, 173
mutowalność, 111 z pliku, 203, 211 lifetime, 415
mutowalność list, 138 z wiersza, 204 pozycyjny, 177
muzyka, 371, 374 zawartości pliku, 205 self, 230
mysza, 348
Skorowidz 427
U W wstawianie
tekstu, 309
układ współrzędnych, 331 wartości znaku cudzysłowu, 38
umiejscowienie widżetu, mutowalne, 151 znaku nowego wiersza,
306 początkowe, 96 38
umieszczanie na półce, 212 zwrotne, 23, 172, 174 wycinanie
UML, Unified Modeling wartość krotek, 126
Language, 258 False, 82, 85 łańcuchów, 116
umyślne pętle None, 118 wycinek, 118
nieskończone, 86 True, 82, 85 wycinki list, 137
uruchamianie programu, 24 wartownik, 79 wyjątek, 214
ustawienie tła, 330 warunek, 69 wyjątek AttributeError, 242
usuwanie prosty, 88 wyjście z pętli, 87
elementu listy, 139 złożony, 88 wykrywanie kolizji, 350, 352
pary klucz-wartość, 158 wcięcie, 70 wypisywanie
wycinka listy, 139 wczytywanie wierszy, 205 krotki, 123
używanie wiązanie widżetów, 303 wartości, 34
cudzysłowów, 32 widoczność wskaźnika znaku lewego ukośnika,
domyślnych wartości myszy, 349 38
parametrów, 179 widżet, 296 wyrażenie, 44
etykiet, 296 Button, 298, 299 wysyłanie komunikatów,
klas pochodnych, 272 Entry, 305, 308 256
klucza, 154 Frame, 297 wyświetlanie
komentarza, 27 Label, 298 duszka, 332
używanie Text, 305, 308 komunikatu, 339
konstruktorów, 230 właściwości menu, 141
krotek, 123, 144 klasy Mouse, 416 obiektu, 236
list, 144 klasy Screen, 410 tekstu, 336
metod łańcucha, 50 klasy Sprite, 336, 411 wartości zmiennej, 60
parametrów zwrotnych, klasy Text, 339, 414 wyników, 142, 148
172 obiektu mouse, 350 wywołanie funkcji, 171
przycisków, 298 obiektu screen, 328 wywoływanie metody, 230
sekwencji specjalnych, właściwość, 245, 246 klasy bazowej, 271
36 angle, 367 statycznej, 240
sekwencji bottom, 346
zagnieżdżonych, 145 mood, 250 Z
słowników, 152 still_playing, 285
stałych, 185 wprowadzanie danych, 60 zagnieżdżanie wywołań
widżetów, 305 współrzędne funkcji, 58
zmiennych globalnych, myszy, 348 zakres, 181
181, 185 punktu, 331 zamykanie pliku, 202
znaku kontynuacji
wiersza, 42
Skorowidz 431