Programowanie Python

Zanurkuj w Pythonie
Stworzone na Wikibooks,
bibliotece wolnych podreczników.
Wydanie I z dnia 17 lutego 2008
Copyright
c 2005-2008 uzytkownicy Wikibooks.
Permission is granted to copy, distribute and/or modify this document under the terms
of the GNU Free Documentation License, Version 1.2 or any later version published by
the Free Software Foundation; with no Invariant Sections, no Front-Cover Texts, and
no Back-Cover Texts. A copy of the license is included in the section entitled “GNU
Free Documentation License”.
Udziela sie zezwolenia na kopiowanie, rozpowszechnianie i/lub modyfikacje tresci artykułów
polskich Wikibooks zgodnie z zasadami Licencji GNU Wolnej Dokumentacji
(GNU Free Documentation License) w wersji 1.2 lub dowolnej pózniejszej opublikowanej
przez Free Software Foundation; bez Sekcji Niezmiennych, Tekstu na Przedniej
Okładce i bez Tekstu na Tylnej Okładce. Kopia tekstu licencji znajduje sie w czesci
zatytułowanej “GNU Free Documentation License”.
Dodatkowe objasnienia sa podane w dodatku “Dalsze wykorzystanie tej ksiazki”.
Wikibooks nie udziela zadnych gwarancji, zapewnien ani obietnic dotyczacych poprawnosci
publikowanych tresci. Nie udziela tez zadnych innych gwarancji, zarówno
jednoznacznych, jak i dorozumianych.
Spis tresci
1 Wstep 1
1.1 O podreczniku . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
2 Instalacja 3
2.1 Który Python jest dla ciebie najlepszy? . . . . . . . . . . . . . . . . . . 4
2.2 Python w systemie Windows . . . . . . . . . . . . . . . . . . . . . . . . 5
2.3 Python w systemie Mac OS . . . . . . . . . . . . . . . . . . . . . . . . . 7
2.4 Python w systemach Linux . . . . . . . . . . . . . . . . . . . . . . . . . 10
2.5 Instalacja ze zródeł . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
2.6 Interaktywna powłoka . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
2.7 Podsumowanie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
3 Pierwszy program 17
3.1 Nurkujemy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
3.2 Deklarowanie funkcji . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
3.3 Dokumentowanie funkcji . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
3.4 Wszystko jest obiektem . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
3.5 Wciecia kodu . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
3.6 Testowanie modułów . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
4 Wbudowane typy danych 29
4.1 Łancuchy znaków i unikod . . . . . . . . . . . . . . . . . . . . . . . . . . 30
4.2 Słowniki . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
4.3 Listy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
4.4 Krotki . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
4.5 Deklarowanie zmiennych . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
4.6 Formatowanie łancucha znaków . . . . . . . . . . . . . . . . . . . . . . . 46
4.7 Odwzorowywanie listy . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
4.8 Łaczenie list i dzielenie łancuchów znaków . . . . . . . . . . . . . . . . . 50
4.9 Kodowanie znaków . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
4.10 Praca z unikodem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
4.11 Podsumowanie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
5 Potega introspekcji 61
5.1 Nurkujemy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
5.2 Argumenty opcjonalne i nazwane . . . . . . . . . . . . . . . . . . . . . . 64
5.3 Dwa sposoby importowania modułów . . . . . . . . . . . . . . . . . . . . 66
5.4 type, str, dir i inne wbudowane funkcje . . . . . . . . . . . . . . . . . . . 68
i
5.5 Funkcja getattr . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
5.6 Filtrowanie listy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
5.7 Operatory and i or . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
5.8 Wyrazenia lambda . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
5.9 Potega introspekcji - wszystko razem . . . . . . . . . . . . . . . . . . . . 84
5.10 Podsumowanie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
6 Obiekty i klasy 89
6.1 Nurkujemy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
6.2 Definiowanie klas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
6.3 Tworzenie instancji klasy . . . . . . . . . . . . . . . . . . . . . . . . . . 97
6.4 Klasa opakowujaca UserDict . . . . . . . . . . . . . . . . . . . . . . . . . 99
6.5 Metody specjalne . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102
6.6 Zaawansowane metody specjalne . . . . . . . . . . . . . . . . . . . . . . 105
6.7 Atrybuty klas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107
6.8 Funkcje prywatne . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109
6.9 Podsumowanie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111
7 Wyjatki i operacje na plikach 113
7.1 Obsługa wyjatków . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114
7.2 Praca na plikach . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 118
7.3 Petla for . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123
7.4 Korzystanie z sys.modules . . . . . . . . . . . . . . . . . . . . . . . . . . 126
7.5 Praca z katalogami . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129
7.6 Wyjatki i operacje na plikach - wszystko razem . . . . . . . . . . . . . . 133
7.7 Wyjatki i operacje na plikach - podsumowanie . . . . . . . . . . . . . . . 135
8 Wyrazenia regularne 137
8.1 Nurkujemy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 138
8.2 Analiza przypadku: Adresy ulic . . . . . . . . . . . . . . . . . . . . . . . 139
8.3 Analiza przypadku: Liczby rzymskie . . . . . . . . . . . . . . . . . . . . 142
8.4 Składnia ?n, m? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 146
8.5 Rozwlekłe wyrazenia regularne . . . . . . . . . . . . . . . . . . . . . . . 150
8.6 Analiza przypadku: Przetwarzanie numerów telefonów . . . . . . . . . . 152
8.7 Podsumowanie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 157
9 Przetwarzanie HTML-a 159
9.1 Nurkujemy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 160
9.2 Wprowadzenie do sgmllib.py . . . . . . . . . . . . . . . . . . . . . . . . 166
9.3 Wyciaganie danych z dokumentu HTML . . . . . . . . . . . . . . . . . . 169
9.4 Wprowadzenie do BaseHTMLProcessor.py . . . . . . . . . . . . . . . . . 172
9.5 locals i globals . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 175
9.6 Formatowanie napisów w oparciu o słowniki . . . . . . . . . . . . . . . . 179
9.7 Dodawanie cudzysłowów do wartosci atrybutów . . . . . . . . . . . . . . 181
9.8 Wprowadzenie do dialect.py . . . . . . . . . . . . . . . . . . . . . . . . . 183
9.9 Przetwarzanie HTML-a - wszystko razem . . . . . . . . . . . . . . . . . 187
9.10 Podsumowanie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 190
ii
10 Przetwarzanie XML-a 191
10.1 Nurkowanie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 192
10.2 Pakiety . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 200
10.3 Parsowanie XML-a . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 203
10.4 Wyszukiwanie elementów . . . . . . . . . . . . . . . . . . . . . . . . . . 207
10.5 Dostep do atrybutów elementów . . . . . . . . . . . . . . . . . . . . . . 209
10.6 Podsumowanie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 211
11 Skrypty i strumienie 213
11.1 Abstrakcyjne zródła wejscia . . . . . . . . . . . . . . . . . . . . . . . . . 214
11.2 Standardowy strumien wejscia, wyjscia i błedów . . . . . . . . . . . . . 219
11.3 Buforowanie odszukanego wezła . . . . . . . . . . . . . . . . . . . . . . . 224
11.4 Wyszukanie bezposrednich elementów potomnych . . . . . . . . . . . . . 226
11.5 Tworzenie oddzielnych funkcji obsługi wzgledem typu wezła . . . . . . . 227
11.6 Obsługa argumentów linii polecen . . . . . . . . . . . . . . . . . . . . . 230
11.7 Skrypty i strumienie - wszystko razem . . . . . . . . . . . . . . . . . . . 234
11.8 Podsumowanie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 236
12 HTTP 237
12.1 Nurkujemy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 238
12.2 Jak nie pobierac danych poprzez HTTP . . . . . . . . . . . . . . . . . . 241
12.3 Własciwosci HTTP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 242
12.4 Debugowanie serwisów HTTP . . . . . . . . . . . . . . . . . . . . . . . . 245
12.5 Ustawianie User-Agent . . . . . . . . . . . . . . . . . . . . . . . . . . . . 247
12.6 Korzystanie z Last-Modified i ETag . . . . . . . . . . . . . . . . . . . . 249
12.7 Obsługa przekierowan . . . . . . . . . . . . . . . . . . . . . . . . . . . . 253
12.8 Obsługa skompresowanych danych . . . . . . . . . . . . . . . . . . . . . 258
12.9 HTTP - wszystko razem . . . . . . . . . . . . . . . . . . . . . . . . . . . 261
12.10Podsumowanie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 264
13 SOAP 265
13.1 Nurkujemy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 266
13.2 Instalowanie odpowiednich bibliotek . . . . . . . . . . . . . . . . . . . . 268
13.3 Pierwsze kroki z SOAP . . . . . . . . . . . . . . . . . . . . . . . . . . . 270
13.4 Debugowanie serwisu sieciowego SOAP . . . . . . . . . . . . . . . . . . . 271
13.5 Wprowadzenie do WSDL . . . . . . . . . . . . . . . . . . . . . . . . . . 273
13.6 Introspekcja SOAP z uzyciem WSDL . . . . . . . . . . . . . . . . . . . . 274
13.7 Wyszukiwanie w Google . . . . . . . . . . . . . . . . . . . . . . . . . . . 277
13.8 Rozwiazywanie problemów . . . . . . . . . . . . . . . . . . . . . . . . . . 280
13.9 Podsumowanie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 284
14 Testowanie jednostkowe 285
14.1 Wprowadzenie do liczb rzymskich . . . . . . . . . . . . . . . . . . . . . . 286
14.2 Nurkujemy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 288
14.3 Wprowadzenie do romantest.py . . . . . . . . . . . . . . . . . . . . . . . 289
14.4 Testowanie poprawnych przypadków . . . . . . . . . . . . . . . . . . . . 293
14.5 Testowanie niepoprawnych przypadków . . . . . . . . . . . . . . . . . . 296
14.6 Testowanie zdroworozsadkowe . . . . . . . . . . . . . . . . . . . . . . . . 299
iii
15 Testowanie 2 303
15.1 roman.py, etap 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 304
15.2 roman.py, etap 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 309
15.3 roman.py, etap 3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 314
15.4 roman.py, etap 4 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 318
15.5 roman.py, etap 5 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 321
16 Refaktoryzacja 325
16.1 Obsługa błedów . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 326
16.2 Obsługa zmieniajacych sie wymagan . . . . . . . . . . . . . . . . . . . . 329
16.3 Refaktoryzacja . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 337
16.4 Postscript . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 342
16.5 Podsumowanie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 345
16.6 Nurkujemy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 346
16.7 Znajdowanie sciezki . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 349
16.8 Filtrowanie listy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 352
16.9 Odwzorowywanie listy . . . . . . . . . . . . . . . . . . . . . . . . . . . . 355
16.10Programowanie koncentrujace sie na danych . . . . . . . . . . . . . . . . 357
16.11Dynamiczne importowanie modułów . . . . . . . . . . . . . . . . . . . . 359
17 Programowanie funkcyjne 361
17.1 Programowanie funkcyjne - wszystko razem . . . . . . . . . . . . . . . . 362
17.2 Podsumowanie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 366
18 Funkcje dynamiczne 367
18.1 Nurkujemy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 368
18.2 plural.py, etap 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 369
18.3 plural.py, etap 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 372
18.4 plural.py, etap 3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 374
18.5 plural.py, etap 4 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 376
18.6 plural.py, etap 5 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 379
18.7 plural.py, etap 6 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 381
18.8 Podsumowanie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 385
19 Optymalizacja szybkosci 387
19.1 Nurkujemy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 388
19.2 Korzystanie z modułu timeit . . . . . . . . . . . . . . . . . . . . . . . . 391
19.3 Optymalizacja wyrazen regularnych . . . . . . . . . . . . . . . . . . . . 393
19.4 Optymalizacja przeszukiwania słownika . . . . . . . . . . . . . . . . . . 397
19.5 Optymalizacja operacji na listach . . . . . . . . . . . . . . . . . . . . . . 401
19.6 Optymalizacja operacji na napisach . . . . . . . . . . . . . . . . . . . . . 404
19.7 Podsumowanie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 406
A Informacje o pliku 407
A.1 Historia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 407
A.2 Informacje o pliku PDF i historia . . . . . . . . . . . . . . . . . . . . . . 407
A.3 Autorzy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 407
A.4 Grafiki . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 407
iv
B Dalsze wykorzystanie tej ksiazki 409
B.1 Wstep . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 409
B.2 Status prawny . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 409
B.3 Wykorzystywanie materiałów z Wikibooks . . . . . . . . . . . . . . . . . 409
C GNU Free Documentation License 411
v

Rozdział 1
Wstep
1
2 ROZDZIAŁ 1. WSTEP
1.1 O podreczniku
Podrecznik ten powstaje na podstawie ksiazki Dive into Python (w wiekszosci jest
to tłumaczenie), której autorem jest Mark Pilgrim, a udostepnionej na licencji GNU
Free Documentation License.
Autorzy i tłumacze
• Mark Pilgrim (autor ksiazki Dive into Python)
• Warszk
• Piotr Kiec
• Roman Frołow
• Andrzej Saski
• Adam Kubiczek
Rozdział 2
Instalacja
3
4 ROZDZIAŁ 2. INSTALACJA
2.1 Który Python jest dla ciebie najlepszy?
Witamy w Pythonie. W tym rozdziale zajmiemy sie instalacja Pythona.
Który Python jest dla ciebie najlepszy?
Aby móc korzystac z Pythona, najpierw nalezy go zainstalowac. A moze juz go
mamy?
Jezeli posiadasz konto na jakimkolwiek serwerze, istnieje duze prawdopodobienstwo,
ze Python jest tam juz zainstalowany. Wiele popularnych dystrybucji Linuksa
standardowo instaluje ten jezyk programowania. Systemy Mac OS X 10.2 i nowsze
posiadaja dosyc okrojona wersje Pythona dostepnego jedynie z poziomu linii polecen.
Zapewne bedziesz chciał zainstalowac wersje, która da Ci wiecej mozliwosci.
Windows domyslnie nie zawiera zadnej wersji Pythona, ale nie załamuj sie! Istnieje
wiele sposobów, by w łatwy sposób zainstalowac Pythona w tym systemie operacyjnym.
Jak widzisz, wersje Pythona sa dostepne na wiele platform i systemów operacyjnych.
Mozemy zdobyc Pythona zarówno dla Windowsa, Mac OS, Mac OS X, wszystkich
wariantów Uniksa, w tym Linuksa czy Solarisa, jak i dla Amigi, OS/2, BeOSa,
czy tez innych systemów, o których najprawdopodobniej nawet nie słyszałes.
Co najwazniejsze, program napisany w Pythonie na jednej platformie, przy zachowaniu
niewielkiej dozy ostroznosci, zadziała na jakiejkolwiek innej. Mozesz na przykład
rozwijac swój program pod Windowsem, a nastepnie przeniesc go do Linuksa.
Wracajac do pytania rozpoczynajacego sekcje, “Który Python jest dla ciebie najlepszy?”.
Odpowiedz jest jedna: jakikolwiek, który mozesz zainstalowac na posiadanym
komputerze.
2.2. PYTHON W SYSTEMIE WINDOWS 5
2.2 Python w systemie Windows
Python w Windowsie
W Windowsie mamy pare sposobów zainstalowania Pythona.
Firma ActiveState tworzy instalator Pythona zwany ActivePython. Zawiera on
kompletna wersje Pythona, IDE z bardzo dobrym edytorem kodu oraz kilka rozszerzen
dla Windowsa, które zapewniaja dostep do specyficznych dla Windowsa usług, API
oraz rejestru.
ActivePython mozna pobrac nieodpłatnie, ale nie jest produktem Open Source.
Wydawany jest kilka miesiecy po wersji oryginalnej.
Druga opcja jest instalacja “oficjalnej” wersji Pythona, rozprowadzanej przez ludzi,
którzy rozwijaja ten jezyk. Jest to wersja ogólnodostepna, Open Source i zawsze
najnowsza.
Instalacja ActivePythona
Oto procedura instalacji ActivePythona:
1. Sciagamy ActivePythona ze strony http://www.activestate.com/Products/
ActivePython/.
2. Jezeli uzywamy Windows 95/98/ME/NT4/2000, bedziemy musieli najpierw zainstalowac
Windows Installer 2.0 dla Windowsa 95/98/Me lub Windows Installer
2.0 dla Windowsa NT4/2000 .
3. Klikamy dwukrotnie na sciagniety plik ActivePython-(pobrana wersja)-win32-ix86.msi
4. Przechodzimy wszystkie kroki instalatora.
5. Po zakonczeniu instalacji wybieramy Start->Programy->ActiveState ActivePython
2.2->PythonWin IDE. Zobaczymy wtedy ekran z napisem podobnym do ponizszego:
PythonWin 2.2.2 (#37, Nov 26 2002, 10:24:37) [MSC 32 bit (Intel)] on win32.
Portions Copyright 1994-2001 Mark Hammond (mhammond@skippinet.com.au) -
see ’Help/About PythonWin’ for further copyright information.
>>>
Instalacja Pythona z Python.org
1. Pobieramy z http://www.python.org/ftp/python/ najnowsza wersje instalatora
dla Windowsa, który oczywiscie bedzie miał rozszerzenie .exe.
2. Klikamy dwukrotnie na instalatorze Python-2.xxx.yyy.msi. Nazwa zalezec bedzie
od sciagnietej wersji Pytona.
3. Jezeli uzywamy Windows 95/98/ME/NT4/2000, bedziemy musieli najpierw zainstalowac
Windows Installer 2.0 dla Windowsa 95/98/Me lub Windows Installer
2.0 dla Windowsa NT4/2000 .
4. Przechodzimy przez wszystkie kroki instalatora.
5. Jezeli nie mamy uprawnien administratora, mozemy wybrac Advanced Options,
a nastepnie Non-Admin Install.
6 ROZDZIAŁ 2. INSTALACJA
6. Po zakonczeniu instalacji, wybieramy Start->Programy->Python 2.x->IDLE
(Python GUI). Zobaczymy ekran z napisem podobnym do ponizszego:
Python 2.5.1 (r251:54863, Apr 18 2007, 08:51:08) [MSC v.1310 32 bit (Intel)] on win32
Type "copyright", "credits" or "license()" for more information.
****************************************************************
Personal firewall software may warn about the connection IDLE
makes to its subprocess using this computer’s internal loopback
interface. This connection is not visible on any external
interface and no data is sent to or received from the Internet.
****************************************************************
IDLE 1.2.1
>>>
2.3. PYTHON W SYSTEMIE MAC OS 7
2.3 Python w systemie Mac OS
Python w Mac OS X
W Mac OS X mozemy miec Pythona na dwa sposoby: instalujac go lub nie robiac
tego. Zapewne bedziesz chciał go zainstalowac.
Mac OS X 10.2 i nowsze domyslnie instaluja okrojona wersje Pythona dostepnego
jedynie z linii polecen. Jezeli nie przeszkadza Ci praca w linii polecen, to poczatkowo
taka wersja moze Tobie wystarczyc. Jednak nie posiada ona parsera XML, wiec jesli
dojdziesz do rozdziału mówiacego na ten temat i tak bedziesz musiał zainstalowac
pełna wersje.
Zamiast wiec uzywac domyslnie zainstalowanej wersji, lepiej bedzie od razu zainstalowac
najnowsza, a która tez dostarczy nam wygodna, graficzna powłoke.
Uruchamianie wersji domyslnie zainstalowanej z systemem
1. Otwieramy katalog /Applications
2. Otwieramy katalog Utilities
3. Klikamy dwukrotnie na Terminal, by otworzyc okienko terminala, które zapewni
nam dostep do linii polecen.
4. Wpisujemy polecenie python.
Powinnismy otrzymac mniej wiecej takie cos:
<span>Welcome to Darwin!
[localhost:~] you% python
Python 2.2 (#1, 07/14/02, 23:25:09)
[GCC Apple cpp-precomp 6.14] on darwin
Type "help", "copyright", "credits", or "license" for more information.
>>> [press Ctrl+D to get back to the command prompt]
[localhost:~] you%</span>
Instalacja najnowszej wersji Pythona
Aby to zrobic postepujemy według ponizszych kroków:
1. Sciagamy obraz dysku MacPython-OSX z http://homepages.cwi.nl/~jack/
macpython/download.html.
2. Jezeli pobrany program nie zostanie uruchomiony przez przegladarke, klikamy
dwukrotnie na MacPython-OSX-(pobrana wersja).dmg by zamontowac obraz
dysku w systemie.
3. Klikamy dwukrotnie na instalator MacPython-OSX.pkg.
4. Instalator poprosi o login i hasło uzytkownika z prawami administratora.
5. Przechodzimy wszystkie kroki instalatora.
6. Po zakonczonej instalacji otwieramy katalog /Applications.
7. Otwieramy katalog MacPython-2.x.
8 ROZDZIAŁ 2. INSTALACJA
8. Klikamy dwukrotnie na PythonIDE by uruchomic Pythona.
MacPython IDE wyswietli ekran powitalny, a nastepnie interaktywna powłoke. Jezeli
jednak powłoka sie nie pojawi, wybieramy Window->Python Interactive (Cmd-0).
Otwarte okienko powinno wygladac podobnie do tego:
Python 2.3 (#2, Jul 30 2003, 11:45:28)
[GCC 3.1 20020420 (prerelease)]
Type "copyright", "credits" or "license" for more information.
MacPython IDE 1.0.1
>>>
Po instalacji najnowszej wersji, domyslnie zainstalowana wersja Pythona nadal
pozostanie w systemie. Podczas uruchamiania skryptów zwróc uwage z jakiej wersji
korzystasz.
Dwie wersje Pythona w Mac OS X
<span>[localhost:~] you% python
Python 2.2 (#1, 07/14/02, 23:25:09)
[GCC Apple cpp-precomp 6.14] on darwin
Type "help", "copyright", "credits", or "license" for more information.
>>> [press Ctrl+D to get back to the command prompt]
[localhost:~] you% /usr/local/bin/python
Python 2.3 (#2, Jul 30 2003, 11:45:28)
[GCC 3.1 20020420 (prerelease)] on darwin
Type "help", "copyright", "credits", or "license" for more information.
>>> [press Ctrl+D to get back to the command prompt]
[localhost:~] you%</span>
Instalacja Pythona z MacPortów
Ta metoda jest najlepsza. Nalezy wpierw pobrac i zainstalowac MacPorts (http:
//www.macports.org/). Nastepnie nalezy odswiezyc porty
sudo port selfupdate
Potem mozemy wyszukiwac interesujace nasz pakiety. Np. znalezienie wszystkich
pakietów do Pythona 2.5.x:
port search py25
Własciwa instalacja Pythona:
sudo port install python25
Wszystkie programy instalowane ta metoda sa składowane w /opt/local. Warto
wiec dodac do sciezki PATH /opt/local/bin.
Dobrze jest tez doinstalowac setuptools, który daje dostep do pythonowego instalatora
pakietów, skryptu easy install.
sudo port install py25-setuptools
2.3. PYTHON W SYSTEMIE MAC OS 9
Przydaje sie, gdy nie ma w portach pakietu dla naszej wersji Pythona, np. IPythona.
Czesc bibliotek mozna instalowac MacPortami, a reszte za pomoca easy setup.
Na przykład IPythona doinstalujemy za pomoca:
sudo easy_install ipython
Mozna tez aktualizowac pakiety:
sudo easy_install -U Pylons
Duze i małe znaki w nazwach pakietów, w wypadku uzycia easy install, nie maja
znaczenia.
Python w Mac OS 9
Mac OS 9 nie posiada domyslnie zadnej wersji Pythona, ale samodzielna instalacja
jest bardzo prosta.
1. Sciagamy plik MacPython23full.bin z http://homepages.cwi.nl/~jack/macpython/
download.html.
2. Jezeli plik nie zostanie automatycznie rozpakowany przez przegladarke, klikamy
dwukrotnie na MacPython23full.bin by to zrobic.
3. Klikamy dwukrotnie instalator MacPython23full.
4. Przechodzimy wszystkie kroki instalatora.
5. Po zakonczonej instalacji otwieramy katalog /Applications.
6. Otwieramy katalog MacPython-OS9 2.x.
7. Kliknij dwukrotnie na Python IDE by uruchomic Pythona.
MacPython IDE wyswietli ekran powitalny, a nastepnie interaktywna powłoke. Jezeli
jednak powłoka sie nie pojawi, wybieramy Window->Python Interactive (Cmd-0).
Otwarte okienko powinno wygladac podobnie do tego:
Python 2.3 (#2, Jul 30 2003, 11:45:28)
[GCC 3.1 20020420 (prerelease)]
Type "copyright", "credits" or "license" for more information.
MacPython IDE 1.0.1
>>>
10 ROZDZIAŁ 2. INSTALACJA
2.4 Python w systemach Linux
Python w dystrybucjach Linuksa
Instalacja z gotowych pakietów binarnych dla konkretnej dystrybucji Linuksa jest
stosunkowo prosta. Wiekszosc dystrybucji posiada juz zainstalowana wersje Pythona.
Mozesz takze pokusic sie o instalacje ze zródeł.
Wiele dystrybucji Linuksa zawiera graficzne narzedzia słuzace do instalacji oprogramowania.
My jednak opiszemy, jak to zrobic w konsoli w wybranych dystrybucjach
Linuksa.
Python w dystrybucji Red Hat Linux
Mozemy zainstalowac Pythona wykorzystujac polecenie rpm:
localhost:~$ su -
Password: [wpisz hasło roota]
[root@localhost root]# wget http://python.org/ftp/python/2.3/rpms/redhat-9/
python2.3-2.3-5pydotorg.i386.rpm
Resolving python.org... done.
Connecting to python.org[194.109.137.226]:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 7,495,111 [application/octet-stream]
...
[root@localhost root]# rpm -Uvh python2.3-2.3-5pydotorg.i386.rpm
Preparing... ############################################ [100%]
1:python2.3 ############################################ [100%]
[root@localhost root]# python #(1)
Python 2.2.2 (#1, Feb 24 2003, 19:13:11)
[GCC 3.2.2 20030222 (Red Hat Linux 3.2.2-4)] on linux2
Type "help", "copyright", "credits", or "license" for more information.
>>> [wcisnij Ctrl+D, zeby wyjsc z programu]
[root@localhost root]# python2.3 #(2)
Python 2.3 (#1, Sep 12 2003, 10:53:56)
[GCC 3.2.2 20030222 (Red Hat Linux 3.2.2-5)] on linux2
Type "help", "copyright", "credits", or "license" for more information.
>>> [wcisnij Ctrl+D, zeby wyjsc z programu]
[root@localhost root]# which python2.3 #(3)
/usr/bin/python2.3
1. Wpisujac polecenie python zostaje uruchomiony Python. Jednak jest to starsza
jego wersja, domyslnie zainstalowana wraz z systemem. To nie jest to, czego
chcemy.
2. Podczas pisania tej ksiazki najnowsza wersja był Python 2.3. Za pomoca polecenia
python2.3 uruchomimy nowsza, własnie zainstalowana wersje.
3. Jest to pełna sciezka do nowszej wersji Pythona, która dopiero co zainstalowalismy.
2.4. PYTHON W SYSTEMACH LINUX 11
Python w dystrybucji Debian
Pythona zainstalujemy wykorzystujac polecenie apt-get.
localhost:~$ su -
Password: [wpisz hasło roota]
localhost:~# apt-get install python
Reading Package Lists... Done
Building Dependency Tree... Done
The following extra packages will be installed:
python2.3
Suggested packages:
python-tk python2.3-doc
The following NEW packages will be installed:
python python2.3
0 upgraded, 2 newly installed, 0 to remove and 3 not upgraded.
Need to get 0B/2880kB of archives.
After unpacking 9351kB of additional disk space will be used.
Do you want to continue? [Y/n] Y
Selecting previously deselected package python2.3.
(Reading database ... 22848 files and directories currently installed.)
Unpacking python2.3 (from .../python2.3_2.3.1-1_i386.deb) ...
Selecting previously deselected package python.
Unpacking python (from .../python_2.3.1-1_all.deb) ...
Setting up python (2.3.1-1) ...
Setting up python2.3 (2.3.1-1) ...
Compiling python modules in /usr/lib/python2.3 ...
Compiling optimized python modules in /usr/lib/python2.3 ...
localhost:~# exit
logout
localhost:~$ python
Python 2.3.1 (#2, Sep 24 2003, 11:39:14)
[GCC 3.3.2 20030908 (Debian prerelease)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> [wcisnij Ctrl+D, zeby wyjsc z programu]
Python w dystrybucji Mandriva
W konsoli z uzytkownika root wpisujemy polecenie:
$ su -
Password: [wpisz hasło roota]
# urpmi python
Python w dystrybucji Fedora/Fedora Core
Aby zainstalowac Pythona w dystrybucji Fedora/Fedora Core nalezy w konsoli
wpisac:
$ su -
Password: [wpisz hasło roota]
# yum install python
12 ROZDZIAŁ 2. INSTALACJA
Mozna tez zainstalowac Pythona przy instalacji systemu, wybierajac pakiety programistyczne.
Python w dystrybucji Gentoo GNU/Linux
W Gentoo do instalacji Pythona mozemy uzyc programu emerge:
$ su -
Password: [wpisz hasło roota]
# emerge python
2.5. INSTALACJA ZE ZRÓDEŁ 13
2.5 Instalacja ze zródeł
Instalacja ze zródeł
Jezeli wolimy zainstalowac Pythona ze zródeł, bedziemy musieli pobrac kod zródłowy
z http://www.python.org/ftp/python/. Wybieramy najnowsza wersje (najwyzszy
numer) i sciagamy plik .tgz, a nastepnie wykonujemy standardowe komendy
instalacyjne (./configure, make, make install).
localhost:~$ su -
Password: [wpisz hasło roota]
localhost:~# wget http://www.python.org/ftp/python/2.3/Python-2.3.tgz
Resolving www.python.org... done.
Connecting to www.python.org[194.109.137.226]:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 8,436,880 [application/x-tar]
...
localhost:~# tar xfz Python-2.3.tgz
localhost:~# cd Python-2.3
localhost:~/Python-2.3# ./configure
checking MACHDEP... linux2
checking EXTRAPLATDIR...
checking for --without-gcc... no
...
localhost:~/Python-2.3# make
gcc -pthread -c -fno-strict-aliasing -DNDEBUG -g -O3 -Wall -Wstrict-prototypes -I.
-I./Include -DPy_BUILD_CORE -o Modules/python.o Modules/python.c
gcc -pthread -c -fno-strict-aliasing -DNDEBUG -g -O3 -Wall -Wstrict-prototypes -I.
-I./Include -DPy_BUILD_CORE -o Parser/acceler.o Parser/acceler.c
gcc -pthread -c -fno-strict-aliasing -DNDEBUG -g -O3 -Wall -Wstrict-prototypes -I.
-I./Include -DPy_BUILD_CORE -o Parser/grammar1.o Parser/grammar1.c
...
localhost:~/Python-2.3# make install
/usr/bin/install -c python /usr/local/bin/python2.3
...
localhost:~/Python-2.3# exit
logout
localhost:~$ which python
/usr/local/bin/python
localhost:~$ python
Python 2.3.1 (#2, Sep 24 2003, 11:39:14)
[GCC 3.3.2 20030908 (Debian prerelease)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> [wcisnij Ctrl+D, zeby wyjsc z programu]
localhost:~$
14 ROZDZIAŁ 2. INSTALACJA
2.6 Interaktywna powłoka
Interaktywna powłoka
Teraz kiedy juz mamy zainstalowanego Pythona, pewnie sie zastanawiamy, co to
jest ta interaktywna powłoka (interactive shell ), która uruchomilismy?
Powiedzmy tak: Python umozliwia prace na dwa sposoby. Jest interpreterem skryptów,
które mozemy uruchomic z linii polecen lub, jak inne aplikacje, dwukrotnie klikajac
na ikonce skryptu, a takze jest interaktywna powłoka, dzieki której mozemy
debugowac, sprawdzac działanie potrzebnych funkcji czy mozliwosci Pythona. Mozesz
nawet potraktowac powłoke jako kalkulator!
Uruchom teraz powłoke Pythona w odpowiedni sposób dla twojego systemu i
sprawdzmy co ona potrafi:
Przykład 1.5 Pierwsze kroki w interaktywnej powłoce
>>> 1 + 1 #(1)
2
>>> print ’hello world’ #(2)
hello world
>>> x = 1 #(3)
>>> y = 2
>>> x + y
3
1. Interaktywna powłoka Pythona moze wyliczac dowolne wyrazenia matematyczne.
2. Potrafi wykonywac dowolne polecenia Pythona.
3. Mozemy przypisac pewne wartosci do zmiennych, które sa pamietane tak długo,
jak długo jest uruchomiona nasza powłoka (ale nie dłuzej).
2.7. PODSUMOWANIE 15
2.7 Instalacja - podsumowanie
Podsumowanie
W tym momencie powinnismy juz miec zainstalowanego Pythona.
W zaleznosci od platformy mozesz miec zainstalowana wiecej niz jedna wersje Pythona.
Jezeli tak własnie jest to musisz uwazac na sciezki dostepu do programu. Piszac
tylko python w linii polecen, moze sie okazac, ze nie uruchamiasz wersji, której akurat
potrzebujesz. Byc moze bedziesz musiał podawac pełna sciezke dostepu lub odpowiednia
nazwe (python2.2, python2.4 itp.)
Gratulujemy i witamy w Pythonie!
16 ROZDZIAŁ 2. INSTALACJA
Rozdział 3
Pierwszy program
17
18 ROZDZIAŁ 3. PIERWSZY PROGRAM
3.1 Pierwszy program
Czy dostrzeglismy, ze wiekszosc ksiazek najpierw przedstawia elementarne zasady
programowania, a potem opisuje, jak korzystajac z nich stworzyc kompletny i działajacy
program? My zrobimy inaczej...
Nurkujemy
Oto kompletny, działajacy program w Pythonie. Prawdopodobnie jest on dla Ciebie
całkowicie niezrozumiały, ale nie przejmuj sie tym, poniewaz zaraz przeanalizujemy
go dokładnie, linia po linii. Przeczytaj go i sprawdz, czy cos jestes w stanie z niego
zrozumiec.
Przykład 2.1 odbchelper.py
#-*- coding: utf-8 -*-
def buildConnectionString(params):
u"""Tworzy łancuch znaków na podstawie słownika parametrów.
Zwraca łancuch znaków.
"""
return ";".join(["%s=%s" % (k, v) for k, v in params.items()])
if __name__ == "__main__":
myParams = {"server":"mpilgrim",
"database":"master",
"uid":"sa",
"pwd":"secret"
}
print buildConnectionString(myParams)
Teraz uruchommy ten program i zobaczmy, co sie stanie.
W IDE ActivePythona w systemie Windows mozemy uruchomic edytowany program
wybierajac File->Run... (Ctrl-R).Wynik wyswietlany jest w interaktywnym
oknie.
W IDE Pythona w systemie Mac OS uruchomimy program wybierajac
Python->Run window... (Cmd-R), jednak wczesniej musimy ustawic pewna wazna
opcje. W tym celu otwieramy plik .py w IDE, wywołujemy menu podreczne klikajac
czarny trójkat w prawym górnym rogu okna i upewniamy sie, ze opcja Run as
main jest zaznaczona.
W systemach Unix (takze w Mac OS X) mozesz uruchomic program z linii polecen
poleceniem python odbchelper.py
W wyniku uruchomienia programu otrzymujemy:
3.1. NURKUJEMY 19
pwd=secret;database=master;uid=sa;server=mpilgrim
20 ROZDZIAŁ 3. PIERWSZY PROGRAM
3.2 Deklarowanie funkcji
Deklarowanie funkcji
Python posiada funkcje, podobnie jak wiele innych jezyków programowania, lecz
nie definiujemy ich w oddzielnych plikach nagłówkowych (jak np. w C++), czy tez nie
dzielimy na sekcje interfejsu i implementacji jak w Pascalu. Jesli potrzebujemy jakiejs
funkcji, po prostu ja deklarujemy, na przykład:
def buildConnectionString(params):
Słowo kluczowe def rozpoczyna deklaracje funkcji, nastepnie podajemy nazwe
funkcji, a potem w nawiasach parametry. Wieksza liczbe parametrów podajemy po
przecinkach.
Jak widac funkcja nie definiuje zwracanego typu. Podczas deklarowania Pythonowych
funkcji nie okreslamy, czy maja one zwracac jakas wartosc, a nawet czy maja
cokolwiek zwracac. W rzeczywistosci kazda funkcja zwraca pewna wartosc. Jezeli w
funkcji znajduje sie instrukcja return, funkcja zwróci okreslona wartosc, wskazana za
pomoca tej instrukcji. W przeciwnym wypadku, gdy dana funkcja nie posiada instrukcji
return, zostanie zwrócona wartosc None, czyli tak zwana wartosc “pusta”, a w
innych jezykach czesto okreslana jako null lub nil.
W Visual Basicu funkcje (te, które zwracaja wartosc) rozpoczynaja sie słowem
kluczowym function, a procedury (z ang. subroutines, nie zwracaja wartosci) deklarujemy
za pomoca słowa sub. W Pythonie nie ma czegos takiego jak procedury. Sa
tylko funkcje, a kazda z nich zwraca wartosc (nawet jezeli jest to None) i wszystkie
rozpoczynaja sie słowem kluczowym def.
Argument params nie ma okreslonego typu. W Pythonie typy zmiennych nie okreslamy
w sposób jawny. Interpreter sam automatycznie rozpoznaje i sledzi typ zmiennej.
W Javie, C++ i innych jezykach z typami statycznymi musimy okreslic typ danych
zwracany przez funkcje, a takze typ kazdego argumentu. W Pythonie nigdzie tego nie
robimy. Bazujac na wartosci jaka została przypisana zmiennej, Python sam okresla jej
typ.
Typy danych w Pythonie a inne jezyki programowania
Jesli chodzi o nadawanie typów, jezyki programowania mozna podzielic na:
statycznie typowane Sa to jezyki, w których typy sa nadawane podczas kompilacji.
Wiele tego typu jezyków programowania wymaga deklarowania wszystkich
zmiennych przed ich uzyciem, przez podanie ich typu. Przykładami takich jezyków
jest Java, czy tez C.
dynamicznie typowane Sa to jezyki, w których typy zmiennych sa nadawane podczas
działania programu. VBScript i Python sa jezykami dynamicznie typowanymi,
poniewaz nadaja one typ zmiennej podczas przypisania do niej wartosci.
3.2. DEKLAROWANIE FUNKCJI 21
silnie typowane Sa to jezyki, w których miedzy róznymi typami widac wyrazna granice.
Jesli mamy pewna liczbe całkowita, to nie mozemy jej traktowac jak jakis
napis bez wczesniejszego przekonwertowania jej na łancuch znaków.
słabo typowane Sa to jezyki, w których mozemy nie zwracac uwagi na typ zmiennej.
Do takich jezyków zaliczymy VBScript. W tym jezyku mozemy, przy tym nie
wykonujac zadnej wyraznej konwersji, połaczyc łancuch znaków ’12’ z liczba
całkowita 3 otrzymujac łancuch ’123’, a nastepnie potraktowac go jako liczbe
całkowita 123. Konwersja jest wykonywana automatycznie.
Python jest jezykiem zarówno dynamicznie typowanym (poniewaz nie wymaga wyraznej
deklaracji typu), jak i silnie typowanym (poniewaz zmienne posiadaja wyraznie
ustalone typy, które nie podlegaja automatycznej konwersji).
22 ROZDZIAŁ 3. PIERWSZY PROGRAM
3.3 Dokumentowanie funkcji
Dokumentowanie funkcji
Funkcje mozna dokumentowac wstawiajac notke dokumentacyjna (ang. doc string).
Przykład 2.2 Definiowanie notki dokumentacyjnej w funkcji buildConnectionString
def buildConnectionString(params):
u"""Tworzy łancuch znaków na podstawie słownika parametrów.
Zwraca łancuch znaków.
"""
return ";".join(["%s=%s" % (k, v) for k, v in params.items()])
Trzy nastepujace po sobie cudzysłowy wykorzystuje sie do tworzenia ciagu znaków,
który moze zajmowac wiele linii. Wszystko co sie znajduje pomiedzy nimi tworzy
pojedynczy łancuch znaków; dotyczy to takze znaków powrotu karetki i cudzysłowu.
Potrójne cudzysłowy mozemy uzywac wszedzie, ale najczesciej wykorzystuje sie je do
tworzenia notek dokumentacyjnych.
Za pomoca potrójnych cudzysłowów ("""...""") mozemy w łatwy sposób zdefiniowac
łancuch znaków zawierajacy pojedyncze jak i podwójne cudzysłowy, podobnie
jak w przypadku qq/../ w Perlu.
Dzieki przedrostkowi u Python zapamieta ten łancuch znaków w taki sposób, ze
bedzie potrafił poprawnie zinterpretowac polskie litery. Wiecej szczegółów na ten temat
dowiemy sie w dalszej czesci tej ksiazki.
Wpowyzszym przykładzie wszystko pomiedzy potrójnymi cudzysłowami jest notka
dokumentacyjna funkcji, czyli opisem do czego dana funkcja słuzy i jak ja uzywac.
Notka dokumentacyjna, o ile istnieje, musi znalezc sie pierwsza w definicji funkcji
(czyli zaraz po dwukropku). Python nie wymaga, aby funkcja posiadała notke dokumentacyjna,
lecz powinnismy ja zawsze tworzyc. Ułatwia ona nam, a takze innym,
zorientowac sie w programie.Warto zaznaczyc, ze notka dokumentacyjna jest dostepna
jako atrybut funkcji nawet w trakcie wykonywania programu.
Wiele Pythonowych IDE wykorzystuje notki dokumentacyjne do “inteligentnej pomocy”,
czyli sugerowania nazwy funkcji przy jednoczesnym wyswietlaniu informacji o
niej (wzietej z notki). Moze to stanowic dla nas bardzo dobra pomoc, oczywiscie pod
warunkiem, ze notki dokumentacyjne zostały przez nas zdefiniowane...
Materiały dodatkowe
Materiały co prawda w jezyku angielskim, ale na pewno warto je chociaz przejrzec:
• PEP 257, na temat konwencji notek dokumentacyjnych,
• Python Style Guide, o tym, jak napisac dobra notke dokumentacyjna,
3.4. WSZYSTKO JEST OBIEKTEM 23
3.4 Wszystko jest obiektem
Wszystko jest obiektem
Wspomnielismy juz wczesniej, ze funkcje w Pythonie posiadaja atrybuty, a one
sa dostepne podczas pracy programu.
Funkcje, podobnie jak wszystko inne w Pythonie, sa obiektami.
Otwórzmy swój ulubiony IDE Pythona i wprowadz nastepujacy kod:
Przykład 2.3 Odwoływanie do napisu dokumentacyjnego funkcji buildConnectionString
>>> import odbchelper #(1)
>>> params = {"server":"mpilgrim", "database":"master", "uid":"sa", "pwd":"secret"}
>>> print odbchelper.buildConnectionString(params) #(2)
pwd=secret;database=master;uid=sa;server=mpilgrim
>>> print odbchelper.buildConnectionString.__doc__ #(3)
Tworzy łancuch znaków na podstawie słownika parametrów.
Zwraca łancuch znaków.
1. Pierwsza linia importuje program odbchelper jako moduł – kawałek kodu, który
mozemy uzywac interaktywnie. (W Rozdziale 4 zobaczymy przykłady programów
podzielonych na wiele modułów.) Kiedy juz zaimportujemy moduł, mozemy odwołac
sie do jego wszystkich publicznych funkcji, klas oraz atrybutów. Moduły
takze moga odwoływac sie do jeszcze innych modułów.
2. Aby wykorzystac jakas funkcje zdefiniowana w zaimportowanym module, musimy
przed nazwa funkcji dołaczyc nazwe modułu. Nie mozemy napisac buildConnectionString,
lecz zamiast tego mozemy dac odbchelper.buildConnectionString. Jesli kiedykolwiek
korzystalismy z klas w Javie, powinnismy zauwazyc pewne podobienstwa.
3. Tym razem zamiast wywoływac funkcje, zapytalismy sie o jeden z atrybutów
funkcji – atrybut doc . W nim Python przechowuje notke dokumentacyjna.
import w Pythonie działa podobnie jak require w Perlu. Kiedy zaimportujemy
jakis moduł, odwołujemy sie do jego funkcji poprzez modul.funkcja.WPerlu wyglada
to troszke inaczej, piszemy modul::funkcja.
Sciezka przeszukiwania modułów
Zanim przejdziemy dalej, nalezy wspomniec o sciezce przeszukiwania modułów.
W Pythonie przegladanych jest kilka miejsc w poszukiwaniu importowanego modułu.
Generalnie przeszukiwane sa wszystkie katalogi zdefiniowane w sys.path. Jest to lista,
która mozemy w łatwy sposób przegladac i modyfikowac w podobny sposób jak inne
listy (jak to robic dowiemy sie w kolejnych rozdziałach).
24 ROZDZIAŁ 3. PIERWSZY PROGRAM
Przykład 2.4 Sciezka przeszukiwania modułów
>>> import sys #(1)
>>> sys.path #(2)
[’’, ’/usr/local/lib/python2.2’, ’/usr/local/lib/python2.2/plat-linux2’,
’/usr/local/lib/python2.2/lib-dynload’, ’/usr/local/lib/python2.2/site-packages’,
’/usr/local/lib/python2.2/site-packages/PIL’, ’/usr/local/lib/python2.2/
site-packages/piddle’]
>>> sys #(3)
<module ’sys’ (built-in)>
>>> sys.path.append(’/my/new/path’) #(4)
1. Zaimportowanie modułu sys spowoduje, ze wszystkie jego funkcje i atrybuty
staja sie dostepne.
2. sys.path to lista nazw katalogów, które sa obecnie przeszukiwane podczas importowania
modułu. (Zawartosc listy zalezna jest od systemu operacyjnego, wersji
Pythona i połozenia jego instalacji, wiec na twoim komputerze moze wygladac
nieco inaczej.) Python przeszuka te katalogi (w zadanej kolejnosci) w poszukiwaniu
pliku .py, który nazywa sie tak samo jak importowany moduł.
3. Własciwie to troche rozminelismy sie z prawda. Sytuacja jest bardziej skomplikowana,
poniewaz nie wszystkie moduły wystepuja jako pliki z rozszerzeniem .py.
Niektóre, tak jak sys sa wbudowane w samego Pythona. Wbudowane moduły
zachowuja sie w ten sam sposób co pozostałe, ale nie mamy bezposredniego dostepu
do ich kodu zródłowego, poniewaz nie sa napisane w Pythonie (moduł sys
napisany jest w C).
4. Kiedy dodamy nowy katalog do sciezki przeszukiwania, Python przy nastepnych
importach przejrzy dodatkowo dodany katalog w poszukiwaniu modułu z rozszerzeniem
.py. Nowy katalog bedzie znajdował sie w sciezkach szukania tak długo,
jak długo uruchomiony bedzie interpreter. (Dowiesz sie wiecej o metodzie append
i innych metodach list w kolejnym rozdziale).
Co to jest obiekt
W Pythonie wszystko jest obiektem i prawie wszystko posiada metody i atrybuty.
Kazda funkcja posiada wbudowany atrybut doc , który zwraca napis dokumentacyjny
zdefiniowany w kodzie funkcji. Moduł sys jest obiektem, który posiada miedzy
innymi atrybut path.
Wdalszym ciagu nie wyjasnilismy jeszcze, co to jest obiekt. Kazdy jezyk programowania
definiuje “obiekt” w inny sposób.Wniektórych jezykach “obiekt” musi posiadac
atrybuty i metody, a w innych wszystkie “obiekty” moga dzielic sie na rózne podklasy.
W Pythonie jest inaczej, niektóre obiekty nie posiadaja ani atrybutów ani metod (wiecej
o tym w kolejnym rozdziale) i nie wszystkie obiekty dziela sie na podklasy (wiecej
o tym w rozdziale 5). Wszystko jest obiektem w tym sensie, ze moze byc przypisane
do zmiennej albo stanowic argument funkcji (wiecej o tym w rozdziale 4).
Poniewaz jest to bardzo wazne, wiec powtórzmy to jeszcze raz: wszystko w Pythonie
jest obiektem. Łancuchy znaków to obiekty, listy to obiekty, funkcje to
obiekty, a nawet moduły to obiekty...
3.4. WSZYSTKO JEST OBIEKTEM 25
Materiały dodatkowe
• Python Reference Manual dokładnie opisuje, co to znaczy, ze wszystko w Pythonie
jest obiektem
• eff-bot podsumowuje obiekty Pythona
26 ROZDZIAŁ 3. PIERWSZY PROGRAM
3.5 Wciecia kodu
Wciecia kodu
Funkcje w Pythonie nie posiadaja sprecyzowanych poczatków i konców oraz zadnych
nawiasów słuzacych do zaznaczania, gdzie funkcja sie zaczyna, a gdzie konczy.
Jedynym separatorem jest dwukropek (:) i wciecia kodu.
Przykład 2.5 Wciecia w funkcji buildConnectionString
def buildConnectionString(params):
u"""Tworzy łancuch znaków na podstawie słownika parametrów.
Zwraca łancuch znaków.
"""
return ";".join(["%s=%s" % (k, v) for k, v in params.items()])
Bloki kodu definiujemy poprzez wciecia. Przez “blok kodu” rozumiemy funkcje,
instrukcje if, petle for i while i tak dalej. Wstawiajac wciecie zaczynamy blok, a
konczymy go przestajac wstawiac wciecia danej wielkosci. Nie ma zadnych nawiasów,
klamer czy słów kluczowych. Oznacza to, ze białe znaki (spacje itp.) maja znaczenie
i ich stosowanie musi byc konsekwentne. W powyzszym przykładzie kod funkcji
(właczajac w to notke dokumentacyjna) został wciety czterema spacjami. Nie musimy
stosowac konkretnie czterech spacji, jednak musimy byc konsekwentni (tzn. jesli pierwsze
wciecie w funkcji miało 3 spacje, to kolejne wciecia takze musza miec 3 spacje).
Linia bez wciecia znajdowac sie bedzie poza funkcja.
Przykład 2.6 Fragment kodu z wcieciem po instrukcji if
def fib(n): #(1)
print ’n =’, n #(2)
if n > 1: #(3)
return n * fib(n - 1)
else: #(4)
print ’koniec’
return 1
1. Powyzsza funkcja, nazwana fib przyjmuje jeden argument: n. Cały kod wewnatrz
funkcji jest wciety.
2. Wypisywanie danych (na standardowe wyjscie) jest bardzo proste, wystarczy
uzyc słowa kluczowego print. Wyrazenie print moze przyjac kazdy typ danych,
na przykład łancuchy znaków, liczby całkowite i inne wbudowane typy danych jak
słowniki i listy, o których dowiemy sie w nastepnym rozdziale. Mozemy nawet
drukowac na ekran rózne wartosci w jednej linii. W tym celu podajemy ciag
wartosci, które chcemy wyswietlic, oddzielajac je przecinkiem. Kazda wartosc
jest wtedy wyswietlana w tej samej linii i oddzielona spacja (znak przecinka nie
jest drukowany). Tak wiec, kiedy funkcje fib wywołamy z argumentem 5, na
ekranie zobaczymy “ ”.
3. Do bloku kodu zaliczamy takze instrukcje if. Jezeli wyrazenie za instrukcja if
bedzie prawdziwe, to zostanie wykonany wciety kod znajdujacy sie zaraz pod
instrukcja if. W przeciwnym wypadku wykonywany jest blok else.
3.5. WCIECIA KODU 27
4. Oczywiscie bloki if oraz else moga składac sie z wiekszej ilosci linii, o ile linie
te maja wciecia z równa iloscia spacji. Tutaj blok else ma dwie linie. Python
nie wymaga zadnej specjalnej składni dla bloków składajacych sie z wielu linii.
Po prostu robimy wciecia o równej liczbie spacji.
Po poczatkowych problemach i nietrafionych porównaniach do Fortrana, pogodzisz
sie z tym i zobaczysz pozytywne cechy wciec. Jedna z głównych zalet jest to, ze wszystkie
programy w Pythonie wygladaja podobnie, poniewaz wciecia kodu sa wymagane
przez sam jezyk i nie zaleza od stylu pisania. Dzieki temu jakikolwiek kod jest prostszy
do czytania i zrozumienia.
Python uzywa znaku powrotu karetki (ang. carriage return), czyli znaku konca linii,
by oddzielic instrukcje. Natomiast dwukropek i wciecia słuza, aby oddzielic bloki kodu.
C++ oraz Java uzywaja sredników do oddzielania instrukcji, a klamry do separacji
bloków kodu.
Materiały dodatkowe
• Python Reference Manual omawia niektóre problemy zwiazane z wcieciami kodu.
• Python Style Guide mówi na temat dobrego stylu tworzenia wciec.
28 ROZDZIAŁ 3. PIERWSZY PROGRAM
3.6 Testowanie modułów
Testowanie modułów
Moduły Pythona to obiekty, które posiadaja kilka przydatnych atrybutów. Mozemy
ich uzyc do łatwego testowania własnych modułów. Oto przykład zastosowania triku
if name :
if __name__ == "__main__":
Zwrócmy uwage na dwie wazne sprawy: pierwsza, ze instrukcja warunkowa if nie
wymaga nawiasów, a druga, ze if konczy sie dwukropkiem, a w nastepnych liniach
wstawiamy wciety kod.
Podobnie jak w jezyku C, Python uzywa == dla porównania i = dla przypisania. W
przeciwienstwie do C, w Pythonie nie mozemy dokonywac przypisania w dowolnych
miejscach kodu, w tym w instrukcji warunkowej (np. if x = 10 nie jest poprawny).
Nie ma szansy, aby przypadkowo przypisac wartosc do zmiennej, zamiast zastosowac
porównanie.
Na czym polega zastosowany trik? Moduły to obiekty, a kazdy z nich posiada
wbudowany atrybut name . name zalezy od tego, w jaki sposób korzystamy z
danego modułu. Jezeli importujemy moduł, wtedy name jest nazwa pliku modułu
bez sciezki do katalogu, czy rozszerzenia pliku. Mozemy takze uruchomic bezposrednio
moduł jako samodzielny program, a wtedy name przyjmie domyslna wartosc
‘‘ main ’’.
>>> import odbchelper
>>> odbchelper.__name__
’odbchelper’
Wiedzac o tym, mozemy zaprojektowac test wewnatrz swojego modułu, wykorzystujac
do tego instrukcje if. Jesli uruchomisz bezposrednio dany moduł, atrybut
name jest równy ‘‘ main ’’, a wiec test zostanie wykonany. Podczas importu
tego modułu, name przyjmie inna wartosc, wiec test sie nie uruchomi. Pomoze nam
to rozwijac i wyszukiwac błedy w programie (czyli debugowac), zanim dany moduł
zintegrujemy z wiekszym programem.
Aby w MacPythonie mozna było skorzystac z tego triku, nalezy kliknac w czarny
trójkat w prawym górnym rogu okna i upewnic sie, ze zaznaczona jest opcja Run as
main .
Materiały dodatkowe
• Python Reference Manual omawia szczegóły dotyczace importowania modułów,
istnieje takze polskie tłumaczenie.
Rozdział 4
Wbudowane typy danych
29
30 ROZDZIAŁ 4. WBUDOWANE TYPY DANYCH
4.1 Łancuchy znaków i unikod
Za moment powrócimy do naszego pierwszego programu, jednak najpierw musimy
zrozumiec, czym sa słowniki, krotki i listy. Jesli choc troche umiesz programowac w
Perlu, to pewnie masz juz pewna wiedzy na temat słowników, czy list, ale pewnie nie
wiesz, czym sa krotki. Najpierw zajmiemy sie jednym z najczesciej uzywanych typów
danych, czyli łancuchami znaków.
Łancuchy znaków i unikod
Łancuchy znaków słuza do przechowywania napisu lub pewnych danych bajtowych.
W Pythonie, podobnie jak w wiekszosci innych jezyków programowania tworzymy
łancuchy znaków 1 (ang. string) poprzez umieszczenie danego tekstu w cudzysłowach.
Przykład 3.1 Definiowanie łancucha znaków
>>> text = "Nie za długi tekst" #(1)
>>> text
’Nie za dxc5x82ugi tekst’ #(2)
>> print text
Nie za długi tekst #(3)
>>> text2 = ’Kolejny napis, ale bez polskich liter’ #(4)
>>> text2
’Kolejny napis, ale bez polskich liter’
>>> text3 = ’Długi tekst,nktóry po przecinku znajdzie sie w nastepnej linii’ #(5)
>>> print text3
Długi tekst,
który po przecinku znajdzie sie w nastepnej linii
>>> text4 = r’Tutaj znaki specjalne np.n t, czy tez x26 nie zostana zinterpretowane’ >>> print text4
Tutaj znaki specjalne np.n t, czy tez x26 nie zostana zinterpretowane
1. W ten sposób stworzylismy łancuch znaków ‘‘Nie za długi tekst’’, który z
kolei przypisalismy do zmiennej text. Zauwazmy, ze wewnatrz łancucha moga
sie znajdowac polskie litery.
2. Otrzymany w tym miejscu wynik na wyjsciu moze sie nieco róznic na róznych
systemach. Jest to zalezne od kodowania znaków w systemie operacyjnym, z którego
korzystamy. Komputer uruchomiony przez autorów korzystał z kodowania
UTF-8. Zauwazmy, ze litera ł w systemie UTF-8 to dwa bajty "xc5x82".
3. Wszystko zostało ładnie wypisane. Tam gdzie jest ł widzimy ł, a nie jakies
“krzaki”.
4. Łancuch znaków nie musi byc deklarowany w podwójnym cudzysłowie, ale moze
byc tez to pojedynczy cudzysłów.
5. Znaki specjalne wstawiamy dodajac odwrotny ukosnik (tzw. backslash) np. n.
1W tym podreczniku łancuchy znaków bedziemy czasami nazywali napisami.
4.1. ŁANCUCHY ZNAKÓW I UNIKOD 31
6. Mozemy takze w stworzyc łancuch znaków w tzw. sposób surowy. Aby to uczynic,
poprzedzamy łancuch znaków litera r. Wewnatrz surowego łancucha znaków
odwrotny ukosnik nie jest interpretowany. Mozna powiedziec, ze znaki specjalne
w takim łancuchu nie sa znakami specjalnymi. To co napiszemy, to bedziemy
mieli.
Unikod
Poniewaz mówimy w jezyku polskim, piszemy w tym jezyku, a ponadto czytamy
w tym jezyku, zapewne chcielibysmy tworzyc programy, które dobrze sobie daja rade
ze znakami tego jezyka. Doskonałym do tego rozwiazaniem jest unikod (ang. unicode).
Unikod przechowuje nie tylko polskie znaki, ale jest systemem reprezentowania znaków
ze wszystkich jezyków swiata
Przykład 3.2 Definiowanie unikodowego łancucha znaków
>>> text = u"Nie za długi tekst" #(1)
>>> text
u’Nie za du0142ugi tekst’ #(2)
>>> print text
Nie za długi tekst
1. Aby utworzyc unikodowy napis, dodajemy przedrostek u i tyle.
2. Otrzymasz taki sam wynik. Dane przechowywane w unikodzie nie zaleza od systemu
kodowania, z którego korzysta Twój komputer.
Pamietamy, jak w poprzednim rozdziale powiedzielismy, ze do notek dokumentacyjnych
został dodany przedrostek u, aby Python potrafił poprawnie zinterpretowac
polskie znaki. Wtedy własnie wykorzystalismy unikod.
Przykład 3.3 Unikod w buildConnectionString def buildConnectionString(params):
u”Tworzy łancuch znaków na podstawie słownika parametrów.
Zwraca łancuch znaków. ”
32 ROZDZIAŁ 4. WBUDOWANE TYPY DANYCH
4.2 Słowniki
Słowniki
Jednym z wbudowanych typów sa słowniki (ang. dictionary). Okreslaja one wzajemna
relacje miedzy kluczem, a wartoscia.
Słowniki w Pythonie sa podobne do haszy w Perlu.WPerlu zmienne przechowujace
hasz sa reprezentowane zawsze przez poczatkowy znak %. W Pythonie typ danych jest
automatycznie rozpoznawany. Pythonowy słownik przypomina ponadto instancje klasy
Hashtable w Javie, a takze instancje obiektu Scripting.Dictionary w Visual Basicu.
Definiowanie słowników
Przykład 3.4 Definiowanie słowników
>>> d = {"server":"mpilgrim", "database":"master"} #(1)
>>> d
{’database’: ’master’, ’server’: ’mpilgrim’}
>>> d["server"] #(2)
’mpilgrim’
>>> d["database"] #(3)
’master’
>>> d["mpilgrim"] #(4)
Traceback (most recent call last):
File "<stdin>", line 1, in ?
KeyError: ’mpilgrim’
1. Najpierw utworzylismy nowy słownik z dwoma elementami i przypisalismy go do
zmiennej d. Kazdy element w słowniku jest para klucz -wartosc, a zbiór elementów
jest ograniczony nawiasem klamrowym.
2. ’server’ jest kluczem, a skojarzona z nim wartoscia jest ’mpilgrim’, do której
odwołujemy sie poprzez d[‘‘server’’].
3. ’database’ jest kluczem, a skojarzona z nim wartoscia jest ’master’, do której
odwołujemy sie poprzez d[‘‘database’’].
4. Mozesz dostac sie do wartosci za pomoca klucza, ale nie mozesz dostac sie
do klucza za pomoca wartosci. Tak wiec d[‘‘server’’] zwraca ’mpilgrim’,
ale wywołanie d[‘‘mpilgrim’’] sprawi, ze zostanie rzucony wyjatek, poniewaz
’mpilgrim’ nie jest kluczem słownika d.
Modyfikowanie słownika
Przykład 3.5 Modyfikowanie słownika
>>> d
{’database’: ’master’, ’server’: ’mpilgrim’}
>>> d["database"] = "pubs" #(1)
>>> d
4.2. SŁOWNIKI 33
{’database’: ’pubs’, ’server’: ’mpilgrim’}
>>> d["uid"] = "sa" #(2)
>>> d
{’database’: ’pubs’, ’uid’: ’sa’, ’server’: ’mpilgrim’}
1. Klucze w słowniku nie moga sie powtarzac. Przypisujac wartosc do istniejacego
klucza, bedziemy nadpisywac starsza wartosc.
2. W kazdej chwili mozesz dodac nowa pare klucz-wartosc. Składnia jest identyczna
do tej, która modyfikuje istniejaca wartosc. Czasami moze to spowodowac pewien
problem. Mozemy myslec, ze dodalismy nowa wartosc do słownika, ale w
rzeczywistosci nadpisalismy juz istniejaca.
Zauwazmy, ze słownik nie przechowuje kluczy w sposób posortowany. Wydawałoby
sie, ze klucz ’uid’ powinien sie znalezc za kluczem ’server’, poniewaz litera s jest
wczesniej w alfabecie niz u. Jednak tak nie jest. Elementy słownika znajduja sie w
losowej kolejnosci.
W słownikach nie istnieje okreslona kolejnosc układania elementów. Nie mozemy
dostac sie do elementów w słowniku w okreslonej, uporzadkowanej kolejnosci (np. w
porzadku alfabetycznym). Oczywiscie sa sposoby, aby to osiagnac, ale te “sposoby”
nie sa wbudowane w sam słownik.
Pracujac ze słownikami pamietajmy, ze wielkosc liter kluczy ma znaczenie.
Przykład 3.6 Nazwy kluczy sa wrazliwe na wielkosc liter
>>> d = {}
>>> d["klucz"] = "wartosc"
>>> d["klucz"] = "inna wartosc" #(1)
>>> d
{’klucz’: ’inna wartosc’}
>>> d["Klucz"] = "jeszcze inna wartosc" #(2)
>>> d
{’klucz’: ’inna wartosc’, ’Klucz’: ’jeszcze inna wartosc’}
1. Przypisujac wartosc do istniejacego klucza zamieniamy stara wartosc na nowa.
2. Nie jest to przypisanie do istniejacego klucza, a poniewaz łancuchy znaków w
Pythonie sa wrazliwe na wielkosc liter, dlatego tez ’klucz’ nie jest tym samym
co ’Klucz’. Utworzylismy nowa pare klucz-wartosc w słowniku. Obydwa klucze
moga sie wydawac podobne, ale dla Pythona sa zupełnie inne.
Przykład 3.7 Mieszanie typów danych w słowniku
>>> d
{’bazadanych’: ’pubs’, ’serwer’: ’mpilgrim’, ’uid’: ’sa’}
>>> d["licznik"] = 3 #(1)
>>> d
{’bazadanych’: ’pubs’, ’serwer’: ’mpilgrim’, ’licznik’: 3, ’uid’: ’sa’}
34 ROZDZIAŁ 4. WBUDOWANE TYPY DANYCH
>>> d[42] = "douglas" #(2)
>>> d
{42: ’douglas’, ’bazadanych’: ’pubs’, ’serwer’: ’mpilgrim’, ’licznik’: 3, ’uid’: ’sa’}
1. Słowniki nie sa przeznaczone tylko dla łancuchów znaków. Wartosc w słowniku
moze byc dowolnym typem danych: łancuchem znaków, liczba całkowita, obiektem,
a nawet innym słownikiem. W pojedynczym słowniku wszystkie wartosci
nie musza byc tego samego typu; mozemy wstawic do niego wszystko, co chcemy.
2. Klucze w słowniku sa bardziej restrykcyjne, ale moga byc łancuchami znaków,
liczbami całkowitymi i kilkoma innymi typami. Klucze wewnatrz jednego słownika
nie musza posiadac tego samego typu.
Usuwanie pozycji ze słownika
Przykład 3.8 Usuwanie pozycji ze słownika
>>> d
{’licznik’: 3, ’bazadanych’: ’master’, ’serwer’: ’mpilgrim’, 42: ’douglas’,
’uid’: ’sa’}
>>> del d[42] >>> d
{’licznik’: 3, ’bazadanych’: ’master’, ’serwer’: ’mpilgrim’, ’uid’: ’sa’}
>>> d.clear() >>> d
{}
1. Instrukcja del kaze usunac okreslona pozycje ze słownika, która jest wskazywana
przez podany klucz.
2. Instrukcja clear usuwa wszystkie pozycje ze słownika. Zbiór pusty ograniczony
przez nawiasy klamrowe oznacza, ze słownik nie ma zadnego elementu.
Materiały dodatkowe
• How to Think Like a Computer Scientist uczy o słownikach i pokazuje, jak wykorzystac
słowniki do tworzenia rzadkich macierzy.
• W Python Knowledge Base mozemy znalesc wiele przykładów kodów wykorzystujacych
słowniki.
• Python Cookbook wyjasnia, jak sortowac wartosci słownika wzgledem klucza.
• Python Library Reference opisuje wszystkie metody słownika.
4.3. LISTY 35
4.3 Listy
Listy
Listy sa jednym z najwazniejszych typów danych. Mozna sie z nimi spotkac w
Visual Basicu. W tym jezyku sa okreslane jako tablice.
Listy przypominaja tablice w Perlu. W Perlu zmienna, która przechowuje liste
rozpoczyna sie od znaku @, natomiast w Pythonie nazwa moze byc dowolna, poniewaz
Python automatycznie rozpoznaje typ.
Listy w Pythonie to cos wiecej niz tablice w Javie (chociaz mozna to traktowac jako
jedno). Lepsza analogia jest klasa ArrayList, w której mozna przechowywac dowolny
obiekt i dynamicznie dodawac nowe pozycje.
Definiowanie list
Przykład 3.9 Definiowanie list
>>> li = ["a", "b", "mpilgrim", "z", "przykład"] #(1)
>>> li
[’a’, ’b’, ’mpilgrim’, ’z’, ’przykład’]
>>> li[0] #(2)
’a’
>>> li[4] #(3)
’przykład’
1. Najpierw zdefiniowalismy liste piecioelementowa. Zauwazmy, ze lista zachowuje
swój oryginalny porzadek i nie jest to przypadkowe. Lista jest uporzadkowanym
zbiorem elementów ograniczonym nawiasem kwadratowym.
2. Lista moze byc uzywana tak jak tablica zaczynajaca sie od 0. Pierwszym elementem
niepustej listy o nazwie li jest zawsze li[0].
3. Ostatnim elementem piecioelementowej listy jest li[4], poniewaz indeksy sa
liczone zawsze od 0.
Przykład 3.10 Ujemne indeksy w listach
>>> li
[’a’, ’b’, ’mpilgrim’, ’z’, ’przykład’]
>>> li[-1] #(1)
’przykład’
>>> li[-3] #(2)
’mpilgrim’
1. Za pomoca ujemnych indeksów odnosimy sie do elementów idacych od konca
do poczatku tzn. li[-1] oznacza ostatni element, li[-2] przedostatni, li[-3]
odnosi sie do 3 od konca elementu itd. Ostatnim elementem niepustej listy jest
zawsze li[-1].
36 ROZDZIAŁ 4. WBUDOWANE TYPY DANYCH
2. Jesli ciezko ci zrozumiec o co w tym wszystkim chodzi, mozesz pomyslec o tym
w ten sposób: li[-n] == li[len(li) - n]. len to funkcja zwracajaca ilosc
elementów listy. Tak wiec w tym przypadku li[-3] == li[5 - 3] == li[2].
Przykład 3.11 Wycinanie list
>>> li
[’a’, ’b’, ’mpilgrim’, ’z’, ’przyklad’]
>>> li[1:3] #(1)
[’b’, ’mpilgrim’]
>>> li[1:-1] #(2)
[’b’, ’mpilgrim’, ’z’]
>>> li[0:3] #(3)
[’a’, ’b’, ’mpilgrim’]
1. Mozesz pobrac podzbiór listy, który jest nazywany “wycinkiem” (ang. slice),
poprzez okreslenie dwóch indeksów. Zwracana wartoscia jest nowa lista zawierajaca
wszystkie elementy z listy rozpoczynajace sie od pierwszego wskazywanego
indeksu (w tym przypadku li[1]) i idac w góre konczy na drugim wskazywanym
indeksie, nie dołaczajac go (w tym przypadku li[3]). Kolejnosc elementów
wzgledem wczesniejszej listy jest takze zachowana.
2. Mozemy takze podac ujemna wartosc któregos indeksu. Wycinanie wtedy takze
dobrze zadziała. Jesli to pomoze, mozemy pomyslec tak: czytamy liste od lewej
do prawej, pierwszy indeks okresla pierwszy potrzebny element, a drugi okresla
element, którego nie chcemy. Zwracana wartosc zawiera wszystko miedzy tymi
dwoma przedziałami.
3. Listy sa indeksowane od zera tzn. w tym przypadku li[0:3] zwraca pierwsze
trzy elementy listy, rozpoczynajac od li[0], a konczac na li[2], ale nie dołaczajac
li[3].
Przykład 3.12 Skróty w wycinaniu
>>> li
[’a’, ’b’, ’mpilgrim’, ’z’, ’przykład’]
>>> li[:3] #(1)
[’a’, ’b’, ’mpilgrim’]
>>> li[3:] #(2) (3)
[’z’, ’przykład’]
>>> li[:] #(2) (4)
[’a’, ’b’, ’mpilgrim’, ’z’, ’przykład’]
1. Jesli lewy indeks wynosi 0, mozemy go opuscic, wartosc 0 jest domyslna. li[:3]
jest tym samym, co li[0:3] z poprzedniego przykładu.
2. Podobnie, jesli prawy indeks jest długoscia listy, mozemy go pominac. Tak wiec
li[3:] jest tym samym, co li[3:5], poniewaz lista ta posiada piec elementów.
3. Zauwazmy pewna symetrycznosc. W piecioelementowej liscie li[:3] zwraca
pierwsze 3 elementy, a li[3:] zwraca dwa ostatnie (a w sumie 3 + 2 = 5).
W rzeczywistosci li[:n] bedzie zwracał zawsze pierwsze n elementów, a li[n:]
4.3. LISTY 37
pozostała liczbe, bez wzgledu na szerokosc listy (n moze byc wieksze od długosci
listy).
4. Jesli obydwa indeksy zostana pominiete, wszystkie elementy zostana dołaczone.
Nie jest to jednak to samo, co oryginalna lista li. Jest to nowa lista, która
posiada wszystkie takie same elementy. li[:] tworzy po prostu kompletna kopie
listy.
Dodawanie elementów do listy
Przykład 3.13 Dodawanie elementów do listy
>>> li
[’a’, ’b’, ’mpilgrim’, ’z’, ’przykład’]
>>> li.append("nowy") #(1)
>>> li
[’a’, ’b’, ’mpilgrim’, ’z’, ’przykład’, ’nowy’]
>>> li.insert(2, "nowy") #(2)
>>> li
[’a’, ’b’, ’nowy’, ’mpilgrim’, ’z’, ’przykład’, ’nowy’]
>>> li.extend(["dwa", "elementy"]) #(3)
>>> li
[’a’, ’b’, ’nowy’, ’mpilgrim’, ’z’, ’przykład’, ’nowy’, ’dwa’, ’elementy’]
1. Dodajemy pojedynczy element do konca listy za pomoca metody append.
2. Za pomoca insert wstawiamy pojedynczy element do listy. Numeryczny argument
jest indeksem, pod którym ma sie znalezc wstawiana wartosc; reszta
elementów, która znajdowała sie pod tym indeksem lub miała wiekszy indeks,
zostanie przesunieta o jeden indeks dalej. Zauwazmy, ze elementy w liscie nie
musza byc unikalne i moga sie powtarzac; w przykładzie mamy dwa oddzielne
elementy z wartoscia ’nowy’ – li[2] i li[6].
3. Za pomoca extend łaczymy liste z inna lista. Nie mozemy wywołac extend z
wieloma argumentami, trzeba ja wywoływac z pojedynczym argumentem – lista.
W tym przypadku ta lista ma dwa elementy.
Przykład 3.14 Róznice miedzy extend a append
>>> li = [’a’, ’b’, ’c’]
>>> li.extend([’d’, ’e’, ’f’]) #(1)
>>> li
[’a’, ’b’, ’c’, ’d’, ’e’, ’f’]
>>> len(li) #(2)
6
>>> li[-1]
’f’
>>> li = [’a’, ’b’, ’c’]
>>> li.append([’d’, ’e’, ’f’]) #(3)
>>> li
[’a’, ’b’, ’c’, [’d’, ’e’, ’f’]]
>>> len(li) #(4)
38 ROZDZIAŁ 4. WBUDOWANE TYPY DANYCH
4
>>> li[-1]
[’d’, ’e’, ’f’]
1. Listy posiadaja dwie metody – extend i append, które wygladaja na to samo,
ale w rzeczywistosci sa całkowicie rózne. extend wymaga jednego argumentu,
który musi byc lista i dodaje kazdy element z tej listy do oryginalnej listy.
2. Rozpoczelismy z lista trójelementowa (’a’, ’b’ i ’c’) i rozszerzylismy ja o inne
trzy elementy (’d’, ’e’ i ’f’) za pomoca extend, tak wiec mamy juz szesc
elementów.
3. append wymaga jednego argumentu, który moze byc dowolnym typem danych.
Metoda ta po prostu dodaje dany element na koniec listy. Wywołalismy append
z jednym argumentem, który jest lista z trzema elementami.
4. Teraz oryginalna lista, pierwotnie zawierajaca trzy elementy, zawiera ich cztery.
Dlaczego cztery? Poniewaz ostatni element przed chwila do niej wstawilismy.
Listy moga wewnatrz przechowywac dowolny typ danych, nawet inne listy. Nie
uzywajmy append, jesli zamierzamy liste rozszerzyc o kilka elementów.
Przeszukiwanie list
Przykład 3.15 Przeszukiwanie list
>>> li
[’a’, ’b’, ’nowy’, ’mpilgrim’, ’z’, ’przykład’, ’nowy’, ’dwa’, ’elementy’]
>>> li.index("przykład") #(1)
5
>>> li.index("nowy") #(2)
2
>>> li.index("c") #(3)
Traceback (most recent call last):
File "<stdin>", line 1, in ?
ValueError: list.index(x): x not in list
>>> "c" in li #(4)
False
1. index znajduje pierwsze wystapienie pewnej wartosc w liscie i zwraca jego indeks.
2. index znajduje pierwsze wystapienie wartosci w liscie.Wtym przykładzie, ’nowy’
wystepuje dwa razy – w li[2] i li[6], ale metoda index bedzie zawsze zwracac
pierwszy indeks, czyli 2.
3. Jesli wartosc nie zostanie znaleziona, Python zgłosi wyjatek. Takie zachowanie
nie jest czesto spotykane w innych jezykach, w wielu jezykach w takich przypadkach
zostaje zwrócony niepoprawny indeks. Takie zachowanie Pythona jest
dosyc dobrym posunieciem, poniewaz umozliwia szybkie wychwycenie błedu w
kodzie, a dzieki temu program nie bedzie błednie działał operujac na niewłasciwym
indeksie.
4.3. LISTY 39
4. Aby sprawdzic czy jakas wartosc jest w liscie uzywamy słowa kluczowego in,
który zwraca True, jesli wartosc zostanie znaleziona lub False jesli nie.
Przed wersja 2.2.1 Python nie posiadał oddzielnego boolowskiego typu danych.
Aby to zrekompensowac, Python mógł interpretowac wiekszosc typów danych jako
bool (czyli wartosc logiczna np. w instrukcjach if) według ponizszego schematu:
• 0 jest fałszem; wszystkie inne liczby sa prawda.
• Pusty łancuch znaków ("") jest fałszem, wszystkie inne sa prawda.
• Pusta lista ([]) jest fałszem; wszystkie inne sa prawda.
• Pusta krotka (()) jest fałszem; wszystkie inne sa prawda.
• Pusty słownik ({}) jest fałszem; wszystkie inne sa prawda.
Wszystkie powyzsze punkty stosowane sa w Pythonie 2.2.1 i nowszych, ale obecnie
mozna takze uzywac typu logicznego bool, który moze przyjmowac wartosc True
(prawda) lub False (fałsz). Zwrócmy uwage, ze wartosci te, tak jak cała składnia
jezyka Python, sa wrazliwe na wielkosc liter.
Usuwanie elementów z listy
Przykład 3.16 Usuwanie elementów z listy
>>> li
[’a’, ’b’, ’nowy’, ’mpilgrim’, ’z’, ’przykład’, ’nowy’, ’dwa’, ’elementy’]
>>> li.remove("z") #(1)
>>> li
[’a’, ’b’, ’nowy’, ’mpilgrim’, ’przykład’, ’nowy’, ’dwa’, ’elementy’]
>>> li.remove("nowy") #(2)
>>> li
[’a’, ’b’, ’mpilgrim’, ’przykład’, ’nowy’, ’dwa’, ’elementy’]
>>> li.remove("c") #(3)
Traceback (most recent call last):
File "<stdin>", line 1, in ?
ValueError: list.remove(x): x not in list
>>> li.pop() #(4)
’elementy’
>>> li
[’a’, ’b’, ’mpilgrim’, ’przykład’, ’nowy’, ’dwa’]
1. remove usuwa pierwsza wystepujaca wartosc w liscie.
2. remove usuwa tylko pierwsza wystepujaca wartosc. W tym przypadku ’nowy’
wystepuje dwa razy, ale li.remove("nowy") usuwa tylko pierwsze wystapienie.
3. Jesli wartosc nie zostanie znaleziona w liscie, Python wygeneruje wyjatek. Nasladuje
on w takich sytuacjach postepowanie metody index.
40 ROZDZIAŁ 4. WBUDOWANE TYPY DANYCH
4. pop jest ciekawa metoda, która wykonuje dwie rzeczy: usuwa ostatni element
z listy i zwraca jego wartosc. Metoda ta rózni sie od li[-1] tym, ze li[-1]
zwraca jedynie wartosc, ale nie zmienia listy, a od li.remove(value) tym, ze
li.remove(value) zmienia liste, ale nie zwraca wartosci.
Uzywanie operatorów na listach
Przykład 3.17 Operatory na listach
>>> li = [’a’, ’b’, ’mpilgrim’]
>>> li = li + [’przykład’, ’nowy’] #(1)
>>> li
[’a’, ’b’, ’mpilgrim’, ’przykład’, ’nowy’]
>>> li += [’dwa’] #(2)
>>> li
[’a’, ’b’, ’mpilgrim’, ’przykład’, ’nowy’, ’dwa’]
>>> li = [1, 2] * 3 #(3)
>>> li
[1, 2, 1, 2, 1, 2]
1. Aby połaczyc dwie listy, mozna tez skorzystac z operatora +. Za pomoca lista =
lista + innalista uzyskujemy ten sam wynik, co za pomoca list.extend(innalista),
ale operator + zwraca nowa liste, podczas gdy extend zmienia tylko istniejaca
liste. Ogólnie extend jest szybszy, szczególnie na duzych listach.
2. Python posiada takze operator +=. Operacja li += [’dwa’] jest równowazna
li.extend([’dwa’]). Operator += działa zarówno na listach, łancuchach znaków
jak i moze byc nadpisany dla dowolnego innego typu danych.
3. Operator * zwielokrotnia podana liste. li = [1, 2] * 3 jest równowazne z li
= [1, 2] + [1, 2] + [1, 2], które łaczy trzy listy w jedna.
Materiały dodatkowe
• How to Think Like a Computer Scientist uczy podstaw zwiazanych z wykorzystywaniem
list, a takze nawiazuje do przekazywania listy jako argument funkcji.
• Python Tutorial pokazuje, ze listy mozna wykorzystywac jako stos lub kolejke.
• Python Knowledge Base odpowiada na czesto zadawane pytania dotyczace list, a
takze posiada takze wiele przykładów kodów zródłowych wykorzystujacych listy.
• Python Library Reference opisuje wszystkie metody, które zawiera lista.
4.4. KROTKI 41
4.4 Krotki
Krotki
Krotka (ang. tuple) jest niezmienna lista. Zawartosc krotki okreslamy tylko podczas
jej tworzenia. Potem nie mozemy juz jej zmienic.
Przykład 3.18 Definiowanie krotki
>>> t = ("a", "b", "mpilgrim", "z", "element") #(1)
>>> t
(’a’, ’b’, ’mpilgrim’, ’z’, ’element’)
>>> t[0] #(2)
’a’
>>> t[-1] #(3)
’element’
>>> t[1:3] #(4)
(’b’, ’mpilgrim’)
1. Krotki definiujemy w identyczny sposób jak liste, lecz z jednym wyjatkiem – zbiór
elementów jest ograniczony w nawiasach okragłych, zamiast w kwadratowych.
2. Podobnie jak w listach, elementy w krotce maja okreslony porzadek. Sa one
indeksowane od 0, wiec pierwszym elementem w niepustej krotce jest zawsze
t[0].
3. Ujemne indeksy ida od konca krotki, tak samo jak w listach.
4. Krotki takze mozna wycinac. Kiedy wycinamy liste, dostajemy nowa liste. Podobnie,
gdy wycinamy krotke dostajemy nowa krotke.
Przykład 3.19 Krotki nie posiadaja zadnej metody
>>> t
(’a’, ’b’, ’mpilgrim’, ’z’, ’element’)
>>> t.append("nowy") #(1)
Traceback (most recent call last):
File "<stdin>", line 1, in ?
AttributeError: ’tuple’ object has no attribute ’append’
>>> t.remove("z") #(2)
Traceback (most recent call last):
File "<stdin>", line 1, in ?
AttributeError: ’tuple’ object has no attribute ’remove’
>>> t.index("przyklad") #(3)
Traceback (most recent call last):
File "<stdin>", line 1, in ?
AttributeError: ’tuple’ object has no attribute ’index’
>>> "z" in t #(4)
True
1. Nie mozna dodawac elementów do krotki. Krotki nie posiadaja metod typu
append, czy tez extend.
42 ROZDZIAŁ 4. WBUDOWANE TYPY DANYCH
2. Nie mozna usuwac elementów z krotki. Nie posiadaja one ani metody remove
ani metody pop.
3. Nie mozna wyszukiwac elementów w krotce. Nie maja one metody index.
4. Mozna jednak wykorzystac operator in, aby sprawdzic, czy krotka zawiera dany
element.
To w koncu do czego sa te krotki przydatne?
• Krotki działaja szybciej niz listy. Jesli definiujemy stały zbiór wartosci, który
bedzie uzywany tylko do iteracji, skorzystajmy z krotek zamiast z listy.
• Jesli chcielibysmy uzywac danych “zabezpieczonych przed zapisem” (np. po to,
zeby program był bezpieczniejszy), wykorzystajmy do tego krotki. Korzystajac
z krotek, zamiast z list, mamy pewnosc, ze dane w nich zawarte nie zostana
nigdzie zmienione. To troche tak, jakbysmy mieli gdzies ukryta instrukcje assert
sprawdzajaca, czy dane sa stałe. W przypadku, gdy nastapi próba nadpisania
pewnych wartosci w krotce, program zwróci wyjatek.
• Pamietasz, jak powiedzielismy, ze klucze w słowniku moga byc łancuchami znaków,
liczbami całkowitymi i “kilkoma innymi typami”? Krotki sa jednym z tych
“innych typów”. W przeciwienstwie do list, moga one zostac uzyte jako klucz
w słowniku. Dlaczego? Jest to dosyc skomplikowane. Klucze w słowniku musza
byc niezmienne. Krotki same w sobie sa niezmienne, jednak jesli wewnatrz krotki
znajdzie sie lista, to krotka ta nie bedzie mogła zostac uzyta jako klucz, poniewaz
lista jest zmienna.Wtakim przypadku krotka nie byłaby słownikowo-bezpieczna.
Aby krotka mogła zostac wykorzystana jako klucz, musi ona zawierac wyłacznie
łancuchy znaków, liczby i inne słownikowo-bezpieczne krotki.
• Krotki uzywane sa do formatowania tekstu, co zobaczymy wkrótce.
Krotki moga zostac w łatwy sposób przekonwertowane na listy. Wbudowana funkcja
tuple, której argumentem jest lista, zwraca krotke z takimi samymi elementami,
natomiast funkcja list, której argumentem jest krotka, zwraca liste. W rezultacie
tuple zamraza liste, a list odmraza krotke.
Materiały dodatkowe
• How to Think Like a Computer Scientist uczy, czym sa krotki i pokazuje, jak
łaczyc je ze soba.
• Python Knowledge Base pokazuje, jak posortowac krotke.
• Python Tutorial omawia, jak zdefniowac krotke z jednym elementem.
4.5. DEKLAROWANIE ZMIENNYCH 43
4.5 Deklarowanie zmiennych
Deklarowanie zmiennych
Wiemy juz troche o słownikach, krotkach i o listach, wiec wrócimy do przykładowego
kodu przedstawionego w rozdziale drugim, do odbchelper.py.
Podobnie jak wiekszosc jezyków programowania Python posiada zarówno zmienne
lokalne jak i globalne, choc nie deklarujemy ich w jakis wyrazny sposób. Zmienne
zostaja utworzone, gdy przypisujemy im pewne wartosci. Natomiast kiedy zmienna
wychodzi poza zasieg, zostaje automatycznie usunieta.
Przykład 3.20 Definiowanie zmiennej myParams
if __name__ == "__main__":
myParams = {"server":"mpilgrim",
"database":"master",
"uid":"sa",
"pwd":"secret"
}
Zwrócmy uwage na wciecia. Instrukcje warunkowe jako bloki kodu sa identyfikowane
za pomoca wciec, podobnie jak funkcje.
Zauwazmy tez, ze dzieki wykorzystaniu backslasha (“”) moglismy przypisanie wartosci
do zmiennej podzielic na kilka linii. Backslashe w Pythonie sa specjalnymi znakami,
które umozliwiaja kontynuacje danej instrukcji w nastepnej linii.
Kiedy polecenie zostanie podzielone na kilka linii za pomoca znaku kontynuacji
(“”), nastepna linia moze zostac wcieta w dowolny sposób. Python nie wezmie tego
wciecia pod uwage.
Scisle mówiac, wyrazenia w nawiasach okragłych, kwadratowych i klamrowych (jak
definiowanie słowników) mozna podzielic na wiele linii bez uzywania znaku kontynuacji
(“”). Niektórzy zalecaja dodawac backslashe nawet wtedy, gdy nie jest to konieczne.
Argumentuja to tym, ze kod staje sie wtedy czytelniejszy. Jest to jednak wyłacznie
kwestia gustu.
Wczesniej nigdy nie deklarowalismy zadnej zmiennej o nazwie myParams, ale własnie
przypisalismy do niej wartosc. Zachowanie to przypomina troche VBScript bez
instrukcji option explicit. Na szczescie, w przeciwienstwie do VBScript, Python
nie pozwala odwoływac sie do zmiennych, do których nie zostały wczesniej przypisane
zadne wartosci. Jesli spróbujemy to zrobic, Python rzuci wyjatek.
Odwoływanie sie do zmiennych
Przykład 3.21 Odwoływanie sie do niezdefiniowanej zmiennej
>>> x
Traceback (most recent call last):
File "<stdin>", line 1, in ?
NameError: name ’x’ is not defined
>>> x = 1
44 ROZDZIAŁ 4. WBUDOWANE TYPY DANYCH
>>> x
1
Kiedys bedziesz za to dziekowac...
Wielozmienne przypisania Jednym z lepszych Pythonowych skrótów jest wielozmienne
przypisanie (ang. multi-variable assignment), czyli jednoczesne (za pomoca
jednego wyrazenia) przypisywanie kilku wartosci do kilku zmiennych.
Przykład 3.22 Przypisywanie wielu wartosci za pomoca jednego wyrazenia
>>> v = (’a’, ’b’, ’e’)
>>> (x, y, z) = v #(1)
>>> x
’a’
>>> y
’b’
>>> z
’e’
1. v jest krotka trzech elementów, a (x, y, z) jest krotka trzech zmiennych. Przypisujac
jedna krotke do drugiej, przypisalismy kazda z wartosci v do odpowiednich
zmiennych (w odpowiedniej kolejnosci).
Moze to zostac wykorzystane w wielu sytuacjach. Czasami chcemy przypisac pewnym
zmiennym pewien zakres wartosci np. od 1 do 10. W jezyku C mozemy utworzyc
typy wyliczeniowe (enum) poprzez reczne utworzenie listy stałych i wartosci jakie przechowuja.
Moze to byc troche nudna i czasochłonna robota, w szczególnosci gdy wartosci
sa kolejnymi liczbami. W Pythonie mozemy wykorzystac wbudowana funkcje range i
wielozmienne przypisanie. W ten sposób z łatwoscia przypiszemy kolejne wartosci do
wielu zmiennych.
Przykład 3.23 Przypisywanie kolejnych wartosci
>>> range(7) #(1)
[0, 1, 2, 3, 4, 5, 6]
>>> (PONIEDZIALEK, WTOREK, SRODA, CZWARTEK, PIATEK, SOBOTA, NIEDZIELA) = range(7) #(2)
>>> PONIEDZIALEK #(3)
0
>>> WTOREK
1
>>> NIEDZIELA
6
1. Wbudowana funkcja range zwraca liste liczb całkowitych. W najprostszej formie
funkcja ta bierze górna granice i zwraca liste liczb od 0 do podanej granicy (ale
juz bez niej). (Mozemy takze ustawic poczatkowa wartosc rózna niz 0, a krok
moze byc inny niz 1. Aby otrzymac wiecej szczegółów wykorzystaj instrukcje
print range. doc .)
4.5. DEKLAROWANIE ZMIENNYCH 45
2. PONIEDZIALEK, WTOREK, SRODA, CZWARTEK, PIATEK, SOBOTA i NIEDZIELA sa zmiennymi,
które zdefiniowalismy. (Ten przykład pochodzi z modułu calendar; nazwy
zostały spolszczone. Moduł calendar umozliwia wyswietlanie kalendarzy,
podobnie jak to robi Uniksowy program cal. Moduł calendar przechowuje dla
odpowiednich dni tygodnia odpowiednie stałe.)
3. Teraz kazda zmienna ma własna wartosc: PONIEDZIALEK ma 0, WTOREK ma 1 itd.
Wielozmienne przypisania mozemy wykorzystac przy tworzeniu funkcji zwracajacych
wiele wartosci w postaci krotki. Zwrócona wartosc takiej funkcji mozemy potraktowac
jako normalna krotke lub tez przypisac wszystkie elementy tej krotki do
osobnych zmiennych za pomoca wielozmiennego przypisania. Wiele standardowych
bibliotek korzysta z tej mozliwosci np. moduł os, który omówimy w rozdziale 6.
Materiały dodatkowe
• Python Reference Manual pokazuje przykłady, kiedy mozna pominac znak kontynuacji
linii, a kiedy musisz go wykorzystac.
• How to Think Like a Computer Scientist wyjasnia, jak za pomoca wielozmiennych
przypisan zamienic wartosci dwóch zmiennych.
46 ROZDZIAŁ 4. WBUDOWANE TYPY DANYCH
4.6 Formatowanie łancucha znaków
Formatowanie łancucha znaków
W Pythonie mozemy formatowac wartosci wewnatrz łancucha znaków. Chociaz
mozemy tworzyc bardzo skomplikowane wyrazenia, jednak najczesciej w prostych przypadkach
wystarczy wykorzystac pole %s, aby wstawic pewien łancuch znaków wewnatrz
innego.
Formatowanie łancucha znaków w Pythonie uzywa tej samej składni, co funkcja
sprintf w jezyku C.
Przykład 3.24 Pierwsza próba formatowania łancucha
>>> k = "uid"
>>> v = "sa"
>>> "%s=%s" % (k, v) #(1)
’uid=sa’
1. Rezultatem wykonania tego wyrazenia jest łancuch znaków. Pierwsze %s zostało
zastapione wartoscia znajdujaca sie w k, a drugie wystapienie %s zostało zastapione
wartoscia v. Wszystkie inne znaki (w tym przypadku znak równosci)
pozostały w miejscach, w których były.
Zauwazmy, ze (k, v) jest krotka. Niedługo zobaczymy, do czego to moze byc przydatne.
Mogłoby sie wydawac, ze formatowanie jest jedna z wielu mozliwosci połaczenia
łancuchów znaków, jednak formatowanie łancucha znaków nie jest tym samym co
łaczenie łancuchów.
Przykład 3.25 Formatowanie tekstu a łaczenie
>>> uid = "sa"
>>> pwd = "secret"
>>> print pwd + " nie jest poprawnym hasłem dla " + uid #(1)
secret nie jest poprawnym hasłem dla sa
>>> print "%s nie jest poprawnym hasłem dla %s" % (pwd, uid) #(2)
secret nie jest poprawnym hasłem dla sa
>>> userCount = 6
>>> print "Uzytkowników: %d" % (userCount, ) #(3) (4)
Uzytkowników: 6
>>> print "Uzytkowników: " + userCount #(5)
Traceback (most recent call last):
File "<stdin>", line 1, in ?
TypeError: cannot concatenate ’str’ and ’int’ objects
1. + jest operatorem łaczacym łancuchy znaków.
2. W tym trywialnym przypadku formatowanie daje ten sam wynik co łaczenie.
4.6. FORMATOWANIE ŁANCUCHA ZNAKÓW 47
3. (userCount, ) jest jednoelementowa krotka. Składnia ta wyglada troche dziwnie,
jednak takie oznaczenie jest jednoznaczne i wiadomo, ze chodzi o krotke. Zawsze
mozna dołaczac przecinek po ostatnim elemencie listy, krotki lub słownika.
Jest on jednak wymagany tylko podczas tworzenia jednoelementowej krotki. Jesli
przecinek nie byłby wymagany, Python nie mógłby stwierdzic czy (userCount)
ma byc krotka, czy tez tylko wartoscia zmiennej userCount.
4. Formatowanie łancucha znaków działa z liczbami całkowitymi. W tym celu uzywamy
%d zamiast %s.
5. Spróbowalismy połaczyc łancuch znaków z czyms, co nie jest łancuchem znaków.
Został rzucony wyjatek. W przeciwienstwie do formatowania, łaczenie łancucha
znaków mozemy wykonywac jedynie na innych łancuchach.
Tak jak sprintf w C, formatowanie tekstu w Pythonie przypomina szwajcarski
scyzoryk. Mamy mnóstwo opcji do wyboru, a takze wiele pól dla róznych typów wartosci.
Przykład 3.26 Formatowanie liczb
>>> print "Dzisiejsza cena akcji: %f" % 50.4625 #(1)
Dzisiejsza cena akcji: 50.462500
>>> print "Dzisiejsza cena akcji: %.2f" % 50.4625 #(2)
Dzisiejsza cena akcji: 50.46
>>> print "Zmiana w stosunku do dnia wczorajszego: %+.2f" % 1.5 #(3)
Zmiana w stosunku do dnia wczorajszego: +1.50
1. Pole formatowania %f traktuje wartosc jako liczbe rzeczywista i pokazuje ja z
dokładnoscia do 6 miejsc po przecinku.
2. Modyfikator ‘‘.2’’ pola %f umozliwia pokazywanie wartosci rzeczywistej z dokładnoscia
do dwóch miejsc po przecinku.
3. Mozna nawet łaczyc modyfikatory. Dodanie modyfikatora + pokazuje plus albo
minus przed wartoscia, w zaleznosci od tego jaki znak ma liczba. Modyfikator
‘‘.2’’ został na swoim miejscu i nadal nakazuje wyswietlanie liczby z dokładnoscia
dwóch miejsc po przecinku.
Materiały dodatkowe
• Python Library Reference wymienia wszystkie opcje formatowania.
• Effective AWK Programming omawia wszystkie opcje formatowania, a takze
mówi o innych zaawansowanych technikach.
48 ROZDZIAŁ 4. WBUDOWANE TYPY DANYCH
4.7 Odwzorowywanie listy
Odwzorowywanie list
Jedna z bardzo uzytecznych cech Pythona sa wyrazenia listowe (ang. list comprehension),
które pozwalaja nam w zwiezły sposób odwzorowac pewna liste na inna,
wykonujac na kazdym jej elemencie pewna funkcje.
Przykład 3.27 Wprowadzenie do wyrazen listowych
>>> li = [1, 9, 8, 4]
>>> [element*2 for element in li] #(1)
[2, 18, 16, 8]
>>> li #(2)
[1, 9, 8, 4]
>>> li = [elem*2 for elem in li] #(3)
>>> li
[2, 18, 16, 8]
1. Aby zrozumiec o co w tym chodzi, spójrzmy na to od strony prawej do lewej.
li jest lista, która odwzorowujemy. Python przechodzi po kazdym elemencie
li, tymczasowo przypisujac wartosc kazdego elementu do zmiennej element,
a nastepnie wyznacza wartosc funkcji element*2 i wstawia wynik do nowej,
zwracanej listy.
2. Wyrazenia listowe nie zmieniaja oryginalnej listy.
3. Zwracany wynik mozemy przypisac do zmiennej, która odwzorowujemy. Nie spowoduje
to zadnych problemów. Python tworzy nowa liste w pamieci, a kiedy
operacja odwzorowywania listy dobiegnie konca, do zmiennej zostanie przypisana
lista znajdujaca sie w pamieci.
W funkcji buildConnectionString zadeklarowanej w rozdziale 2 skorzystalismy z
wyrazen listowych:
["%s=%s" % (k, v) for k, v in params.items()]
Zauwazmy, ze najpierw wykonujemy funkcje items, znajdujaca sie w słowniku
params. Funkcja ta zwraca wszystkie dane znajdujace sie w słowniku w postaci listy
krotek.
Przykład 3.28 Funkcje keys, values i items
>>> params = {"server":"mpilgrim", "database":"master", "uid":"sa", "pwd":"secret"}
>>> params.keys() #(1)
[’pwd’, ’database’, ’uid’, ’server’]
>>> params.values() #(2)
[’secret’, ’master’, ’sa’, ’mpilgrim’]
>>> params.items() #(3)
[(’pwd’, ’secret’), (’database’, ’master’), (’uid’, ’sa’), (’server’, ’mpilgrim’)]
4.7. ODWZOROWYWANIE LISTY 49
1. W słowniku metoda keys zwraca liste wszystkich kluczy. Lista ta nie jest uporzadkowana
zgodnie z kolejnoscia, z jaka definiowalismy słownik (pamietamy, ze
elementy w słowniku sa nieuporzadkowane).
2. Metoda values zwraca liste wszystkich wartosci. Lista ta jest zgodna z porzadkiem
listy zwracanej przez metode keys, czyli dla wszystkich wartosci x zachodzi
params.values()[x] == params[params.keys()[x]].
3. Metoda items zwraca liste krotek w formie (klucz, wartosc). Lista zawiera
wszystkie dane ze słownika.
Spójrzmy jeszcze raz na funkcje buildConnectionString. Przyjmuje ona liste
params.items(), odwzorowuje ja na nowa liste, korzystajac dla kazdego elementu z
formatowania łancucha znaków. Nowa lista ma tyle samo elementów co params.items(),
lecz kazdy element nowej listy jest łancuchem znaków, który zawiera zarówno klucz,
jak i skojarzona z nim wartosc ze słownika params.
Przykład 3.29 Wyrazenia listowe w buildConnectionString
>>> params = {"server":"mpilgrim", "database":"master", "uid":"sa", "pwd":"secret"}
>>> params.items()
[(’server’, ’mpilgrim’), (’uid’, ’sa’), (’database’, ’master’), (’pwd’, ’secret’)]
>>> [k for k, v in params.items()] #(1)
[’server’, ’uid’, ’database’, ’pwd’]
>>> [v for k, v in params.items()] #(2)
[’mpilgrim’, ’sa’, ’master’, ’secret’]
>>> ["%s=%s" % (k, v) for k, v in params.items()] #(3)
[’server=mpilgrim’, ’uid=sa’, ’database=master’, ’pwd=secret’]
1. Wykonujac iteracje po liscie params.items() uzywamy dwóch zmiennych. Zauwazmy,
ze w ten sposób korzystamy w petli z wielozmiennego przypisania.
Pierwszym elementem params.items() jest (’server’, ’mpilgrim’), dlatego
tez podczas pierwszej iteracji odwzorowywania listy zmienna k bedzie przechowywała
wartosc ’server’, a v wartosc ’mpilgrim’. W tym przypadku ignorujemy
wartosc v, dołaczajac tylko wartosc znajdujaca sie w k do zwracanej listy.
Otrzymamy taki sam wynik, gdy wywołamy params.keys().
2. Wtym miejscu wykonujemy to samo, ale zamiast zmiennej v ignorujemy zmienna
k. Otrzymamy taki sam wynik, jakbysmy wywołali params.values().
3. Dzieki temu, ze przerobilismy obydwa poprzednie przykłady i skorzystalismy z
formatowania łancucha znaków, otrzymalismy liste łancuchów znaków. Kazdy
łancuch znaków tej listy zawiera zarówno klucz, jak i wartosc pewnego elementu
ze słownika. Wynik wyglada podobnie do wyjscia pierwszego programu. Ponadto
porzadek został taki sam, jaki był w słowniku.
Materiały dodatkowe
• Python Tutorial omawia inny sposób odwzorowywania listy – za pomoca wbudowanej
funkcji map.
• Python Tutorial pokazuje kilka przykładów, jak mozna korzystac z wyrazen listowych.
50 ROZDZIAŁ 4. WBUDOWANE TYPY DANYCH
4.8 Łaczenie list i dzielenie łancuchów znaków
Łaczenie listy i dzielenie łancuchów znaków
Mamy liste, której elementy sa w formie klucz=wartosc. Załózmy, ze chcielibysmy
połaczyc je wszystkie w pojedynczy łancuch. Aby to zrobic, wykorzystamy metode
join obiektu typu string.
Ponizej został przedstawiony przykład łaczenia listy w łancuch znaków, który wykorzystalismy
w funkcji buildConnectionString:
return ";".join(["%s=%s" % (k, v) for k, v in params.items()])
Zanim przejdziemy dalej zastanówmy sie nad pewna kwestia. Funkcje sa obiektami,
łancuchy znaków sa obiektami... wszystko jest obiektem. Mozna by było dojsc do
wniosku, ze takze zmienna jest obiektem, ale to akurat nie jest prawda. Spójrzmy na
ten przykład i zauwazmy, ze łancuch znaków ‘‘;’’ sam w sobie jest obiektem i z niego
mozna wywołac metode join. Zmienne sa etykietami dla obiektów.
Metoda join łaczy elementy listy w jeden łancuch znaków, a kazdy element w
zwracanym łancuchu jest oddzielony od innego separatorem. W naszym przykładzie
jest nim ‘‘;’’, lecz moze nim byc dowolny łancuch znaków.
Metoda join działa tylko z listami przechowujacymi łancuchy znaków. Nie korzysta
ona z zadnych wymuszen czy konwersji. Łaczenie listy, która posiada co najmniej jeden
lub wiecej elementów niebedacych łancuchem znaków, rzuci wyjatek.
Przykład 3.30 Wyjscie odbchelper.py
>>> params = {"server":"mpilgrim", "database":"master", "uid":"sa", "pwd":"secret"}
>>> ["%s=%s" % (k, v) for k, v in params.items()]
[’pwd=secret’, ’database=master’, ’uid=sa’, ’server=mpilgrim’]
>>> ";".join(["%s=%s" % (k, v) for k, v in params.items()])
’pwd=secret;database=master;uid=sa;server=mpilgrim’
Powyzszy łancuch znaków otrzymalismy podczas uruchamiania odbchelper.py.
W Pythonie znajdziemy takze metode analogiczna do metody join, ale która zamiast
łaczyc, dzieli łancuch znaków na liste. Jest to funkcja split.
Przykład 3.31 Dzielenie łancuchów znaków
>>> li = [’pwd=secret’, ’database=master’, ’uid=sa’, ’server=mpilgrim’]
>>> s = ";".join(li)
>>> s
’pwd=secret;database=master;uid=sa;server=mpilgrim’
>>> s.split(";") #(1)
[’pwd=secret’, ’database=master’, ’uid=sa’, ’server=mpilgrim’]
>>> s.split(";", 1) #(2)
[’pwd=secret’, ’database=master;uid=sa;server=mpilgrim’]
1. split, przeciwienstwo funkcji join, dzieli łancuch znaków na wieloelementowa
liste. Separator (w przykładzie ‘‘;’’) nie bedzie wystepował w zadnym elemencie
zwracanej listy, zostanie pominiety.
4.8. ŁACZENIE LIST I DZIELENIE ŁANCUCHÓW ZNAKÓW 51
2. Do funkcji split mozemy dodac opcjonalny drugi argument, który okresla, na
jaka maksymalna liczbe kawałków ma zostac podzielony łancuch. (O opcjonalnych
argumentach w funkcji dowiemy sie w nastepnym rozdziale.)
tekst.split(separator, 1) jest przydatnym sposobem dzielenia łancucha na
dwa fragmenty. Pierwszy fragment dotyczy łancucha znajdujacego sie przed pierwszym
wystapieniem separatora (jest on pierwszym elementem zwracanej listy), a drugi
fragment zawiera dalszy fragment łancucha znajdujacego sie za pierwszym znalezionym
separatorem (jest drugim elementem listy). Dalsze separatory nie sa brane pod
uwage.
Materiały dodatkowe
• Python Knowledge Base odpowiada na czesto zadawane pytania dotyczace łancuchów
znaków, a takze posiada wiele przykładów wykorzystywania łancuchów
znaków.
• Python Library Reference wymienia wszystkie metody łancuchów znaków.
• The Whole Python FAQ wyjasnia, dlaczego join jest metoda łancucha znaków
zamiast listy.
52 ROZDZIAŁ 4. WBUDOWANE TYPY DANYCH
4.9 Kodowanie znaków
Kodowanie znaków
W komputerze pewnym znakom odpowiadaja pewne liczby, a kodowanie znaków
okresla, która liczba odpowiada jakiej literze. W łancuchu znaków kazdy symbol zajmuje
8 bitów, co daje nam do dyspozycji tylko 256 róznych symboli. Podstawowym
systemem kodowania jest ASCII. Przyporzadkowuje on liczbom z zakresu od 0 do 127
znaki alfabetu angielskiego, cyfry i niektóre inne symbole. Pozostałe standardowe systemy
kodowania rozszerzaja standard ASCII, dlatego znaki z przedziału od 0 do 127
w kazdym systemie kodowania sa takie same.
Przykład 3.32 Znaki jako liczby i na odwrót
>>> ord(’a’) #(1)
97
>>> chr(99) #(2)
’c’
>>> ord(’%’)
37 #(3)
>>> chr(115)
’s’
>>> chr(261)
Traceback (most recent call last): #(4)
File "<stdin>", line 1, in ?
ValueError: chr() arg not in range(261)
>>> chr(188)
’xbc’ #(5)
1. Funkcji ord zwraca liczbe, która odpowiada danemu symbolowi. W tym przypadku
literze “a” odpowiada liczba 97.
2. Za pomoca funkcji chr dowiadujemy sie, jaki znak odpowiada danej liczbie. Liczbie
99 odpowiada znak “c”.
3. Procent (“%”) odpowiada liczbie 37.
4. Kazdy symbol odpowiada liczbie z zakresu od 0 do 255. Liczba 261 nie miesci
sie w jednym bajcie, dlatego wyskoczył nam wyjatek.
5. Co prawda liczba 188 miesci sie w 8-bitach, ale nie miesci sie w standardzie
ASCII i dlatego tego symbolu Python nie moze jednoznacznie zinterpretowac. W
systemie kodowania ISO 8859-2 liczba ta odpowiada znakowi “z”, ale w systemie
kodowania Windows-1250 (znany tez jako CP-1250) znakowi “ˇL”.
Kazdy edytor tekstu zapisuje tworzone przez nas programy korzystajac z jakiegos
kodowania, chocby z samego ASCII. Dobrze jest korzystac z edytora, który daje nam
mozliwosc ustawienia kodowania znaków. Kiedy wiemy, w jakim systemie kodowania
został zapisany nasz skrypt, powinnismy jeszcze o tym poinformowac Pythona.
4.9. KODOWANIE ZNAKÓW 53
Informowanie Pythona o kodowaniu znaków
Wrócmy do odbchelper.py. Na samym poczatku dodalismy linie 2:
#-*- coding: utf-8 -*-
W ten sposób ustawiamy kodowanie znaków danego pliku, a nie całego programu
(program moze sie składac z wielu plików). Zreszta, jesli nie zdefiniujemy kodowania
znaków, Python nas o tym uprzedzi:
sys:1: DeprecationWarning: Non-ASCII character ’textbackslash{}xc5’ in file test.py on line 5
but no encoding declared; see http://www.python.org/peps/pep-0263.html for detils
Jesli skorzystalismy z innego kodowania znaków, zamiast utf-8 oczywiscie napiszemy
cos innego. Dodajac polskie znaki z reguły korzysta sie z kodowania UTF-8 lub
ISO-8859-2, a czasami w przypadku Windowsa z Windows-1250.
Ale co wtedy, gdy nie mamy mozliwosci ustawic kodowania znaków i nie wiemy z
jakiego korzysta nasz edytor? Mozna to sprawdzic metoda prób i błedów:
#-*- coding: {tu wstawiamy utf-8, iso-8859-2 lub windows-1250} -*-
print "zazółc gesla jazn"
A moze pora zmienic edytor?
Unikod jeszcze raz
Jak wiemy, unikod jest systemem reprezentowania znaków ze wszystkich róznych
jezyków swiata.
Zaraz powrócimy do Pythona.
Notatka historyczna. Przed powstaniem unikodu istniały oddzielne systemy kodowania
znaków dla kazdego jezyka, a co przed chwila troche omówilismy. Kazdy z nich
korzystał z tych samych liczb (0-255) do reprezentowania znaków danego jezyka. Niektóre
jezyki (jak rosyjski) miały wiele sprzecznych standardów reprezentowania tych
samych znaków. Inne jezyki (np. japonski) posiadaja tak wiele znaków, ze wymagaja
wielu bajtów, aby zapisac cały zbiór jego znaków. Wymiana dokumentów pomiedzy
tymi systemami była trudna, poniewaz komputer nie mógł stwierdzic, z którego systemu
kodowania skorzystał autor. Komputer widział tylko liczby, a liczby moga oznaczac
rózne rzeczy. Zaczeto sie zastanawiac nad przechowywaniem tych dokumentów
w tym samym miejscu (np. w tej samej tabeli bazy danych); trzeba było przechowywac
informacje o kodowaniu kazdego kawałku tekstu, a takze trzeba było za kazdym
razem informowac o kodowaniu przekazywanego tekstu. Wtedy tez zaczeto myslec o
wielojezycznych dokumentach, w których znaki z wielu jezyków znajduja sie w tym samym
dokumencie. (Wykorzystywały one zazwyczaj kod ucieczki, aby przełaczyc tryb
kodowania; ciach, jestes w rosyjskim trybie, wiec 241 znaczy to; ciach, jestes w greckim
trybie, wiec 241 znaczy cos innego itd.) Unikod został zaprojektowany po to, aby
rozwiazywac tego typu problemy.
Aby rozwiazac te problemy, unikod reprezentuje wszystkie znaki jako 2-bajtowe
liczby, od 0 do 65535 3 Kazda 2-bajtowa liczba reprezentuje unikalny znak, który jest
2W tym podreczniku bedziemy korzystac z kodowania UTF-8
3Niestety ciagle jest to nadmiernym uproszczeniem. Unikod został rozszerzony, aby obsługiwac
teksty w starozytnych odmianach chinskiego, koreanskiego, czy japonskiego, ale one maja tak duzo
znaków, ze 2-bajtowy system nie moze reprezentowac ich wszystkich.
54 ROZDZIAŁ 4. WBUDOWANE TYPY DANYCH
wykorzystywany w co najmniej jednym jezyku swiata. (Znaki które sa wykorzystywane
w wielu jezykach swiata, maja ten sam kod numeryczny.) Mamy dokładnie jedna liczbe
na znak i dokładnie jeden znak na liczbe. Dane unikodu nigdy nie sa dwuznaczne.
7-bitowy ASCII przechowuje wszystkie angielskie znaki za pomoca liczb z zakresu
od 0 do 127. (65 jest wielka litera “A”, 97 jest mała litera “a” itd.) Jezyk angielski
posiada bardzo prosty alfabet, wiec moze sie całkowicie zmiescic w 7-bitowym ASCII.
Jezyki zachodniej Europy jak jezyk francuski, hiszpanski, czy tez niemiecki, korzystaja
z systemu kodowania nazwanego ISO-8859-1 (inne okreslenie to “latin-1”), które korzysta
z 7-bitowych znaków ASCII dla liczb od 0 do 127, ale rozszerza zakres 128-255
dla znaków typu “˜n” (241), czy “¨u” (252). Takze unikod wykorzystuje te same znaki
jak 7-bitowy ASCII dla zakresu od 0 do 127, a takze te same znaki jak ISO-8859-1 w
zakresie od 128 do 255, a potem w zakresie od 256 do 65535 rozszerza znaki do innych
jezyków.
Kiedy korzystamy z danych w postaci unikodu, moze zajsc potrzeba przekonwertowania
danych na jakis inny system kodowania np. gdy potrzebujemy współdziałac z
innym komputerowym systemem, a który oczekuje danych w okreslonym 1-bajtowym
systemie kodowania, czy tez wysłac dane na terminal, który nie obsługuje unikodu,
czy tez do drukarki.
I po tej notatce, powrócmy do Pythona.
Przykład 3.33 Unikod w Pythonie
>>> ord(u"a")
261 #(1)
>>> print unichr(378) #(2)
z
1. W unikodzie polski znak “a” jednoznacznie odpowiada cyfrze 261.
2. Za pomoca funkcji unichr, dowiadujemy sie jakiemu znakowi odpowiada dana
liczba. Liczbie 378 odpowiada polska litera “z”. Python automatycznie zakoduje
wypisywany napis unikodowy, aby został poprawnie wyswietlony na naszym systemie.
Dlaczego warto korzystac z unikodu? Jest kilka powodów:
• Unikod bardzo dobrze sobie radzi z róznymi miedzynarodowymi znakami.
• Reprezentacja unikodowa jest jednoznaczna; jednej liczbie odpowiada dokładnie
jeden znak.
• Nie musimy sie zamartwiac szczegółami technicznymi np. czy dwa łancuchy, które
ze soba łaczymy sa w takim samym systemie kodowania 4.
• Python potrafi własciwie zinterpretowac wszystkie znaki (np. co jest litera, co
jest białym znakiem, a co jest cyfra).
• Korzystajac z unikodu zapobiegamy wielu problemom.
Dlatego wszedzie, gdzie bedziemy korzystali z polski znaków, bedziemy korzystali
z unikodu.
4W szczególnosci moze sie to zrobic niewygodne, kiedy korzystamy tylko ze standardowych łancuchów
znaków, a współpracujace ze soba moduły korzystaja z róznych systemów kodowania np. jedne
z ISO 8859-2, a inne z UTF-8.
4.9. KODOWANIE ZNAKÓW 55
Materiały dodatkowe
• PEP 0263 wyjasnia, w jaki sposób skonfigurowac kodowanie kodu zródłowego.
56 ROZDZIAŁ 4. WBUDOWANE TYPY DANYCH
4.10 Praca z unikodem
Praca z unikodem
Unikodowymi napisami posługujemy sie w identyczny sposób jak normalnymi łancuchami
znaków.
Przykład 3.34 Posługiwanie sie unikodem
>>> errmsg = u’Nie mozna otworzyc pliku’ #(1)
>>> print errmsg #(2)
Nie mozna otworzyc pliku
>>> print errmsg + u’, brak dostepu.’ #(3)
Nie mozna otworzyc pliku, brak dostepu.
>>> errmsg.split() #(4)
[u’Nie’, u’mou017cna’, u’otworzyu0107’, u’pliku’]
>>> print u"Bład: %s"%errmsg
Bład: Nie mozna otworzyc pliku
1. Tworzymy unikodowy napis i przypisujemy go do zmiennej errmsg.
2. Wypisujac dowolny unikod, Python go zakoduje w taki sposób, aby był zgodny
z kodowaniem znaków wyjscia, a wiec dany napis zostanie zawsze wypisany z
polskimi znakami.
3. Z unikodem operujemy identycznie, jak z innymi łancuchami znaków. Mozemy
na przykład dwa unikody ze soba łaczyc.
4. Mozemy takze podzielic unikod na liste.
5. Ponadto, podobnie jak w przypadku standardowego łancucha znaków, mozemy
tez unikod formatowac.
Unikod a łancuchy znaków
Funkcjonalnosc unikodu mozemy łaczyc ze standardowymi łancuchami znaków, o
ile z operacji tych jasno wynika, co chcemy osiagnac.
Przykład 3.35 Unikod w połaczeniu z łancuchami znaków
>>> file = ’myfile.txt’
>>> errmsg + ’ ’ + file #(1)
u’Nie mou017cna otworzyu0107 pliku myfile.txt’
>>> "%s %s"%(errmsg, file) #(2)
u’Nie mou017cna otworzyu0107 pliku myfile.txt’
>>> errmsg + ’, brak dostepu.’ #(3)
Traceback (most recent call last):
File "<stdin>", line 1, in ?
UnicodeDecodeError: ’ascii’ codec can’t decode byte 0xc4 in position 11:
ordinal not in range(128)
>>> "Bład: %s"%errmsg #(4)
4.10. PRACA Z UNIKODEM 57
Traceback (most recent call last):
File "<stdin>", line 1, in ?
UnicodeDecodeError: ’ascii’ codec can’t decode byte 0xc4 in position 11:
ordinal not in range(128)
1. Unikod mozemy połaczyc z łancuchem znaków. Powstaje nowy napis unikodowy.
2. Mozemy formatowac łancuch znaków korzystajac z unikodowych wartosci. Tu
takze powstaje nowy unikod.
3. Python rzucił wyjatek; nie potrafi przekształcic napisu ’, brak dostepu’ na
napis unikodowy. Z unikodem mozemy łaczyc jedynie łancuchy znaków w systemie
ASCII (czyli zawierajace jedynie angielskie litery i kilka innych symboli np.
przecinki, kropki itp.). W tym przypadku Python nie wie, z jakiego kodowania
korzystamy.
4. Tutaj mamy analogiczna sytuacje. Python nie potrafi przekształcic napisu ’Bład:
%s’ na unikod i rzuca wyjatek.
Python zakłada, ze kodowaniem wszystkich łancuchów znaków jest ASCII 5, dlatego
jesli mieszamy tekst unikodowy z łancuchami znaków, powinnismy dopilnowac,
aby łancuchy znaków zawierały znaki nalezace do ASCII (czyli nie moga posiadac
polskich znaków).
encode i decode
A co wtedy, gdy chcemy przekształcic unikodowy napis na łancuch znaków? Łancuchy
znaków sa jakos zakodowane, wiec aby stworzyc łancuch znaków, musimy go
na cos zakodowac np. ISO 8859-2, czy tez UTF-8. W tym celu korzystamy z metody
encode unikodu.
Przykład 3.36 Metoda encode
>>> errmsg.encode(’iso-8859-2’) #(1)
’Nie moxbfna otworzyxe6 pliku’
>>> errmsg.encode(’utf-8’)
’Nie moxc5xbcna otworzyxc4x87 pliku’ #(2)
1. Za pomoca metody encode informujemy Pythona na jakie kodowanie znaków
chcemy zakodowac pewien unikod. W tym przypadku otrzymujemy łancuch znaków
zakodowany w systemie ISO 8859-2.
2. Tutaj otrzymujemy ten sam napis, ale zakodowany w systemie UTF-8.
Operacje odwrotna, czyli odkodowania, wykonujemy za pomoca funkcji decode.
Na przykład:
5Istnieje mozliwosc zmiany domyslnego kodowania łancuchów znaków na inny, ale nie powinno
sie tego robic. Zmiana domyslnego kodowania wprowadza pewne komplikacje i sprawia, ze programy
staja sie nieprzenosne, dlatego nie bedziemy tego opisywac w tej ksiazce.
58 ROZDZIAŁ 4. WBUDOWANE TYPY DANYCH
Przykład 3.37 Metoda decode
>>> msg = errmsg.encode(’utf-8’) #(1)
>>> msg.decode(’utf-8’) #(2)
u’Nie mou017cna otworzyu0107 pliku’
>>> print msg.decode(’utf-8’)
Nie mozna otworzyc pliku
1. W tym miejscu zakodowalismy napis errmsg na UTF-8.
2. Za pomoca metody decode odkodowalismy zakodowany łancuch znaków i zwrócony
został unikod.
Łancuch znaków przechowuje znaki w zakodowanej postaci np. w systemie UTF-8,
ISO 8859-1, czy ISO 8859-4; moze byc wieloznaczny. Natomiast unikod jest jednoznaczny,
wiec nie jest zakodowany w tego typu systemach kodowania. Poniewaz łancuch
znaków jest zakodowany, musimy odkodowac (za pomoca decode), aby otrzymac
niezakodowany unikod. Z kolei unikod, poniewaz jest niezakodowany, musimy zakodowac
(za pomoca encode), aby otrzymac zakodowany łancuch znaków.
4.11. PODSUMOWANIE 59
4.11 Wbudowane typy danych - podsumowanie
Podsumowanie
Teraz juz powinnismy wiedziec w jaki sposób działa program odbchelper.py i zrozumniec,
dlaczego otrzymalismy takie wyjscie.
def buildConnectionString(params):
u"""Tworzy łancuchów znaków na podstawie słownika parametrów.
Zwraca łancuch znaków.
"""
return ";".join(["%s=%s" % (k, v) for k, v in params.items()])
if __name__ == "__main__":
myParams = {"server":"mpilgrim",
"database":"master",
"uid":"sa",
"pwd":"secret"
}
print buildConnectionString(myParams)
Na wyjsciu z programu otrzymujemy:
pwd=secret;database=master;uid=sa;server=mpilgrim
Zanim przejdziemy do nastepnego rozdziału, upewnijmy sie czy potrafimy:
• uzywac IDE Pythona w trybie interaktywnym
• napisac program i uruchamiac go przy pomocy twojego IDE lub z linii polecen
• tworzyc łancuchy znaków i napisy unikodowe
• importowac moduły i wywoływac funkcje w nich zawarte
• deklarowac funkcje, uzywac napisów dokumentacyjnych (ang. docstring), zmiennych
lokalnych, a takze uzywac odpowiednich wciec
• definiowac słowniki, krotki i listy
• dostawac sie do atrybutów i metod dowolnego obiektu, właczajac w to łancuchy
znaków, listy, słowniki, funkcje i moduły
• łaczyc wartosci poprzez formatowanie łancucha znaków
• odwzorowywac liste na inna liste
• dzielic łancuch znaków na liste i łaczyc liste w łancuch znaków
• poinformowac Pythona, z jakiego kodowania znaków korzystamy
• wykorzystywac metody encode i decode, aby przekształcic unikod w łancuch
znaków.
60 ROZDZIAŁ 4. WBUDOWANE TYPY DANYCH
Rozdział 5
Potega introspekcji
61
62 ROZDZIAŁ 5. POTEGA INTROSPEKCJI
5.1 Potega introspekcji
W tym rozdziale dowiemy sie o jednej z mocnych stron Pythona – introspekcji.
Jak juz wiemy, wszystko w Pythonie jest obiektem, natomiast introspekcja jest kodem,
który postrzega funkcje i moduły znajdujace sie w pamieci jako obiekty, a takze pobiera
o nich informacje i operuje nimi.
Nurkujemy
Zacznijmy od kompletnego, działajacego programu. Przegladajac kod na pewno
rozumiesz juz niektóre jego fragmenty. Przy niektórych liniach znajduja sie liczby w
komentarzach; korzystamy tu z koncepcji, które wykorzystywalismy juz w rozdziale
drugim. Nie przejmuj sie, jezeli nie rozumiesz czesci tego programu. W rozdziale tym
wszystkiego sie jeszcze nauczysz.
Przykład 4.1 apihelper.py
def info(object, spacing=10, collapse=1): #(1) (2) (3)
u"""Wypisuje metody i ich notki dokumentacyjne.
Argumentem moze byc moduł, klasa, lista, słownik, czy tez łancuch znaków."""
methodList = [e for e in dir(object) if callable(getattr(object, e))]
processFunc = collapse and (lambda s: " ".join(s.split())) or (lambda s: s)
print "textbackslash{}n".join(["%s %s" %
(method.ljust(spacing),
processFunc(unicode(getattr(object, method).__doc__)))
for method in methodList])
if __name__ == "__main__": #(4) (5)
print info.__doc__
1. Ten moduł ma jedna funkcje info. Zgodnie ze swoja deklaracja wymaga ona
trzech argumentów: object, spacing oraz collapse. Dwa ostatnie parametry
sa opcjonalne, co za chwile zobaczymy.
2. Funkcja info posiada wieloliniowa notke dokumentacyjna, która opisuje jej zastosowanie.
Zauwaz, ze funkcja nie zwraca zadnej wartosci. Ta funkcja bedzie
wykorzystywana, aby wykonac pewna czynnosc, a nie zeby otrzymac pewna wartosc.
3. Kod wewnatrz funkcji jest wciety.
4. Sztuczka z if name pozwala wykonac programowi cos uzytecznego, kiedy
zostanie uruchomiony samodzielnie. Jesli powyzszy kod zaimportujemy jako moduł
do innego programu, kod pod ta instrukcja nie zostanie wykonany. W tym
wypadku program wypisuje po prostu notke dokumentacyjna funkcji info.
5. Instrukcja if wykorzystuje == (dwa znaki równosci), aby porównac dwie wartosci.
W instrukcji if nie musimy korzystac z nawiasów okragłych.
5.1. NURKUJEMY 63
Funkcja info została zaprojektowana tak, aby ułatwic sobie prace w IDE Pythona.
IDE bierze jakis obiekt, który posiada funkcje lub metody (jak na przykład moduł
zawierajacy funkcje lub liste, która posiada metody) i wyswietla funkcje i ich notki
dokumentacyjne.
Przykład 4.2 Proste zastosowanie apihelper.py
>>> from apihelper import info
>>> li = []
>>> info(li)
[...ciach...]
append L.append(object) -- append object to end
count L.count(value) -> integer -- return number of occurrences of value
extend L.extend(iterable) -- extend list by appending elements from the iterable
index L.index(value, [start, [stop]]) -> integer -- return first index of value
insert L.insert(index, object) -- insert object before index
pop L.pop([index]) -> item -- remove and return item at index (default last)
remove L.remove(value) -- remove first occurrence of value
reverse L.reverse() -- reverse *IN PLACE*
sort L.sort(cmp=None, key=None, reverse=False) -- stable sort *IN PLACE*;
cmp(x, y) -> -1, 0, 1
Domyslnie wynik jest formatowany tak, by był łatwy do odczytania. Notki dokumentacyjne
składajace sie z wielu linii zamieniane sa na jednoliniowe, ale te opcje
mozemy zmienic ustawiajac wartosc 0 dla argumentu collapse. Jezeli nazwa funkcji
jest dłuzsza niz 10 znaków, mozemy okreslic inna wartosc dla argumentu spacing, by
ułatwic sobie czytanie.
Przykład 4.3 Zaawansowane uzycie apihelper.py
>>> import odbchelper
>>> info(odbchelper)
buildConnectionString Tworzy łancuchów znaków na podstawie słownika parametrów.
Zwraca łancuch znaków.
>>> info(odbchelper, 30)
buildConnectionString Tworzy łancuchów znaków na podstawie słownika parametrów.
Zwraca łancuch znaków.
>>> info(odbchelper, 30, 0)
buildConnectionString Tworzy łancuchów znaków na podstawie słownika parametrów.
Zwraca łancuch znaków.
64 ROZDZIAŁ 5. POTEGA INTROSPEKCJI
5.2 Argumenty opcjonalne i nazwane
Argumenty opcjonalne i nazwane
W Pythonie argumenty funkcji moga posiadac wartosci domyslne. Jezeli funkcja
zostanie wywołana bez podania pewnego argumentu, argumentowi temu zostanie przypisana
jego domyslna wartosc. Co wiecej mozemy podawac argumenty w dowolnej
kolejnosci poprzez uzycie ich nazw.
Ponizej przykład funkcji info z dwoma argumentami opcjonalnymi:
def info(object, spacing=10, collapse=1):
spacing oraz collapse sa argumentami opcjonalnymi, poniewaz maja przypisane
wartosci domyslne. Argument object jest wymagany, poniewaz nie posiada wartosci
domyslnej. Jezeli info zostanie wywołana tylko z jednym argumentem, spacing
przyjmie wartosci 10, a collapse wartosc 1. Jezeli wywołamy te funkcje z dwoma
argumentami, jedynie collapse przyjmuje wartosc domyslna (czyli 1).
Załózmy, ze chcielibysmy okreslic wartosc dla collapse, ale dla argumentu spacing
chcielibysmy skorzystac z domyslnej wartosci. W wiekszosci jezyków programowania
jest to niewykonalne, poniewaz wymagaja one od nas wywołania funkcji z trzema argumentami.
Na szczescie w Pythonie mozemy okreslac argumenty w dowolnej kolejnosci
poprzez odwołanie sie do ich nazw.
Przykład 4.4 Rózne sposoby wywołania funkcji info
info(odbchelper) #(1)
info(odbchelper, 12) #(2)
info(odbchelper, collapse=0) #(3)
info(spacing=15, object=odbchelper) #(4)
1. Kiedy wywołamy te funkcje z jednym argumentem, spacing przyjmie wartosc
domyslna równa 10, a collapse wartosc 1.
2. Kiedy podamy dwa argumenty, collapse przyjmie wartosc domyslna, czyli 1.
3. Tutaj podajemy argument collapse odwołujac sie do jego nazwy i okreslamy
wartosc, która chcemy mu przypisac. spacing przyjmuje wartosc domyslna – 10.
4. Nawet wymagany argument (jak object, który nie posiada wartosci domyslnej)
moze zostac okreslony poprzez swoja nazwe i moze wystapic na jakimkolwiek
miejscu w wywołaniu funkcji.
Takie działanie moze sie wydawac troche niejasne, dopóki nie zdamy sobie sprawy,
ze lista argumentów jest po prostu słownikiem. Gdy wywołujemy dana funkcje w sposób
“normalny”, czyli bez podawania nazw argumentów, Python dopasowuje wartosci
do okreslonych argumentów w takiej kolejnosci w jakiej zostały zadeklarowane. Najczesciej
bedziemy wykorzystywali tylko “normalne” wywołania funkcji, ale zawsze mamy
mozliwosc bardziej elastycznego podejscia do okreslania kolejnosci argumentów.
Jedyna rzecza, która musimy zrobic by poprawnie wywołac funkcje jest okreslenie
w jakikolwiek sposób wartosci dla kazdego wymaganego argumentu. Sposób i kolejnosc
okreslania argumentów zalezy tylko od nas.
5.2. ARGUMENTY OPCJONALNE I NAZWANE 65
Materiały dodatkowe
• Python Tutorial omawia w jaki sposób domyslne wartosci sa okreslane, czyli co
sie stanie, gdy domyslny argument bedzie lista lub tez pewnym wyrazeniem.
66 ROZDZIAŁ 5. POTEGA INTROSPEKCJI
5.3 Dwa sposoby importowania modułów
Dwa sposoby importowania modułów
W Pythonie mamy dwa sposoby importowania modułów. Obydwa sa przydatne,
dlatego tez powinnismy umiec je wykorzystywac. Jednym ze sposobów jest uzycie polecenia
import module, który moglismy zobaczyc w podrozdziale “Wszystko jest obiektem”.
Istnieje inny sposób, który realizuje te sama czynnosc, ale posiada pewne róznice.
Ponizej został przedstawiony przykład wykorzystujacy instrukcje from module
import:
from apihelper import info
Jak widzimy, składnia tego wyrazenia jest bardzo podobna do import module, ale z
jedna wazna róznica: atrybuty i metody danego modułu sa importowane bezposrednio
do lokalnej przestrzeni nazw, a wiec beda dostepne bezposrednio i nie musimy okreslac,
z którego modułu korzystamy. Mozemy importowac okreslone pozycje albo skorzystac
z from module import *, aby zaimportowac wszystko.
from module import * w Pythonie przypomina use module w Perlu, a Pythonowe
import module przypomina Perlowskie require module.
from module import * w Pythonie jest analogia do import module.* w Javie, a
import module w Pythonie przypomina import module w Javie.
Przykład 4.5 Róznice miedzy import module a from module import
>>> import types
>>> types.FunctionType #(1)
<type ’function’>
>>> FunctionType #(2)
Traceback (most recent call last):
File "<stdin>", line 1, in ?
NameError: name ’FunctionType’ is not defined
>>> from types import FunctionType #(3)
>>> FunctionType #(4)
<type ’function’>
1. Moduł types nie posiada zadnych metod. Posiada on jedynie atrybuty okreslajace
wszystkie typy zdefiniowane przez Pythona. Zauwazmy, ze atrybut tego
modułu (w tym przypadku FunctionType) musi byc poprzedzony nazwa modułu
– types.
2. FunctionType nie został sam w sobie okreslony w przestrzeni nazw; istnieje on
tylko w kontekscie modułu types.
3. Za pomoca tego wyrazenia atrybut FunctionType z modułu types został zaimportowany
bezposrednio do lokalnej przestrzeni nazw.
5.3. DWA SPOSOBY IMPORTOWANIA MODUŁÓW 67
4. Teraz mozemy odwoływac sie bezposrednio do FunctionType, bez odwoływania
sie do types.
Kiedy powinnismy uzywac from module import?
• Kiedy czesto odwołujemy sie do atrybutów i metod, a nie chcemy wielokrotnie
wpisywac nazwy modułu, wtedy najlepiej wykorzystac from module import.
• Jesli potrzebujemy selektywnie zaimportowac tylko kilka atrybutów lub metod,
powinnismy skorzystac z from module import.
• Jesli moduł zawiera atrybuty lub metody, które posiadaja taka sama nazwe jaka
jest w naszym module, powinnismy wykorzystac import module, aby uniknac
konfliktu nazw.
W pozostałych przypadkach to kwestia stylu programowania, mozna spotkac kod
napisany obydwoma sposobami.
Uzywajmy from module import * oszczednie, poniewaz taki sposób importowania
utrudnia okreslenie, skad pochodzi dana funkcja lub atrybut, a to z kolei utrudnia
debugowanie.
Materiały dodatkowe
• eff-bot opowie nam wiecej na temat róznic miedzy import module a from module
import.
• Python Tutorial omawia zaawansowane techniki importu, właczajac w to from
module import *.
68 ROZDZIAŁ 5. POTEGA INTROSPEKCJI
5.4 type, str, dir i inne wbudowane funkcje
type, str, dir i inne wbudowane funkcje
Python posiada mały zbiór bardzo uzytecznych wbudowanych funkcji. Wszystkie
inne funkcje znajduja sie w róznych modułach. Była to swiadoma decyzja projektowa,
aby uniknac przeładowania rdzenia jezyka, jak to ma miejsce w przypadku innych
jezyków (jak np. Visual Basic czy Object Pascal).
Funkcja type
Funkcja type zwraca typ danych podanego obiektu. Wszystkie typy znajduja sie
w module types. Funkcja ta moze sie okazac przydatna podczas tworzenia funkcji
obsługujacych kilka typów danych.
Przykład 4.6 Wprowadzenie do type
>>> type(1) #(1)
<type ’int’>
>>> li = []
>>> type(li) #(2)
<type ’list’>
>>> import odbchelper
>>> type(odbchelper) #(3)
<type ’module’>
>>> import types #(4)
>>> type(odbchelper) == types.ModuleType
True
1. Argumentem type moze byc cokolwiek: stała, łancuch znaków, lista, słownik,
krotka, funkcja, klasa, moduł, wszystkie typy sa akceptowane.
2. Kiedy podamy funkcji type dowolna zmienna, zostanie zwrócony jej typ.
3. type takze działa na modułach.
4. Mozemy uzywac stałych z modułu types, aby porównywac typy obiektów. Wykorzystuje
to funkcja info, co wkrótce zobaczymy.
Funkcja str
Funkcja str przekształca dane w łancuch znaków. Kazdy typ danych moze zostac
przekształcony w łancuch znaków.
Przykład 4.7 Wprowadzenie do str
>>> str(1) #(1)
’1’
>>> horsemen = [’war’, ’pestilence’, ’famine’]
>>> horsemen
[’war’, ’pestilence’, ’famine’]
>>> horsemen.append(’Powerbuilder’)
5.4. TYPE, STR, DIR I INNE WBUDOWANE FUNKCJE 69
>>> str(horsemen) #(2)
"[’war’, ’pestilence’, ’famine’, ’Powerbuilder’]"
>>> str(odbchelper) #(3)
"<module ’odbchelper’ from ’c:docbookdippyodbchelper.py’>"
>>> str(None) #(4)
’None’
1. Mozna było sie spodziewac, ze str działa na tych prostych, podstawowych typach
takich jak np. liczby całkowite. Prawie kazdy jezyk programowania posiada
funkcje konwertujaca liczbe całkowita na łancuch znaków.
2. Jakkolwiek funkcja str działa na kazdym obiekcie dowolnego typu, w tym przypadku
jest to lista składajaca sie z kilku elementów.
3. Argumentem funkcji str moze byc takze moduł. Zauwazmy, ze łancuch reprezentujacy
moduł zawiera sciezke do miejsca, w którym sie ten moduł znajduje.
Na róznych komputerach moze byc ona inna.
4. Subtelnym, lecz waznym zachowaniem funkcji str jest to, ze argumentem moze
byc nawet wartosc None (Pythonowej wartosci pusta, czesto okreslanej w innych
jezykach przez null). Dla takiego argumentu funkcja zwraca napis ’None’.
Wkrótce wykorzystamy te mozliwosc.
Funkcja unicode
Funkcja unicode pełni ta sama funkcje, co str, ale zamiast łancucha znaków tworzy
unikod.
Przykład 4.8 Wprowadzenie do unicode
>>> unicode(1) #(1)
u’1’
>>> unicode(odbchelper) #(2)
u"<module ’odbchelper’ from ’c:docbookdippyodbchelper.py’>"
>>> print unicode(horsemen[0])
u’war’
>>> unicode(’jezdziectwo’) #(3)
Traceback (most recent call last):
File "<stdin>", line 1, in ?
UnicodeDecodeError: ’ascii’ codec can’t decode byte 0xc5 in position 2:
ordinal not in range(128)
>>> unicode(’jezdziectwo’, ’utf-8’) #(4)
u’jeu017adziectwo’
1. Podobnie, jak w przypadku str, do funkcji unicode mozemy przekazac dowolny
obiekt np. moze to byc liczba. Przekonwertowalismy liczbe na napis unikodowy.
2. Argumentem funkcji unicode moze byc takze moduł.
3. Poniewaz litera “z” nie nalezy do ASCII, wiec Python nie potrafi jej zinterpretowac.
Zostaje rzucony wyjatek.
70 ROZDZIAŁ 5. POTEGA INTROSPEKCJI
4. Do funkcji unicode mozemy przekazac drugi, opcjonalny argument encoding,
który okresla, w jakim systemie kodowania jest zakodowany łancuch znaków.
Komputer, na którym został uruchomiony ten przykład, korzystał z kodowania
UTF-8, wiec przekazany łancuch znaków takze bedzie w tym systemie kodowania.
Funkcja dir
Kluczowa funkcja wykorzystana w info jest funkcja dir. Funkcja ta zwraca liste
atrybutów i metod pewnego obiektu np. modułu, funkcji, łancuch znaków, listy,
słownika... niemal wszystkiego.
Przykład 4.9 Wprowadzenie do dir
>>> li = []
>>> dir(li) #(1)
[’__add__’, ’__class__’, ’__contains__’, ’__delattr__’, ’__delitem__’,
’__delslice__’, ’__doc__’, ’__eq__’, ’__ge__’, ’__getattribute__’,
’__getitem__’, ’__getslice__’, ’__gt__’, ’__hash__’, ’__iadd__’, ’__imul__’,
’__init__’, ’__iter__’, ’__le__’, ’__len__’, ’__lt__’, ’__mul__’, ’__ne__’,
’__new__’, ’__reduce__’, ’__reduce_ex__’, ’__repr__’, ’__reversed__’,
’__rmul__’, ’__setattr__’, ’__setitem__’, ’__setslice__’, ’__str__’,
’append’, ’count’, ’extend’, ’index’, ’insert’, ’pop’, ’remove’, ’reverse’, ’sort’]
>>> d = {}
>>> dir(d) #(2)
[[...,’clear’, ’copy’, ’fromkeys’, ’get’, ’has_key’, ’items’, ’iteritems’,
’iterkeys’, ’itervalues’, ’keys’, ’pop’, ’popitem’, ’setdefault’, ’update’,
’values’]
>>> import odbchelper
>>> dir(odbchelper) #(3)
[’__builtins__’, ’__doc__’, ’__file__’, ’__name__’, ’buildConnectionString’]
1. li jest lista, dlatego tez dir(li) zwróci nam liste wszystkich metod, które posiada
lista. Zwrócmy uwage na to, ze zwracana lista zawiera nazwy metod w
formie łancucha znaków, a nie metody same w sobie. Metody zaczynajace sie i
konczace dwoma dolnymi myslnikami sa metodami specjalnymi.
2. d jest słownikiem, dlatego dir(d) zwróci liste nazw metod słownika. Co najmniej
jedna z nich, metoda keys, powinna wygladac znajomo.
3. Dzieki temu funkcja ta staje sie interesujaca. odbchelper jest modułem, wiec za
pomoca dir(odbchelper) otrzymamy liste nazw atrybutów tego modułu właczajac
w to wbudowane atrybuty np. name , czy tez doc , a takze jakiekolwiek
inne np. zdefiniowane przez nas funkcje. W tym przypadku odbchelper posiada
tylko jedna, zdefiniowana przez nas metode – funkcje buildConnectionString
opisana w rozdziale “Pierwszy program”.
Funkcja callable
Funkcja callable zwraca True, jesli podany obiekt moze byc wywoływany, a False
w przeciwnym przypadku. Do wywoływalnych obiektów zaliczamy funkcje, metody
klas, a nawet same klasy. (Wiecej o klasach mozemy przeczytac w nastepnym rozdziale.)
5.4. TYPE, STR, DIR I INNE WBUDOWANE FUNKCJE 71
Przykład 4.10 Wprowadzenie do callable
>>> import string
>>> string.punctuation #(1)
’!"#$%&’()*+,-./:;<=>?@[]^_‘{|}~’
>>> string.join #(2)
<function join at 00C55A7C>
>>> callable(string.punctuation) #(3)
False
>>> callable(string.join) #(4)
True
>>> print string.join.__doc__ #(5)
join(list [,sep]) -> string
Return a string composed of the words in list, with
intervening occurrences of sep. The default separator is a
single space.
(joinfields and join are synonymous)
1. Nie zaleca sie, zeby wykorzystywac funkcje z modułu string (chociaz wciaz wiele
osób uzywa funkcji join), ale moduł ten zawiera wiele przydatnych stałych jak
np. string.punctuation, który zawiera wszystkie standardowe znaki przestankowe,
wiec z niego tutaj skorzystalismy.
2. Funkcja string.join łaczy liste w łancuch znaków.
3. string.punctuation nie jest wywoływalny, jest łancuchem znaków. (Typ string
posiada metody, które mozemy wywoływac, lecz sam w sobie nie jest wywoływalny.)
4. string.join mozna wywołac. Jest to funkcja przyjmujaca dwa argumenty.
5. Kazdy wywoływalny obiekt moze posiadac notke dokumentacyjna. Kiedy wykonamy
funkcje callable na kazdym atrybucie danego obiektu, bedziemy mogli
potencjalnie okreslic, którymi atrybutami chcemy sie bardziej zainteresowac (metody,
funkcje, klasy), a które chcemy pominac (stałe itp.).
Wbudowane funkcje
type, str, unicode, dir i wszystkie pozostałe wbudowane funkcje sa umieszczone w
specjalnym module o nazwie builtin (nazwa z dwoma dolnymi myslnikami przed i
po nazwie). Jesli to pomoze, mozemy załozyc, ze Python automatycznie wykonuje przy
starcie polecenie from builtin import *, które bezposrednio importuje wszystkie
wbudowane funkcje do uzywanej przez nas przestrzeni nazw. Zaleta tego, ze funkcje
te znajduja sie w module, jest to, ze mozemy dostac informacje o wszystkich wbudowanych
funkcjach i atrybutach poprzez moduł builtin . Wykorzystajmy funkcje info
podajac jako argument ten moduł i przejrzyjmy wyswietlony spis. Niektóre z wazniejszych
funkcji w module builtin zgłebimy pózniej. (Niektóre z wbudowanych klas
błedów np. AttributeError, powinny wygladac znajomo.).
72 ROZDZIAŁ 5. POTEGA INTROSPEKCJI
Przykład 4.11 Wbudowane atrybuty i funkcje
>>> from apihelper import info
>>> import __builtin__
>>> info(__builtin__, 20)
ArithmeticError Base class for arithmetic errors.
AssertionError Assertion failed.
AttributeError Attribute not found.
EOFError Read beyond end of file.
EnvironmentError Base class for I/O related errors.
Exception Common base class for all exceptions.
FloatingPointError Floating point operation failed.
IOError I/O operation failed.
[...ciach...]
Wraz z Pythonem dostajemy doskonała dokumentacje, zawierajaca wszystkie potrzebne
informacje o modułach oferowanych przez ten jezyk. Porównujac do innych
jezyków, z Pythonem nie dostajemy podrecznika man, czy odwołan do innych zewnetrznych
podreczników, wszystko co potrzebujemy znajdujemy wewnatrz samego Pythona.
Materiały dodatkowe
• Python Library Reference dokumentuje wszystkie wbudowane funkcje i wszystkie
wbudowane wyjatki.
5.5. FUNKCJA GETATTR 73
5.5 Funkcja getattr
Funkcja getattr
Powinnismy juz wiedziec, ze w Pythonie funkcje sa obiektami. Ponadto mozemy
dostac referencje do funkcji bez znajomosci jej nazwy przed uruchomieniem programu.
W tym celu podczas działania programu nalezy wykorzystac funkcje getattr.
Przykład 4.12 Funkcja getattr
>>> li = ["Larry", "Curly"]
>>> li.pop #(1)
<built-in method pop of list object at 010DF884>
>>> getattr(li, "pop") #(2)
<built-in method pop of list object at 010DF884>
>>> getattr(li, "append")("Moe") #(3)
>>> li
["Larry", "Curly", "Moe"]
>>> getattr({}, "clear") #(4)
<built-in method clear of dictionary object at 00F113D4>
>>> getattr((), "pop") #(5)
Traceback (most recent call last):
File "<stdin>", line 1, in ?
AttributeError: ’tuple’ object has no attribute ’pop’
1. Dzieki temu dostalismy referencje do metody pop. Zauwazmy, ze w ten sposób
nie wywołujemy metody pop; aby ja wywołac musielibysmy wpisac polecenie
li.pop(). Otrzymujemy referencje do tej metody. (Adres szesnastkowy wyglada
inaczej na róznych komputerach, dlatego wyjscia beda sie nieco róznic.)
2. Operacja ta takze zwróciła referencje do metody pop, lecz tym razem nazwa
metody jest okreslona poprzez łancuch znaków w argumencie funkcji getattr.
getattr jest bardzo przydatna, wbudowana funkcja, która zwraca pewien atrybut
dowolnego obiektu. Tutaj wykorzystujemy obiekt, który jest lista, a atrybutem
jest metoda pop.
3. Dzieki temu przykładowi mozemy zobaczyc, jaki duzy potencjał kryje sie w
funkcji getattr. W tym przypadku zwracana wartoscia getattr jest metoda
(referencja do metody). Metode te mozemy wykonac podobnie, jak bysmy bezposrednio
wywołali li.append(‘‘Moe’’). Tym razem nie wywołujemy funkcji
bezposrednio, lecz okreslamy nazwe funkcji za pomoca łancucha znaków.
4. getattr bez problemu pracuje na słownikach
5. Teoretycznie getattr powinien pracowac na krotkach, jednak krotki nie posiadaja
zadnej metody, dlatego getattr spowoduje wystapienie wyjatku zwiazanego
z brakiem atrybutu o podanej nazwie.
getattr na modułach
getattr działa nie tylko na wbudowanych typach danych. Argumentem tej funkcji
moze byc takze moduł.
74 ROZDZIAŁ 5. POTEGA INTROSPEKCJI
Przykład 4.13 Funkcja getattr w apihelper.py
>>> import odbchelper
>>> odbchelper.buildConnectionString #(1)
<function buildConnectionString at 00D18DD4>
>>> getattr(odbchelper, "buildConnectionString") #(2)
<function buildConnectionString at 00D18DD4>
>>> object = odbchelper
>>> method = "buildConnectionString"
>>> getattr(object, method) #(3)
<function buildConnectionString at 00D18DD4>
>>> type(getattr(object, method)) #(4)
<type ’function’>
>>> import types
>>> type(getattr(object, method)) == types.FunctionType
True
>>> callable(getattr(object, method)) #(5)
True
1. Polecenie to zwraca nam referencje do funkcji buildConnectionString z modułu
odbchelper, który przeanalizowalismy w Rozdziale 2.
2. Wykorzystujac getattr, mozemy dostac taka sama referencje, do tej samej funkcji.
Wogólnosci getattr(obiekt, ‘‘atrybut’’) jest odpowiednikiem obiekt.atrybut.
Jesli obiekt jest modułem, atrybutem moze byc cokolwiek zdefiniowane w tym
module np. funkcja, klasa czy zmienna globalna.
3. Te mozliwosc wykorzystalismy w funkcji info. Obiekt o nazwie object został
przekazany jako argument do funkcji getattr, ponadto przekazalismy nazwe
pewnej metody lub funkcji jako zmienna method.
4. W tym przypadku zmienna method przechowuje nazwe funkcji, co mozna sprawdzic
pobierajac typ zwracanej wartosci.
5. Poniewaz zmienna method jest funkcja, wiec mozna ja wywoływac. Zatem w
wyniku wywołania callable otrzymalismy wartosc True.
getattr jako funkcja posredniczaca
Funkcja getattr jest powszechnie uzywana jako funkcja posredniczaca (ang. dispatcher).
Na przykład mamy napisany program, który moze wypisywac dane w róznych
formatach (np. HTML i PS). Wówczas dla kazdego formatu wyjscia mozemy
zdefiniowac odpowiednia funkcje, a podczas wypisywania danych na wyjscie getattr
bedzie nam posredniczył miedzy tymi funkcjami. Jesli wydaje sie to troche zagmatwane,
zaraz zobaczymy przykład.
Wyobrazmy sobie program, który potrafi wyswietlac statystyki strony w formacie
HTML, XML i w czystym tekscie. Wybór własciwego formatu moze byc okreslony w
linii polecen lub przechowywany w pliku konfiguracyjnym. Moduł statsout definiuje
trzy funkcje – output html, output xml i output text, a wówczas program główny
moze zdefiniowac pojedyncza funkcje, która wypisuje dane na wyjscie:
5.5. FUNKCJA GETATTR 75
Przykład 4.14 Posredniczenie za pomoca getattr
import statsout
def output(data, format="text"): #(1)
output_function = getattr(statsout, "output_%s" % format) #(2)
return output_function(data) #(3)
1. Funkcja output wymaga jednego argumentu o nazwie data, który ma zawierac
dane do wypisania na wyjscie. Funkcja ta moze takze przyjac jeden opcjonalny
argument format, który okresla format wyjscia. Gdy argument format nie zostanie
okreslony, przyjmie on wartosc ‘‘text’’, a funkcja sie zakonczy wywołujac
funkcje output text, która wypisuje dane na wyjscie w postaci czystego tekstu.
2. Łancucha znaków ‘‘output ’’ połaczylismy z argumentem format, aby otrzymac
nazwe funkcji. Nastepnie pobralismy funkcje o tej nazwie z modułu statsout.
Dzieki temu w przyszłosci bedzie łatwiej rozszerzyc program nie zmieniajac funkcji
posredniczacej, aby obsługiwał wiecej wyjsciowych programów. W tym celu
wystarczy dodac odpowiednia funkcje do statsout np. output pdf i wywołujemy
funkcje output podajac argument format jako ‘‘pdf’’.
3. Teraz mozemy wywołac funkcje wyjsciowa w taki sam sposób jak inne funkcje.
Zmienna output function jest referencja do odpowiedniej funkcji w statsout.
Czy znalezlismy bład w poprzednim przykładzie? Jest to bardzo niestabilne rozwiazanie,
ponadto nie ma tu kontroli błedów. Co sie stanie gdy uzytkownik poda format,
który nie zdefiniowalismy w statsout? Funkcja getattr rzuci nam wyjatek zwiazany
z błednym argumentem, czyli podana nazwa funkcji, która nie istnieje w module
statsout.
Na szczescie do funkcji getattr mozemy podac trzeci, opcjonalny argument, czyli
domyslnie zwracana wartosc, gdy podany atrybut nie istnieje.
Przykład 4.15 Domyslnie zwracana wartosc w getattr
import statsout
def output(data, format="text"):
output_function = getattr(statsout, "output_%s" % format, statsout.output_text)
return output_function(data) #(1)
1. Ta funkcja juz na pewno bedzie działała poprawnie, poniewaz podalismy trzeci
argument w wywołaniu funkcji getattr. Trzeci argument jest domyslna wartoscia,
która zostanie zwrócona, gdy podany atrybut, czy metoda nie zostanie
znaleziona.
Jak moglismy zauwazyc, funkcja getattr jest niezwykle uzyteczna. Jest ona sercem
introspekcji. W nastepnych rozdziałach zobaczymy jeszcze wiecej przydatnych
przykładów.
76 ROZDZIAŁ 5. POTEGA INTROSPEKCJI
5.6 Filtrowanie listy
Filtrowanie listy
Jak juz wiemy, Python ma potezne mozliwosci odwzorowania list w inne listy poprzez
wyrazenia listowe (rozdział “Odwzorowywanie listy”). Wyrazenia listowe mozemy
tez łaczyc z mechanizmem filtrowania, dzieki któremu pewne elementy listy sa
odwzorowywane a pewne pomijane.
Ponizej przedstawiono składnie filtrowania listy:
[wyrazenie odwzorowujace for element in odwzorowywana lista if wyrazenie filtrujace]
Jest to wyrazenie listowe z pewnym rozszerzeniem. Poczatek wyrazenia jest identyczny,
ale koncowa czesc zaczynajaca sie od if, jest wyrazeniem filtrujacym. Wyrazenie
filtrujace moze byc dowolnym wyrazeniem, które moze zostac zinterpretowane
jako wyrazenie logiczne. Kazdy element dla którego wyrazenie to bedzie prawdziwe,
zostanie dołaczony do wyjsciowej listy. Wszystkie inne elementy dla których wyrazenie
filtrujace jest fałszywe, zostana pominiete i nie trafia do wyjsciowej listy.
Przykład 4.16 Filtrowanie listy
>>> li = ["a", "mpilgrim", "foo", "b", "c", "b", "d", "d"]
>>> [elem for elem in li if len(elem) > 1] #(1)
[’mpilgrim’, ’foo’]
>>> [elem for elem in li if elem != "b"] #(2)
[’a’, ’mpilgrim’, ’foo’, ’c’, ’d’, ’d’]
>>> [elem for elem in li if li.count(elem) == 1] #(3)
[’a’, ’mpilgrim’, ’foo’, ’c’]
1. W tym przykładzie wyrazenie odwzorowujace nie jest skomplikowane (zwraca po
prostu wartosc kazdego elementu), wiec skoncentrujmy sie na wyrazeniu filtrujacym.
Kiedy Python przechodzi przez kazdy element listy, sprawdza czy wyrazenie
filtrujace jest prawdziwe dla tego elementu. Jesli tak bedzie, to Python wykona
wyrazenie odwzorowujace na tym elemencie i wstawi odwzorowany element do
zwracanej listy. W tym przypadku odfiltrowujemy wszystkie łancuchy znaków,
które maja co najwyzej jeden znak, tak wiec otrzymujemy liste wszystkich dłuzszych
napisów.
2. Tutaj odfiltrowujemy elementy, które przechowuja wartosc ‘‘b’’. Zauwazmy,
ze to wyrazenie listowe odfiltrowuje wszystkie wystapienia ‘‘b’’, poniewaz za
kazdym razem, gdy dany element bedzie równy ‘‘b’’, wyrazenie filtrujace bedzie
fałszywe, a zatem wartosc ta nie zostanie wstawiona do zwracanej listy.
3. count jest metoda listy, która zwraca ilosc wystapien danej wartosci w liscie.
Mozna sie domyslac, ze ten filtr usunie duplikujace sie wartosci, przez co zostanie
zwrócona lista, która zawiera tylko jedna kopie kazdej wartosci z oryginalnej listy.
Jednak tak sie nie stanie. Wartosci, które pojawiaja sie dwukrotnie w oryginalnej
liscie (w tym wypadku ‘‘b’’ i ‘‘d’’) zostana całkowicie odrzucone. Istnieje
mozliwosc usuniecia duplikatów z listy, jednak filtrowanie listy nie daje nam
takiej mozliwosci.
5.6. FILTROWANIE LISTY 77
Wrócmy teraz do apihelper.py, do ponizszej linii:
methodList = [method for method in dir(object) if callable(getattr(object, method))]
To wyrazenie listowe wyglada skomplikowanie, a nawet jest skomplikowane, jednak
podstawowa struktura jest taka sama. Całe to wyrazenie zwraca liste, która zostaje
przypisana do zmiennej methodList. Pierwsza czesc to czesc odwzorowujaca liste.
Wyrazenie odwzorowujace zwraca wartosc danego elementu. dir(object) zwraca liste
atrybutów i metod obiektu object, czyli jest to po prostu lista, która odwzorowujemy.
Tak wiec nowa czescia jest tylko wyrazenie filtrujace znajdujace sie za instrukcja if.
To wyrazenie nie jest takie straszne, na jakie wyglada. Juz poznalismy funkcje
callable, getattr oraz in. Jak juz wiemy z poprzedniego podrozdziału, funkcja
getattr(object, method) zwraca obiekt funkcji (czyli referencje do tej funkcji), jesli
object jest modułem, a method jest nazwa funkcji w tym module.
Podsumowujac, wyrazenie bierze pewien obiekt (nazwany object). Nastepnie pobiera
liste nazw atrybutów tego obiektu, a takze metod i funkcji oraz kilka innych
rzeczy. Nastepnie odrzuca te rzeczy, które nas nie interesuja, czyli pobieramy nazwy
kazdego atrybutu/metody/funkcji, a nastepnie za pomoca getattr pobieramy referencje
do atrybutów o tych nazwach. Potem za pomoca funkcji callable sprawdzamy, czy
ten obiekt jest wywoływalny, a dzieki temu dowiadujemy sie, czy mamy do czynienia
z metoda lub jakas funkcja. Moga to byc na przykład funkcje wbudowane (np. metoda
listy o nazwie pop), czy tez funkcje zdefiniowane przez uzytkownika (np. funkcja
buildConnectionString z modułu odbchelper). Nie musimy natomiast martwic sie
o inne atrybuty jak np. wbudowany do kazdego modułu atrybut name (nie jest on
wywoływalny, czyli callable zwróci wartosc False).
Materiały dodatkowe
Python Tutorial omawia inny sposób filtrowania listy, za pomoca wbudowanej funkcji
filter.
78 ROZDZIAŁ 5. POTEGA INTROSPEKCJI
5.7 Operatory and i or
Operatory and i or
Operatory and i or odpowiadaja boolowskim operacjom logicznym, jednak nie
zwracaja one wartosci logicznych. Zamiast tego zwracaja któras z podanych wartosci.
Przykład 4.17 Poznajemy and
>>> ’a’ and ’b’ #(1)
’b’
>>> ’’ and ’b’ #(2)
’’
>>> ’a’ and ’b’ and ’c’ #(3)
’c’
1. Podczas uzywania and wartosci sa oceniane od lewej do prawej. 0, ’’, [], (),
{} i None sa fałszem w kontekscie logicznym, natomiast wszystko inne jest
prawda. Cóz, prawie wszystko. Domyslnie instancje klasy w kontekscie logicznym
sa prawda, ale mozesz zdefiniowac specjalne metody w swojej klasie, które
sprawia, ze bedzie ona fałszem w kontekscie logicznym. Wszystkiego o klasach i
specjalnych metodach nauczymy sie w rozdziale “Obiekty i klasy”. Jesli wszystkie
wartosci sa prawda w kontekscie logicznym, and zwraca ostatnia wartosc. W
tym przypadku and najpierw bierze ’a’, co jest prawda, a potem ’b’, co tez jest
prawda, wiec zwraca ostatnia wartosc, czyli ’b’.
2. Jesli jakas wartosc jest fałszywa w kontekscie logicznym, and zwraca pierwsza
fałszywa wartosc. W tym wypadku ’’ jest pierwsza fałszywa wartoscia.
3. Wszystkie wartosci sa prawda, tak wiec and zwraca ostatnia wartosc, ’c’.
Przykład 4.18 Poznajemy or
>>> ’a’ or ’b’ #(1)
’a’
>>> ’’ or ’b’ #(2)
’b’
>>> ’’ or [] or {} #(3)
{}
>>> def sidefx():
... print "in sidefx()"
... return 1
>>> ’a’ or sidefx() #(4)
’a’
1. Uzywajac or wartosci sa oceniane od lewej do prawej, podobnie jak w and. Jesli
jakas wartosc jest prawda, or zwraca ta wartosc natychmiast. W tym wypadku,
’a’ jest pierwsza wartoscia prawdziwa.
2. or wyznacza ’’ jako fałsz, ale potem ’b’, jako prawde i zwraca ’b’.
3. Jesli wszystkie wartosci sa fałszem, or zwraca ostatnia wartosc. or ocenia ’’
jako fałsz, potem [] jako fałsz, potem {} jako fałsz i zwraca {}.
5.7. OPERATORY AND I OR 79
4. Zauwazmy, ze or ocenia kolejne wartosci od lewej do prawej, dopóki nie znajdzie
takiej, która jest prawda w kontekscie logicznym, a pozostała reszte ignoruje.
Tutaj, funkcja sidefx nigdy nie jest wywołana, poniewaz juz ’a’ jest prawda i
’a’ zostanie zwrócone natychmiastowo.
Jesli jestes osoba programujaca w jezyku C, na pewno znajome jest ci wyrazenie
bool ? a : b, z którego otrzymamy a, jesli bool jest prawda, lub b w przeciwnym wypadku.
Dzieki sposobowi działania operatorów and i or w Pythonie, mozemy osiagnac
podobny efekt.
Sztuczka and-or
Przykład 4.19 Poznajemy sztuczke and-or
>>> a = "first"
>>> b = "second"
>>> 1 and a or b #(1)
’first’
>>> 0 and a or b #(2)
’second’
1. Ta składnia wyglada podobnie do wyrazenia bool ? a : b w C. Całe wyrazenie
jest oceniane od lewej do prawej, tak wiec najpierw okreslony zostanie and. Czyli
1 and ’first’ daje ’first’, potem ’first’ or ’second’ daje ’first’.
2. 0 and ’first’ daje 0, a potem 0 or ’second’ daje ’second’.
Jakkolwiek te wyrazenia Pythona sa po prostu logika boolowska, a nie specjalna
konstrukcja jezyka. Istnieje jedna bardzo wazna róznica pomiedzy Pythonowa sztuczka
and-or, a składnia bool ? a : b w C. Jesli wartosc a jest fałszem, wyrazenie to nie
bedzie działało tak, jakbysmy chcieli. Mozna sie na tym niezle przejechac, co zobaczymy
w ponizszym przykładzie.
Przykład 4.20 Kiedy zawodzi sztuczka and-or
>>> a = ""
>>> b = "second"
>>> 1 and a or b #(1)
’second’
1. Poniewaz a jest pustym napisem, który Python uwaza za fałsz w kontekscie
logicznym, wiec 1 and ’’ daje ’’, a nastepnie ’’ or ’second’ daje ’second’.
Ups! To nie to, czego oczekiwalismy.
Sztuczka and-or, czyli wyrazenie bool and a or b, nie bedzie działało w identyczny
sposób, jak wyrazenie w C bool ? a : b, jesli a bedzie fałszem w kontekscie
logicznym.
Prawdziwa sztuczka kryjaca sie za sztuczka and-or, jest upewnienie sie, czy wartosc
a nigdy nie jest fałszywa. Jednym ze sposobów na wykonanie tego to przekształcenie
a w [a] i b w [b], a potem pobranie pierwszego elementu ze zwróconej listy, którym
bedzie a lub b.
80 ROZDZIAŁ 5. POTEGA INTROSPEKCJI
Przykład 4.21 Bezpieczne uzycie sztuczki and-or
>>> a = ""
>>> b = "second"
>>> (1 and [a] or [b])[0] #(1)
’’
1. Jako ze [a] jest nie pusta lista, wiec nigdy nie bedzie fałszem (tylko pusta lista
jest fałszem). Nawet gdy a jest równe 0 lub ’’ lub inna wartoscia dajaca fałsz,
lista [a] bedzie prawda, poniewaz ma jeden element.
Jak dotad, ta sztuczka moze wydawac sie bardziej uciazliwa niz pomocna. Mozesz
przeciez osiagnac to samo zachowanie instrukcja if, wiec po co to całe zamieszanie.
Cóz, w wielu przypadkach, wybierasz pomiedzy dwoma stałymi wartosciami, wiec
mozesz uzyc prostszego zapisu i sie nie martwic, poniewaz wiesz, ze wartosc zawsze
bedzie prawda. I nawet kiedy potrzebujesz uzyc bardziej skomplikowanej, bezpiecznej
formy, istnieja powody, aby i tak czasami korzystac z tej sztuczki. Na przykład, sa
pewne przypadki w Pythonie gdzie instrukcje if nie sa dozwolone np. w wyrazeniach
lambda.
W Pythonie 2.5 wprowadzono odpowiednik wyrazenia bool ? a : b z jezyka C;
jest ono postaci: a if bool else b. Jesli wartosc bool bedzie prawdziwa, to z wyrazenia
zostanie zwrócona wartosc a, a jesli nie, to otrzymamy wartosc b.
Materiały dodatkowe
• Python Cookbook omawia alternatywy do triku and-or.
5.8. WYRAZENIA LAMBDA 81
5.8 Wyrazenia lambda
Wyrazenia lambda
Python za pomoca pewnych wyrazen pozwala nam zdefiniowac jednolinijkowe minifunkcje.
Te tzw. funkcje lambda sa zapozyczone z Lispa i moga byc uzyte wszedzie tam,
gdzie potrzebna jest funkcja.
Przykład 4.22 Tworzymy funkcje lambda
>>> def f(x):
... return x*2
...
>>> f(3)
6
>>> g = lambda x: x*2 #(1)
>>> g(3)
6
>>> (lambda x: x*2)(3) #(2)
6
1. W ten sposób tworzymy funkcje lambda, która daje ten sam efekt jak normalna
funkcja nad nia. Zwrócmy uwage na skrócona składnie: nie ma nawiasów wokół
listy argumentów, brakuje tez słowa kluczowego return (cała funkcja moze byc
tylko jednym wyrazeniem). Funkcja nie posiada równiez nazwy, ale moze byc
wywołana za pomoca zmiennej, do której zostanie przypisana.
2. Mozemy uzyc funkcji lambda bez przypisywania jej do zmiennej. Moze taki sposób
korzystania z wyrazen lambda nie jest zbyt przydatny, ale w ten sposób
mozemy zobaczyc, ze za pomoca tego wyrazenia tworzymy funkcje jednolinijkowa.
Podsumowujac, funkcja lambda jest funkcja, która pobiera dowolna liczbe argumentów
(właczajac argumenty opcjonalne) i zwraca wartosc, która otrzymujemy po
wykonaniu pojedynczego wyrazenia. Funkcje lambda nie moga zawierac polecen i nie
moga zawierac wiecej niz jednego wyrazenia. Nie próbujmy upchac zbyt duzo w funkcje
lambda; zamiast tego jesli potrzebujemy cos bardziej złozonego, zdefiniujmy normalna
funkcje.
Korzystanie z wyrazen lambda zalezy od stylu programowania. Uzywanie ich nigdy
nie jest wymagane; wszedzie gdzie moglibysmy z nich skorzystac, równie dobrze moglibysmy
zdefiniowac oddzielna, normalna funkcje i uzyc jej zamiast funkcji lambda.
Funkcje lambda mozemy uzywac np. w miejscach, w których potrzebujemy specyficznego,
nieuzywalnego powtórnie kodu. Dzieki temu nie zasmiecamy kodu wieloma
małymi jednoliniowymi funkcjami.
Funkcje lambda w prawdziwym swiecie
Ponizej przedstawiamy funkcje lambda wykorzystana w apihelper.py:
processFunc = collapse and (lambda s: " ".join(s.split())) or (lambda s: s)
82 ROZDZIAŁ 5. POTEGA INTROSPEKCJI
Zauwazmy, ze uzyta jest tu prosta forma sztuczki and-or, która jest bezpieczna,
poniewaz funkcja lambda jest zawsze prawda w kontekscie logicznym. (To nie znaczy,
ze funkcja lambda nie moze zwracac wartosci bedacej fałszem. Funkcja jest zawsze
prawda w kontekscie logicznym, ale jej zwracana wartosc moze byc czymkolwiek.)
Zauwazmy równiez, ze uzywamy funkcji split bez argumentów. Widzielismy juz
ja uzyta z jednym lub dwoma argumentami. Jesli nie podamy argumentów, wówczas
domyslnym separatorem tej funkcji sa białe znaki (czyli spacja, znak nowej linii, znak
tabulacji itp.).
Przykład 4.23 split bez argumentów
>>> s = "this isnattest" #(1)
>>> print s
this is
a test
>>> print s.split() #(2)
[’this’, ’is’, ’a’, ’test’]
>>> print " ".join(s.split()) #(3)
’this is a test’
1. Tutaj mamy wieloliniowy napis, zdefiniowany przy pomocy znaków sterujacych
zamiast uzycia trzykrotnych cudzysłowów. n jest znakiem nowej linii, a t znakiem
tabulacji.
2. split bez zadnych argumentów dzieli na białych znakach, a trzy spacje, znak
nowej linii i znak tabulacji sa białymi znakami.
3. Mozemy unormowac białe znaki poprzez podzielenie napisu za pomoca metody
split, a potem powtórne złaczenie metoda join, uzywajac pojedynczej spacji
jako separatora. To własnie robi funkcja info, aby zwinac wieloliniowe notki
dokumentacyjne w jedna linie.
Co wiec własciwie funkcja info robi z tymi funkcjami lambda, dzieleniami i sztuczkami
and-or?
processFunc = collapse and (lambda s: " ".join(s.split())) or (lambda s: s)
processFunc jest teraz referencja do funkcji, ale zalezna od zmiennej collapse.
Jesli collapse jest prawda, processFunc(string) bedzie zwijac białe znaki, a w
przeciwnym wypadku processFunc(string) zwróci swój argument niezmieniony.
Aby to zrobic w mniej zaawansowanym jezyku (np. w Visual Basicu), prawdopodobnie
stworzylibysmy funkcje, która pobiera napis oraz argument collapse i uzywa
instrukcji if, aby okreslic czy zawijac białe znaki czy tez nie, a potem zwracałaby
odpowiednia wartosc. Takie podejscie nie byłoby zbyt efektywne, poniewaz funkcja
musiałaby obsłuzyc kazdy mozliwy przypadek. Za kazdym jej wywołaniem, musiałaby
zdecydowac czy zawijac białe znaki zanim dałaby nam to, co chcemy. W Pythonie
logike wyboru mozemy wyprowadzic poza funkcje i zdefiniowac funkcje lambda, która
jest dostosowana do tego, aby dac nam dokładnie to (i tylko to), co chcemy. Takie
podejscie jest bardziej efektywne, bardziej eleganckie i mniej podatne na błedy typu
“Ups! Te argumenty miały byc w odwrotnej kolejnosci...”.
5.8. WYRAZENIA LAMBDA 83
Materiały dodatkowe
• Python Knowledge Base omawia, jak wykorzystywac lambda, aby wywoływac
funkcje w sposób niebezposredni.
• Python Tutorial pokazuje, jak dostac sie do zewnetrznych zmiennych z wnetrza
funkcji lambda.
• The Whole Python FAQ zawiera przykłady, pokazujace, jak mozna zagmatwac
kod funkcji lambda.
84 ROZDZIAŁ 5. POTEGA INTROSPEKCJI
5.9 Potega introspekcji - wszystko razem
Wszystko razem
Ostatnia linia kodu, jedyna której jeszcze nie rozpracowalismy, to ta, która odwala
cała robote. Teraz umieszczamy wszystkie puzzle w jednym miejscu i nadchodzi czas,
aby je ułozyc.
To jest najwazniejsza czesc apihelper.py:
print "n".join(["%s %s" %
(method.ljust(spacing),
processFunc(unicode(getattr(object, method).__doc__)))
for method in methodList])
Zauwazmy, ze jest to tylko jedne wyrazenie podzielone na wiele linii, ale które nie
uzywa znaku kontynuacji (znaku odwrotnego ukosnika, ). Pamietasz jak powiedzielismy,
ze pewne instrukcje moga byc rozdzielone na kilka linii bez uzywania odwrotnego
ukosnika? Wyrazenia listowe sa jednym z tego typu wyrazen, poniewaz całe wyrazenie
jest zawarte w nawiasach kwadratowych.
Zacznijmy od konca i posuwajmy sie w tył. Wyrazenie
for method in methodList
okazuje sie byc wyrazeniem listowym. Jak wiemy, methodList jest lista wszystkich
metod danego obiektu, które nas interesuja, wiec za pomoca tej petli przechodzimy te
liste wykorzystujac zmienna method.
Przykład 4.24 Dynamiczne pobieranie notki dokumentacyjnej
>>> import odbchelper
>>> object = odbchelper #(1)
>>> method = ’buildConnectionString’ #(2)
>>> getattr(object, method) #(3)
<function buildConnectionString at 010D6D74>
>>> print getattr(object, method).__doc__ #(4)
Tworzy łancuchów znaków na podstawie słownika parametrów.
Zwraca łancuch znaków.
1. W funkcji info, object jest obiektem do którego otrzymujemy pomoc, a ten
obiekt zostaje przekazany jako argument.
2. Podczas iterowania listy methodList, method jest nazwa aktualnej metody.
3. Uzywajac funkcji getattr otrzymujemy referencje do funkcji method z modułu
object.
4. Teraz wypisanie notki dokumentacyjnej bedzie bardzo proste.
Nastepnym elementem puzzli jest uzycie unicode na notce dokumentacyjnej. Jak
sobie przypominamy, unicode jest wbudowana funkcja, która przekształca dane na
unikod, ale notka dokumentacyjna jest zawsze łancuchem znaków, wiec po co ja jeszcze
konwertowac na unikod? Nie kazda funkcja posiada notke dokumentacyjna, a jesli ona
nie istnieje, atrybut doc ma wartosc None.
5.9. POTEGA INTROSPEKCJI - WSZYSTKO RAZEM 85
Przykład 4.25 Po co uzywac unicode na notkach dokumentacyjnych?
>>> def foo(): print 2
>>> foo()
2
>>> foo.__doc__ #(1)
>>> foo.__doc__ == None #(2)
True
>>> unicode(foo.__doc__) #(3)
u’None’
1. Mozemy łatwo zdefiniowac funkcje, która nie posiada notki dokumentacyjnej,
tak wiec jej atrybut doc ma wartosc None. Dezorientujace jest to, ze jesli
bezposrednio odwołamy sie do atrybutu doc , IDE w ogóle nic nie wypisze.
Jednak jesli sie troche zastanowimy nad tym, takie zachowanie IDE ma jednak
pewien sens 1
2. Mozemy sprawdzic, ze wartosc atrybutu doc aktualnie wynosi None przez
porównanie jej bezposrednio z ta wartoscia.
3. W tym przypadku funkcja unicode przyjmuje pusta wartosc, None i zwraca jej
unikodowa reprezentacje, czyli ’None’.
4. li.append. doc jest łancuchem znaków. Zauwazmy, ze wszystkie angielskie
notki dokumentacyjne Pythona korzystaja ze znaków ASCII, dlatego mozemy
spokojnie je przekonwertowac do unikodu za pomoca funkcji unicode.
W SQL musimy skorzystac z IS NULL zamiast z = NULL, aby porównac cos z pusta
wartoscia. W Pythonie mozemy uzyc albo == None albo is None, lecz is None jest
szybsze.
Teraz kiedy juz mamy pewnosc, ze otrzymamy unikod, mozemy przekazac otrzymany
unikodowy napis do processFunc, która juz zdefiniowalismy jako funkcje zwijajaca
lub niezwijajaca białe znaki (w zaleznosci od przekazanego argumentu). Czy juz
wiemy, dlaczego wykorzystalismy unicode? Do przekonwertowania wartosci None na
reprezentacje w postaci unikodowego łancucha znaków. processFunc przyjmuje argument
bedacy unikodem i wywołuje jego metode split. Nie zadziałałoby to, gdybysmy
przekazali samo None, poniewaz None nie posiada metody o nazwie split i rzucony
zostałby wyjatek. Moze sie zastanawiasz, dlaczego nie konwertujemy do str? Poniewaz
tworzone przez nas notki sa napisami unikodowymi, w których nie wszystkie znaki
naleza do ASCII, a zatem str rzuciłby wyjatek.
Idac wstecz, widzimy, ze ponownie uzywamy formatowania łancucha znaków, aby
połaczyc wartosci zwrócone przez processFunc i przez metode ljust. Jest to metoda
łancucha znaków (dodajmy, ze napis unikodowy takze jest łancuchem znaków, tylko
nieco o wiekszych mozliwosciach), której jeszcze nie poznalismy.
1Pamietamy, ze kazda funkcja zwraca pewna wartosc? Jesli funkcja nie wykorzystuje instrukcji
return, zostaje zwrócone None, a czeste wyswietlanie None po wykonaniu pewnych funkcji przez IDE
Pythona mogłoby byc troche uciazliwe.
86 ROZDZIAŁ 5. POTEGA INTROSPEKCJI
Przykład 4.26 Poznajemy ljust
>>> s = ’buildConnectionString’
>>> s.ljust(30) #(1)
’buildConnectionString ’
>>> s.ljust(20) #(2)
’buildConnectionString’
1. ljust wypełnia napis spacjami do zadanej długosci. Z tej mozliwosci korzysta
funkcja info, aby stworzyc dwie kolumny na wyjsciu i aby wszystkie notki dokumentacyjne
umiescic w drugiej kolumnie.
2. Jesli podana długosc jest mniejsza niz długosc napisu, ljust zwróci po prostu
napis niezmieniony. Metoda ta nigdy nie obcina łancucha znaków.
Juz prawie skonczylismy. Majac nazwe metody method uzupełniona spacjami poprzez
ljust i (prawdopodobnie zwinieta) notke dokumentacyjna otrzymana z wywołania
processFunc, łaczymy je i otrzymujemy pojedynczy napis, łancuch znaków. Poniewaz
odwzorowujemy liste methodList, dostajemy liste złozona z takich łancuchów
znaków. Uzywajac metody join z napisu ‘‘n’’, łaczymy te liste w jeden łancuch
znaków, gdzie kazdy elementem listy znajduje sie w oddzielnej linii i ostatecznie wypisujemy
rezultat.
Przykład 4.27 Wypisywanie listy
>>> li = [’a’, ’b’, ’c’]
>>> print "n".join(li) #(1)
a
b
c
1. Ta sztuczka moze byc pomocna do znajdowania błedów, gdy pracujemy na listach,
a w Pythonie zawsze pracujemy na listach.
I to juz był ostatni element puzzli. Teraz powinienes zrozumiec ten kod.
print "n".join(["%s %s" %
(method.ljust(spacing),
processFunc(unicode(getattr(object, method).__doc__)))
for method in methodList])
5.10. PODSUMOWANIE 87
5.10 Potega introspekcji - podsumowanie
Podsumowanie
Program apihelper.py i jego wyjscie powinno teraz nabrac sensu.
def info(object, spacing=10, collapse=1):
u"""Wypisuje metody i ich notki dokumentacyjne.
Argumentem moze byc moduł, klasa, lista, słownik, czy tez łancuch znaków."""
methodList = [e for e in dir(object) if callable(getattr(object, e))]
processFunc = collapse and (lambda s: " ".join(s.split())) or (lambda s: s)
print "n".join(["%s %s" %
(method.ljust(spacing),
processFunc(unicode(getattr(object, method).__doc__)))
for method in methodList])
if __name__ == "__main__":
print info.__doc__
A z tutaj mamy przykład wyjscia, które otrzymujemy z programu apihelper.py:
>>> from apihelper import info
>>> li = []
>>> info(li)
append L.append(object) -- append object to end
count L.count(value) -> integer -- return number of occurrences of value
extend L.extend(iterable) -- extend list by appending elements from the iterable
index L.index(value, [start, [stop]]) -> integer -- return first index of value
insert L.insert(index, object) -- insert object before index
pop L.pop([index]) -> item -- remove and return item at index (default last)
remove L.remove(value) -- remove first occurrence of value
reverse L.reverse() -- reverse *IN PLACE*
sort L.sort(cmp=None, key=None, reverse=False) -- stable sort *IN PLACE*;
cmp(x, y) -> -1, 0, 1 append
Zanim przejdziemy do nastepnego rozdziału, upewnijmy sie, ze nie mamy problemów
z wykonywaniem ponizszych czynnosci:
• Definiowanie i wywoływanie funkcji z opcjonalnymi i nazwanymi argumentami
• Importowanie modułów za pomoca import module i from module import
• Uzywanie str do przekształcenia jakiejkolwiek przypadkowej wartosci na reprezentacje
w postaci łancucha znaków, a takze uzywanie unicode do przekształcania
wartosci na unikod.
• Uzywanie getattr do dynamicznego otrzymywania referencji do funkcji i innych
atrybutów
• Tworzenie wyrazen listowych z filtrowaniem
• Rozpoznawanie sztuczki and-or i uzywanie jej w sposób bezpieczny
88 ROZDZIAŁ 5. POTEGA INTROSPEKCJI
• Definiowanie funkcji lambda
• Przypisywanie funkcji do zmiennych i wywoływanie funkcji przez zmienna. Trudno
jest to mocniej zaakcentowac, jednak umiejetnosc wykonywania tego jest niezbedne
do lepszego rozumienia Pythona. Zobaczymy bardziej złozone aplikacje
opierajace sie na tej koncepcji w dalszej czesci ksiazki.
Rozdział 6
Obiekty i klasy
89
90 ROZDZIAŁ 6. OBIEKTY I KLASY
6.1 Obiekty i klasy
Rozdział ten zaznajomi nas ze zorientowanym obiektowo programowaniem przy
uzyciu jezyka Python.
Nurkujemy
Ponizej znajduje sie kompletny program, który oczywiscie działa. Czytanie notki
dokumentacyjnej modułu, klasy, czy tez funkcji jest pomocne w zrozumieniu co dany
program własciwie robi i w jaki sposób działa. Jak zwykle, nie martwmy sie, ze nie
mozemy wszystkiego zrozumiec. W koncu zasada działania tego programu zostanie
dokładnie opisana w dalszej czesci tego rozdziału.
Przykład 5.1 fileinfo.py
#-*- coding: utf-8 -*-
u"""Framework do pobierania metadanych specyficznych dla danego typu pliku.
Mozna utworzyc instancje odpowiedniej klasy podajac jej nazwe pliku w konstruktorze.
Zwrócony obiekt zachowuje sie jak słownik posiadajacy pare klucz-wartosc
dla kazdego fragmentu metadanych.
import fileinfo
info = fileinfo.MP3FileInfo("/music/ap/mahadeva.mp3")
print "n".join(["%s=%s" % (k, v) for k, v in info.items()])
Lub uzyc funkcji listDirectory, aby pobrac informacje o wszystkich plikach w katalogu.
for info in fileinfo.listDirectory("/music/ap/", [".mp3"]):
...
Framework moze byc roszerzony poprzez dodanie klas dla poszczególnych typów plików, np.:
HTMLFileInfo, MPGFileInfo, DOCFileInfo. Kazda klasa jest całkowicie odpowiedzialna
za własciwe sparsowanie swojego pliku; zobacz przykład MP3FileInfo.
"""
import os
import sys
def stripnulls(data):
u"usuwa białe znaki i nulle"
return data.replace("�0", " ").strip()
class FileInfo(dict):
u"przechowuje metadane pliku"
def init (self, filename=None):
dict. init (self)
self["plik"] = filename
class MP3FileInfo(FileInfo):
6.1. NURKUJEMY 91
u"przechowuje znaczniki ID3v1.0 MP3"
tagDataMap = {u"tytuł" : ( 3, 33, stripnulls),
"artysta" : ( 33, 63, stripnulls),
"album" : ( 63, 93, stripnulls),
"rok" : ( 93, 97, stripnulls),
"komentarz" : ( 97, 126, stripnulls),
"gatunek" : (127, 128, ord)}
def parse(self, filename):
u"parsuje znaczniki ID3v1.0 z pliku MP3"
self.clear()
try:
fsock = open(filename, "rb", 0)
try:
fsock.seek(-128, 2)
tagdata = fsock.read(128)
finally:
fsock.close()
if tagdata[:3] == ’TAG’:
for tag, (start, end, parseFunc) in self.tagDataMap.items():
self[tag] = parseFunc(tagdata[start:end])
except IOError:
pass
def setitem (self, key, item):
if key == "plik" and item:
self. parse(item)
FileInfo. setitem (self, key, item)
def listDirectory(directory, fileExtList):
u"zwraca liste obiektów zawierajacych metadane dla plików o podanych rozszerzeniach"
fileList = [os.path.normcase(f) for f in os.listdir(directory)]
fileList = [os.path.join(directory, f) for f in fileList
if os.path.splitext(f)[1] in fileExtList]
def getFileInfoClass(filename, module=sys.modules[FileInfo. module ]):
u"zwraca klase metadanych pliku na podstawie podanego rozszerzenia"
subclass = "%sFileInfo" % os.path.splitext(filename)[1].upper()[1:]
return hasattr(module, subclass) and getattr(module, subclass) or FileInfo
return [getFileInfoClass(f)(f) for f in fileList]
if name == " main ":
for info in listDirectory("/music/ singles/", [".mp3"]): #(1)
print "n".join("%s=%s" % (k, v) for k, v in info.items())
print
1. Wynik wykonania tego programu zalezy od tego jakie pliki mamy na twardym
dysku. Aby otrzymac sensowne wyjscie, potrzebujemy zmienic sciezke, aby wskazywał
na katalog, w którym przechowujemy pliki MP3.
W wyniku wykonania tego programu mozemy otrzymac podobne wyjscie do ponizszego.
Jest niemal niemozliwe, abys otrzymał identyczne wyjscie.
92 ROZDZIAŁ 6. OBIEKTY I KLASY
album=
rok=1999
komentarz=http://mp3.com/ghostmachine
tytuł=A Time Long Forgotten (Concept
artysta=Ghost in the Machine
gatunek=31
plik=/music/_singles/a_time_long_forgotten_con.mp3
album=Rave Mix
rok=2000
komentarz=http://mp3.com/DJMARYJANE
tytuł=HELLRAISER****Trance from Hell
artysta=***DJ MARY-JANE***
gatunek=31
plik=/music/_singles/hellraiser.mp3
album=Rave Mix
rok=2000
komentarz=http://mp3.com/DJMARYJANE
tytuł=KAIRO****THE BEST GOA
artysta=***DJ MARY-JANE***
gatunek=31
plik=/music/_singles/kairo.mp3
album=Journeys
rok=2000
komentarz=http://mp3.com/MastersofBalan
tytuł=Long Way Home
artysta=Masters of Balance
gatunek=31
plik=/music/_singles/long_way_home1.mp3
album=
rok=2000
komentarz=http://mp3.com/cynicproject
tytuł=Sidewinder
artysta=The Cynic Project
gatunek=18
plik=/music/_singles/sidewinder.mp3
album=Digitosis@128k
rok=2000
komentarz=http://mp3.com/artists/95/vxp
tytuł=Spinning
artysta=VXpanded
gatunek=255
plik=/music/_singles/spinning.mp3
6.2. DEFINIOWANIE KLAS 93
6.2 Definiowanie klas
Definiowanie klas
Python jest całkowicie zorientowany obiektowo: mozemy definiowac własne klasy,
dziedziczyc z własnych lub wbudowanych klas, a takze tworzyc instancje zdefiniowanych
przez siebie klas.
Tworzenie klas w Pythonie jest proste. Podobnie jak z funkcjami, nie uzywamy
oddzielnego interfejsu definicji. Po prostu definiujemy klase i zaczynamy ja implementowac.
Klasa w Pythonie rozpoczyna sie słowem kluczowym class, po którym
nastepuje nazwa klasy, a nastepnie w nawiasach okragłych umieszczamy, z jakich klas
dziedziczymy.
Przykład 5.3 Prosta klasa
class Nicosc(object): #(1)
pass #(2) (3)
1. Nazwa tej klasy to Nicosc, a klasa ta dziedziczy z wbudowanej klasy object. Nazwy
klas sa zazwyczaj pisane przy uzyciu wielkich liter np. KazdeSlowoOdzieloneWielkaLitera,
ale to kwestia konwencji nazewnictwa; nie jest to wymagane.
2. Klasa ta nie definiuje zadnych metod i atrybutów, ale zeby kod był zgodny ze
składnia Pythona, musimy cos umiescic w definicji, tak wiec uzylismy pass. Jest
to zastrzezone przez Pythona słowo, które mówi interpreterowi “przejdz dalej,
nic tu nie ma”. Instrukcja ta nie wykonuje zadnej operacji i powinnismy stosowac
ja, gdy chcemy zostawic funkcje lub klase pusta.
3. Prawdopodobnie zauwazylismy juz, ze elementy w klasie sa wyszczególnione
za pomoca wciec, podobnie jak kod funkcji, instrukcji warunkowych, petli itp.
Pierwsza nie wcieta instrukcja nie bedzie nalezała juz do klasy.
Od Pythona 2.2 wprowadzono nowy styl klas (ang. new-style classes), którego bedziemy
sie uczyli. Aby pisac klasy w nowym stylu, musimy dziedziczyc je z którejs
wbudowanej klasy, najczesciej bedzie to object. W przeciwnym wypadku pisane przez
nas klasy beda w starym stylu i niektóre mozliwosci nie beda dostepne. Dzieki takiemu
zachowaniu Pythona, jest on kompatybilny wstecz.
Instrukcja pass w Pythonie jest analogiczna do pustego zbioru nawiasów klamrowych
({}) w Javie lub w jezyku C++.
W prawdziwym swiecie wiekszosc klas definiuje własne metody i atrybuty. Jednak,
jak mozna zobaczyc wyzej, definicja klasy, oprócz swojej nazwy z dodatkiem object
w nawiasach, nie musi nic zawierac. Programisci C++ moga zauwazyc, ze w Pythonie
klasy nie maja wyraznie sprecyzowanych konstruktorów i destruktorów. Pythonowe
klasy maja cos, co przypomina konstruktor – metode init .
94 ROZDZIAŁ 6. OBIEKTY I KLASY
Przykład 5.4 Definiowanie klasy FileInfo
class FileInfo(dict): #(1)
1. Jak juz wiemy, klasy, z których chcemy dziedziczyc wyszczególniamy w nawiasach
okragłych, które z kolei umieszczamy bezposrednio po nazwie naszej klasy. Tak
wiec klasa FileInfo dziedziczy z wbudowanej klasy dict, a ta klasa, to po prostu
klasa słownika.
W Pythonie klase, z której dziedziczymy, wyszczególniamy w nawiasach okragłych,
umieszczonych bezposrednio po nazwie naszej klasy. Python nie posiada specjalnego
słowa kluczowego jak np. extends w Javie.
Python obsługuje dziedziczenie wielokrotne. Wystarczy w nawiasach okragłych,
umiejscowionych zaraz po nazwie klasy, wstawic nazwy klas, z których chcemy dziedziczyc
i oddzielic je przecinkami np. class klasa(klasa1,klasa2).
Inicjalizowanie i implementowanie klasy
Ten przykład przedstawia inicjalizacje klasy FileInfo za pomoca metody init .
Przykład 5.5 Inicjalizator klasy FileInfo
class FileInfo(dict):
u"przechowuje metadane pliku" #(1)
def __init__(self, filename=None): #(2) (3) (4)
1. Klasy moga (a nawet powinny) posiadac takze notke dokumentacyjna, podobnie
jak moduły i funkcje.
2. Metoda init jest wywoływana bezposrednio po utworzeniu instancji klasy.
Moze kusic, aby nazwac ja konstruktorem klasy, co jednak nie jest prawda. Metoda
init wyglada podobnie do konstruktora (z reguły init jest pierwsza
metoda definiowana dla klasy), działa podobnie (jest pierwszym fragmentem
kodu wykonywanego w nowo utworzonej instancji klasy), a nawet podobnie
brzmi (słowo “init” sugeruje, ze jest to konstruktor). Niestety nie jest to prawda,
poniewaz obiekt jest juz utworzony przed wywołaniem metody init , a my
juz otrzymujemy poprawna referencje do swiezo utworzonego obiektu. Jednak
init w Pythonie, jest tym co najbardziej przypomina konstruktor, a ponadto
pełni prawie taka sama role.
3. Pierwszym argumentem kazdej metody znajdujacej sie w klasie, właczajac w
to init , jest zawsze referencja do biezacej instancji naszej klasy. Według
powszechnej konwencji, argument ten jest zawsze nazywany self. W metodzie
init self odwołuje sie do własnie utworzonego obiektu; w innych metodach
klasy, self odwołuje sie do instancji, z której wywołalismy dana metode. Mimo,
ze musimy wyraznie okreslic argument self podczas definiowania metody, ale nie
okreslamy go w czasie wywoływania metody; Python dodaje go automatycznie.
6.2. DEFINIOWANIE KLAS 95
4. Metoda init moze posiadac dowolna liczbe argumentów i podobnie jak w
funkcjach, argumenty moga byc zdefiniowane z domyslnymi wartosciami (w ten
sposób staja sie argumentami opcjonalnymi).Wtym przypadku argument filename
ma domyslna wartosc okreslona jako None, który jest Pythonowa pusta wartoscia.
Według konwencji, pierwszy argument metody nalezacej do pewnej klasy (referencja
do biezacej instancji klasy) jest nazywany self. Argument ten pełni ta sama role,
co zastrzezone słowo this w C++ czy Javie, ale self nie jest zastrzezonym słowem
w Pythonie, jest jedynie konwencja nazewnictwa. Niemniej lepiej pozostac przy tej
konwencji, poniewaz jest to wrecz bardzo silna umowa.
Przykład 5.6 Kodowanie klasy FileInfo class FileInfo(dict):
u"przechowuje metadane pliku"
def __init__(self, filename=None):
dict.__init__(self) #(1)
self["plik"] = filename #(2)
#(3)
1. Niektóre jezyki pseudo-zorientowane obiektowo jak Powerbuilder posiadaja koncepcje
“rozszerzania” konstruktorów i innych zdarzen, w których metoda nalezaca
do nadklasy jest wykonywana automatycznie przed metoda podklasy. Python
takiego czegos nie wykonuje; zawsze nalezy wyraznie wywołac odpowiednia
metode nalezaca do przodka klasy.
2. Klasa ta działa podobnie jak słownik (w koncu z niego dziedziczymy), co moglismy
zauwazyc po spojrzeniu na te linie. Przypisalismy argument filename jako
wartosc klucza ‘‘plik’’ w naszym obiekcie.
3. Zauwazmy, ze metoda init nigdy nie zwraca zadnej wartosci.
Kiedy uzywac self i init
Podczas definiowania metody pewnej klasy, musimy wyraznie wstawic self jako
pierwszy argument kazdej metody, właczajac w to init . Kiedy wywołujemy metode
z klasy nadrzednej, musimy dołaczyc argument self, ale jesli wywołujemy metode z
zewnatrz, nie okreslamy argumentu self, po prostu go pomijamy. Python automatycznie
wstawi odpowiednia referencje za nas. Na poczatku moze sie to wydawac troche
namieszane, jednak wynika to z pewnych róznic, o których jeszcze nie wiemy 1.
Metoda init jest opcjonalna, ale jesli ja definiujemy, musimy pamietac o wywołaniu
metody init , która nalezy do przodka klasy. W szczególnosci, jesli potomek
chce poszerzyc pewne zachowanie przodka, dana metoda podklasy musi w odpowiednim
miejscu bezposrednio wywoływac metode nalezaca do klasy nadrzednej, oczywiscie
z odpowiednimi argumentami.
1Wynika to z róznic pomiedzy metodami instancji klasy (ang. bound method), a metodami samej
klasy (ang. unbound method)
96 ROZDZIAŁ 6. OBIEKTY I KLASY
Materiały dodatkowe
• New-style classes opisuje klasy w nowym stylu
• Python Tutorial opisuje klasy, przestrzenie nazw i dziedziczenie
6.3. TWORZENIE INSTANCJI KLASY 97
6.3 Tworzenie instancji klasy
Tworzenie instancji klasy
Tworzenie instancji klas jest dosyc proste. W tym celu wywołujemy klase tak jakby
była funkcja, dodajac odpowiednie argumenty, które sa okreslone w metodzie init .
Zwracana wartoscia bedzie zawsze nowo utworzony obiekt.
Przykład 5.7 Tworzenie instacji klasy FileInfo
>>> import fileinfo
>>> f = fileinfo.FileInfo("/music/_singles/kairo.mp3") #(1)
>>> f.__class__ #(2)
<class ’fileinfo.FileInfo’>
>>> f.__doc__ #(3)
u’przechowuje metadane pliku’
>>> f #(4)
{’plik’: ’/music/_singles/kairo.mp3’}
1. Utworzylismy instancje klasy FileInfo (zdefiniowana w module fileinfo) i
przypisalismy własnie utworzony obiekt do zmiennej f. Skorzystalismy z parametru
‘‘/music/ singles/kairo.mp3’’, a który bedzie odpowiadał argumentowi
filename w metodzie init klasy FileInfo.
2. Kazda instancja pewnej klasy ma wbudowany atrybut class , który jest klasa
danego obiektu. Programisci Javy moga byc zaznajomieni z klasa Class, która
posiada metody takie jak getName, czy tez getSuperclass, aby pobrac metadane
o pewnym obiekcie. W Pythonie ten rodzaj metadadanych, jest dostepny
bezposrednio z obiektu wykorzystujac atrybuty takie jak class , name , czy
bases .
3. Mozemy pobrac notke dokumentacyjna w podobny sposób, jak to czynilismy w
przypadku funkcji czy modułu. Wszystkie instancje klasy współdziela te sama
notke dokumentacyjna.
4. Pamietamy, ze metoda init przypisuje argument filename do self[‘‘plik’’]?
To dobrze, w tym miejscu mamy rezultat tej operacji. Argumenty podawane podczas
tworzenia instancji pewnej klasy sa wysłane do metody init (wyłaczajac
pierwszy argument, self. Python zrobił to za nas).
W Pythonie, aby utworzyc instancje pewnej klasy, wywołujemy klase tak, jakby to
była zwykła funkcja zwracajaca pewna wartosc. Python nie posiada jakiegos kluczowego
słowa jakim jest np. operator new w Javie czy C++.
Odsmiecanie pamieci
Jesli tworzenie nowej instancji jest proste, to jej usuwanie jest jeszcze prostsze.
W ogólnosci nie musimy wyraznie zwalniac instancji klasy, poniewaz Python robi to
automatycznie, gdy wychodzi one poza swój zasieg. W Pythonie rzadko wystepuja
wycieki pamieci.
98 ROZDZIAŁ 6. OBIEKTY I KLASY
Przykład 5.8 Próba zaimplementowania wycieku pamieci
>>> def leakmem():
... f = fileinfo.FileInfo(’/music/_singles/kairo.mp3’) #(1)
...
>>> for i in range(100):
... leakmem() #(2)
1. Za kazdym razem, gdy funkcja leakmem jest wywoływana, zostaje utworzona instancja
klasy FileInfo, a ta zostaje przypisana do zmiennej f, która jest lokalna
zmienna wewnatrz funkcji. Funkcja ta konczy sie bez jakiegokolwiek wyraznego
zwolnienia pamieci zajmowanej przez zmienna f, a wiec spodziewalibysmy sie
wycieku pamieci, lecz tak nie bedzie. Kiedy funkcja sie konczy, lokalna zmienna
f wychodzi poza swój zasieg. W tym miejscu nie ma wiecej zadnych referencji
do nowej instancji FileInfo, poniewaz nigdzie nie przypisywalismy jej do czegos
innego niz f, tak wiec Python zniszczy instancje za nas.
2. Niezaleznie od tego, jak wiele razy wywołamy funkcje leakmem, nigdy nie nastapi
wyciek pamieci, poniewaz za kazdym razem kiedy to zrobimy, Python bedzie
niszczył nowo utworzony obiekt przed wyjsciem z funkcji leakmem.
Technicznym terminem tego sposobu odsmiecania pamieci jest “zliczanie odwołan”
(zobacz w Wikipedii). Python przechowuje liste referencji do kazdej utworzonej instancji.
W powyzszym przykładzie, mamy tylko jedna referencje do instancji FileInfo –
zmienna f. Kiedy funkcja sie konczy, zmienna f wychodzi poza zasieg, wiec licznik
odwołan zmniejsza sie do 0 i Python zniszczy te instancje automatycznie.
W poprzednich wersjach Pythona wystepowały sytuacje, gdy zliczanie odwołan zawodziło
i Python nie mógł wyczyscic po nas pamieci. Jesli tworzylismy dwie instancje,
które odwoływały sie do siebie nawzajem (np. instancja listy dwukierunkowej 2, w
których kazdy wezeł wskazuje na poprzedni i nastepny znajdujacy sie w liscie), zadna
instancja nie była niszczona automatycznie, poniewaz Python uwazał (poprawnie), ze
ciagle mamy referencje do kazdej instancji. Od Pythona 2.0 mamy dodatkowy sposób
odsmiecania pamieci nazywany po ang. mark-and-sweep (oznacz i zamiataj, zobacz w
Wikipedii), dzieki któremu Python w sprytny sposób wykrywa rózne wirtualne blokady
i poprawnie czysci cykliczne odwołania.
Podsumowujac, w jezyku tym mozna po prostu zapomniec o zarzadzaniu pamiecia
i pozostawic te sprawe Pythonowi.
Materiały dodatkowe
• Python Library Reference omawia wbudowane atrybuty podobne do class
• Python Library Reference dokumentuje moduł gc, który daje niskopoziomowa
kontrole nad odsmiecaniem pamieci
2ang. double linked list
6.4. KLASA OPAKOWUJACA USERDICT 99
6.4 Klasa opakowujaca UserDict
Klasa opakowujaca UserDict
Wrócimy na chwile do przeszłosci. Za czasów, kiedy nie mozna było dziedziczyc
wbudowanych typów danych np. słownika, powstawały tzw. klasy opakowujace, które
pełniły te same funkcje, co typy wbudowane, ale mozna je było dziedziczyc. Klasa
opakowujaca dla słownika była klasa UserDict, która nadal jest dostepna wraz z nowymi
wersjami Pythona. Przygladniecie sie implementacji tej klasy moze byc dla nas
cenna lekcja. Zatem zajrzyjmy do kodu zródłowego klasy UserDict, który znajduja sie
w module UserDict. Moduł ten z kolei jest przechowywany w katalogu lib instalacji
Pythona, a pełna nazwa pliku to UserDict.py (nazwa modułu z rozszerzeniem .py).
W IDE ActivePython na Windowsie mozemy szybko otworzyc dowolny moduł,
który znajduje sie w sciezce do bibliotek, gdy wybierzemy File->Locate...
(Ctrl-L).
Przykład 5.9 Definicja klasy UserDict
class UserDict: #(1)
def init (self, dict=None): #(2)
self.data = {} #(3)
if dict is not None: self.update(dict) #(4) (5)
1. Klasa UserDict nie dziedziczy nic z innych klas. Jednak nie patrzmy sie na to,
pamietajmy, zeby zawsze dziedziczyc z object (lub innego wbudowanego typu),
bo wtedy mamy dostep do dodatkowych mozliwosci, które daja nam klasy w
nowym stylu.
2. Jak pamietamy, metoda init jest wywoływana bezposrednio po utworzeniu
instancji klasy. Przy tworzeniu instancji klasy UserDict mozemy zdefiniowac
poczatkowe wartosci, poprzez przekazanie słownika w argumencie dict.
3. WPythonie mozemy tworzyc atrybuty danych (zwane polami w Javie i PowerBuilderze).
Atrybuty to kawałki danych przechowywane w konkretnej instancji klasy
(moglibysmy je nazwac atrybutami instancji). W tym przypadku kazda instancja
klasy UserDict bedzie posiadac atrybut data. Aby odwołac sie do tego pola
z kodu spoza klasy, dodajemy z przodu nazwe instancji np. instancja.data;
robimy to w identyczny sposób, jak odwołujemy sie do funkcji poprzez nazwe
modułu, w którym ta funkcja sie znajduje. Aby odwołac sie do atrybutu danych
z wnetrza klasy, uzywamy self. Zazwyczaj wszystkie atrybuty sa inicjalizowane
sensownymi wartosciami juz w metodzie init . Jednak nie jest to wymagane,
gdyz atrybuty, podobnie jak zmienne lokalne, sa tworzone, gdy po raz pierwszy
przypisze sie do nich jakas wartosc.
4. Metoda update powiela zachowanie metody słownika: kopiuje wszystkie klucze i
wartosci z jednego słownika do drugiego. Metoda ta nie czysci słownika docelowego
(tego, z którego wywołalismy metode), ale jesli były tam juz jakies klucze,
to zostana one nadpisane tymi, które sa w słowniku zródłowym; pozostałe klucze
nie zmienia sie. Myslmy o update jak o funkcji łaczenia, nie kopiowania.
100 ROZDZIAŁ 6. OBIEKTY I KLASY
5. Z tej składni nie korzystalismy jeszcze w tej ksiazce. Jest to instrukcja if, ale
zamiast wcietego bloku, który rozpoczynałby sie w nastepnej linii, korzystamy
tu z instrukcji, która znajduje sie w jednej linii, zaraz za dwukropkiem. Jest
to całkowicie poprawna, skrótowa składnia, której mozemy uzywac, jesli mamy
tylko jedna instrukcje w bloku (tak jak pojedyncza instrukcja bez klamer w
C++). Mozemy albo skorzystac z tej skrótowej składni, albo tworzyc wciete
bloki, jednak nie mozemy ich ze soba łaczyc w odniesieniu do tego samego bloku
kodu.
Java i Powerbuilder moga przeciazac funkcje majace rózne listy argumentów, na
przykład klasa moze miec kilka metod z taka sama nazwa, ale z rózna liczba argumentów
lub z argumentami róznych typów. Inne jezyki (na przykład PL/SQL) obsługuja
nawet przeciazanie funkcji, które róznia sie jedynie nazwa argumentu np. jedna klasa
moze miec kilka metod z ta sama nazwa, ta sama liczba argumentów o tych samych
typów, ale inaczej nazwanych. Python nie ma zadnej z tych mozliwosci, nie ma tu
w ogóle przeciazania funkcji. Metody sa jednoznacznie definiowane przez ich nazwy
i w danej klasie moze byc tylko jedna metoda o danej nazwie. Jesli wiec mamy w
jakiejs klasie potomnej metode init , to zawsze zasłoni ona metode init klasy
rodzicielskiej, nawet jesli klasa pochodna definiuje ja z innymi argumentami. Ta uwaga
stosuje sie do wszystkich metod.
Guido, pierwszy twórca Pythona, tak wyjasnia zasłanianie funkcji: “Klasy pochodne
moga zasłonic metody klas bazowych. Poniewaz metody nie maja zadnych
specjalnych przywilejów, kiedy wywołujemy inne metody tego samego obiektu, moze
okazac sie, ze metoda klasy bazowej wywołujaca inna metode zdefiniowana w tej samej
klasie bazowej wywołuje własciwie metode klasy pochodnej, która ja zasłania.
(Dla programistów C++: wszystkie metody w Pythonie zachowuja sie tak, jakby były
wirtualne.)” Jesli dla Ciebie nie ma to sensu, mozesz to zignorowac. Po prostu warto
było o tym wspomniec.
Zawsze przypisujmy wartosci poczatkowe wszystkim zmiennym obiektu w jego
metodzie init . Oszczedzi to godzin debugowania w poszukiwaniu wyjatków
AtributeError, które sa spowodowane odwołaniami do niezainicjalizowanych (czyli
nieistniejacych) atrybutów.
Przykład 5.10 Standardowe metody klasy UserDict
def clear(self): self.data.clear() #(1)
def copy(self): #(2)
if self. class is UserDict: #(3)
return UserDict(self.data)
import copy #(4)
return copy.copy(self)
def keys(self): return self.data.keys() #(5)
def items(self): return self.data.items()
def values(self): return self.data.values()
6.4. KLASA OPAKOWUJACA USERDICT 101
1. clear jest normalna metoda klasy; jest dostepna publicznie i moze byc wołana
przez kogokolwiek w dowolnej chwili. Zauwazmy, ze w clear, jak we wszystkich
metodach klas, pierwszym argumentem jest self. (Pamietajmy, ze nie dodajemy
self, gdy wywołujemy metode; Python robi to za nas.) Zwrócmy uwage na podstawowa
ceche tej klasy opakowujacej: przechowuje ona prawdziwy słownik w
atrybucie data i definiuje wszystkie metody wbudowanego słownika, a w kazdej
z tych metod zwraca wynik identyczny do odpowiedniej metody słownika.
(Gdybysmy zapomnieli, metoda słownika clear czysci cały słownik kasujac jego
wszystkie klucze i wartosci.)
2. Metoda słownika o nazwie copy zwraca nowy słownik, który jest dokładna kopia
oryginału (majacy takie same pary klucz-wartosc). Natomiast klasa UserDict
nie moze po prostu wywołac self.data.copy, poniewaz ta metoda zwraca wbydowany
słownik, a my chcemy zwrócic nowa instancje klasy tej samej klasy, jaka
ma self.
3. Uzywamy atrybutu class , zeby sprawdzic, czy self jest obiektem klasy
UserDict; jesli tak, to jestesmy w domu, bo wiemy, jak zrobic kopie UserDict:
tworzymy nowy obiekt UserDict i przekazujemy mu słownik wyciagniety z self.data,
a wtedy mozemy od razu zwrócic nowy obiekt UserDict nie wykonujac nawet
instrukcji import copy z nastepnej linii.
4. Jesli self. class nie jest UserDict-em, to self musi byc jakas podklasa
UserDict-a, a w takim przypadku zycie wymaga uzycia pewnych trików. UserDict
nie wie, jak utworzyc dokładna kopie jednego ze swoich potomków. W tym celu
mozemy np. znajac atrybuty zdefiniowane w podklasie, wykonac na nich petle,
podczas której kopiujemy kazdy z tych atrybutów. Na szczescie istnieje moduł,
który wykonuje dokładnie to samo, nazywa sie on copy. Nie bedziemy sie tutaj
wdawac w szczegóły (choc jest to wypasny moduł, jesli sie w niego troche
wgłebimy). Wystarczy wiedziec, ze copy potrafi kopiowac dowolne obiekty, a tu
widzimy, jak mozemy z niego skorzystac.
5. Pozostałe metody sa bezposrednimi przekierowaniami, które wywołuja wbudowane
metody z self.data.
Od Pythona 2.2 nie korzystamy z klasy UserDict, poniewaz od tej wersji mozemy
juz dziedziczyc z wbudowanych typów danych.
Materiały dodatkowe
• Python Library Reference dokumentuje moduł copy
102 ROZDZIAŁ 6. OBIEKTY I KLASY
6.5 Metody specjalne
Pobieranie i ustawianie elementów
Oprócz normalnych metod, jest tez kilka (moze kilkanascie) metod specjalnych,
które mozna definiowac w klasach Pythona. Nie wywołujemy ich bezposrednio z naszego
kodu (jak zwykłe metody). Wywołuje je za nas Python w okreslonych okolicznosciach
lub gdy uzyjemy okreslonej składni np. za pomoca metod specjalnych mozemy
nadpisac operacje dodawania, czy tez odejmowania.
Z normalnym słownikiem mozemy zrobic duzo wiecej, niz bezposrednio wywołac
jego metody. Same metody nie wystarcza. Mozemy na przykład pobierac i wstawiac
elementy dzieki wykorzystaniu odpowiedniej składni, bez jawnego wywoływania metod.
Mozemy tak robic dzieki metodom specjalnym. Python odpowiednie elementy
składni przekształca na odpowiednie wywołania funkcji specjalnych.
Przykład 5.12 Metoda getitem
>>> f = fileinfo.FileInfo("/music/_singles/kairo.mp3")
>>> f
{’plik’:’/music/_singles/kairo.mp3’}
>>> f.__getitem__("plik") #(1)
’/music/_singles/kairo.mp3’
>>> f["plik"] #(2)
’/music/_singles/kairo.mp3’
1. Metoda specjalna getitem wyglada dosc prosto. Ta metoda specjalna pozwala
słownikowi zwrócic pewna wartosc na podstawie podanego klucza. A jak
ta metode mozemy wywołac? Mozemy to zrobic bezposrednio, ale w praktyce nie
robimy w ten sposób, byłoby to niezbyt wygodne. Najlepiej pozwolic Pythonowi
wywołac te metode za nas.
2. Z takiej składni korzystamy, by dostac pewna wartosc ze słownika. W rzeczywistosci
Python automatycznie przekształca taka składnie na wywołanie metody
f. getitem (‘‘plik’’). Własnie dlatego getitem nazywamy metoda specjalna:
nie tylko mozemy ja wywołac, ale Python wywołuje te metode takze za
nas, kiedy skorzystamy z odpowiedniej składni.
Istnieje takze analogiczna do getitem metoda setitem , która zamiast pobierac
pewna wartosc, zmienia dana wartosc korzystajac z pewnego klucza.
Przykład 5.13 Metoda setitem
>>> f
{’plik’:’/music/_singles/kairo.mp3’}
>>> f.__setitem__("gatunek", 31) #(1)
>>> f
{’plik’:’/music/_singles/kairo.mp3’, ’gatunek’:31}
>>> f["gatunek"] = 32 #(2)
>>> f
{’plik’:’/music/_singles/kairo.mp3’, ’gatunek’:32}
6.5. METODY SPECJALNE 103
1. Analogicznie do getitem , mozemy za pomoca setitem zmienic wartosc
pewnego klucza znajdujacego sie w słowniku. Podobnie, jak w przypadku getitem
nie musimy jej wywoływac w sposób bezposredni. Python wywoła setitem ,
jesli tylko uzyjemy odpowiedniej składni.
2. W taki praktyczny sposób korzystamy ze słownika. Za pomoca tej linii kodu
Python wywołuje w sposób ukryty f. setitem (‘‘gatunek’’, 32).
setitem jest metoda specjalna, poniewaz Python wywołuje ja za nas, ale ciagle
jest metoda klasy. Kiedy definiujemy klasy, mozemy definiowac pewne metody, nawet
jesli nadklasa ma juz zdefiniowana te metoda. W ten sposób nadpisujemy (ang.
override) metody nadklas. Tyczy sie to takze metod specjalnych.
Koncepcja ta jest baza całego szkieletu, który analizujemy w tym rozdziale. Kazdy
typ plików moze posiadac własna klase obsługi, która wie, w jaki sposób pobrac metadane
z konkretnego typu plików. Natychmiast po poznaniu niektórych atrybutów
(jak nazwa pliku i połozenie), klasa obsługi bedzie wiedziała, jak pobrac dalsze metaatrybuty
automatycznie. Mozemy to zrobic poprzez nadpisanie metody setitem ,
w której sprawdzamy poszczególne klucze i jesli dany klucz zostanie znaleziony, wykonujemy
dodatkowe operacje.
Na przykład MP3FileInfo jest podklasa FileInfo. Kiedy w MP3FileInfo ustawiamy
klucz ‘‘plik’’, nie tylko ustawiamy wartosc samego klucza ‘‘plik’’ (jak to
robi słownik), lecz takze zagladamy do samego pliku, odczytujemy tagi MP3 i tworzymy
pełny zbiór kluczy. Ponizszy przykład pokazuje, w jaki sposób to działa.
Przykład 5.14 Nadpisywanie metody setitem w klasie MP3FileInfo
def setitem (self, key, item): #(1)
if key == "plik" and item: #(2)
self. parse(item) #(3)
FileInfo. setitem (self, key, item) #(4)
1. Zwrócmy uwage na kolejnosc i liczbe argumentów w setitem . Pierwszym argumentem
jest instancja danej klasy (argument self), z której ta metoda została
wywołana, nastepnym argumentem jest klucz (argument key), który chcemy
ustawic, a trzecim jest wartosc (argument item), która chcemy skojarzyc z danym
kluczem. Kolejnosc ta jest wazna, poniewaz Python bedzie wywoływał te
metoda w takiej kolejnosci i z taka liczba argumentów. (Nazwy argumentów nic
nie znacza, wazna jest ich ilosc i kolejnosc.)
2. W tym miejscu zawarte jest sedno całej klasy MP3FileInfo: jesli przypisujemy
pewna wartosc do klucza ‘‘plik’’, chcemy wykonac dodatkowo pewne operacje.
3. Dodatkowe operacje dla klucza ‘‘plik’’ zawarte sa w metodzie parse. Jest to
inna metoda klasy MP3FileInfo. Kiedy wywołujemy metode parse uzywamy
zmiennej self. Gdybysmy wywołali samo parse, odnieslibysmy sie do normalnej
funkcji, która jest zdefiniowana poza klasa, a tego nie chcemy wykonac. Kiedy
natomiast wywołamy self. parse bedziemy odnosic sie do metody znajdujacej
sie wewnatrz klasy. Nie jest to niczym nowym. W identyczny sposób odnosimy
sie do atrybutów obiektu.
4. Po wykonaniu tej dodatkowej operacji, chcemy wykonac metode nadklasy. Pamietajmy,
ze Python nigdy nie zrobi tego za nas; musimy zrobic to recznie.
104 ROZDZIAŁ 6. OBIEKTY I KLASY
Zwrócmy uwage na to, ze odwołujemy sie do bezposredniej nadklasy, czyli do
FileInfo, chociaz on nie posiada zadnej metody o nazwie setitem . Jednak
wszystko jest w porzadku, poniewaz Python bedzie wedrował po drzewie
przodków jeszcze wyzej dopóki nie znajdzie klasy, która posiada metode, która
wywołujemy. Tak wiec ta linia kodu znajdzie i wywoła metode setitem , która
jest zdefiniowana w samej wbudowanej klasie słownika, w klasie dict.
Kiedy odwołujemy sie do danych zawartych w atrybucie instancji, musimy okreslic
nazwe atrybutu np. self.attribute. Podczas wywoływania metody klasy, musimy
okreslic nazwe metody np. self.method.
Przykład 5.15 Ustawianie klucza ‘‘plik’’ w MP3FileInfo
>>> import fileinfo
>>> mp3file = fileinfo.MP3FileInfo() #(1)
>>> mp3file
{’plik’:None}
>>> mp3file["plik"] = "/music/_singles/kairo.mp3" #(2)
>>> mp3file
{’album’: ’Rave Mix’, ’rok’: ’2000’, ’komentarz’: ’http://mp3.com/DJMARYJANE’,
u’tytuu0142’: ’KAIRO****THE BEST GOA’, ’artysta’: ’***DJ MARY-JANE***’,
’gatunek’: 31, ’plik’: ’/music/_singles/kairo.mp3’}
>>> mp3file["plik"] = "/music/_singles/sidewinder.mp3" #(3)
>>> mp3file
{’album’: ’’, ’rok’: ’2000’, ’nazwa’: ’/music/_singles/sidewinder.mp3’,
’komentarz’: ’http://mp3.com/cynicproject’, u’tytuu0142’: ’Sidewinder’,
’artysta’: ’The Cynic Project’, ’gatunek’: 18}
1. Najpierw tworzymy instancje klasy MP3FileInfo bez podawania nazwy pliku.
(Mozemy tak zrobic, poniewaz argument filename metody init jest opcjonalny.)
Poniewaz MP3FileInfo nie posiada własnej metody init , Python
idzie wyzej po drzewie nadklas i znajduje metode init w klasie FileInfo.
Z kolei init w tej klasie recznie wykonuje metode init w klasie dict, a
potem ustawia klucz ‘‘plik’’ na wartosc w zmiennej filename, który wynosi
None, poniewaz pominelismy nazwe pliku. Ostatecznie mp3file poczatkowo jest
słownikiem (własciwie klasa potomna słownika) z jednym kluczem ‘‘plik’’,
którego wartosc wynosi None.
2. Teraz rozpoczyna sie prawdziwa zabawa. Ustawiajac klucz ‘‘plik’’ w mp3file
spowoduje wywołanie metody setitem klasy MP3FileInfo (a nie słownika,
czyli klasy dict). Z kolei metoda ta zauwaza, ze ustawiamy klucz ‘‘plik’’
z prawdziwa wartoscia (item jest prawda w kontekscie logicznym) i wywołuje
self. parse. Chociaz jeszcze nie analizowalismy działania metody parse,
mozemy na podstawie wyjscia zobaczyc, ze ustawia ona kilka innych kluczy jak
‘‘album’’, ‘‘artysta’’, ‘‘gatunek’’, u‘‘tytuł’’ (w unikodzie, bo korzystamy
z polskich znaków), ‘‘rok’’, czy tez ‘‘comment’’.
3. Kiedy zmienimy klucz ‘‘plik’’, proces ten zostanie wykonany ponownie. Python
wywoła setitem , który nastepnie wywoła self. parse, a ten ustawi
wszystkie inne klucze.
6.6. ZAAWANSOWANE METODY SPECJALNE 105
6.6 Zaawansowane metody specjalne
Zaawansowane metody specjalne
W Pythonie, oprócz getitem i setitem , sa jeszcze inne metody specjalne,
. Niektóre z nich pozwalaja dodac funkcjonalnosc, której sie nawet nie spodziewamy.
Ten przykład pokazuje inne metody specjalne znanej juz nam klasy UserDict.
Przykład 5.16 Inne metody specjalne w klasie UserDict
def repr (self): return repr(self.data) #(1)
def cmp (self, dict): #(2)
if isinstance(dict, UserDict):
return cmp(self.data, dict.data)
else:
return cmp(self.data, dict)
def len (self): return len(self.data) #(3)
def delitem (self, key): del self.data[key] #(4)
1. repr jest metoda specjalna, która zostanie wywołana, gdy uzyjemy repr(obiekt).
repr jest wbudowana funkcja Pythona, która zwraca reprezentacje danego obiektu
w postaci łancucha znaków. Działa dla dowolnych obiektów, nie tylko obiektów
klas. Juz wielokrotnie uzywalismy tej funkcji, nawet o tym nie wiedzac. Gdy
w oknie interaktywnym wpisujemy nazwe zmiennej i naciskamy ENTER, Python
uzywa repr do wyswietlenia wartosci zmiennej. Stwórzmy słownik d z jakimis
danymi i wywołajmy repr(d), zeby sie o tym przekonac.
2. Metoda cmp zostanie wywoływana, gdy porównujemy za pomoca == dwa dowolne
obiekty Pythona, nie tylko instancje klas. Aby porównac wbudowane typy
danych (i nie tylko), wykorzystywane sa pewne reguły np. słowniki sa sobie
równy, gdy maja dokładnie takie same pary klucz-wartosc; łancuchy znaków sa
sobie równe, gdy maja taka sama długosc i zawieraja taki sam ciag znaków. Dla
instancji klas mozemy zdefiniowac metode cmp i zaimplementowac sposób porównania
własnorecznie, a potem uzywac == do porównywania obiektów klasy.
Python wywoła cmp za nas.
3. Metoda len zostanie wywołana, gdy uzyjemy len(obiekt). len jest wbudowana
funkcja Pythona, która zwraca długosc obiektu. Ta metoda działa dla
dowolnego obiektu, który mozna uznac za obiekt posiadajacy jakas długosc. Długosc
łancucha znaków jest równa ilosci jego znaków, długosc słownika, to liczba
jego kluczy, a długosc listy lub krotki to liczba ich elementów. Dla obiektów klas
mozesz zdefiniowac metode len i samemu zaimplementowac obliczanie długosci,
a nastepnie mozemy uzywac len(obiekt). Python za nas wywoła metode
specjalna len .
4. Metoda delitem zostanie wywołana, gdy uzyjemy del obiekt[klucz]. Dzieki
tej funkcji mozemy usuwac pojedyncze elementy ze słownika. Kiedy uzyjemy del
dla pewnej instancji, Python wywoła metode delitem za nas.
106 ROZDZIAŁ 6. OBIEKTY I KLASY
W Javie sprawdzamy, czy dwie referencje do łancucha znaków zajmuja to samo
fizyczne miejsce w pamieci, uzywajac str1 == str2. Jest to zwane identycznoscia
obiektów i w Pythonie zapisywane jest jako str1 is str2. Aby porównac wartosci
łancuchów znaków w Javie, uzylibysmy str1.equals(str2). W Pythonie uzyskujemy
to samo, gdy napiszemy str1 == str2. Programisci Javy, którzy zostali nauczeni, ze
swiat jest lepsze, poniewaz == w Javie porównuje identycznosc, zamiast wartosci, moga
miec trudnosci z akceptacja braku tego “dobra” w Pythonie.
Metody specjalne sprawiaja, ze dowolna klasa moze przechowywac pary kluczwartosc
w ten sposób, jak to robi słownik.Wtym celu definiujemy metode setitem .
Kazda klasa moze działac jak sekwencja, dzieki metodzie getitem . Obiekty dowolnej
klasy, które posiadaja metode cmp , moga byc porównywane przy uzyciu ==. A
jesli dana klasa reprezentuje cos, co ma pewna długosc, nie musimy definiowac metody
getLength; po prostu definiujemy metode len i korzystamy z len(obiekt).
Inne obiektowo zorientowane jezyki programowania pozwalaja nam zdefiniowac
tylko fizyczny model obiektu (“Ten obiekt ma metode getLength”). Metody specjalne
Pythona pozwalaja zdefiniowac logiczny model obiektu (“ten obiekt ma długosc”).
Python posiada wiele innych metod specjalnych. Sa takie, które pozwalaja klasie
zachowywac sie jak liczby, umozliwiajac dodawanie, odejmowanie i inne operacje
arytmetyczne na obiektach. (Klasycznym przykładem jest klasa reprezentujaca liczby
zespolone z czescia rzeczywista i urojona, na których mozemy wykonywac wszystkie
działania). Metoda call pozwala klasie zachowywac sie jak funkcja, a dzieki czemu
mozemy bezposrednio wywoływac instancje pewnej klasy.
Materiały dodatkowe
• Python Reference Manual dokumentuje wszystkie specjalne metody klasy
6.7. ATRYBUTY KLAS 107
6.7 Atrybuty klas
Atrybuty klas
Wiemy juz, co to sa atrybuty, które sa czescia konkretnych obiektów. W Python
mozemy tworzyc tez atrybuty klas, czyli zmienne nalezace do samej klasy (a nie do
instancji tej klasy).
Przykład 5.17 Atrybuty klas
class MP3FileInfo(FileInfo):
u"przechowuje znaczniki ID3v1.0 MP3"
tagDataMap = {u"tytuł" : ( 3, 33, stripnulls),
"artysta" : ( 33, 63, stripnulls),
"album" : ( 63, 93, stripnulls),
"rok" : ( 93, 97, stripnulls),
"komentarz" : ( 97, 126, stripnulls),
"gatunek" : (127, 128, ord)}
>>> import fileinfo
>>> fileinfo.MP3FileInfo #(1)
<class ’fileinfo.MP3FileInfo’>
>>> fileinfo.MP3FileInfo.tagDataMap #(2)
{’album’: (63, 93, <function stripnulls at 0xb7c000d4>),
’rok’: (93, 97, <function stripnulls at 0xb7c000d4>),
’komentarz’: (97, 126, <function stripnulls at 0xb7c000d4>),
u’tytuu0142’: (3, 33, <function stripnulls at 0xb7c000d4>),
’artysta’: (33, 63, <function stripnulls at 0xb7c000d4>),
’gatunek’: (127, 128, <built-in function ord>)}
>>> m = fileinfo.MP3FileInfo() #(3)
>>> m.tagDataMap
{’album’: (63, 93, <function stripnulls at 0xb7c000d4>),
’rok’: (93, 97, <function stripnulls at 0xb7c000d4>),
’komentarz’: (97, 126, <function stripnulls at 0xb7c000d4>),
u’tytuu0142’: (3, 33, <function stripnulls at 0xb7c000d4>),
’artysta’: (33, 63, <function stripnulls at 0xb7c000d4>),
’gatunek’: (127, 128, <built-in function ord>)}
1. MP3FileInfo jest klasa, nie jest instancja klasy.
2. tagDataMap jest atrybutem klasy i jest dostepny juz przed stworzeniem jakiegokolwiek
obiektu danej klasy.
3. Atrybuty klas sa dostepne na dwa sposoby: poprzez bezposrednie odwołanie do
klasy lub poprzez jakakolwiek instancjetej klasy.
W Javie zarówno zmienne statyczne (w Pythonie zwane atrybutami klas), jak i
zmienne obiektów (w Pythonie zwane atrybuty lub atrybutami instancji) sa definiowane
bezposrednio za definicja klasy (jedne ze słowem kluczowym static, a inne bez
niego). W Pythonie tylko atrybuty klas sa definiowane bezposrednio po definicji klasy.
Atrybuty instancji definiujemy w metodzie init .
108 ROZDZIAŁ 6. OBIEKTY I KLASY
Atrybuty klas moga byc uzywane jako stałe tych klas (w takim celu korzystamy z
nich w klasie MP3FileInfo), ale tak naprawde nie sa stałe. Mozna je zmieniac.
W Pythonie nie istnieja stałe. Wszystko mozemy zmienic, jesli tylko odpowiednio
sie postaramy. To efekt jednej z podstawowych zasad Pythona: powinno sie zniechecac
do złego działania, ale nie zabraniac go. Jesli naprawde chcesz zmienic wartosc None,
mozesz to zrobic, ale nie przychodz z płaczem, kiedy twój kod stanie sie niemozliwy
do debugowania.
Przykład 5.18 Modyfikowanie atrybutów klas
>>> class counter(object):
... count = 0 #(1)
... def __init__(self):
... self.__class__.count += 1 #(2)
...
>>> counter
<class __main__.counter>
>>> counter.count #(3)
0
>>> c = counter()
>>> c.count #(4)
1
>>> counter.count
1
>>> d = counter() #(5)
>>> d.count
2
>>> c.count
2
>>> counter.count
2
1. count jest atrybutem klasy counter.
2. class jest wbudowanym atrybutem kazdego obiektu. Jest to referencja do
klasy, której obiektem jest self (w tym wypadku do klasy counter).
3. Poniewaz count jest atrybutem klasy, jest dostepny porzez bezposrednie odwołanie
do klasy, przed stworzeniem jakiegokolwiek obiektu.
4. Kiedy tworzymy instancje tej klasy, automatycznie zostanie wykonana metoda
init , która zwieksza atrybut tej klasy o nazwie count o 1. Operacja ta
wpływa na klase sama w sobie, a nie tylko na dopiero co stworzony obiekt.
5. Stworzenie drugiego obiektu ponownie zwiekszy atrybut count. Zauwazmy, ze
atrybuty klas sa wspólne dla klasy i wszystkich jej instancji.
6.8. FUNKCJE PRYWATNE 109
6.8 Funkcje prywatne
Funkcje prywatne
Jak wiekszosc jezyków programowania, Python posiada koncepcje elementów prywatnych:
• prywatne funkcje, które nie moga byc wywoływane spoza modułów w których sa
zdefiniowane
• prywatne metody klas, które nie moga byc spoza nich wywołane
• prywatne atrybuty do których nie ma dostepu spoza klasy
Inaczej niz w wiekszosci jezyków, to czy element jest prywatny, czy nie, zalezy tylko
od jego nazwy.
Jezeli nazwa funkcji, metody czy atrybutu zaczyna sie od dwóch podkreslen (ale
nie konczy sie nimi), to wtedy element ten jest prywatny, wszystko inne jest publiczne.
Python nie posiada koncepcji chronionych metod (tak jak na przykład w Javie, C++,
które sa dostepnych tylko w tej klasie oraz w klasach z niej dziedziczacych). Metody
moga byc tylko prywatne (dostepne tylko z wnetrza klasy), badz publiczne (dostepne
wszedzie).
W klasie MP3FileInfo istnieja tylko dwie metody: parse oraz setitem . Jak
juz zostało przedstawione, setitem jest metoda specjalna: zostanie wywołana, gdy
skorzystamy ze składni dostepu do słownika bezposrednio na instancji danej klasy.
Jednak ta metoda jest publiczna i mozna ja wywołac bezposrednio (nawet z zewnatrz
modułu fileinfo), jesli istnieje jakis dobry do tego powód. Jednak metoda parse
jest prywatna, poniewaz posiada dwa podkreslenia na poczatku nazwy.
W Pythone wszystkie specjalne metody (takie jak setitem ) oraz wbudowane
atrybuty (np. doc ) trzymaja sie standardowej konwencji nazewnictwa. Ich nazwy
zaczynaja sie oraz koncza dwoma podkresleniami. Nie nazywajmy własnych metod w
ten sposób, bo jesli tak bedziemy robic, mozemy wprawic w zakłopotanie nie tylko
siebie, ale i inne osoby czytajace nasz kod pózniej.
Przykład 5.19 Próba wywołania metody prywatnej
>>> import fileinfo
>>> m = fileinfo.MP3FileInfo()
>>> m.__parse("/music/_singles/kairo.mp3") #(1)
Traceback (most recent call last):
File "<stdin>", line 1, in ?
AttributeError: ’MP3FileInfo’ object has no attribute ’__parse’
1. Jesli spróbujemy wywołac prywatna metode, Python rzuci nieco mylacy wyjatek,
który informuje, ze metoda nie istnieje. Oczywiscie istnieje ona, lecz jest prywatna,
wiec nie jest mozliwe uzyskanie dostepu do niej spoza klasy. Scisle mówiac,
metody prywatne sa dostepne spoza klasy w której były zdefiniowane, jednak nie
w taki prosty sposób. Tak naprawde nic w Pythonie nie jest prywatne, nazwy
metod prywatnych sa kodowane oraz odkodowane “w locie”, aby wygladały na
niedostepne, gdy korzystamy z ich prawdziwych nazw. Mozemy jednak wywołac
110 ROZDZIAŁ 6. OBIEKTY I KLASY
metode parse klasy MP3FileInfo przy pomocy nazwy MP3FileInfo. parse.
Informacja ta jest dosyc interesujaca, jednak obiecajmy sobie nigdy nie korzystac
z niej w prawdziwym kodzie. Metody prywatne sa prywatne nie bez powodu,
lecz jak wiele rzeczy w Pythonie, ich prywatnosc jest kwestia konwencji, nie przymusu.
Materiały dodatkowe
• Python Tutorial omawia działanie prywatnych zmiennych od srodka
6.9. PODSUMOWANIE 111
6.9 Obiekty i klasy - podsumowanie
Podsumowanie
To wszystko, jesli chodzi o triki z obiektami. W rozdziale dwunastym zobaczymy, w
jaki sposób wykorzystywac metody specjalne w normalnej aplikacji w której bedziemy
zajmowali sie tworzeniem posredników (ang. proxy) dla zdalnych usług sieciowych z
uzyciem getattr.
W nastepnym rozdziale dalej bedziemy uzywac kodu programu fileinfo.py, aby poznac
takie pojecia jak wyjatki, operacje na plikach oraz petle for.
Zanim zanurkujemy w nastepnym rozdziale, upewnijmy sie, ze nie mamy problemów
z:
• definiowaniem i tworzeniem instancji klasy
• definiowaniem metody init oraz innych metod specjalnych, a takze wiem,
kiedy sa one wywoływane
• dziedziczeniem innych klas (w ten sposób tworzac podklase danej klasy)
• definiowaniem atrybutów instancji i atrybutów klas, a takze rozumiemy róznice
miedzy nimi
• definiowaniem prywatnych metod oraz atrybutów
112 ROZDZIAŁ 6. OBIEKTY I KLASY
Rozdział 7
Wyjatki i operacje na plikach
113
114 ROZDZIAŁ 7. WYJATKI I OPERACJE NA PLIKACH
7.1 Obsługa wyjatków
W tym rozdziale zajmiemy sie wyjatkami, obiektami pliku, petlami for oraz modułami
os i sys. Jesli uzywalismy wyjatków w innych jezykach programowania, mozemy
tylko szybko przyjrzec sie składni Pythona, która odpowiada za obsługe wyjatków, ale
powinnismy zwrócic uwage na czesc, która omawia w jaki sposób zarzadzac plikami.
Obsługa wyjatków
Jak wiele innych jezyków programowania, Python obsługuje wyjatki. Przy pomocy
bloków try...except przechwytujemy wyjatki, natomiast raise zas rzuca wyjatek.
Python uzywa słów kluczowych try i except do obsługi wyjatków, natomiast za
pomoca słowa raise wyrzuca wyjatki. Java i C++ uzywaja słów try i catch do
przechwytywania wyjatków, a słowa throw do ich generacji.
Wyjatki sa w Pythonie wszedzie. Praktycznie kazdy moduł w bibliotece standardowej
Pythona ich uzywa. Sam interpreter Pythona równiez rzuca wyjatki w róznych
sytuacjach. Juz wiele razy widzielismy je w tej ksiazce:
• Próba uzycia nieistniejacego klucza w słowniku rzuci wyjatek KeyError
• Wyszukiwanie w liscie nieistniejacej wartosci rzuci wyjatek ValueError
• Wywołanie nieistniejacej metody obiektu rzuci wyjatek AttributeError
• Uzycie nieistniejacej zmiennej rzuci wyjatek NameError
• Mieszanie niezgodnych typów danych spowoduje wyjatek TypeError
W kazdym z tych przypadków, gdy uzywalismy IDE Pythona i wystapił bład, to
został wypisany wyjatek (w zaleznosci od uzytego IDE na przykład na czerwono). Jest
to tak zwany nieobsłuzony wyjatek. Kiedy podczas wykonywania programu został rzucony
wyjatek, nie było w nim specjalnego kodu, który by go wykrył i zaznajomił sie z
nim, dlatego obsługa tego wyjatku zostaje zrzucona na domyslne zachowanie Pythona,
które z kolei wypisuje troche informacji na temat błedu i konczy prace programu. W
przypadku IDE nie jest to wielka przeszkoda, ale wyobrazmy sobie, co by sie stało,
gdyby podczas wykonywania własciwego programu nastapiłby taki bład, a co z kolei
spowodowałoby, ze program wyłaczyłby sie.
Jednak efektem wyjatku nie musi byc katastrofa programu. Kiedy wyjatki zostana
rzucone, moga zostac obsłuzone. Czasami przyczyna wystapienia wyjatku jest bład w
kodzie (na przykład próba uzycia zmiennej, która nie istnieje), jednak bardzo czesto
wyjatek mozemy przewidziec. Jesli otwieramy plik, moze on nie istniec. Jesli łaczysz
sie z baza danych, moze ona byc niedostepna lub mozemy nie miec odpowiednich
parametrów dostepu np. hasła. Jesli wiemy, ze jakas linia kodu moze wygenerowac
wyjatek, powinnismy próbowac ja obsłuzyc przy pomocy bloku try...except.
Przykład 6.1 Otwieranie nieistniejacego pliku
>>> fsock = open("/niemapliku", "r") #(1)
Traceback (most recent call last):
7.1. OBSŁUGA WYJATKÓW 115
File "<stdin>", line 1, in ?
IOError: [Errno 2] No such file or directory: ’/niemapliku’
>>> try:
... fsock = open("c:/niemapliku.txt") #(2)
... except IOError:
... print "Plik nie istnieje"
... print "Ta linia zawsze zostanie wypisana" #(3)
Plik nie istnieje
Ta linia zawsze zostanie wypisana
1. Uzywajac wbudowanej funkcji open, mozemy spróbowac otworzyc plik do odczytu
(wiecej o tej funkcji w nastepnym podrozdziale). Jednak ten plik nie istnieje
i dlatego zostanie rzucony wyjatek IOError. Poniewaz nie przechwytujemy tego
wyjatku Python po prostu wypisuje troche danych pomocnych przy znajdywaniu
błedu, a potem zakancza działanie programu.
2. Próbujemy otworzyc ten sam nieistniejacy plik, jednak tym razem robimy to
we wnetrzu bloku try...except. Gdy metoda open rzuca wyjatek IOError,
jestesmy na to przygotowany. Linia except IOError: przechwytuje ten wyjatek
i wykonuje blok kodu, który w tym wypadku wypisuje bardziej przyjazny opis
błedu.
3. Gdy wyjatek zostanie juz obsłuzony, program wykonuje sie dalej w sposób normalny,
od pierwszej linii po bloku try...except. Zauwazmy, ze ta linia zawsze
wypisze tekst “Ta linia zawsze zostanie wypisana”, niezaleznie, czy wyjatek wystapi,
czy tez nie. Jesli naprawde mielibysmy taki plik na dysku, to i tak ta linia
zostałaby wykonana.
Wyjatki moga wydawac sie nieprzyjazne (w koncu, jesli nie przechwycimy wyjatku,
program zostanie przerwany), jednak pomyslmy, z jakiej innej alternatywy moglibysmy
skorzystac. Czy chcielibysmy dostac bezuzyteczny obiekt, który przestawia nieistniejacy
plik? I tak musielibysmy sprawdzic jego poprawnosc w jakis sposób, a jesli bysmy
tego nie zrobili, to nasz program wykonałby jakies dziwne, nieprzewidywalne operacje,
których bysmy sie nie spodziewali. Nie byłaby to wcale dobra zabawa. Z wyjatkami
błedy wystepuja natychmiast i mozemy je obsługiwac w standardowy sposób u zródła
problemu.
Wykorzystanie wyjatków do innych celów
Jest wiele innych sposobów wykorzystania wyjatków, oprócz do obsługi błedów.
Dobrym przykładem jest importowanie modułów Pythona, sprawdzajac czy nastapił
wyjatek. Jesli moduł nie istnieje zostanie rzucony wyjatek ImportError. Dzieki temu
mozemy zdefiniowac wiele poziomów funkcjonalnosci, które zaleza od modułów dostepnych
w czasie wykonania, a dzieki temu mozemy wspierac róznorodne platformy
(kod zalezny od platformy jest podzielony na oddzielne moduły).
Mozemy tez zdefiniowac własne wyjatki, tworzac klase, która dziedziczy z wbudowanej
klasy Exception, a nastepnie mozemy rzucac wyjatki przy pomocy polecenia
raise. Mozemy zajrzec do czesci “materiały dodatkowe”, aby dowiedziec sie wiecej na
ten temat.
Nastepny przykład pokazuje, w jaki sposób wykorzystywac wyjatki, aby obsłuzyc
funkcjonalnosc zdefiniowana jedynie dla konkretnej platformy. Kod pochodzi z modułu
116 ROZDZIAŁ 7. WYJATKI I OPERACJE NA PLIKACH
getpass, który jest modułem opakowujacym, którym umozliwia pobranie hasła od
uzytkownika. Pobieranie hasła jest całkowicie rózne na platformach UNIX, Windows,
czy Mac OS, ale kod ten obsługuje wszystkie te róznice.
Przykład 6.2 Obsługa funkcjonalnosci zdefiniowanej dla konkretnej platformy
# Bind the name getpass to the appropriate function
try:
import termios, TERMIOS #(1)
except ImportError:
try:
import msvcrt #(2)
except ImportError:
try:
from EasyDialogs import AskPassword #(3)
except ImportError:
getpass = default_getpass #(4)
else: #(5)
getpass = AskPassword
else:
getpass = win_getpass
else:
getpass = unix_getpass
1. termios jest modułem okreslonym dla UNIX-a, który dostarcza niskopoziomowa
kontrole nad terminalem wejscia. Jesli moduł ten jest niedostepny (poniewaz, nie
ma tego na naszym systemie, poniewaz system tego nie obsługuje), importowanie
nawali, a Python rzuci wyjatek ImportError, który przechwycimy.
2. OK, nie mamy termios, wiec spróbujmy z msvcrt, który jest modułem charakterystycznym
dla systemu Windows, a dostarcza on API do wielu przydatnych
funkcji dla tego systemu. Jesli to takze nie zadziała, Python rzuci wyjatek
ImportError, który takze przechwycimy.
3. Jesli pierwsze dwa nie zadziałaja, próbujemy zaimportowac funkcje z EasyDialogs,
która jest w module okreslonym dla Mac OS-a, który dostarcza funkcje przeznaczone
dla wyskakujacych okien dialogowych róznego typu. I ponownie, jesli moduł
nie istnieje, Python rzuci wyjatek ImportError, który tez przechwytujemy.
4. Zaden z tych modułów, które sa przeznaczone dla konkretnej platformy, nie sa
dostepne (jest to mozliwe, poniewaz Python został przeportowany na wiele róznych
platform), wiec musimy powrócic do domyslnej funkcji do pobierania hasła
(która jest zdefiniowana gdzies w module getpass). Zauwazmy, co robimy:
przypisujemy funkcje default getpass do zmiennej getpass. Jesli czytalismy
oficjalna dokumentacje getpass, mówi ona, ze moduł getpass definiuje funkcje
getpass. Wykonuje to poprzez powiazanie getpass z odpowiednia funkcja,
która zalezy od naszej platformy. Kiedy wywołujemy funkcje getpass, tak naprawde
wywołujemy funkcje okreslona dla konkretnej platformy, która okreslił
za nas powyzszy kod. Nie musimy sie martwic, na jakiej platformie uruchamiamy
nasz kod – wywołujemy tylko getpass, a funkcja ta wykona zawsze odpowiednia
czynnosc.
7.1. OBSŁUGA WYJATKÓW 117
5. Blok try...except, podobnie jak instrukcja if, moze posiadac klauzule else.
Jesli zaden wyjatek nie zostanie rzucony podczas wykonywania bloku try, spowoduje
to wywołanie klauzuli else. W tym przypadku oznacza to, ze import
from EasyDialogs import AskPassword zadziałał, a wiec mozemy powiazac
getpass z funkcja AskPassword. Kazdy inny blok try...except w przedstawionym
kodzie posiada podobna klauzule else, która pozwala, gdy zostanie
zaimportowany działajacy moduł, przypisac do getpass odpowiednia funkcje.
Materiały dodatkowe
• Python Tutorial mówi na temat definiowania, rzucania własnych wyjatków i
jednoczesnej obsługi wielu wyjatków
• Python Library Reference opisuje wszystkie wbudowane wyjatki
• Python Library Reference dokumentuje moduł getpass.
• Python Library Reference dokumentuje moduł traceback, który zapewnia niskopoziomowy
dostep do atrybutów wyjatków, po tym, jak wyjatek zostanie
rzucony.
• Python Reference Manual omawia bardziej technicznie blok try...except.
118 ROZDZIAŁ 7. WYJATKI I OPERACJE NA PLIKACH
7.2 Praca na plikach
Praca z obiektami plików
Python posiada wbudowana funkcje open, słuzaca do otwierania plików z dysku.
open zwraca obiekt pliku posiadajacy metody i atrybuty, dzieki którym mozemy dostac
sie do pliku i wykonywac na nim pewne operacje.
Przykład 6.3 Otwieranie plików
>>> f = open("/muzyka/_single/kairo.mp3", "rb") #(1)
>>> f #(2)
<open file ’/muzyka/_single/kairo.mp3’, mode ’rb’ at 010E3988>
>>> f.mode #(3)
’rb’
>>> f.name #(4)
’/muzyka/_single/kairo.mp3’
1. Metoda open przyjmuje do trzech argumentów: nazwe pliku, tryb i argument
buforowania. Tylko pierwszy z nich, nazwa pliku, jest wymagany; pozostałe dwa
sa opcjonalne. Jesli nie sa podane, plik zostanie otwarty do odczytu w trybie
tekstowym. Tutaj otworzylismy plik do odczytu w trybie binarnym. (print
open. doc da nam swietne objasnienie wszystkich mozliwych trybów.)
2. Metoda open zwraca obiekt (w tym momencie nie powinno to juz byc zaskoczeniem).
Obiekt pliku ma kilka uzytecznych atrybutów.
3. Atrybut mode obiektu pliku mówi nam, w jakim trybie został on otwarty.
4. Atrybut name zwraca sciezke do pliku, który jest dostepny z tego obiektu.
Czytanie z pliku
Otworzywszy plik, bedziemy chcieli odczytac z niego informacje, tak jak pokazano
to w nastepnym przykładzie.
Przykład 6.4 Czytanie pliku
>>> f
<open file ’/muzyka/_single/kairo.mp3’, mode ’rb’ at 010E3988>
>>> f.tell() #(1)
0
>>> f.seek(-128, 2) #(2)
>>> f.tell() #(3)
7542909
>>> tagData = f.read(128) #(4)
>>> tagData
’TAGKAIRO****THE BEST GOA ***DJ MARY-JANE***
Rave Mix 2000http://mp3.com/DJMARYJANE �37’
>>> f.tell() #(5)
7543037
7.2. PRACA NA PLIKACH 119
1. Obiekt pliku przechowuje stan otwartego pliku. Metoda tell zwraca aktualna
pozycje w otwartym pliku. Z uwagi na to, ze nie robilismy jeszcze nic z tym
plikiem, aktualna pozycja to 0, czyli poczatek pliku.
2. Metoda seek obiektu pliku słuzy do poruszania sie po otwartym pliku. Jej drugi
argument okresla znaczenie pierwszego argument; jesli argument drugi wynosi
0, oznacza to, ze pierwszy argument odnosi sie do pozycji bezwzglednej (czyli
liczac od poczatku pliku), 1 oznacza przeskok do innej pozycji wzgledem pozycji
aktualnej (liczac od pozycji aktualnej), 2 oznacza przeskok do danej pozycji
wzgledem konca pliku. Jako ze tagi MP3, o które nam chodzi, przechowywane
sa na koncu pliku, korzystamy z opcji 2 i przeskakujemy do pozycji oddalonej o
128 bajtów od konca pliku.
3. Metoda tell potwierdza, ze rzeczywiscie zmienilismy pozycje pliku.
4. Metoda read czyta okreslona liczbe bajtów z otwartego pliku i zwraca dane w
postaci łancucha znaków, które zostały odczytane. Opcjonalny argument okresla
maksymalna liczbe bajtów do odczytu. Jesli nie zostanie podany argument,
read bedzie czytał do konca pliku. (W tym przypadku moglibysmy uzyc samego
read(), poniewaz wiemy dokładnie w jakiej pozycji w pliku jestesmy i w rzeczywistosci
odczytujemy ostanie 128 bajtów.) Odczytane dane przypisujemy do
zmiennej tagData, a biezaca pozycja zostaje uaktualniana na podstawie ilosci
odczytanych bajtów.
5. Metoda tell potwierdza, ze zmieniła sie biezaca pozycja. Jesli pokusimy sie o
wykonanie obliczenia, zauwazymy, ze po odczytaniu 128 bajtów aktualna pozycja
wzrosła o 128.
Zamykanie pliku
Otwarte pliki zajmuja zasoby systemu, a inne aplikacje czasami moga nie miec do
nich dostepu (zalezy to od trybu otwarcia pliku), dlatego bardzo wazne jest zamykanie
plików tak szybko, jak tylko skonczymy na nich prace.
Przykład 6.5 Zamykanie pliku
>>> f
<open file ’/muzyka/_single/kairo.mp3’, mode ’rb’ at 010E3988>
>>> f.closed #(1)
False
>>> f.close() #(2)
>>> f
<closed file ’/muzyka/_single/kairo.mp3’, mode ’rb’ at 010E3988>
>>> f.closed #(3)
True
>>> f.seek(0) #(4)
Traceback (most recent call last):
File "<stdin>", line 1, in ?
ValueError: I/O operation on closed file
>>> f.tell()
Traceback (most recent call last):
File "<stdin>", line 1, in ?
120 ROZDZIAŁ 7. WYJATKI I OPERACJE NA PLIKACH
ValueError: I/O operation on closed file
>>> f.read()
Traceback (most recent call last):
File "<stdin>", line 1, in ?
ValueError: I/O operation on closed file
>>> f.close() #(5)
1. Atrybut closed obiektu pliku mówi, czy plik jest otwarty, czy tez nie. W tym
przypadku plik jeszcze jest otwarty (closed jest równe False).
2. Aby zamknac plik nalezy wywołac metode close obiektu pliku. Zwalnia to blokade,
która nałozona była na plik (jesli była nałozona), oczyszcza buforowane
dane (jesli jakiekolwiek dane w nim wystepuja), które system nie zdazył jeszcze
rzeczywiscie zapisac, a nastepnie zwalnia zasoby.
3. Atrybut closed potwierdza, ze plik jest zamkniety.
4. To ze plik został zamkniety, nie oznacza od razu, ze obiekt przestaje istniec.
Zmienna f bedzie istniec póki nie wyjdzie poza swój zasieg, lub nie zostanie skasowana
recznie. Aczkolwiek zadna z metod słuzacych do operowania na otwartym
pliku, nie bedzie działac od momentu jego zamkniecia; wszystkie rzucaja wyjatek.
5. Wywołanie close na pliku uprzednio zamknietym nie zwraca wyjatku; w przypadku
błedu cicho sobie z nim radzi.
Błedy wejscia/wyjscia
Zrozumienie kodu fileinfo.py z poprzedniego rozdziału, nie powinno juz byc problemem.
Kolejny przykład pokazuje, jak bezpiecznie otwierac i zamykac pliki oraz jak
nalezycie obchodzic sie z błedami.
Przykład 6.6 Obiekty pliku w MP3FileInfo
try: #(1)
fsock = open(filename, "rb", 0) #(2)
try:
fsock.seek(-128, 2) #(3)
tagdata = fsock.read(128) #(4)
finally: #(5)
fsock.close()
except IOError: #(6)
pass
1. Poniewaz otwieranie pliku i czytanie z niego jest ryzykowne, a takze operacje te
moga rzucic wyjatek, cały ten kod jest ujety w blok try...except. (Hej, czy
zestandaryzowane wciecia nie sa swietne? To moment, w którym zaczynasz je
doceniac.)
2. Funkcja open moze rzucic wyjatek IOError. (Plik moze nie istniec.)
7.2. PRACA NA PLIKACH 121
3. Funkcja seek moze rzucic wyjatek IOError. (Plik moze byc mniejszy niz 128
bajtów.)
4. Funkcja read moze rzucic wyjatek IOError. (Byc moze dysk posiada uszkodzony
sektor, albo plik znajduje sie na dysku sieciowym, a siec własnie przestała działac.)
5. Nowosc: blok try...finally. Nawet po udanym otworzeniu pliku przez open
chcemy byc całkowicie pewni, ze zostanie on zamkniety niezaleznie od tego, czy
metody seek i read rzuca wyjatki, czy tez nie. Własnie do takich rzeczy słuzy
blok try...finally: kod z bloku finally zostanie zawsze wykonany, nawet jesli
jakas instrukcja bloku try rzuci wyjatek. Nalezy o tym myslec jak o kodzie wykonywanym
na zakonczenie operacji, niezaleznie od tego co działo sie wczesniej.
6. Nareszcie poradzimy sobie z wyjatkiem IOError. Moze to byc wyjatek wywołany
przez którakolwiek z funkcji open, seek, czy read. Tym razem nie jest to dla nas
istotne, gdyz jedyna rzecza, która zrobimy to zignorowanie tego wyjatku i kontynuowanie
dalszej pracy programu. (Pamietajmy, pass jest wyrazeniem Pythona,
które nic nie robi.) Takie cos jest całkowicie dozwolone; to ze przechwycilismy
dany wyjatek, nie oznacza, ze musimy z nim cokolwiek robic. Wyjatek zostanie
potraktowany tak, jakby został obsłuzony, a kod bedzie normalnie kontynuowany
od nastepnej linijki kodu po bloku try...except.
Zapisywanie do pliku
Jak mozna przypuszczac, istnieje równiez mozliwosc zapisywania danych do plików
w sposób bardzo podobny do odczytywania. Wyrózniamy dwa podstawowe tryby
otwierania plików:
• w trybie “append”, w którym dane beda dodawane na koncu pliku
• w trybie “write”, w którym plik zostanie nadpisany.
Oba tryby, jesli plik nie bedzie istniał, utworza go automatycznie, dlatego nie ma
potrzeby na fikusne działania typu “jesli dany plik jeszcze nie istnieje, utwórz nowy pusty
plik, aby móc go otworzyc po raz pierwszy”. Po prostu otwieramy plik i zaczynamy
do niego zapisywac dane.
Przykład 6.7 Pisanie do pliku
>>> logfile = open(’test.log’, ’w’) #(1)
>>> logfile.write(’udany test’) #(2)
>>> logfile.close()
>>> print open(’test.log’).read() #(3)
udany test
>>> logfile = open(’test.log’, ’a’) #(4)
>>> logfile.write(’linia 2’)
>>> logfile.close()
>>> print open(’test.log’).read() #(5)
udany testlinia 2
122 ROZDZIAŁ 7. WYJATKI I OPERACJE NA PLIKACH
1. Zaczynamy odwaznie: tworzymy nowy lub nadpisujemy istniejacy plik test.log,
a nastepnie otwieramy do zapisu. (Drugi argument "w" oznacza otwieranie pliku
do zapisu.) Tak, jest to dokładnie tak niebezpieczne, jak brzmi. Miejmy nadzieje,
ze poprzednia zawartosc pliku nie była istota, bo juz jej nie ma.
2. Dane do nowo otwartego pliku dodajemy za pomoca metody write obiektu zwróconego
przez open.
3. Ten jednowierszowiec otwiera plik, czyta jego zawartosc i drukuje na ekran.
4. Przypadkiem wiemy, ze test.log istnieje (w koncu własnie skonczylismy do
niego pisac), wiec mozemy go otworzyc i dodawac dane. (Argument , a" oznacza
otwieranie pliku w trybie dodawania danych na koniec pliku.) Własciwie moglibysmy
to zrobic nawet wtedy, gdyby plik nie istniał, poniewaz otworzenie pliku
w trybie , a" spowoduje jego powstanie, jesli bedzie to potrzebne. Otworzenie w
trybie , a" nigdy nie uszkodzi aktualnej zawartosci pliku.
5. Jak widac, zarówno pierwotnie zapisane, jak i dopisane dane, aktualnie znajduja
sie w pliku test.log. Nalezy zauwazyc, ze znaki konca linii nie sa uwzglednione.
Jako ze nie zapisywalismy znaków konca linii w zadnym z przypadków, plik ich
nie zawiera. Znaki konca linii mozemy zapisac za pomoca symbolu "n". Z uwagi
na fakt, iz tego nie zrobilismy, całosc danych w pliku wyladowała w jednej linijce.
Materiały dodatkowe
• Python Tutorial opisuje, jak czytac i zapisywac pliki, w tym takze, jak czytac
pliki linia po linii lub jak wczytac wszystkie linie na raz do listy
• Python Knowledge Base odpowiada na najczesciej zadawane pytania dotyczace
plików
• Python Library Reference omawia wszystkie metody obiektu pliku.
7.3. PETLA FOR 123
7.3 Petla for
Petla for
Podobnie jak wiele innych jezyków, Python posiada petle for. Jedynym powodem,
dla którego nie widzielismy tej petli wczesniej jest to, ze Python posiada tyle innych
uzytecznych rzeczy, ze nie potrzebujemy jej az tak czesto.
Wiele jezyków programowania nie posiada typu danych o takich mozliwosciach, jakie
daje lista w Pythonie, dlatego tez w tych jezykach trzeba wykonywac duzo manualnej
pracy, trzeba okreslac poczatek, koniec i krok, aby przejsc zakres liczb całkowitych,
znaków lub innych iteracyjnych jednostek. Jednak pythonowa petla for, przechodzi
cała liste w identyczny sposób, jak ma to miejsce w wyrazeniach listowych.
Przykład 6.8 Wprowadzenie do petli for
>>> li = [’a’, ’b’, ’e’]
>>> for s in li: #(1)
... print s #(2)
a
b
e
>>> print "n".join(li) #(3)
a
b
e
1. Składnia petli for jest bardzo podobna do składni wyrazenia listowego. li jest
lista, a zmienna s bedzie przyjmowac kolejne wartosci elementów tej listy podczas
wykonywania petli, zaczynajac od elementu pierwszego.
2. Podobnie jak wyrazenie if i inne bloki tworzone za pomoca wciec, petla for
moze posiadac dowolna liczbe linii kodu.
3. To główna przyczyna, dla której jeszcze nie widzielismy petli for. Po prostu jej
nie potrzebowalismy. Jest to zadziwiajace, jak czesto trzeba wykorzystywac petle
for w innych jezykach, podczas gdy tak naprawde potrzebujemy metody join,
czy tez wyrazenia listowego.
Wykonanie “normalnej” (zgodnie ze standardami Visual Basica), licznikowej petli
for jest takze bardzo proste.
Przykład 6.9 Prosty licznik
>>> for i in range(5): #(1)
... print i
0
1
2
3
4
>>> li = [’a’, ’b’, ’c’, ’d’, ’e’]
>>> for i in range(len(li)): #(2)
124 ROZDZIAŁ 7. WYJATKI I OPERACJE NA PLIKACH
... print li[i]
a
b
c
d
e
1. Jak zobaczylismy w przykładzie 3.23, “Przypisywanie kolejnych wartosci”, funkcja
range tworzy liste liczb całkowitych, a ta liste bedziemy chcieli przejsc za
pomoca petli. Powyzszy kod byc moze wyglada troche dziwacznie, lecz bywa
przydatny do tworzenia petli licznikowej.
2. Nigdy tego nie róbmy. Jest to visualbasicowy styl myslenia. Skonczmy z nim.
Powinno sie iterowac liste, jak pokazano to w poprzednim przykładzie.
Petle for nie zostały stworzone do tworzenia prostych petli licznikowych. Słuza one
raczej do przechodzenia po wszystkich elementach w danym obiekcie. Ponizej pokazano
przykład, jak mozna iterowac po słowniku za pomoca petli for:
Przykład 6.10 Iterowanie po elementach słownika
>>> import os
>>> for k, v in os.environ.items(): #(1) (2)
... print "%s=%s" % (k, v)
USERPROFILE=C:Documents and Settingsmpilgrim
OS=Windows_NT
COMPUTERNAME=MPILGRIM
USERNAME=mpilgrim
[...ciach...]
>>> print "n".join(["%s=%s" % (k, v)
... for k, v in os.environ.items()]) #(3)
USERPROFILE=C:Documents and Settingsmpilgrim
OS=Windows_NT
COMPUTERNAME=MPILGRIM
USERNAME=mpilgrim
[...ciach...]
1. os.environ jest słownikiem zmiennych srodowiskowych zdefiniowanych w systemie
operacyjnym. W Windowsie mamy tutaj zmienne uzytkownika i systemu
dostepne z MS-DOS-a. W Uniksie mamy tu zmienne wyeksportowane w twojej
powłoce przez skrypty startowe. W systemie Mac OS nie ma czegos takiego jak
zmienne srodowiskowe, wiec słownik ten jest pusty.
2. os.environ.items() zwraca liste krotek w postaci [(klucz1, wartosc1), (klucz2,
wartosc2), ...]. Petla for iteruje po tej liscie. W pierwszym przebiegu petli,
przypisuje ona wartosc klucz1 do zmiennej k, a wartosc wartosc1 do v, wiec
k = ‘‘USERPROFILE’’, a v = ‘‘C:Documents and Settingsmpilgrim’’. W
drugim przebiegu petli, k przyjmuje wartosc drugiego klucza, czyli ‘‘OS’’, a v
bierze odpowiadajaca temu kluczowi wartosc, czyli ‘‘Windows NT’’.
7.3. PETLA FOR 125
3. Za pomoca wielozmiennego przypisania i wyrazen listowych, mozemy zastapic
cała petle for jednym wyrazeniem. Z której metody bedziemy korzystac w kodzie,
jest kwestia stylu pisania. Niektórym sie to moze podobac, poniewaz za
pomoca wyrazenia listowego jasno stwierdzamy, ze to co robimy to odwzorowywanie
słownika na liste, a nastepnie łaczymy otrzymana liste w jeden napis. Inni
z kolei preferuja pisanie z uzyciem petli for. Otrzymane wyjscie programu jest
identyczne w obydwu przypadkach, jakkolwiek wersja w tym przykładzie jest
nieco szybsza, poniewaz tutaj tylko raz wykorzystujemy instrukcje print.
Spójrzmy teraz na petle for w MP3FileInfo z przykładu fileinfo.py wprowadzonego
w rozdziale 5.
Przykład 6.11 Petla for w MP3FileInfo
tagDataMap = {u"tytuł" : ( 3, 33, stripnulls),
"artysta" : ( 33, 63, stripnulls),
"album" : ( 63, 93, stripnulls),
"rok" : ( 93, 97, stripnulls),
"komentarz" : ( 97, 126, stripnulls),
"gatunek" : (127, 128, ord)} #(1)
[...]
if tagdata[:3] == ’TAG’:
for tag, (start, end, parseFunc) in self.tagDataMap.items(): #(2)
self[tag] = parseFunc(tagdata[start:end]) #(3)
1. tagDataMap jest atrybutem klasy, który definiuje tagi, jakie bedziemy szukali
w pliku MP3. Tagi sa przechowywane w polach o ustalonej długosci. Poniewaz
czytamy ostatnie 128 bajtów pliku, bajty od 3 do 32 z przeczytanych danych sa
zawsze tytułem utworu, bajty od 33 do 62 sa zawsze nazwa artysty, od 63 do 92
mamy nazwe albumu itd. Zauwazmy, ze tagDataMap jest słownikiem krotek, a
kazda krotka przechowuje dwie liczby całkowite i jedna referencje do funkcji.
2. Wyglada to skomplikowanie, jednak takie nie jest. Struktura zmiennych w petli
for odpowiada strukturze elementów listy zwróconej poprzez metode items. Pamietamy,
ze items zwraca liste krotek w formie (klucz, wartosc). Jesli pierwszym
elementem tej listy jest (u"tytuł", (3, 33, <function stripnulls at
0xb7c91f7c>)), tak wiec podczas pierwszego przebiegu petli, tag jest równe
u"tytuł", start przyjmuje wartosc 3, end wartosc 33, a parseFunc zawiera
funkcje stripnulls.
3. Kiedy juz wydobedziemy wszystkie parametry tagów pliku MP3, zapisanie danych
odnoszacych sie do okreslonych tagów bedzie proste. W tym celu wycinamy
napis tagdata od wartosci w zmiennej start do wartosci w zmiennej end, aby
pobrac aktualne dane dla tego tagu, a nastepnie wywołujemy funkcje parseFunc,
aby przetworzyc te dane, a potem przypisujemy zwrócona wartosc do klucza tag
w słowniku self (scislej, self jest instancja podklasy słownika). Po przejsciu
wszystkich elementów w tagDataMap, self bedzie przechowywał wartosci dla
wszystkich tagów, a my bedziemy mogli sie dowiedziec o utworze, co tylko bedziemy
chcieli.
126 ROZDZIAŁ 7. WYJATKI I OPERACJE NA PLIKACH
7.4 Korzystanie z sys.modules
Korzystanie z sys.modules
Moduły, podobnie jak wszystko inne w Pythonie, sa obiektami. Jesli wczesniej zaimportowalismy
pewien moduł, mozemy pobrac do niego referencje za posrednictwem
globalnego słownika sys.modules.
Przykład 6.12 Wprowadzenie do sys.modules
>>> import sys #(1)
>>> print ’n’.join(sys.modules.keys()) #(2)
win32api
os.path
os
exceptions
__main__
ntpath
nt
sys
__builtin__
site
signal
UserDict
stat
1. Moduł sys zawiera informacje dotyczace systemu jak np. wersje uruchomionego
Pythona (sys.version lub sys.version info) i opcje dotyczace systemu
np. maksymalna dozwolona głebokosc rekurencji (sys.getrecursionlimit() i
sys.setrecursionlimit()).
2. sys.modules jest słownikiem zawierajacym wszystkie moduły, które zostały zaimportowane
od czasu startu Pythona. W słowniku tym kluczem jest nazwa
danego modułu, a wartoscia jest obiekt tego modułu. Dodajmy, ze jest tu wiecej
modułów niz nasz program zaimportował. Python wczytuje niektóre moduły
podczas startu, a jesli uzywasz IDE Pythona , sys.modules zawiera wszystkie
moduły zaimportowane przez wszystkie programy uruchomione wewnatrz IDE.
Ponizszy przykład pokazuje, jak wykorzystywac sys.modules.
Przykład 6.13 Korzystanie z sys.modules
>>> import fileinfo #(1)
>>> print ’n’.join(sys.modules.keys())
win32api
os.path
os
fileinfo
exceptions
__main__
ntpath
7.4. KORZYSTANIE Z SYS.MODULES 127
nt
sys
__builtin__
site
signal
UserDict
stat
>>> fileinfo
<module ’fileinfo’ from ’fileinfo.pyc’>
>>> sys.modules["fileinfo"] #(2)
<module ’fileinfo’ from ’fileinfo.pyc’>
1. Podczas importowania nowych modułów, zostaja one dodane do sys.modules.
To tłumaczy dlaczego ponowne zaimportowanie tego samego modułu jest bardzo
szybkie. Otóz Python aktualnie posiada wczytany i zapamietany moduł w
sys.modules, wiec za drugim razem kiedy importujemy moduł, Python spoglada
po prostu tylko do tego słownika.
2. Podajac nazwe wczesniej zaimportowanego modułu (w postaci łancucha znaków),
mozemy pobrac referencje do samego modułu poprzez bezposrednie wykorzystanie
słownika sys.modules.
Kolejny przykład pokazuje, jak wykorzystywac atrybut klasy module razem ze
słownikiem sys.modules, aby pobrac referencje do modułu, w którym ta klasa jest
zdefiniowana.
Przykład 6.14 Atrybut klasy module
>>> from fileinfo import MP3FileInfo
>>> MP3FileInfo.__module__ #(1)
’fileinfo’
>>> sys.modules[MP3FileInfo.__module__] #(2)
<module ’fileinfo’ from ’fileinfo.pyc’>
1. Kazda klasa Pythona posiada wbudowany atrybut klasy, jakim jest module ,
a który przechowuje nazwe modułu, w którym dana klasa jest zdefiniowana.
2. Łaczac to z sys.modules, mozemy pobrac referencje do modułu, w którym ta
klasa jest zdefiniowana.
Teraz juz jestes przygotowany do tego, aby zobaczyc w jaki sposób sys.modules
jest wykorzystywany w fileinfo.py, czyli przykładowym programie wykorzystanym w
rozdziale 5. Ponizszy przykład przedstawia fragment kodu.
Przykład 6.15 sys.modules w fileinfo.py
def getFileInfoClass(filename, module=sys.modules[FileInfo.__module__]): #(1)
u"zwraca klase metadanych pliku na podstawie podanego rozszerzenia"
subclass = "%sFileInfo" % os.path.splitext(filename)[1].upper()[1:] #(2)
return hasattr(module, subclass) and getattr(module, subclass) or FileInfo #(3)
128 ROZDZIAŁ 7. WYJATKI I OPERACJE NA PLIKACH
1. Jest to funkcja z dwoma argumentami. Argument filename jest wymagany, ale
module jest argumentem opcjonalnym i domyslnie wskazuje na moduł, który zawiera
klase FileInfo. Wyglada to nieefektywnie, poniewaz moze sie wydawac,
ze Python wykonuje wyrazenie sys.modules za kazdym razem, gdy funkcja zostaje
wywołana. Tak na prawde, Python wykonuje domyslne wyrazenia tylko raz,
podczas pierwszego zaimportowania modułu. Jak zobaczymy pózniej, nigdy nie
wywołamy tej funkcji z argumentem module, wiec argument module słuzy nam
raczej jako stała na poziomie tej funkcji.
2. Funkcji tej przyjrzymy sie pózniej, po tym, jak zanurkujemy w module os.
Na razie zaufaj, ze linia ta sprawia, ze subclass przechowuje nazwe klasy np.
MP3FileInfo.
3. Juz wiemy, ze funkcja getattr zwraca nam referencje do obiektu poprzez nazwe.
hasattr jest funkcja uzupełniajaca, która sprawdza, czy obiekt posiada
okreslony atrybut. W tym przypadku sprawdzamy, czy moduł posiada okreslona
klase (funkcja ta działa na dowolnym obiekcie i dowolnym atrybucie, podobnie
jak getattr). Ten kod mozemy na jezyk polski przetłumaczyc w ten sposób:
„Jesli ten moduł posiada klase o nazwie zawartej w zmiennej subclass, to ja
zwróc, w przeciwnym wypadku zwróc klase FileInfo”.
Materiały dodatkowe
• Python Tutorial omawia dokładnie, kiedy i jak domyslne argumenty sa wyznaczane
• Python Library Reference dokumentuje moduł sys
7.5. PRACA Z KATALOGAMI 129
7.5 Praca z katalogami
Praca z katalogami
Moduł os.path zawiera kilka funkcji słuzacych do manipulacji plikami i katalogami
(w systemie Windows nazywanymi folderami). Przyjrzymy sie teraz obsłudze sciezek
i odczytywaniu zawartosci katalogów.
Przykład 6.16 Tworzenie sciezek do plików
>>> import os
>>> os.path.join("c:musicap", "mahadeva.mp3") #(1) (2)
’c:musicapmahadeva.mp3’
>>> os.path.join("c:musicap", "mahadeva.mp3") #(3)
’c:musicapmahadeva.mp3’
>>> os.path.expanduser("~") #(4)
’c:Documents and SettingsmpilgrimMy Documents’
>>> os.path.join(os.path.expanduser("~"), "Python") #(5)
’c:Documents and SettingsmpilgrimMy DocumentsPython’
1. os.path jest referencja do modułu, a ten moduł zalezy od platformy z jakiej
korzystamy. Tak jak getpass niweluje róznice miedzy platformami ustawiajac
getpass na funkcje odpowiednia dla naszego systemu, tak os ustawia path na
moduł specyficzny dla konkretnej platformy.
2. Funkcja join modułu os.path tworzy sciezke dostepu do pliku z jednej lub kilku
sciezek czesciowych. W tym przypadku po prostu łaczy dwa łancuchy znaków.
(Zauwazmy, ze w Windowsie musimy uzywac podwójnych ukosników.)
3. W tym, troche bardziej skomplikowanym, przypadku, join dopisze dodatkowy
ukosnik do sciezki przed dołaczeniem do niej nazwy pliku. Nie musimy pisac
małej głupiej funkcji addSlashIfNecessary, poniewaz madrzy ludzie zrobili juz
to za nas.
4. expanduser rozwinie w sciezce znak na sciezke katalogu domowego aktualnie
zalogowanego uzytkownika. Ta funkcja działa w kazdym systemie, w którym
uzytkownicy maja swoje katalogi domowe, miedzy innymi w systemach Windows,
UNIX i Mac OS X, ale w systemie Mac OS nie otrzymujemy zadnych efektów.
5. Uzywajac tych technik, mozemy łatwo tworzyc sciezki do plików i katalogów
wewnatrz katalogu domowego.
Przykład 6.17 Rozdzielanie sciezek
>>> os.path.split("c:musicapmahadeva.mp3") #(1)
(’c:musicap’, ’mahadeva.mp3’)
>>> (filepath, filename) = os.path.split("c:musicapmahadeva.mp3") #(2)
>>> filepath #(3)
’c:musicap’
>>> filename #(4)
’mahadeva.mp3’
>>> (shortname, extension) = os.path.splitext(filename) #(5)
130 ROZDZIAŁ 7. WYJATKI I OPERACJE NA PLIKACH
>>> shortname
’mahadeva’
>>> extension
’.mp3’
1. Funkcja split dzieli pełna sciezke i zwraca liste, która zawiera sciezke do katalogu
i nazwe pliku. Pamietasz, jak mówilismy, ze mozna uzywac wielozmiennego
przypisania do zwracania kilku wartosci z funkcji? split jest taka własnie funkcja.
2. Przypisujesz wynik działania funkcji split do krotki dwóch zmiennych. Kazda
zmienna bedzie teraz zawierac wartosc odpowiedniego elementu krotki zwróconej
przez funkcje split.
3. Pierwsza zmienna, filepath, zawiera pierwszy element zwróconej listy – sciezke
pliku.
4. Druga zmienna, filename, zawiera drugi element listy – nazwe pliku.
5. Moduł os.path zawiera tez funkcje splitext, która zwraca krotke zawierajaca
własciwa nazwe pliku i jego rozszerzenie. Uzywamy tej samej techniki, co poprzednio,
do przypisania kazdej czesci do osobnej zmiennej.
Przykład 6.18 Wyswietlanie zawartosci katalogu
>>> os.listdir("c:music_singles") #(1)
[’a_time_long_forgotten_con.mp3’, ’hellraiser.mp3’,
’kairo.mp3’, ’long_way_home1.mp3’, ’sidewinder.mp3’,
’spinning.mp3’]
>>> dirname = "c:"
>>> os.listdir(dirname) #(2)
[’AUTOEXEC.BAT’, ’boot.ini’, ’CONFIG.SYS’, ’cygwin’,
’docbook’, ’Documents and Settings’, ’Incoming’, ’Inetpub’, ’IO.SYS’,
’MSDOS.SYS’, ’Music’, ’NTDETECT.COM’, ’ntldr’, ’pagefile.sys’,
’Program Files’, ’Python20’, ’RECYCLER’,
’System Volume Information’, ’TEMP’, ’WINNT’]
>>> [f for f in os.listdir(dirname)
... if os.path.isfile(os.path.join(dirname, f))] #(3)
[’AUTOEXEC.BAT’, ’boot.ini’, ’CONFIG.SYS’, ’IO.SYS’, ’MSDOS.SYS’,
’NTDETECT.COM’, ’ntldr’, ’pagefile.sys’]
>>> [f for f in os.listdir(dirname)
... if os.path.isdir(os.path.join(dirname, f))] #(4)
[’cygwin’, ’docbook’, ’Documents and Settings’, ’Incoming’,
’Inetpub’, ’Music’, ’Program Files’, ’Python20’, ’RECYCLER’,
’System Volume Information’, ’TEMP’, ’WINNT’]
1. Funkcja listdir pobiera sciezke do katalogu i zwraca liste jego zawartosci.
2. listdir zwraca zarówno pliki jak i katalogi, bez wskazania które sa którymi.
3. Mozemy uzyc filtrowania listy i funkcji isfile modułu os.path, aby oddzielic
pliki od katalogów. isfile przyjmuje sciezke do pliku i zwraca True, jesli reprezentuje
ona plik albo False w innym przypadku. W przykładzie uzywamy
7.5. PRACA Z KATALOGAMI 131
os.path.join, aby uzyskac pełna sciezke, ale isfile pracuje tez ze sciezkami
wzglednymi wobec biezacego katalogu. Mozemy uzyc os.getcwd() aby pobrac
biezacy katalog.
4. os.path zawiera tez funkcje isdir, która zwraca True, jesli sciezka reprezentuje
katalog i False w innym przypadku. Mozemy jej uzyc do uzyskania listy
podkatalogów.
Przykład 6.19 Listowanie zawartosci katalogu w fileinfo.py
def listDirectory(directory, fileExtList):
u"zwraca liste obiektów zawierajacych metadane dla plików o podanych rozszerzeniach"
fileList = [os.path.normcase(f) for f in os.listdir(directory)] #(1) (2)
fileList = [os.path.join(directory, f) for f in fileList
if os.path.splitext(f)[1] in fileExtList] #(3) (4) (5)
1. os.listdir(directory) zwraca liste wszystkich plików i podkatalogów w katalogu
directory.
2. Iterujac po liscie z uzyciem zmiennej f, wykorzystujemy os.path.normcase(f),
aby znormalizowac wielkosc liter zgodnie z domyslna wielkoscia liter w systemem
operacyjnym. Funkcja normcase jest uzyteczna, prosta funkcja, która stanowi
równowaznik pomiedzy systemami operacyjnymi, w których wielkosc liter w nazwie
pliku nie ma znaczenia, w którym np. mahadeva.mp3 i mahadeva.MP3 sa
takimi samymi plikami. Na przykład w Windowsie i Mac OS, normcase bedzie
konwertował cała nazwe pliku na małe litery, a w systemach kompatybilnych z
UNIX-em funkcja ta bedzie zwracała niezmieniona nazwe pliku.
3. Iterujac ponownie po liscie z uzyciem f, wykorzystujemy os.path.splitext(f),
aby podzielic nazwe pliku na nazwe i jej rozszerzenie.
4. Dla kazdego pliku sprawdzamy, czy rozszerzenie jest w liscie plików, o które nam
chodzi (czyli fileExtList, która została przekazana do listDirectory).
5. Dla kazdego pliku, który nas interesuje, wykorzystujemy os.path.join(directory,
f), aby skonstruowac pełna sciezke pliku i zwrócic liste zawierajaca pełne sciezki.
Jesli to mozliwe, powinnismy korzystac z funkcji w modułach os i os.path do
manipulacji plikami, katalogami i sciezkami. Te moduły opakowuja moduły specyficzne
dla konkretnego systemu, wiec funkcje takie, jak os.path.split pooprawnie działaja
w systemach UNIX, Windows, Mac OS i we wszystkich innych systemach wspieranych
przez Pythona.
Jest jeszcze inna metoda dostania sie do zawartosci katalogu. Metoda ta jest bardzo
potezna i uzywa zestawu symboli wieloznacznych (ang. wildcard), z którymi mozna sie
spotkac pracujac w linii polecen.
Przykład 6.20 Listowanie zawartosci katalogu przy pomocy glob
132 ROZDZIAŁ 7. WYJATKI I OPERACJE NA PLIKACH
>>> os.listdir("c:music_singles") #(1)
[’a_time_long_forgotten_con.mp3’, ’hellraiser.mp3’,
’kairo.mp3’, ’long_way_home1.mp3’, ’sidewinder.mp3’,
’spinning.mp3’]
>>> import glob
>>> glob.glob(’c:music_singles*.mp3’) #(2)
[’c:music_singlesa_time_long_forgotten_con.mp3’,
’c:music_singleshellraiser.mp3’,
’c:music_singleskairo.mp3’,
’c:music_singleslong_way_home1.mp3’,
’c:music_singlessidewinder.mp3’,
’c:music_singlesspinning.mp3’]
>>> glob.glob(’c:music_singless*.mp3’) #(3)
[’c:music_singlessidewinder.mp3’,
’c:music_singlesspinning.mp3’]
>>> glob.glob(’c:music**.mp3’) #(4)
1. Jak wczesniej powiedzielismy, os.listdir pobiera sciezke do katalogu i zwraca
wszystkie pliki i podkatalogi, które sie w nim znajduja.
2. Z drugiej strony, moduł glob na podstawie podanego wyrazenia składajacego sie
z symboli wieloznacznych, zwraca pełne sciezki wszystkich plików, które spełniaja
te wyrazenie. Tutaj wyrazenie jest sciezka do katalogu plus "*.mp3", który bedzie
dopasowywał wszystkie pliki .mp3. Dodajmy, ze kazdy element zwracanej listy
jest juz pełna sciezka do pliku.
3. Jesli chcemy znalezc wszystkie pliki w okreslonym katalogu, gdzie nazwa zaczyna
sie od s", a konczy na ".mp3", mozemy to zrobic w ten sposób.
4. Teraz rozwaz taki scenariusz: mamy katalog z muzyka z kilkoma podkatalogami,
wewnatrz których sa pliki .mp3. Mozemy pobrac liste wszystkich tych plików
za pomoca jednego wywołania glob, wykorzystujac połaczenie dwóch wyrazen.
Pierwszym jest "*.mp3" (wyszukuje pliki .mp3), a drugim sa same w sobie sciezki
do katalogów, aby przetworzyc kazdy podkatalog w c:music. Ta prosto wygladajaca
funkcja daje nam niesamowite mozliwosci!
Materiały dodatkowe
• Python Knowledge Base odpowiada na najczesciej zadawane pytanie na temat
modułu os
• Python Library Reference dokumentuje moduł os i moduł os.path
7.6. WYJATKI I OPERACJE NA PLIKACH - WSZYSTKO RAZEM 133
7.6 Wyjatki i operacje na plikach - wszystko razem
Wszystko razem
Jeszcze raz ułozymy wszystkie puzzle domina w jednym miejscu. Juz poznalismy,
w jaki sposób działa kazda linia kodu. Powrócimy do tego jeszcze raz i zobaczymy, jak
to wszystko jest ze soba dopasowane.
Przykład 6.21 listDirectory
def listDirectory(directory, fileExtList): #(1)
u"zwraca liste obiektów zawierajacych metadane dla plików o podanych rozszerzeniach"
fileList = [os.path.normcase(f) for f in os.listdir(directory)]
fileList = [os.path.join(directory, f) for f in fileList
if os.path.splitext(f)[1] in fileExtList] #(2)
def getFileInfoClass(filename, module=sys.modules[FileInfo. module ]): #(3)
u"zwraca klase metadanych pliku na podstawie podanego rozszerzenia"
subclass = "%sFileInfo" % os.path.splitext(filename)[1].upper()[1:] #(4)
return hasattr(module, subclass) and getattr(module, subclass) or FileInfo #(5)
return [getFileInfoClass(f)(f) for f in fileList] #(6)
1. listDirectory jest główna atrakcja tego modułu. Przyjmuje ona na wejsciu
katalog (np. c:music singles) i liste interesujacych nas rozszerzen plików
(jak np. [’.mp3’]), a nastepnie zwraca liste instancji klas, które sa podklasami
słownika, a przechowuja metadane na temat kazdego interesujacego nas pliku
w tym katalogu. I to wszystko jest wykonywane za pomoca kilku prostych linii
kodu.
2. Jak dowiedzielismy sie w poprzednim podrozdziale, ta linia kodu zwraca liste
pełnych sciezek wszystkich plików z danego katalogu, które maja interesujace
nas rozszerzenie (podane w argumencie fileExtList).
3. Starzy programisci Pascala znaja zagniezdzone funkcje (funkcje wewnatrz funkcji),
ale wiekszosc ludzi jest zdziwionych, gdy mówi sie im, ze Python je wspiera.
Zagniezdzona funkcja getFileInfoClass moze byc wywołana tylko z funkcji, w
której jest zadeklarowana, czyli z listDirectory. Jak w przypadku kazdej innej
funkcji, nie musimy przejmowac sie deklaracja interfejsu, ani niczym innym. Po
prostu definiujemy funkcje i implementujamy ja.
4. Teraz, gdy juz znamy moduł os, ta linia powinna nabrac sensu. Pobiera ona rozszerzenie
pliku (os.path.splitext(filename)[1]), przekształca je do duzych
liter (.upper()), odcina kropke ([1:]) i tworzy nazwe klasy uzywajac łancucha
formatujacego. c:musicapmahadeva.mp3 zostaje przekształcone na .mp3, potem
na .MP3, a nastepnie na MP3 i na koncu otrzymujemy MP3FileInfo.
5. Majac nazwe klasy obsługujacej ten plik, sprawdzamy czy tak klasa istnieje w
tym module. Jesli tak, zwracamy te klase, jesli nie – klase bazowa FileInfo. To
bardzo wazne: zwracamy klase. Nie zwracamy obiektu, ale klase sama w sobie.
6. Dla kazdego pliku z listy fileList wywołujemy getFileInfoClass z nazwa
pli”ku (f). Wywołanie getFileInfoClass(f) zwraca klase. Dokładnie nie wiadomo
jaka, ale to nam nie przeszkadza. Potem tworzymy obiekt tej klasy (jaka by
134 ROZDZIAŁ 7. WYJATKI I OPERACJE NA PLIKACH
ona nie była) i przekazujemy nazwe pliku (znów f) do jej metody init . Jak
pamietamy z wczesniejszych rozdziałów, metoda init klasy FileInfo ustawia
wartosc self[name"], co powoduje wywołanie setitem klasy pochodnej,
czyli MP3FileInfo, zeby odpowiednio przetworzyc plik i wyciagnac jego metadane.
Robimy to wszystko dla kazdego interesujacego pliku i zwracamy liste
obiektów wynikowych.
Zauwazmy, ze metoda listDirectory jest bardzo ogólna. Nie wie w zaden sposób,
z jakimi typami plików bedzie pracowac, ani jakie klasy sa zdefiniowane do obsługi
tych plików. Zaczyna prace od przejrzenia katalogu, w poszukiwaniu plików do
przetwarzania, a potem analizuje swój moduł, zeby sprawdzic, jakie klasy obsługi
(np. MP3FileInfo) sa zdefiniowane. Mozemy rozszerzyc ten program, zeby obsługiwac
inne typy plików definiujac klasy o odpowiednich nazwach: HTMLFileInfo dla plików
HTML, DOCFileInfo dla plików Worda itp. listDirectory, bez potrzeby modyfikacji
kodu tej funkcji, obsłuzy je wszystkie, zrzucajac całe przetwarzanie na odpowiednie
klasy i zbierajac otrzymane wyniki.
7.7. WYJATKI I OPERACJE NA PLIKACH - PODSUMOWANIE 135
7.7 Wyjatki i operacje na plikach - podsumowanie
Podsumowanie
Program fileinfo.py wprowadzony w rozdziale 5 powinien juz byc zrozumiały.
u"""Framework do pobierania matedanych specyficznych dla danego typu pliku.
Mozna utworzyc instancje odpowiedniej klasy podajac jej nazwe pliku w konstruktorze.
Zwrócony obiekt zachowuje sie jak słownik posiadajacy pare klucz-wartosc
dla kazdego fragmentu metadanych.
import fileinfo
info = fileinfo.MP3FileInfo("/music/ap/mahadeva.mp3")
print "n".join(["%s=%s" % (k, v) for k, v in info.items()])
Lub uzyc funkcji listDirectory, aby pobrac informacje o wszystkich plikach w katalogu.
for info in fileinfo.listDirectory("/music/ap/", [".mp3"]):
...
Framework moze byc roszerzony poprzez dodanie klas dla poszczególnych typów plików, np.:
HTMLFileInfo, MPGFileInfo, DOCFileInfo. Kazda klasa jest całkowicie odpowiedzialna
za własciwe sparsowanie swojego pliku; zobacz przykład MP3FileInfo.
"""
import os
import sys
def stripnulls(data):
u"usuwa białe znaki i nulle"
return data.replace("�0", " ").strip()
class FileInfo(dict):
u"przechowuje metadane pliku"
def __init__(self, filename=None):
dict.__init__(self)
self["plik"] = filename
class MP3FileInfo(FileInfo):
u"przechowuje znaczniki ID3v1.0 MP3"
tagDataMap = {u"tytuł" : ( 3, 33, stripnulls),
"artysta" : ( 33, 63, stripnulls),
"album" : ( 63, 93, stripnulls),
"rok" : ( 93, 97, stripnulls),
"komentarz" : ( 97, 126, stripnulls),
"gatunek" : (127, 128, ord)}
def __parse(self, filename):
u"parsuje znaczniki ID3v1.0 z pliku MP3"
self.clear()
try:
136 ROZDZIAŁ 7. WYJATKI I OPERACJE NA PLIKACH
fsock = open(filename, "rb", 0)
try:
fsock.seek(-128, 2)
tagdata = fsock.read(128)
finally:
fsock.close()
if tagdata[:3] == ’TAG’:
for tag, (start, end, parseFunc) in self.tagDataMap.items():
self[tag] = parseFunc(tagdata[start:end])
except IOError:
pass
def __setitem__(self, key, item):
if key == "plik" and item:
self.__parse(item)
FileInfo.__setitem__(self, key, item)
def listDirectory(directory, fileExtList):
u"zwraca liste obiektów zawierajacych metadane dla plików o podanych rozszerzeniach"
fileList = [os.path.normcase(f) for f in os.listdir(directory)]
fileList = [os.path.join(directory, f) for f in fileList
if os.path.splitext(f)[1] in fileExtList]
def getFileInfoClass(filename, module=sys.modules[FileInfo.__module__]):
u"zwraca klase metadanych pliku na podstawie podanego rozszerzenia"
subclass = "%sFileInfo" % os.path.splitext(filename)[1].upper()[1:]
return hasattr(module, subclass) and getattr(module, subclass) or FileInfo
return [getFileInfoClass(f)(f) for f in fileList]
if __name__ == "__main__":
for info in listDirectory("/music/_singles/", [".mp3"]):
print "n".join(["%s=%s" % (k, v) for k, v in info.items()])
print
Zanim zanurkujemy w nastepnym rozdziale, upewnijmy sie, ze nie mamy problemów
z:
• przechwytywaniem wyjatków za pomoca try...except
• chronieniem zewnetrznych zasobów za pomoca try...finally
• czytaniem plików
• korzystaniem z wielozmiennych przypisan w petli for
• Wykorzystywaniem modułu os do niezaleznego od platformy zarzadzania plikami
• Dynamicznym tworzeniem instancji klas nieznanych typów poprzez traktowanie
klas jak obiektów
Rozdział 8
Wyrazenia regularne
137
138 ROZDZIAŁ 8. WYRAZENIA REGULARNE
8.1 Wyrazenia regularne
Wyrazenia regularne sa bardzo uzytecznymi, a zarazem standardowymi srodkami
wyszukiwania, zamiany i przetwarzania tekstu wykorzystujac skomplikowane wzorce.
Jesli wykorzystywalismy juz wyrazenia regularne w innych jezykach (np. w Perlu),
to pewnie zauwazymy, ze składnia jest bardzo podobna, ponadto mozemy przeczytac
podsumowanie modułu re, aby przegladnac dostepne funkcje i ich argumenty.
Nurkujemy
Łancuchy znaków maja metody, które słuza wyszukiwaniu (index, find i count),
zmienianiu (replace) i przetwarzaniu (split), ale sa one ograniczone do najprostszych
przypadków. Metody te wyszukuja pojedynczy, zapisany na stałe ciag znaków i zawsze
uwzgledniaja wielkosc liter. Aby wyszukac cos niezaleznie od wielkosci liter w łancuchu
s, musimy uzyc s.lower() lub s.upper() i upewnic sie, ze nasz tekst do wyszukania
ma odpowiednia wielkosc liter. Metody słuzace do zamiany i podziału maja takie same
ograniczenia.
Jesli to, co próbujemy zrobic jest mozliwe przy uzyciu metod łancucha znaków,
powinnismy ich uzyc. Sa szybkie, proste i czytelne. Jesli jednak okazuje sie, ze uzywamy
wielu róznych metod i instrukcji if do obsługi przypadków szczególnych albo jesli
łaczysz je z uzyciem split, join i wyrazen listowych w dziwny i nieczytelny sposób,
mozemy byc zmuszeni do przejscia na wyrazenia regularne.
Pomimo, ze składnia wyrazen regularnych jest zwarta i niepodobna do normalnego
kodu, wynik moze byc czytelniejszy niz uzycie długiego ciagu metod łancucha znaków.
Sa nawet sposoby dodawania komentarzy wewnatrz wyrazen regularnych, aby były
one praktycznie samodokumentujace sie.
8.2. ANALIZA PRZYPADKU: ADRESY ULIC 139
8.2 Analiza przypadku: Adresy ulic
Analiza przypadku: Adresy ulic
Ta seria przykładów została zainspirowana problemami z prawdziwego zycia. Kilka
lat temu gdzies nie w Polsce, zaszła potrzeba oczyszczenia i ustandaryzowania adresów
ulic zaimportowanych ze starego systemu, zanim zostana zaimportowane do nowego.
(Zauwazmy, ze nie jest to jakis wyimaginowany przykład. Moze on sie okazac przydatny.)
Ponizszy przykład przyblizy nas do tego problemu.
Przykład 7.1 Dopasowywanie na koncu napisu
>>> s = ’100 NORTH MAIN ROAD’
>>> s.replace(’ROAD’, ’RD.’) #(1)
’100 NORTH MAIN RD.’
>>> s = ’100 NORTH BROAD ROAD’
>>> s.replace(’ROAD’, ’RD.’) #(2)
’100 NORTH BRD. RD.’
>>> s[:-4] + s[-4:].replace(’ROAD’, ’RD.’) #(3)
’100 NORTH BROAD RD.’
>>> import re #(4)
>>> re.sub(’ROAD$’, ’RD.’, s) #(5) (6)
’100 NORTH BROAD RD.’
1. Naszym celem jest ustandaryzowanie adresów ulic, wiec skrótem od ’ROAD’ jest
’RD.’. Na pierwszy rzut oka wydaje sie, ze po prostu mozna wykorzystac metode
łancucha znaków, jaka jest replace. Zakładamy, ze wszystkie dane zapisane
sa za pomoca wielkich liter, wiec nie powinno byc problemów wynikajacych z
niedopasowania, ze wzgledu na wielkosc liter. Wyszukujemy stały napis, jakim
jest ’ROAD’. Jest to bardzo płytki przykład, wiec s.replace poprawnie zadziała.
2. Zycie niestety jest troche bardziej skomplikowane, o czym dosc szybko mozna
sie przekonac. Problem w tym przypadku polega na tym, ze ’ROAD’ wystepuje
w adresie dwukrotnie: raz jako czesc nazwy ulicy (’BROAD’) i drugi raz jako oddzielne
słowo. Metoda replace znajduje te dwa wystapienia i slepo je zamienia,
niszczac adres.
3. Aby rozwiazac problem z adresami, gdzie podciag ’ROAD’ wystepuje kilka razy,
mozemy wykorzystac taki pomysł: tylko szukamy i zamieniamy ’ROAD’ w ostatnich
czterech znakach adresu (czyli s[-4:]), a zostawiamy pozostała czesc (czyli
s[:-4]). Jednak, jak mozemy zreszta zobaczyc, takie rozwiazanie jest dosyc niewygodne.
Na przykład polecenie, które chcemy wykorzystac, zalezy od długosc
zamienianego napisu (jesli chcemy zamienic ’STREET’ na ’ST.’, wykorzystamy
s[:-6] i s[-6:].replace(...)). Chciałoby sie do tego wrócic za szesc miesiecy
i to debugowac? Pewnie nie.
4. Nadszedł odpowiedni czas, aby przejsc do wyrazen regularnych.WPythonie cała
funkcjonalnosc wyrazen regularnych zawarta jest w module re.
5. Spójrzmy na pierwszy parametr, ’ROAD$’. Jest to proste wyrazenie regularne,
które dopasuje ’ROAD’ tylko wtedy, gdy wystapi on na koncu tekstu. Znak $
znaczy “koniec napisu”. (Mamy takze analogiczny znak, znak daszka ^, który
znaczy “poczatek napisu”.)
140 ROZDZIAŁ 8. WYRAZENIA REGULARNE
6. Korzystajac z funkcji re.sub, przeszukujemy napis s i podciag pasujacy do wyrazenia
regularnego ’ROAD$’ zamieniamy na ’RD.’. Dzieki temu wyrazeniu dopasowujemy
’ROAD’ na koncu napisu s, lecz napis ’ROAD’ nie zostanie dopasowany
w słowie ’BROAD’, poniewaz znajduje sie on w srodku napisu s.
Wracajac do historii z porzadkowaniem adresów, okazało sie, ze poprzedni przykład,
dopasowujacy ’ROAD’ na koncu adresu, nie był poprawny, poniewaz nie wszystkie
adresy dołaczały ’ROAD’ na koncu adresu. Niektóre adresy konczyły sie tylko nazwa
ulicy. Wiele razy to wyrazenie zadziałało poprawnie, jednak gdy mielismy do czynienia
z ulica ’BROAD’, wówczas wyrazenie regularne dopasowywało ’ROAD’ na koncu napisu
jako czesc słowa ’BROAD’, a takiego wyniku nie oczekiwalismy.
Przykład 7.2 Dopasowywanie całych wyrazów
>>> s = ’100 BROAD’
>>> re.sub(’ROAD$’, ’RD.’, s)
’100 BRD.’
>>> re.sub(’bROAD$’, ’RD.’, s) #(1)
’100 BROAD’
>>> re.sub(r’bROAD$’, ’RD.’, s) #(2)
’100 BROAD’
>>> s = ’100 BROAD ROAD APT. 3’
>>> re.sub(r’bROAD$’, ’RD.’, s) #(3)
’100 BROAD ROAD APT. 3’
>>> re.sub(r’bROADb’, ’RD.’, s) #(4)
’100 BROAD RD. APT 3’
1. W istocie chcielismy odnalezc ’ROAD’ znajdujace sie na koncu napisu i jest samodzielnym
słowem, a nie czescia dłuzszego wyrazu. By opisac cos takiego za
pomoca wyrazen regularnych korzystamy z b, które znaczy tyle co “tutaj musi
znajdowac sie poczatek lub koniec wyrazu”. W Pythonie jest to nieco skomplikowane
przez fakt, iz znaki specjalne (takie jak np. ) musza byc poprzedzone
własnie znakiem . Zjawisko to okreslane jest czasem plaga ukosników (ang.
backslash plague) i wydaje sie byc jednym z powodów łatwiejszego korzystania z
wyrazen regularnych w Perlu niz w Pythonie. Z drugiej jednak strony, w Perlu
składnia wyrazen regularnych wymieszana jest ze składnia jezyka, co utrudnia
stwierdzenie czy bład tkwi w naszym wyrazeniu regularnym, czy w błednym
uzyciu składni jezyka.
2. Eleganckim obejsciem problemu plagi ukosników jest wykorzystywanie tzw. surowych
napisów (ang. raw string), które opisywalismy w rozdziale 3, poprzez
umieszczanie przed napisami litery r. Python jest w ten sposób informowany o
tym, iz zaden ze znaków specjalnych w tym napisie nie ma byc interpretowany;
’t’ odpowiada znakowi tab, jednak r’t’ oznacza tyle, co litera t poprzedzona
znakiem . Przy wykorzystaniu wyrazen regularnych zalecane jest stosowanie surowych
napisów; w innym wypadku wyrazenia szybko staja sie niezwykle skomplikowane
(a przeciez juz ze swej natury nie sa proste).
3. Cóz... Niestety wkrótce okazuje sie, iz istnieje wiecej przypadków przeczacych
logice naszego postepowania. W tym przypadku ’ROAD’ było samodzielnym słowem,
jednak znajdowało sie w srodku napisu, poniewaz na jego koncu umieszczony
był jeszcze numer mieszkania. Z tego powodu nasze biezace wyrazenie nie
8.2. ANALIZA PRZYPADKU: ADRESY ULIC 141
zostało odnalezione, funkcja re.sub niczego nie zamieniła, a co za tym idzie
napis został zwrócony w pierwotnej postaci (co nie było naszym celem).
4. Aby rozwiazac ten problem wystarczy zamienic w wyrazeniu regularnym $ na
kolejne b. Teraz bedzie ono pasowac do kazdego samodzielnego słowa ’ROAD’,
niezaleznie od jego pozycji w napisie.
142 ROZDZIAŁ 8. WYRAZENIA REGULARNE
8.3 Analiza przypadku: Liczby rzymskie
Analiza przypadku: Liczby rzymskie
Najprawdopodobniej spotkalismy sie juz gdzies z liczbami rzymskimi. Mozna je
spotkac w starszych filmach ogladanych w telewizji (np. “Copyright MCMXLVI” zamiast
“Copyright 1946”) lub na scianach bibliotek, czy uniwersytetów (napisy typu
“załozone w MDCCCLXXXVIII” zamiast “załozone w 1888 roku”). Moglismy je takze
zobaczyc na przykład w referencjach bibliograficznych. Ten system reprezentowania
liczb siega czasów starozytnego Rzymu (stad nazwa).
W liczbach rzymskich wykorzystuje sie siedem znaków, które na rózne sposoby sie
powtarza i łaczy, aby zapisac pewna liczbe:
• I = 1
• V = 5
• X = 10
• L = 50
• C = 100
• D = 500
• M = 1000
Ponizej znajduja sie podstawowe zasady konstruowania liczb rzymskich:
• Znaki sa addytywne. I to 1, II to 2, a III to 3. VI to 6 (dosłownie, „5 i 1”), VII
to 7, a VIII to 8.
• Znaki dziesiatek (I, X, C i M) moga sie powtarzac do trzech razy. Za czwartym
nalezy odjac od nastepnego wiekszego znaku piatek. Nie mozna zapisac liczby 4
jako IIII. Zamiast tego napiszemy IV (“o 1 mniej niz 5”). Liczba 40 zapisujemy
jako XL (o 10 mniej niz 50), 41 jako XLI, 42 jako XLII, 43 jako XLIII, a potem
44 jako XLIV (o 10 mniej niz 50, a potem o 1 mniej niz 5).
• Podobnie w przypadku 9. Musimy odejmowac od wyzszego znaku dziesiatek: 8
to VIII, lecz 9 zapiszemy jako IX (o 1 mniej niz 10), a nie jako VIIII (poniewaz
znak nie moze sie powtarzac cztery razy). Liczba 90 to XC, a 900 zapiszemy jako
CM.
• Znaki piatek nie moga sie powtarzac. Liczba 10 jest zawsze reprezentowana przez
X, nigdy przez VV. Liczba 100 to zawsze C, nigdy LL.
• Liczby rzymskie sa zawsze pisane od najwyzszych do najnizszych i czytane od
lewej do prawej, wiec porzadek znaków jest bardzo wazny. DC to 600, jednak
CD jest kompletnie inna liczba (400, poniewaz o 100 mniej niz 500). CI to 101,
jednak IC nie jest zadna poprawna liczba rzymska (nie mozemy bezposrednio
odejmowac 1 od 100, musimy to zapisac jako XCIX, o 10 mniej niz 100, dodac 1
mniej niz 10).
8.3. ANALIZA PRZYPADKU: LICZBY RZYMSKIE 143
Sprawdzamy tysiace
Jak sprawdzic, czy jakis łancuch znaków jest liczba rzymska? Spróbujmy sprawdzac
cyfra po cyfrze. Jako, ze liczby rzymskie sa zapisywane zawsze od najwyzszych do
najnizszych, zacznijmy od tych najwyzszych: tysiecy. Dla liczby 1000 i wiekszych,
tysiace zapisywane sa przez serie liter M.
Przykład 7.3 Sprawdzamy tysiace
>>> import re
>>> pattern = ’^M?M?M?$’ #(1)
>>> re.search(pattern, ’M’) #(2)
<SRE_Match object at 0106FB58>
>>> re.search(pattern, ’MM’) #(3)
<SRE_Match object at 0106C290>
>>> re.search(pattern, ’MMM’) #(4)
<SRE_Match object at 0106AA38>
>>> re.search(pattern, ’MMMM’) #(5)
>>> re.search(pattern, "") #(6)
<SRE_Match object at 0106F4A8>
1. Ten wzorzec składa sie z 3 czesci:
• ^, które umieszczone jest w celu dopasowania jedynie poczatku łancucha.
Gdybysmy go nie umiescili, wzorzec pasowałby do kazdego wystapienia znaków
M, czego przeciez nie chcemy. Chcemy, aby dopasowane zostały jedynie
znaki M znajdujace sie na poczatku łancucha, o ile w ogóle istnieja.
• M?, które ma wykryc, czy istnieje pojedyncza litera M. Jako, ze powtarzamy
to trzykrotnie, dopasujemy od zera do trzech liter M w szeregu.
• $, w celu dopasowania wzorca do konca łancucha. Gdy połaczymy to ze
znakiem ^ na poczatku, otrzymamy wzorzec, który musi pasowac do całego
łancucha, bez zadnych znaków przed czy po serii znaków M.
2. Sednem modułu re jest funkcja search, która jako argumenty przyjmuje wyrazenie
regularne (wzorzec) i łancuch znaków (’M’), a nastepnie próbuje go
dopasowac do wzorca. Gdy zostanie dopasowany, search zwraca obiekt który
posiada wiele metod, które opisuja dopasowanie. Jesli nie uda sie dopasowac,
search zwraca None, co jest Pythonowa pusta wartoscia i nic nie oznacza. Na
razie jedyne, co nas interesuje, to czy wzorzec został dopasowany, czy nie, a co
mozemy stwierdzic przez sprawdzenie, co zwróciła funkcja search. ’M’ pasuje
do wzorca, gdyz pierwsze opcjonalne M zostało dopasowane, a drugie i trzecie
zostało zignorowane.
3. ’MM’ pasuje, gdyz pierwsze i drugie opcjonalne M zostało dopasowane, a trzecie
zignorowane.
4. ’MMM’ równiez pasuje do wzorca, gdyz wszystkie trzy opcjonalne wystapienia M
we wzorcu zostały dopasowane.
5. ’MMMM’ nie pasuje, gdyz pomimo dopasowania pierwszych trzech opcjonalnych
znaków M, za trzecim wzorzec wymaga, aby łancuch sie skonczył, a w naszym
łancuchu znaków znajduje sie kolejna litera M. Tak wiec search zwraca wartosc
None.
144 ROZDZIAŁ 8. WYRAZENIA REGULARNE
6. Co ciekawe, pusty łancuch tez pasuje do wzorca, gdyz wszystkie wystapienia M
sa opcjonalne.
Sprawdzamy setki
Setki sa nieco trudniejsze, poniewaz schemat zapisu nie jest az tak prosty jak w
wypadku tysiecy. Mamy wiec nastepujace mozliwosci:
• 100 = C
• 200 = CC
• 300 = CCC
• 400 = CD
• 500 = D
• 600 = DC
• 700 = DCC
• 800 = DCCC
• 900 = CM
Wynika z tego, ze mamy 4 wzorce:
• CM
• CD
• Zero do trzech wystapien C (zero, gdyz moze nie byc zadnej setki)
• D, po którym nastepuje zero do trzech C
Ostatnie dwa wzorce mozemy połaczyc w opcjonalne D, a za nim od zera do trzech
C.
Ponizszy przykład ilustruje jak sprawdzac setki w liczbach Rzymskich.
Przykład 7.4 Sprawdzamy setki
>>> import re
>>> pattern = ’^M?M?M?(CM|CD|D?C?C?C?)$’ #(1)
>>> re.search(pattern, ’MCM’) #(2)
<SRE_Match object at 01070390>
>>> re.search(pattern, ’MD’) #(3)
<SRE_Match object at 01073A50>
>>> re.search(pattern, ’MMMCCC’) #(4)
<SRE_Match object at 010748A8>
>>> re.search(pattern, ’MCMC’) #(5)
>>> re.search(pattern, "") #(6)
<SRE_Match object at 01071D98>
8.3. ANALIZA PRZYPADKU: LICZBY RZYMSKIE 145
1. Ten wzorzec zaczyna sie tak samo jak poprzedni, rozpoczynajac sprawdzanie od
poczatku łancucha (^), potem sprawdzajac tysiace (M?M?M?). Tutaj zaczyna sie
nowa czesc, która definiuje 3 alternatywne wzorce rozdzielone pionowa kreska
(|): CM, CD, i D?C?C?C? (opcjonalne D, po którym nastepuje od zera do trzech
opcjonalnych znaków C). Analizator wyrazen regularnych sprawdza kazdy ze
wzorców w kolejnosci od lewej do prawej, wybiera pierwszy pasujacy i ignoruje
reszte.
2. ’MCM’ pasuje, gdyz pierwsza litera M pasuje, drugie i trzecie M jest ignorowane, i
CM pasuje (gdyz CD oraz D?C?C?C? nie sa nawet sprawdzane). MCM to rzymska
liczba 1900.
3. ’MD’ pasuje, poniewaz pierwsze M pasuje, drugie i trzecie M z wzorca jest ignorowane,
oraz D pasuje do wzorca D?C?C?C? (wystapienia znaku C jest opcjonalne,
wiec analizator je ignoruje). MD to rzymska liczba 1500.
4. ’MMMCCC’ pasuje, gdyz pasuja wszystkie trzy pierwsze znaki M, a fragment D?C?C?C?
we wzorcu pasuje do CCC (D jest opcjonalne). MMMCCC to 3300.
5. ’MCMC’ nie pasuje, Pierwsze M pasuje, CM równiez, ale $ juz nie, gdyz nasz łancuch
zamiast sie skonczyc, ma kolejna litere C. Nie została ona dopasowana do wzorca
D?C?C?C?, gdyz został on wykluczony przez wystapienie wzorca CM.
6. Co ciekawe, pusty łancuch znaków dalej pasuje do naszego wzorca, gdyz wszystkie
znaki M sa opcjonalne, tak jak kazdy ze znaków we wzorcu D?C?C?C?.
Uff! Widzimy, jak szybko wyrazenia regularne staja sie brzydkie? A jak na razie
wprowadzilismy do niego tylko tysiace i setki. Ale jesli dokładnie sledzilismy cały ten
rozdział, dziesiatki i jednostki nie powinny stanowic dla Ciebie problemu, poniewaz
wzór jest identyczny. A teraz zajmiemy sie inna metoda wyrazania wzorca.
146 ROZDZIAŁ 8. WYRAZENIA REGULARNE
8.4 Składnia ?n, m?
Składnia {n, m}
W poprzednim podrozdziale poznalismy wzorce, w których ten sam znak mógł sie
powtarzac co najwyzej trzy razy. Tutaj przedstawimy inny sposób zapisania takiego
wyrazenia, a który wydaje sie byc bardziej czytelny. Najpierw spójrzmy na metody
wykorzystane w poprzednim przykładzie.
Przykład 7.5 Stary sposób: kazdy znak opcjonalny
>>> import re
>>> pattern = ’^M?M?M?$’
>>> re.search(pattern, ’M’) #(1)
<_sre.SRE_Match object at 0x008EE090>
>>> pattern = ’^M?M?M?$’
>>> re.search(pattern, ’MM’) #(2)
<_sre.SRE_Match object at 0x008EEB48>
>>> pattern = ’^M?M?M?$’
>>> re.search(pattern, ’MMM’) #(3)
<_sre.SRE_Match object at 0x008EE090>
>>> re.search(pattern, ’MMMM’) #(4)
1. Instrukcja ta dopasowuje poczatek napisu, a nastepnie pierwsza litere M, lecz nie
dopasowuje drugiego i trzeciego M (wszystko jest w porzadku, poniewaz sa one
opcjonalne), a nastepnie koniec napisu.
2. Tutaj zostaje dopasowany poczatek napisu, a nastepnie pierwsze i drugie opcjonalne
M, jednak nie zostaje dopasowane trzecie M (ale wszystko jest w ok, poniewaz
jest to opcjonalne), ale zostaje dopasowany koniec napisu.
3. Zostanie dopasowany poczatek napisu, a nastepnie wszystkie opcjonalne M, a
potem koniec napisu.
4. Dopasowany zostanie poczatek napisu, nastepnie wszystkie opcjonalne M, jednak
koniec tekstu nie zostanie dopasowany, poniewaz pozostanie jedno niedopasowane
M, dlatego tez nic nie zostanie dopasowane, a operacja zwróci None.
Przykład 7.6 Nowy sposób: od n do m
>>> pattern = ’^M{0,3}$’ #(1)
>>> re.search(pattern, ’M’) #(2)
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, ’MM’) #(3)
<_sre.SRE_Match object at 0x008EE090>
>>> re.search(pattern, ’MMM’) #(4)
<_sre.SRE_Match object at 0x008EEDA8>
>>> re.search(pattern, ’MMMM’) #(5)
1. Ten wzorzec mówi: “dopasuj poczatek napisu, potem od zera do trzech znaków
M, a nastepnie koniec napisu”. 0 i 3 moze byc dowolna liczba. Jesli chcielibysmy
dopasowac co najmniej jeden, lecz nie wiecej niz trzy znaki M, powiniennismy
napisac M{1,3}.
8.4. SKŁADNIA ?N, M? 147
2. Dopasowujemy poczatek napisu, potem jeden znak M z trzech mozliwych, a nastepnie
koniec napisu.
3. Tutaj zostaje dopasowany poczatek napisu, nastepnie dwa M z trzech mozliwych,
a nastepnie koniec napisu.
4. Zostanie dopasowany poczatek napisu, potem trzy znaki M z trzech mozliwych,
a nastepnie koniec napisu.
5. W tym miejscu dopasowujemy poczatek napisu, potem trzy znaki M z posród
trzech mozliwych, lecz nie dopasujemy konca napisu. To wyrazenie regularne
pozwala wykorzystac tylko trzy litery M, zanim dojdzie do konca napisu, a my
mamy cztery, wiec ten wzorzec niczego nie dopasuje i zwróci None.
Nie mamy programowalnej mozliwosci okreslenia, czy dwa wyrazenia sa równowazne.
Najlepszym sposobem, aby to zrobic, jest wykonanie wielu testów w celu przekonania
sie, czy otrzymujemy takie same wyniki. Wiecej na temat pisania testów dowiemy
sie w dalszej czesci tej ksiazki.
Sprawdzanie dziesiatek i jednosci
Teraz rozszerzmy wyrazenie wykrywajace liczby rzymskie, zeby odnajdywało tez
dziesiatki i jednosci. Ten przykład pokazuje sprawdzanie dziesiatek.
Przykład 7.7 Sprawdzanie dziesiatek
>>> pattern = ’^M?M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)$’
>>> re.search(pattern, ’MCMXL’) #(1)
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, ’MCML’) #(2)
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, ’MCMLX’) #(3)
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, ’MCMLXXX’) #(4)
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, ’MCMLXXXX’) #(5)
1. To dopasuje poczatek łancucha, potem pierwsze opcjonalne M, dalej CM i XL, a
potem koniec łancucha. Zapamietajmy, ze składnia (A|B|C) oznacza “dopasuj
dokładnie jedno z A, B lub C”. W tym wypadku dopasowalismy XL, wiec ignorujemy
XC i L?X?X?X? i przechodzimy do konca łancucha. MCMXL to 1940.
2. Tutaj dopasowujemy poczatek łancucha, pierwsze opcjonalne M, potem CM i
L?X?X?X?. Z tego ostatniego elementu dopasowane zostaje tylko L, a opcjonalne
X zostaja pominiete. Potem przechodzimy na koniec łancucha. MCML to 1950.
3. Dopasowuje poczatek napisu, potem pierwsze opcjonalne M, nastepnie CM, potem
opcjonalne L i pierwsze opcjonalne X, pomijajac drugie i trzecie opcjonalne X, a
nastepnie dopasowuje koniec napisu. MCMLX jest rzymska reprezentacja liczby
1960.
148 ROZDZIAŁ 8. WYRAZENIA REGULARNE
4. Tutaj zostanie dopasowany poczatek napisu, nastepnie pierwsze opcjonalne M, potem
CM, nastepnie opcjonalne L, wszystkie trzy opcjonalne znaki X i w koncu dopasowany
zostanie koniec napisu. MCMLXXX jest rzymska reprezentacja liczby
1980.
5. To dopasuje poczatek napisu, nastepnie pierwsze opcjonalne M, potem CM, opcjonalne
L, wszystkie trzy opcjonalne znaki X, jednak nie moze dopasowac konca
napisu, poniewaz pozostał jeszcze jeden niewliczony znak X. Zatem cały wzorzec
nie zostanie dopasowany, wiec zostanie zwrócone None. MCMLXXXX nie jest
poprawna liczba rzymska.
Ponizej przedstawiono podobne wyrazenie, które dodatkowo sprawdza jednosci.
Oszczedzimy sobie szczegółów.
>>> pattern = ’^M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)(IX|IV|V?I?I?I?)$’
Jak bedzie wygladało to wyrazenie wykorzystujac składnie {n,m}? Zobaczmy na
ponizszy przykład.
Przykład 7.8 Sprawdzanie liczb rzymskich korzystajac ze składni {n, m}
>>> pattern = ’^M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$’
>>> re.search(pattern, ’MDLV’) #(1)
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, ’MMDCLXVI’) #(2)
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, ’MMMDCCCLXXXVIII’) #(3)
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, ’I’) #(4)
<_sre.SRE_Match object at 0x008EEB48>
1. Dopasowany zostanie poczatek napisu, potem jeden z mozliwych czterech znaków
M i nastepnie D?C{0,3}. Z kolei D?C{0,3} dopasuje opcjonalny znak D i zero z
trzech mozliwych znaków C. Idac dalej, dopasowany zostanie L?X{0,3} poprzez
dopasowanie opcjonalnego znaku L i zero z mozliwych trzech znaków X. Nastepnie
dopasowujemy V?I{0,3} dodajac opcjonalne V i zero z trzech mozliwych znaków
I, a ostatecznie dopasowujemy koniec napisu. MDLV jest rzymska reprezentacja
liczby 1555.
2. To dopasuje poczatek napisu, nastepnie dwa z czterech mozliwych znaków M, a
potem D?C{0,3} z D i jednym z trzech mozliwych znaków C. Dalej dopasujemy
L?X{0,3} z L i jednym z trzech mozliwych znaków X, a nastepnie V?I{0,3} z V
i jednym z trzech mozliwych znaków I, a w koncu koniec napisu. MMDCLXVI
jest reprezentacja liczby 2666.
3. Tutaj dopasowany zostanie poczatek napisu, a potem trzy z trzech znaków M, a
nastepnie D?C{0,3} ze znakiem D i trzema z trzech mozliwych znaków C. Potem
dopasujemy L?X{0,3} z L i trzema z trzech znaków X, nastepnie V?I{0,3} z V i
trzema z trzech mozliwych znaków I, a ostatecznie koniec napisu. MMMDCCCLXXXVIII
jest reprezentacja liczby 3888 i ponadto jest najdłuzsza liczba Rzymska,
która mozna zapisac bez rozszerzonej składni.
8.4. SKŁADNIA ?N, M? 149
4. Obserwuj dokładnie. Dopasujemy poczatek napisu, potem zero z czterech M, nastepnie
dopasowujemy D?C{0,3} pomijajac opcjonalne D i dopasowujac zero z
trzech znaków C. Nastepnie dopasowujemy L?X{0,3} pomijajac opcjonalne L i
dopasowujac zero z trzech znaków X, a potem dopasowujemy V?I{0,3} pomijajac
opcjonalne V, ale dopasowujac jeden z trzech mozliwych I. Ostatecznie
dopasowujemy koniec napisu.
Jesli przesledziłes to wszystko i zrozumiałes to za pierwszym razem, jestes bardzo
bystry. Teraz wyobrazmy sobie sytuacje, ze próbujemy zrozumiec jakies inne wyrazenie
regularne, które znajduje sie w centralnej, krytycznej funkcji wielkiego programu.
Albo wyobraz sobie nawet, ze wracasz do swojego wyrazenia regularnego po kilku
miesiacach. Sytuacje takie moga nie wygladac ciekawie...
W nastepnym podrozdziale dowiemy sie o alternatywnej składni, która pomaga
zrozumiec i zarzadzac wyrazeniami.
150 ROZDZIAŁ 8. WYRAZENIA REGULARNE
8.5 Rozwlekłe wyrazenia regularne
Rozwlekłe wyrazenia regularne
Jak na razie, mielismy do czynienia z czyms, co nazywam “zwiezłymi” wyrazeniami
regularnymi. Jak pewnie zauwazylismy, sa one trudne do odczytania i nawet, jesli juz
je rozszyfrujemy, nie ma gwarancji, ze zrobimy to za np. szesc miesiecy. To, czego
potrzebujemy, to dokumentacja w ich tresci.
Python pozwala na to przez tworzenie rozwlekłych wyrazen regularnych (ang. verbose
regular expressions). Róznia sie one od zwiezłych dwoma rzeczami:
• Białe znaki sa ignorowane. Spacje, znaki tabulacji, znaki nowej linii nie sa dopasowywane
jako spacje, znaki tabulacji lub znaki nowej linii. Znaki te nie sa w
ogóle dopasowywane. (Jesli bysmy chcieli jednak dopasowac którys z nich, musisz
poprzedzic je odwrotnym ukosnikiem ().)
• Komentarze sa ignorowane. Komentarz w rozwlekłym wyrazeniu regularnym wyglada
dokładnie tak samo, jak w kodzie Pythona: zaczyna sie od # i leci az do
konca linii. W tym przypadku jest to komentarz w wieloliniowym łancuchu znaków,
a nie w kodzie zródłowym, ale zasada działania jest taka sama.
Łatwiej bedzie to zrozumiec jesli skorzystamy z przykładu. Skorzystajmy ze zwiezłego
wyrazenia regularnego, które utworzylismy wczesniej i zróbmy z niego rozwlekłe.
Ten przykład pokazuje jak.
Przykład 7.9 Wyrazenia regularne z komentarzami
>>> pattern = """
^ # poczatek łancucha znaków
M{0,3} # tysiace - 0 do 3 M
(CM|CD|D?C{0,3}) # setki - 900 (CM), 400 (CD), 0-300 (0 do 3 C),
# lub 500-800 (D, a po nim 0 do 3 C)
(XC|XL|L?X{0,3}) # dziesiatki - 90 (XC), 40 (XL), 0-30 (0 do 3 X),
# or 50-80 (L, a po nim 0 do 3 X)
(IX|IV|V?I{0,3}) # jednosci - 9 (IX), 4 (IV), 0-3 (0 do 3 I),
# lub 5-8 (V, a po nim 0 do 3 I)
$ # koniec łancucha znaków
"""
>>> re.search(pattern, ’M’, re.VERBOSE) #(1)
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, ’MCMLXXXIX’, re.VERBOSE) #(2)
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, ’MMMDCCCLXXXVIII’, re.VERBOSE) #(3)
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, ’M’) #(4)
1. Najwazniejsza rzecza o której nalezy pamietac, gdy korzystamy z rozwlekłych wyrazen
regularnych jest to, ze musimy przekazac dodatkowy argument: re.VERBOSE.
Jest to stała zdefiniowana w module re, która sygnalizuje, ze wyrazenie powinno
byc traktowane jako rozwlekłe. Jak widzimy, ten wzorzec ma mnóstwo białych
znaków (które sa ignorowane) i kilka komentarzy (które tez sa ignorowane). Gdy
usuniemy białe znaki i komentarze, to pozostanie dokładnie to samo wyrazenie
8.5. ROZWLEKŁE WYRAZENIA REGULARNE 151
regularne, jakie otrzymalismy w poprzednim przykładzie, ale o wiele bardziej
czytelne. (Zauwazmy, ze co prawda łancuch znaków posiada polskie znaki, ale
nie tworzymy go w unikodzie, poniewaz i tak te znaki nie maja dla nas zadnego
znaczenia, poniewaz sa w komentarzach.)
2. To dopasowuje poczatek łancucha, potem jedno z trzech mozliwych M, potem CM,
L i trzy z trzech mozliwych X, a nastepnie IX i koniec łancucha.
3. To dopasowuje poczatek łancucha, potem trzy z trzech mozliwych M, dalej D, trzy
z trzech mozliwych C, L z trzema mozliwymi X, potem V z trzema mozliwymi I
i na koniec koniec łancucha.
4. Tutaj nie udało sie dopasowac niczego. Czemu? Poniewaz nie przekazalismy flagi
re.VERBOSE, wiec funkcja re.search traktuje to wyrazenie regularne jako zwiezłe,
z duza iloscia białych znaków i kratek. Python nie rozpoznaje samodzielnie,
czy kazemy mu dopasowac zwiezłe, czy moze rozwlekłe wyrazenie regularne i
przyjmuje, ze kazde jest zwiezłe, chyba ze wyraznie wskazemy, ze tak nie jest.
152 ROZDZIAŁ 8. WYRAZENIA REGULARNE
8.6 Analiza przypadku: Przetwarzanie numerów telefonów
Analiza przypadku: Przetwarzanie numerów telefonów
Do tej pory koncentrowalismy sie na dopasowywaniu całych wzorców. Albo pasował
albo nie. Ale wyrazenia regularne sa duzo potezniejsze. Gdy zostanie dopasowane,
mozna wyciagnac z niego wybrane kawałki i dzieki temu sprawdzic co gdzie zostało
dopasowane.
Oto kolejny przykład z zycia wziety, z jakim mozna sie spotkac: przetwarzanie
amerykanskich numerów telefonów. Klient chciał móc wprowadzac numer w dowolnej
formie w jednym polu, ale potem chciał, zeby przechowywac oddzielnie numer kierunkowy,
numer w dwóch czesciach i opcjonalny numer wewnetrzny w bazie danych firmy.
W Internecie mozna znalezc wiele takich wyrazen regularnych, ale zadne z nich nie
jest az tak bardzo restrykcyjne.
Oto przykłady numerów telefonów jakie miał program przetwarzac:
• 800-555-1212
• 800 555 1212
• 800.555.1212
• (800) 555-1212
• 1-800-555-1212
• 800-555-1212-1234
• 800-555-1212x1234
• 800-555-1212 ext. 1234
• work 1-(800) 555.1212 #1234
Całkiem duze zróznicowanie! W kazdym z tych przypadków musimy wiedziec, ze
numerem kierunkowy 800, ze pierwsza czescia numeru jest 555, druga 1212, a dla tych
z numerem wewnetrznym 1234.
Spróbujmy rozwiazac ten problem. Ponizszy przykład pokazuje pierwszy krok.
Przykład 7.10 Odnajdywanie numerów
>>> phonePattern = re.compile(r’^(d{3})-(d{3})-(d{4})$’) #(1)
>>> phonePattern.search(’800-555-1212’).groups() #(2)
(’800’, ’555’, ’1212’)
>>> phonePattern.search(’800-555-1212-1234’) #(3)
>>>
1. Zawsze odczytujemy wyrazenie regularne od lewej do prawej. Tutaj dopasowujemy
poczatek łancucha znaków, potem (d{3}). Co to takiego te (d{3})? {3}
oznacza “dopasuj dokładnie 3 wystapienia” (jest to wariacja składni {n, m}). d
oznacza “jakakolwiek cyfra” (od 0 do 9). Umieszczenie ich w nawiasach oznacza
“dopasuj dokładnie 3 cyfry, i zapamietaj je jako grupe, o która mozna zapytac
pózniej”. Nastepnie mamy dopasowac myslnik. Dalej dopasuj kolejna grupe
8.6. ANALIZA PRZYPADKU: PRZETWARZANIE NUMERÓW TELEFONÓW153
dokładnie trzech cyfr, a nastepnie kolejny myslnik, i ostatnia grupe tym razem
czterech cyfr. Na koniec dopasuje koniec łancucha znaków.
2. Aby otrzymac grupy, które zapamieta moduł przetwarzania wyrazen regularnych,
nalezy skorzystac z metody groups() obiektu zwracanego przez funkcje
search. Zwróci ona krotke z iloscia elementów równa ilosci grup zdefiniowanych
w wyrazeniu regularnym. W tym przypadku mamy trzy grupy: dwie po 3 cyfry
i ostatnia czterocyfrowa.
3. To jednak nie jest rozwiazaniem naszego problemu, bo nie dopasowuje numeru
telefonu z numerem wewnetrznym. Musimy wiec je rozszerzyc.
Przykład 7.11 Odnajdywanie numeru wewnetrznego
>>> phonePattern = re.compile(r’^(d{3})-(d{3})-(d{4})-(d+)$’) #(1)
>>> phonePattern.search(’800-555-1212-1234’).groups() #(2)
(’800’, ’555’, ’1212’, ’1234’)
>>> phonePattern.search(’800 555 1212 1234’) #(3)
>>>
>>> phonePattern.search(’800-555-1212’) #(4)
>>>
1. To wyrazenie regularne jest praktycznie identyczne z wczesniejszym. Tak jak
wczesniej, dopasowujemy poczatek łancucha, potem zapamietywana grupe trzech
cyfr, myslnik, zapamietywana grupe trzech cyfr, myslnik i zapamietywana grupe
czterech cyfr. Nowa czescia jest kolejny myslnik i zapamietywana grupa jednej
lub wiecej cyfr. Na koncu jak w poprzednim przykładzie dopasowujemy koniec
łancucha.
2. Metoda groups() zwraca teraz krotke czterech elementów, poniewaz wyrazenie
regularne definiuje teraz cztery grupy do zapamietania.
3. Niestety nie jest to wersja ostateczna, gdyz zakładamy, ze kazda czesc numeru
telefonu jest rozdzielona myslnikiem. Co jesli beda rozdzielone spacja, kropka
albo przecinkiem? Potrzebujemy bardziej ogólnego rozwiazania.
4. Ups! Nie tylko to wyrazenie nie robi wszystkiego co powinno, ale cofnelismy sie
wstecz, gdyz teraz nie dopasowuje ono numerów bez numeru wewnetrznego. To
nie jest to co chcielismy; jesli w numerze jest podany numer wewnetrzny, to
chcemy go znac, ale jesli go nie ma, to i tak chcemy znac inne czesci numeru
telefonu.
Nastepny przykład pokazuje wyrazenie regularne, które obsługuje rózne separatory
miedzy czesciami numeru telefonu.
Przykład 7.12 Obsługa róznych separatorów
>>> phonePattern = re.compile(r’^(d{3})D+(d{3})D+(d{4})D+(d+)$’) #(1)
>>> phonePattern.search(’800 555 1212 1234’).groups() #(2)
(’800’, ’555’, ’1212’, ’1234’)
>>> phonePattern.search(’800-555-1212-1234’).groups() #(3)
(’800’, ’555’, ’1212’, ’1234’)
154 ROZDZIAŁ 8. WYRAZENIA REGULARNE
>>> phonePattern.search(’80055512121234’) #(4)
>>>
>>> phonePattern.search(’800-555-1212’) #(5)
>>>
1. Teraz dopasowujemy poczatek łancucha, grupe trzech cyfr, potem D+... zaraz,
zaraz, co to jest? D dopasowuje dowolny znak, który nie jest cyfra, a + oznacza
“jeden lub wiecej”. Wiec D+ dopasowuje jeden lub wiecej znaków nie bedacych
cyfra. Korzystamy z niego, aby dopasowac rózne separatory, nie tylko myslniki.
2. Korzystanie z D+ zamiast z - pozwala na dopasowywanie numerów telefonów
ze spacjami w roli separatora czesci.
3. Oczywiscie myslniki tez działaja.
4. Niestety, to dalej nie jest ostateczna wersja, poniewaz nie obsługuje ona braku
jakichkolwiek separatorów.
5. No i dalej nie rozwiazany pozostał problem mozliwosci braku numeru wewnetrznego.
Mamy wiec dwa problemy do rozwiazania, ale w obu przypadkach rozwiazemy
problem ta sama technika.
Nastepny przykład pokazuje wyrazenie regularne pasujace takze do numeru bez
separatorów.
Przykład 7.13 Obsługa numerów bez separatorów
>>> phonePattern = re.compile(r’^(d{3})D*(d{3})D*(d{4})D*(d*)$’) #(1)
>>> phonePattern.search(’80055512121234’).groups() #(2)
(’800’, ’555’, ’1212’, ’1234’)
>>> phonePattern.search(’800.555.1212 x1234’).groups() #(3)
(’800’, ’555’, ’1212’, ’1234’)
>>> phonePattern.search(’800-555-1212’).groups() #(4)
(’800’, ’555’, ’1212’, ’’)
>>> phonePattern.search(’(800)5551212 x1234’) #(5)
>>>
1. Jedyna zmiana jakiej dokonalismy od ostatniego kroku to zamiana wszystkich
+ na *. Zamiast D+ pomiedzy czesciami numeru telefonu dopasowujemy teraz
D*. Pamietasz, ze + oznacza “1 lub wiecej”? * oznacza “0 lub wiecej”. Tak wiec
teraz jestesmy w stanie przetworzyc numer nawet bez separatorów.
2. Nareszcie działa! Dlaczego? Dopasowany został poczatek łancucha, grupa 3 cyfr
(800), potem zero znaków nienumerycznych, potem znowu zapamietywana grupa
3 cyfr (555), znowu zero znaków nienumerycznych, zapamietywana grupa 4 cyfr
(1212), zero znaków nienumerycznych, numer wewnetrzny (1234) i nareszcie koniec
łancucha.
3. Inne odmiany tez działaja np. numer rozdzielony kropkami ze spacja i x-em przed
numerem wewnetrznym.
4. Wreszcie udało sie tez rozwiazac problem z brakiem numeru wewnetrznego. Tak
czy siak groups() zwraca nam krotke z 4 elementami, ale ostatni jest tutaj pusty.
8.6. ANALIZA PRZYPADKU: PRZETWARZANIE NUMERÓW TELEFONÓW155
5. Niestety jeszcze nie skonczylismy. Co tutaj jest nie tak? Przed numerem kierunkowym
znajduje sie dodatkowy znak "(", a nasze wyrazenie zakłada, ze numer
kierunkowy znajduje sie na samym przodzie. Nie ma problemu, mozemy zastosowac
te sama metode co do znaków rozdzielajacych.
Nastepny przykład pokazuje jak sobie radzic ze znakami wiodacymi w numerach
telefonów.
Przykład 7.14 Obsługa znaków na poczatku numeru telefonu
>>> phonePattern = re.compile(r’^D*(d{3})D*(d{3})D*(d{4})D*(d*)$’) #(1)
>>> phonePattern.search(’(800)5551212 ext. 1234’).groups() #(2)
(’800’, ’555’, ’1212’, ’1234’)
>>> phonePattern.search(’800-555-1212’).groups() #(3)
(’800’, ’555’, ’1212’, ’’)
>>> phonePattern.search(’work 1-(800) 555.1212 #1234’) #(4)
>>>
1. Wzorzec w tym przykładzie jest taki sam jak w poprzednim, z wyjatkiem tego, ze
teraz na poczatku łancucha dopasowujemy D* przed pierwsza zapamietywana
grupa (numerem kierunkowym). Zauwaz ze tych znaków nie zapamietujemy (nie
sa one w nawiasie). Jesli je napotkamy, to ignorujemy je i przechodzimy do
numeru kierunkowego.
2. Teraz udało sie przetworzyc numer telefonu z nawiasem otwierajacym na poczatku.
(Zamykajacy był juz wczesniej obsługiwany; był traktowany jako nienumeryczny
znak pasujacy do teraz drugiego D*.)
3. Tak na wszelki wypadek sprawdzamy czy nie popsulismy czegos. Jako, ze poczatkowy
znak jest całkowicie opcjonalny, nastepuje dopasowanie w dokładnie
taki sam sposób jak w poprzednim przykładzie.
4. W tym miejscu wyrazenia regularne sprawiaja, ze chce sie człowiekowi rozbic
bardzo duzym młotem monitor. Dlaczego to nie pasuje? Wszystko za sprawa 1
przed numerem kierunkowym (numer kierunkowy USA), a przeciez przyjelismy,
ze na poczatku moga byc tylko nienumeryczne znaki. Ech...
Cofnijmy sie na chwile. Jak na razie wszystkie wyrazenia dopasowywalismy od
poczatku łancucha. Ale teraz widac wyraznie, ze na poczatku naszego łancucha mamy
nieokreslona liczbe znaków których kompletnie nie potrzebujemy. Po co mamy wiec
dopasowywac poczatek łancucha? Jesli tego nie zrobimy, to przeciez pominie on tyle
znaków ile mu sie uda, a przeciez o to nam chodzi. Takie podejscie prezentuje nastepny
przykład.
Przykład 7.15 Numerze telefonu, znajde cie gdziekolwiek jestes!
>>> phonePattern = re.compile(r’(d{3})D*(d{3})D*(d{4})D*(d*)$’) #(1)
>>> phonePattern.search(’work 1-(800) 555.1212 #1234’).groups() #(2)
(’800’, ’555’, ’1212’, ’1234’)
>>> phonePattern.search(’800-555-1212’) #(3)
(’800’, ’555’, ’1212’, ’’)
>>> phonePattern.search(’80055512121234’) #(4)
(’800’, ’555’, ’1212’, ’1234’)
156 ROZDZIAŁ 8. WYRAZENIA REGULARNE
1. Zauwaz, ze brakuje ^ w tym wyrazeniu regularnym, Teraz juz nie dopasowujemy
poczatku łancucha, bo przeciez nikt nie powiedział, ze wyrazenie musi pasowac
do całego łancucha, a nie do fragmentu. Mechanizm wyrazen regularnych sam
zadba o namierzenie miejsca do którego ono pasuje (o ile w ogóle).
2. Teraz nareszcie pasuje numer ze znakami na poczatku (w tym cyframi) i dowolnymi,
jakimikolwiek separatorami w srodku.
3. Na wszelki wypadek sprawdzamy i to. Działa!
4. To tez działa.
Widzimy, jak szybko wyrazenia regularne wymykaja sie spod kontroli? Rzucmy
okiem na jedna z poprzednich przykładów. Widzimy róznice pomiedzy nim i nastepnym?
Póki jeszcze rozumiemy to co napisalismy, rozpiszmy to jako rozwlekłe wyrazenie
regularne, zeby nie zapomniec, co jest co i dlaczego.
Przykład 7.16 Przetwarzanie numerów telefonu (wersja finalna)
>>> phonePattern = re.compile(r’’’
# nie dopasowuj poczatku łancucha, numer moze sie zaczac gdziekolwiek
(d{3}) # numer kierunkowy - 3 cyfry (np. ’800’)
D* # opcjonalny nienumeryczny separator
(d{3}) # pierwsza czesc numeru - 3 cyfry (np. ’555’)
D* # opcjonalny separator
(d{4}) # druga czesc numeru (np. ’1212’)
D* # opcjonalny separator
(d*) # numer wewnetrzny jest opcjonalny i moze miec dowolna długosc
$ # koniec łancucha
’’’, re.VERBOSE)
>>> phonePattern.search(’work 1-(800) 555.1212 #1234’).groups() #(1)
(’800’, ’555’, ’1212’, ’1234’)
>>> phonePattern.search(’800-555-1212’) #(2)
(’800’, ’555’, ’1212’, ’’)
1. Pomijajac fakt, ze jest ono podzielone na wiele linii, to wyrazenie jest dokładnie
takie samo jak po ostatnim kroku, wiec nie jest niespodzianka, ze dalej działa
jak powinno.
2. Jeszcze jedna próba. Tak, działa! Skonczone!
8.7. PODSUMOWANIE 157
8.7 Wyrazenia regularne - podsumowanie
Podsumowanie
To co przedstawilismy tutaj, to zaledwie wierzchołek góry lodowej, odnosnie tego co
potrafia wyrazenia regularne. Innymi słowy, mimo ze jestesmy teraz nimi przytłoczeni,
uwierzmy, ze jeszcze nic nie widzielismy.
Powinienes juz byc zaznajomiony z ponizszymi technikami:
• ^ dopasowuje poczatek napisu.
• $ dopasowuje koniec napisu.
• b dopasowuje poczatek lub koniec słowa.
• d dopasowuje dowolna cyfre.
• D dopasowuje dowolny znak, który nie jest cyfra.
• x? dopasowuje opcjonalny znak x (innymi słowy, dopasowuje x zero lub jeden
raz).
• x* dopasowuje x zero lub wiecej razy.
• x+ dopasowuje x jeden lub wiecej razy.
• x{n,m} dopasowuje znak x co najmniej n razy, lecz nie wiecej niz m razy.
• (a|b|c) dopasowuje a albo b albo c.
• (x) generalnie jest to zapamietana grupa. Mozna otrzymac wartosc, która została
dopasowana, wykorzystujac metode groups() obiektu zwróconego przez
re.search.
Wyrazenia regularne daja ekstremalnie potezne mozliwosci, lecz nie zawsze sa poprawnym
rozwiazaniem do kazdego problemu. Powinno sie wiecej o nich poczytac, aby
sie dowiedziec, kiedy beda one odpowiednie podczas rozwiazywania pewnych problemów,
czy tez kiedy moga bardziej powodowac problemy, niz je rozwiazywac.
“Niektórzy ludzie, kiedy napotkaja problem, mysla: ’Wiem, uzyje wyrazen
regularnych’. I teraz maja dwa problemy.”
– Jamie Zawinski
158 ROZDZIAŁ 8. WYRAZENIA REGULARNE
Rozdział 9
Przetwarzanie HTML-a
159
160 ROZDZIAŁ 9. PRZETWARZANIE HTML-A
9.1 Przetwarzanie HTML-a
Nurkujemy
Na comp.lang.python czesto mozna zobaczyc pytania w stylu “jak mozna znalezc
wszystkie nagłówki/obrazki/linki w moim dokumencie HTML?”, “jak moge sparsowac/
przetłumaczyc/przerobic tekst mojego dokumentu HTML tak, aby zostawic znaczniki
w spokoju?” lub tez “jak moge natychmiastowo dodac/usunac/zacytowac atrybuty
z wszystkich znaczników mojego dokumentu HTML?”. Rozdział ten odpowiada
na wszystkie te pytania.
Ponizej przedstawiono w dwóch czesciach całkowicie działajacy program. Pierwsza
czesc, BaseHTMLProcessor.py jest ogólnym narzedziem, które przetwarza pliki HTML
przechodzac przez wszystkie znaczniki i bloki tekstowe. Druga czesc, dialect.py, jest
przykładem tego, jak wykorzystac BaseHTMLProcessor.py, aby przetłumaczyc tekst
dokumentu HTML, lecz przy tym zostawiajac znaczniki w spokoju. Przeczytaj notki
dokumentacyjne i komentarze w celu zorientowania sie, co sie tutaj własciwie dzieje.
Duza czesc tego kodu wyglada jak czarna magia, poniewaz nie jest oczywiste w jaki
sposób dowolna z metod klasy jest wywoływana. Jednak nie martw sie, wszystko zostanie
wyjasnione w odpowiednim czasie.
Przykład 8.1 BaseHTMLProcessor.py
#-*- coding: utf-8 -*-
from sgmllib import SGMLParser
import htmlentitydefs
class BaseHTMLProcessor(SGMLParser):
def reset(self):
# dodatek (wywoływane przez SGMLParser. init )
self.pieces = []
SGMLParser.reset(self)
def unknown starttag(self, tag, attrs):
# wywoływane dla kazdego poczatkowego znacznika
# attrs jest lista krotek (atrybut, wartosc)
# np. dla <pre class="screen"> bedziemy mieli tag="pre",
# attrs=[("class", "screen")]
# Chcielibysmy zrekonstruowac oryginalne znaczniki i atrybuty, ale
# moze sie zdarzyc, ze umiescimy w cudzysłowach wartosci, które nie były
# zacytowane w zródle dokumentu, a takze mozemy zmienic rodzaj
# cudzysłowów w wartosci danego atrybutu (pojedyncze cudzysłowy lub podwójne).
# Dodajmy, ze niepoprawnie osadzony kod nie-HTML-owy (np. kod JavaScript)
# moze zostac zle sparsowany przez klase bazowa, a to spowoduje bład
# wykonania skryptu.
# Cały nie-HTML musi byc umieszczony w komentarzu HTML-a (<!-- kod -->),
# aby parser zostawił ten niezmieniony (korzystajac z handle comment).
strattrs = "".join([’ %s="%s"’ % (key, value) for key, value in attrs])
self.pieces.append("<%(tag)s%(strattrs)s>" % locals())
9.1. NURKUJEMY 161
def unknown endtag(self, tag):
# wywoływane dla kazdego znacznika koncowego np. dla </pre>, tag bedzie równy "pre"
# Rekonstruuje oryginalny znacznik koncowy w wyjsciowym dokumencie
self.pieces.append("</%(tag)s>" % locals())
def handle charref(self, ref):
# wywoływane jest dla kazdego odwołania znakowego np. dla "&#160;",
# ref bedzie równe "160"
# Rekonstruuje oryginalne odwołanie znakowe.
self.pieces.append("&#%(ref)s;" % locals())
def handle entityref(self, ref):
# wywoływane jest dla kazdego odwołania do encji np. dla "&copy;",
# ref bedzie równe "copy"
# Rekonstruuje oryginalne odwołanie do encji.
self.pieces.append("&%(ref)s" % locals())
# standardowe encje HTML-a sa zakonczone srednikiem; pozostałe encje
# (encje spoza HTML-a) nie sa
if htmlentitydefs.entitydefs.has key(ref):
self.pieces.append(";")
def handle data(self, text):
# wywoływane dla kazdego bloku czystego teksu np. dla danych spoza dowolnego
#znacznika, w których nie wystepuja zadne odwołania znakowe, czy odwołania do encji.
# Przechowuje dosłownie oryginalny tekst.
self.pieces.append(text)
def handle comment(self, text):
# wywoływane dla kazdego komentarza np. <!-- wpis kod JavaScript w tym miejscu -->
# Rekonstruuje oryginalny komentarz.
# Jest to szczególnie wazne, gdy dokument zawiera kod przeznaczony
# dla przegladarki (np. kod Javascript) wewnatrz komentarza, dzieki temu
# parser moze przejsc przez ten kod bez zakłócen;
# wiecej szczegółów w komentarzu metody unknown starttag.
self.pieces.append("<!--%(text)s-->" % locals())
def handle pi(self, text):
# wywoływane dla kazdej instrukcji przetwarzania np. <?instruction>
# Rekonstruuje oryginalna instrukcje przetwarzania
self.pieces.append("<?%(text)s>" % locals())
def handle decl(self, text):
# wywoływane dla deklaracji typu dokumentu, jesli wystepuje, np.
# <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
# "http://www.w3.org/TR/html4/loose.dtd">
# Rekonstruuje oryginalna deklaracje typu dokumentu
self.pieces.append("<!%(text)s>" % locals())
def output(self):
u"""Zwraca przetworzony HTML jako pojedynczy łancuch znaków"""
162 ROZDZIAŁ 9. PRZETWARZANIE HTML-A
return "".join(self.pieces)
if name == " main ":
for k, v in globals().items():
print k, "=", v
Przykład 8.2 dialect.py
#-*- coding: utf-8 -*-
import re
from BaseHTMLProcessor import BaseHTMLProcessor
class Dialectizer(BaseHTMLProcessor):
subs = ()
def reset(self):
# dodatek (wywoływany przez init klasy bazowej)
# Resetuje wszystkie atrybuty
self.verbatim = 0
BaseHTMLProcessor.reset(self)
def start pre(self, attrs):
# wywoływane dla kazdego znacznika <pre> w zródle HTML
# Zwieksza licznik trybu dosłownosci verbatim, a nastepnie
# obsługuje ten znacznik normalnie
self.verbatim += 1
self.unknown starttag("pre", attrs)
def end pre(self):
# wywoływane dla kazdego znacznika </pre>
# Zmiejsza licznik trybu dosłownosci verbatim
self.unknown endtag("pre")
self.verbatim -= 1
def handle data(self, text):
# metoda nadpisana
# wywoływane dla kazdego bloku tekstu w zródle
# Jesli jest w trybie dosłownym, zapisuje tekst niezmieniony;
# inaczej przetwarza tekst za pomoca szeregu podstawien
self.pieces.append(self.verbatim and text or self.process(text))
def process(self, text):
# wywoływane z handle data
# Przetwarza kazdy blok wykonujac serie podstawien
# za pomoca wyrazen regularnych (podstawienia sa definiowane przez
# klasy pochodne)
for fromPattern, toPattern in self.subs:
9.1. NURKUJEMY 163
text = re.sub(fromPattern, toPattern, text)
return text
class ChefDialectizer(Dialectizer):
u"""konwertuje HTML na mowe szwedzkiego szefa kuchni
oparte na klasycznym chef.x, copyright (c) 1992, 1993 John Hagerman
"""
subs = ((r’a([nu])’, r’u1’),
(r’A([nu])’, r’U1’),
(r’aB’, r’e’),
(r’AB’, r’E’),
(r’enb’, r’ee’),
(r’Bew’, r’oo’),
(r’Beb’, r’e-a’),
(r’be’, r’i’),
(r’bE’, r’I’),
(r’Bf’, r’ff’),
(r’Bir’, r’ur’),
(r’(w*?)i(w*?)$’, r’1ee2’),
(r’bow’, r’oo’),
(r’bo’, r’oo’),
(r’bO’, r’Oo’),
(r’the’, r’zee’),
(r’The’, r’Zee’),
(r’thb’, r’t’),
(r’Btion’, r’shun’),
(r’Bu’, r’oo’),
(r’BU’, r’Oo’),
(r’v’, r’f’),
(r’V’, r’F’),
(r’w’, r’w’),
(r’W’, r’W’),
(r’([a-z])[.]’, r’1. Bork Bork Bork!’))
class FuddDialectizer(Dialectizer):
u"""konwertuje HTML na mowe Elmer Fudda"""
subs = ((r’[rl]’, r’w’),
(r’qu’, r’qw’),
(r’thb’, r’f’),
(r’th’, r’d’),
(r’n[.]’, r’n, uh-hah-hah-hah.’))
class OldeDialectizer(Dialectizer):
u"""konwertuje HTML na pozorowany jezyk srednioangielski"""
subs = ((r’i([bcdfghjklmnpqrstvwxyz])eb’, r’y1’),
(r’i([bcdfghjklmnpqrstvwxyz])e’, r’y11e’),
(r’ickb’, r’yk’),
(r’ia([bcdfghjklmnpqrstvwxyz])’, r’e1e’),
(r’e[ea]([bcdfghjklmnpqrstvwxyz])’, r’e1e’),
164 ROZDZIAŁ 9. PRZETWARZANIE HTML-A
(r’([bcdfghjklmnpqrstvwxyz])y’, r’1ee’),
(r’([bcdfghjklmnpqrstvwxyz])er’, r’1re’),
(r’([aeiou])reb’, r’1r’),
(r’ia([bcdfghjklmnpqrstvwxyz])’, r’i1e’),
(r’tionb’, r’cioun’),
(r’ionb’, r’ioun’),
(r’aid’, r’ayde’),
(r’ai’, r’ey’),
(r’ayb’, r’y’),
(r’ay’, r’ey’),
(r’ant’, r’aunt’),
(r’ea’, r’ee’),
(r’oa’, r’oo’),
(r’ue’, r’e’),
(r’oe’, r’o’),
(r’ou’, r’ow’),
(r’ow’, r’ou’),
(r’bhe’, r’hi’),
(r’veb’, r’veth’),
(r’seb’, r’e’),
(r"’sb", r’es’),
(r’icb’, r’ick’),
(r’icsb’, r’icc’),
(r’icalb’, r’ick’),
(r’tleb’, r’til’),
(r’llb’, r’l’),
(r’ouldb’, r’olde’),
(r’ownb’, r’oune’),
(r’unb’, r’onne’),
(r’rryb’, r’rye’),
(r’estb’, r’este’),
(r’ptb’, r’pte’),
(r’thb’, r’the’),
(r’chb’, r’che’),
(r’ssb’, r’sse’),
(r’([wybdp])b’, r’1e’),
(r’([rnt])b’, r’11e’),
(r’from’, r’fro’),
(r’when’, r’whan’))
def translate(url, dialectName="chef"):
u"""pobiera plik na podstawie URL-a
i tłumaczy korzystajac z dialektu, gdzie
dialekt in ("chef", "fudd", "olde")"""
import urllib
sock = urllib.urlopen(url)
htmlSource = sock.read()
sock.close()
parserName = "%sDialectizer" % dialectName.capitalize()
parserClass = globals()[parserName]
9.1. NURKUJEMY 165
parser = parserClass()
parser.feed(htmlSource)
parser.close()
return parser.output()
def test(url):
u"""testuje wszystkie dialekty na pewnym URL-u"""
for dialect in ("chef", "fudd", "olde"):
outfile = "%s.html" % dialect
fsock = open(outfile, "wb")
fsock.write(translate(url, dialect))
fsock.close()
import webbrowser
webbrowser.open new(outfile)
if name == " main ":
test("http://diveintopython.org/odbchelper list.html")
Uruchamiajac ten skrypt, przetłumaczymy podrozdział 3.2, z ksiazki ”Dive Into
Python“, na pozorowany szwedzki kuchmistrza z Muppetów, udawany jezyk Elmer
Fudda (z kreskówek Królik Bugs) i pozorowany jezyk srednioangielski (luzno oparty
na ”Chaucer’s The Canterbury Tales“). Jesli spojrzymy na zródło HTML wyjsciowej
strony, zobaczymy, ze znaczniki i atrybuty zostały nietkniete, lecz tekst miedzy
znacznikami został ”przetłumaczonyna udawany jezyk. Jesli przygladniemy sie jeszcze
bardziej, zobaczymy, ze tylko tytuły i akapity zostały przetłumaczone. Przedstawione
kody i wyniki działania programu zostały niezmienione.
Przykład 8.3 Wyjscie z dialect.py
<div class="abstract">
<p>Lists awe <span class="application">Pydon</span>’s wowkhowse datatype.
If youw onwy expewience wif wists is awways in
<span class="application">Visuaw Basic</span> ow (God fowbid) de datastowe
in <span class="application">Powewbuiwdew</span>, bwace youwsewf fow
<span class="application">Pydon</span> wists.</p>
</div>
166 ROZDZIAŁ 9. PRZETWARZANIE HTML-A
9.2 Wprowadzenie do sgmllib.py
Wprowadzenie do sgmllib.py
Przetwarzanie HTML-a jest podzielone na trzy etapy: podzielenie dokumentu na
elementy składowe, manipulowanie tymi elementami i ponowna rekonstrukcja tych
kawałków do HTML-a. Pierwszy krok jest wykonywany przez sgmllib.py, który jest
czescia standardowej biblioteki Pythona.
Kluczem do zrozumienia tego rozdziału jest uswiadomienie sobie, ze HTML to nie
tylko tekst, jest to tekst z pewna struktura. Struktura ta powstaje z mniej lub bardziej
hierarchicznych sekwencji znaczników poczatkowych i znaczników koncowych. Zazwyczaj
nie pracujemy z HTML-em w sposób strukturalny, raczej tekstowo w edytorze
tekstu lub wizualnie w przegladarce internetowej, czy innym narzedziu. sgmllib.py
prezentuje HTML strukturalnie.
sgmllib.py zawiera jedna wazna klase: SGMLParser. SGMLParser rozbiera HTML
na uzyteczne kawałki takie jak znaczniki poczatkowe i znaczniki koncowe. Jak tylko
udaje mu sie rozebrac jakies dane na przydatne kawałki, wywołuje odpowiednia metode,
w zaleznosci co zostało znalezione. Zeby wykorzystac parser, tworzymy podklase
SGMLParser-a i nadpisujemy te metody. Mówiac, ze sgmllib.py prezentuje HTML
strukturalnie, mielismy na mysli to, ze struktura dokumentu HTML jest okreslana
poprzez wywoływane metody, a takze argumenty przekazywane do tych metod.
SGMLParser parsuje HTML na 8 rodzajów danych i wykonuje odpowiednie metody
dla kazdego z nich:
Znacznik poczatkowy Znacznik HTML, który rozpoczyna blok np. <html>, <head>,
<body> lub <pre> lub samodzielne znaczniki jak <br> lub <img>. Kiedy odnajdzie
znacznik tagname, to SGMLParser bedzie szukał metod o nazwie start tagname
lub do tagname. Na przykład, jesli odnajdzie znacznik <pre>, to bedzie szukał
metod start pre lub do pre. Jesli je znajdzie, SGMLParser wywoła te metody z
lista atrybutów tego znacznika.Wprzeciwnym wypadku wywoła unknown starttag
z nazwa znacznika i lista atrybutów.
Znacznik koncowy Znacznik HTML, który konczy blok np. </html>, </head>, </body>
lub </pre>. Kiedy odnajdzie znacznik koncowy, SGMLParser bedzie szukał metody
o nazwie end tagname. Jesli ja znajdzie, wywoła te metode, jesli nie, wywoła
metode unknown endtag z nazwa znacznika.
Odwołania znakowe Znak specjalny, do którego dowołujemy sie podajac jego dziesietny
lub szesnastkowy odpowiednik np. &amp;#160;. Kiedy odwołanie znakowe
zostanie odnalezione, SGMLParser wywoła handle charref z tekstem dziesietnego
lub szesnastkowego odpowiednika znaku.
Odwołanie do encji Encja HTML to np. &amp;copy;. Kiedy zostanie znaleziona,
SGMLParser wywołuje handle entityref z nazwa encji.
Komentarz Element HTML, który jest ograniczony przez <!-- ... -->. Kiedy zostanie
znaleziony, SGMLParser wywołuje handle comment z zawartoscia komentarza.
Instrukcje przetwarzania Instrukcje przetwarzania HTML sa ograniczone przez <?
... >. Kiedy zostana odnalezione, SGMLParser wywołuje handle pi z zawartoscia
instrukcji przetwarzania.
9.2. WPROWADZENIE DO SGMLLIB.PY 167
Deklaracja Deklaracja HTML np. typu dokumentu (DOCTYPE), jest ograniczona przez
<! ... >. Kiedy zostanie znaleziona, SGMLParser wywołuje handle decl z zawartoscia
deklaracji.
Dane tekstowe Bloki tekstu. Wszystko inne, co sie nie miesci w innych 7 kategoriach.
Kiedy zostana one znalezione, SGMLParser wywoła handle data z tekstem.
sgmllib.py posiada zestaw testów, które to ilustruja.Mozemy uruchomic sgmllib.py,
podajac w linii polecen nazwe pliku, a bedzie on wyswietlał znaczniki i inne elementy
podczas parsowania. Zrobione jest to poprzez utworzenie podklasy SGMLParser i zdefiniowanie
metod unknown starttag, unknown endtag, handle data i innych metod,
które beda po prostu wyswietlac swoje argumenty.
W ActivePython IDE pod Windows mozemy okreslic argumenty linii polecen za
pomoca “Run script”. Rózne argumenty oddzielamy spacja.
Przykład 8.4 Test sgmllib.py
<span>c:python23lib> type "c:downloadsdiveintopythonhtmltocindex.html"</span>
<!DOCTYPE html
PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>Dive Into Python</title>
<link rel="stylesheet" href="diveintopython.css" type="text/css">
[...ciach...]
Tutaj jest kawałek spisu tresci angielskiej wersji tej ksiazki, w HTML-u. Oczywiscie
sciezki do plików mozesz miec troche inne. (Angielska wersje tej ksiazki, w formacie
HTML, mozesz znalezc na http://diveintopython.org/.)
Uruchomiajac to za pomoca zestawu testów sgmllib.py, zobaczymy:
<span>c:python23lib> python sgmllib.py
"c:downloadsdiveintopythonhtmltocindex.html"</span>
data: ’nn’
start tag: <html lang="en" >
data: ’n ’
start tag: <head>
data: ’n ’
start tag: <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1" >
data: ’n n ’
start tag: <title>
data: ’Dive Into Python’
end tag: </title>
data: ’n ’
start tag: <link rel="stylesheet" href="diveintopython.css" type="text/css" >
data: ’n ’
168 ROZDZIAŁ 9. PRZETWARZANIE HTML-A
[...ciach...]
Taki jest plan reszty tego rozdziału:
• Dziedziczymy po SGMLParser, aby stworzyc klasy, które wydobywaja interesujace
dane z dokumentu HTML.
• Dziedziczymy po SGMLParser, aby stworzyc podklase BaseHTMLProcessor, która
nadpisuje wszystkie 8 metod obsługi i wykorzystujemy je, aby zrekonstruowac
oryginalny dokument HTML z otrzymywanych kawałków.
• Dziedziczymy po BaseHTMLProcessor, aby utworzyc Dialectizer, który dodaje
kilka metod w celu specjalnego przetworzenia okreslonych znaczników HTML.
Ponadto nadpisuje metode handle data, aby zapewnic mozliwosc przetwarzania
bloków tekstowych pomiedzy znacznikami HTML.
• Dziedziczymy po Dialectizer, aby stworzyc klasy, które definiuja zasady przetwarzania
tekstu wykorzystane w Dialectizer.handle data.
• Piszemy zestaw testów, które korzystaja z prawdziwej strony internetowej, http:
//diveintopython.org/, i ja przetwarzaja.
Przy okazji dowiemy sie, czym jest locals i globals, a takze jak formatowac
łancuchy znaków za pomoca słowników.
9.3. WYCIAGANIE DANYCH Z DOKUMENTU HTML 169
9.3 Wyciaganie danych z dokumentu HTML
Wyciaganie danych z dokumentu HTML
Aby wyciagnac dane z dokumentu HTML, tworzymy podklase klasy SGMLParser i
definiujemy dla encji lub kazdego znacznika, który nas interesuje, odpowiednia metode.
Pierwszym krokiem do wydobycia danych z dokumentu HTML jest zdobycie jakiegos
dokumentu. Jesli posiadamy jakis dokument HTML na swoim twardym dysku,
mozemy wykorzystac funkcje do obsługi plików, aby go odczytac, jednak prawdziwa
zabawa rozpoczyna sie, gdy wezmiemy HTML z istniejacej strony internetowej.
Przykład 8.5 Moduł urllib
>>> import urllib #(1)
>>> sock = urllib.urlopen("http://diveintopython.org/") #(2)
>>> htmlSource = sock.read() #(3)
>>> sock.close() #(4)
>>> print htmlSource #(5)
<!DOCTYPE html
PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>Dive Into Python</title>
<link rel="stylesheet" href="diveintopython.css" type="text/css">
<link rev="made" href="mailto:f8dy@diveintopython.org">
<meta name="generator" content="DocBook XSL Stylesheets V1.52.2">
<meta name="description" content=" This book lives at .
If you’re reading it somewhere else, you may not have the latest version.">
<meta name="keywords" content="Python, Dive Into Python, tutorial,
object-oriented, programming,
documentation, book, free">
<link rel="alternate" type="application/rss+xml" title="RSS"
href="http://diveintopython.org/history.xml">
</head>
<body>
<table id="Header" width="100%" border="0" cellpadding="0" cellspacing="0" summary="">
<tr>
<td id="breadcrumb" colspan="6">&nbsp;</td>
</tr>
<tr>
<td colspan="3" id="logocontainer">
<h1 id="logo">Dive Into Python</h1>
<p id="tagline">Python from novice to pro</p>
</td>
[...ciach...]
1. Moduł urllib jest czescia standardowej biblioteki Pythona. Zawiera on funkcje
słuzace do pobierania informacji o danych, a takze pobierania samych danych z
170 ROZDZIAŁ 9. PRZETWARZANIE HTML-A
internetu na podstawie adresu URL (głównie strony internetowe).
2. Najprostszym sposobem wykorzystania urllib-a jest pobranie całego tekstu
strony internetowej przy pomocy funkcji urlopen. Otworzenie URL-a jest równie
proste, jak otworzenie pliku. Zwracana wartosc funkcji urlopen przypomina
normalny obiekt pliku i posiada niektóre analogiczne metody do obiektu pliku.
3. Najprostsza czynnoscia, która mozemy wykonac na obiekcie zwróconym przez
urlopen, jest wywołanie read. Metoda ta odczyta cały HTML strony internetowej
i zwróci go w postaci łancucha znaków. Obiekt ten posiada takze metode
readlines, która czyta tekst linia po linii, dodajac kolejne linie do listy.
4. Kiedy skonczymy prace na tym obiekcie, powinnismy go jeszcze zamknac za
pomoca close, podobnie jak normalny plik.
5. Mamy kompletny dokument HTML w postaci łancucha znaków, pobrany ze
strony domowej http://diveintopython.org/ i jestesmy przygotowani do tego, aby
go sparsowac.
Przykład 8.6 Wprowadzenie do urllister.py
from sgmllib import SGMLParser
class URLLister(SGMLParser):
def reset(self): #(1)
SGMLParser.reset(self)
self.urls = []
def start_a(self, attrs): #(2)
href = [v for k, v in attrs if k==’href’] #(3) (4)
if href:
self.urls.extend(href)
1. reset jest wywoływany przez metode init SGMLParser-a, a takze mozna
go wywołac recznie juz po utworzeniu instancji parsera. Zatem, jesli potrzebujemy
powtórnie zainicjalizowac instancje parsera, który był wczesniej uzywany,
zrobimy to za pomoca reset (nie przez init ). Nie ma potrzeby tworzenia
nowego obiektu.
2. Zawsze, kiedy parser odnajdzie znacznik <a>, wywoła metode start a. Znacznik
moze posiadac atrybut href, a takze inne jak na przykład name, czy title. Parametr
attrs jest lista krotek [(atrybut1, wartosc1), (atrybut2, wartosc2),
...]. Znacznik ten moze byc takze samym <a>, poprawnym (lecz bezuzytecznym)
znacznikiem HTML, a w tym przypadku attrs bedzie pusta lista.
3. Mozemy stwierdzic, czy znacznik <a> posiada atrybut href, za pomoca prostego
wielozmiennego wyrazenie listowego.
4. Porównywanie napisów (np. k==’href’) jest zawsze wrazliwe na wielkosc liter,
lecz w tym przypadku takie uzycie jest bezpieczne, poniewaz SGMLParser konwertuje
podczas tworzenia attrs nazwy atrybutów na małe litery.
9.3. WYCIAGANIE DANYCH Z DOKUMENTU HTML 171
Przykład 8.7 Korzystanie z urllister.py
>>> import urllib, urllister
>>> usock = urllib.urlopen("http://diveintopython.org/")
>>> parser = urllister.URLLister()
>>> parser.feed(usock.read()) #(1)
>>> usock.close() #(2)
>>> parser.close() #(3)
>>> for url in parser.urls: print url #(4)
toc/index.html
#download
#languages
toc/index.html
appendix/history.html
download/diveintopython-html-5.0.zip
download/diveintopython-pdf-5.0.zip
download/diveintopython-word-5.0.zip
download/diveintopython-text-5.0.zip
download/diveintopython-html-flat-5.0.zip
download/diveintopython-xml-5.0.zip
download/diveintopython-common-5.0.zip
[... ciach ...]
1. Wywołujemy metode feed zdefiniowana w SGMLParser, aby “nakarmic” parser
przekazujac mu kod HTML-a. Metoda ta przyjmuje łancuch znaków, którym w
tym przypadku bedzie wartosc zwrócona przez usock.read().
2. Podobnie jak pliki, powinnismy zamknac swoje obiekty URL, kiedy juz nie beda
ci potrzebne.
3. Powinienes takze zamknac obiekt parsera, lecz z innego powodu. Podczas czytania
danych przekazujemy je do parsera, lecz metoda feed nie gwarantuje, ze
wszystkie przekazane dane, zostały przetworzone. Parser moze te dane zbuforowac
i czekac na dalsza porcje danych. Kiedy wywołamy close, mamy pewnosc,
ze bufor zostanie oprózniony i wszystko zostanie całkowicie sparsowane.
4. Poniewaz parser został zamkniety, wiec parsowanie zostało zakonczone i parser.urls
zawiera liste wszystkich URL-i, do których linki zawiera dokument HTML. (Twoje
wyjscie moze wygladac inaczej, poniewaz z biegiem czasu linki mogły ulec zmianie.)
172 ROZDZIAŁ 9. PRZETWARZANIE HTML-A
9.4 Wprowadzenie do BaseHTMLProcessor.py
Wprowadzenie do BaseHTMLProcessor.py
SGMLParser nie tworzy niczego samodzielnie. On po prostu parsuje, parsuje i parsuje
i wywołuje metode dla kazdej interesujacej rzeczy jaka znajdzie, ale te metody
nie wykonuja niczego. SGMLParser jest konsumentem HTML-a: bierze HTML-a i rozkłada
go na małe, strukturalne czesci. Jak juz widzielismy w poprzednim podrozdziale,
mozemy dziedziczyc po klasie SGMLParser, aby zdefiniowac klasy, które przechwyca
poszczególne znaczniki i jakos to pozytecznie wykorzystaja, np. stworza liste odnosników
na danej stronie internetowej. Teraz pójdziemy krok dalej i zdefiniujemy klase,
która przechwyci wszystko, co zgłosi SGMLParser i zrekonstruuje kompletny dokument
HTML. Uzywajac terminologii technicznej nasza klasa bedzie producentem HTML-a.
BaseHTMLProcessor dziedziczy po SGMLParser i dostarcza 8 istotnych metod
obsługi: unknown starttag, unknown endtag, handle charref, handle entityref,
handle comment, handle pi, handle decl i handle data.
Przykład 8.8 Wprowadzenie do BaseHTMLProcessor.py
class BaseHTMLProcessor(SGMLParser):
def reset(self): #(1)
self.pieces = []
SGMLParser.reset(self)
def unknown_starttag(self, tag, attrs): #(2)
strattrs = "".join([’ %s="%s"’ % (key, value) for key, value in attrs])
self.pieces.append("<%(tag)s%(strattrs)s>" % locals())
def unknown_endtag(self, tag): #(3)
self.pieces.append("</%(tag)s>" % locals())
def handle_charref(self, ref): #(4)
self.pieces.append("&#%(ref)s;" % locals())
def handle_entityref(self, ref): #(5)
self.pieces.append("&%(ref)s" % locals())
if htmlentitydefs.entitydefs.has_key(ref):
self.pieces.append(";")
def handle_data(self, text): #(6)
self.pieces.append(text)
def handle_comment(self, text): #(7)
self.pieces.append("begin{comment} %(text)s end{comment}
" % locals())
def handle_pi(self, text): #(8)
self.pieces.append("<?%(text)s>" % locals())
def handle_decl(self, text):
9.4. WPROWADZENIE DO BASEHTMLPROCESSOR.PY 173
self.pieces.append("<!%(text)s>" % locals())
1. reset, wołany przez SGMLParser. init , inicjalizuje self.pieces jako pusta
liste przed wywołaniem metody klasy przodka. self.pieces jest atrybutem,
który bedzie przechowywał czesci konstruowanego dokumentu HTML. Kazda
metoda bedzie rekonstruowac HTML parsowany przez SGMLParser i kazda z tych
metod bedzie dodawac jakis tekst do self.pieces. Zauwazmy, ze self.pieces
jest lista. Moglibysmy ulec pokusie, aby zdefiniowac ten atrybut jako obiekt
łancucha znaków i po prostu dołaczac do niego kolejne kawałki tekstu. To takze
by działało, ale Python jest duzo bardziej wydajny pracujac z listami. 1
2. Poniewaz BaseHTMLProcessor nie definiuje zadnej metody dla poszczególnych
znaczników (jak np. metoda start a w URLLister), SGMLParser bedzie wywoływał
dla kazdego poczatkowego znacznika metode unknown starttag. Ta metoda
przyjmuje na wejsciu znacznik (argument tag) i liste par postaci nazwa atrybutu/
wartosc atrybutu (argument attrs), a nastepnie rekonstruuje oryginalnego
HTML-a i dodaje do self.pieces. Napis formatujacy jest tutaj nieco dziwny;
rozwikłamy to pózniej w tym rozdziale (a takze ta dziwnie wygladajaca funkcje
locals).
3. Rekonstrukcja znaczników koncowych jest duzo prostsza; po prostu pobieramy
nazwe znacznika i opakowujemy nawiasami ostrymi </...>.
4. Gdy SGMLParser napotka odwołanie znakowe wywołuje metode handle charref
i przekazuje jej sama wartosc odwołania. Jesli dokument HTML zawiera &#160;,
ref przyjmie wartosc 160. Rekonstrukcja oryginalnego kompletnego odwołania
znakowego wymaga po prostu dodania znaków &#...;.
5. Odwołanie do encji jest podobne do odwołania znakowego, ale nie zawiera znaku
kratki (#). Rekonstrukcja oryginalnego odwołania do encji wymaga dodania znaków
&;...;. (Własciwie, jak wskazał na to pewien czytelnik, jest to nieco bardziej
skomplikowane. Tylko niektóre standardowe encje HTML-a koncza sie znakiem
srednika; inne podobnie wygladajace encje juz nie. Na szczescie dla nas
zbiór standardowych encji HTML-a zdefiniowany jest w Pythonie w słowniku w
module o nazwie htmlentitydefs. Stad ta dodatkowa instrukcja if.)
6. Bloki tekstu sa po prostu dołaczane do self.pieces bez zadnych zmian, w
postaci dosłownej.
7. Komentarze HTML-a opakowywane sa znakami <!--...-->.
1 Powodem dla którego Python jest lepszy w pracy z listami niz napisami, jest fakt iz listy sa modyfikowalne
(mutable), a napisy sa niemodyfikowalne (immutable). Co oznacza, ze zwiekszeniem listy
jest dodanie do niej po prostu nowego elementu i zaktualizowanie indeksu. Natomiast poniewaz napis
nie moze byc zmieniony po utworzeniu, z reguły kod s = s + nowy utworzy całkowicie nowy napis
powstały z połaczenia oryginalnego napisu s i napisu nowy, a oryginalny napis zostanie zniszczony. To
wymaga wielu kosztownych operacji zarzadzania pamiecia, a wielkosc zaangazowanego wysiłku rosnie
wraz z długoscia napisu, a wiec wykonywanie kodu s = s + nowy w petli jest zabójcze.Wterminologii
technicznej dodanie n elementów do listy oznacza złozonosc O(n), podczas gdy dodanie n elementów
do napisu złozonosc O(n2). Z drugiej strony Python korzysta z prostej optymalizacji, polegajacej na
tym, ze jesli dany łancuch znaków posiada tylko jedno odwołanie, to nie tworzy nowego łancucha,
tylko rozszerza stary i akurat w tym przypadku byłoby nieco szybciej na łancuchach znaków (wówczas
złozonosc byłaby O(n)).
174 ROZDZIAŁ 9. PRZETWARZANIE HTML-A
8. Instrukcje przetwarzania wstawiane sa pomiedzy znakami <?...>.
Wazne Specyfikacja HTML-a wymaga, aby wszystkie nie-HTML-owe elementy
(jak np. JavaScript) były zawarte pomiedzy HTML-owymi znakami komentarza, ale
nie wszystkie strony internetowe robia to własciwie (a współczesne przegladarki internetowe
sa wyrozumiałe w tym wzgledzie). BaseHTMLProcessor natomiast nie jest
wyrozumiały; jesli skrypt jest osadzony niewłasciwie, bedzie on sparsowany tak, jakby
był HTML-em. Np. jesli skrypt zawiera znaki mniejszosci i równosci SGMLParser moze
błednie pomyslec, ze znalazł znaczniki i atrybuty. SGMLParser zawsze konwertuje
nazwy znaczników i atrybutów na małe znaki, co moze zepsuc działanie skryptu i
BaseHTMLProcessor zawsze otacza wartosci atrybutów znakami cudzysłowu (nawet
jesli oryginalny dokument HTML uzywał apostrofów lub nie uzywał niczego), co juz
na pewno zepsuje skrypt. Zawsze chron swój skrypt osadzony w HTML-u znakami
komentarza.
Przykład 8.9 BaseHTMLProcessor i jego metoda output
def output(self): #(1)
u"""Zwraca przetworzony HTML jako pojedynczy łancuch znaków"""
return "".join(self.pieces)
1. To jest jedyna metoda, która nie jest wołana przez klase przodka, czyli klase
SGMLParser. Poniewaz pozostałe metody umieszczaja swoje zrekonstruowane kawałki
HTML-a w self.pieces, ta funkcja jest potrzebna, aby połaczyc wszystkie
te kawałki w jeden napis. Gdyz jak juz wspomniano wczesniej Python jest
swietny w obsłudze list i z reguły mierny w obsłudze napisów, kompletny napis
wyjsciowy tworzony jest tylko wtedy, gdy ktos o to wyraznie poprosi.
Materiały dodatkowe
• W3C omawia odwołania znakowe i encje.
• Python Library Reference potwierdza Twoje podejrzenia, iz moduł htmlentitydefs
jest dokładnie tym na co wyglada.
9.5. LOCALS I GLOBALS 175
9.5 locals i globals
locals i globals
Odejdzmy teraz na minutke od przetwarzania HTML-a. Porozmawiajmy o tym, jak
Python obchodzi sie ze zmiennymi. Python posiada dwie wbudowane funkcje, locals
i globals, które pozwalaja nam uzyskac w słownikowy sposób dostep do zmiennych
lokalnych i globalnych.
Pamietasz locals? Pierwszy raz moglismy ja zobaczyc tutaj:
def unknown_starttag(self, tag, attrs):
strattrs = "".join([’ %s="%s"’ % (key, value) for key, value in attrs])
self.pieces.append("<%(tag)s%(strattrs)s>" % locals())
Nie, czekaj, nie mozesz jeszcze sie uczyc o locals. Najpierw, musisz nauczyc sie,
czym sa przestrzenie nazw. Przedstawimy teraz troche suchego materiału, lecz waznego,
dlatego tez zachowaj uwage.
Python korzysta z czegos, co sie nazywa przestrzenia nazw (ang. namespace), aby
sledzic zmienne. Przestrzen nazw jest własciwie słownikiem, gdzie kluczami sa nazwy
zmiennych, a wartosciami słownika sa wartosci tych zmiennych. Mozemy dostac sie do
przestrzeni nazw, jak do Pythonowego słownika, co zreszta zobaczymy za chwilke.
Z dowolnego miejsca Pythonowego programu mamy dostep do kliku przestrzeni
nazw. Kazda funkcja posiada własna przestrzen nazw, nazywana lokalna przestrzenia
nazw, a która sledzi zmienne funkcji, właczajac w to jej argumenty i lokalnie zdefiniowane
zmienne. Kazdy moduł posiada własna przestrzen nazw, nazwana globalna
przestrzenia nazw, a która sledzi zmienne modułu, właczajac w to funkcje, klasy i
inne zaimportowane moduły, a takze zmienne zdefiniowane w tym module i stałe. Jest
takze wbudowana przestrzen nazw, dostepna z kazdego modułu, a która przechowuje
funkcje wbudowane i wyjatki.
Kiedy pewna linia kodu pyta sie o wartosc zmiennej x, Python przeszuka wszystkie
przestrzenie nazw, aby ja znalezc, w ponizszym porzadku:
1. lokalna przestrzen nazw – okreslona dla biezacej funkcji lub metody pewnej klasy.
Jesli funkcja definiuje jakas lokalna zmienna x, Python wykorzysta ja i zakonczy
szukanie.
2. przestrzeni nazw, w której dana funkcja została zagniezdzona i przestrzeniach
nazw, które znajduja sie wyzej w “zagniezdzonej” hierarchii.
3. globalna przestrzen nazw – okreslona dla biezacego modułu. Jesli moduł definiuje
zmienna lub klase o nazwie x, Python wykorzysta ja i zakonczy szukanie.
4. wbudowana przestrzen nazw – globalna dla wszystkich modułów. Poniewaz jest
to ostatnia deska ratunku, Python przyjmie, ze x jest nazwa wbudowanej funkcji
lub zmiennej.
Jesli Python nie znajdzie x w zadnej z tych przestrzeni nazw, poddaje sie i wyrzuca
wyjatek NameError z wiadomoscia “name ’x’ is not defined”, która zobaczylismy
w przykładzie 3.21, lecz nie jestesmy w stanie ocenic, jak Python zadziała, zanim dostaniemy
ten bład.
176 ROZDZIAŁ 9. PRZETWARZANIE HTML-A
Python korzysta z zagniezdzonych przestrzeni nazw (ang. nested scope). Jesli tworzymy
wewnatrz pewnej funkcji inna funkcje, Python stworzy nowa zagniezdzona przestrzen
nazw dla zagniezdzonej funkcji, która jednoczesnie bedzie lokalna przestrzenia
nazw, ale jednoczesnie bedziemy mogli korzystac z przestrzeni nazw funkcji, w której
dana funkcje zagniezdzamy i pozostałych przestrzeni nazw (globalnej i wbudowanej).
Zmieszałes sie? Nie panikuj! Jest to naprawde wypasne. Podobnie, jak wiele rzeczy
w Pythonie, przestrzenie nazw sa bezposrednio dostepne podczas wykonywania
programu. Jak? Do lokalnej przestrzeni nazw mamy dostep poprzez wbudowana funkcje
locals, a globalna (na poziomie modułu) przestrzen nazw jest dostepna poprzez
wbudowana funkcje globals.
Przykład 8.10 Wprowadzenie do locals
>>> def foo(arg): #(1)
... x = 1
... print locals()
...
>>> foo(7) #(2)
{’arg’: 7, ’x’: 1}
>>> foo(’bar’) #(3)
{’arg’: ’bar’, ’x’: 1}
1. Funkcja foo posiada dwie zmienne w swojej lokalnej przestrzeni nazw: arg, której
wartosc jest przekazana do funkcji, a takze x, która jest zdefiniowana wewnatrz
funkcji.
2. locals zwraca słownik par nazwa/wartosc. Kluczami słownika sa nazwy zmiennych
w postaci napisów. Wartosciami słownika sa biezace wartosci tych zmiennych.
Zatem wywołujac foo z 7, wypiszemy słownik zawierajacy dwie lokalne
zmienne tej funkcji, czyli arg (o wartosci 7) i x (o wartosci 1).
3. Pamietaj, Python jest dynamicznie typowany, dlatego tez mozemy w prosty sposób
jako argument arg przekazac napis. Funkcja (a takze wywołanie locals)
beda nadal działac jak nalezy. locals działa z wszystkimi zmiennymi dowolnych
typów danych.
To co locals robi dla lokalnej (nalezacej do funkcji) przestrzeni nazw, globals
robi dla globalnej (modułu) przestrzeni nazw. globals jest bardziej ekscytujace, poniewaz
przestrzen nazw modułu jest bardziej pasjonujaca 2. Przestrzen nazw modułu
nie tylko przechowuje zmienne i stałe na poziomie tego modułu, lecz takze funkcje i
klasy zdefiniowane w tym module. Ponadto dołaczone do tego jest cokolwiek, co zostało
zaimportowane do tego modułu.
Pamietasz róznice miedzy from module import, a import module? Za pomoca
import module, zaimportujemy sam moduł, który zachowa własna przestrzen nazw,
a to jest przyczyna, dlaczego musimy odwołac sie do nazwy modułu, aby dostac sie
do jakiejs funkcji lub atrybutu (piszac module.function). Z kolei za pomoca from
module import rzeczywiscie importujemy do własnej przestrzeni nazw okreslona funkcje
i atrybuty z innego modułu, a dzieki temu odwołujemy sie do niego bezposrednio,
2To zdanie za wiele nie wnosi.
9.5. LOCALS I GLOBALS 177
bez wskazywania modułu, z którego one pochodza. Dzieki funkcji globals mozemy
zobaczyc, ze rzeczywiscie tak jest.
Spójrzmy na ponizszy blok kodu, który znajduje sie na dole BaseHTMLProcessor.
py.
Przykład 8.11 Wprowadzenie do globals
if __name__ == "__main__":
for k, v in globals().items(): #(1)
print k, "=", v
1. Na wypadek gdyby wydawało Ci sie to straszne, to pamietaj, ze widzielismy to
juz wczesniej. Funkcja globals zwraca słownik, po którym nastepnie iterujemy
słownik wykorzystujac metode items i wielozmienne przypisanie. Jedyna nowa
rzecza jest funkcja globals.
Teraz, uruchamiajac skrypt z linii polecen otrzymamy takie wyjscie (twoje wyjscie
moze sie nieco róznic, jest zalezne od systemu i miejsca instalacji Pythona):
c:docbookdippy> python BaseHTMLProcessor.py
SGMLParser = sgmllib.SGMLParser #(1)
htmlentitydefs = <module ’htmlentitydefs’ from ’C:Python23libhtmlentitydefs.py’> #(2)
BaseHTMLProcessor = __main__.BaseHTMLProcessor #(3)
__name__ = __main__ #(4)
[...ciach ...]
1. SGMLParser został zaimportowany z sgmllib, wykorzystujac from module import.
Oznacza to, ze został zaimportowany bezposrednio do przestrzeni nazw modułu
i w tym tez miejscu jest.
2. W przeciwienstwie do SGMLParsera, htmlentitydefs został zaimportowany wykorzystujac
instrukcje import. Oznacza to, ze moduł htmlentitydefs sam w sobie
jest przestrzenia nazw, ale zmienna entitydefs wewnatrz htmlentitydefs
juz nie.
3. Moduł ten definiuje jedna klase, BaseHTMLProcessor i oto ona. Dodajmy, ze ta
wartosc jest klasa sama w sobie, a nie jakas specyficzna instancja tej klasy.
4. Pamietasz trik if name ? Kiedy uruchamiamy moduł (zamiast importowac
go z innego modułu), to wbudowany atrybut name ma specjalna wartosc,
" main ". Poniewaz uruchomilismy ten moduł jako skrypt z linii polecen, wartosc
name wynosi " main ", dlatego tez zostanie wykonany mały kod testowy,
który wypisuje globals.
Korzystajac z funkcji locals i globals mozemy pobrac dynamicznie wartosc dowolnej
zmiennej, dzieki przekazaniu nazwy zmiennej w postaci napisu. Funkcjonalnosc
ta jest analogiczna do getattr, która pozwala dostac sie do dowolnej funkcji, dzieki
przekazaniu jej nazwy w postaci napisu.
Ponizej pokazemy inna wazna róznice miedzy funkcjami locals i globals, a o
której powinienismy sie dowiedziec, zanim nas to ukasi. Jakkolwiek to i tak Ciebie
ukasi, ale przynajmniej bedziesz pamietał, ze była o tym mowa w tym podreczniku.
178 ROZDZIAŁ 9. PRZETWARZANIE HTML-A
Przykład 8.12 locals jest tylko do odczytu, a globals juz nie
def foo(arg):
x = 1
print locals() #(1)
locals()["x"] = 2 #(2)
print "x=",x #(3)
z = 7
print "z=",z
foo(3)
globals()["z"] = 8 #(4)
print "z=",z #(5)
1. Poniewaz foo zostało wywołane z argumentem 3, wiec zostanie wypisane {’arg’:
3, ’x’: 1}. Nie powinno to byc zaskoczeniem.
2. locals jest funkcja zwracajaca słownik i w tym miejscu zmieniamy wartosc w
tym słowniku. Mozemy myslec, ze wartosc zmiennej x zostanie zmieniona na 2,
jednak tak nie bedzie. locals własciwie nie zwraca lokalnej przestrzeni nazw,
zwraca jego kopie. Zatem zmieniajac ja, nie zmieniamy wartosci zmiennych w
lokalnej przestrzeni nazw.
3. Zostanie wypisane x= 1, a nie x= 2.
4. Po tym, jak zostalismy poparzeni przez locals, mozemy myslec, ze ta operacja
nie zmieni wartosci z, ale w rzeczywistosci zmieni.Wskutek wewnetrznych róznic
implementacyjnych 3, globals zwraca aktualna, globalna przestrzen nazw, a
nie jej kopie; całkowicie odwrotne zachowanie w stosunku do locals. Tak wiec
dowolna zmiana zwróconego przez globals słownika bezposrednio wpływa na
zmienne globalne.
5. Wypisze z= 8, a nie z= 7.
3Nie bedziemy sie wdawac w szczegóły
9.6. FORMATOWANIE NAPISÓW W OPARCIU O SŁOWNIKI 179
9.6 Formatowanie napisów w oparciu o słowniki
Formatowanie napisów w oparciu o słowniki
Dlaczego uczylismy sie na temat funkcji locals i globals? Poniewaz teraz mozemy
sie nauczyc formatowania napisów w oparciu o słowniki. Jak juz mówilismy, regularne
formatowanie napisów umozliwia w łatwy sposób wstawianie wartosci do napisów.
Wartosci sa wyszczególnione w krotce i w odpowiednim porzadku wstawione do napisu,
gdzie wystepuje pole formatujace. O ile jest to skuteczne, nie zawsze tworzy kod łatwy
do czytania, zwłaszcza, gdy zostaje wstawianych wiele wartosci. Zeby zrozumiec o co
chodzi, nie wystarczy po prostu jednorazowo przesledzic napis; trzeba ciagle skakac
miedzy czytanym napisem, a czytana krotka wartosci.
Tutaj mamy alternatywna forme formatowania napisu, a która zamiast krotek wykorzystuje
słowniki.
Przykład 8.13 Formatowanie napisów w oparciu o słowniki
>>> params = {"server":"mpilgrim", "database":"master", "uid":"sa", "pwd":"secret"}
>>> "%(pwd)s" % params #(1)
’secret’
>>> "%(pwd)s nie jest poprawnym hasłem dla %(uid)s" % params #(2)
’secret nie jest poprawnym hasłem dla sa’
>>> "%(database)s of mind, %(database)s of body" % params #(3)
’master of mind, master of body’
1. Zamiast korzystac z krotki wartosci, formujemy napis formatujacy, który korzysta
ze słownika params. Ponadto zamiast prostego pola %s w napisie, pole
zawiera nazwe w nawiasach okragłych. Nazwa ta jest wykorzystana jako klucz w
słowniku params i zostaje zastapione odpowiednia wartoscia, secret, w miejscu
wystapienia pola %(pwd)s.
2. Takie formatowanie moze posiadac dowolna liczbe odwołan do kluczy. Kazdy
klucz musi istniec w podanym słowniku, poniewaz inaczej formatowanie zakonczy
sie niepowodzeniem i zostanie rzucony wyjatek KeyError.
3. Mozemy nawet wykorzystac ten sam klucz kilka razy. Kazde wystapienie zostanie
zastapione odpowiednia wartoscia.
Zatem dlaczego uzywac formatowania napisu w oparciu o słowniki? Moze to wygladac
na nadmierne wmieszanie słownika z kluczami i wartosciami, aby wykonac proste
formatowanie napisu.Wrzeczywistosci jest bardzo przydatne, kiedy juz sie ma słownik
z kluczami o sensownych nazwach i wartosciach, jak np. locals.
Przykład 8.14 Formatowanie napisu w BaseHTMLProcessor.py
def handle_comment(self, text):
self.pieces.append("<!--%(text)s-->" % locals()) #(1)
1. Formatowanie za pomoca słowników jest powszechnie uzywane z wbudowana
funkcje locals. Oznacza to, ze mozemy wykorzystywac nazwy zmiennych lokalnych
wewnatrz napisu formatujacego (w tym przypadku text, który został
przykazany jako argument do metody klasy) i kazda nazwa zmiennej zostanie
180 ROZDZIAŁ 9. PRZETWARZANIE HTML-A
zastapiona jej wartoscia. Jesli text przechowuje wartosc ’Poczatek stopki’,
formatowany napis «!--%(text)s-->"% locals() zostanie wygenerowany jako
’<!--Poczatek stopki-->’.
Przykład 8.15 Wiecej formatowania opartego na słownikach
def unknown_starttag(self, tag, attrs):
strattrs = "".join([’ %s="%s"’ % (key, value) for key, value in attrs]) #(1)
self.pieces.append("<%(tag)s%(strattrs)s>" % locals()) #(2)
1. Kiedy metoda ta zostaje wywołana, attrs jest lista krotek postaci klucz/wartosc,
podobnie jak zwrócona wartosc metody słownika items, a to oznacza, ze
mozemy wykorzystac wielozmienne przypisanie, aby wykonac na niej iteracje.
Powinnismy juz byc zaznajomieni z tymi operacjami, ale jest tu tego troche
duzo, wiec przesledzmy je po kolei:
(a) Przypuscmy, ze attrs wynosi [(’href’, ’index.html’), (’title’, ’Idz do strony
domowej’)].
(b) Wpierwszym przebiegu odwzorowywania listy, key przyjmie wartosc ’href’,
a value wezmie wartosc ’index.html’.
(c) Formatowanie napisu ’ %s=‘‘%s’’’ % (key, value) przekształci sie na ’
href=‘‘index.html’’’. Napis ten bedzie pierwszym elementem zwróconej
listy.
(d) W drugim przebiegu, key przyjmie wartosc ’title’, a value wartosc ’Idz
do strony domowej’.
(e) Formatowanie napisu przekształci to na ’ title=‘‘Idz do strony domowej’’’.
(f) Po wykonaniu wyrazenia listowego, zwrócona lista bedzie przechowywała te
dwa wygenerowane napisy, a strattrs bedzie połaczeniem obydwu tych elementów,
czyli bedzie przechowywał ’ href=‘‘index.html’’ title=‘‘Go
to home page’’’.
2. Teraz formatujac napis za pomoca słownika, wstawiamy wartosc zmiennej tag
i strattrs do napisu. Zatem jesli tag wynosił ’a’, w ostatecznosci otrzymamy
wynik ’<a href="index.html"title="Idz do strony domowej’»’ i to nastepnie
dodajemy do self.pieces.
Korzystanie z słownikowego formatowania napisu i funkcji locals jest wygodnym
sposobem, aby tworzyc czytelniejsze skomplikowane wyrazenia listowe, lecz trzeba zapłacic
pewna cene. Jest tutaj drobny narzut wydajnosci zwiazany z wywołaniem funkcji
locals, poniewaz locals wykonuje kopie lokalnej przestrzeni nazw.
9.7. DODAWANIE CUDZYSŁOWÓW DO WARTOSCI ATRYBUTÓW 181
9.7 Dodawanie cudzysłowów do wartosci atrybutów
Dodawanie cudzysłowów do wartosci atrybutów
Dosc powszechnym pytaniem na comp.lang.python jest “Mam kilka dokumentów
HTML z wartosciami atrybutów bez cudzysłowów i chciałbym odpowiednio te cudzysłowy
dodac. Jak moge to zrobic?” 4 (Przewaznie wynika to z dołaczenia do projektu
nowego kierownika, bedacego wyznawca HTML-owych standardów i bezwzglednie wymagajacego,
aby wszystkie strony bezbłednie przechodziły kontrole HTML-owych walidatorów.
Wartosci atrybutów bez cudzysłowów sa powszechnym naruszeniem HTMLwego
standardu.) Niezaleznie od powodu, uzupełnienie cudzysłowów jest łatwe przy
pomocy klasy BaseHTMLProcessor.
BaseHTMLProcessor konsumuje HTML-a (poniewaz jest potomkiem klasy SGMLParser)
i produkuje równowazny HTML, ale ten wyjsciowy HTML nie jest identyczny z wejsciowym.
Znaczniki i nazwy atrybutów zostana zapisane małymi literami, nawet jesli
wczesniej były duzymi lub wymieszanymi, a wartosci atrybutów zostana zamkniete w
podwójnych cudzysłowach, nawet jesli wczesniej były otoczone pojedynczymi cudzysłowami
lub nie miały zadnych cudzysłowów. To jest taki efekt uboczny, z którego
mozemy tu skorzystac.
Przykład 8.16 Dodawanie cudzysłowów do wartosci atrybutów
>>> htmlSource = """ #(1)
... <html>
... <head>
... <title>Test page</title>
... </head>
... <body>
... <ul>
... <li><a href=index.html>Strona główna</a></li>
... <li><a href=toc.html>Spis tresci</a></li>
... <li><a href=history.html>Historia zmian</a></li>
... </body>
... </html>
... """
>>> from BaseHTMLProcessor import BaseHTMLProcessor
>>> parser = BaseHTMLProcessor()
>>> parser.feed(htmlSource) #(2)
>>> print parser.output() #(3)
<html>
<head>
<title>Test page</title>
</head>
<body>
<ul>
<li><a href="index.html">Strona główna</a></li>
4 No dobra, to nie jest az tak powszechne pytanie. Nie jest czestsze niz “Jakiego edytora powinienem
uzywac do pisania kodu w Pythonie?” (odpowiedz: Emacs) lub “Python jest lepszy czy gorszy od
Perla?” (odpowiedz: “Perl jest gorszy od Pythona, poniewaz ludzie chcieli aby był gorszy.” -Larry
Wall, 10/14/1998). Jednak pytania o przetwarzanie HTML-a pojawiaja sie w takiej czy innej formie
około raz na miesiac i wsród tych pytan, to jest dosc popularne.
182 ROZDZIAŁ 9. PRZETWARZANIE HTML-A
<li><a href="toc.html">Spis tresci</a></li>
<li><a href="history.html">Historia zmian</a></li>
</body>
</html>
1. Zauwazmy, ze wartosci atrybutów href w znacznikach <a> nie sa ograniczone
cudzysłowami. (Jednoczesnie zauwazmy, ze uzywamy potrójnych cudzysłowów
do czegos innego niz notki dokumentacyjnej i to bezposrednio w IDE. Sa one
bardzo uzyteczne.)
2. “Karmimy” parser.
3. Uzywajac funkcji output zdefiniowanej w klasie BaseHTMLProcessor, otrzymujemy
wyjscie jako pojedynczy kompletny łancuch znaków ze wszystkimi wartosciami
atrybutów w cudzysłowach. Pomyslmy, jak wiele własciwie sie tutaj
działo: SGMLParser sparsował cały dokument HTML, podzielił go na znaczniki,
odwołania, dane tekstowe itp.; BaseHTMLProcessor uzył tych elementów do zrekonstruowania
czesci HTML-a (które nadal sa składowane w parser.pieces,
jesli chcesz je zobaczyc); na koncu wywołalismy parser.output, która to metoda
połaczyła wszystkie czesci HTML-a w jeden napis.
9.8. WPROWADZENIE DO DIALECT.PY 183
9.8 Wprowadzenie do dialect.py
Dialectizer jest prostym (i niezbyt madrym) potomkiem klasy BaseHTMLProcessor.
Dokonuje on na bloku tekstu serii podstawien, ale wszystko co znajduje sie wewnatrz
bloku <pre>...</pre> pozostawia niezmienione.
Aby obsłuzyc bloki <pre> definiujemy w klasie Dialectizer metody: start pre i
end pre.
Przykład 8.17 Obsługa okreslonych znaczników
def start_pre(self, attrs): #(1)
self.verbatim += 1 #(2)
self.unknown_starttag("pre", attrs) #(3)
def end_pre(self): #(4)
self.unknown_endtag("pre") #(5)
self.verbatim -= 1 #(6)
1. start pre jest wywoływany za kazdym razem, gdy SGMLParser znajdzie znacznik
<pre> w zródle HTML-a. (Za chwile zobaczymy dokładnie, jak to sie dzieje.)
Ta metoda przyjmuje jeden parametr: attrs, który zawiera atrybuty znacznika
(jesli jakies sa). attrs jest lista krotek postaci klucz/wartosc, taka sama jaka
przyjmuje unknown starttag.
2. W metodzie reset, inicjalizujemy atrybut, który słuzy jako licznik znaczników
<pre>. Za kazdym razem, gdy natrafiamy na znacznik <pre>, zwiekszamy licznik,
natomiast gdy natrafiamy na znacznik </pre> zmniejszamy licznik. (Moglibysmy
tez uzyc po prostu flagi i ustawiac ja na wartosc True, a nastepnie False, ale
nasz sposób jest równie łatwy, a dodatkowo obsługujemy dziwny (ale mozliwy)
przypadek zagniezdzonych znaczników <pre>.) Za chwile zobaczymy jak mozna
wykorzystac ten licznik.
3. To jest ta jedyna akcja wykonywana dla znaczników <pre>. Przekazujemy tu liste
atrybutów do metody unknown starttag, aby wykonała ona domyslna akcje.
4. Metoda end pre jest wywoływana za kazdym razem, gdy SGMLParser znajdzie
znacznik </pre>. Poniewaz znaczniki koncowe nie moga miec atrybutów, ta metoda
nie przyjmuje zadnych parametrów.
5. Po pierwsze, chcemy wykonac domyslna akcje dla znacznika koncowego.
6. Po drugie, zmniejszamy nasz licznik, co sygnalizuje nam zamkniecie bloku <pre>.
W tym momencie warto sie zagłebic nieco bardziej w klase SGMLParser. Wielokrotnie
stwierdzalismy, ze SGMLParser wyszukuje i wywołuje specyficzne metody dla
kazdego znacznika, jesli takowe istnieja. Na przykład własnie zobaczylismy definicje
metod start pre i end pre do obsługi <pre> i </pre>. Ale jak to sie dzieje? No cóz,
to nie jest zadna magia. To jest po prostu dobry kawałek kodu w Pythonie.
184 ROZDZIAŁ 9. PRZETWARZANIE HTML-A
Przykład 8.18 SGMLParser
def finish_starttag(self, tag, attrs): #(1)
try:
method = getattr(self, ’start_’ + tag) #(2)
except AttributeError: #(3)
try:
method = getattr(self, ’do_’ + tag) #(4)
except AttributeError:
self.unknown_starttag(tag, attrs) #(5)
return -1
else:
self.handle_starttag(tag, method, attrs) #(6)
return 0
else:
self.stack.append(tag)
self.handle_starttag(tag, method, attrs)
return 1 #(7)
def handle_starttag(self, tag, method, attrs):
method(attrs) #(8)
1. W tym momencie SGMLParser znalazł juz poczatkowy znacznik i sparsował liste
atrybutów. Ostatnia rzecz jaka została do zrobienia, to ustalenie czy istnieje
specjalna metoda obsługi dla tego znacznika lub czy powinnismy skorzystac z
metody domyslnej (unknown starttag).
2. Za “magia” klasy SGMLParser nie kryje sie nic wiecej niz nasz stary przyjaciel
getattr. Moze jeszcze tego wczesniej nie zauwazylismy, ale getattr poszukuje
metod zdefiniowanych zarówno w danym obiekcie jak i w jego potomkach. Tutaj
obiektem jest self, czyli biezaca instancja. A wiec jesli tag przyjmie wartosc
’pre’, to wywołanie getattr bedzie poszukiwało metody start pre w biezacej
instancji, która jest instancja klasy Dialectizer.
3. Metoda getattr rzuca wyjatek AttributeError, jesli metoda, której szuka nie
istnieje w danym obiekcie (oraz w zadnym z jego potomków), ale to jest w porzadku,
poniewaz wywołanie getattr zostało otoczone blokiem try...except i
wyjatek AttributeError zostaje przechwycony.
4. Poniewaz nie znalezlismy metody start xxx, sprawdzamy jeszcze metode do xxx
zanim sie poddamy. Ta alternatywna grupa metod generalnie słuzy do obsługi
znaczników samodzielnych, jak np. <br>, które nie maja znacznika koncowego.
Jednak mozemy uzywac metod z obu grup. Jak widac SGMLParser sprawdza
obie grupy dla kazdego znacznika. (Jednak nie powinienes definiowac obu metod
obsługi start xxx i do xxx dla tego samego znacznika; wtedy i tak zostanie
wywołana tylko start xxx.)
5. Nastepny wyjatek AttributeError, który oznacza, ze kolejne wywołanie getattr
odnoszace sie doe do xxx takze zawiodło. Poniewaz nie znalezlismy ani metody
start xxx, ani do xxx dla tego znacznika, przechwytujemy wyjatek i wycofujemy
sie do metody domyslnej unknown starttag.
9.8. WPROWADZENIE DO DIALECT.PY 185
6. Pamietajmy, bloki try...except moga miec takze klauzule else, która jest
wywoływana jesli nie wystapi zaden wyjatek wewnatrz bloku try...except.
Logiczne, to oznacza, ze znalezlismy metode do xxx dla tego znacznika, a wiec
wywołujemy ja.
7. A tak przy okazji nie przejmuj sie tymi róznymi zwracanymi wartosciami; teoretycznie
one cos oznaczaja, ale w praktyce nie sa wykorzystywane. Nie martw
sie takze tym self.stack.append(tag); SGMLParser sledzi samodzielnie, czy
znaczniki poczatkowe sa zrównowazone z odpowiednimi znacznikami koncowymi,
ale jednoczesnie do niczego tej informacji nie wykorzystuje. Teoretycznie moglibysmy
wykorzystac ten moduł do sprawdzania, czy znaczniki sa całkowicie
zrównowazone, ale prawdopodobnie nie warto i wykracza to poza zakres tego
rozdziału. W tej chwili masz lepsze powody do zmartwienia.
8. Metody start xxx i do xxx nie sa wywoływane bezposrednio. Znacznik tag,
metoda method i atrybuty attrs sa przekazywane do tej funkcji, czyli do
handle starttag, aby klasy potomne mogły ja nadpisac i tym samym zmienic
sposób obsługi znaczników poczatkowych. Nie potrzebujemy az tak niskopoziomowej
kontroli, a wiec pozwalamy tej metodzie zrobic swoje, czyli wywołac
metody (start xxx lub do xxx) z lista atrybutów. Pamietajmy, argument
method jest funkcja zwrócona przez getattr, a funkcje sa obiektami. (Wiem,
wiem, zaczynasz miec dosc słuchania tego w kółko. Przestaniemy o tym powtarzac,
jak tylko zabraknie sposobów na wykorzystanie tego faktu.) Tutaj obiekt
funkcji method jest przekazywany do metody jako argument, a ta metoda wywołuje
te funkcje. W tym momencie nie istotne jest co to jest za funkcja, jak sie
nazywa, gdzie jest zdefiniowana; jedyna rzecz jaka jest wazna, to to ze jest ona
wywoływana z jednym argumentem, attrs.
A teraz wrócmy do naszego poczatkowego programu: Dialectizer. Gdy go zostawilismy,
bylismy w trakcie definiowania metod obsługi dla znaczników <pre> i </pre>.
Pozostała juz tylko jedna rzecz do zrobienia, a mianowicie przetworzenie bloków tekstu
przy pomocy zdefiniowanych podstawien. W tym celu musimy nadpisac metode
handle data.
Przykład 8.19 Nadpisanie metody handle data
def handle_data(self, text): #(1)
self.pieces.append(self.verbatim and text or self.process(text)) #(2)
1. Metoda handle data jest wywoływana z tylko jednym argumentem, tekstem do
przetworzenia.
2. W klasie nadrzednej BaseHTMLProcessor metoda handle data po prostu dodaje
tekst do wyjsciowego bufora self.pieces. Tutaj zasada działania jest
tylko troche bardziej skomplikowana. Jesli jestesmy w bloku <pre>...</pre>,
self.verbatim bedzie miało jakas wartosc wieksza od 0 i tekst trafi do bufora
wyjsciowego nie zmieniony. W przeciwnym razie wywołujemy oddzielna metode
do wykonania podstawien i rezultat umieszczamy w buforze wyjsciowym. Wykorzystujemy
tutaj jednolinijkowiec, który wykorzystuje sztuczke and-or.
Juz jestes blisko całkowitego zrozumienia Dialectizer. Ostatnim brakujacym ogniwem
jest sam charakter podstawien w tekscie. Jesli znasz Perla, to wiesz, ze kiedy
186 ROZDZIAŁ 9. PRZETWARZANIE HTML-A
wymagane sa kompleksowe zmiany w tekscie, to jedynym prawdziwym rozwiazaniem
sa wyrazenia regularne. Klasy w dalszej czesci dialect.py definiuje serie wyrazen regularnych,
które operuja na tekscie pomiedzy znacznikami HTML. My juz mamy przeanalizowany
cały rozdział o wyrazeniach regularnych. Zapewne nie masz ochoty znowu
mozolic sie z wyrazeniami regularnymi, prawda? Juz wystarczajaco duzo sie nauczylismy,
jak na jeden rozdział.
9.9. PRZETWARZANIE HTML-A - WSZYSTKO RAZEM 187
9.9 Przetwarzanie HTML-a - wszystko razem
Wszystko razem
Nadszedł czas, aby połaczyc w całosc wiedze, która zdobylismy do tej pory.
Przykład 8.20 Funkcja translate, czesc 1
def translate(url, dialectName="chef"): #(1)
import urllib #(2)
sock = urllib.urlopen(url) #(3)
htmlSource = sock.read()
sock.close()
1. Funkcja translate przyjmuje opcjonalny argument dialectName, który jest łancuchem
znaków okreslajacym uzywany dialekt. Zaraz zobaczymy, jak to jest wykorzystywane.
2. Moment, tam jest wazna instrukcja w tej funkcji! Jest to w pełni dozwolone w
Pythonie działanie. Instrukcja import uzywalismy zwykle na samym poczatku
programu, aby zaimportowany moduł był dostepny w dowolnym miejscu. Ale
mozemy takze importowac moduły w samej funkcji, przez co beda one dostepne
tylko z jej poziomu. Jezeli jakiegos moduły potrzebujemy uzyc tylko w jednej
funkcji, jest to najlepszy sposób aby zachowac modularnosc twojego programu.
(Docenisz to, gdy okaze sie, ze twój weekendowy hack wyrósł na wazace 800 linii
dzieło sztuki, a ty własnie zdecydujesz sie podzielic to na mniejsze czesci).
3. Tutaj otwieramy połaczenie i do zmiennej htmlSource pobieramy zródło HTML
spod wskazanego adresu URL.
Przykład 8.21 Funkcja translate, czesc 2: coraz ciekawiej
parserName = "%sDialectizer" % dialectName.capitalize() #(1)
parserClass = globals()[parserName] #(2)
parser = parserClass() #(3)
1. capitalize jest metoda łancucha znaków, z która sie jeszcze nie spotkalismy;
zmienia ona pierwszy znak na wielka litere, a wszystkie pozostałe znaki na małe
litery. W połaczeniu z prostym formatowaniem napisu, nazwa dialektu zamieniana
jest na nazwe odpowiadajacej mu klasy. Jezeli dialectName ma wartosc
’chef’, parserName przyjmie wartosc ’ChefDialectizer’.
2. W tym miejscu mamy nazwe klasy (w zmiennej parserName) oraz dostep do
globalnej przestrzeni nazw, poprzez słownik globals(). Łaczac obie informacje
dostajemy referencje do klasy o okreslonej nazwie. (Pamietajmy, ze klasy
sa obiektami i moga byc przypisane do zmiennej, jak kazdy inny obiekt). Jezeli
parserName ma wartosc ’ChefDialectizer’, parserClass bedzie klasa
ChefDialectizer.
3. Ostatecznie, majac obiekt klasy (parserClass) chcemy zainicjowac te klase.
Wiemy juz jak zrobic—po prostu wywołujemy klase w taki sposób, jakby była to
funkcja. Fakt, ze klasa jest przechowywana w lokalnej zmiennej nie robi zadnej
188 ROZDZIAŁ 9. PRZETWARZANIE HTML-A
róznicy, po prostu wywołujemy lokalna zmienna jak funkcje, i na wyjsciu wyskakuje
instancja klasy. Jezeli parserClass jest klasa ChefDialectizer, parser
bedzie instancja klasy ChefDialectizer.
Zastanawiasz sie, poniewaz istnieja tylko 3 klasy Dialectizer, dlaczego by nie
uzyc po prostu instrukcji case? (W porzadku, w Pythonie nie ma instrukcji case, ale
zawsze mozna uzyc serii instrukcji if). Z jednego powodu: elastycznosci programu.
Funkcja translate nie ma pojecia, jak wiele zdefiniowalismy podklas Dialectizera.
Wyobrazmy sobie, ze definiujemy jutro nowa klase FooDialectizer — funkcja
translate zadziała bez przeróbek.
Nawet lepiej – wyobrazmy sobie, ze umieszczasz klase FooDialectizer w osobnym
module i importujesz ja poprzez from module import. Jak wczesniej moglismy sie
przekonac, taka operacja dołaczy to do globals(), wiec funkcja translate nadal
bedzie działac prawidłowo bez koniecznosci dokonywania modyfikacji, nawet wtedy,
gdy FooDialectizer znajdzie sie w oddzielnym pliku.
Teraz wyobrazmy sobie, ze nazwa dialektu pochodzi skads z poza programu, moze
z bazy danych lub z wartosci wprowadzonej przez uzytkownika. Mozemy uzyc jakakolwiek
ilosc pythonowych skryptów po stronie serwera, aby dynamicznie generowac
strony internetowe; taka funkcja mogłaby przekazac URL i nazwe dialektu (oba w
postaci łancucha znaków) w zapytania zadania strony internetowej, i zwrócic “przetłumaczona”
strone.
Na koniec wyobrazmy sobie framework Dialectizer z wbudowana obsługa pluginów.
Mozemy umiescic kazda podklase Dialectizer-a w osobnym pliku pozostawiajac
jedynie w pliku dialect.py funkcje translate. Jezeli zachowasz stały schemat
nazewnictwa klas, funkcja translate moze dynamicznie importowac potrzebna klase
z odpowiedniego pliku, jedynie na podstawie podanej nazwy dialektu. (Dynamicznego
importowanie omówimy to w dalszej czesci tego podrecznika). Aby dodac nowy dialekt,
wystarczy, ze utworzymy odpowiednio nazwany plik (np. foodialect.py zawierajacy
klase FooDialectizer) w katalogu z plug-inami. Wywołujac funkcje translate z nazwa
dialektu ’foo’, odnajdzie ona moduł foodialect.py i automatycznie zaimportuje
klase FooDialectizer.
Przykład 8.22 Funkcja translate, czesc 3
parser.feed(htmlSource) #(1)
parser.close() #(2)
return parser.output() #(3)
1. Po tym całym wyobrazaniu sobie, co robiło sie juz nudne, mamy funkcje feed,
która przeprowadza cała transformacje. Poniewaz całe zródło HTML-a mamy w
jednym łancuchu znaków, wiec funkcje feed wywołujemy tylko raz. Oczywiscie
mozemy wywoływac ja dowolna ilosc razy, a parser za kazdym razem przeprowadzi
transformacje. Na przykład, jezeli obawiamy sie o zuzycie pamieci (albo
wiemy, ze bedziemy parsowali naprawde wielkie strony HTML), mozemy umiescic
ta funkcje w petli, w której bedziemy odczytywał tylko kawałek HTML-a i
karmił nim parser. Efekty beda takie same.
2. Poniewaz funkcja feed wykorzystuje wewnetrzny bufor, powinnismy zawsze po
zakonczeniu operacji wywołac funkcje close() parsera (nawet jezeli przesłalismy
parserowi całosc za jednym razem). W przeciwnym wypadku mozemy stwierdzic,
ze w otrzymanym wyniku brakuje kilku ostatnich bajtów.
9.9. PRZETWARZANIE HTML-A - WSZYSTKO RAZEM 189
3. Pamietajmy, ze funkcja output, która zdefiniowalismy samodzielnie w klasie
BaseHTMLProcessor, łaczy wszystkie zbuforowane kawałki i zwraca całosc w
postaci pojedynczego łancucha znaków.
I własnie w taki sposób, “przetłumaczylismy” strone internetowa, podajac jedynie
jej adres URL i nazwe dialektu.
190 ROZDZIAŁ 9. PRZETWARZANIE HTML-A
9.10 Przetwarzanie HTML-a - podsumowanie
Podsumowanie
Python dostarcza potezne narzedzie do operowania na HTML-u—biblioteke sgmllib.py,
która obudowuje kod HTML w model obiektowy. Mozemy uzywac tego narzedzia na
wiele sposobów:
• parsujac HTML w poszukiwania specyficznych informacji
• gromadzac wyniki, np. tak jak to robi URL lister.
• modyfikujac strukture w dowolny sposób, np. dodawac cudzysłowy do atrybutów
• transformujac HTML w inny format, poprzez manipulowanie tekstem bez ruszania
znaczników, np. tak jak nasz Dialectizer
Po tych wszystkich przykładach, powinnismy umiec wykonywac wszystkie z tych
operacji:
• Uzywac odpowiednio locals() i globals(), aby dostac sie do przestrzeni nazw
• Formatowac łancuchy w oparciu o słowniki
Rozdział 10
Przetwarzanie XML-a
191
192 ROZDZIAŁ 10. PRZETWARZANIE XML-A
10.1 Przetwarzanie XML-a
Nurkujemy
Kolejne dwa rozdziały sa na temat przetwarzania XML-a w Pythonie. Bedzie to
przydatne, jesli juz wiesz, jak wygladaja dokumenty XML, a które sa wykonane ze
strukturalnych znaczników okreslajacych hierarchie elementów itp. Jesli nic z tego nie
rozumiesz, mozesz przeczytac cos na ten temat na Wikipedii.
Nawet jesli nie interesuje Ciebie temat XML-a i tak dobrze by było przeczytac te
rozdziały, poniewaz omawiaja one wiele waznych tematów jak pakiety, argumenty linii
polecen, a takze jak wykorzystywac getattr jako posrednik metod.
Bycie magistrem filozofii nie jest wymagane, chociaz jesli kiedys spotkalismy sie z
tekstami napisanymi przez Immanuel Kanta, lepiej zrozumiemy przykładowy program,
niz jesli byłbys specjalista w czyms przydatnym, jak informatyka.
Mamy dwa sposoby pracy z XML-em. Jeden jest nazywany SAX (Simple API for
XML), który działa w ten sposób, ze czyta przez chwile dokument XML i wywołuje dla
kazdego odnalezionego elementu odpowiednie metody. (Jesli przeczytalismy rozdział
8, powinno to wygladac znajomo, poniewaz w taki sposób pracuje moduł sgmllib.)
Inny jest nazywany DOM (Document Object Model ), a pracuje w ten sposób, ze jednorazowo
czyta cały dokument XML i tworzy wewnetrzna reprezentacje, wykorzystujac
klasy Pythona powiazane w strukture drzewa. Python posiada standardowe moduły do
obydwu sposobów parsowania, ale rozdział ten opisze tylko, jak wykorzystywac DOM.
Ponizej znajduje sie kompletny program Pythona, który generuje pseudolosowe
wyjscie oparte na gramatyce bezkontekstowej zdefiniowanej w formacie XML. Nie
przejmujmy sie, jesli nie zrozumielismy, co to znaczy. Bedziemy głebiej badac zarówno
wejscie programu, jak i jego wyjscie w tym i nastepnym rozdziale.
Przykład 9.1 kgp/kgp.py
u"""Generator Kanta dla Pythona
Generuje pseudofilozofie oparta na gramatyce bezkontekstowej
Uzycie: python kgp.py [options] [source]
Opcje:
-g ..., --grammar=... uzywa okreslonego pliku gramatyki lub adres URL
-h, --help wyswietla ten komunikat pomocy
-d wyswietla informacje debugowania podczas parsowania
Przykłady:
kgp.py generuje kilka akapitów z filozofia Kanta
kgp.py -g husserl.xml generuje kilka akapitów z filozofia Husserla
kpg.py "<xref id=’paragraph’/>" generuje akapit Kanta
kgp.py template.xml czyta template.xml, aby okreslic, co ma generowac
"""
from xml.dom import minidom
import random
import toolbox
10.1. NURKOWANIE 193
import sys
import getopt
debug = 0
class NoSourceError(Exception): pass
class KantGenerator(object):
u"""generuje pseudofilozofie oparta na gramatyce bezkontekstowej"""
def init (self, grammar, source=None):
self.loadGrammar(grammar)
self.loadSource(source and source or self.getDefaultSource())
self.refresh()
def load(self, source):
u"""wczytuje XML-owe zródłow wejscia, zwraca sparsowany dokument XML
- adres URL z plikiem XML ("http://diveintopython.org/kant.xml")
- nazwe lokalnego pliku XML ("~/diveintopython/common/py/kant.xml")
- standardowe wejscie ("-")
- biezacy dokument XML w postaci łancucha znaków
"""
sock = toolbox.openAnything(source)
xmldoc = minidom.parse(sock).documentElement
sock.close()
return xmldoc
def loadGrammar(self, grammar):
u"""wczytuje gramatyke bezkontekstowa"""
self.grammar = self. load(grammar)
self.refs = {}
for ref in self.grammar.getElementsByTagName("ref"):
self.refs[ref.attributes["id"].value] = ref
def loadSource(self, source):
u"""wczytuje zródło source"""
self.source = self. load(source)
def getDefaultSource(self):
u"""zgaduje domyslne zródło biezacej gramatyki
Domyslnym zródłem bedzie jeden z <ref>-ów, do którego nic sie
nie odwołuje. Moze brzmi to skomplikowanie, ale tak naprawde nie jest.
Przykład: Domyslnym zródłem dla kant.xml jest
"<ref id=’section’/>", poniewaz ’section’ jest jednym <ref>-em, który
nie jest nigdzie <xref>-em w gramatyce.
W wielu gramatykach, domyslne zródło bedzie tworzyło
najdłuzsze (i najbardziej interesujace) wyjscie.
"""
194 ROZDZIAŁ 10. PRZETWARZANIE XML-A
xrefs = {}
for xref in self.grammar.getElementsByTagName("xref"):
xrefs[xref.attributes["id"].value] = 1
xrefs = xrefs.keys()
standaloneXrefs = [e for e in self.refs.keys() if e not in xrefs]
if not standaloneXrefs:
raise NoSourceError, "can’t guess source, and no source specified"
return ’<xref id="%s"/>’ % random.choice(standaloneXrefs)
def reset(self):
u"""resetuje parser"""
self.pieces = []
self.capitalizeNextWord = 0
def refresh(self):
u"""resetuje bufor wyjsciowy, ponownie parsuje cały plik zródłowy
i zwraca wyjscie
Poniewaz parsowanie dosyc duzo korzysta z przypadkowosci, jest to
łatwy sposób, aby otrzymac nowe wyjscie bez potrzeby ponownego wczytywania
pliku gramatyki.
"""
self.reset()
self.parse(self.source)
return self.output()
def output(self):
u"""wyjsciowy, wygenerowany tekst"""
return "".join(self.pieces)
def randomChildElement(self, node):
u"""wybiera przypadkowy potomek wezła
Jest to uzyteczna funkcja wykorzystywana w do xref i do choice.
"""
choices = [e for e in node.childNodes
if e.nodeType == e.ELEMENT NODE]
chosen = random.choice(choices)
if debug:
sys.stderr.write(’%s available choices: %sn’ %
(len(choices), [e.toxml() for e in choices]))
sys.stderr.write(’Chosen: %sn’ % chosen.toxml())
return chosen
def parse(self, node):
u"""parsuje pojedynczy wezeł XML
Parsowany dokument XML (from minidom.parse) jest drzewem wezłów
złozonym z róznych typów. Kazdy wezeł reprezentuje instancje
odpowiadajacej jej klasy Pythona (Element dla znacznika, Text
10.1. NURKOWANIE 195
dla danych tekstowych, Document dla dokumentu). Ponizsze wyrazenie
konstruuje nazwe klasy opartej na typie wezła, który parsujemy
("parse Element" dla wezła o typie Element,
"parse Text" dla wezła o typie Text itp.), a nastepnie wywołuje te metody.
"""
parseMethod = getattr(self, "parse %s" % node. class . name )
parseMethod(node)
def parse Document(self, node):
u"""parsuje wezeł dokumentu
Wezeł dokument sam w sobie nie jest interesujacy (przynajmnie dla nas), ale
jego jedyne dziecko, node.documentElement jest: jest głównym wezłem
gramatyki.
"""
self.parse(node.documentElement)
def parse Text(self, node):
u"""parsuje wezeł tekstowy
Tekst wezła tekstowego jest zazwyczaj dodawany bez zmiany do wyjsciowego bufora.
Jedynym wyjatkiem jest to, ze <p class=’sentence’> ustawia flage, aby
pierwsza litere nastepnego słowa była wielka. Jesli ta flaga jest ustawiona,
pierwsza litere tekstu robimy wielka i resetujemy te flage.
"""
text = node.data
if self.capitalizeNextWord:
self.pieces.append(text[0].upper())
self.pieces.append(text[1:])
self.capitalizeNextWord = 0
else:
self.pieces.append(text)
def parse Element(self, node):
u"""parsuje element
XML-owy element odpowiada biezacemu znacznikowi zródła:
<xref id=’...’>, <p chance=’...’>, <choice> itp.
Kazdy typ elementu jest obsługiwany za pomoca odpowiedniej, własnej metody.
Podobnie jak to robilismy w parse(), konstruujemy nazwe metody
opartej na nazwie elementu ("do xref" dla znacznika <xref> itp.), a potem
wywołujemy te metode.
"""
handlerMethod = getattr(self, "do %s" % node.tagName)
handlerMethod(node)
def parse Comment(self, node):
u"""parsuje komentarz
Gramatyka moze zawierac komentarze XML, ale my je pominiemy
196 ROZDZIAŁ 10. PRZETWARZANIE XML-A
"""
pass
def do xref(self, node):
u"""obsługuje znacznik <xref id=’...’>
Znacznik <xref id=’...’> jest odwołaniem do znacznika <ref id=’...’>.
Znacznik <xref id=’sentence’/> powoduje to, ze zostaje wybrany w przypadkowy sposób
potomek znacznika <ref id=’sentence’>.
"""
id = node.attributes["id"].value
self.parse(self.randomChildElement(self.refs[id]))
def do p(self, node):
u"""obsługuje znacznik <p>
Znacznik <p> jest jadrem gramatyki. Moze zawierac niemal
wszystko: tekst w dowolnej formie, znaczniki <choice>, znaczniki <xref>,
a nawet inne znaczniki <p>. Jesli atrybut "class=’sentence’" zostanie
znaleziony, flaga zostaje ustawiona i nastepne słowo bedzie zapisane
duza litera. Jesli zostanie znaleziony atrybut "chance=’X’", to mamy
X% szansy, ze znacznik zostanie wykorzystany
(i mamy (100-X)% szansy, ze zostanie całkowicie pominiety)
"""
keys = node.attributes.keys()
if "class" in keys:
if node.attributes["class"].value == "sentence":
self.capitalizeNextWord = 1
if "chance" in keys:
chance = int(node.attributes["chance"].value)
doit = (chance > random.randrange(100))
else:
doit = 1
if doit:
for child in node.childNodes: self.parse(child)
def do choice(self, node):
u"""obsługuje znacznik <choice>
Znacznik <choice> zawiera jeden lub wiecej znaczników <p>. Jeden znacznik <p>
zostaje wybrany przypadkowo i jest nastepnie wykorzystywany do generowania
tekstu wyjsciowego.
"""
self.parse(self.randomChildElement(node))
def usage():
print doc
def main(argv):
grammar = "kant.xml"
10.1. NURKOWANIE 197
try:
opts, args = getopt.getopt(argv, "hg:d", ["help", "grammar="])
except getopt.GetoptError:
usage()
sys.exit(2)
for opt, arg in opts:
if opt in ("-h", "--help"):
usage()
sys.exit()
elif opt == ’-d’:
global debug
debug = 1
elif opt in ("-g", "--grammar"):
grammar = arg
source = "".join(args)
k = KantGenerator(grammar, source)
print k.output()
if name == " main ":
main(sys.argv[1:])
Przykład 9.2 kgp/toolbox.py
u"""Róznorodne uzyteczne funkcje"""
def openAnything(source):
u"""URI, nazwa pliku lub łancuch znaków --> strumien
Funkcja ta pozwala zdefiniowac parser, który przyjmuje dowolne zródło wejscia
(URL, sciezke do lokalnego pliku lub znajdujacego sie gdzies w sieci,
czy tez biezace dane w postaci łancucha znaków)
i traktuje je w odpowiedni sposób. Zwracany obiekt bedzie zawierał
wszystkie podstawowe metody odczytu (read, readline, readlines).
Kiedy juz obiekt nie bedzie potrzebny, nalezy go
zamknac za pomoca metody .close().
Przykłady:
>>> from xml.dom import minidom
>>> sock = openAnything("http://localhost/kant.xml")
>>> doc = minidom.parse(sock)
>>> sock.close()
>>> sock = openAnything("c:inetpubwwwrootkant.xml")
>>> doc = minidom.parse(sock)
>>> sock.close()
>>> sock = openAnything("<ref id=’conjunction’><text>and</text><text>or</text></ref>")
>>> doc = minidom.parse(sock)
>>> sock.close()
"""
198 ROZDZIAŁ 10. PRZETWARZANIE XML-A
if hasattr(source, "read"):
return source
if source == "-":
import sys
return sys.stdin
# próbuje otworzyc za pomoca modułu urllib (gdy source jest plikiem
# dostepnym z http, ftp lub URL-a)
import urllib
try:
return urllib.urlopen(source)
except (IOError, OSError):
pass
# próbuje otworzyc za pomoca wbudowanej funkcji open (jesli source jest sciezka
# do lokalnego pliku)
try:
return open(source)
except (IOError, OSError):
pass
# traktuje source jako łancuch znaków
import StringIO
return StringIO.StringIO(str(source))
Uruchom sam program kgp.py, który bedzie parsował domyslna, oparta na XML
gramatyke w kgp/kant.xml, a nastepnie wypisze kilka filozoficznych akapitów w stylu
Immanuela Kanta.
Przykład 9.3 Przykładowe wyjscie kgp/kgp.py
[you@localhost kgp]$ python kgp.py
As is shown in the writings of Hume, our a priori concepts, in
reference to ends, abstract from all content of knowledge; in the study
of space, the discipline of human reason, in accordance with the
principles of philosophy, is the clue to the discovery of the
Transcendental Deduction. The transcendental aesthetic, in all
theoretical sciences, occupies part of the sphere of human reason
concerning the existence of our ideas in general; still, the
never-ending regress in the series of empirical conditions constitutes
the whole content for the transcendental unity of apperception. What
we have alone been able to show is that, even as this relates to the
architectonic of human reason, the Ideal may not contradict itself, but
it is still possible that it may be in contradictions with the
employment of the pure employment of our hypothetical judgements, but
natural causes (and I assert that this is the case) prove the validity
of the discipline of pure reason. As we have already seen, time (and
it is obvious that this is true) proves the validity of time, and the
architectonic of human reason, in the full sense of these terms,
10.1. NURKOWANIE 199
abstracts from all content of knowledge. I assert, in the case of the
discipline of practical reason, that the Antinomies are just as
necessary as natural causes, since knowledge of the phenomena is a
posteriori.
The discipline of human reason, as I have elsewhere shown, is by
its very nature contradictory, but our ideas exclude the possibility of
the Antinomies. We can deduce that, on the contrary, the pure
employment of philosophy, on the contrary, is by its very nature
contradictory, but our sense perceptions are a representation of, in
the case of space, metaphysics. The thing in itself is a
representation of philosophy. Applied logic is the clue to the
discovery of natural causes. However, what we have alone been able to
show is that our ideas, in other words, should only be used as a canon
for the Ideal, because of our necessary ignorance of the conditions.
[...ciach...]
Jest to oczywiscie kompletny bełkot. No dobra, nie całkowity bełkot. Jest składniowo
i gramatycznie poprawny (chociaz bardzo wielomówny). Niektóre fragmenty
moga byc rzeczywiscie prawda (lub przy najmniej z niektórymi Kant by sie zgodził), a
niektóre sa ewidentnie nieprawdziwe, a wiele fragmentów jest po prostu niespójnych.
Lecz wszystko jest w stylu Immanuela Kanta.
Interesujaca rzecza w tym programie jest to, ze nie ma tu nic, co okresla Kanta.
Cała zawartosc poprzedniego przykładu pochodzi z pliku gramatyki, kgp/kant.xml.
Jesli kazemy programowi wykorzystac inny plik gramatyki (który mozemy okreslic z
linii polecen), wyjscie bedzie kompletnie rózne.
Przykład 9.4 Proste wyjscie kgp/kgp.py
[you@localhost kgp]$ python kgp.py -g binary.xml
00101001
[you@localhost kgp]$ python kgp.py -g binary.xml
10110100
200 ROZDZIAŁ 10. PRZETWARZANIE XML-A
10.2 Pakiety
Pakiety
W rzeczywistosci przetwarzanie dokumentu XML jest bardzo proste, wystarczy
jedna linia kodu. Jednakze, zanim dojdziemy do tej linii kodu, bedziemy musieli krótko
omówic, czym sa pakiety.
Przykład 9.5 Ładowanie dokumentu XML
>>> from xml.dom import minidom #(1)
>>> xmldoc = minidom.parse(’~/diveintopython/common/py/kgp/binary.xml’)
1. Tej składni jeszcze nie widzielismy. Wyglada to niemal, jak from module import,
który znamy i kochamy, ale z “.” wyglada na cos wyzszego i innego niz proste
import. Tak na prawde xml jest czyms, co jest znane pod nazwa pakiet (ang.
package), dom jest zagniezdzonym pakietem wewnatrz xml-a, a minidom jest modułem
znajdujacym sie wewnatrz xml.dom.
Brzmi to skomplikowanie, ale tak naprawde nie jest. Jesli spojrzymy na konkretna
implementacje, moze nam to pomóc. Pakiet to niewiele wiecej niz katalog z modułami,
a zagniezdzone pakiety sa podkatalogami. Moduły wewnatrz pakietu (lub zagniezdzonego
pakietu) sa nadal zwykłymi plikami .py z wyjatkiem tego, ze sa w podkatalogu,
zamiast w głównym katalogu lib/ instalacji Pythona.
Przykład 9.6 Plikowa struktura pakietu
Python21/ katalog główny instalacji Pythona (katalog domowy plików wykonywalnych)
|
+−−lib/ katalog bibliotek (katalog domowy standardowych modułow)
|
+−− xml/ pakiet xml (w rzeczywistosci katalog z innymi rzeczami wewnatrz niego)
|
+−−sax/ pakiet xml.sax (ponownie, po prostu katalog)
|
+−−dom/ pakiet xml.dom (zawiera minidom.py)
|
+−−parsers/ pakiet xml.parsers (uzywany wewnetrznie)
Dlatego kiedy powiesz from xml.dom import minidom, Python zrozumie to jako
“znajdz w katalogu xml katalog dom, a nastepnie szukaj tutaj modułu minidom i zaimportuj
go jako minidom”. Lecz Python jest nawet madrzejszy; nie tylko mozemy
zaimportowac cały moduł zawarty wewnatrz pakietu, ale takze mozemy wybiórczo zaimportowac
wybrane klasy czy funkcje z modułu znajdujacego sie wewnatrz pakietu.
Mozemy takze zaimportowac sam pakiet jako moduł. Składnia bedzie taka sama; Python
wywnioskuje, co masz na mysli na podstawie struktury plików pakietu i automatycznie
wykona poprawna czynnosc.
Przykład 9.7 Pakiety takze sa modułami
10.2. PAKIETY 201
>>> from xml.dom import minidom #(1)
>>> minidom
<module ’xml.dom.minidom’ from ’C:Python21libxmldomminidom.pyc’>
>>> minidom.Element
<class xml.dom.minidom.Element at 01095744>
>>> from xml.dom.minidom import Element #(2)
>>> Element
<class xml.dom.minidom.Element at 01095744>
>>> minidom.Element
<class xml.dom.minidom.Element at 01095744>
>>> from xml import dom #(3)
>>> dom
<module ’xml.dom’ from ’C:Python21libxmldom__init__.pyc’>
>>> import xml #(4)
>>> xml
<module ’xml’ from ’C:Python21libxml__init__.pyc’>
1. Wtym miejscu importujemy moduł (minidom) z zagniezdzonego pakietu (xml.dom).
W wyniku tego minidom został zaimportowany do naszej przestrzeni nazw. Aby
sie odwołac do klasy wewnatrz tego modułu (np. Element), bedziemy musieli
nazwe klasy poprzedzic nazwa modułu.
2. Tutaj importujemy klase (Element) z modułu (minidom), a ten moduł z zagniezdzonego
pakietu (xml.dom). W wyniku tego Element został zaimportowany
bezposrednio do naszje przestrzeni nazw. Dodajmy, ze nie koliduje to z poprzednim
importem; teraz do klasy Element mozemy sie odwoływac na dwa sposoby
(lecz nadal jest to ta sama klasa).
3. W tym miejscu importujemy pakiet dom (zagniezdzony pakiet xml-a) jako sam
w sobie moduł. Dowolny poziom pakietu moze byc traktowany jako moduł, co
zreszta zobaczymy za moment. Moze nawet miec swoje własne atrybuty i metody,
tak jak moduły, które widzielismy wczesniej.
4. Tutaj importujemy jako moduł główny poziom pakietu xml.
Wiec jak moze pakiet (który na dysku jest katalogiem) zostac zaimportowany i
traktowany jako moduł (który jest zawsze plikiem na dysku)? Odpowiedzia jest magiczny
plik init .py. Wiemy, ze pakiety nie sa po prostu katalogami, ale sa one
katalogami ze specyficznym plikiem wewnatrz, init .py. Plik ten definiuje atrybuty
i metody tego pakietu. Na przykład xml.dom posiada klase Node, która jest
zdefiniowana w xml/dom/ init .py. Kiedy importujemy pakiet jako moduł (np. dom
z xml-a), to tak naprawde importujemy jego plik init .py.
Pakiet jest katalogiem ze specjalnym plikiem init .py wewnatrz niego. Plik
init .py definiuje atrybuty i metody pakietu. Nie musi on definiowac niczego, moze
byc nawet pustym plikiem, lecz musi istniec. Jesli nie istnieje plik init .py, katalog
jest tylko katalogiem, a nie pakietem, wiec nie moze byc zaimportowany lub zawierac
modułów, czy tez zagniezdzonych pakietów.
Wiec dlaczego meczyc sie z pakietami? Umozliwiaja one logiczne pogrupowanie
powiazanych ze soba modułów. Zamiast stworzenia pakietu xml z wewnetrznymi pakietami
sax i dom, autorzy mogliby umiescic cała funkcjonalnosc sax w xmlsax.py, a
202 ROZDZIAŁ 10. PRZETWARZANIE XML-A
cała funkcjonalnosc dom w xmldom.py, czy tez nawet zamiescic wszystko w pojedynczym
module. Jednak byłoby to niewygodne (podczas pisania tego podrecznika pakiet
xml posiadał prawie 6000 linii kodu) i trudne w zarzadzaniu (dzieki oddzielnym plikom
zródłowym, wiele osób moze równoczesnie pracowac nad róznymi czesciami).
Jesli kiedykolwiek bedziemy planowali napisac wielki podsystem w Pythonie (lub
co bardziej prawdopodobne, kiedy zauwazymy, ze nasz mały podsystem rozrósł sie
do duzego), zainwestujmy troche czasu w zaprojektowanie dobrej architektury systemu
pakietów. Jest to jedna z wielu rzeczy w Pythonie, w których jest dobry, wiec
skorzystajmy z tej zalety.
10.3. PARSOWANIE XML-A 203
10.3 Parsowanie XML-a
Parsowanie XML
Jak juz mówilismy, parsowanie XML-a własciwie jest bardzo proste: jedna linijka
kodu. Co z tym zrobimy dalej, to juz zalezy wyłacznie od nas samych.
Przykład 9.8 Ładowanie dokumentu XML (tym razem naprawde)
>>> from xml.dom import minidom #(1)
>>> xmldoc = minidom.parse(’~/zanurkuj_w_pythonie/py/kgp/binary.xml’) #(2)
>>> xmldoc #(3)
<xml.dom.minidom.Document instance at 010BE87C>
>>> print xmldoc.toxml() #(4)
<?xml version="1.0" ?>
<grammar>
<ref id="bit">
<p>0</p>
<p>1</p>
</ref>
<ref id="byte">
<p><xref id="bit"/><xref id="bit"/><xref id="bit"/><xref id="bit"/>
<xref id="bit"/><xref id="bit"/><xref id="bit"/><xref id="bit"/></p>
</ref>
</grammar>
1. Jak juz widzielismy w poprzednim podrozdziale, ta instrukcja importuje moduł
minidom z pakietu xml.dom.
2. Tutaj jest ta jedna linia kodu, która wykonuje cała robote: minidom.parse pobiera
jeden argument i zwraca sparsowana reprezentacje dokumentu XML. Argumentem
moze byc wiele rzeczy; w tym wypadku jest to po prostu nazwa pliku
dokumentu XML na lokalnym dysku. (Aby kontynuowac musimy zmienic sciezke
tak, aby wskazywała na katalog, w którym przechowujemy pobrane z sieci przykład.)
Mozemy takze jako parametr przekazac obiekt pliku lub nawet obiekt plikopodobny
(ang. file-like object). Skorzystamy z tej elastycznosci pózniej w tym
rozdziale.
3. Obiektem zwróconym przez minidom.parse jest obiekt Document, który jest
klasa pochodna klasy Node. Ten obiekt Document jest korzeniem złozonej struktury
drzewiastej połaczonych ze soba obiektów Pythona, która w pełni reprezentuje
dokument XML przekazany funkcji minidom.parse.
4. toxml jest metoda klasy Node (a zatem jest tez dostepna w obiekcie Document
otrzymanym z minidom.parse). toxml wypisuje XML reprezentowany przez
dany obiekt Node. Dla wezła, którym jest obiekt Document, wypisuje ona cały
dokument XML.
Skoro juz mamy dokument XML w pamieci, mozemy zaczac po nim wedrowac.
204 ROZDZIAŁ 10. PRZETWARZANIE XML-A
Przykład 9.9 Pobieranie wezłów potomnych
>>> xmldoc.childNodes #(1)
[<DOM Element: grammar at 17538908>]
>>> xmldoc.childNodes[0] #(2)
<DOM Element: grammar at 17538908>
>>> xmldoc.firstChild #(3)
<DOM Element: grammar at 17538908>
1. Kazdy wezeł posiada atrybut childNodes, który jest lista obiektów Node. Obiekt
Document zawsze ma tylko jeden wezeł potomny, element główny (korzen) dokumentu
XML (w tym przypadku element grammar).
2. Aby dostac sie do pierwszego (i w tym wypadku jedynego) wezła potomnego,
uzywamy po prostu zwykłej składni do obsługi list. Pamietajmy, tu nie dzieje sie
nic nadzwyczajnego; to jest po prostu zwykła lista Pythona zwykłych pythonowych
obiektów.
3. Poniewaz pobieranie pierwszego wezła potomnego danego wezła jest bardzo uzyteczna
i czesta czynnoscia, klasa Node posiada atrybut firstChild, który jest
synonimem dla childNodes[0]. (Jest tez atrybut lastChild, który jest synonimem
dla childNodes[-1].)
Przykład 9.10 Metoda toxml działa w kazdym wezle
>>> grammarNode = xmldoc.firstChild
>>> print grammarNode.toxml() #(1)
<grammar>
<ref id="bit">
<p>0</p>
<p>1</p>
</ref>
<ref id="byte">
<p><xref id="bit"/><xref id="bit"/><xref id="bit"/><xref id="bit"/>
<xref id="bit"/><xref id="bit"/><xref id="bit"/><xref id="bit"/></p>
</ref>
</grammar>
1. Poniewaz metoda toxml jest zdefiniowana w klasie Node, jest ona dostepna w
kazdym wezle XML-a, nie tylko w elemencie Document.
Przykład 9.11 Wezłami potomnymi moze byc takze tekst
>>> grammarNode.childNodes #(1)
[<DOM Text node "n">, <DOM Element: ref at 17533332>,
<DOM Text node "n">, <DOM Element: ref at 17549660>, <DOM Text node "n">]
>>> print grammarNode.firstChild.toxml() #(2)
>>> print grammarNode.childNodes[1].toxml() #(3)
<ref id="bit">
10.3. PARSOWANIE XML-A 205
<p>0</p>
<p>1</p>
</ref>
>>> print grammarNode.childNodes[3].toxml() #(4)
<ref id="byte">
<p><xref id="bit"/><xref id="bit"/><xref id="bit"/><xref id="bit"/>
<xref id="bit"/><xref id="bit"/><xref id="bit"/><xref id="bit"/></p>
</ref>
>>> print grammarNode.lastChild.toxml() #(5)
1. Patrzac na XML w kgp/binary.xml, moglibysmy pomyslec, ze wezeł grammar
ma tylko dwa wezły potomne, czyli dwa elementy ref. Ale chyba o czyms zapominamy:
o znakach konca linii! Za elementem ’<grammar>’ i przed pierwszym
’<ref>’ jest znak konca linii i zalicza sie on do wezłów potomnych elementu
grammar. Podobnie jest tez znak konca linii po kazdym ’</ref>’; to takze zalicza
sie do wezłów potomnych. Tak wiec grammar.childNodes jest własciwie
lista 5 obiektów: 3 obiekty Text i 2 obiekty Element.
2. Pierwszym potomkiem jest obiekt Text reprezentujacy znak konca linii za znacznikiem
’<grammar>’ i przed pierwszym ’<ref>’.
3. Drugim potomkiem jest obiekt Element reprezentujacy pierwszy element ref.
4. Czwartym potomkiem jest obiekt Element reprezentujacy drugi element ref.
5. Ostatnim potomkiem jest obiekt Text reprezentujacy znak konca linii za znacznikiem
koncowym ’</ref>’ i przed znacznikiem koncowym ’</grammar>’.
Przykład 9.12 Drazenie az do tekstu
>>> grammarNode
<DOM Element: grammar at 19167148>
>>> refNode = grammarNode.childNodes[1] #(1)
>>> refNode
<DOM Element: ref at 17987740>
>>> refNode.childNodes #(2)
[<DOM Text node "n">, <DOM Text node " ">, <DOM Element: p at 19315844>,
<DOM Text node "n">, <DOM Text node " ">,
<DOM Element: p at 19462036>, <DOM Text node "n">]
>>> pNode = refNode.childNodes[2]
>>> pNode
<DOM Element: p at 19315844>
>>> print pNode.toxml() #(3)
<p>0</p>
>>> pNode.firstChild #(4)
<DOM Text node "0">
>>> pNode.firstChild.data #(5)
u’0’
1. Jak juz widzielismy w poprzednim przykładzie, pierwszym elementem ref jest
grammarNode.childNodes[1], poniewaz childNodes[0] jest wezłem typu Text
dla znaku konca linii.
206 ROZDZIAŁ 10. PRZETWARZANIE XML-A
2. Element ref posiada swój zbiór wezłów potomnych, jeden dla znaku konca linii,
oddzielny dla znaków spacji, jeden dla elementu p i tak dalej.
3. Mozesz uzyc metody toxml nawet tutaj, głeboko wewnatrz dokumentu.
4. Element p ma tylko jeden wezeł potomny (nie mozemy tego zobaczyc na tym
przykładzie, ale spójrzmy na pNode.childNodes jesli nie wierzymy) i jest nim
obiekt Text dla pojednyczego znaku ’0’.
5. Atrybut .data wezła Text zawiera rzeczywisty napis, jaki ten tekstowy wezeł
reprezentuje. Zauwazmy, ze wszystkie dane tekstowe przechowywane sa w unikodzie.
10.4. WYSZUKIWANIE ELEMENTÓW 207
10.4 Wyszukiwanie elementów
Wyszukiwanie elementów
Przemierzanie dokumentu XML poprzez przechodzenie przez kazdy wezeł z osobna
mogłoby byc nuzace. Jesli poszukujesz czegos szczególnego, co jest zagrzebane głeboko
w dokumencie XML, istnieje skrót, którego mozesz uzyc, aby znalezc to szybko:
getElementsByTagName.
W tym podrozdziale uzywac bedziemy pliku gramatyki kgp/binary.xml, który wyglada
tak:
Przykład 9.20 binary.xml
<span><?xml version="1.0"?>
<!DOCTYPE grammar PUBLIC "-//diveintopython.org//DTD Kant Generator Pro v1.0//EN" "kgp.dtd">
<grammar>
<ref id="bit">
<p>0</p>
<p>1</p>
</ref>
<ref id="byte">
<p><xref id="bit"/><xref id="bit"/><xref id="bit"/><xref id="bit"/>
<xref id="bit"/><xref id="bit"/><xref id="bit"/><xref id="bit"/></p>
</ref>
</grammar></span>
Zawiera on dwa elementy ref: ’bit’ i ’byte’. ’bit’ moze przyjmowac wartosci
’0’ lub ’1’, a ’byte’ moze sie składac z osmiu bitów.
Przykład 9.21 Wprowadzenie do getElementsByTagName
>>> from xml.dom import minidom
>>> xmldoc = minidom.parse(’binary.xml’)
>>> reflist = xmldoc.getElementsByTagName(’ref’) #(1)
>>> reflist
[<DOM Element: ref at 136138108>, <DOM Element: ref at 136144292>]
>>> print reflist[0].toxml()
<ref id="bit">
<p>0</p>
<p>1</p>
</ref>
>>> print reflist[1].toxml()
<ref id="byte">
<p><xref id="bit"/><xref id="bit"/><xref id="bit"/><xref id="bit"/>
<xref id="bit"/><xref id="bit"/><xref id="bit"/><xref id="bit"/></p>
</ref>
1. getElementsByTagName przyjmuje jeden argument: nazwe elementu, który chcemy
znalezc. Zwraca liste obiektów Element, odpowiednia do znalezionych elementów
XML posiadajacych podana nazwe. W tym przypadku znalezlismy dwa elementy
ref.
208 ROZDZIAŁ 10. PRZETWARZANIE XML-A
Przykład 9.22 Kazdy element mozemy przeszukiwac
>>> firstref = reflist[0] #(1)
>>> print firstref.toxml()
<ref id="bit">
<p>0</p>
<p>1</p>
</ref>
>>> plist = firstref.getElementsByTagName("p") #(2)
>>> plist
[<DOM Element: p at 136140116>, <DOM Element: p at 136142172>]
>>> print plist[0].toxml() #(3)
<p>0</p>
>>> print plist[1].toxml()
<p>1</p>
1. Kontynuujac poprzedni przykład, pierwszy obiekt naszej listy reflist jest elementem
ref ’bit’.
2. Mozemy uzyc tej samej metody getElementsByTagName na tym obiekcie klasy
Element, aby znalezc wszystkie elementy ¡p¿ wewnatrz tego elementu ref ’bit’.
3. Tak jak poprzednio metoda getElementsByTagName zwraca liste wszystkich elementów
jakie znajdzie. W tym przypadku mamy dwa, po jednym na kazdy bit.
Przykład 9.23 Przeszukiwanie jest własciwie rekurencyjne
>>> plist = xmldoc.getElementsByTagName("p") #(1)
>>> plist
[<DOM Element: p at 136140116>, <DOM Element: p at 136142172>,
<DOM Element: p at 136146124>]
>>> plist[0].toxml() #(2)
’<p>0</p>’
>>> plist[1].toxml()
’<p>1</p>’
>>> plist[2].toxml() #(3)
’<p><xref id="bit"/><xref id="bit"/><xref id="bit"/><xref id="bit"/>
<xref id="bit"/><xref id="bit"/><xref id="bit"/><xref id="bit"/></p>’
1. Zauwazmy róznice pomiedzy tym i poprzednim przykładem. Poprzednio szukalismy
elementów p wewnatrz firstref, lecz teraz szukamy elementów p wewnatrz
xmldoc, czyli obiektu najwyzszego poziomu reprezentujacego cały dokument
XML. To wyszukiwanie znajduje elementy p zagniezdzone wewnatrz elementów
ref wewnatrz głównego elementu gramatyki.
2. Pierwsze dwa elementy p znajduja sie wewnatrz pierwszego elementu ref (element
ref ’bit’).
3. Ostatni element p, to ten wewnatrz drugiego elementu ref (element ref ’byte’).
10.5. DOSTEP DO ATRYBUTÓW ELEMENTÓW 209
10.5 Dostep do atrybutów elementów
Dostep do atrybutów elementów
Elementy XML-a moga miec jeden lub wiele atrybutów i jest niewiarygodnie łatwo
do nich dotrzec, gdy dokument XML został juz sparsowany.
W tym podrozdziale bedziemy korzystac z pliku kgp/binary.xml, który juz widzielismy
w poprzednim podrozdziale.
Podrozdział ta moze byc lekko zagmatwany z powodu nakładajacej sie terminologii.
Elementy dokumentu XML maja atrybuty, a obiekty Pythona takze maja atrybuty.
Gdy parsujemy dokument XML, otrzymujemy jakas grupe obiektów Pythona, które
reprezentuja wszystkie czesci dokumentu XML, a niektóre z tych obiektów Pythona
reprezentuja atrybuty elementów XML-a. Jednak te obiekty (Python), które reprezentuja
atrybuty (XML), takze maja atrybuty (Python), które sa uzywane do pobierania
róznych czesci atrybutu (XML), który ten obiekt reprezentuje. Uprzedzalismy, ze to
jest troche zagmatwane.
Przykład 9.24 Dostep do atrybutów elementów
>>> xmldoc = minidom.parse(’binary.xml’)
>>> reflist = xmldoc.getElementsByTagName(’ref’)
>>> bitref = reflist[0]
>>> print bitref.toxml()
<ref id="bit">
<p>0</p>
<p>1</p>
</ref>
>>> bitref.attributes #(1)
<xml.dom.minidom.NamedNodeMap instance at 0x81e0c9c>
>>> bitref.attributes.keys() #(2) (3)
[u’id’]
>>> bitref.attributes.values() #(4)
[<xml.dom.minidom.Attr instance at 0x81d5044>]
>>> bitref.attributes["id"] #(5)
<xml.dom.minidom.Attr instance at 0x81d5044>
1. Kazdy obiekt Element ma atrybut o nazwie attributes, który jest obiektem
klasy NamedNodeMap. Brzmi groznie, ale takie nie jest, poniewaz obiekt NamedNodeMap
jest obiektem działajacym jak słownik, a wiec juz wiemy, jak go uzywac.
2. Traktujac obiekt NamedNodeMap jak słownik, mozemy pobrac liste nazw atrybutów
tego elementu uzywajac attributes.keys(). Ten element ma tylko jeden
atrybut: ’id’.
3. Nazwy atrybutów, jak kazdy inny tekst w dokumencie XML, sa zapisane w postaci
unikodu.
4. Znowu traktujac NamedNodeMap jak słownik, mozemy pobrac liste wartosci atrybutów
uzywajac attributes.values(). Wartosci same w sobie takze sa obiektami
typu Attr. Jak wydobyc uzyteczne informacje z tego obiektu zobaczymy w
nastepnym przykładzie.
210 ROZDZIAŁ 10. PRZETWARZANIE XML-A
5. Nadal traktujac NamedNodeMap jak słownik, mozemy dotrzec do poszczególnych
atrybutów poprzez ich nazwy, uzywajac normalnej składni dla słowników. (Szczególnie
uwazni czytelnicy juz wiedza jak klasa NamedNodeMap realizuje ten fajny
trik: poprzez definicje metody specjalnej o nazwie getitem . Inni czytelnicy
moga pocieszyc sie faktem, iz nie musza rozumiec jak to działa, aby uzywac tego
efektywnie.)
Przykład 9.25 Dostep do poszczególnych atrybutów
>>> a = bitref.attributes["id"]
>>> a
<xml.dom.minidom.Attr instance at 0x81d5044>
>>> a.name #(1)
u’id’
>>> a.value #(2)
u’bit’
1. Obiekt Attr w całosci reprezentuje pojedynczy atrybut XML-a pojedynczego
elementu XML-a. Nazwa atrybutu (ta sama, której uzylismy do znalezienia tego
obiektu w bitref.attributes pseudo-słownikowym obiekcie NamedNodeMap)
znajduje sie w a.name.
2. Własciwa wartosc tekstowa tego atrybutu XML-a znajduje sie w a.value.
Podobnie jak w słowniku atrybuty elementu XML-a nie sa uporzadkowane. Moze
sie zdarzyc, ze w oryginalnym dokumencie XML atrybuty beda ułozone w pewnym
okreslonym porzadku i moze sie zdarzyc, ze obiekty Attr beda równiez ułozone w
takim samym porzadku po sparsowaniu dokumentu do obiektów Pythona, ale te uporzadkowania
sa tak naprawde przypadkowe i nie powinny miec zadnego specjalnego
znaczenia. Zawsze powiniennismy odwoływac sie do poszczególnych atrybutów poprzez
nazwe, tak jak do elementów słownika poprzez klucz.
10.6. PODSUMOWANIE 211
10.6 Przetwarzanie XML-a - podsumowanie
Podsumowanie
OK, to by było na tyle ciezkich tematów o XML-u. Nastepny rozdział bedzie nadal
wykorzystywał te same przykładowe programy, ale bedzie zwracał uwage na inne
aspekty, które sprawiaja, ze program jest bardziej elastyczny: wykorzystywanie strumieni
do przetwarzania wejscia, uzywanie funkcji getattr jako posrednika, a takze
korzystanie z flag w linii polecen, aby pozwolic uzytkownikom skonfigurowac program
bez zmieniania kodu zródłowego.
Przed przejsciem do nastepnego rozdziału, powinnismy nie miec problemów z:
• parsowaniem dokumentów XML za pomoca minidom-a, przeszukiwaniem sparsowanego
dokumentu, a takze z dostepem do dowolnego atrybutu
• uporzadkowywaniem złozonych bibliotek w pakiety
212 ROZDZIAŁ 10. PRZETWARZANIE XML-A
Rozdział 11
Skrypty i strumienie
213
214 ROZDZIAŁ 11. SKRYPTY I STRUMIENIE
11.1 Abstrakcyjne zródła wejscia
Abstrakcyjne zródła wejscia
Jedna z najwazniejszych mozliwosci Pythona jest jego dynamiczne wiazanie, a jednym
z najbardziej przydatnych przykładów wykorzystania tego jest obiekt plikopodobny
(ang. file-like object).
Wiele funkcji, które wymagaja jakiegos zródła wejscia, mogłyby po prostu przyjmowac
jako argument nazwe pliku, nastepnie go otwierac, czytac, a na koncu go zamykac.
Jednak tego nie robia. Zamiast działac w ten sposób, jako argument przyjmuja obiekt
pliku lub obiekt plikopodobny.
W najprostszym przypadku obiekt plikopodobny jest dowolnym obiektem z metoda
read, która przyjmuje opcjonalny parametr wielkosci, size, a nastepnie zwraca łancuch
znaków. Kiedy wywołujemy go bez parametru size, odczytuje wszystko, co jest
do przeczytania ze zródła wejscia, a potem zwraca te wszystkie dane jako pojedynczy
łancuch znaków. Natomiast kiedy wywołamy metode read z parametrem size, to odczyta
ona tyle bajtów ze zródła wejscia, ile wynosi wartosc size, a nastepnie zwróci
te dane. Kiedy ponownie wywołamy te metode, zostanie odczytana i zwrócona dalsza
porcja danych (czyli dane beda czytane od miejsca, w którym wczesniej skonczono
czytac).
Powyzej opisalismy, w jaki sposób działaja prawdziwe pliki. Jednak nie musimy
sie ograniczac do prawdziwych plików. Zródłem wejscia moze byc wszystko: plik na
dysku, strona internetowa, czy nawet jakis łancuch znaków. Dopóki przekazujemy do
funkcji obiekt plikopodobny, a funkcja ta po prostu wywołuje metode read, to funkcja
moze obsłuzyc dowolny rodzaj wejscia, bez posiadania jakiegos specjalnego kodu dla
kazdego rodzaju wejscia.
Moze sie zastanawiamy, co ma to wspólnego z przetwarzaniem XML-a? Otóz minidom.parse
jest taka funkcja, do której mozemy przekazac obiekt plikopodobny.
Przykład 10.1 Parsowanie XML-u z pliku
>>> from xml.dom import minidom
>>> fsock = open(’binary.xml’) #(1)
>>> xmldoc = minidom.parse(fsock) #(2)
>>> fsock.close() #(3)
>>> print xmldoc.toxml() #(4)
<?xml version="1.0" ?>
<grammar>
<ref id="bit">
<p>0</p>
<p>1</p>
</ref>
<ref id="byte">
<p><xref id="bit"/><xref id="bit"/><xref id="bit"/><xref id="bit"/>
<xref id="bit"/><xref id="bit"/><xref id="bit"/><xref id="bit"/></p>
</ref>
</grammar>
1. Najpierw otwieramy plik z dysku. Otrzymujemy przez to obiekt pliku.
2. Przekazujemy obiekt pliku do funkcji minidom.parse, która wywołuje metode
read z fsock i czyta dokument XML z tego pliku.
11.1. ABSTRAKCYJNE ZRÓDŁA WEJSCIA 215
3. Koniecznie wywołujemy metode close obiektu pliku, jak juz skonczylismy na
nim prace. minidom.parse nie zrobi tego za nas.
4. Wywołujac ze zwróconego dokumentu XML metode toxml(), wypisujemy cały
dokument.
Dobrze, to wszystko wyglada jak kolosalne marnotrawstwo czasu. W koncu juz
wczesniej widzielismy, ze minidom.parse moze przyjac jako argument nazwe pliku i
wykonac cała robote z otwieraniem i zamykaniem automatycznie. Prawda jest, ze jesli
chcemy sparsowac lokalny plik, mozemy przekazac nazwe pliku do minidom.parse, a
funkcja ta bedzie umiała madrze to wykorzystac. Lecz zauwazmy jak podobne i łatwe
jest takze parsowanie dokumentu XML pochodzacego bezposrednio z Internetu.
Przykład 10.2 Parsowanie XML-a z URL-a
>>> import urllib
>>> usock = urllib.urlopen(’http://slashdot.org/slashdot.rdf’) #(1)
>>> xmldoc = minidom.parse(usock) #(2)
>>> usock.close() #(3)
>>> print xmldoc.toxml() #(4)
<?xml version="1.0" ?>
<rdf:RDF xmlns="http://my.netscape.com/rdf/simple/0.9/"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<channel>
<title>Slashdot</title>
<link>http://slashdot.org/</link>
<description>News for nerds, stuff that matters</description>
</channel>
<image>
<title>Slashdot</title>
<url>http://images.slashdot.org/topics/topicslashdot.gif</url>
<link>http://slashdot.org/</link>
</image>
<item>
<title>To HDTV or Not to HDTV?</title>
<link>http://slashdot.org/article.pl?sid=01/12/28/0421241</link>
</item>
[...ciach...]
1. Jak juz zaobserwowalismy w poprzednim rozdziale, urlopen przyjmuje adres
URL strony internetowej i zwraca obiekt plikopodobny. Ponadto, co jest bardzo
wazne, obiekt ten posiada metode read, która zwraca zródło danej strony internetowej.
2. Teraz przekazujemy ten obiekt plikopodobny do minidom.parse, która posłusznie
wywołuje metode read i parsuje dane XML, które zostaja zwrócone przez read.
Fakt, ze te dane przychodza teraz bezposrednio z Internetu, jest kompletnie
216 ROZDZIAŁ 11. SKRYPTY I STRUMIENIE
nieistotny. minidom.parse nie ma o stronach internetowych zadnego pojecia; on
tylko wie cos o obiektach plikopodobnych.
3. Jak tylko obiekt plikopodobny, który podarował nam urlopen, nie bedzie potrzebny,
koniecznie zamykamy go.
4. Przy okazji, ten URL jest prawdziwy i on naprawde jest dokumentem XML.
Reprezentuje on aktualne nagłówki, techniczne newsy i plotki w Slashdocie.
Przykład 10.3 Parsowanie XML-a z łancucha znaków (prosty sposób, ale
mało elastyczny)
>>> contents = "<grammar><ref id=’bit’><p>0</p><p>1</p></ref></grammar>"
>>> xmldoc = minidom.parseString(contents) #(1)
>>> print xmldoc.toxml()
<?xml version="1.0" ?>
<grammar><ref id="bit"><p>0</p><p>1</p></ref></grammar>
1. minidom posiada metode parseString, która przyjmuje cały dokument XML w
postaci łancucha znaków i parsuje go. Mozemy ja wykorzystac zamiast minidom.parse,
jesli wiemy, ze posiadamy cały dokument w formie łancucha znaków.
OK, to mozemy korzystac z funkcji minidom.parse zarówno do parsowania lokalnych
plików jak i odległych URL-ów, ale do parsowania łancuchów znaków wykorzystujemy...
inna funkcje. Oznacza to, ze jesli chcielibysmy, aby nasz program mógł dac
wyjscie z pliku, adresu URL lub łancucha znaków, potrzebujemy specjalnej logiki, aby
sprawdzic czy mamy do czynienia z łancuchem znaków, a jesli tak, to wywołac funkcje
parseString zamiast parse. Jakie to niesatysfakcjonujace...
Gdyby tylko był sposób, aby zmienic łancuch znaków na obiekt plikopodobny, to
moglibysmy po prostu przekazac ten obiekt do minidom.parse. I rzeczywiscie, istnieje
moduł specjalnie zaprojektowany do tego: StringIO.
Przykład 10.4 Wprowadzenie do StringIO
>>> contents = "<grammar><ref id=’bit’><p>0</p><p>1</p></ref></grammar>"
>>> import StringIO
>>> ssock = StringIO.StringIO(contents) #(1)
>>> ssock.read() #(2)
"<grammar><ref id=’bit’><p>0</p><p>1</p></ref></grammar>"
>>> ssock.read() #(3)
’’
>>> ssock.seek(0) #(4)
>>> ssock.read(15) #(5)
’<grammar><ref i’
>>> ssock.read(15)
"d=’bit’><p>0</p"
>>> ssock.read()
’><p>1</p></ref></grammar>’
>>> ssock.close() #(6)
1. Moduł StringIO zawiera tylko jedna klase, takze nazwana StringIO, która pozwala
zamienic napis w obiekt plikopodobny. Klasa StringIO podczas tworzenia
instancji przyjmuje jako parametr łancuch znaków.
11.1. ABSTRAKCYJNE ZRÓDŁA WEJSCIA 217
2. Teraz juz mamy obiekt plikopodobny i mozemy robic wszystkie mozliwe plikopodobne
operacje. Na przykład read, która zwraca oryginalny łancuch.
3. Wywołujac ponownie read otrzymamy pusty napis. W ten sposób działa prawdziwy
obiekt pliku; kiedy juz zostanie przeczytany cały plik, nie mozna czytac
wiecej bez wyraznego przesuniecia do poczatku pliku. Obiekt StringIO pracuje
w ten sam sposób.
4. Mozemy jawnie przesunac sie do poczatku napisu, podobnie jak mozemy sie
przesunac w pliku, wykorzystujac metode seek obiektu klasy StringIO.
5. Mozemy takze czytac fragmentami łancuch znaków, dzieki przekazaniu parametr
wielkosci size do metody read.
6. Za kazdym razem, kiedy wywołamy read, zostanie nam zwrócona pozostała czesc
napisu, która nie została jeszcze przeczytana.Wdokładnie ten sam sposób działa
obiekt pliku.
Przykład 10.5 Parsowanie XML-a z łancucha znaków (sposób z obiektem
plikopodobnym)
>>> contents = "<grammar><ref id=’bit’><p>0</p><p>1</p></ref></grammar>"
>>> ssock = StringIO.StringIO(contents)
>>> xmldoc = minidom.parse(ssock) #(1)
>>> ssock.close()
>>> print xmldoc.toxml()
<?xml version="1.0" ?>
<grammar><ref id="bit"><p>0</p><p>1</p></ref></grammar>
1. Teraz mozemy przekazac obiekt plikopodobny (w rzeczywistosci instancje StringIO)
do funkcji minidom.parse, która z kolei wywoła metode read z tego obiektu plikopodobnego
i szczesliwie wszystko przeparsuje, nie zdajac sobie nawet sprawy,
ze wejscie to pochodzi z łancucha znaków.
To juz wiemy, jak za pomoca pojedynczej funkcji, minidom.parse, sparsowac dokument
XML przechowywany na stronie internetowej, lokalnym pliku, czy w łancuchu
znaków. Dla strony internetowej wykorzystamy urlopen, aby dostac obiekt plikopodobny;
dla lokalnego pliku, wykorzystamy open; a w przypadku łancucha znaków
skorzystamy z StringIO. Lecz teraz pójdzmy troche do przodu i uogólnijmy tez te
róznice.
Przykład 10.6 openAnything
def openAnything(source): #(1)
# próbuje otworzyc za pomoca urllib (jesli source jest URL-em do http, ftp itp.)
import urllib
try:
return urllib.urlopen(source) #(2)
except (IOError, OSError):
pass
# próbuje otworzyc za pomoca wbudowanej funkcji open (gdy source jest sciezka do pliku)
218 ROZDZIAŁ 11. SKRYPTY I STRUMIENIE
try:
return open(source) #(3)
except (IOError, OSError):
pass
# traktuje source jako łancuch znaków z danymi
import StringIO
return StringIO.StringIO(str(source)) #(4)
1. Funkcja openAnything przyjmuje pojedynczy argument, source, i zwraca obiekt
plikopodobny. source jest łancuchem znaków o róznym charakterze. Moze sie
odnosic do adresu URL (np. ’http://slashdot.org/slashdot.rdf’), moze byc
globalna lub lokalna sciezka do pliku (np. ’binary.xml’), czy tez łancuchem
znaków przechowujacym dokument XML, który ma zostac sparsowany.
2. Najpierw sprawdzamy, czy source jest URL-em. Robimy to brutalnie: próbujemy
otworzyc to jako URL i cicho pomijamy błedy spowodowane próba otworzenia
czegos, co nie jest URL-em. Jest to własciwie eleganckie w tym sensie, ze jesli
urllib bedzie kiedys obsługiwał nowe typy URL-i, nasz program takze je obsłuzy
i to bez koniecznosci zmiany kodu. Jesli urllib jest w stanie otworzyc source,
to return wykopie nas bezposrednio z funkcji i ponizsze instrukcje try nie beda
nigdy wykonywane.
3. W innym przypadku, gdy urllib wrzasnał na nas i powiedział, ze source nie
jest poprawnym URL-em, zakładamy, ze jest to sciezka do pliku znajdujacego
sie na dysku i próbujemy go otworzyc. Ponownie, nic nie robimy, by sprawdzic,
czy source jest poprawna nazwa pliku (zasady okreslajace poprawnosc nazwy
pliku sa znaczaco rózne na róznych platformach, dlatego prawdopodobnie i tak
bysmy to zle zrobili). Zamiast tego, na slepo otwieramy plik i cicho pomijamy
wszystkie błedy.
4. W tym miejscu zakładamy, ze source jest łancuchem znaków, który przechowuje
dokument XML (poniewaz nic innego nie zadziałało), dlatego wykorzystujemy
StringIO, aby utworzyc obiekt plikopodobny i zwracamy go. (Tak naprawde,
poniewaz wykorzystujemy funkcje str, source nie musi byc nawet łancuchem
znaków; moze byc nawet dowolnym obiektem, a z którego zostanie wykorzystana
jego tekstowa reprezentacja, a która jest zdefiniowana przez specjalna metode
str .)
Teraz mozemy wykorzystac funkcje openAnything w połaczeniu z minidom.parse,
aby utworzyc funkcje, która przyjmuje zródło source, które w jakis sposób odwołuje
sie do dokumentu XML (moze to robic za pomoca adresu URL, lokalnego pliku, czy
tez dokumentu przechowywanego jako łancuch znaków), i parsuje je.
Przykład 10.7 Wykorzystanie openAnything w kgp/kgp.py
class KantGenerator:
def _load(self, source):
sock = toolbox.openAnything(source)
xmldoc = minidom.parse(sock).documentElement
sock.close()
return xmldoc
11.2. STANDARDOWY STRUMIEN WEJSCIA, WYJSCIA I BŁEDÓW 219
11.2 Standardowy strumien wejscia, wyjscia i błedów
Standardowy strumien wejscia, wyjscia i błedów
Uzytkownicy Uniksa sa juz prawdopodobnie zapoznani z koncepcja standardowego
wejscia, standardowego wyjscia i standardowego strumienia błedów. Ten podrozdział
jest dla pozostałych osób.
Standardowe wyjscie i strumien błedów (powszechnie uzywana skrócona forma to
stdout i stderr ) sa strumieniami danych wbudowanymi do kazdego systemu Unix.
Kiedy cos wypisujemy, idzie to do strumienia stdout; kiedy wystapi bład w programie,
a program wypisze informacje pomocne przy debugowaniu (jak traceback w Pythonie),
to wszystko pójdzie do strumienia stderr. Te dwa strumienie sa zwykle połaczone z
oknem terminala, na którym pracujemy, wiec jezeli program cos wypisuje, zobaczymy
to na wyjsciu, a kiedy program spowoduje bład, zobaczymy informacje debugujace.
(Jesli pracujemy w systemie z okienkowym IDE Pythona, stdout i stderr domyslnie
beda połaczone z “interaktywnym oknem”.)
Przykład 10.8 Wprowadzenie do stdout i stderr
>>> for i in range(3):
... print ’Nurkujemy’ #(1)
Nurkujemy
Nurkujemy
Nurkujemy
>>> import sys
>>> for i in range(3):
... sys.stdout.write(’Nurkujemy’) #(2)
NurkujemyNurkujemyNurkujemy
>>> for i in range(3):
... sys.stderr.write(’Nurkujemy’) #(3)
NurkujemyNurkujemyNurkujemy
1. Jak zobaczylismy w przykładzie 6.9, “Prosty licznik”, mozemy wykorzystac wbudowana
funkcje range, aby zbudowac prosta petle licznikowa, która powtarza
pewna operacje okreslona liczbe razy.
2. stdout jest obiektem plikopodobnym; wywołujac jego funkcje write bedziemy
wypisywac na wyjscie napis, który przekazalismy. W rzeczywistosc, to własnie
funkcja print naprawde robi; dodaje ona znak nowej linii do wypisywanego
napisu, a nastepnie wywołuje sys.stdout.write.
3. W tym prostym przypadku stdout i stderr wysyłaja wyjscie do tego samego
miejsca: do IDE Pythona (jesli jestesmy w nim) lub do terminala (jesli mamy
uruchomionego Pythona z linii polecen). Podobnie jak stdout, stderr nie dodaje
znaku nowej linii za nas; jesli chcemy, aby ten znak został dodany, musimy to
zrobic sami.
Zarówno stdout i stderr sa obiektami plikopodobnymi, a które omawialismy w
podrozdziale 10.1, “Abstrakcyjne zródła wejscia”, lecz te sa tylko do zapisu. Nie posiadaja
one metody read, tylko write. Jednak nadal sa one obiektami plikopodobnymi i
220 ROZDZIAŁ 11. SKRYPTY I STRUMIENIE
mozemy do nich przypisac inny obiekt pliku lub obiekt plikopodobny, aby przekierowac
ich wyjscie.
Przykład 10.9 Przekierowywanie wyjscia
[you@localhost kgp]$ python stdout.py
Nurkujemy
[you@localhost kgp]$ cat out.log
Ta wiadomosc bedzie logowana i nie zostanie wypisana na wyjscie
(W Windowsie mozemy wykorzystac polecenie type, zamiast cat, aby wyswietlic
zawartosc pliku.)
#-*- coding: utf-8 -*-
#stdout.py
import sys
print ’Nurkujemy’ #(1)
saveout = sys.stdout #(2)
fsock = open(’out.log’, ’w’) #(3)
sys.stdout = fsock #(4)
print ’Ta wiadomosc bedzie logowana i nie zostanie wypisana na wyjscie’ #(5)
sys.stdout = saveout #(6)
fsock.close() #(7)
1. To zostanie wypisane w interaktywnym oknie IDE (lub w terminalu, jesli skrypt
został uruchomiony z linii polecen).
2. Zawsze, zanim przekierujemy standardowe wyjscie, przypisujemy gdzies stdout,
dzieki temu, bedziemy potem mogli do niego normalnie wrócic.
3. Otwieramy plik do zapisu. Jesli plik nie istnieje, zostanie utworzony. Jesli istnieje,
zostanie nadpisany.
4. Całe pózniejsze wyjscie zostanie przekierowane do pliku, który własnie otworzylismy.
5. Zostanie to wypisane tylko do pliku out.log; nie bedzie widoczne w oknie IDE
lub w terminalu.
6. Przywracamy stdout do poczatkowej, oryginalnej postaci.
7. Zamykamy plik out.log.
Dodajmy, ze w wypisywanym łancuchu znaków uzylismy polskich znaków, a poniewaz
nie skorzystalismy z unikodu, wiec napis ten zostanie wypisany w takiej samej
postaci, w jakiej został zapisany w pliku Pythona (czyli wiadomosc zostanie zapisana
w kodowaniu utf-8). Gdybysmy skorzystali z unikodu, musielibysmy wyraznie zakodowac
ten napis do jakiegos kodowania za pomoca metody encode, poniewaz Python
nie wie, z jakiego kodowania chce korzystac utworzony przez nas plik (plik out.log
przypisany do zmiennej stdout).
Przekierowywanie standardowego strumienia błedów (stderr ) działa w ten sam
sposób, wykorzystujac sys.stderr, zamiast sys.stdout.
11.2. STANDARDOWY STRUMIEN WEJSCIA, WYJSCIA I BŁEDÓW 221
Przykład 10.10 Przekierowywanie informacji o błedach
[you@localhost kgp]$ python stderr.py
[you@localhost kgp]$ cat error.log
Traceback (most recent line last):
File "stderr.py", line 6, in ?
raise Exception(’ten bład bedzie logowany)
Exception: ten bład bedzie logowany
#stderr.py
#-*- coding: utf-8 -*-
import sys
fsock = open(’error.log’, ’w’) #(1)
sys.stderr = fsock #(2)
raise Exception(’ten bład bedzie logowany’) #(3) (4)
1. Otwieramy plik error.log, gdzie chcemy przechowywac informacje debugujace.
2. Przekierowujemy standardowy strumien błedów, dzieki przypisaniu obiektu nowo
otwartego pliku do sys.stderr.
3. Rzucamy wyjatek. Zauwazmy, ze na ekranie wyjsciowym nic nie zostanie wypisane.
Wszystkie informacje traceback zostały zapisane w error.log.
4. Zauwazmy takze, ze nie zamknelismy jawnie pliku error.log, a nawet nie przypisalismy
do sys.stderr jego pierwotnej wartosci. To jest wspaniałe, ze kiedy
program sie rozwali (z powodu wyjatku), Python wyczysci i zamknie wszystkie
pliki za nas. Nie ma zadnej róznicy, czy stderr zostanie przywrócony, czy tez
nie, poniewaz program sie rozwala, a Python konczy działanie. Przywrócenie
wartosci do oryginalnej, jest bardziej wazne dla stdout, jesli zamierzasz pózniej
wykonywac jakies inne operacje w tym samym skrypcie.
Poniewaz powszechnie wypisuje sie informacje o błedach na standardowy strumien
błedów, Python posiada skrótowa składnie, która mozna wykorzystac do bezposredniego
przekierowywania wyjscia.
Przykład 10.11 Wypisywanie do stderr
>>> print ’wchodzimy do funkcji’
wchodzimy do funkcji
>>> import sys
>>> print >> sys.stderr, ’wchodzimy do funkcji’ #(1)
wchodzimy do funkcji
1. Ta skrótowa składnia wyrazenia print moze byc wykorzystywana do pisania do
dowolnego, otwartego pliku, lub do obiektu plikopodobnego. W tym przypadku,
mozemy przekierowac pojedyncza instrukcje print do stderr bez wpływu na
nastepne instrukcje print.
Z innej strony, standardowe wejscia jest obiektem pliku tylko do odczytu i reprezentuje
dane przechodzace z niektórych wczesniejszych programów. Prawdopodobnie
222 ROZDZIAŁ 11. SKRYPTY I STRUMIENIE
nie jest to zrozumiałe dla klasycznych uzytkowników Mac OS-a lub nawet dla uzytkowników
Windows, którzy nie mieli za wiele do czynienia z linia polecen MS-DOS-a.
Działa to w ten sposób, ze konstruujemy ciag polecen w jednej linii, w taki sposób, ze
to co jeden program wypisuje na wyjscie, nastepny w tym ciagu traktuje jako wejscie.
Pierwszy program prosto wypisuje wszystko na standardowe wyjscie (bez korzystania
ze specjalnych przekierowan, wykorzystuje normalna instrukcje print itp.), a nastepny
program czyta ze standardowego wejscia, a system operacyjny udostepnia połaczenie
pomiedzy wyjsciem pierwszego programu, a wyjsciem kolejnego.
Przykład 10.12 Ciag polecen
[you@localhost kgp]$ python kgp.py -g binary.xml #(1)
01100111
[you@localhost kgp]$ cat binary.xml #(2)
<?xml version="1.0"?>
<!DOCTYPE grammar PUBLIC "-//diveintopython.org//DTD Kant Generator Pro v1.0//EN"
"kgp.dtd">
<grammar>
<ref id="bit">
<p>0</p>
<p>1</p>
</ref>
<ref id="byte">
<p><xref id="bit"/><xref id="bit"/><xref id="bit"/><xref id="bit"/>
<xref id="bit"/><xref id="bit"/><xref id="bit"/><xref id="bit"/></p>
</ref>
</grammar>
[you@localhost kgp]$ cat binary.xml | python kgp.py -g - #(3) (4)
10110001
1. Jak zobaczylismy w podrozdziale 9.1, “Nurkujemy”, polecenie to wyswietli ciag
osmiu przypadkowych bitów, 0 i 1.
2. Dzieki temu po prostu wypiszemy cała zawartosc pliku binary.xml. (Uzytkownicy
Windowsa powinni wykorzystac polecenie type zamiast cat.)
3. Polecenie to wypisuje zawartosc pliku kgp/binary.xml, ale znak “|” (ang. pipe),
oznacza, ze standardowe wyjscie nie zostanie wypisana na ekran. Zamiast tego,
zawartosc standardowego wyjscia zostanie wykorzystane jako standardowe wejscie
nastepnego programu, który w tym przypadku jest skryptem Pythona.
4. Zamiast okreslac modułu (np. binary.xml), dajemy “-”, który kaze naszemu
skryptowi wczytac gramatyke ze standardowego wejscia, zamiast z okreslonego
pliku na dysku. (Wiecej o tym, w jaki sposób to sie dzieje w nastepnym przykładzie.)
Zatem efekt bedzie taki sam, jak w pierwszym poleceniu, gdzie bezposrednio
okreslamy plik gramatyki, ale tutaj zwrócmy uwage na rozszerzone mozliwosci.
Zamiast wywoływac cat binary.xml, moglibysmy uruchomic skrypt,
który by dynamicznie generował gramatyke, a nastepnie mógłby ja doprowadzic
do naszego skryptu. Dane mogłyby przyjsc skadkolwiek: z bazy danych, innego
skryptu generujacego gramatyke lub jeszcze inaczej. Zaleta tego jest to, ze nie
musimy zmieniac w zaden sposób kgp/kgp.py, aby dołaczyc jakas funkcjonalnosc.
11.2. STANDARDOWY STRUMIEN WEJSCIA, WYJSCIA I BŁEDÓW 223
Jedynie, co potrzebujemy, to mozliwosc wczytania gramatyki ze standardowego
wejscia, a cała logike dodatkowej funkcjonalnosci mozemy rozdzielic wewnatrz
innego programu.
Wiec w jaki sposób skrypt “wie”, zeby czytac ze standardowego wejscia, gdy plik
gramatyki to “-”? To nie jest zadna magia; to tylko własnie prosty kod.
Przykład 10.13 Czytanie ze standardowego wejscia w kgp/kgp.py
def openAnything(source):
if source == "-": #(1)
import sys
return sys.stdin
# try to open with urllib (if source is http, ftp, or file URL)
import urllib
try:
[... ciach ...]
1. Jest to funkcja openAnything z kgp/toolbox.py, która wczesniej badalismy w
podrozdziale 10.1, , Abstrakcyjne zródła wejscia”. Wszystko, co musimy zrobic,
to dodanie trzech linii kodu na poczatku, aby sprawdzic, czy zródłem nie jest -";
jesli tak, to zwracamy sys.stdin. Naprawde, to tylko tyle! Pamietasz, stdin jest
obiektem plikopodobnym z metoda read, wiec pozostała czesc kodu (w kgp/kgp.py,
gdzie wywołujemy funkcje openAnything) w zaden sposób nie zmieniamy.
224 ROZDZIAŁ 11. SKRYPTY I STRUMIENIE
11.3 Buforowanie odszukanego wezła
Buforowanie odszukanego wezła
kgp/kgp.py stosuje kilka sztuczek, które moga, lecz nie musza, byc uzyteczne przy
przetwarzaniu XML-a. Pierwsza z nich wykorzystuje spójna strukture dokumentów
wejsciowych do utworzenia bufora wezłów.
Plik gramatyki definiuje szereg elementów ref. Kazdy z nich zawiera jeden lub
wiecej elementów p, które moga zawierac wiele róznych rzeczy, włacznie z elementami
xref. Gdy napotykamy element xref, wyszukujemy odpowiedni element ref z tym
samym atrybutem id i wybieramy jeden z elementów potomnych elementu ref i parsujemy
go. (W nastepnym podrozdziale zobaczymy jak dokonywany jest ten losowy
wybór.)
W taki sposób rozwijamy gramatyke: definiujemy elementy ref dla najmniejszych
czesci, nastepnie definiujemy elementy ref, które zawieraja te pierwsze elementy ref
poprzez uzycie xref itd. Potem parsujemy “najwieksza” referencje, przechodzimy po
kolei do kazdego elementu xref i ostatecznie generujemy prawdziwy tekst. Ten wygenerowany
tekst zalezy od tej (losowej) decyzji podjetej przy wypełnianiu elementu
xref, a wiec efekt moze byc inny za kazdym razem.
To wszystko jest bardzo elastyczne, ale ma jedna wade: wydajnosc. Gdy napotkamy
element xref i potrzebujemy odszukac odpowiedniego dla niego elementu ref, to pojawia
sie problem. Element xref ma atrybut id i chcemy odszukac element ref, który
ma taki sam atrybut id, ale nie ma prostego sposobu aby to zrobic. Powolnym sposobem
na zrobienie tego byłoby pobranie pełnej listy elementów ref za kazdym razem,
a nastepnie przeszukanie jej w petli pod katem atrybutu id. Szybkim sposobem jest
utworzenie takiej listy raz, a nastepnie utworzenie bufora w postaci słownika.
Przykład 10.14 loadGrammar
def loadGrammar(self, grammar):
self.grammar = self._load(grammar)
self.refs = {} #(1)
for ref in self.grammar.getElementsByTagName("ref"): #(2)
self.refs[ref.attributes["id"].value] = ref #(3) (4)
1. Rozpoczynamy od utworzenia pustego słownika self.refs.
2. Jak juz widzielismy w podrozdziale 9.5, “Wyszukiwanie elementów”,
getElementsByTagName zwraca liste wszystkich elementów o podanej nazwie.
Takze łatwo mozemy uzyskac liste wszystkich elementów ref, a nastepnie po
prostu przeszukac ja w petli.
3. Jak juz widzielismy w podrozdziale 9.6, “Dostep do atrybutów elementów”, mozemy
pobrac atrybut elementu poprzez nazwe uzywajac standardowej składni
słownikowej. Takze kluczami słownika self.refs beda wartosci atrybutu id
kazdego elementu ref.
4. Wartosciami słownika self.refs beda elementy ref jako takie. Jak juz widzielismy
w podrozdziale 9.3, “Parsowanie XML-a”, kazdy element, kazdy wezeł,
kazdy komentarz, kazdy kawałek tekstu w parsowanym dokumencie XML jest
obiektem.
11.3. BUFOROWANIE ODSZUKANEGO WEZŁA 225
Gdy tylko bufor (cache) zostanie utworzony, po napotkaniu elementu xref, aby
odnalezc element ref z takim samym atrybutem id, mozemy po prostu siegnac do
słownika self.refs.
Przykład 10.15 Uzycie bufora elementów ref
def do_xref(self, node):
id = node.attributes["id"].value
self.parse(self.randomChildElement(self.refs[id]))
Funkcje randomChildElement zgłebimy w nastepnym podrozdziale.
226 ROZDZIAŁ 11. SKRYPTY I STRUMIENIE
11.4 Wyszukanie bezposrednich elementów potomnych
Wyszukanie bezposrednich elementów potomnych
Inna przydatna technika przy parsowaniu dokumentów XML jest odnajdywanie
wszystkich bezposrednich elementów potomnych (dzieci) danego elementu. Na przykład
w pliku gramatyki element ref moze miec szereg elementów p, a kazdy z nich
moze zawierac wiele rzeczy, włacznie z innymi elementami p. Chcemy wyszukac tylko
te elementy p, które sa potomkami elementu ref, a nie elementy p, które sa potomkami
innych elementów p.
Pewnie myslisz, ze mozesz do tego celu po prostu uzyc funkcji getElementsByTagName,
ale niestety nie mozesz. Funkcja getElementsByTagName przeszukuje rekurencyjnie i
zwraca pojedyncza liste wszystkich elementów jakie znajdzie. Poniewaz elementy p
moga zawierac inne elementy p, nie mozemy uzyc funkcji getElementsByTagName.
Zwróciłaby ona zagniezdzone elementy p, a tego nie chcemy. Aby znalezc tylko bezposrednie
elementy potomne, musimy to wykonac samodzielnie.
Przykład 10.16 Wyszukanie bezposrednich elementów potomnych
def randomChildElement(self, node):
choices = [e for e in node.childNodes
if e.nodeType == e.ELEMENT_NODE] #(1) (2) (3)
chosen = random.choice(choices) #(4)
return chosen
1. Jak juz widzielismy w przykładzie 9.9, “Pobieranie wezłów potomnych”, atrybut
childNodes zwraca liste wszystkich elementów potomnych danego elementu.
2. Jednakze, jak juz widziałes w przykładzie 9.11, “Wezłami potomnymi moze byc
takze tekst”, lista zwrócona przez childNodes zawiera cała róznorodnosc typów
wezłów, właczajac to wezły tekstowe. W tym wypadku szukamy jednak tylko
potomków, które sa elementami.
3. Kazdy wezeł posiada atrybut nodeType, który moze przyjmowac wartosci
ELEMENT NODE, TEXT NODE, COMMENT NODE i wiele innych. Pełna lista mozliwych
wartosci znajduje sie w pliku init .py w pakiecie xml.dom. (Zajrzyj do podrozdziału
9.2, “Pakiety”, aby sie wiecej dowiedziec o pakietach.) Ale my jestesmy
zainteresowani wezłami, które sa elementami, a wiec mozemy odfiltrowac z listy
tylko te elementy, których atrybut nodeType jest równy ELEMENT NODE.
4. Gdy tylko mamy juz liste własciwych elementów, wybór losowego elementu jest
łatwy. Python udostepnia moduł o nazwie random, który zawiera kilka funkcji.
Funkcja random.choice pobiera liste z dowolna iloscia elementów i zwraca
losowy element. Np. jesli element ref zawiera kilka elementów p, to choices bedzie
lista elementów p, a do chosen zostanie przypisany dokładnie jeden z nich,
wybrany losowo.
11.5. TWORZENIE ODDZIELNYCH FUNKCJI OBSŁUGI WZGLEDEM TYPUWEZŁA227
11.5 Tworzenie oddzielnych funkcji obsługi wzgledem
typu wezła
Tworzenie oddzielnych funkcji obsługi wzgledem typu wezła
Trzecim uzytecznym chwytem podczas przetwarzania XML-a jest podzielenie kodu
w logiczny sposób na funkcje oparte na typie wezła i nazwie elementu. Parsujac dokument
przetwarzamy rozmaite typy wezłów, które sa reprezentowane przez obiekty
Pythona. Poziom główny dokumentu jest bezposrednio reprezentowany przez obiekt
klasy Document. Z kolei Document zawiera jeden lub wiecej obiektów klasy Element
(reprezentujace znaczniki XML-a), a kazdy z nich moze zawierac inne obiekty klasy
Element, obiekty klasy Text (fragmenty tekstu), czy obiektów Comment (osadzone
komentarze w dokumencie). Python pozwala w łatwy sposób napisac funkcje posredniczaca,
która rozdziela logike dla kazdego rodzaju wezła.
Przykład 10.17 Nazwy klas parsowanych obiektów XML
>>> from xml.dom import minidom
>>> xmldoc = minidom.parse(’kant.xml’) #(1)
>>> xmldoc
<xml.dom.minidom.Document instance at 0x01359DE8>
>>> xmldoc.__class__ #(2)
<class xml.dom.minidom.Document at 0x01105D40>
>>> xmldoc.__class__.__name__ #(3)
’Document’
1. Załózmy na moment, ze kgp/kant.xml jest w biezacym katalogu.
2. Jak powiedzielismy w podrozdziale “Pakiety”, obiekt zwrócony przez parsowany
dokument jest instancja klasy Document, która została zdefiniowana w
minidom.py w pakiecie xml.dom. Jak zobaczylismy w podrozdziale “Tworzenie
instancji klasy”, class jest wbudowanym atrybutem kazdego obiektu Pythona.
3. Ponadto name jest wbudowanym atrybutem kazdej klasy Pythona. Atrybut
ten przechowuje napis, a napis ten nie jest niczym tajemniczym, jest po prostu
nazwa danej klasy. (Zobacz podrozdział “Definiowanie klas”.)
To fajnie, mozemy pobrac nazwe klasy dowolnego wezła XML-a (poniewaz wezły
sa reprezentowane przez Pythonowe obiekty). Jak mozna wykorzystac te zalete, aby
rozdzielic logike parsowania dla kazdego typu wezła? Odpowiedzia jest getattr, który
pierwszy raz zobaczylismy w podrozdziale “Funkcja getattr”.
Przykład 10.18 parse, ogólna funkcja posredniczaca dla wezła XML
def parse(self, node):
parseMethod = getattr(self, "parse_%s" % node.__class__.__name__) #(1) (2)
parseMethod(node) #(3)
1. Od razu, zauwazmy, ze konstruujemy dłuzszy napis oparty na nazwie klasy przekazanego
wezła (jako argument node). Zatem, jesli przekazemy wezeł Documentu,
konstruujemy napis ’parse Document’ itd.
228 ROZDZIAŁ 11. SKRYPTY I STRUMIENIE
2. Teraz, jesli potraktujemy te nazwe jako nazwe funkcji, otrzymamy dzieki getattr
referencje do funkcji.
3. Ostatecznie, mozemy wywołac te funkcje, przekazujac sam node jako argument.
Nastepny przykład przedstawia definicje tych funkcji.
Przykład 10.19 Funkcje wywoływane przez funkcje posredniczaca parse
def parse_Document(self, node): #(1)
self.parse(node.documentElement)
def parse_Text(self, node): #(2)
text = node.data
if self.capitalizeNextWord:
self.pieces.append(text[0].upper())
self.pieces.append(text[1:])
self.capitalizeNextWord = 0
else:
self.pieces.append(text)
def parse_Comment(self, node): #(3)
pass
def parse_Element(self, node): #(4)
handlerMethod = getattr(self, "do_%s" % node.tagName)
handlerMethod(node)
1. parse Document jest wywołany tylko raz, poniewaz jest tylko jeden wezeł klasy
Document w dokumencie XML i tylko jeden obiekt klasy Document w przeparsowanej
reprezentacji XML-a. Tu po prostu idziemy dalej i parsujemy czesc główna
pliku gramatyki.
2. parse Text jest wywoływany tylko na wezłach reprezentujacych fragmenty tekstu.
Funkcja wykonuje kilka specjalnych operacji zwiazanych z automatycznym
wstawianiem duzej litery na poczatku słowa pierwszego zdania, ale w innym
wypadku po prostu dodaje reprezentowany tekst do listy.
3. parse Comment jest tylko “przejazdzka”; metoda ta nic nie robi, poniewaz nie
musimy sie troszczyc o komentarze wstawione w plikach definiujacym gramatyke.
Pomimo tego, zauwazmy, ze nadal musimy zdefiniowac funkcje i wyraznie stwierdzic,
zeby nic nie robiła. Jesli funkcja nie bedzie istniała, funkcja parse nawali
tak szybko, jak napotka sie na komentarz, poniewaz bedzie próbowała znalezc
nieistniejaca funkcje parse Comment. Definiujac oddzielna funkcje dla kazdego
typu wezła, nawet jesli nam ta funkcja nie jest potrzebna, pozwalamy ogólnej
funkcji parsujacej byc prosta i krótka.
4. Metoda parse Element jest w rzeczywistosci funkcja posredniczaca, oparta na
nazwie znacznika elementu. Idea jest taka sama: wez odrózniajace sie od siebie
elementy (elementy, które róznia sie nazwa znacznika) i wyslij je do odpowiedniej,
odrebnej funkcji. Konstruujemy napis typu ’do xref’ (dla znacznika <xref>),
znajdujemy funkcje o takiej nazwie i wywołujemy ja. I robimy podobnie dla
11.5. TWORZENIE ODDZIELNYCH FUNKCJI OBSŁUGI WZGLEDEM TYPUWEZŁA229
kazdej innej nazwy znacznika, która zostanie znaleziona, oczywiscie w pliku gramatyki
(czyli znaczniki <p>, czy tez <choice>).
W tym przykładzie funkcja posredniczaca parse i parse Element po prostu znajduja
inne metody w tej samej klasie. Jesli przetwarzanie jest bardzo złozone (lub mamy
bardzo duzo nazw znaczników), powinnismy rozdzielic swój kod na kilka oddzielnych
modułów i wykorzystac dynamiczne importowanie, aby zaimportowac kazdy moduł,
a nastepnie wywołac wszystkie potrzebne nam funkcje. Dynamiczne importowanie zostanie
omówione w rozdziale “Programowanie funkcyjne”.
230 ROZDZIAŁ 11. SKRYPTY I STRUMIENIE
11.6 Obsługa argumentów linii polecen
Obsługa argumentów linii polecen
Python całkowicie wspomaga tworzenie programów, które moga zostac uruchomione
z linii polecen, łacznie z argumentami linii polecen, czy zarówno z krótkim lub
długim stylem flag, które okreslaja opcje. Nie ma to nic wspólnego z XML-em, ale
omawiany skrypt wykorzystuje w dobry sposób linie polecen, dlatego tez nadeszła
odpowiednia pora, aby to omówic.
Ciezko mówic o linii polecen bez wiedzy, w jaki sposób argumenty linii polecen sa
ujawniane do programu, dlatego tez napiszmy prosty program, aby to zobaczyc.
Przykład 10.20 Wprowadzenie do sys.argv
#argecho.py
import sys
for arg in sys.argv: #(1)
print arg
1. Kazdy argument linii polecen przekazany do programu, zostanie umieszczony w
sys.argv, który jest własciwie lista.Wtym miejscu wypisujemy kazdy argument
w oddzielnej linii.
Przykład 10.21 Zawartosc sys.argv
[you@localhost py]$ python argecho.py #(1)
argecho.py
[you@localhost py]$ python argecho.py abc def #(2)
argecho.py
abc
def
[you@localhost py]$ python argecho.py −−help #(3)
argecho.py
−−help
[you@localhost py]$ python argecho.py −m kant.xml #(4)
argecho.py
−m
kant.xml
1. Najpierw musimy sobie uswiadomic, ze sys.argv przechowuje nazwe uruchomionego
skryptu. Wiedze te wykorzystamy pózniej, w rozdziale “Programowanie
funkcyjne”. Na razie nie zamartwiaj sie tym.
2. Argumenty linii polecen sa oddzielane przez spacje i kazdy z nich ukazuje sie w
liscie sys.argv jako oddzielny argument.
3. Flagi linii polecen np. −−help, takze pokaza sie jako osobne elementy w sys.argv.
4. Zeby było ciekawiej, niektóre z flag linii polecen same przyjmuja, wymagaja
argumentów. Na przykład, tutaj mamy jedna flage (−m), która na dodatek takze
przyjmuje argument (w przykładzie kant.xml). Zarówno flaga sama w sobie, a
11.6. OBSŁUGA ARGUMENTÓW LINII POLECEN 231
takze argument flagi sa kolejnymi elementami w liscie sys.argv. Python w zaden
sposób nie bedzie próbował ich powiazac; otrzymamy sama liste.
Jak mozemy zobaczyc, z pewnoscia mamy wszystkie informacje przekazane do linii
polecen, ale nie wygladaja na tak proste, aby z nich faktycznie skorzystac. Dla
nieskomplikowanych programów, które przyjmuja tylko jeden argument bez zadnych
flag, mozemy po prostu wykorzystac sys.argv[1], aby sie do niego dostac. Nie ma sie
czym wstydzic. Dla bardziej złozonych programów bedzie potrzebny moduł getopt.
Przykład 10.22 Wprowadzenie do getopt
def main(argv):
grammar = "kant.xml" #(1)
try:
opts, args = getopt.getopt(argv, "hg:d", ["help", "grammar="]) #(2)
except getopt.GetoptError: #(3)
usage() #(4)
sys.exit(2)
[...]
if __name__ == "__main__":
main(sys.argv[1:])
1. Od razu zobacz na sam dół przykładu. Wywołujemy funkcje main z argumentem
sys.argv[1:]. Zapamietaj, sys.argv[0] jest nazwa skryptu, który uruchomilismy;
nie martwimy sie o to, w jaki sposób jest przetwarzana linia polecen, wiec
odcinamy i przekazujemy reszte listy.
2. To najciekawsze miejsce w tym przykładzie. Funkcja getopt modułu getopt
przyjmuje trzy parametry: liste argumentów (która otrzymalismy z sys.argv[1:]),
napis zawierajacy wszystkie mozliwe jedno-znakowe flagi, które program akceptuje,
a takze liste dłuzszych flag, które sa odpowiednikami krótszych, jednoznakowych
wersji. Na pierwszy rzut oka wydaje sie to troche zamotane, ale w
dalszej czesci szerzej to omówimy.
3. Jesli cos nie poszło pomyslnie podczas parsowania flag linii polecen, getopt
rzuca wyjatek, który nastepnie przechwytujemy. Informujemy funkcje getopt
o wszystkich flagach, które rozumie nasz program, zatem ostatecznie oznacza, ze
uzytkownik przekazał niektóre niezrozumiałe przez nas flagi.
4. Jest to praktycznie standard wykorzystywany w swiecie Uniksa, kiedy do skryptu
zostana przekazane niezrozumiałe flagi, wypisujemy streszczona pomoc dotyczaca
uzycia programu i wdziecznie zakanczamy go. Dodajmy, ze nie przedstawilismy
tutaj funkcji usage. Jeszcze trzeba bedzie ja gdzies zaimplementowac,
aby wypisywała streszczenie pomocy; nie dzieje sie to automatycznie.
Wiec czym sa te wszystkie parametry przekazane do funkcji getopt? A wiec, pierwszy
jest po prostu surowa lista argumentów i flag przekazanych do linii polecen (bez
pierwszego elementu, czyli nazwy skryptu, który wycielismy przed wywołaniem funkcji
main). Drugi parametr jest lista krótkich flag linii polecen, które akceptuje skrypt.
232 ROZDZIAŁ 11. SKRYPTY I STRUMIENIE
"hg:d"
-h
wyswietla streszczona pomoc
-g ...
korzysta z okreslonego pliku gramatyki lub URL-a
-d
pokazuje informacje debugujace podczas parsowania
Pierwsza i trzecia flaga sa zwykłymi, samodzielnymi flagami; mozemy je okreslic lub
nie. Flagi te wykonuja pewne czynnosci (wypisuja pomoc) lub zmieniaja stan (właczaja
debugowanie). Jakkolwiek, za druga flaga (-g) musi sie znalezc pewien argument, który
bedzie nazwa pliku gramatyki, który ma zostac przeczytany. W rzeczywistosci moze
byc nazwa pliku lub adresem strony strony web, ale my jeszcze nie wiemy, czym jest
(bedziemy z tym kombinowac pózniej), ale wiemy, ze ma byc czyms. Poinformowalismy
getopt o tym, ze ma byc cos za ta flaga, poprzez wstawienie w drugim parametrze
dwukropka po literze g.
Zeby to bardziej skomplikowac, skrypt akceptuje zarówno krótkie flagi (np. -h, jak
i długie flagi (jak --help), a my chcemy, zeby słuzyły one do tego samego. I po to jest
trzeci parametr w getopt. Okresla on liste długich flag, które odpowiadaja krótkim
flagom zdefiniowanym w drugim parametrze.
["help", "grammar="]
--help
wyswietla streszczona pomoc
--grammar ...
korzysta z okreslonego pliku gramatyki lub URL-a
Zwrócmy uwage na trzy sprawy:
1. Wszystkie długie flagi w linii polecen sa poprzedzone dwoma myslnikami, ale
podczas wywoływania getopt nie dołaczamy tych myslników.
2. Po fladze --grammar musi zawsze wystapic dodatkowy argument, identycznie jak
z flaga -g. Informujemy o tym poprzez znak równosci w "grammar=".
3. Lista długich flag jest krótsza niz lista krótkich flag, poniewaz flaga -d nie ma
swojego dłuzszego odpowiednika. Jedynie -d bedzie właczał debugowanie. Jednak
porzadek krótkich i długich flag musi byc ten sam, dlatego tez najpierw musimy
okreslic wszystkie krótkie flagi odpowiadajace dłuzszym flagom, a nastepnie
pozostała czesc krótszych flag, które nie maja swojego dłuzszego odpowiednika.
Jeszcze sie nie pogubiłes? To spójrz na własciwy kod i zobacz, czy nie staje sie dla
ciebie zrozumiały.
Przykład 10.23 Obsługa argumentów linii polecen w kgp/kgp.py
def main(argv): #(1)
grammar = "kant.xml"
try:
opts, args = getopt.getopt(argv, "hg:d", ["help", "grammar="])
11.6. OBSŁUGA ARGUMENTÓW LINII POLECEN 233
except getopt.GetoptError:
usage()
sys.exit(2)
for opt, arg in opts: #(2)
if opt in ("-h", "--help"): #(3)
usage()
sys.exit()
elif opt == ’-d’: #(4)
global _debug
_debug = 1
elif opt in ("-g", "--grammar"): #(5)
grammar = arg
source = "".join(args) #(6)
k = KantGenerator(grammar, source)
print k.output()
1. Zmienna grammar bedzie przechowywac sciezke do pliku gramatyki, z którego
bedziemy korzystac. W tym miejscu inicjalizujemy ja tak, aby w przypadku, gdy
nie zostanie okreslona w linii polecen (za pomoca flagi -g lub --grammar) miała
jakas domyslna wartosc.
2. Zmienna opts, która otrzymujemy z wartosc zwróconej przez getopt, przechowuje
liste krotek: flage i argument. Jesli flaga nie przyjmuje argumentu, to argument
bedzie miał wartosc None. Ułatwia to wykonywanie petli na flagach.
3. getopt kontroluje, czy flagi linii polecen sa akceptowalne, ale nie wykonuje zadnej
konwersji miedzy długimi, a krótkimi flagami. Jesli okreslimy flage -h, opt
bedzie zawierac -h", natomiast jesli okreslimy flage --help, opt bedzie zawierac
--help". Zatem musimy kontrolowac obydwa warianty.
4. Pamietamy, ze fladze -d nie odpowiada zadna dłuzsza wersja, dlatego tez kontrolujemy
tylko te krótka flage. Jesli zostanie ona odnaleziona, ustawiamy globalna
zmienna, do której pózniej bedziemy sie odwoływac, aby wypisywac informacje
debugujace. (Flaga ta była wykorzystywana podczas projektowania skryptu. A
co, myslisz, ze wszystkie przedstawione przykłady działały od razu??)
5. Jesli znajdziemy plik gramatyki spotykajac flage -g lub −−grammar, zapisujemy
argument, który nastepuje po tej fladze (przechowywany w zmiennej arg), do
zmiennej grammar, nadpisujac przy tym domyslna wartosc, zainicjalizowana na
poczatku funkcji main.
6. Ok. Wykonalismy petle przez wszystkie flagi i przetworzylismy je. Oznacza to,
ze pozostała czesc musi byc argumentami linii polecen, a zostały one zwrócone
przez funkcje getopt do zmiennej args. W tym przypadku traktujemy je jako
materiał zródłowy dla parsera. Jesli nie zostały okreslone zadne argumenty linii
polecen, args bedzie pusta lista, wiec source w wyniku tego bedzie pustym
napisem.
234 ROZDZIAŁ 11. SKRYPTY I STRUMIENIE
11.7 Skrypty i strumienie - wszystko razem
Wszystko razem
Przemierzylismy kawał drogi. Zatrzymajmy sie na chwile i zobaczmy jak te wszystkie
elementy do siebie pasuja. Zaczniemy od skryptu, który pobiera argumenty z linii
polecen uzywajac modułu getopt.
def main(argv):
...
try:
opts, args = getopt.getopt(argv, "hg:d", ["help", "grammar="])
except getopt.GetoptError:
...
for opt, arg in opts:
...
Tworzymy nowa instancje klasy KantGenerator i przekazujemy jej plik z gramatyka
oraz zródło, które moze byc, ale nie musi, podane w linii polecen.
k = KantGenerator(grammar, source)
Instancja klasy KantGenerator automatycznie wczytuje gramatyke, która jest plikiem
XML. Wykorzystujemy nasza funkcje openAnything do otwarcia pliku (który
moze byc ulokowany lokalnie lub na zdalnym serwerze), nastepnie uzywamy wbudowanego
zestawu funkcji parsujacych minidom do sparsowania XML-a do postaci drzewa
obiektów Pythona.
def _load(self, source):
sock = toolbox.openAnything(source)
xmldoc = minidom.parse(sock).documentElement
sock.close()
Ach i po drodze wykorzystujemy nasza wiedze o strukturze dokumentu XML do
utworzenia małego bufora referencji, którymi sa po prostu elementy dokumentu XML.
def loadGrammar(self, grammar):
for ref in self.grammar.getElementsByTagName("ref"):
self.refs[ref.attributes["id"].value] = ref
Jesli został podany jakis materiał zródłowy w linii polecen, uzywamy go. W przeciwnym
razie na podstawie gramatyki wyszukujemy referencje na najwyzszym poziomie
(ta do której nie maja odnosników zadne inne elementy) i uzywamy jej jako punktu
startowego.
def getDefaultSource(self):
xrefs = {}
for xref in self.grammar.getElementsByTagName("xref"):
xrefs[xref.attributes["id"].value] = 1
xrefs = xrefs.keys()
standaloneXrefs = [e for e in self.refs.keys() if e not in xrefs]
return ’<xref id="%s"/>’ % random.choice(standaloneXrefs)
11.7. SKRYPTY I STRUMIENIE - WSZYSTKO RAZEM 235
Teraz przedzieramy sie przez materiał zródłowy. Ten materiał to takze XML i
parsujemy go wezeł po wezle. Aby podzielic nieco kod i uczynic go łatwiejszym w
utrzymaniu, uzywamy oddzielnych funkcji obsługi (ang. handlers) dla kazdego typu
wezła.
def parse_Element(self, node):
handlerMethod = getattr(self, "do_%s" % node.tagName)
handlerMethod(node)
Przelatujemy przez gramatyke, parsujac wszystkie elementy potomne kazdego elementu
p,
def do_p(self, node):
...
if doit:
for child in node.childNodes: self.parse(child)
zastepujac elementy choice losowym elementem potomnym,
def do_choice(self, node):
self.parse(self.randomChildElement(node))
i zastepujac elementy xref losowym elementem potomnym odpowiedniego elementu
ref, który wczesniej został zachowany w buforze.
def do_xref(self, node):
id = node.attributes["id"].value
self.parse(self.randomChildElement(self.refs[id]))
W koncu parsujemy wszystko do zwykłego tekstu,
def parse_Text(self, node):
text = node.data
...
self.pieces.append(text)
który wypisujemy.
def main(argv):
...
k = KantGenerator(grammar, source)
print k.output()
236 ROZDZIAŁ 11. SKRYPTY I STRUMIENIE
11.8 Skrypty i strumienie - podsumowanie
Wszystko razem
Python posiada zestaw poteznych bibliotek do parsowania i manipulacji dokumentami
XML. Moduł minidom parsuje plik XML zamieniajac go w obiekt Pythona i
pozwalajac na swobodny dostep do dowolnych jego elementów. Idac dalej, w rozdziale
tym pokazalismy, w jaki sposób uzyc Pythona do tworzenia “prawdziwych” skryptów
linii polecen przyjmujacych argumenty, obsługujacych rózne błedy, a nawet potrafiacych
pobrac dane z wyjscia innego programu.
Zanim przejdziemy do nastepnego rozdziału, powinnismy z łatwoscia:
• posługiwac sie standardowym strumien wejscia, wyjscia i błedów.
• definiowac dynamiczne funkcje posredniczace za pomoca funkcji getattr.
• korzystac z argumentów linii polecen i sprawdzac ich poprawnosc za pomoca
modułu getopt.
Rozdział 12
HTTP
237
238 ROZDZIAŁ 12. HTTP
12.1 HTTP
Nurkujemy
Do tej pory nauczylismy sie juz przetwarzac HTML i XML, zobaczylismy jak pobrac
strone internetowa, a takze jak parsowac dane XML pobrane poprzez URL. Zagłebmy
sie teraz nieco bardziej w tematyke usług sieciowych HTTP.
W uproszczeniu, usługi sieciowe HTTP sa programistycznym sposobem wysyłania
i odbierania danych ze zdalnych serwerów, wykorzystujac do tego bezposrednio
transmisje po HTTP. Jezeli chcesz pobrac dane z serwera, uzyj po prostu metode
GET protokołu HTTP; jezeli chcesz wysłac dane do serwera, uzyj POST. (Niektóre,
bardziej zaawansowane API serwisów HTTP definiuja takze sposób modyfikacji i usuwania
istniejacych danych – za pomoca metod HTTP PUT i DELETE). Innymi słowy,
“czasowniki” wbudowane w protokół HTTP (GET, POST, PUT i DELETE) posrednio przekształcaja
operacje HTTP na operacje na poziomie aplikacji: odbierania, wysyłania,
modyfikacji i usuwania danych.
Główna zaleta tego podejscia jest jego prostota. Popularnosc tego rozwiazania została
udowodniona poprzez ogromna liczbe róznych witryn. Dane – najczesciej w formacie
XML – moga byc wygenerowane i przechowane statycznie, albo tez generowane
dynamicznie poprzez skrypty po stronie serwera, i inne popularne jezyki, właczajac w
to biblioteke HTTP. Łatwiejsze jest takze debugowanie, poniewaz mozemy wywołac
dowolna usługe sieciowa w dowolnej przegladarce internetowej i obserwowac zwracane
surowe dane. Współczesne przegladarki takze czytelnie sformatuja otrzymane dane
XML, i pozwola na szybka nawigacje wsród nich.
Przykłady uzycia czystych usług sieciowych typu XML poprzez HTTP:
• Amazon API (http://www.amazon.com/webservices) pozwala na pobieranie informacji
o produktach oferowanych w sklepie Amazon.com
• National Weather Service (http://www.nws.noaa.gov/alerts/) (United States) i
Hong Kong Observatory (http://demo.xml.weather.gov.hk/) (Hong Kong) oferuje
informowanie o pogodzie w formie usługi sieciowej
• Atom API (http://atomenabled.org/) – zarzadzanie zawartoscia stron www
W kolejnych rozdziałach zapoznamy sie z róznymi API, które wykorzystuja protokół
HTTP jako nosnik do wysyłania i odbierania danych, ale które nie przekształcaja
operacji na poziomie aplikacji na operacje w HTTP (zamiast tego tuneluja wszystko
poprzez HTTP POST). Ale ten rozdział koncentruje sie na wykorzystywaniu metody
GET protokołu HTTP do pobierania danych z serwera – poznamy kilka cech HTTP,
które pozwola nam jak najlepiej wykorzystac mozliwosci czystych usług sieciowych
HTTP.
Ponizej jest bardziej zaawansowana wersja modułu openanything.py, który przedstawilismy
w poprzednim rozdziale:
import urllib2, urlparse, gzip
from StringIO import StringIO
USER_AGENT = ’OpenAnything/%s +http://diveintopython.org/http_web_services/’
% __version__
class SmartRedirectHandler(urllib2.HTTPRedirectHandler):
12.1. NURKUJEMY 239
def http_error_301(self, req, fp, code, msg, headers):
result = urllib2.HTTPRedirectHandler.http_error_301(
self, req, fp, code, msg, headers)
result.status = code
return result
def http_error_302(self, req, fp, code, msg, headers):
result = urllib2.HTTPRedirectHandler.http_error_302(
self, req, fp, code, msg, headers)
result.status = code
return result
class DefaultErrorHandler(urllib2.HTTPDefaultErrorHandler):
def http_error_default(self, req, fp, code, msg, headers):
result = urllib2.HTTPError(
req.get_full_url(), code, msg, headers, fp)
result.status = code
return result
def openAnything(source, etag=None, lastmodified=None, agent=USER_AGENT):
u"""URL, nazwa pliku lub łancuch znaków --> strumien
Funkcja ta pozwala tworzyc parsery, które przyjmuja jakies zródło wejscia
(URL, sciezke do pliku lokalnego lub gdzies w sieci lub dane w postaci łancucha znaków),
a nastepnie zaznajamia sie z nim w odpowiedni sposób. Zwracany obiekt bedzie
posiadał wszystkie podstawowe metody czytania wejscia (read, readline, readlines).
Ponadto korzystamy z .close(), gdy obiekt juz nam nie bedzie potrzebny.
Kiedy zostanie podany argument etag, zostanie on wykorzystany jako wartosc
nagłówka zadania URL-a If-None-Match.
Jesli argument lastmodified zostanie podany, musi byc on formie
łancucha znaków okreslajacego czas i date w GMT.
Data i czas sformatowana w tym łancuchu zostanie wykorzystana
jako wartosc nagłówka zadania If-Modified-Since.
Jesli argument agent zostanie okreslony, bedzie on wykorzystany
w nagłówku zadania User-Agent.
"""
if hasattr(source, ’read’):
return source
if source == ’-’:
return sys.stdin
if urlparse.urlparse(source)[0] == ’http’:
# otwiera URL za pomoca urllib2
request = urllib2.Request(source)
request.add_header(’User-Agent’, agent)
240 ROZDZIAŁ 12. HTTP
if lastmodified:
request.add_header(’If-Modified-Since’, lastmodified)
if etag:
request.add_header(’If-None-Match’, etag)
request.add_header(’Accept-encoding’, ’gzip’)
opener = urllib2.build_opener(SmartRedirectHandler(), DefaultErrorHandler())
return opener.open(request)
# próbuje otworzyc za pomoca wbudowanej funkcji open (jesli source to nazwa pliku)
try:
return open(source)
except (IOError, OSError):
pass
# traktuje source jak łancuch znaków
return StringIO(str(source))
def fetch(source, etag=None, lastmodified=None, agent=USER_AGENT):
u"""Pobiera dane z URL, pliku, strumienia lub łancucha znaków"""
result = {}
f = openAnything(source, etag, lastmodified, agent)
result[’data’] = f.read()
if hasattr(f, ’headers’):
# zapisuje ETag, jesli go wysłał do nas serwer
result[’etag’] = f.headers.get(’ETag’)
# zapisuje nagłówek Last-Modified, jesli został do nas wysłany
result[’lastmodified’] = f.headers.get(’Last-Modified’)
if f.headers.get(’content-encoding’) == ’gzip’:
# odkompresowuje otrzymane dane, poniewaz sa one zakompresowane jako gzip
result[’data’] = gzip.GzipFile(fileobj=StringIO(result[’data’])).read()
if hasattr(f, ’url’):
result[’url’] = f.url
result[’status’] = 200
if hasattr(f, ’status’):
result[’status’] = f.status
f.close()
return result
12.2. JAK NIE POBIERAC DANYCH POPRZEZ HTTP 241
12.2 Jak nie pobierac danych poprzez HTTP
Jak nie pobierac danych poprzez HTTP
Załózmy, ze chcemy pobrac jakis zasób poprzez HTTP, jak np. RSS (Really Simple
Syndication). Jednak nie chcemy pobrac go tylko jednorazowo, lecz cyklicznie, np. co
godzine, aby miec najswiezsze informacje ze strony, która udostepnia RSS. Zróbmy to
najpierw w bardzo prosty i szybki sposób, a potem zobaczymy jak mozna to zrobic
lepiej.
Przykład 11.2 Pobranie RSS w szybki i prosty sposób
>>> import urllib
>>> data = urllib.urlopen(’http://diveintomark.org/xml/atom.xml’).read() #(1)
>>> print data
<?xml version="1.0" encoding="iso-8859-1"?>
<feed version="0.3"
xmlns="http://purl.org/atom/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xml:lang="en">
<title mode="escaped">dive into mark</title>
<link rel="alternate" type="text/html" href="http://diveintomark.org/"/>
[... ciach ...]
1. Pobranie czegokolwiek poprzez HTTP jest w Pythonie niesamowicie łatwe; własciwie
jest to jedna linijka kodu. Moduł urllib ma bardzo poreczna funkcje
urlopen, która przyjmuje na wejsciu adres strony, a zwraca obiekt pliko-podobny,
z którego mozna odczytac cała zawartosc strony przy pomocy metody read. Prosciej
juz nie moze byc.
A wiec co jest z tym nie tak? Cóz, do szybkiej próby podczas testowania, czy programowania,
to w zupełnosci wystarczy. Chcielismy zawartosc RSS-a i mamy ja. Ta
sama technika działa dla kazdej strony internetowej. Jednak gdy zaczniemy myslec w
kategoriach serwisów internetowych, z których chcemy korzystac regularnie – pamietajmy,
ze chcielismy pobierac tego RSS-a co godzine – wtedy takie działanie staje sie
niewydajne i prymitywne.
Porozmawiajmy o podstawowych cechach HTTP.
242 ROZDZIAŁ 12. HTTP
12.3 Własciwosci HTTP
Mozemy wyróznic piec waznych własciwosci HTTP, z których powinnismy korzystac.
User-Agent
User-Agent jest to po prostu sposób w jaki klient moze poinformowac serwer kim
jest w trakcie zadania strony internetowej, RSS (Really Simple Syndication) lub jakiejkolwiek
usługi internetowej poprzez HTTP. Gdy klient zgłasza zadanie do danego
zasobu, to zawsze powinien informowac kim jest tak szczegółowo, jak to tylko mozliwe.
Pozwala to administratorowi serwera na skontaktowanie sie z programista, twórca
aplikacji klienckiej, jesli cos idzie nie tak.
Domyslnie Python wysyła standardowy nagłówek postaci User-Agent:
Python-urllib/1.15. W nastepnej sekcji zobaczymy jak to zmienic na cos bardziej
szczegółowego.
Przekierowania
Czasami zasoby zmieniaja połozenie. Witryny internetowe sa reorganizowane
a strony przesuwane pod nowe adresy. Nawet usługi internetowe ulegaja
reorganizacji. RSS spod adresu http://example.com/index.xml moze sie
przesunac do http://example.com/xml/atom.xml. Moze sie takze przesunac
cała domena, gdy reorganizacja jest przeprowadzana na wieksza skale, np.
http://www.example.com/index.xml moze zostac przekierowana do http://serverfarm-
1.example.com/index.xml.
Zawsze gdy zadamy jakiegos zasobu od serwera HTTP, serwer ten dołacza do swojej
odpowiedzi kod statusu. Kod statusu 200 oznacza “wszystko w porzadku, oto strona o
która prosilismy”. Kod statusu 404 oznacza “strona nieznaleziona”. (Prawdopodobnie
spotkalismy sie z błedami 404 podczas surfowania w sieci.)
Protokół HTTP ma dwa rózne sposoby, aby dac do zrozumienia, ze dany zasób
został przesuniety. Kod statusu 302 jest to tymczasowe przekierowanie i oznacza on
“ups, to zostało tymczasowo przesuniete tutaj” (a ten tymczasowy adres umieszczany
jest w nagłówku Location:). Kod statusu 301 jest to przekierowanie trwałe i oznacza
“ups, to zostało przesuniete na stałe” (a nowy adres jest podawany w nagłówku
Location:). Gdy otrzymamy kod statusu 302 i nowy adres, to specyfikacja HTTP
mówi, ze powinnismy uzyc nowego adresu, aby pobrac to czego dotyczyło zadanie, ale
nastepnym razem przy próbie dostepu do tego samego zasobu powinnismy spróbowac
ponownie starego adresu. Natomiast gdy dostaniemy kod statusu 301 i nowy adres, to
powinnismy juz od tego momentu uzywac tylko tego nowego adresu.
urllib.urlopen automatycznie “sledzi” przekierowania, jesli otrzyma stosowny
kod statusu od serwera HTTP, ale niestety nie informuje o tym fakcie. Ostatecznie
otrzymujemy dane, o które prosilismy, ale nigdy nie wiadomo, czy biblioteka nie podazyła
samodzielnie za przekierowaniem. Tak wiec nieswiadom niczego dalej mozemy
próbowac korzystac ze starego adresu i za kazdym razem nastapi przekierowanie pod
nowy adres. To powoduje wydłuzenie drogi, co nie jest zbyt wydajne! W dalszej czesci
tego rozdziału zobaczymy, jak sobie radzic z trwałymi przekierowaniami własciwie i
wydajnie.
12.3. WŁASCIWOSCI HTTP 243
Last-Modified/If-Modified-Since
Niektóre dane zmieniaja sie bez przerwy. Strona domowa cnn.com jest aktualizowana
co piec minut. Z drugiej strony strona domowa google.com zmienia sie raz na
kilka tygodni (gdy wrzucaja jakies swiateczne logo lub reklamuja jakas nowa usługe).
Usługi internetowe nie róznia sie pod tym wzgledem. Serwer zwykle wie kiedy dane,
które pobieramy sie ostatnio zmieniły, a protokół HTTP pozwala serwerowi na dołaczenie
tej daty ostatniej modyfikacji do zadanych danych.
Gdy poprosimy o te same dane po raz drugi (lub trzeci, lub czwarty), mozemy
podac serwerowi date ostatniej modyfikacji (ang. last-modified date), która dostalismy
ostatnim razem. Wysyłamy serwerowi nagłówek If-Modified-Since wraz ze swoim
zadaniem, wraz z data otrzymana od serwera ostatnim razem. Jesli dane nie uległy
zmianie od tamtego czasu, to serwer odsyła specjalny kod statusu 304, który oznacza
“dane nie zmieniły sie od czasu, gdy o nie ostatnio pytałes”. Dlaczego jest to lepsze
rozwiazanie? Bo gdy serwer odsyła kod 304, to nie wysyła ponownie danych. Wszystko
co otrzymujemy to kod statusu. Tak wiec nie musimy ciagle pobierac tych samych danych
w kółko, jesli nie uległy zmianie. Serwer zakłada, ze juz mamy te dane zachowane
gdzies lokalnie.
Wszystkie nowoczesne przegladarki internetowe wspieraja sprawdzanie daty ostatniej
modyfikacji. Byc moze kiedys odwiedzilismy jakas strone jednego dnia, a potem
odwiedzilismy ja ponownie nastepnego dnia i zauwazylismy, ze nie uległa ona zmianie,
a jednoczesnie zadziwiajaco szybko sie załadowała. Przegladarka zachowała zawartosc
tej strony w lokalnym buforze podczas pierwszej wizyty, a podczas drugiej automatycznie
wysłała date ostatniej modyfikacji otrzymana za pierwszym razem. Serwer po
prostu odpowiedział kodem 304: Not Modified, a wiec przegladarka wiedziała, ze
musi załadowac strone z lokalnego bufora. Usługi internetowe moga byc takze takie
sprytne.
Biblioteka URL Pythona nie ma wbudowanego wsparcia dla kontroli daty ostatniej
modyfikacji, ale poniewaz mozemy dodawac dowolne nagłówki do kazdego zadania i
czytac dowolne nagłówki z kazdej odpowiedzi, to mozemy dodac taka kontrole samodzielnie.
ETag/If-None-Match
Znaczniki ETag sa alternatywnym sposobem na osiagniecie tego samego celu, co poprzez
kontrole daty ostatniej modyfikacji: nie pobieramy ponownie danych, które sie
nie zmieniły. A działa to tak: serwer wysyła jakas sume kontrolna danych (w nagłówku
ETag) razem z zadanymi danymi. Jak ta suma kontrolna jest ustalana, to zalezy wyłacznie
od serwera. Gdy po raz drugi chcemy pobrac te same dane, dołaczamy sume
kontrolna z nagłówka ETag w nagłówku If-None-Match: i jesli dane sie nie zmieniły
serwer odesle kod statusu 304. Tak jak w przypadku kontroli daty ostatniej modyfikacji,
serwer odsyła tylko kod 304; nie wysyła po raz drugi tych samych danych. Poprzez
dołaczenie sumy kontrolnej ETag do drugiego zadania mówimy serwerowi, iz nie ma
potrzeby, aby wysyłał po raz drugi tych samych danych, jesli nadal odpowiadaja one
tej sumie kontrolnej, poniewaz cały czas mamy dane pobrane ostatnio.
Biblioteka URL Pythona nie ma wbudowanego wsparcia dla znaczników ETag, ale
zobaczymy, jak mozna je dodac w dalszej czesci tego rozdziału.
244 ROZDZIAŁ 12. HTTP
Kompresja
Ostatnia istotna własciwoscia HTTP, która mozemy ustawic, jest kompresja gzip.
Gdy mówimy o usługach sieciowych za posrednictwem HTTP, to zwykle mówimy o
przesyłaniu XML-i tam i z powrotem. XML jest tekstem i to zwykle całkiem rozwlekłym
tekstem, a tekst dobrze sie kompresuje. Gdy zadamy jakiegos zasobu poprzez
HTTP, to mozemy poprosic serwer, jesli ma jakies nowe dane do wysłania, aby wysłał
je w formie skompresowanej. Dołaczamy wtedy nagłówek Accept-encoding: gzip do
zadania i jesli serwer wspiera kompresje, odesle on dane skompresowane w formacie
gzip i oznaczy je nagłówkiem Content-encoding: gzip.
Biblioteka URL Pythona nie ma wbudowanego wsparcia dla kompresji gzip jako
takiej, ale mozemy dodawac dowolne nagłówki do zadania a Python posiada oddzielny
moduł gzip, który zawiera funkcje, których mozna uzyc do dekompresji danych samodzielnie.
Zauwazmy, ze nasz jednolinijkowy skrypt pobierajacy RSS nie uwzglednia zadnej
z tych własciwosci HTTP. Zobaczmy, jak mozemy go udoskonalic.
12.4. DEBUGOWANIE SERWISÓW HTTP 245
12.4 Debugowanie serwisów HTTP
Debugowanie serwisów HTTP
Na poczatek właczmy debugowanie w pythonowej bibliotece HTTP i zobaczmy
co zostanie przesłane. Wiadomosci zdobyte poprzez przeanalizowanie wypisanych informacji
debugujacych, beda przydatne w tym rozdziale, gdy bedziemy chcieli dodac
nowe mozliwosci do naszego programu.
Przykład 11.3 Debugowanie HTTP
>>> import httplib
>>> httplib.HTTPConnection.debuglevel = 1 #(1)
>>> import urllib
>>> feeddata = urllib.urlopen(’http://diveintomark.org/xml/atom.xml’).read()
connect: (diveintomark.org, 80) #(2)
send: ’
GET /xml/atom.xml HTTP/1.0 #(3)
Host: diveintomark.org #(4)
User-agent: Python-urllib/1.15 #(5)

reply: ’HTTP/1.1 200 OKrn’ #(6)
header: Date: Wed, 14 Apr 2004 22:27:30 GMT
header: Server: Apache/2.0.49 (Debian GNU/Linux)
header: Content-Type: application/atom+xml
header: Last-Modified: Wed, 14 Apr 2004 22:14:38 GMT #(7)
header: ETag: "e8284-68e0-4de30f80" #(8)
header: Accept-Ranges: bytes
header: Content-Length: 26848
header: Connection: close
1. urllib jest zalezny od innej standardowej biblioteki Pythona: httplib. Zwykle
nie musimy importowac modułu httplib bezposrednio (urllib robi to automatycznie),
ale w tym wypadku to zrobilismy, a wiec mozemy ustawic flage trybu
debugowania w klasie HTTPConnection, która moduł urllib wykorzystuje wewnetrznie
do nawiazania połaczenia z serwerem HTTP. To jest niezwykle przydatna
technika. Kilka innych bibliotek Pythona ma podobne flagi trybu debugowania,
ale nie ma jakiegos szczególnego standardu nazywania ich i ustawiania;
trzeba przeczytac dokumentacje kazdej biblioteki, aby zobaczyc, czy taka flaga
jest dostepna.
2. Teraz gdy juz flage debugowania mamy ustawiona, informacje na temat zadan
HTTP i odpowiedzi sa wyswietlane w czasie rzeczywistym. Pierwsza rzecza jaka
mozemy zauwazyc jest to, iz łaczymy sie z serwerem diveintomark.org na porcie
80, który jest standardowym portem dla HTTP.
3. Gdy zgłaszamy zadanie po zasobów RSS, urllib wysyła trzy linie do serwera.
Pierwsza linia zawiera polecenie HTTP i sciezke do zasobu (bez nazwy domeny).
Wszystkie zadania w tym rozdziale beda uzywały polecenia GET, ale w nastepnym
rozdziale o SOAP zobaczymy, ze tam do wszystkiego uzywane jest polecenie
POST. Podstawowa składnia jest jednak taka sama niezaleznie od polecenia.
246 ROZDZIAŁ 12. HTTP
4. Druga linia odnosi sie do nagłówka Host, który zawiera nazwe domeny serwisu,
do którego kierujemy zadanie. Jest to istotne, poniewaz pojedynczy serwer HTTP
moze obsługiwac wiele oddzielnych domen. Na tym serwerze jest aktualnie obsługiwanych
12 domen; inne serwery moga obsługiwac setki lub nawet tysiace.
5. Trzecia linia to nagłówek User-Agent. To co tu widac, to jest standardowy nagłówek
User-Agent dodany domyslnie przez biblioteke urllib. W nastepnej sekcji
pokazemy jak zmienic to na cos bardziej konkretnego.
6. Serwer odpowiada kodem statusu i kilkoma nagłówkami (byc moze z jakimis
danymi, które zostały zachowane w zmiennej feeddata). Kodem statusu jest
tutaj liczba 200, która oznacza “wszystko w porzadku, prosze to dane o które
prosiłes”. Serwer takze podaje date odpowiedzi na zadanie, troche informacji na
temat samego serwera i typ zawartosci (ang. content type) zwracanych danych.
W zaleznosci od aplikacji, moze byc to przydatne lub tez nie. Zauwazmy, ze
zazadalismy RSS-a i faktycznie otrzymalismy RSS-a (application/atom+xml
jest to zarejestrowany typ zawartosci dla zasobów RSS).
7. Serwer podaje, kiedy ten RSS był ostatnio modyfikowany (w tym wypadku około
13 minut temu). Mozemy odesłac te date serwerowi z powrotem nastepnym razem,
gdy zazadamy tego samego zasobu, a serwer bedzie mógł wykonac sprawdzenie
daty ostatniej modyfikacji.
8. Serwer podaje takze, ze ten RSS ma sume kontrolna ETag o wartosci “e8284-
68e0-4de30f80”. Ta suma kontrolna sama w sobie nie ma zadnego znaczenia; nie
mozna z nia zrobic nic poza wysłaniem jej z powrotem do serwera przy nastepnej
próbie dostepu do tego zasobu. Wtedy serwer moze jej uzyc do sprawdzenia, czy
dane sie zmieniły od ostatniego razu czy nie.
12.5. USTAWIANIE USER-AGENT 247
12.5 Ustawianie User-Agent
Ustawianie User-Agent
Pierwszym krokiem, aby udoskonalic swój klient serwisu HTTP jest własciwe zidentyfikowanie
siebie za pomoca nagłówka User-Agent. Aby to zrobic, potrzebujemy
wyjsc poza prosty urllib i zanurkowac w urllib2.
Przykład 11.4 Wprowadzenie do urllib2
>>> import httplib
>>> httplib.HTTPConnection.debuglevel = 1 #(1)
>>> import urllib2
>>> request = urllib2.Request(’http://diveintomark.org/xml/atom.xml’) #(2)
>>> opener = urllib2.build_opener() #(3)
>>> feeddata = opener.open(request).read() #(4)
connect: (diveintomark.org, 80)
send: ’
GET /xml/atom.xml HTTP/1.0
Host: diveintomark.org
User-agent: Python-urllib/2.1

reply: ’HTTP/1.1 200 OKrn’
header: Date: Wed, 14 Apr 2004 23:23:12 GMT
header: Server: Apache/2.0.49 (Debian GNU/Linux)
header: Content-Type: application/atom+xml
header: Last-Modified: Wed, 14 Apr 2004 22:14:38 GMT
header: ETag: "e8284-68e0-4de30f80"
header: Accept-Ranges: bytes
header: Content-Length: 26848
header: Connection: close
1. Jesli w dalszym ciagu masz otwarty IDE na przykładzie z poprzedniego podrozdziału,
mozesz ten punkt pominac. Polecenie to włacza debugowanie HTTP,
dzieki której mozemy zobaczyc, co rzeczywiscie wysyłamy i co zostaje przysłane
do nas.
2. Pobieranie zasobów HTTP za pomoca urllib2 składa sie z trzech krótkich etapów.
Pierwszy krok to utworzenie obiektu zadania (instancji klasy Request).
Klasa Request przyjmuje jako parametr URL zasobów, z których bedziemy
ewentualnie pobierac dane. Dodajmy, ze za pomoca tego kroku jeszcze nic nie
pobieramy.
3. Drugim krokiem jest zbudowanie otwieracza (ang. opener) URL-a. Moze on przyjac
dowolna liczbe funkcji obsługi, które kontroluja, w jaki sposób obsługiwac odpowiedzi.
Ale mozemy takze zbudowac otwieracz bez podawania zadnych funkcji
obsługi, a co robimy w tym fragmencie. Pózniej, kiedy bedziemy omawiac przekierowania,
zobaczymy w jaki sposób zdefiniowac własne funkcje obsługi.
4. Ostatnim krokiem jest kazanie otwieraczowi, aby korzystajac z utworzonego
obiektu zadania otworzył URL. Widzimy, ze wszystkie otrzymane informacje
248 ROZDZIAŁ 12. HTTP
debugujace zostały wypisane. W tym kroku tak pobieramy zasoby, a zwrócone
dane przechowujemy w feeddata.
Przykład 11.5 Dodawanie nagłówków do zadania
>>> request #(1)
<urllib2.Request instance at 0x00250AA8>
>>> request.get_full_url()
http://diveintomark.org/xml/atom.xml
>>> request.add_header(’User-Agent’,
... ’OpenAnything/1.0 +http://diveintopython.org/’) #(2)
>>> feeddata = opener.open(request).read() #(3)
connect: (diveintomark.org, 80)
send: ’
GET /xml/atom.xml HTTP/1.0
Host: diveintomark.org
User-agent: OpenAnything/1.0 +http://diveintopython.org/ #(4)

reply: ’HTTP/1.1 200 OKrn’
header: Date: Wed, 14 Apr 2004 23:45:17 GMT
header: Server: Apache/2.0.49 (Debian GNU/Linux)
header: Content-Type: application/atom+xml
header: Last-Modified: Wed, 14 Apr 2004 22:14:38 GMT
header: ETag: "e8284-68e0-4de30f80"
header: Accept-Ranges: bytes
header: Content-Length: 26848
header: Connection: close
1. Kontynuujemy z poprzedniego przykładu. Mamy juz utworzony obiekt Request
z URL-em, do którego chcemy sie dostac.
2. Za pomoca metody add header obiektu klasy Request, mozemy dodac do zadania
dowolny nagłówek HTTP. Pierwszy argument jest nagłówkiem, a drugi jest
wartoscia dostarczona do tego nagłówka. Konwencja dyktuje, aby User-Agent
powinien byc w takim formacie: nazwa aplikacji, nastepnie slash, a potem numer
wersji. Pozostała czesc moze miec dowolna forme, jednak znalezc mnóstwo wariacji
wykorzystania tego nagłówka, ale gdzies powinno sie umiescic URL aplikacji.
User-Agent jest zazwyczaj logowany przez serwer wraz z innymi szczegółami na
temat twojego zadania. Właczajac URL twojej aplikacji, pozwalasz administratorom
danego serwera skontaktowac sie z toba, jesli jest cos zle.
3. Obiekt opener utworzony wczesniej moze byc takze ponownie wykorzystany.
Ponownie wysyłamy te same dane, ale tym razem ze zmienionym nagłówkiem
User-Agent.
4. Tutaj wysyłamy ustawiony przez nas nagłówek User-Agent, w miejsce domyslnego,
wysyłanego przez Pythona. Jesli bedziesz uwaznie patrzył, zobaczysz, ze
zdefiniowalismy nagłówek User-Agent, ale tak naprawde wysłalismy nagłówek
User-agent. Widzisz róznice? urllib2 zmienia litery w ten sposób, ze tylko
pierwsza litera jest wielka. W rzeczywistosci nie ma to zadnego znaczenia. Specyfikacja
HTTP mówi, ze wielkosc liter nazwy pola nagłówka nie jest wazna.
12.6. KORZYSTANIE Z LAST-MODIFIED I ETAG 249
12.6 Korzystanie z Last-Modified i ETag
Korzystanie z Last-Modified i ETag
Teraz gdy juz wiesz jak dodawac własne nagłówki do swoich zadan HTTP, zobaczmy
jak wykorzystac nagłówki Last-Modified i ETag.
Ponizsze przykłady pokazuja wyjscie z wyłaczonym debugowaniem. Jesli nadal
masz je właczone (tak jak w poprzedniej sekcji), mozesz je wyłaczyc poprzez takie
ustawienie: httplib.HTTPConnection.debuglevel = 0. Albo mozesz pozostawic debugowanie
właczone, jesli to Ci pomoze.
Przykład 11.6 Testowanie Last-Modified
>>> import urllib2
>>> request = urllib2.Request(’http://diveintomark.org/xml/atom.xml’)
>>> opener = urllib2.build_opener()
>>> firstdatastream = opener.open(request)
>>> firstdatastream.headers.dict #(1)
{’date’: ’Thu, 15 Apr 2004 20:42:41 GMT’,
’server’: ’Apache/2.0.49 (Debian GNU/Linux)’,
’content-type’: ’application/atom+xml’,
’last-modified’: ’Thu, 15 Apr 2004 19:45:21 GMT’,
’etag’: ’"e842a-3e53-55d97640"’,
’content-length’: ’15955’,
’accept-ranges’: ’bytes’,
’connection’: ’close’}
>>> request.add_header(’If-Modified-Since’,
... firstdatastream.headers.get(’Last-Modified’)) #(2)
>>> seconddatastream = opener.open(request) #(3)
Traceback (most recent call last):
File "<stdin>", line 1, in ?
File "c:python23liburllib2.py", line 326, in open
’_open’, req)
File "c:python23liburllib2.py", line 306, in _call_chain
result = func(*args)
File "c:python23liburllib2.py", line 901, in http_open
return self.do_open(httplib.HTTP, req)
File "c:python23liburllib2.py", line 895, in do_open
return self.parent.error(’http’, req, fp, code, msg, hdrs)
File "c:python23liburllib2.py", line 352, in error
return self._call_chain(*args)
File "c:python23liburllib2.py", line 306, in _call_chain
result = func(*args)
File "c:python23liburllib2.py", line 412, in http_error_default
raise HTTPError(req.get_full_url(), code, msg, hdrs, fp)
urllib2.HTTPError: HTTP Error 304: Not Modified
1. Pamietasz wszystkie te nagłówki HTTP, które były wyswietlane przy właczonym
debugowaniu? To jest sposób na dotarcie do nich programowo: firstdatastream.headers
jest obiektem działajacym jak słownik i pozwala on na pobranie kazdego nagłówka
zwracanego przez serwer HTTP.
250 ROZDZIAŁ 12. HTTP
2. Przy drugim zadaniu dodajemy nagłówek If-Modified-Since z data ostatniej
modyfikacji z pierwszego zadania. Jesli data nie uległa zmianie, serwer powinien
zwrócic kod statusu 304.
3. To wystarczy do stwierdzenia, ze dane nie uległy zmianie. Mozemy zobaczyc w
zrzucie błedów, ze urllib2 rzucił wyjatek specjalny: HTTPError w odpowiedzi na
kod statusu 304. To jest troche niezwykłe i nie całkiem pomocne. W koncu to nie
jest bład; specjalnie poprosilismy serwer o nie przesyłanie zadnych danych, jesli
nie uległy one zmianie i dane nie uległy zmianie, a wiec serwer powiedział, iz nie
wysłał zadnych danych. To nie jest bład; to jest dokładnie to czego oczekiwalismy.
urllib2 rzuca wyjatek HTTPError takze w sytuacjach, które mozna traktowac
jak błedy, np. 404 (strona nieznaleziona). Własciwie to rzuca on wyjatek HTTPError
dla kazdego kodu statusu innego niz 200 (OK), 301 (stałe przekierowanie), lub 302
(tymczasowe przekierowanie). Do naszych celów byłoby przydatne przechwycenie tych
kodów statusów i po prostu zwrócenie ich bez rzucania zadnych wyjatków. Aby to
zrobic, musimy zdefiniowac własna klase obsługi URL-i (ang. URL handler).
Ponizsza klasa obsługi URL-i jest czescia modułu openanything.py.
Przykład 11.7 Definiowanie klas obsługi URL-i
class DefaultErrorHandler(urllib2.HTTPDefaultErrorHandler): #(1)
def http_error_default(self, req, fp, code, msg, headers): #(2)
result = urllib2.HTTPError(
req.get_full_url(), code, msg, headers, fp)
result.status = code #(3)
return result
1. urllib2 jest zaprojektowana jako zbiór klas obsługi URL-i. Kazda z tych klas
moze definiowac dowolna liczbe metod. Gdy cos sie wydarzy – jak np. bład
HTTP lub nawet kod 304 – urllib2 uzywa introspekcji do odnalezienia w liscie
zdefiniowanych klas obsługi URL-i metody, która moze obsłuzyc to zdarzenie.
Uzywalismy podobnej introspekcji w rozdziale 9-tym, Przetwarzanie XML-a do
zdefiniowania metod obsługi dla róznych typów wezłów, ale urllib2 jest bardziej
elastyczny i przeszukuje tyle klas obsługi ile jest zdefiniowanych dla biezacego
zadania.
2. urllib2 przeszukuje zdefiniowane klasy obsługi i wywołuje metode http error default,
gdy otrzyma kod statusu 304 od serwera. Definiujac własna klase obsługi błedów,
mozemy zapobiec rzucaniu wyjatków przez urllib2. Zamiast tego tworzymy
obiekt HTTPError, ale zwracamy go, zamiast rzucania go jako wyjatek.
3. To jest kluczowa czesc: przed zwróceniem zachowujemy kod statusu zwrócony
przez serwer HTTP. To pozwala na dostep do niego programowi wywołujacemu.
Przykład 11.8 Uzywanie własnych klas obsługi URL-i
>>> request.headers #(1)
{’If-modified-since’: ’Thu, 15 Apr 2004 19:45:21 GMT’}
>>> import openanything
>>> opener = urllib2.build_opener(
... openanything.DefaultErrorHandler()) #(2)
12.6. KORZYSTANIE Z LAST-MODIFIED I ETAG 251
>>> seconddatastream = opener.open(request)
>>> seconddatastream.status #(3)
304
>>> seconddatastream.read() #(4)
’’
1. Kontynuujemy poprzedni przykład, a wiec obiekt Request jest juz utworzony i
nagłówek If-Modified-Since został juz dodany.
2. To jest klucz: poniewaz mamy zdefiniowana własna klase obsługi URL-i, musimy
powiedziec urllib2, aby teraz jej uzywał. Pamietasz jak mówiłem, iz urllib2
podzielił proces dostepu do zasobów HTTP na trzy etapy i to nie bez powodu?
Oto dlaczego wywołanie funkcji build opener jest odrebnym etapem. Poniewaz
na jej wejsciu mozesz podac własne klasy obsługi URL-i, które powoduja zmiane
domyslnego działania urllib2.
3. Teraz mozemy zasób otworzyc po cichu a z powrotem otrzymujemy obiekt, który
wraz z nagłówkami (uzyj seconddatastream.headers.dict, aby je pobrac),
zawiera takze kod statusu HTTP. W tym wypadku, tak jak oczekiwalismy, tym
statusem jest 304, który oznacza ze dane nie zmieniły sie od ostatniego razu, gdy
o nie prosilismy.
4. Zauwaz, ze gdy serwer odsyła kod statusu 304, to nie przesyła ponownie danych.
I o to w tym wszystkim chodzi: aby oszczedzic przepustowosc poprzez niepobieranie
ponowne danych, które nie uległy zmianie. A wiec jesli potrzebujesz tych
danych, to musisz zachowac je w lokalnym buforze po pierwszym pobraniu.
Z nagłówka ETag korzystamy bardzo podobnie. Jednak zamiast sprawdzania nagłówka
Last-Modified i przesyłania If-Modified-Since, sprawdzamy nagłówek ETag a
przesyłamy If-None-Match. Zacznijmy całkiem nowa sesje w naszym IDE.
Przykład 11.9 Uzycie ETag/If-None-Match
>>> import urllib2, openanything
>>> request = urllib2.Request(’http://diveintomark.org/xml/atom.xml’)
>>> opener = urllib2.build_opener(
... openanything.DefaultErrorHandler())
>>> firstdatastream = opener.open(request)
>>> firstdatastream.headers.get(’ETag’) #(1)
’"e842a-3e53-55d97640"’
>>> firstdata = firstdatastream.read()
>>> print firstdata #(2)
<?xml version="1.0" encoding="iso-8859-1"?>
<feed version="0.3"
xmlns="http://purl.org/atom/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xml:lang="en">
<title mode="escaped">dive into mark</title>
<link rel="alternate" type="text/html" href="http://diveintomark.org/"/>
[.. ciach ..]
>>> request.add_header(’If-None-Match’,
252 ROZDZIAŁ 12. HTTP
... firstdatastream.headers.get(’ETag’)) #(3)
>>> seconddatastream = opener.open(request)
>>> seconddatastream.status #(4)
304
>>> seconddatastream.read() #(5)
’’
1. Uzywajac pseudo-słownika firstdatastream.headers mozemy pobrac nagłówek
ETag zwrócony przez serwer. (Co sie stanie, jesli serwer nie zwróci nagłówka
ETag? Wtedy ta linia powinna zwrócic None.)
2. OK, mamy dane.
3. Teraz przy drugim wywołaniu ustawiamy w nagłówku If-None-Match wartosc
sumy kontrolnej z ETag otrzymanego przy pierwszym wywołaniu.
4. Drugie wywołanie działa prawidłowo bez zadnych zakłócen (bez rzucania zadnych
wyjatków) i ponownie widzimy, ze serwer odesłał status kodu 304. Bazujac
na sumie kontrolnej nagłówka ETag, która wysłalismy za drugim razem, wie on
ze dane nie zmieniły sie.
5. Niezaleznie od tego, czy kod 304 jest rezultatem sprawdzania daty Last-Modified
czy sumy kontrolnej ETag, nigdy nie otrzymamy z powrotem ponownie tych
samych danych, a jedynie kod statusu 304. I o to chodziło.
W tych przykładach serwer HTTP obsługiwał zarówno nagłówek Last-Modified
jak i ETag, ale nie wszystkie serwery to potrafia. Jako uzytkownik serwisów internetowych
powinienes byc przygotowany do uzywania ich obu, ale musisz programowac
defensywnie na wypadek gdyby serwer obsługiwał tylko jeden z tych nagłówków lub
zaden.
12.7. OBSŁUGA PRZEKIEROWAN 253
12.7 Obsługa przekierowan
Obsługa przekierowan
Mozemy obsługiwac trwałe i tymczasowe przekierowania uzywajac róznego rodzaju
własnych klas obsługi URL-i.
Po pierwsze zobaczmy dlaczego obsługa przekierowan jest konieczna.
Przykład 11.10 Dostep do usługi internetowej bez obsługi przekierowan
>>> import urllib2, httplib
>>> httplib.HTTPConnection.debuglevel = 1 #(1)
>>> request = urllib2.Request(
... ’http://diveintomark.org/redir/example301.xml’) #(2)
>>> opener = urllib2.build_opener()
>>> f = opener.open(request)
connect: (diveintomark.org, 80)
send: ’
GET /redir/example301.xml HTTP/1.0
Host: diveintomark.org
User-agent: Python-urllib/2.1

reply: ’HTTP/1.1 301 Moved Permanentlyrn’ #(3)
header: Date: Thu, 15 Apr 2004 22:06:25 GMT
header: Server: Apache/2.0.49 (Debian GNU/Linux)
header: Location: http://diveintomark.org/xml/atom.xml #(4)
header: Content-Length: 338
header: Connection: close
header: Content-Type: text/html; charset=iso-8859-1
connect: (diveintomark.org, 80)
send: ’
GET /xml/atom.xml HTTP/1.0 #(5)
Host: diveintomark.org
User-agent: Python-urllib/2.1

reply: ’HTTP/1.1 200 OKrn’
header: Date: Thu, 15 Apr 2004 22:06:25 GMT
header: Server: Apache/2.0.49 (Debian GNU/Linux)
header: Last-Modified: Thu, 15 Apr 2004 19:45:21 GMT
header: ETag: "e842a-3e53-55d97640"
header: Accept-Ranges: bytes
header: Content-Length: 15955
header: Connection: close
header: Content-Type: application/atom+xml
>>> f.url #(6)
’http://diveintomark.org/xml/atom.xml’
>>> f.headers.dict
{’content-length’: ’15955’,
’accept-ranges’: ’bytes’,
’server’: ’Apache/2.0.49 (Debian GNU/Linux)’,
254 ROZDZIAŁ 12. HTTP
’last-modified’: ’Thu, 15 Apr 2004 19:45:21 GMT’,
connection’: ’close’,
’etag’: ’"e842a-3e53-55d97640"’,
’date’: ’Thu, 15 Apr 2004 22:06:25 GMT’,
’content-type’: ’application/atom+xml’}
>>> f.status
Traceback (most recent call last):
File "<stdin>", line 1, in ?
AttributeError: addinfourl instance has no attribute ’status’
1. Lepiej bedziesz mógł zobaczyc co sie dzieje, gdy właczysz tryb debugowania.
2. To jest URL, który ma ustawione trwałe przekierowanie do RSS-a pod adresem
http://diveintomark.org/xml/atom.xml.
3. Gdy próbujemy pobrac dane z tego adresu, to serwer odsyła kod statusu 301
informujacy o tym, ze ten zasób został przeniesiony na stałe.
4. Serwer przesyła takze nagłówek Location:, który zawiera nowy adres tych danych.
5. urllib2 zauwaza ten kod statusu dotyczacy przekierowania i automatycznie próbuje
pobrac dane spod nowej lokalizacji podanej w nagłówku Location:.
6. Obiekt zwrócony przez opener zawiera juz nowy adres (po przekierowaniu) i
wszystkie nagłówki zwrócone po drugim zadaniu (zwrócone z nowego adresu).
Jednak brakuje kodu statusu, a wiec nie mamy mozliwosci programowego stwierdzenia,
czy to przekierowanie było trwałe, czy tylko tymczasowe. A to ma wielkie
znaczenie: jesli to było przekierowanie tymczasowe, wtedy musimy ponowne zadania
kierowac pod stary adres, ale jesli to było trwałe przekierowanie (jak w
tym przypadku), to nowe zadania od tego momentu powinny byc kierowane do
nowej lokalizacji.
To nie jest optymalne, ale na szczescie łatwe do naprawienia. urllib2 nie zachowuje
sie dokładnie tak, jak tego chcemy, gdy napotyka na kody 301 i 302, a wiec zmienmy
to zachowanie. Jak? Przy pomocy własnej klasy obsługi URL-i, tak jak to zrobilismy
w przypadku kodu 304.
Ponizsza klasa jest zdefiniowana w openanything.py.
Przykład 11.11 Definiowanie klasy obsługi przekierowan
class SmartRedirectHandler(urllib2.HTTPRedirectHandler): #(1)
def http_error_301(self, req, fp, code, msg, headers):
result = urllib2.HTTPRedirectHandler.http_error_301( #(2)
self, req, fp, code, msg, headers)
result.status = code #(3)
return result
def http_error_302(self, req, fp, code, msg, headers): #(4)
result = urllib2.HTTPRedirectHandler.http_error_302(
self, req, fp, code, msg, headers)
result.status = code
12.7. OBSŁUGA PRZEKIEROWAN 255
return result
1. Obsługa przekierowan w urllib2 jest zdefiniowana w klasie o nazwie
HTTPRedirectHandler. Nie chcemy całkowicie zmieniac działania tej klasy,
chcemy je tylko lekko rozszerzyc, a wiec dziedziczymy po niej, a wtedy bedziemy
mogli wywoływac metody klasy nadrzednej do wykonania całej ciezkiej roboty.
2. Gdy napotkany zostanie status kodu 301 przesłany przez serwer, urllib2 przeszuka
liste klas obsługi i wywoła metode http error 301. Pierwsza rzecza, jaka
wykona nasza wersja, jest po prostu wywołanie metody http error 301 przodka,
która wykona cała robote zwiazana ze znalezieniem nagłówka Location: i przekierowaniem
zadania pod nowy adres.
3. Tu jest kluczowa sprawa: zanim wykonamy return, zachowujemy kod statusu
(301), aby program wywołujacy mógł go pózniej odczytac.
4. Przekierowania tymczasowe (kod statusu 302) działaja w ten sam sposób: nadpisujemy
metode http error 302, wywołujemy metode przodka i zachowujemy
kod statusu przed powrotem z metody.
A wiec jaka mamy z tego korzysc? Mozemy teraz utworzyc klase pozwalajaca na
dostep do zasobów internetowych wraz z nasza własna klasa obsługi przekierowan i
bedzie ona nadal dokonywała przekierowan automatycznie, ale tym razem bedzie ona
takze udostepniała kod statusu przekierowania.
Przykład 11.12 Uzycie klasy obsługi przekierowan do wykrycia przekierowan
trwałych
>>> request = urllib2.Request(’http://diveintomark.org/redir/example301.xml’)
>>> import openanything, httplib
>>> httplib.HTTPConnection.debuglevel = 1
>>> opener = urllib2.build_opener(
... openanything.SmartRedirectHandler()) #(1)
>>> f = opener.open(request)
connect: (diveintomark.org, 80)
send: ’GET /redir/example301.xml HTTP/1.0
Host: diveintomark.org
User-agent: Python-urllib/2.1

reply: ’HTTP/1.1 301 Moved Permanentlyrn’ #(2)
header: Date: Thu, 15 Apr 2004 22:13:21 GMT
header: Server: Apache/2.0.49 (Debian GNU/Linux)
header: Location: http://diveintomark.org/xml/atom.xml
header: Content-Length: 338
header: Connection: close
header: Content-Type: text/html; charset=iso-8859-1
connect: (diveintomark.org, 80)
send: ’
GET /xml/atom.xml HTTP/1.0
Host: diveintomark.org
256 ROZDZIAŁ 12. HTTP
User-agent: Python-urllib/2.1

reply: ’HTTP/1.1 200 OKrn’
header: Date: Thu, 15 Apr 2004 22:13:21 GMT
header: Server: Apache/2.0.49 (Debian GNU/Linux)
header: Last-Modified: Thu, 15 Apr 2004 19:45:21 GMT
header: ETag: "e842a-3e53-55d97640"
header: Accept-Ranges: bytes
header: Content-Length: 15955
header: Connection: close
header: Content-Type: application/atom+xml
>>> f.status #(3)
301
>>> f.url
’http://diveintomark.org/xml/atom.xml’
1. Po pierwsze tworzymy opener z przed chwila zdefiniowana klasa obsługi przekierowan.
2. Wysłalismy zadanie i otrzymalismy w odpowiedzi kod statusu 301. W
tym momencie wołana jest metoda http error 301. Wywołujemy metode
przodka, która odnajduje przekierowanie i wysyła zadanie pod nowa lokalizacje
(http://diveintomark.org/xml/atom.xml).
3. Tu jest nasza korzysc: teraz nie tylko mamy dostep do nowego URL-a, ale takze
do kodu statusu przekierowania, a wiec mozemy stwierdzic, ze było to przekierowanie
trwałe. Przy nastepnym zadaniu tych danych, powinnismy uzyc nowego
adresu (http://diveintomark.org/xml/atom.xml, jak widac w f.url). Jesli mamy
zachowana dana lokalizacje w pliku konfiguracyjnym lub w bazie danych, to powinnismy
zaktualizowac ja, aby nie odwoływac sie ponownie do starego adresu.
To jest pora do aktualizacji ksiazki adresowej.
Ta sama klasa obsługi przekierowan moze takze pokazac, ze nie powinnismy aktualizowac
naszej ksiazki adresowej.
Przykład 11.13 Uzycie klasy obsługi przekierowan do wykrycia przekierowan
tymczasowych
>>> request = urllib2.Request(
... ’http://diveintomark.org/redir/example302.xml’) #(1)
>>> f = opener.open(request)
connect: (diveintomark.org, 80)
send: ’
GET /redir/example302.xml HTTP/1.0
Host: diveintomark.org
User-agent: Python-urllib/2.1

reply: ’HTTP/1.1 302 Foundrn’ #(2)
header: Date: Thu, 15 Apr 2004 22:18:21 GMT
header: Server: Apache/2.0.49 (Debian GNU/Linux)
12.7. OBSŁUGA PRZEKIEROWAN 257
header: Location: http://diveintomark.org/xml/atom.xml
header: Content-Length: 314
header: Connection: close
header: Content-Type: text/html; charset=iso-8859-1
connect: (diveintomark.org, 80)
send: ’
GET /xml/atom.xml HTTP/1.0 #(3)
Host: diveintomark.org
User-agent: Python-urllib/2.1

reply: ’HTTP/1.1 200 OKrn’
header: Date: Thu, 15 Apr 2004 22:18:21 GMT
header: Server: Apache/2.0.49 (Debian GNU/Linux)
header: Last-Modified: Thu, 15 Apr 2004 19:45:21 GMT
header: ETag: "e842a-3e53-55d97640"
header: Accept-Ranges: bytes
header: Content-Length: 15955
header: Connection: close
header: Content-Type: application/atom+xml
>>> f.status #(4)
302
>>> f.url
http://diveintomark.org/xml/atom.xml
1. To jest przykładowy URL skonfigurowany tak, aby powiadamiac klientów o tymczasowym
przekierowaniu do http://diveintomark.org/xml/atom.xml.
2. Serwer odsyła z powrotem kod statusu 302 wskazujacy na tymczasowe przekierowanie.
Tymczasowa lokalizacja danych jest podana w nagłówku Location:.
3. urllib2 wywołuje nasza metode http error 302, która wywołuje metode przodka
o tej samej nazwie w urllib2.HTTPRedirectHandler, która wykonuje przekierowanie
do nowej lokalizacji. Wtedy nasza metoda http error 302 zachowuje
kod statusu (302), a wiec wywołujaca aplikacja moze go pózniej odczytac.
4. I oto mamy prawidłowo wykonane przekierowanie do
http://diveintomark.org/xml/atom.xml. f.status informuje, iz było to
przekierowanie tymczasowe, co oznacza, ze ponowne zadania powinnismy kierowac
pod stary adres (http://diveintomark.org/redir/example302.xml). Moze
nastepnym razem znowu nastapi przekierowanie, a moze nie. Moze nastapi
przekierowanie pod całkiem inny adres. Nie do nas nalezy ta decyzja. Serwer
powiedział, ze to przekierowanie było tylko tymczasowe, a wiec powinnismy
to uszanowac. Teraz dostarczamy wystarczajaca ilosc informacji, aby aplikacja
wywołujaca była w stanie to uszanowac.
258 ROZDZIAŁ 12. HTTP
12.8 Obsługa skompresowanych danych
Obsługa skompresowanych danych
Ostatnia wazna własciwoscia HTTP, która bedziemy chcieli obsłuzyc, bedzie kompresja.
Wiele serwisów sieciowych posiada zdolnosc wysyłania skompresowanych danych,
dzieki czemu wielkosc wysyłanych danych moze zmalec nawet o 60% lub wiecej.
Sprawdza sie to w szczególnosci w XML-owych serwisach sieciowych, poniewaz dane
XML kompresuja sie bardzo dobrze.
Serwery nie dadza nam skompresowanych danych, jesli im nie powiemy, ze potrafimy
je obsłuzyc.
Przykład 11.14. Informowanie serwera, ze chcielibysmy otrzymac skompresowane
dane
>>> import urllib2, httplib
>>> httplib.HTTPConnection.debuglevel = 1
>>> request = urllib2.Request(’http://diveintomark.org/xml/atom.xml’)
>>> request.add_header(’Accept-encoding’, ’gzip’) #(1)
>>> opener = urllib2.build_opener()
>>> f = opener.open(request)
connect: (diveintomark.org, 80)
send: ’
GET /xml/atom.xml HTTP/1.0
Host: diveintomark.org
User-agent: Python-urllib/2.1
Accept-encoding: gzip #(2)

reply: ’HTTP/1.1 200 OKrn’
header: Date: Thu, 15 Apr 2004 22:24:39 GMT
header: Server: Apache/2.0.49 (Debian GNU/Linux)
header: Last-Modified: Thu, 15 Apr 2004 19:45:21 GMT
header: ETag: "e842a-3e53-55d97640"
header: Accept-Ranges: bytes
header: Vary: Accept-Encoding
header: Content-Encoding: gzip #(3)
header: Content-Length: 6289 #(4)
header: Connection: close
header: Content-Type: application/atom+xml
1. To jest najwazniejsza czesc: kiedy utworzymy obiekt Request, dodajemy nagłówek
Accept-encoding, aby powiedziec serwerowi, ze akceptujemy dane zakodowane
jako gzip. gzip jest nazwa wykorzystanego algorytmu kompresji. Teoretycznie
powinny byc dostepne inne algorytmy kompresji, ale gzip jest algorytmem
kompresji wykorzystywanym przez 99% serwisów sieciowych.
2. W tym miejscu nagłówek idzie przez linie.
3. I w tym miejscu otrzymujemy informacje o tym, co serwer przesyła nam z powrotem:
nagłówek Content-Encoding: gzip oznacza, ze dane które otrzymalismy
zostały skompresowane jako gzip.
12.8. OBSŁUGA SKOMPRESOWANYCH DANYCH 259
4. Nagłówek Content-Length oznacza długosc danych skompresowanych, a nie zdekompresowanych.
Jak zobaczymy za minutke, rzeczywista wielkosc zdekompresowanych
danych wynosi 15955, zatem dane zostały skompresowane o ponad
60%.
Przykład 11.15. Dekompresowanie danych
>>> compresseddata = f.read() #(1)
>>> len(compresseddata)
6289
>>> import StringIO
>>> compressedstream = StringIO.StringIO(compresseddata) #(2)
>>> import gzip
>>> gzipper = gzip.GzipFile(fileobj=compressedstream) #(3)
>>> data = gzipper.read() #(4)
>>> print data #(5)
<?xml version="1.0" encoding="iso-8859-1"?>
<feed version="0.3"
xmlns="http://purl.org/atom/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xml:lang="en">
<title mode="escaped">dive into mark</title>
<link rel="alternate" type="text/html" href="http://diveintomark.org/"/>
<-- rest of feed omitted for brevity -->
>>> len(data)
15955
1. Kontynuujac z poprzedniego przykładu, f jest obiektem plikopodobnym zwróconym
przez otwieracz URL-a. Korzystajac z jego metody read() zazwyczaj dostaniemy
nieskompresowane dane, ale poniewaz te dane beda skompresowane gzipem,
to jest dopiero pierwszy krok, aby otrzymac dane, które naprawde chcemy.
2. OK, ten krok jest troszeczke okrezny. Python posiada moduł gzip, który czyta
(i własciwie takze zapisuje) pliki skompresowane jako gzip. Jednak my nie mamy
pliku na dysku, mamy skompresowany bufor w pamieci, a nie chcemy tworzyc
tymczasowego pliku, aby te dane dekompresowac. Zatem tworzymy obiekt plikopodobny
przechowujacy w pamieci skompresowane dane (compresseddata) korzystajac
z modułu StringIO. Pierwszy raz wspomnielismy o StringIO w poprzednim
rozdziale, ale teraz znalezlismy kolejny sposób, aby go wykorzystac.
3. Teraz tworzymy instancje klasy GzipFile. Ten “plik” jest obiektem plikopodobnym
compressedstream.
4. To jest linia, która wykonuje cała własciwa prace: “czyta” z GzipFile zdekompresowane
dane. Ten “plik” nie jest prawdziwym plikiem na dysku. gzipper
w rzeczywistosci “czyta” z obiektu plikopodobnego, który stworzylismy za pomoca
StringIO, aby opakowac skompresowane dane znajdujace sie tylko w
pamieci w zmiennej compresseddata w obiekt plikopodobny. Ale skad przyszły
te skompresowane dane? Oryginalnie pobralismy je z odległego serwera
HTTP dzieki “odczytaniu” obiektu plikopodobnego, który utworzylismy za pomoca
urllib2.build opener. I fantastycznie, to wszystko po prostu działa. Zaden
element w tym łancuchu nie ma pojecia, ze jego poprzednik tylko udaje, ze
jest tym za co sie podaje.
260 ROZDZIAŁ 12. HTTP
5. Zobaczmy, sa to prawdziwe dane. (tak naprawde 15955 bajtów)
“Lecz czekaj!” — usłyszałem Twój krzyk, “Mozna to nawet zrobic prosciej”. Myslisz,
ze skoro opener.open zwraca obiekt plikopodobny, wiec dlaczego nie wyciac posrednika
StringIO i po prostu przekazac f bezposrednio do GzipFile? OK, moze tak
nie myslałes, ale tak czy inaczej nie przejmuj sie tym, poniewaz to nie działa.
Przykład 11.16. Dekompresowanie danych bezposrednio z serwera
>>> f = opener.open(request) #(1)
>>> f.headers.get(’Content-Encoding’) #(2)
’gzip’
>>> data = gzip.GzipFile(fileobj=f).read() #(3)
Traceback (most recent call last):
File "<stdin>", line 1, in ?
File "c:python23libgzip.py", line 217, in read
self._read(readsize)
File "c:python23libgzip.py", line 252, in _read
pos = self.fileobj.tell() # Save current position
AttributeError: addinfourl instance has no attribute ’tell’
1. Kontynuujac z poprzedniego przykładu, mamy juz obiekt zadania Request z
ustawionym nagłówkiem Accept-encoding: gzip header.
2. Zaraz po otworzeniu zadania otrzymujemy nagłówki z serwera (ale nie pobieramy
jeszcze zadnych danych). Jak mozemy zobaczyc ze zwróconego nagłówka
Content-Encoding, dane te zostały skompresowane na gzip.
3. Poniewaz opener.open zwraca obiekt plikopodobny, a z dopiero co odczytanego
nagłówka wynika, ze otrzymane dane beda skompresowane na gzip-a, to dlaczego
nie przekazac prosto otrzymany obiekt plikopodobny bezposrednio do GzipFile?
Kiedy “czytamy” z instancji GzipFile-a, bedziemy “czytali” skompresowane
dane z serwera HTTP i dekompresowali je w locie. Jest to dobry pomysł, ale
niestety nie działa. GzipFile potrzebuje zapisywac swoja pozycje i przesuwac sie
blizej lub dalej po skompresowanym pliku, poniewaz w taki sposób działa kompresja
gzip. Nie działa to, kiedy “plikiem” jest strumien bajtów przychodzacych
z zewnetrznego serwera; jedynie co mozemy z tym zrobic, to jednorazowo pobierac
dane bajty nie przesuwajac sie wstecz lub do przodu w strumieniu danych.
Zatem nieelegancki sposób z wykorzystaniem StringIO jest najlepszym rozwiazaniem:
pobieramy skompresowane dane, tworzymy z nich obiekt plikopodobny
za pomoca StringIO, a nastepnie dekompresujemy dane wewnatrz niego.
12.9. HTTP - WSZYSTKO RAZEM 261
12.9 HTTP - wszystko razem
Python/HTTP — wszystko razem
Widzielismy juz wszystkie elementy potrzebne do utworzenia inteligentnego klienta
usługi internetowej. Teraz zobaczmy jak to wszystko do siebie pasuje.
Przykład 11.17. Funkcja openanything
Ta funkcja jest zdefiniowana w pliku openanything.py.
def openAnything(source, etag=None, lastmodified=None, agent=USER_AGENT):
# non-HTTP code omitted for brevity
if urlparse.urlparse(source)[0] == ’http’: #(1)
# open URL with urllib2
request = urllib2.Request(source)
request.add_header(’User-Agent’, agent) #(2)
if etag:
request.add_header(’If-None-Match’, etag) #(3)
if lastmodified:
request.add_header(’If-Modified-Since’, lastmodified) #(4)
request.add_header(’Accept-encoding’, ’gzip’) #(5)
opener = urllib2.build_opener(SmartRedirectHandler(), DefaultErrorHandler()) #(6)
return opener.open(request) #(7)
1. urlparse to bardzo poreczny moduł do, na pewno zgadłes, parsowania URL-i.
Jego podstawowa funkcja, takze nazywajaca sie urlparse, przyjmuje na wejsciu
URL-a i dzieli go na taka krotke (schemat, domena, sciezka, parametry, parametry
w zadaniu i identyfikator fragmentu). Jednyna wsród tych rzeczy jaka
musimy sie przejmowac jest schemat, który decyduje o tym czy mamy do czynienie
z URL-em HTTP (który to moduł urllib2 moze obsłuzyc).
2. Przedstawiamy sie serwerowi HTTP przy pomocy nagłówka User-Agent przesłanego
przez funkcje wywołujaca. Jesli nie zostałaby podana wartosc User-Agent,
uzylibysmy wczesniej zdefiniowanej wartosci w openanything.py. Nigdy nie nalezy
uzywac domyslnej wartosci zdefiniowanej w urllib2.
3. Jesli została podana suma kontrolna dla ETag, wysyłamy ja w nagłówku If-None-Match.
4. Jesli została podana data ostatniej modyfikacji, wysyłamy ja w nagłówku If-Modified-Since.
5. Powiadamiamy serwer, ze chcemy dane skompresowane, jesli to jest tylko mozliwe.
6. Wywołujemy funkcje build opener, która wykorzystuje nasze własne klasy obsługi
URL-i: SmartRedirectHandler do obsługi przekierowan 301 i 302 i DefaultErrorHandler
do taktownej obsługi 304, 404, i innych błednych sytuacji.
7. I to wszystko! Otwieramy URL-a i zwracamy pliko-podobny (ang. file-like) obiekt
do funkcji wywołujacej.
Przykład 11.18. Funkcja fetch
Ta funkcja jest zdefiniowana w pliku openanything.py.
262 ROZDZIAŁ 12. HTTP
def fetch(source, etag=None, last_modified=None, agent=USER_AGENT):
’’’Fetch data and metadata from a URL, file, stream, or string’’’
result = {}
f = openAnything(source, etag, last_modified, agent) #(1)
result[’data’] = f.read() #(2)
if hasattr(f, ’headers’):
# save ETag, if the server sent one
result[’etag’] = f.headers.get(’ETag’) #(3)
# save Last-Modified header, if the server sent one
result[’lastmodified’] = f.headers.get(’Last-Modified’) #(4)
if f.headers.get(’content-encoding’, ) == ’gzip’: #(5)
# data came back gzip-compressed, decompress it
result[’data’] = gzip.GzipFile(fileobj=StringIO(result[’data’]])).read()
if hasattr(f, ’url’): #(6)
result[’url’] = f.url
result[’status’] = 200
if hasattr(f, ’status’): #(7)
result[’status’] = f.status
f.close()
return result
1. Po pierwsze wywołujemy funkcje openAnything z URL-em, suma kontrolna
ETag, data ostatniej modyfikacji (ang. Last-Modified date) i wartoscia User-Agent.
2. Czytamy aktualne dane zwrócone przez serwer. Moga one byc spakowane; jesli
tak, to pózniej je rozpakowujemy.
3. Zachowujemy sume kontrolna ETag wrócona przez serwer, takze aplikacja wywołujaca
moze ja przesłac ponownie nastepnym razem i mozemy ja przekazac
dalej do openAnything, która moze ja wetknac do nagłówka If-None-Match i
przesłac do zdalnego serwera.
4. Zachowujemy takze date ostatniej modyfikacji.
5. Jesli serwer powiedział, ze wysłał spakowane dane, rozpakowujemy je.
6. Jesli dostalismy URL-a z powrotem od serwera, zachowujemy go i zakładamy, ze
kod statusu wynosi 200, dopóki nie przekonamy sie, ze jest inaczej.
7. Jesli któras z naszych klas obsługi URL-i przechwyci jakis kod statusu, zachowujemy
go takze.
Przykład 11.19. Uzycie openanything.py
>>> import openanything
>>> useragent = ’MyHTTPWebServicesApp/1.0’
>>> url = ’http://diveintopython.org/redir/example301.xml’
>>> params = openanything.fetch(url, agent=useragent) #(1)
>>> params #(2)
{’url’: ’http://diveintomark.org/xml/atom.xml’,
’lastmodified’: ’Thu, 15 Apr 2004 19:45:21 GMT’,
’etag’: ’"e842a-3e53-55d97640"’,
12.9. HTTP - WSZYSTKO RAZEM 263
’status’: 301,
’data’: ’<?xml version="1.0" encoding="iso-8859-1"?>
<feed version="0.3"
<-- rest of data omitted for brevity -->’}
>>> if params[’status’] == 301: #(3)
... url = params[’url’]
>>> newparams = openanything.fetch(
... url, params[’etag’], params[’lastmodified’], useragent) #(4)
>>> newparams
{’url’: ’http://diveintomark.org/xml/atom.xml’,
’lastmodified’: None,
’etag’: ’"e842a-3e53-55d97640"’,
’status’: 304,
’data’: } #(5)
1. Za pierwszym razem, gdy pobieramy jakis zasób, nie mamy zadnej sumy kontrolnej
ETag ani daty ostatniej modyfikacji, a wiec opuszczamy te parametry. (To
sa parametry opcjonalne.)
2. Z powrotem otrzymujemy słownik kilku uzytecznych nagłówków, kod statusu
HTTP i aktualne dane zwrócone przez serwer. Funkcja openanything zajmuje
sie samodzielnie rozpakowaniem archiwum gzip; nie zajmujemy sie tym na tym
poziomie.
3. Jesli kiedykolwiek otrzymamy kod statusu 301, czyli trwałe przekierowanie, to
musimy zaktualizowac naszego URL-a na nowy adresu.
4. Gdy po raz drugi pobieramy ten sam zasób, to mamy wiele informacji, które
mozemy przekazac: (byc moze zaktualizowany) URL, ETag z ostatniego razu,
data ostatniej modyfikacji i oczywiscie nasz User-Agent.
5. Z powrotem ponownie otrzymujemy słownik, ale dane nie uległy zmianie, a wiec
wszystko co dostalismy to był kod statusu 304 i zadnych danych.
264 ROZDZIAŁ 12. HTTP
12.10 HTTP - podsumowanie
Podsumowanie
Teraz moduł openanything.py i jego funkcje powinny miec dla Ciebie sens.
Mozemy wyróznic 5 waznych cech usług internetowych na bazie HTTP, które kazdy
klient powinien uwzgledniac:
• Identyfikacja aplikacji poprzez własciwe ustawienie nagłówka User-Agent.
• Własciwa obsługa trwałych przekierowan.
• Uwzglednienie sprawdzania daty ostatniej modyfikacji (ang. Last-Modified), aby
uniknac ponownego pobierania danych, które nie uległy zmianie.
• Uwzglednienie sprawdzania sum kontrolnych z nagłówka ETag, aby uniknac ponownego
pobierania danych, które nie uległy zmianie.
• Uwzglednienie kompresji gzip, aby zredukowac wielkosc przesyłanych danych,
nawet gdy dane uległy zmianie.
Rozdział 13
SOAP
265
266 ROZDZIAŁ 13. SOAP
13.1 SOAP
SOAP
Rozdział 11 przyblizył temat serwisów sieciowych HTTP zorientowanych na dokumenty.
“Wejsciowym parametrem” był URL, a “zwracana wartoscia” był konkretny
dokument XML, który mozna było sparsowac.
Ten rozdział przyblizy serwis sieciowy SOAP, który jest bardziej strukturalnym podejsciem
do problemu. Zamiast zajmowac sie bezposrednio zadaniami HTTP i dokumentami
XML, SOAP pozwala nam symulowac wywoływanie funkcji, które zwracaja
natywne typy danych. Jak zobaczymy, złudzenie to jest niemal perfekcyjne: “wywołujemy”
funkcje za pomoca biblioteki SOAP korzystajac ze standardowej, wywołujacej
składni Pythona a funkcja zdaje sie zwracac obiekty i wartosci Pythona. Jednak pod
ta przykrywka, biblioteka SOAP w rzeczywistosci wykonuje złozona transakcje wymagajaca
wielu dokumentów XML i zdalnego serwera.
SOAP jest złozona specyfikacja i powiedzenie, ze SOAP słuzy wyłacznie do zdalnego
wywoływania funkcji bedzie troche wprowadzało w bład. Niektórzy mogliby
stwierdzic, ze SOAP pozwala na jednostronne, asynchroniczne przekazywanie komunikatów
i zorientowane na dokumenty serwisy sieciowe. I Ci ludzie takze mieliby racje;
SOAP moze byc wykorzystywany w ten sposób, a takze na wiele innych. Jednak ten
rozdział przyblizy tak zwany “styl RPC” (Remote Procedure Call), czyli wywoływanie
zewnetrznych funkcji i otrzymywanie z nich wyników.
Nurkujemy
Korzystasz z Google, prawda? Jest to popularna wyszukiwarka. Chciałes kiedys
miec programowy dostep do wyników wyszukiwania za pomoca Google? Teraz mozesz.
Ponizej mamy program, który poszukuje w Google za pomoca Pythona.
Przykład 12.1. search.py
from SOAPpy import WSDL
# you’ll need to configure these two values;
# see http://www.google.com/apis/
WSDLFILE = ’/path/to/copy/of/GoogleSearch.wsdl’
APIKEY = ’YOUR_GOOGLE_API_KEY’
_server = WSDL.Proxy(WSDLFILE)
def search(q):
"""Search Google and return list of {title, link, description}"""
results = _server.doGoogleSearch(
APIKEY, q, 0, 10, False, "", False, "", "utf-8", "utf-8")
return [{"title": r.title.encode("utf-8"),
"link": r.URL.encode("utf-8"),
"description": r.snippet.encode("utf-8")}
for r in results.resultElements]
if __name__ == ’__main__’:
import sys
for r in search(sys.argv[1])[:5]:
print r[’title’]
13.1. NURKUJEMY 267
print r[’link’]
print r[’description’]
print
Mozesz importowac to jako moduł i wykorzystywac to w wiekszych programach,
a takze mozesz uruchomic ten skrypt z linii polecen. W linii polecen przekazujemy
zapytanie szukania jako argument linii polecen, a program wypisuje nam URL, tytuł
i opis z pieciu pierwszych wyników wyszukiwania.
Tutaj mamy przykładowe wyjscie, gdy wyszkujemy słowo “python”.
Przykład 12.2. Przykładowe uzycie search.py
C:diveintopythoncommonpy> python search.py "python"
<b>Python</b> Programming Language
http://www.python.org/
Home page for <b>Python</b>, an interpreted, interactive, object-oriented,
extensible<br> programming language. <b>...</b> <b>Python</b>
is OSI Certified Open Source: OSI Certified.
<b>Python</b> Documentation Index
http://www.python.org/doc/
<b>...</b> New-style classes (aka descrintro). Regular expressions. Database
API. Email Us.<br> docs@<b>python</b>.org. (c) 2004. <b>Python</b>
Software Foundation. <b>Python</b> Documentation. <b>...</b>
Download <b>Python</b> Software
http://www.python.org/download/
Download Standard <b>Python</b> Software. <b>Python</b> 2.3.3 is the
current production<br> version of <b>Python</b>. <b>...</b>
<b>Python</b> is OSI Certified Open Source:
Pythonline
http://www.pythonline.com/
Dive Into <b>Python</b>
http://diveintopython.org/
Dive Into <b>Python</b>. <b>Python</b> from novice to pro. Find:
<b>...</b> It is also available in multiple<br> languages. Read
Dive Into <b>Python</b>. This book is still being written. <b>...</b>
268 ROZDZIAŁ 13. SOAP
13.2 Instalowanie odpowiednich bibliotek
Instalowanie odpowiednich bibliotek
W odróznieniu od pozostałego kodu w tej ksiazce, ten rozdział wymaga bibliotek,
które nie sa instalowane wraz z Pythonem. Zanim zanurkujemy w usługi SOAP, musisz
doinstalowac trzy biblioteki: PyXML, fpconst i SOAPpy.
12.2.1. Instalacja PyXML
Pierwsza biblioteka jakiej potrzebujemy jest PyXML, zbiór bibliotek do obsługi XML,
które dostarczaja wieksza funkcjonalnosc niz wbudowane biblioteki XML, które omawialismy
w rozdziale 9.
Procedura 12.1.
Oto sposób instalacji PyXML:
1. Wejdz na http://pyxml.sourceforge.net/, kliknij Downloads i pobierz ostatnia
wersje dla Twojego systemu operacyjnego.
2. Jesli uzywasz Windowsa, to masz kilka mozliwosci. Upewnij sie, ze pobierasz
wersje PyXML, która odpowiada wersji Pythona, którego uzywasz.
3. Kliknij dwukrotnie na pliku instalatora. Jesli pobrałes PyXML 0.8.3 dla Windowsa
i Pythona 2.3, to programem instalatora bedzie plik PyXML-0.8.3.win32-
py2.3.exe.
4. Wykonaj wszystkie kroki instalatora.
5. Po zakonczeniu instalacji zamknij instalator. Nie bedzie zadnych widocznych
skutków tego, iz instalacja zakonczyła sie powodzeniem (zadnych programów
zaistalowanych w menu Start lub nowych skrótów na pulpicie). PyXML jest po
prostu zbiorem bibliotek XML uzywanych przez inne programy.
Aby zweryfikowac czy PyXML zainstalował sie poprawnie, uruchom IDE Pythona i
sprawdz wersje zainstalowanych bibliotek XML, tak jak w tym przykładzie.
Przykład 12.3. Weryfikacja instalacji PyXML
>>> import xml
>>> xml.__version__
’0.8.3’
Ta wersja powinna odpowiadac numerowi wersji instalatora PyXML, który pobrałes
i uruchomiłes.
12.2.2. Instalacja fpconst
Druga biblioteka jaka potrzebujemy jest fpconst, zbiór stałych i funkcji do obsługi
wartosci zmienno-przecinkowych IEEE754. Dostarcza ona wartosci specjalne To-Nie-
Liczba (ang. Not-a-Number) (NaN), Dodatnia Nieskonczonosc (ang. Positive Infinity)
(Inf) i Ujemna Nieskonczonosc (ang. Negative Infinity) (-Inf), które sa czescia specyfikacji
typów danych SOAP.
Procedura 12.2.
A oto procedura instalacji fpconst:
1. Pobierz ostatnia wersje fpconst z http://www.analytics.washington.edu/
statcomp/projects/rzope/fpconst/ lub http://www.python.org/pypi/fpconst/.
13.2. INSTALOWANIE ODPOWIEDNICH BIBLIOTEK 269
2. Sa tam dwa pliki do pobrania, jeden w formacie .tar.gz, a drugi w formacie
.zip. Jesli uzywasz Windowsa, pobierz ten w formacie .zip; w przeciwnym razie
ten w formacie .tar.gz.
3. Rozpakuj pobrany plik. W Windows XP mozesz kliknac prawym przyciskiem
na pliku i wybrac pozycje Extract All; na wczesniejszych wersjach Windowsa
bedzie potrzebny dodatkowy program, np. WinZip. Na Mac OS X mozesz kliknac
dwukrotnie na spakowanym pliku, aby go rozpakowac przy pomocy Stuffit
Expander.
4. Otwórz okno linii polecen i przejdz do katalogu, w którym rozpakowałes pliki
fpconst.
5. Wpisz python setup.py install, aby uruchomic program instalujacy.
Aby zweryfikowac, czy fpconst zainstalował sie prawidłowo, uruchom IDE Pythona
i sprawdz numer wersji.
Przykład 12.4. Weryfikacja instalacji fpconst
>>> import fpconst
>>> fpconst.__version__
’0.6.0’
Ten numer wersji powinien odpowiadac wersji archiwum fpconst, które pobrałes
i zainstalowałes.
12.2.3. Instalacja SOAPpy
Trzecim i ostatnim wymogiem jest sama biblioteka: SOAPpy.
Procedura 12.3.
A oto procedura instalacji SOAPpy:
1. Wejdz na http://pywebsvcs.sourceforge.net/ i wybierz Ostatnie Oficjalne
Wydanie (ang. Latest Official Release) w sekcji SOAPpy.
2. Sa tam dwa pliki do wyboru. Jesli uzywasz Windowsa, pobierz plik .zip; w
przeciwnym wypadku pobierz plik .tar.gz.
3. Rozpakuj pobrany plik, tak jak to zrobiłes z fpconst.
4. Otwórz okno linii polecen i przejdz do katalogu, w którym rozpakowałes pliki
SOAPpy.
5. Wpisz python setup.py install, aby uruchomic program instalujacy.
Aby zweryfikowac, czy SOAPpy zostało zainstalowane poprawnie, uruchom IDE
Pythona i sprawdz numer wersji.
Przykład 12.5. Weryfikacja instalacji SOAPpy
>>> import SOAPpy
>>> SOAPpy.__version__
’0.11.4’
Ten numer wersji powinien odpowiadac wersji archiwum SOAPpy, które pobrałes
i zainstalowałes.
270 ROZDZIAŁ 13. SOAP
13.3 Pierwsze kroki z SOAP
Pierwsze kroki z SOAP
Sercem SOAP jest zdolnosc wywoływania zdalnych funkcji. Jest wiele publicznie
dostepnych serwerów SOAP, które udostepniaja proste funkcje do celów demonstracyjnych.
Najbardziej popularnym publicznie dostepnym serwerem SOAP jest http://www.
xmethods.net/. Ponizszy przykład wykorzystuje funkcje demostracyjna, która pobiera
kod pocztowy w USA i zwraca aktualna temperature w tym regionie.
Przykład 12.6. Pobranie aktualnej temperatury
>>> from SOAPpy import SOAPProxy #(1)
>>> url = ’http://services.xmethods.net:80/soap/servlet/rpcrouter’
>>> namespace = ’urn:xmethods-Temperature’ #(2)
>>> server = SOAPProxy(url, namespace) #(3)
>>> server.getTemp(’27502’) #(4)
80.0
1. Dostep do zdalnego serwera SOAP mozemy uzyskac poprzez klase proxy
SOAPProxy. Proxy wykonuje cała wewnetrzna robote zwiazana z SOAP, włacznie
z utworzeniem dokumentu XML zadania z nazwy funkcji i listy jej argumentów,
wysłaniem zadania za posrednictwem HTTP do zdalnego serwera SOAP, sparsowaniem
dokumentu XML odpowiedzi i utworzeniem wbudowanych wartosci
Pythona, które zwraca. Zobaczymy jak wygladaja te dokumenty XML w nastepnej
sekcji.
2. Kazda usługa SOAP posiada URL, który obsługuje wszystkie zadania. Ten sam
URL jest uzywany do wywoływania wszystkich funkcji. Ta konkretna usługa ma
tylko jedna funkcje, ale pózniej w tym rozdziale zobaczymy przykłady API Google’a,
które ma kilka funkcji. URL usługi jest współdzielony przez wszystkie
funkcje. Kazda usługa SOAP ma takze przestrzen nazw, która jest definiowana
przez serwer i jest zupełnie dowolna. Jest ona po prostu czescia konfiguracji
wymagana do wywoływania metod SOAP. Pozwala ona serwerowi na wykorzystywanie
jednego URL-a dla usługi i odpowiednie przekierowywanie zadan pomiedzy
kilkoma niepowiazanymi ze soba usługami. To jest podobne do podziału
modułów Pythona na pakiety.
3. Tworzymy instancje SOAPProxy podajac URL usługi i przestrzen nazw usługi.
Ta operacja nie wykonuje jeszcze zadnego połaczenia z serwerem SOAP; ona po
prostu tworzy lokalny obiekt Pythona.
4. Teraz, gdy wszystko jest odpowiednio skonfigurowane, mozemy własciwie wywołac
zdalne metody SOAP tak jakby to były lokalne funkcje. Przekazujemy
argumenty tak jak do normalnych funkcji i pobieramy wartosci zwrotne tez tak
jak od normalnych funcji. Ale pod spodem tak naprawde dzieje sie niezwykle
duzo.
A wiec zajrzyjmy pod spód.
13.4. DEBUGOWANIE SERWISU SIECIOWEGO SOAP 271
13.4 Debugowanie serwisu sieciowego SOAP
Debugowanie serwisu sieciowego SOAP
Biblioteki SOAP dostarczaja łatwego sposobu na zobaczenie co sie tak naprawde
dzieje za kulisami.
Właczenie debugowania to jest po prostu kwestia ustawienia dwóch flag w konfiguracji
SOAPProxy.
Przykład 12.7. Debugowanie serwisów SOAP
>>> from SOAPpy import SOAPProxy
>>> url = ’http://services.xmethods.net:80/soap/servlet/rpcrouter’
>>> n = ’urn:xmethods-Temperature’
>>> server = SOAPProxy(url, namespace=n) #(1)
>>> server.config.dumpSOAPOut = 1 #(2)
>>> server.config.dumpSOAPIn = 1
>>> temperature = server.getTemp(’27502’) #(3)
*** Outgoing SOAP ******************************************************
<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"
xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/"
xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance"
xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:xsd="http://www.w3.org/1999/XMLSchema">
<SOAP-ENV:Body>
<ns1:getTemp xmlns:ns1="urn:xmethods-Temperature" SOAP-ENC:root="1">
<v1 xsi:type="xsd:string">27502</v1>
</ns1:getTemp>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
************************************************************************
*** Incoming SOAP ******************************************************
<?xml version=’1.0’ encoding=’UTF-8’?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<SOAP-ENV:Body>
<ns1:getTempResponse xmlns:ns1="urn:xmethods-Temperature"
SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<return xsi:type="xsd:float">80.0</return>
</ns1:getTempResponse>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
************************************************************************
>>> temperature
80.0
1. Po pierwsze tworzymy normalnie SOAPProxy podajac URL serwisu i przestrzen
nazw.
272 ROZDZIAŁ 13. SOAP
2. Po drugie właczamy debugowanie poprzez ustawienie server.config.dumpSOAPIn
i server.config.dumpSOAPOut.
3. Po trzecie wywołujemy jak zwykle zdalna metode SOAP. Biblioteka SOAP wyswietli
zarówno wychodzacy dokument XML zadania, jak i przychodzacy dokument
XML odpowiedzi. To jest cała ciezka praca jaka SOAPProxy wykonuje dla
Ciebie. Przerazajace, nie prawdaz? Rozbiezmy to na czynniki.
Wiekszosc dokumentu XML zadania, który jest wysyłany do serwera, to sa elementy
stałe. Zignoruj wszystkie te deklaracje przestrzeni nazw; one nie ulegaja zmianie
(lub sa bardzo podobne) w trakcie wszystkich wywołan SOAP. Sercem “wywołania
funkcji” jest ten fragment w elemencie <Body>:
<ns1:getTemp #(1)
xmlns:ns1="urn:xmethods-Temperature" #(2)
SOAP-ENC:root="1">
<v1 xsi:type="xsd:string">27502</v1> #(3)
</ns1:getTemp>
1. Nazwa elementu jest nazwa funkcji: getTemp. SOAPProxy uzywa getattr jako
dyspozytora. Zamiast wywoływania poszczególnych metod lokalnych bazujac na
nazwie metody, uzywa on nazwy metody do skonstruowania dokumentu XML
zadania.
2. Element XML-a dotyczacy funkcji zawarty jest w konkretnej przestrzeni nazw,
która to jest ta podana podczas tworzenia instancji klasy SOAPProxy. Nie przejmuj
sie tym SOAP-ENC:root; to tez jest stały element.
3. Argumenty funkcji takze zostały przekształcone na XML-a. SOAPProxy uzywajac
introspekcji analizuje kazdy argument, aby okreslic jego typ (w tym wypadku jest
to string). Typ argumentu trafia do atrybutu xsi:type, a zaraz za nim podana
jest jego wartosc.
Zwracany dokument XML jest równie prosty do zrozumienia, jesli tylko wiesz co
nalezy zignorowac. Skup sie na tym fragmencie wewnatrz elementu <Body>:
<ns1:getTempResponse #(1)
xmlns:ns1="urn:xmethods-Temperature" #(2)
SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<return xsi:type="xsd:float">80.0</return> #(3)
</ns1:getTempResponse>
1. Serwer zawarł wartosc zwracana przez funkcje w elemencie <getTempResponse>.
Zgodnie z konwencja ten element jest nazwa funkcji plus słowo Response. Ale
tak naprawde to moze byc prawie cokolwiek; wazna rzecza jaka SOAPProxy
rozpatruje nie jest nazwa elementu, ale przestrzen nazw.
2. Serwer zwraca odpowiedz w tej samej przestrzeni nazw, której uzylismy w zadaniu,
tej samej przestrzeni nazw, która podalismy, gdy po raz pierwszy tworzylismy
obiekt klasy SOAPProxy. Dalej w tym rozdziale zobaczymy co sie stanie,
jesli zapomnimy podac przestrzen nazw podczas tworzenia obiektu SOAPProxy.
3. Zwracana wartosc jest podana wraz z jej typem (czyli float). SOAPProxy korzysta
z tego typu danych do utworzenia własciwego wbudowanego typu danych
Pythona i zwraca go.
13.5. WPROWADZENIE DO WSDL 273
13.5 Wprowadzenie do WSDL
Wprowadzenie do WSDL
Klasa SOAPProxy przezroczyscie przekształca wywołania lokalnych metod na wywołania
zdalnych metod SOAP. Jak moglismy zobaczyc, jest z tym duzo roboty, ale
SOAPProxy wykonuje to szybko i niewidocznie. Jednak nie dostarcza on zadnych
srodków słuzacych do introspekcji metod.
Rozwazmy to: dwa poprzednie podrozdziały pokazały przykłady wywoływania prostych
zdalnych metod SOAP z jednym argumentem i jedna zwracana wartoscia, a obydwa
były prostym typem danych.Wymagało to znajomosci URL-a serwisu, przestrzeni
nazw serwisu, nazwy funkcji, liczby argumentów i typu danych kazdego argumentu.
Jesli cos z tego pominiemy lub popełnimy w czyms bład, wszystko nawali.
Jednak nie powinno to byc wielka niespodzianka. Jesli chcemy wywołac lokalna
funkcje, musimy znac pakiet lub moduł, w którym ona sie znajduje (odpowiednik
URL-a serwisu i przestrzeni nazw). Potrzebujemy takze znac poprawna nazwe funkcji
i poprawna liczbe argumentów. Python doskonale radzi sobie z typami danych bez
wyraznego ich okreslenia, jednak my nadal musimy wiedziec, ile argumentów mamy
przekazac i na ile zwróconych wartosci bedziemy oczekiwac.
Ogromna róznica tkwi w introspekcji. Jak zobaczylismy w Rozdziale 4, Python
pozwala Tobie odkrywac moduły i funkcje podczas wykonywania programu. Mozemy
wypisac wszystkie funkcje dostepne w danym module, a takze gdy troche popracujemy
dokopac sie do deklaracji i argumentów pojedynczych funkcji.
WSDL pozwala robic to samo z serwisami internetowymi SOAP. W jezyku angielskim
WSDL jest skrótem od “Web Services Description Language”. Mimo ze został
elastycznie napisany, aby opisywac wiele rodzajów róznych serwisów sieciowych, jest
czesto wykorzystywany do opisywania serwisów SOAP.
Plik WSDL jest własnie... plikiem. A dokładniej, jest plikiem XML. Zazwyczaj
znajduje sie na tym samym serwerze, który wykorzystujemy do uzycia serwisu SOAP.
Pózniej w tym rozdziale, pobierzemy plik opisujacy API Google i wykorzystamy go
lokalnie. Nie oznacza to, ze bedziemy wywoływac Google lokalnie, poniewaz plik WSDL
nadal bedzie opisywał zewnetrzne funkcje rezydujace gdzies na serwerze Google.
Plik WSDL przechowuje opis wszystkiego, co jest zwiazane z wywoływaniem serwisu
SOAP, czyli:
• URL serwisu i przestrzen nazw
• typ serwisu sieciowego (prawdopodobnie wywołania funkcji sa wykonywane za
pomoca SOAP, jednak, jak było powiedziane wczesniej, WSDL jest wystarczajaco
elastyczny, aby opisac cała gamme róznych serwisów)
• liste dostepnych funkcji
• argumenty kazdej funkcji
• typy danych kazdego argumentu
• zwracane wartosci kazdej funkcji i ich typy danych
Innymi słowy, plik WSDL mówi o wszystkim, co potrzebujemy wiedziec, aby móc
wywoływac serwisy SOAP.
274 ROZDZIAŁ 13. SOAP
13.6 Introspekcja SOAP z uzyciem WSDL
Introspekcja SOAP z uzyciem WSDL
Podobnie jak wiele rzeczy w obszarze serwisów sieciowych, WSDL posiada burzliwa
historie, pełna politycznych sporów i intryg. Jednak przeskoczymy ten watek
historyczny, poniewaz moze wydawac sie nudny. Istnieje takze troche innych standardów,
które pełnia podobna funkcje, jednak WDSL jest najbardziej popularny, zatem
nauczmy sie go uzywac.
Najbardziej fundamentalna rzecza, na która nam pozwala WSDL jest odkrywanie
dostepnych metod oferowanych przez serwer SOAP.
Przykład 12.8. Odkrywanie dostepnych metod
>>> from SOAPpy import WSDL #(1)
>>> wsdlFile = ’http://www.xmethods.net/sd/2001/TemperatureService.wsdl’)
>>> server = WSDL.Proxy(wsdlFile) #(2)
>>> server.methods.keys() #(3)
[u’getTemp’]
1. SOAPpy zawiera parser WSDL.Wczasie pisania tego podrozdziału, parser ten okreslany
był jako moduł na wczesnym etapie rozwoju, jednak nie było problemów
podczas testowania z parsowanymi plikami WSDL.
2. Aby skorzystac z pliku WSDL, ponownie korzystamy z klasy posredniczacej
(ang. proxy), WSDL.Proxy, która przyjmuje pojedynczy argument: plik WSDL.
Zauwazmy, ze w tym przypadku przekazalismy adres URL pliku WSDL, który
jest przechowywany gdzies na zdalnym serwerze, ale klasa posredniczaca równie
dobrze sobie z nim radzi jak z lokalna kopia pliku WSDL. Podczas tworzenia
’posrednika” WSDL, plik WSDL zostanie pobrany i sparsowany, wiec jesli wystapia
jakies błedy w pliku WSDL (lub gdy bedziemy mieli problemy z siecia),
bedziemy o tym wiedziec natychmiast.
3. Klasa posredniczaca WSDL przechowuje dostepne funkcje w postaci pythonowego
słownika, server.methods. Zatem, aby pobrac liste dostepnych metod,
wystarczy wywołac metode keys nalezaca do słownika.
OK, wiec juz wiemy, ze ten serwer SOAP oferuje jedna metode: getTemp. Jednak
w jaki sposób ja wywołac? Obiekt posredniczacy WSDL moze nam takze o tym
powiedziec.
Przykład 12.9. Odkrywanie argumentów metody
>>> callInfo = server.methods[’getTemp’] #(1)
>>> callInfo.inparams #(2)
[<SOAPpy.wstools.WSDLTools.ParameterInfo instance at 0x00CF3AD0>]
>>> callInfo.inparams[0].name #(3)
u’zipcode’
>>> callInfo.inparams[0].type #(4)
(u’http://www.w3.org/2001/XMLSchema’, u’string’)
1. Słownik server.methods jest wypełniony okreslona przez SOAPpy struktura nazwana
CallInfo. Obiekt CallInfo zawiera informacje na temat jednej okreslonej
funkcji, właczajac w to argumenty funkcji.
13.6. INTROSPEKCJA SOAP Z UZYCIEM WSDL 275
2. Argumenty funkcji sa przechowywane w callInfo.inparams, która jest pythonowa
lista obiektów ParameterInfo, które z kolei zawieraja informacje na temat
kazdego parametru.
3. Kazdy obiekt ParameterInfo przechowuje atrybut name, który jest nazwa argumentu.
Nie trzeba znac nazwy argumentu, aby wywołac funkcje poprzez SOAP,
jednak SOAP obsługuje argumenty nazwane w wywołaniach funkcji (podobnie
jak Python), a za pomoca WSDL.Proxy bedziemy mogli poprawnie obsługiwac
nazywane argumenty, które zostaja przekazywane do zewnetrznej funkcji (oczywiscie,
jesli to właczymy).
4. Ponadto kazdy parametr ma wyraznie okreslony typ, a korzysta tu z typów
zdefiniowanych w XML Schema. Widzielismy to juz wczesniej; przestrzen nazw
“XML Schema” była czescia “formularza umowy”, jednak to zignorowalismy i
nadal mozemy to ignorowac, poniewaz tutaj do niczego nie jest nam to potrzebne.
Parametr zipcode jest łancuchem znaków i jesli przekazemy pythonowy łancuch
znaków do obiektu WSDL.Proxy, zostanie on poprawnie zmapowany i wysłany
na serwer.
WSDL takze nas informuje o zwracanych przez funkcje wartosciach.
Przykład 12.10. Odkrywanie zwracanych wartosci metody
>>> callInfo.outparams #(1)
[<SOAPpy.wstools.WSDLTools.ParameterInfo instance at 0x00CF3AF8>]
>>> callInfo.outparams[0].name #(2)
u’return’
>>> callInfo.outparams[0].type
(u’http://www.w3.org/2001/XMLSchema’, u’float’)
1. Uzupełnieniem do argumentów funkcji callInfo.inparams jest callInfo.outparams,
który odnosi sie do zwracanej wartosci. Jest to takze lista, poniewaz funkcje wywoływane
poprzez SOAP moga zwracac wiele wartosci, podobnie zreszta jak
funkcje Pythona.
2. Kazdy obiekt ParameterInfo zawiera atrybuty name i type. Funkcja ta zwraca
pojedyncza wartosci nazwana return, która jest liczba zmiennoprzecinkowa (czyli
float)..
Teraz połaczmy zdobyta wiedze i wywołajmy serwis sieciowy SOAP poprzez posrednika
WSDL.
Przykład 12.11. Wywoływanie usługi sieciowej poprzez WSDL.Proxy
>>> from SOAPpy import WSDL
>>> wsdlFile = ’http://www.xmethods.net/sd/2001/TemperatureService.wsdl’)
>>> server = WSDL.Proxy(wsdlFile) #(1)
>>> server.getTemp(’90210’) #(2)
66.0
>>> server.soapproxy.config.dumpSOAPOut = 1 #(3)
>>> server.soapproxy.config.dumpSOAPIn = 1
>>> temperature = server.getTemp(’90210’)
*** Outgoing SOAP ******************************************************
<?xml version="1.0" encoding="UTF-8"?>
276 ROZDZIAŁ 13. SOAP
<SOAP-ENV:Envelope SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"
xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/"
xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance"
xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:xsd="http://www.w3.org/1999/XMLSchema">
<SOAP-ENV:Body>
<ns1:getTemp xmlns:ns1="urn:xmethods-Temperature" SOAP-ENC:root="1">
<v1 xsi:type="xsd:string">90210</v1>
</ns1:getTemp>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
************************************************************************
*** Incoming SOAP ******************************************************
<?xml version=’1.0’ encoding=’UTF-8’?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<SOAP-ENV:Body>
<ns1:getTempResponse xmlns:ns1="urn:xmethods-Temperature"
SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<return xsi:type="xsd:float">66.0</return>
</ns1:getTempResponse>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
************************************************************************
>>> temperature
66.0
1. Widzimy, ze konfiguracja jest prostsza niz wywoływanie serwisu SOAP bezposrednio,
poniewaz plik WSDL zawiera informacje zarówno o URL serwisu, jak i
o przestrzeni nazw, która potrzebujemy, aby wywołac serwis. Tworzony obiekt
WSDL.Proxy pobiera plik WSDL, parsuje go i konfiguruje obiekt SOAPProxy,
który bedzie wykorzystywał do wywoływania konkretnego serwisu SOAP.
2. Po utworzeniu obiektu WSDL.Proxy, mozemy wywoływac funkcje równie prosto
jak za pomoca obiektu SOAPProxy. Nie jest to zaskakujace; WSDL.Proxy jest
własnie otoczka (ang. wrapper) dla SOAPProxy z kilkoma dodanymi metodami,
a wiec składnia wywoływania funkcji jest taka sama.
3. Mozemy dostac sie do obiektu SOAPProxy w WSDL.Proxy za pomoca server.soapproxy.
Ta opcja jest przydatna, aby właczyc debugowanie, dlatego tez kiedy wywołujemy
funkcje poprzez posrednika WSDL, jego SOAPProxy bedzie pokazywał przychodzace
i wychodzace przez łacze dokumenty XML.
13.7. WYSZUKIWANIE W GOOGLE 277
13.7 Wyszukiwanie w Google
Wyszukiwanie w Google
Powrócmy wreszcie do przykładu zamieszczonego na poczatku rozdziału, który robi
cos bardziej uzytecznego i interesujacego niz mierzenie obecnej temperatury.
Google dostarcza API SOAP dla korzystania z wyników wyszukiwania wewnatrz
programów. By móc z niego korzystac musisz zarejestrowac konto w Google Web Services.
Procedura 12.4. Zakładanie konta w Google Web Services
1. Wejdz na strone http://www.google.com/apis/ i stwórz konto Google. Potrzebny
jest Ci do tego tylko adres email. Po rejestracji poczta elektroniczna dostaniesz
swój klucz licencyjny Google API. Bedziesz z niego korzystac przy kazdym wywołaniu
funkcji wyszukiwarki Google.
2. Równiez ze strony http://www.google.com/apis/ pobierz zestaw dewelopera GoogleWeb
API. Zawiera on przykładowy kod w kilku jezykach programowania (ale
nie w Pythonie) i, co istotniejsze, plik WSDL.
3. Rozpakuj tenze zestaw i odnajdz w nim plik GoogleSearch.wsdl. Skopiuj go w
bezpieczne miejsce na swoim dysku. Przyda sie w dalszej czesci rozdziału.
Gdy bedziesz juz miec klucz dewelopera i plik WSDL Google w jakims pewnym
miejscu mozesz zaczac zabawe z Google Web Services.
Przykład 12.12. Wglad w głab Google Web Services
>>> from SOAPpy import WSDL
>>> server = WSDL.Proxy(’/path/to/your/GoogleSearch.wsdl’) #(1)
>>> server.methods.keys() #(2)
[u’doGoogleSearch’, u’doGetCachedPage’, u’doSpellingSuggestion’]
>>> callInfo = server.methods[’doGoogleSearch’]
>>> for arg in callInfo.inparams: #(3)
... print arg.name.ljust(15), arg.type
key (u’http://www.w3.org/2001/XMLSchema’, u’string’)
q (u’http://www.w3.org/2001/XMLSchema’, u’string’)
start (u’http://www.w3.org/2001/XMLSchema’, u’int’)
maxResults (u’http://www.w3.org/2001/XMLSchema’, u’int’)
filter (u’http://www.w3.org/2001/XMLSchema’, u’boolean’)
restrict (u’http://www.w3.org/2001/XMLSchema’, u’string’)
safeSearch (u’http://www.w3.org/2001/XMLSchema’, u’boolean’)
lr (u’http://www.w3.org/2001/XMLSchema’, u’string’)
ie (u’http://www.w3.org/2001/XMLSchema’, u’string’)
oe (u’http://www.w3.org/2001/XMLSchema’, u’string’)
1. Rozpoczecie korzystania z GoogleWeb Services jest proste: utwórz obiekt WSDL.Proxy
i wskaz mu miejsce, gdzie znajduje sie Twoja lokalna kopia pliku WSDL Google.
2. Wedle zawartosci pliku WSDL, Google udostepnia trzy funkcje: doGoogleSearch,
doGetCachedPage i doSpellingSuggestion. Robia dokładnie to, co sugeruja ich
nazwy. Pierwsza z nich wykonuje wyszukiwanie i zwraca jego wyniki, druga daje
dostep do kopii strony na serwerach Google (z okresu, kiedy była ostatnio odwiedzona
przez googlebota), a trzecia sugeruje poprawe błedów literowych we
wpisywanych hasłach.
278 ROZDZIAŁ 13. SOAP
3. Funkcja doGoogleSearch ma kilka parametrów róznego typu. Zauwaz, ze o ile z
zawartosci pliku WSDL mozna wywnioskowac rodzaj i typ argumentów, o tyle
niemozliwe jest stwierdzenie jak je wykorzystac. Teoretycznie mogłyby byc takze
okreslone przedziały, do których musza nalezec argumenty, jednak plik WSDL
Google nie jest tak szczegółowy. WSDL.Proxy nie czyni cudów — moze dostarczyc
Ci tylko informacji zawartych w pliku WSDL.
Ponizej znajduje sie zestawienie parametrów funkcji doGoogleSearch:
• key — Twój klucz licencyjny otrzymany po rejestracji konta Google Web Services.
• q — Słowo lub wyrazenie, którego szukasz. Składnia jest dokładnie taka sama jak
formularza wyszukiwania na stronie www Google, wiec zadziałaja tutaj wszelkie
znane Ci sztuczki lub zaawansowana składnia wyszukiwarki.
• start—Indeks wyniku wyszukiwania, od którego beda liczone zwrócone wyniki.
Podobnie do wersji interaktywnej wyszukiwarki, funkcja ta zwraca 10 wyników
na raz. Chcac uzyskac druga “strone” wyników wyszukiwania podajemy tutaj
10.
• maxResults — Liczba wyników do zwrócenia. Ograniczona z góry do 10, aczkolwiek,
gdy interesuje cie tylko kilka wyników, w celu oszczedzenia transferu
mozna podac wartosc mniejsza.
• filter — Podana wartosc True spowoduje, iz Google odfiltruje duplikaty stron
z wyników wyszukiwania.
• restrict — Ustawienie countryXX, gdzie XX to kod panstwa spowoduje wyswietlenie
wyników tylko dla danego panstwa, np. countryUK spowoduje wyszukiwanie
tylko dla Zjednoczonego Królestwa. Dopuszczalnymi wartosciami sa tez
linux, mac i bsd, które spowoduja wyszukiwanie w zdefiniowanych przez Google
zbiorach stron o tematyce technicznej, lub unclesam, które spowoduje wyszukiwanie
w materiałach dotyczacych rzadu i administracji Stanów Zjednoczonych.
• safeSearch — Dla wartosci True Google odfiltruje z wyników strony pornograficzne.
• lr (ang. “language restrict”—ograniczenie jezykowe)—Ustawienie konkretnego
kodu jezyka spowoduje wyswietlenie tylko stron w podanym jezyku.
• ie and oe (ang. “input encoding” — kodowanie wejsciowe, ang. “output encoding”
— kodowanie wyjsciowe) — Parametry przestarzałe. Oba musza przyjac
wartosc utf-8.
Przykład 12.13. Wyszukiwanie w Google
>>> from SOAPpy import WSDL
>>> server = WSDL.Proxy(’/path/to/your/GoogleSearch.wsdl’)
>>> key = ’YOUR_GOOGLE_API_KEY’
>>> results = server.doGoogleSearch(key, ’mark’, 0, 10, False, "",
... False, "", "utf-8", "utf-8") #(1)
>>> len(results.resultElements) #(2)
10
13.7. WYSZUKIWANIE W GOOGLE 279
>>> results.resultElements[0].URL #(3)
’http://diveintomark.org/’
>>> results.resultElements[0].title
’dive into <b>mark</b>’
1. Po przygotowaniu obiektu WSDL.Proxy mozemy wywołac server.doGoogleSearch
z wszystkimi dziesiecioma parametrami. Pamietaj o korzystaniu z własnego klucza
licencyjnego Google API otrzymanego podczas rejestracji w Google Web
Services.
2. Funkcja zwraca mnóstwo informacji, ale wpierw spójrzmy własnie na wyniki
wyszukiwania. Sa przechowywane w results.resultElements, a dostac sie do
nich mozemy tak jak do elementów zwykłej pythonowej listy.
3. Kazdy ze składników resultElements jest obiektem zawierajacym adres URL
(URL), tytuł (title), urywek tekstu strony (snippet) oraz inne uzyteczne atrybuty.
W tym momencie mozesz juz korzystac z normalnych technik introspekcji
Pythona do podejrzenia zawartosci tego obiektu (np. dir(results.resultElements[0])).
Mozesz takze te zawartosc podejrzec przy pomocy obiektu WSDL proxy i atrybutu
outparams samej funkcji. Obie techniki daja ten sam rezultat.
Obiekt wynikowy zawiera wiecej niz tylko wyniki wyszukiwania. Na przykład: informacje
na temat procesu szukania (ile trwał, ile wyników znaleziono — pomimo tego,
ze zwrócono tylko 10). Interfejs www wyszukiwarki pokazuje te informacje, wiec sa tez
dostepne metodami programistycznymi.
Przykład 12.14. Pobieranie z Google informacji pomocniczych
>>> results.searchTime #(1)
0.224919
>>> results.estimatedTotalResultsCount #(2)
29800000
>>> results.directoryCategories #(3)
[<SOAPpy.Types.structType item at 14367400>:
{’fullViewableName’:
’Top/Arts/Literature/World_Literature/American/19th_Century/Twain,_Mark’,
’specialEncoding’: }]
>>> results.directoryCategories[0].fullViewableName
’Top/Arts/Literature/World_Literature/American/19th_Century/Twain,_Mark’
1. To wyszukiwanie zajeło 0.224919 sekund. Wynik ten nie uwzglednia czasu poswieconego
na przesył oraz odbiór dokumentów XML protokołu SOAP. Jest to
wyłacznie czas poswiecony przez silnik Google na przetworzenie zapytania, juz
po otrzymaniu go.
2. Znaleziono około 30 milionów pasujacych stron. Dostep do kolejnych dziesiatek
z tego zbioru uzyskamy za pomoca zmiany argumentu start metody
server.doGoogleSearch i kolejnych jej wywołan.
3. Dla niektórych zapytan Google zwraca takze liste powiazanych kategorii z
katalogu Google. Dołaczajac zwrócone w ten sposób URL-e do przedrostka
http://directory.google.com/ uzyskamy adresy odpowiednich stron katalogu.
280 ROZDZIAŁ 13. SOAP
13.8 Rozwiazywanie problemów
Rozwiazywanie problemów
Oczywiscie swiat serwisów SOAP to nie jest tylko kraina mlekiem i miodem płynaca.
Czasami cos idzie nie tak.
Jak juz widziałes w tym rozdziale na SOAP składa sie kilka warstw. Jest tam
warstwa HTTP, poniewaz SOAP wysyła dokumenty XML do i odbiera te dokumenty
od serwera HTTP. A wiec wszystkie techniki dotyczace debugowania, których nauczyłes
sie w rozdziale 11 HTTP, maja zastosowanie takze tutaj. Mozesz zaimportowac
httplib i ustawic httplib.HTTPConnection.debuglevel = 1, aby zobaczyc
cały ruch odbywajacy sie poprzez HTTP.
Poza ta warstwa HTTP jest wiele rzeczy, która moga sie nie powiesc. SOAPpy wykonuje
godna podziwu robote ukrywajac przed Toba składnie SOAP, ale to oznacza
takze, ze moze byc trudne zdiagnozowanie problemu, gdy takowy sie pojawi.
Oto kilka przykładów pomyłek, które robiłem uzywajac serwisów SOAP i komunikaty
błedów jakie one spowodowały.
Przykład 12.15. Wywoływanie metod z niewłasciwie skonfigurowanym
Proxy
>>> from SOAPpy import SOAPProxy
>>> url = ’http://services.xmethods.net:80/soap/servlet/rpcrouter’
>>> server = SOAPProxy(url) #(1)
>>> server.getTemp(’27502’) #(2)
<Fault SOAP-ENV:Server.BadTargetObjectURI:
Unable to determine object id from call: is the method element namespaced?>
Traceback (most recent call last):
File "<stdin>", line 1, in ?
File "c:python23Libsite-packagesSOAPpyClient.py", line 453, in __call__
return self.__r_call(*args, **kw)
File "c:python23Libsite-packagesSOAPpyClient.py", line 475, in __r_call
self.__hd, self.__ma)
File "c:python23Libsite-packagesSOAPpyClient.py", line 389, in __call
raise p
SOAPpy.Types.faultType: <Fault SOAP-ENV:Server.BadTargetObjectURI:
Unable to determine object id from call: is the method element namespaced?>
1. Zauwazyłes pomyłke? Tworzymy recznie SOAPProxy i prawidłowo podajemy
URL serwisu, ale nie podalismy przestrzeni nazw. Poniewaz wiele serwisów moze
działas na tym samym URL-u, przestrzen nazw jest bardzo istotna, aby ustalic
do którego serwisu próbujemy sie odwołac, a nastepnie jaka metode własciwie
wywołujemy.
2. Serwer odpowiada poprzez wysłanie SOAP Fault, który SOAPpy zamienia na
pythonowy wyjatek typu SOAPpy.Types.faultType. Wszystkie błedy zwracane
przez serwer SOAP zawsze beda obiektami SOAP Fault, a wiec łatwo mozemy te
wyjatki przechwycic. W tym przypadku, ta czytelna dla człowieka czesc SOAP
Fault daje wskazówke do tego jaki jest problem: element metoda nie jest zawarty
w przestrzeni nazw, poniewaz oryginalny obiekt SOAPProxy nie został skonfigurowany
z przestrzenia nazw serwisu.
13.8. ROZWIAZYWANIE PROBLEMÓW 281
Błedna konfiguracja podstawowych elementów serwisu SOAP jest jednym z problemów,
które ma za zadanie rozwiazac WSDL. Plik WSDL zawiera URL serwisu i
przestrzen nazw, a wiec nie mozna ich podac błednie. Oczywiscie nadal sa inne rzeczy,
które moga zostac podane błednie.
Przykład 12.16. Wywołanie metody z nieprawidłowymi argumentami
>>> wsdlFile = ’http://www.xmethods.net/sd/2001/TemperatureService.wsdl’
>>> server = WSDL.Proxy(wsdlFile)
>>> temperature = server.getTemp(27502) #(1)
<Fault SOAP-ENV:Server: Exception while handling service request:
services.temperature.TempService.getTemp(int) -- no signature match> #(2)
Traceback (most recent call last):
File "<stdin>", line 1, in ?
File "c:python23Libsite-packagesSOAPpyClient.py", line 453, in __call__
return self.__r_call(*args, **kw)
File "c:python23Libsite-packagesSOAPpyClient.py", line 475, in __r_call
self.__hd, self.__ma)
File "c:python23Libsite-packagesSOAPpyClient.py", line 389, in __call
raise p
SOAPpy.Types.faultType: <Fault SOAP-ENV:Server: Exception while handling service request:
services.temperature.TempService.getTemp(int) -- no signature match>
1. Zauwazyłes pomyłke? To jest subtelna pomyłka: wywołujemy server.getTemp
z liczba całkowita (ang. integer) zamiast z łancuchem znaków (ang. string). Jak
juz widziałes w pliku WSDL, funkcja getTemp() SOAP przyjmuje pojedynczy
argument, kod pocztowy, który musi byc łancuchem znaków. WSDL.Proxy nie
bedzie konwertował typów danych; musimy podac dokładnie te typy danych jakich
serwer oczekuje.
2. I znowu, serwer zwraca SOAP Fault i czytelna dla człowieka czesc komunikatu
błedu daje wskazówke do tego, gdzie lezy problem: wywołujemy funkcje getTemp
z liczba całkowita, ale nie ma zdefiniowanej funkcji o tej nazwie, która przyjmowałaby
liczbe całkowita. W teorii SOAP pozwala na przeciazanie funkcji, a wiec
jeden serwis SOAP mógłby posiadac dwie funkcje o tej samej nazwie i z taka
sama liczba argumentów, ale z argumentami o róznych typach. O to dlaczego
tak wazne jest podawanie własciwych typów. i dlaczego WSDL.Proxy nie konwertuje
typów danych. Gdyby to robił, to mogłoby sie zdarzyc, ze wywołalibysmy
zupełnie inna funkcje! Powodzenia w debugowaniu takiego błedu. Duzo łatwiej
jest byc krytycznym wobec typów danych i zgłaszac błedy tak szybko jak to tylko
mozliwe.
Jest takze mozliwe napisanie kodu Pythona, który oczekuje innej liczby zwracanych
wartosci, niz zdalna funkcja własciwie zwraca.
Przykład 12.17. Wywołanie metody i oczekiwanie niewłasciwej liczby
zwracanych wartosci
>>> wsdlFile = ’http://www.xmethods.net/sd/2001/TemperatureService.wsdl’
>>> server = WSDL.Proxy(wsdlFile)
>>> (city, temperature) = server.getTemp(27502) #(1)
Traceback (most recent call last):
File "<stdin>", line 1, in ?
TypeError: unpack non-sequence
282 ROZDZIAŁ 13. SOAP
1. Zauwazyłes pomyłke? server.getTemp zwraca tylko jedna wartosc, liczbe zmiennoprzecinkowa
(ang. float), ale my napisalismy kod, który zakłada, ze otrzymamy
dwie wartosci i próbuje je przypisac do dwóch oddzielnych zmiennych.
Zauwaz, ze tutaj nie pojawił sie wyjatek SOAP fault. Co do zdalnego serwera, to
wszystko odbyło sie jak nalezy. Bład pojawił sie dopiero po zakonczeniu transakcji
SOAP, WSDL.Proxy zwrócił liczbe zmiennoprzecinkowa a nasz lokalny interpreter
Pythona próbował zgodnie z naszym zaleceniem podzielic ja pomiedzy
dwie zmienne. Poniewaz funkcja zwróciła tylko jedna wartosc, został zgłoszony
wyjatek Pythona, a nie SOAP Fault.
A co z serwisem Google? Najczestszym problemem jaki z nim miałem było to, ze
zapominałem własciwie ustawic klucz aplikacji.
Przykład 12.18. wywołanie metody z błedem specyficznym dla aplikacji
>>> from SOAPpy import WSDL
>>> server = WSDL.Proxy(r’/path/to/local/GoogleSearch.wsdl’)
>>> results = server.doGoogleSearch(’foo’, ’mark’, 0, 10, False, "", #(1)
... False, "", "utf-8", "utf-8")
<Fault SOAP-ENV:Server: #(2)
Exception from service object: Invalid authorization key: foo:
<SOAPpy.Types.structType detail at 14164616>:
{’stackTrace’:
’com.google.soap.search.GoogleSearchFault: Invalid authorization key: foo
at com.google.soap.search.QueryLimits.lookUpAndLoadFromINSIfNeedBe(
QueryLimits.java:220)
at com.google.soap.search.QueryLimits.validateKey(QueryLimits.java:127)
at com.google.soap.search.GoogleSearchService.doPublicMethodChecks(
GoogleSearchService.java:825)
at com.google.soap.search.GoogleSearchService.doGoogleSearch(
GoogleSearchService.java:121)
at sun.reflect.GeneratedMethodAccessor13.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
at java.lang.reflect.Method.invoke(Unknown Source)
at org.apache.soap.server.RPCRouter.invoke(RPCRouter.java:146)
at org.apache.soap.providers.RPCJavaProvider.invoke(
RPCJavaProvider.java:129)
at org.apache.soap.server.http.RPCRouterServlet.doPost(
RPCRouterServlet.java:288)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:760)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:853)
at com.google.gse.HttpConnection.runServlet(HttpConnection.java:237)
at com.google.gse.HttpConnection.run(HttpConnection.java:195)
at com.google.gse.DispatchQueue$WorkerThread.run(DispatchQueue.java:201)
Caused by: com.google.soap.search.UserKeyInvalidException: Key was of wrong size.
at com.google.soap.search.UserKey.<init>(UserKey.java:59)
at com.google.soap.search.QueryLimits.lookUpAndLoadFromINSIfNeedBe(
QueryLimits.java:217)
... 14 more
’}>
Traceback (most recent call last):
13.8. ROZWIAZYWANIE PROBLEMÓW 283
File "<stdin>", line 1, in ?
File "c:python23Libsite-packagesSOAPpyClient.py", line 453, in __call__
return self.__r_call(*args, **kw)
File "c:python23Libsite-packagesSOAPpyClient.py", line 475, in __r_call
self.__hd, self.__ma)
File "c:python23Libsite-packagesSOAPpyClient.py", line 389, in __call
raise p
SOAPpy.Types.faultType: <Fault SOAP-ENV:Server: Exception from service object:
Invalid authorization key: foo:
<SOAPpy.Types.structType detail at 14164616>:
{’stackTrace’:
’com.google.soap.search.GoogleSearchFault: Invalid authorization key: foo
at com.google.soap.search.QueryLimits.lookUpAndLoadFromINSIfNeedBe(
QueryLimits.java:220)
at com.google.soap.search.QueryLimits.validateKey(QueryLimits.java:127)
at com.google.soap.search.GoogleSearchService.doPublicMethodChecks(
GoogleSearchService.java:825)
at com.google.soap.search.GoogleSearchService.doGoogleSearch(
GoogleSearchService.java:121)
at sun.reflect.GeneratedMethodAccessor13.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
at java.lang.reflect.Method.invoke(Unknown Source)
at org.apache.soap.server.RPCRouter.invoke(RPCRouter.java:146)
at org.apache.soap.providers.RPCJavaProvider.invoke(
RPCJavaProvider.java:129)
at org.apache.soap.server.http.RPCRouterServlet.doPost(
RPCRouterServlet.java:288)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:760)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:853)
at com.google.gse.HttpConnection.runServlet(HttpConnection.java:237)
at com.google.gse.HttpConnection.run(HttpConnection.java:195)
at com.google.gse.DispatchQueue$WorkerThread.run(DispatchQueue.java:201)
Caused by: com.google.soap.search.UserKeyInvalidException: Key was of wrong size.
at com.google.soap.search.UserKey.<init>(UserKey.java:59)
at com.google.soap.search.QueryLimits.lookUpAndLoadFromINSIfNeedBe(
QueryLimits.java:217)
... 14 more
’}>
1. Zauwazyłes pomyłke? Nie ma błedów w samej składni wywołania lub w liczbie
argumentów lub w typach danych. Problem jest specyficzny dla tej konkretnej
aplikacji: pierwszym argumentem powinien byc nasz klucz aplikacji, ale foo nie
jest prawidłowym kluczem dla Google.
2. Serwer Google odpowiada poprzez SOAP Fault i niesamowicie długi komunikat
błedu, który zawiera kompletny zrzut stosu Javy. Zapamietaj, ze wszystkie błedy
SOAP sa oznaczane poprzez SOAP Faults: błedy w konfiguracjach, błedy w
argumentach funkcji i błedy specyficzne dla aplikacji jak ten. Zakopana gdzies
tam jest kluczowa informacja: Invalid authorization key: foo (niewłasciwy
klucz autoryzacji).
284 ROZDZIAŁ 13. SOAP
13.9 SOAP - podsumowanie
Podsumowanie
Serwisy internetowe SOAP sa bardzo skomplikowane. Specyfikacja jest bardzo ambitna
i próbuje sprostac wielu róznym przypadkom uzycia dla serwisów internetowych.
Ten rozdział dotknał jednego z prostszych przypadków uzycia.
Zanim zanurkujemy do nastepnego rozdziału, upewnij sie, ze opanowałes nastepujace
kwestie:
• Połaczenie sie z serwerem SOAP i wywołanie zdalnych metod
• Załadowanie pliku WSDL i uzycie go do introspekcji zdalnych metod
• Debugowanie wywołan SOAP ze sledzeniem komunikacji sieciowej
• Rozwiazywanie problemów z najczestszymi błedami dotyczacymi SOAP
Rozdział 14
Testowanie jednostkowe
285
286 ROZDZIAŁ 14. TESTOWANIE JEDNOSTKOWE
14.1 Wprowadzenie do liczb rzymskich
Wprowadzenie do liczb rzymskich
W poprzednich rozdziałach “nurkowalismy” poprzez bezposrednie przygladanie sie
kodowi, aby zrozumiec go tak szybko, jak to mozliwe. Teraz, gdy juz troche obeznalismy
Pythona, troche sie cofniemy i spojrzymy na kroki, które trzeba wykonac przed
napisaniem kodu.
Kilka rozdziałów wczesniej pisalismy, debugowalismy i optymalizowalismy zbiór
uzytecznych funkcji, które słuzyły do konwersji z i na liczby rzymskie. W Podrozdziale
7.3, “Analiza przypadku: Liczby rzymskie”, opisalismy mechanizm konstruowania i
sprawdzania poprawnosci liczb w zapisie rzymskim, lecz teraz cofnijmy sie troche i
zastanówmy sie, co moglibysmy uwzglednic, aby rozszerzyc to narzedzie, by w dwóch
kierunkach.
Zasady tworzenia liczb rzymskich prowadza do kilku interesujacych obserwacji:
1. Istnieje tylko jeden poprawny sposób reprezentowania pewnej liczby w postaci
rzymskiej.
2. Odwrotnosc tez jest prawda: jesli ciag znaków jest poprawna liczba rzymska, to
reprezentuje ona tylko jedna liczbe (tzn. mozemy ja przeczytac tylko w jeden
sposób).
3. Tylko ograniczony zakres liczb moze byc zapisany jako liczby rzymskie, a dokładniej
liczby od 1 do 3999 (Liczby rzymskiej posiadaja kilka sposobów wyrazania
wiekszych liczb np. poprzez dodanie nadkreslenia nad cyframi rzymskimi, co
oznacza, ze normalna wartosc tej liczby trzeba pomnozyc przez 1000, jednak
nie bedziemy sie wdawac w szczegóły. Dla potrzeb tego rozdziału, załozymy, ze
liczby rzymskie ida od 1 do 3999).
4. Nie mamy mozliwosc zapisania 0 jako liczby rzymskiej. (Co ciekawe, starozytni
Rzymianie nie wyobrazali sobie 0 jako liczby. Za pomoca liczb liczymy, ile czegos
mamy, jednak jak mozemy policzyc cos, czego nie mamy?)
5. Nie mozemy w postaci liczby rzymskiej zapisac liczby ujemnej.
6. W postaci liczby rzymskiej nie mozemy zapisywac ułamków, czy liczb, które nie
sa całkowite.
Biorac to wszystko pod uwage, co mozemy oczekiwac od zbioru funkcji, które konwertuja
z i na liczby rzymskie? Wymagania roman.py:
1. toRoman powinien zwracac rzymska reprezentacje wszystkich liczb całkowitych z
zakresu od 1 do 3999.
2. toRoman powinien nie zadziałac (ang. fail ), gdy otrzyma liczbe całkowita z poza
przedziału od 1 do 3999.
3. toRoman powinien nie zadziałac, gdy otrzyma niecałkowita liczbe.
4. fromRoman powinien przyjmowac poprawna liczbe rzymska i zwrócic liczbe, która
ja reprezentuje.
5. fromRoman powinien nie zadziałac, kiedy otrzyma niepoprawna liczbe rzymska.
14.1. WPROWADZENIE DO LICZB RZYMSKICH 287
6. Kiedy dana liczbe konwertujemy na liczbe rzymska, a nastepnie z powrotem
na liczbe, powinnismy otrzymac te sama liczbe, z która zaczynalismy. Wiec dla
kazdego n od 1 do 3999 fromRoman(toRoman(n)) == n.
7. toRoman powinien zawsze zwrócic liczbe rzymska korzystajac z wielkich liter.
8. fromRoman powinien akceptowac jedynie liczby rzymskie składajace sie z wielkich
liter (tzn. powinien nie zadziałac, gdy otrzyma wejscie złozone z małych liter).
288 ROZDZIAŁ 14. TESTOWANIE JEDNOSTKOWE
14.2 Testowanie - nurkujemy
Nurkujemy
Teraz, kiedy w pełni zdefiniowalismy zachowanie funkcji konwertujacych, zrobimy
cos odrobine niespodziewanego: napiszemy zestaw testów, który pokaze, co te funkcje
potrafia, a takze upewni nas, ze robia dokładnie to, co chcemy. Dobrze usłyszeliscie:
zaczniemy od napisania kodu testujacego kod, który nie został jeszcze napisany.
Takie podejscie nazywa sie testowaniem jednostkowym, poniewaz zestaw dwóch
funkcji konwertujacych moze byc napisany i przetestowany jako jednostka, niezaleznie
od kodu wiekszego programu, jakiego czescia moze sie ów zestaw stac w przyszłosci.
Python posiada gotowe narzedzie słuzace do testowania jednostkowego — moduł o
nazwie unittest.
Moduł unittest jest czescia dystrybucji jezyka Python od wersji 2.1 wzwyz. Uzytkownicy
starszych wersji (np. Python 2.0) moga pobrac ten moduł ze strony pyunit.
sourceforge.net.
Testowanie jednostkowe jest waznym elementem strategii rozwoju oprogramowania,
w której na pierwszym miejscu stawia sie testowanie. Jesli ma byc napisany jakis
test, to wazne jest, aby był on napisany jak najwczesniej (mozliwie przed napisaniem
testowanego kodu) oraz aby był aktualizowany wraz ze zmieniajacymi sie wymaganiami.
Testowanie jednostkowe nie zastepuje testowania wyzszego poziomu, takiego
jak testowanie funkcjonalne czy systemowe, ale jest bardzo istotne we wszystkich fazach
rozwoju oprogramowania:
1. Jeszcze przed napisaniem kodu zmusza nas do sprecyzowania i wyrazenia wymagan
w uzyteczny sposób.
2. Podczas pisania kodu chroni nas od niepotrzebnego kodowania. Kiedy wszystkie
testy przechodza, testowana funkcja jest juz gotowa.
3. Podczas refaktoryzacji upewnia nas, ze nowa wersja kodu zachowuje sie tak samo,
jak stara.
4. W procesie utrzymania kodu ochrania nas, kiedy ktos przychodzi do nas z krzykiem,
ze nasza ostatnia zmiana popsuła jego stary kod (“Alez prosze pana, w
momencie mojego czekinowania wszystkie testy przechodziły...”).
5. Podczas programowania w zespole zwieksza pewnosc, ze nowy kod, który chcemy
dodac, nie popsuje kodu innych osób, poniewaz najpierw uruchomimy ich testy
(Widziałem to juz podczas tzw. sprintów. Zespół dzieli zadanie miedzy siebie,
kazdy otrzymuje specyfikacje tego, nad czym bedzie pracowac, pisze do tego
testy jednostkowe, a nastepnie dzieli sie tymi testami z pozostałymi członkami
zespołu. W ten sposób nikt nie posunie sie zbyt daleko w rozwijaniu kodu, który
nie współdziała z kodem innych osób).
14.3. WPROWADZENIE DO ROMANTEST.PY 289
14.3 Wprowadzenie do romantest.py
Wprowadzenie do romantest.py
Ponizej przedstawiono pełny zestaw testów do funkcji konwertujacych, które nie
zostały jeszcze napisane, ale wkrótce znajda sie w roman.py. Nie jest wcale oczywiste,
jak to wszystko ze soba działa; zadna z ponizszych klas i metod nie odnosi sie do
zadnej innej. Jak wkrótce zobaczymy, ma to swoje uzasadnienie.
Przykład 13.1. romantest.py
Jesli jeszcze tego nie zrobiłes, mozesz pobrac ten oraz inne przykłady (http://
diveintopython.org/download/diveintopython-examples-5.4.zip) uzywane w tej
ksiazce.
"""Unit test for roman.py"""
import roman
import unittest
class KnownValues(unittest.TestCase):
knownValues = ( (1, ’I’),
(2, ’II’),
(3, ’III’),
(4, ’IV’),
(5, ’V’),
(6, ’VI’),
(7, ’VII’),
(8, ’VIII’),
(9, ’IX’),
(10, ’X’),
(50, ’L’),
(100, ’C’),
(500, ’D’),
(1000, ’M’),
(31, ’XXXI’),
(148, ’CXLVIII’),
(294, ’CCXCIV’),
(312, ’CCCXII’),
(421, ’CDXXI’),
(528, ’DXXVIII’),
(621, ’DCXXI’),
(782, ’DCCLXXXII’),
(870, ’DCCCLXX’),
(941, ’CMXLI’),
(1043, ’MXLIII’),
(1110, ’MCX’),
(1226, ’MCCXXVI’),
(1301, ’MCCCI’),
(1485, ’MCDLXXXV’),
(1509, ’MDIX’),
(1607, ’MDCVII’),
290 ROZDZIAŁ 14. TESTOWANIE JEDNOSTKOWE
(1754, ’MDCCLIV’),
(1832, ’MDCCCXXXII’),
(1993, ’MCMXCIII’),
(2074, ’MMLXXIV’),
(2152, ’MMCLII’),
(2212, ’MMCCXII’),
(2343, ’MMCCCXLIII’),
(2499, ’MMCDXCIX’),
(2574, ’MMDLXXIV’),
(2646, ’MMDCXLVI’),
(2723, ’MMDCCXXIII’),
(2892, ’MMDCCCXCII’),
(2975, ’MMCMLXXV’),
(3051, ’MMMLI’),
(3185, ’MMMCLXXXV’),
(3250, ’MMMCCL’),
(3313, ’MMMCCCXIII’),
(3408, ’MMMCDVIII’),
(3501, ’MMMDI’),
(3610, ’MMMDCX’),
(3743, ’MMMDCCXLIII’),
(3844, ’MMMDCCCXLIV’),
(3888, ’MMMDCCCLXXXVIII’),
(3940, ’MMMCMXL’),
(3999, ’MMMCMXCIX’))
def testToRomanKnownValues(self):
"""toRoman should give known result with known input"""
for integer, numeral in self.knownValues:
result = roman.toRoman(integer)
self.assertEqual(numeral, result)
def testFromRomanKnownValues(self):
"""fromRoman should give known result with known input"""
for integer, numeral in self.knownValues:
result = roman.fromRoman(numeral)
self.assertEqual(integer, result)
class ToRomanBadInput(unittest.TestCase):
def testTooLarge(self):
"""toRoman should fail with large input"""
self.assertRaises(roman.OutOfRangeError, roman.toRoman, 4000)
def testZero(self):
"""toRoman should fail with 0 input"""
self.assertRaises(roman.OutOfRangeError, roman.toRoman, 0)
def testNegative(self):
"""toRoman should fail with negative input"""
self.assertRaises(roman.OutOfRangeError, roman.toRoman, -1)
14.3. WPROWADZENIE DO ROMANTEST.PY 291
def testNonInteger(self):
"""toRoman should fail with non-integer input"""
self.assertRaises(roman.NotIntegerError, roman.toRoman, 0.5)
class FromRomanBadInput(unittest.TestCase):
def testTooManyRepeatedNumerals(self):
"""fromRoman should fail with too many repeated numerals"""
for s in (’MMMM’, ’DD’, ’CCCC’, ’LL’, ’XXXX’, ’VV’, ’IIII’):
self.assertRaises(roman.InvalidRomanNumeralError, roman.fromRoman, s)
def testRepeatedPairs(self):
"""fromRoman should fail with repeated pairs of numerals"""
for s in (’CMCM’, ’CDCD’, ’XCXC’, ’XLXL’, ’IXIX’, ’IVIV’):
self.assertRaises(roman.InvalidRomanNumeralError, roman.fromRoman, s)
def testMalformedAntecedent(self):
"""fromRoman should fail with malformed antecedents"""
for s in (’IIMXCC’, ’VX’, ’DCM’, ’CMM’, ’IXIV’,
’MCMC’, ’XCX’, ’IVI’, ’LM’, ’LD’, ’LC’):
self.assertRaises(roman.InvalidRomanNumeralError, roman.fromRoman, s)
class SanityCheck(unittest.TestCase):
def testSanity(self):
"""fromRoman(toRoman(n))==n for all n"""
for integer in range(1, 4000):
numeral = roman.toRoman(integer)
result = roman.fromRoman(numeral)
self.assertEqual(integer, result)
class CaseCheck(unittest.TestCase):
def testToRomanCase(self):
"""toRoman should always return uppercase"""
for integer in range(1, 4000):
numeral = roman.toRoman(integer)
self.assertEqual(numeral, numeral.upper())
def testFromRomanCase(self):
"""fromRoman should only accept uppercase input"""
for integer in range(1, 4000):
numeral = roman.toRoman(integer)
roman.fromRoman(numeral.upper())
self.assertRaises(roman.InvalidRomanNumeralError,
roman.fromRoman, numeral.lower())
if __name__ == "__main__":
unittest.main()
292 ROZDZIAŁ 14. TESTOWANIE JEDNOSTKOWE
Materiały dodatkowe
• Na stronie domowej PyUnit znajduje sie szeroka dyskusja na temat uzywania
zrebu unittest, łacznie z zagadnieniami zaawansowanymi, których w tym rozdziale
nie poruszono.
• Czesto Zadawane Pytania dotyczace PyUnit wyjasniaja, dlaczego przypadki testowe
umieszczane sa oddzielnie w stosunku do kodu, który testuja.
• Python Library Reference podsumowuje moduł unittest
• ExtremeProgramming.org omawia przyczyny, dla których nalezy pisac testy jednostkowe
• Na stronach The Portland Pattern Repository mozna znalezc trwajaca wciaz
dyskusje na temat testów jednostkowych, standardowa definicje testu jednostkowego,
przyczyny pisania testów przed pisaniem kodu oraz wiele dogłebnych
analiz na ten temat.
14.4. TESTOWANIE POPRAWNYCH PRZYPADKÓW 293
14.4 Testowanie poprawnych przypadków
Tworzenie poszczególnych przypadków testowych nalezy do najbardziej podstawowych
elementów testowania jednostkowego. Przypadek testowy stanowi odpowiedz na
pewne pytanie dotyczace kodu, który jest testowany.
Przypadek testowy powinien:
• ...działac bez koniecznosci wprowadzania danych przez człowieka. Testowanie
jednostkowe powinno byc zautomatyzowane.
• ...samodzielnie stwierdzac, czy testowana funkcja działa poprawnie, czy nie, bez
koniecznosci interpretacji wyników przez człowieka.
• ...działac w izolacji, oddzielnie i niezaleznie od innych przypadków testowych
(nawet wówczas, gdy testuja one te same funkcje). Kazdy przypadek testowy
powinien byc “wyspa”.
Zbudujmy wiec pierwszy przypadek testowy, biorac powyzsze pod uwage. Mamy
nastepujace wymaganie:
1. Funkcja toRoman powinna zwracac tekstowa reprezentacje w zapisie rzymskim
wszystkich liczb całkowitych z przedziału od 1 do 3999.
Przykład 13.2. testToRomanKnownValues
class KnownValues(unittest.TestCase): #(1)
knownValues = ( (1, ’I’),
(2, ’II’),
(3, ’III’),
(4, ’IV’),
(5, ’V’),
(6, ’VI’),
(7, ’VII’),
(8, ’VIII’),
(9, ’IX’),
(10, ’X’),
(50, ’L’),
(100, ’C’),
(500, ’D’),
(1000, ’M’),
(31, ’XXXI’),
(148, ’CXLVIII’),
(294, ’CCXCIV’),
(312, ’CCCXII’),
(421, ’CDXXI’),
(528, ’DXXVIII’),
(621, ’DCXXI’),
(782, ’DCCLXXXII’),
(870, ’DCCCLXX’),
(941, ’CMXLI’),
294 ROZDZIAŁ 14. TESTOWANIE JEDNOSTKOWE
(1043, ’MXLIII’),
(1110, ’MCX’),
(1226, ’MCCXXVI’),
(1301, ’MCCCI’),
(1485, ’MCDLXXXV’),
(1509, ’MDIX’),
(1607, ’MDCVII’),
(1754, ’MDCCLIV’),
(1832, ’MDCCCXXXII’),
(1993, ’MCMXCIII’),
(2074, ’MMLXXIV’),
(2152, ’MMCLII’),
(2212, ’MMCCXII’),
(2343, ’MMCCCXLIII’),
(2499, ’MMCDXCIX’),
(2574, ’MMDLXXIV’),
(2646, ’MMDCXLVI’),
(2723, ’MMDCCXXIII’),
(2892, ’MMDCCCXCII’),
(2975, ’MMCMLXXV’),
(3051, ’MMMLI’),
(3185, ’MMMCLXXXV’),
(3250, ’MMMCCL’),
(3313, ’MMMCCCXIII’),
(3408, ’MMMCDVIII’),
(3501, ’MMMDI’),
(3610, ’MMMDCX’),
(3743, ’MMMDCCXLIII’),
(3844, ’MMMDCCCXLIV’),
(3888, ’MMMDCCCLXXXVIII’),
(3940, ’MMMCMXL’),
(3999, ’MMMCMXCIX’)) #(2)
def testToRomanKnownValues(self): #(3)
"""toRoman should give known result with known input"""
for integer, numeral in self.knownValues:
result = roman.toRoman(integer) #(4) #(5)
self.assertEqual(numeral, result) #(6)
1. Wcelu utworzenia przypadku testowego tworzymy nowa podklase klasy TestCase
z modułu unittest. Klasa TestCase udostepnia wiele uzytecznych metod, które
mozna uzyc we własnym przypadku testowym celem przetestowania okreslonych
warunków.
2. Jest to lista par liczba całkowita/wartosc w zapisie rzymskim, których poprawnosc
sprawdziłem recznie. Zawiera ona dziesiec najnizszych liczb, liczbe najwieksza,
kazda liczbe, która jest reprezentowana przy pomocy jednego znaku w
zapisie rzymskim oraz pewne inne, losowo wybrane wartosci. Celem przypadku
testowego nie jest przetestowanie wszystkich mogacych sie pojawic danych wejsciowych,
lecz pewnej reprezentatywnej próbki.
14.4. TESTOWANIE POPRAWNYCH PRZYPADKÓW 295
3. Kazdy pojedynczy test posiada swoja metode, która nie bierze zadnych parametrów
oraz nie zwraca zadnej wartosci. Jesli metoda zakonczy sie normalnie
bez rzucenia wyjatku, uznaje sie wówczas, ze taki test przeszedł; jesli z metody
zostanie rzucony wyjatek, wówczas uznaje sie, ze test nie przeszedł.
4. W tym miejscu wołamy funkcje toRoman. (Rzeczywiscie, funkcja ta nie została
jeszcze napisana, ale kiedy juz ja napiszemy, ta własnie linijka spowoduje jej wywołanie).
Zauwazmy, ze własnie zdefiniowalismy API funkcji toRoman: pobiera
ona argument typu int (liczbe, która ma zostac przekształcona na zapis rzymski)
i zwraca wartosc typu string (rzymska reprezentacje wartosci przekazanej
w parametrze). Jesli rzeczywiste API bedzie inne, ten test zakonczy sie niepowodzeniem.
5. Zauwazmy równiez, ze podczas wywoływania toRoman nie łapiemy zadnych wyjatków.
Jest to celowe. Funkcja toRoman nie powinna zgłaszac wyjatków w sytuacji,
gdy wywołujemy ja z prawidłowymi wartosciami, a wszystkie wartosci, z
którymi ja wywołujemy, sa poprawne. Jesli toRoman rzuci wyjatek, test zakonczy
sie niepowodzeniem.
6. Jesli załozymy, ze funkcja toRoman została poprawnie zdefiniowana i wywołana
oraz poprawnie sie zakonczyła, zwracajac pewna wartosc, to ostatnia rzecza,
jaka musimy sprawdzic, jest to, czy zwrócona wartosc jest poprawna. Tego rodzaju
sprawdzenie jest bardzo powszechne, a w klasie TestCase istnieje metoda
assertEqual, która moze w tym pomóc: sprawdza ona, czy dwie wartosci sa sobie
równe. Jesli wartosc zwrócona przez funkcje toRoman (result) nie jest równa
znanej nam, spodziewanej wartosci (numeral), assertEqual spowoduje rzucenie
wyjatku, a test zakonczy sie niepowodzeniem. Jesli te dwie wartosci sa równe,
metoda ta nic nie robi. Jesli kazda wartosc zwrócona przez toRoman pasuje do
wartosci, której sie spodziewamy, to assertEqual nigdy nie rzuci wyjatku, a
wiec testToRomanKnownValues zakonczy sie normalnie, co oznacza, ze funkcja
toRoman przeszła ten test.
296 ROZDZIAŁ 14. TESTOWANIE JEDNOSTKOWE
14.5 Testowanie niepoprawnych przypadków
Testowanie funkcji w sytuacji, w której na wejsciu pojawiaja sie wyłacznie poprawne
wartosci, nie jest wystarczajace; nalezy dodatkowo sprawdzic, ze funkcja konczy
sie niepowodzeniem, gdy otrzymuje ona niepoprawne dane wejsciowe. Nie moze to
byc jednak dowolne niepowodzenie; musi byc ono dokładnie takie, jakiego sie spodziewamy.
Przypomnijmy sobie pozostałe wymagania dotyczace funkcji toRoman:
2. Funkcja toRoman powinna konczyc sie niepowodzeniem, gdy przekazana jest jej
wartosc spoza przedziału od 1 do 3999.
3. Funkcja toRoman powinna konczyc sie niepowodzeniem, gdy przekazana jest jej
wartosc nie bedaca liczba całkowita.
Wjezyku Python funkcje koncza sie niepowodzeniem wówczas, gdy rzucaja wyjatki.
W module unittest znajduja sie natomiast metody, dzieki którym mozna wykryc, czy
funkcja, otrzymawszy niepoprawne dane wejsciowe, rzuca odpowiedni wyjatek:
Przykład 13.3. Testowanie niepoprawnych danych wejsciowych do funkcji
toRoman
class ToRomanBadInput(unittest.TestCase):
def testTooLarge(self):
"""toRoman should fail with large input"""
self.assertRaises(roman.OutOfRangeError, roman.toRoman, 4000) #(1)
def testZero(self):
"""toRoman should fail with 0 input"""
self.assertRaises(roman.OutOfRangeError, roman.toRoman, 0) #(2)
def testNegative(self):
"""toRoman should fail with negative input"""
self.assertRaises(roman.OutOfRangeError, roman.toRoman, -1)
def testNonInteger(self):
"""toRoman should fail with non-integer input"""
self.assertRaises(roman.NotIntegerError, roman.toRoman, 0.5) #(3)
1. Klasa TestCase z modułu unittest udostepnia metode assertRaises, która
przyjmuje nastepujace argumenty: wyjatek, którego sie spodziewamy, funkcje,
która testujemy oraz argumenty, które maja byc przekazane do funkcji (jesli testowana
funkcja przyjmuje wiecej niz jeden argument, nalezy je wszystkie przekazac
po kolei do funkcji assertRaises, która przekaze je w tej własnie kolejnosci
do testowanej funkcji). Zwróccie baczna uwage na to, co tutaj robimy: zamiast
recznego wywoływania funkcji i sprawdzania, czy został rzucony wyjatek
odpowiedniego typu (poprzez otoczenie wywołania blokiem try...except), uzywamy
funkcji assertRaises, która robi to wszystko za nas. Wszystko, co nalezy
zrobic, to przekazac typ wyjatku (roman.OutOfRangeError), funkcje (toRoman)
oraz jej argument (4000), a assertRaises zajmie sie wywołaniem toRoman z
przekazanym parametrem oraz sprawdzeniem, czy rzucony wyjatek to rzeczywiscie
roman.OutOfRangeError. (Zauwazmy równiez, ze do funkcji assertRaises
przekazujemy funkcje toRoman jako parametr; nie wywołujemy jej ani nie przekazujemy
jej nazwy w postaci napisu. Czy wspominałem ostatnio, jak bardzo
14.5. TESTOWANIE NIEPOPRAWNYCH PRZYPADKÓW 297
przydatne jest to, ze w jezyku Python wszystko jest obiektem, właczajac w to
funkcje i wyjatki?)
2. Oprócz przetestowania wartosci zbyt duzych nalezy tez przetstowac wartosci
zbyt małe. Pamietajmy, ze w zapisie rzymskim nie mozna wyrazic ani wartosci
0, ani liczb ujemnych, wiec dla kazdej z tych sytuacji mamy przypadek testowy
(testZero i testNegative). W funkcji testZero sprawdzamy, czy toRoman
rzuca wyjatek roman.OutOfRangeError, gdy jest wywołana z wartoscia 0; jesli
nie rzuci tego wyjatku (zarówno z powodu zwrócenia pewnej wartosci jak i
rzucenia jakiegos innego wyjatku), test powinien zakonczyc sie niepowodzeniem.
3. Wymaganie #3 okresla, ze toRoman nie moze przyjac jako danych wejsciowych
liczb niecałkowitych, wiec tutaj upewniamy sie, ze dla wartosci 0.5 toRoman rzuci
wyjatek roman.NotIntegerError. Jesli toRoman nie rzuci takiego wyjatku, test
ten powinien zakonczyc sie niepowodzeniem.
Kolejne dwa wymagania sa podobne do pierwszych trzech, przy czym odnosza sie
one do funkcji fromRoman zamiast toRoman:
4. Funkcja fromRoman powinna przyjmowac napis bedacy poprawna liczba w zapisie
rzymskim i zwracac liczbe całkowita, która ten napis reprezentuje.
5. Funkcja fromRoman powinna zakonczyc sie niepowodzeniem, gdy otrzyma na
wejsciu napis nie bedacy poprawna liczba w zapisie rzymskim.
Wymaganie #4 jest obsługiwane w podobny sposób, jak wymaganie #1, poprzez
iterowanie po zestawie znanych wartosci i testowanie kazdej z nich. Wymaganie #5
jest z kolei obsługiwane podobnie, jak wymagania #2 i #3, poprzez testowanie serii
niepoprawnych ciagów wejsciowych i sprawdzanie, czy fromRoman rzuca odpowiedni
wyjatek.
Przykład 13.4. Testowanie niepoprawnych danych wejsciowych do funkcji
fromRoman
class FromRomanBadInput(unittest.TestCase):
def testTooManyRepeatedNumerals(self):
"""fromRoman should fail with too many repeated numerals"""
for s in (’MMMM’, ’DD’, ’CCCC’, ’LL’, ’XXXX’, ’VV’, ’IIII’):
self.assertRaises(roman.InvalidRomanNumeralError, roman.fromRoman, s) #(1)
def testRepeatedPairs(self):
"""fromRoman should fail with repeated pairs of numerals"""
for s in (’CMCM’, ’CDCD’, ’XCXC’, ’XLXL’, ’IXIX’, ’IVIV’):
self.assertRaises(roman.InvalidRomanNumeralError, roman.fromRoman, s)
def testMalformedAntecedent(self):
"""fromRoman should fail with malformed antecedents"""
for s in (’IIMXCC’, ’VX’, ’DCM’, ’CMM’, ’IXIV’,
’MCMC’, ’XCX’, ’IVI’, ’LM’, ’LD’, ’LC’):
self.assertRaises(roman.InvalidRomanNumeralError, roman.fromRoman, s)
1. Nie ma tu nic nowego do powiedzenia: wzorzec postepowania jest dokładnie taki
sam jak w przypadku testowania niepoprawnego wejscia do funkcji toRoman. Zaznacze
tylko, ze mamy teraz nieco inny wyjatek: roman.InvalidRomanNumeralError.
Okazało sie wiec, ze potrzebujemy trzech okreslonych przez nas wyjatków, które
298 ROZDZIAŁ 14. TESTOWANIE JEDNOSTKOWE
powinny zostac zdefiniowane w roman.py (wraz z roman.OutOfRangeError i
roman.NotIntegerError). Kiedy juz zajmiemy sie implementacja roman.py w
dalszej czesci tego rozdziału, dowiesz sie, jak definiowac własne wyjatki.
14.6. TESTOWANIE ZDROWOROZSADKOWE 299
14.6 Testowanie zdroworozsadkowe
Dosc czesto zdarza sie, ze pewien fragment kodu zawiera zbiór funkcji powiazanych
ze soba; zwykle sa to funkcje konwertujace, z których pierwsza przekształca A do B, a
druga przekształca B do A. W takim przypadku rozsadnie jest utworzyc “zdroworozsadkowe
sprawdzenie”, dzieki któremu upewnimy sie, ze mozemy przekształcic A do
B i z powrotem do A bez utraty dokładnosci, bez wprowadzania błedów zaokraglen i
bez powodowania jakichkolwiek błedów innego typu.
Rozwazmy nastepujace wymaganie:
6. Jesli mamy pewna wartosc liczbowa, która przekształcamy na reprezentacje w zapisie
rzymskim, a te przekształcamy z powrotem do wartosci liczbowej, powinnismy otrzymac
wartosc, od której rozpoczynalismy przekształcenie. A wiec fromRoman(toRoman(n))
== n dla kazdego n w przedziale 1..3999.
Przykład 13.5. Testowanie toRoman wzgledem fromRoman
class SanityCheck(unittest.TestCase):
def testSanity(self):
"""fromRoman(toRoman(n))==n for all n"""
for integer in range(1, 4000): #(1) #(2)
numeral = roman.toRoman(integer)
result = roman.fromRoman(numeral)
self.assertEqual(integer, result) #(3)
1. Funkcje range widzielismy juz wczesniej, z tym, ze tutaj wywołana jest ona z
dwoma parametrami, dzieki czemu zwraca liste kolejnych liczb całkowitych z
przedziału od wartosci bedacej pierwszym argumentem funkcji (1) do wartosci
bedacej drugim argumentem funkcji (4000), bez tej wartosci. Zwróci wiec kolejne
liczby z przedziału 1..3999, które stanowia zakres poprawnych wartosci wejsciowych
do funkcji konwertujacej na notacje rzymska.
2. Jesli juz tu jestesmy, to wspomne tylko, ze integer nie jest słowem kluczowym
jezyka Python; zostało ono uzyte po prostu jako nazwa zmiennej.
3. Własciwa logika testujaca jest oczywista: bierzemy liczbe całkowita (integer),
przekształcamy ja do reprezentacji rzymskiej (numeral), nastepnie reprezentacje
ta przekształcamy z powrotem do wartosci całkowitej (result) i upewniamy
sie, ze otrzymalismy te sama wartosc, od której rozpoczelismy przekształcenia.
Jesli nie jest to prawda, wówczas assertEqual rzuci wyjatek, a test natychmiast
zakonczy sie niepowodzeniem. Jesli zas kazda liczba po przekształceniach jest
równa wartosci poczatkowej to assertEqual zakonczy sie prawidłowo, równiez
testSanity zakonczy sie prawidłowo, a test zakonczy sie powodzeniem.
Ostatnie dwa wymagania róznia sie od poprzednich, poniewaz wydaja sie arbitralne
i trywialne zarazem:
7. Funkcja toRoman powinna zwracac napis reprezentujacy liczbe w notacji rzymskiej
przy uzyciu wyłacznie wielkich liter.
8. Funkcja fromRoman powinna akceptowac na wejsciu napisy reprezentujace liczby
w notacji rzymskiej pisane wyłacznie wielkimi literami (tj. powinna zakonczyc sie
niepowodzeniem, gdy w napisie wejsciowym znajduja sie małe litery).
300 ROZDZIAŁ 14. TESTOWANIE JEDNOSTKOWE
Nie da sie ukryc, ze wymagania te sa troche arbitralne. Moglibysmy przeciez ustalic,
ze fromRoman przyjmuje zarówno napisy składajace sie z małych liter, jak równiez
napisy zawierajace zarówno małe, jak i duze litery. Z drugiej strony, wymagania te
nie sa całkowicie arbitralne: jesli toRoman zawsze zwraca napisy składajace sie z wielkich
liter, wówczas fromRoman musi akceptowac na wejsciu przynajmniej te napisy,
które składaja sie wyłacznie z wielkich liter, inaczej “zdroworozsadkowe sprawdzenie”
(wymaganie #6) zakonczy sie niepowodzeniem. Ustalenie, ze na wejsciu przyjmujemy
napisy złozone wyłacznie z wielkich liter, jest arbitralne, jednak — jak potwierdzi to
kazdy integrator systemów — wielkosc znaków ma zawsze znaczenie, a wiec warto od
razu te kwestie wyspecyfikowac. A skoro warto ja wyspecyfikowac, to warto ja równiez
przetestowac.
Przykład 13.6. Testowanie wielkosci znaków
class CaseCheck(unittest.TestCase):
def testToRomanCase(self):
"""toRoman should always return uppercase"""
for integer in range(1, 4000):
numeral = roman.toRoman(integer)
self.assertEqual(numeral, numeral.upper()) #(1)
def testFromRomanCase(self):
"""fromRoman should only accept uppercase input"""
for integer in range(1, 4000):
numeral = roman.toRoman(integer)
roman.fromRoman(numeral.upper()) #(2) #(3)
self.assertRaises(roman.InvalidRomanNumeralError,
roman.fromRoman, numeral.lower()) #(4)
1. Najciekawsze w powyzszym tescie jest to, jak wielu rzeczy on nie testuje. Nie
testuje tego, czy wartosc zwrócona przez toRoman jest prawidłowa czy chocby
spójna; na te pytania odpowiadaja inne przypadki testowe. Ten przypadek testowy
sprawdza wyłacznie wielkosc liter. Poniewaz zarówno on jak i “sprawdzenie
zdroworozsadkowe” przebiegaja przez wszystkie wartosci z zakresu i wywołuja
toRoman, to mozecie spotkac sie z pokusa, aby obydwa te przypadki połaczyc
w jeden1. Jednak działanie takie pogwałciłoby jedna z podstawowych zasad testowania:
kazdy przypadek testowy powinien odpowiadac na dokładnie jedno
pytanie. Wyobrazmy sobie, ze połaczylismy sprawdzenie wielkosci liter ze sprawdzeniem
zdroworozsadkowym, a nowopowstały przypadek testowy zakonczył sie
niepowodzeniem. W takiej sytuacji stanelibysmy przed koniecznoscia głebszego
przeanalizowania tego przypadku, aby dowiedziec sie, w której czesci testu pojawił
sie problem, a wiec co tak naprawde owo niepowodzenie oznacza. Jesli musicie
analizowac wyniki testów po to, aby dowiedziec sie, co one oznaczaja, to jest to
oczywisty znak, ze wasze przypadki testowe zostały zle zaprojektowane.
2. Podobna lekcje otrzymujemy w tym miejscu: nawet, jesli “wiemy”, ze funkcja
toRoman zawsze zwraca wielkie litery, to aby przetestowac, ze fromRoman przyjmuje
napis złozony z wielkich liter, tutaj jawnie przekształcamy wartosc wynikowa
toRoman do wielkich liter. Dlaczego to robimy? Otóz dlatego, ze zwracanie
1“Opre sie wszystkiemu za wyjatkiem pokusy.” –Oscar Wilde
14.6. TESTOWANIE ZDROWOROZSADKOWE 301
przez toRoman wielkich liter wynika z niezaleznego wymagania. Jesli to wymaganie
zostanie zmienione tak, ze na przykład, funkcja ta bedzie zawsze zwracała
małe litery, to choc testToRomanCase bedzie musiał sie zmienic, ten test bedzie
wciaz działał. To kolejna z podstawowych zasad testowania: kazdy przypadek
testowy musi działac niezaleznie od innych przypadków. Kazdy test jest wyspa.
3. Zauwazcie, ze wartosci zwracanej przez fromRoman nigdzie nie przypisujemy. W
jezyku Python taka składnia jest poprawna; jesli funkcja zwraca pewna wartosc,
ale nikt nie jest nia zainteresowany, Python po prostu te wartosc wyrzuca. W
tym przypadku własnie tego chcemy. Ten przypadek testowy w zaden sposób nie
testuje wartosci zwracanej; testuje jedynie to, czy fromRoman akceptuje napis
złozony z wielkich liter i nie rzuca przy tym wyjatku.
4. Ta linijka, choc skomplikowana, bardzo przypomina to, co zrobilismy w testach
ToRomanBadInput i FromRomanBadInput. W tym tescie upewniamy sie,
ze wywołanie pewnej funkcji (roman.fromRoman) z pewnym szczególnym parametrem
(numeral.lower(), biezaca wartosc rzymska pochodzaca z petli, pisana
małymi literami) rzuci okreslony wyjatek (roman.InvalidRomanNumeralError).
Jesli tak sie stanie (dla kazdej wartosci z petli), test zakonczy sie powodzeniem;
jesli zas przynajmniej raz zdarzy sie cos innego (zostanie rzucony inny wyjatek
lub zostanie zwrócona wartosc bez rzucania wyjatku), test zakonczy sie niepowodzeniem.
W nastepnym rozdziale zobaczymy, jak napisac kod, który wszystkie te testy przechodzi.
302 ROZDZIAŁ 14. TESTOWANIE JEDNOSTKOWE
Rozdział 15
Testowanie 2
303
304 ROZDZIAŁ 15. TESTOWANIE 2
15.1 roman.py, etap 1
Teraz, gdy juz sa gotowe testy jednostkowe, nadszedł czas na napisanie testowanego
przez nie kodu. Zrobimy to w kilku etapach, dzieki czemu bedziecie mieli okazje
najpierw zobaczyc, ze wszystkie testy koncza sie niepowodzeniem, a nastepnie przesledzic,
w jaki sposób zaczynaja przechodzic, jeden po drugim, tak, ze w koncu zapełnione
zostana wszelkie luki w module roman1.py.
Przykład 14.1. roman1.py
Plik jest dostepny w katalogu in py/roman/stage1/ wewnatrz katalogu examples.
Jesli jeszcze tego nie zrobiliscie, mozecie pobrac ten oraz inne przykłady uzywane
w tej ksiazce stad.
"""Convert to and from Roman numerals"""
#Define exceptions
class RomanError(Exception): pass #(1)
class OutOfRangeError(RomanError): pass #(2)
class NotIntegerError(RomanError): pass
class InvalidRomanNumeralError(RomanError): pass #(3)
def toRoman(n):
"""convert integer to Roman numeral"""
pass #(4)
def fromRoman(s):
"""convert Roman numeral to integer"""
pass
1. W ten sposób w jezyku Python definiujemy nasze własne wyjatki. Wyjatki sa
klasami, a tworzy sie je przez utworzenie klasy pochodnej po jednej z juz istniejacych
klas reprezentujacych wyjatki. Zaleca sie (choc nie jest to wymagane), aby
klasy pochodne tworzyc po klasie Exception bedacej klasa bazowa dla wszystkich
wyjatków wbudowanych. W tym miejscu definiuje RomanError, która stanowic
bedzie kasa bazowa dla wszystkich nowych klas wyjatków, o których powiem pózniej.
Utworzenie bazowej klasy wyjatku jest kwestia stylu; równie łatwo mógłbym
kazda nowa klase wyjatku wyprowadzic bezposrednio z klasy Exception.
2. Wyjatki OutOfRangeError oraz NotIntegerError beda wykorzystywane przez
funkcje fromRoman do poinformowania otoczenia o róznych nieprawidłowosciach
w danych wejsciowych, tak jak zostało to zdefiniowane w ToRomanBadInput.
3. Wyjatek InvalidRomanNumeralError bedzie wykorzystany przez funkcje fromRoman
do oznaczenia nieprawidłowosci w danych wejsciowych, tak jak zostało to zdefiniowane
w FromRomanBadInput.
4. Na tym etapie dazymy do tego, aby zdefiniowac API kazdej z naszych funkcji,
jednak nie chcemy jeszcze pisac ich kodu. Sygnalizujemy to uzywajac słowa
kluczowego pass.
Nadeszła teraz wielka chwila (wchodza werble!): mozemy w koncu uruchomic testy
na naszym małym, kadłubkowym module. W tej chwili kazdy przypadek testowy
15.1. ROMAN.PY, ETAP 1 305
powinien zakonczyc sie niepowodzeniem. W istocie, jesli na etapie 1 którykolwiek test
przejdzie, powinnismy wrócic do romantests.py i zastanowic sie, dlaczego napisalismy
tak bezuzyteczny test, ze przechodzi on dla funkcji, które w rzeczywistosci nic nie
robia.
Uruchomcie romantest1.py podajac w linii polecen opcje -v, dzieki której otrzymamy
dokładniejsze informacje i bedziemy mogli przesledzic, ktory test jest uruchamiany.
Przy odrobinie szczescia wyjscie powinno wygladac tak:
Przykład 14.2. Wyjscie programu romantest1.py testujacego roman1.py
fromRoman should only accept uppercase input ... ERROR
toRoman should always return uppercase ... ERROR
fromRoman should fail with malformed antecedents ... FAIL
fromRoman should fail with repeated pairs of numerals ... FAIL
fromRoman should fail with too many repeated numerals ... FAIL
fromRoman should give known result with known input ... FAIL
toRoman should give known result with known input ... FAIL
fromRoman(toRoman(n))==n for all n ... FAIL
toRoman should fail with non-integer input ... FAIL
toRoman should fail with negative input ... FAIL
toRoman should fail with large input ... FAIL
toRoman should fail with 0 input ... FAIL
======================================================================
ERROR: fromRoman should only accept uppercase input
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:docbookdippyromanstage1romantest1.py", line 154, in testFromRomanCase
roman1.fromRoman(numeral.upper())
AttributeError: ’None’ object has no attribute ’upper’
======================================================================
ERROR: toRoman should always return uppercase
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:docbookdippyromanstage1romantest1.py", line 148, in testToRomanCase
self.assertEqual(numeral, numeral.upper())
AttributeError: ’None’ object has no attribute ’upper’
======================================================================
FAIL: fromRoman should fail with malformed antecedents
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:docbookdippyromanstage1romantest1.py", line 133, in testMalformedAntecedent
self.assertRaises(roman1.InvalidRomanNumeralError, roman1.fromRoman, s)
File "c:python21libunittest.py", line 266, in failUnlessRaises
raise self.failureException, excName
AssertionError: InvalidRomanNumeralError
======================================================================
FAIL: fromRoman should fail with repeated pairs of numerals
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:docbookdippyromanstage1romantest1.py", line 127, in testRepeatedPairs
306 ROZDZIAŁ 15. TESTOWANIE 2
self.assertRaises(roman1.InvalidRomanNumeralError, roman1.fromRoman, s)
File "c:python21libunittest.py", line 266, in failUnlessRaises
raise self.failureException, excName
AssertionError: InvalidRomanNumeralError
======================================================================
FAIL: fromRoman should fail with too many repeated numerals
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:docbookdippyromanstage1romantest1.py", line 122,
in testTooManyRepeatedNumerals
self.assertRaises(roman1.InvalidRomanNumeralError, roman1.fromRoman, s)
File "c:python21libunittest.py", line 266, in failUnlessRaises
raise self.failureException, excName
AssertionError: InvalidRomanNumeralError
======================================================================
FAIL: fromRoman should give known result with known input
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:docbookdippyromanstage1romantest1.py", line 99,
in testFromRomanKnownValues
self.assertEqual(integer, result)
File "c:python21libunittest.py", line 273, in failUnlessEqual
raise self.failureException, (msg or ’%s != %s’ % (first, second))
AssertionError: 1 != None
======================================================================
FAIL: toRoman should give known result with known input
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:docbookdippyromanstage1romantest1.py", line 93,
in testToRomanKnownValues
self.assertEqual(numeral, result)
File "c:python21libunittest.py", line 273, in failUnlessEqual
raise self.failureException, (msg or ’%s != %s’ % (first, second))
AssertionError: I != None
======================================================================
FAIL: fromRoman(toRoman(n))==n for all n
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:docbookdippyromanstage1romantest1.py", line 141, in testSanity
self.assertEqual(integer, result)
File "c:python21libunittest.py", line 273, in failUnlessEqual
raise self.failureException, (msg or ’%s != %s’ % (first, second))
AssertionError: 1 != None
======================================================================
FAIL: toRoman should fail with non-integer input
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:docbookdippyromanstage1romantest1.py", line 116, in testNonInteger
self.assertRaises(roman1.NotIntegerError, roman1.toRoman, 0.5)
File "c:python21libunittest.py", line 266, in failUnlessRaises
15.1. ROMAN.PY, ETAP 1 307
raise self.failureException, excName
AssertionError: NotIntegerError
======================================================================
FAIL: toRoman should fail with negative input
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:docbookdippyromanstage1romantest1.py", line 112, in testNegative
self.assertRaises(roman1.OutOfRangeError, roman1.toRoman, -1)
File "c:python21libunittest.py", line 266, in failUnlessRaises
raise self.failureException, excName
AssertionError: OutOfRangeError
======================================================================
FAIL: toRoman should fail with large input
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:docbookdippyromanstage1romantest1.py", line 104, in testTooLarge
self.assertRaises(roman1.OutOfRangeError, roman1.toRoman, 4000)
File "c:python21libunittest.py", line 266, in failUnlessRaises
raise self.failureException, excName
AssertionError: OutOfRangeError
======================================================================
FAIL: toRoman should fail with 0 input #(1)
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:docbookdippyromanstage1romantest1.py", line 108, in testZero
self.assertRaises(roman1.OutOfRangeError, roman1.toRoman, 0)
File "c:python21libunittest.py", line 266, in failUnlessRaises
raise self.failureException, excName
AssertionError: OutOfRangeError #(2)
----------------------------------------------------------------------
Ran 12 tests in 0.040s #(3)
FAILED (failures=10, errors=2) #(4)
1. Po uruchomieniu skryptu zostaje wywołana funkcja unittest.main(), która z
kolei wywołuje kazda z metod zdefiniowanych w kazdej klasie wewnatrz romantest.py.
Dla kazdego przypadku testowego wypisywany jest napis dokumentujacy odpowiadajacej
mu metody oraz to, czy przypadek testowy przeszedł, czy nie. Tak,
jak sie spodziewalismy, zaden test nie przeszedł.
2. Dla kazdego przypadku testowego, który zakonczył sie niepowodzeniem,
unittest wypisuje zawartosc stosu, dzieki czemu widac dokładnie, co sie stało.
W tym przypadku wywołanie funkcji assertRaises (znanej równiez pod nazwa
failUnlessRaises) spowodowało rzucenie wyjatku AssertionError z tego powodu,
ze w tescie spodziewalismy sie, ze toRoman rzuci OutOfRangeError, a taki
wyjatek nie został rzucony.
3. Po wypisaniu szczegółów, unittest wypisuje podsumowanie zawierajace informacje
o tym, ile testów zostało uruchomionych oraz jak długo one trwały.
4. Ogólnie rzecz biorac, test jednostkowy nie przechodzi, jesli przynajmniej jeden
przypadek testowy nie przechodzi. Kiedy przypadek testowy nie przej308
ROZDZIAŁ 15. TESTOWANIE 2
dzie, unittest rozróznia niepowodzenia (failures) i błedy (errors). Niepowodzenie
wystepuje w przypadku wywołan metod assertXYZ, np. assertEqual czy
assertRaises, które koncza sie niepowodzeniem, poniewaz nie został spełniony
pewien zakładany warunek albo nie został rzucony spodziewany wyjatek. Bład
natomiast wystepuje wówczas, gdy zostanie rzucony jakikolwiek inny wyjatek
i to zarówno w kodzie testowanym, jak i w kodzie samego testu. Na przykład
bład wystapił w metodzie testFromRomanCase (“Funkcja fromRoman powinna
akceptowac na wejsciu napisy zawierajace wyłacznie wielkie litery”), poniewaz
wywołanie numeral.upper() rzuciło wyjatek AttributeError: toRoman miało
zwrócic napis, a tego nie zrobiło. Natomiast testZero (“Funkcja toRoman otrzymujaca
na wejsciu wartosc 0 powinna zakonczyc sie niepowodzeniem”) zakonczyła
sie niepowodzeniem, poniewaz wywołanie fromRoman nie rzuciło wyjatku
InvalidRomanNumeral, którego spodziewał sie assertRaises.
15.2. ROMAN.PY, ETAP 2 309
15.2 roman.py, etap 2
Strukture modułu roman mamy juz z grubsza okreslona, nadszedł wiec czas na
napisanie kodu i sprawienie, ze nasze testy zaczna w koncu przechodzic.
Przykład 14.3. roman2.py
Plik jest dostepny w katalogu in py/roman/stage2/ wewnatrz katalogu examples.
Jesli jeszcze tego nie zrobiliscie, mozecie pobrac ten oraz inne przykłady uzywane
w tej ksiazce stad.
"""Convert to and from Roman numerals"""
#Define exceptions
class RomanError(Exception): pass
class OutOfRangeError(RomanError): pass
class NotIntegerError(RomanError): pass
class InvalidRomanNumeralError(RomanError): pass
#Define digit mapping
romanNumeralMap = ((’M’, 1000), #(1)
(’CM’, 900),
(’D’, 500),
(’CD’, 400),
(’C’, 100),
(’XC’, 90),
(’L’, 50),
(’XL’, 40),
(’X’, 10),
(’IX’, 9),
(’V’, 5),
(’IV’, 4),
(’I’, 1))
def toRoman(n):
"""convert integer to Roman numeral"""
result = ""
for numeral, integer in romanNumeralMap:
while n >= integer: #(2)
result += numeral
n -= integer
return result
def fromRoman(s):
"""convert Roman numeral to integer"""
pass
1. romanNumeralMap jest krotka krotek, która definiuje trzy elementy:
(a) reprezentacje znakowa najbardziej podstawowych liczb rzymskich; zauwazcie,
ze nie sa to wyłacznie liczby, których reprezentacja składa sie z jednego
znaku; zdefiniowane sa równiez pary dwuznakowe, takie jak CM (“o
310 ROZDZIAŁ 15. TESTOWANIE 2
sto mniej niz tysiac”), dzieki którym kod funkcji toRoman bedzie znacznie
prostszy
(b) porzadek liczb rzymskich; sa one uporzadkowane malejaco wzgledem ich
liczbowej wartosci od M do I
(c) wartosc liczbowa odpowiadajaca reprezentacji rzymskiej; kazda wewnetrzna
krotka jest para (reprezentacja rzymska, wartosc liczbowa)
2. To jest własnie miejsce, w którym widac, ze opłacało sie wprowadzic opisana
wyzej bogata strukture danych — nie potrzebujemy zadnej specjalnej logiki do
obsłuzenia reguły odejmowania. Aby przekształcic wartosc liczbowa do reprezentacji
rzymskiej wystarczy przeiterowac po romanNumeralMap szukajac najwyzszej
wartosci całkowitej mniejszej badz równej wartosci wejsciowej. Po jej znalezieniu
dopisujemy odpowiadajaca jej reprezentacje rzymska na koniec napisu wyjsciowego,
odejmujemy jej wartosc od wartosci wejsciowej, pierzemy, płuczemy,
powtarzamy.
Przykład 14.4. Jak działa toRoman
Jesli sposób działania funkcji toRoman nie jest całkiem jasny, dodajcie na koniec
petli while instrukcje print:
while n >= integer:
result += numeral
n -= integer
print ’subtracting’, integer, ’from input, adding’, numeral, ’to output’
>>> import roman2
>>> roman2.toRoman(1424)
subtracting 1000 from input, adding M to output
subtracting 400 from input, adding CD to output
subtracting 10 from input, adding X to output
subtracting 10 from input, adding X to output
subtracting 4 from input, adding IV to output
’MCDXXIV’
Funkcja toRoman wydaje sie działac, przynajmniej w przypadku tego szybkiego, recznego
sprawdzenia. Czy jednak przechodzi ona testy? Cóz, niezupełnie.
Przykład 14.5. Wyjscie programu romantest2.py testujacego roman2.py
Pamietajcie o tym, aby uruchomic romantest2.py z opcja -v w linii polecen, dzieki
czemu właczy sie tryb “rozwlekły”.
fromRoman should only accept uppercase input ... FAIL
toRoman should always return uppercase ... ok #(1)
fromRoman should fail with malformed antecedents ... FAIL
fromRoman should fail with repeated pairs of numerals ... FAIL
fromRoman should fail with too many repeated numerals ... FAIL
fromRoman should give known result with known input ... FAIL
toRoman should give known result with known input ... ok #(2)
fromRoman(toRoman(n))==n for all n ... FAIL
toRoman should fail with non-integer input ... FAIL #(3)
toRoman should fail with negative input ... FAIL
15.2. ROMAN.PY, ETAP 2 311
toRoman should fail with large input ... FAIL
toRoman should fail with 0 input ... FAIL
1. Poniewaz w romanNumeralMap reprezentacja liczb rzymskich jest wyrazona przy
pomocy wielkich liter, funkcja toRoman rzeczywiscie zawsze zwraca napisy złozone
z wielkich liter. A wiec ten test przechodzi.
2. Tu pojawia sie istotna wiadomosc: obecna wersja toRoman przechodzi test znanych
wartosci. Choc test ten nie jest zbyt wyczerpujacy, sprawdza on wiele sposród
poprawnych danych wejsciowych, wliczajac w to wartosci, które powinny
dac w wyniku kazda reprezentacje jednoliterowa, najwieksza mozliwa wartosc
(3999) czy tez wartosc, która daje w wyniku najdłuzsza reprezentacje rzymska
(3888). Na tej podstawie mozemy byc raczej pewni, ze funkcja zwróci poprawna
reprezentacje dla wszystkich poprawnych danych wejsciowych.
3. Niestety, funkcja “nie działa” dla nieprawidłowych danych wejsciowych; nie przechodzi
zaden test badajacy działanie funkcji dla niepoprawnych danych. Ma to
sens, poniewaz nie umiescilismy jeszcze w kodzie funkcji zadnego sprawdzenia dotyczacego
błednych danych. Testy, o których tu mówimy, sprawdzaja (uzywajac
assertRaises), czy w takich sytuacjach zostaje rzucony odpowiedni wyjatek, a
my nigdzie go nie rzucamy. Zrobimy to jednak juz w nastepnym etapie.
Ponizej znajduje sie dalszy ciag wyjscia po uruchomieniu testów jednostkowych,
prezentujacy szczegóły niepowodzen. Jest ich az 10.
======================================================================
FAIL: fromRoman should only accept uppercase input
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:docbookdippyromanstage2romantest2.py", line 156, in testFromRomanCase
roman2.fromRoman, numeral.lower())
File "c:python21libunittest.py", line 266, in failUnlessRaises
raise self.failureException, excName
AssertionError: InvalidRomanNumeralError
======================================================================
FAIL: fromRoman should fail with malformed antecedents
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:docbookdippyromanstage2romantest2.py", line 133, in testMalformedAntecedent
self.assertRaises(roman2.InvalidRomanNumeralError, roman2.fromRoman, s)
File "c:python21libunittest.py", line 266, in failUnlessRaises
raise self.failureException, excName
AssertionError: InvalidRomanNumeralError
======================================================================
FAIL: fromRoman should fail with repeated pairs of numerals
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:docbookdippyromanstage2romantest2.py", line 127, in testRepeatedPairs
self.assertRaises(roman2.InvalidRomanNumeralError, roman2.fromRoman, s)
File "c:python21libunittest.py", line 266, in failUnlessRaises
312 ROZDZIAŁ 15. TESTOWANIE 2
raise self.failureException, excName
AssertionError: InvalidRomanNumeralError
======================================================================
FAIL: fromRoman should fail with too many repeated numerals
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:docbookdippyromanstage2romantest2.py", line 122,
in testTooManyRepeatedNumerals
self.assertRaises(roman2.InvalidRomanNumeralError, roman2.fromRoman, s)
File "c:python21libunittest.py", line 266, in failUnlessRaises
raise self.failureException, excName
AssertionError: InvalidRomanNumeralError
======================================================================
FAIL: fromRoman should give known result with known input
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:docbookdippyromanstage2romantest2.py", line 99,
in testFromRomanKnownValues
self.assertEqual(integer, result)
File "c:python21libunittest.py", line 273, in failUnlessEqual
raise self.failureException, (msg or ’%s != %s’ % (first, second))
AssertionError: 1 != None
======================================================================
FAIL: fromRoman(toRoman(n))==n for all n
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:docbookdippyromanstage2romantest2.py", line 141, in testSanity
self.assertEqual(integer, result)
File "c:python21libunittest.py", line 273, in failUnlessEqual
raise self.failureException, (msg or ’%s != %s’ % (first, second))
AssertionError: 1 != None
======================================================================
FAIL: toRoman should fail with non-integer input
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:docbookdippyromanstage2romantest2.py", line 116, in testNonInteger
self.assertRaises(roman2.NotIntegerError, roman2.toRoman, 0.5)
File "c:python21libunittest.py", line 266, in failUnlessRaises
raise self.failureException, excName
AssertionError: NotIntegerError
======================================================================
FAIL: toRoman should fail with negative input
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:docbookdippyromanstage2romantest2.py", line 112, in testNegative
self.assertRaises(roman2.OutOfRangeError, roman2.toRoman, -1)
File "c:python21libunittest.py", line 266, in failUnlessRaises
raise self.failureException, excName
AssertionError: OutOfRangeError
======================================================================
15.2. ROMAN.PY, ETAP 2 313
FAIL: toRoman should fail with large input
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:docbookdippyromanstage2romantest2.py", line 104, in testTooLarge
self.assertRaises(roman2.OutOfRangeError, roman2.toRoman, 4000)
File "c:python21libunittest.py", line 266, in failUnlessRaises
raise self.failureException, excName
AssertionError: OutOfRangeError
======================================================================
FAIL: toRoman should fail with 0 input
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:docbookdippyromanstage2romantest2.py", line 108, in testZero
self.assertRaises(roman2.OutOfRangeError, roman2.toRoman, 0)
File "c:python21libunittest.py", line 266, in failUnlessRaises
raise self.failureException, excName
AssertionError: OutOfRangeError
----------------------------------------------------------------------
Ran 12 tests in 0.320s
FAILED (failures=10)
314 ROZDZIAŁ 15. TESTOWANIE 2
15.3 roman.py, etap 3
roman.py, etap 3
Teraz juz toRoman odpowiednio sobie radzi z dobrym wejsciem (liczbami całkowitymi
od 1 do 3999), wiec teraz jest czas zajac sie niepoprawnym wejsciem (wszystkim
innym).
Przykład 14.6. roman3.py
Plik ten jest dostepny z py/roman/stage3/ w katalogu przykładów.
Jesli jeszcze tego nie zrobiłes, mozesz pobrac ten i inne przykłady wykorzystane w
tej ksiazce.
"""Convert to and from Roman numerals"""
#Define exceptions
class RomanError(Exception): pass
class OutOfRangeError(RomanError): pass
class NotIntegerError(RomanError): pass
class InvalidRomanNumeralError(RomanError): pass
#Define digit mapping
romanNumeralMap = ((’M’, 1000),
(’CM’, 900),
(’D’, 500),
(’CD’, 400),
(’C’, 100),
(’XC’, 90),
(’L’, 50),
(’XL’, 40),
(’X’, 10),
(’IX’, 9),
(’V’, 5),
(’IV’, 4),
(’I’, 1))
def toRoman(n):
"""convert integer to Roman numeral"""
if not (0 < n < 4000): #(1)
raise OutOfRangeError, "number out of range (must be 1..3999)" #(2)
if int(n) <> n: #(3)
raise NotIntegerError, "non-integers can not be converted"
result = "" #(4)
for numeral, integer in romanNumeralMap:
while n >= integer:
result += numeral
n -= integer
return result
def fromRoman(s):
15.3. ROMAN.PY, ETAP 3 315
"""convert Roman numeral to integer"""
pass
1. Jest to przyjemny pythonowy skrót: wielokrotne porównanie. Jest to odpowiedniek
do if not ((0 < n) and (n < 4000)), jednak łatwiejszy do odczytu.
Za pomoca tego kontrolujemy zakres wartosci i sprawdzamy, czy wprowadzona
liczba nie jest za duza, ujemna, czy tez równa zero.
2. Wyrzucamy wyjatek za pomoca wyrazenia raise. Mozemy wyrzucic kazdy wbudowane
wyjatek, a takze inny zdefiniowany przez nas wyjatek. Drugi parametr,
wiadomosc błedu, jest opcjonalny; jesli dostaniemy wyjatek i nigdzie jego nie
obsłuzymy, zostanie on wyswietlone w traceback (w postaci sladów stosu).
3. Za pomoca tego sprawdzamy, czy liczba nie jest całkowita. Liczby nie bedace
liczbami całkowitymi nie moga zostac przekonwertowane na system rzymski.
4. Pozostała czesc funkcji jest niezmieniona.
Przykład 14.7. Obserwujemy, jak toRoman radzi sobie z błednym wejsciem
>>> import roman3
>>> roman3.toRoman(4000)
Traceback (most recent call last):
File "<interactive input>", line 1, in ?
File "roman3.py", line 27, in toRoman
raise OutOfRangeError, "number out of range (must be 1..3999)"
OutOfRangeError: number out of range (must be 1..3999)
>>> roman3.toRoman(1.5)
Traceback (most recent call last):
File "<interactive input>", line 1, in ?
File "roman3.py", line 29, in toRoman
raise NotIntegerError, "non-integers can not be converted"
NotIntegerError: non-integers can not be converted
Przykład 14.8. Wyjscie romantest3.py w zaleznosci od roman3.py
fromRoman should only accept uppercase input ... FAIL
toRoman should always return uppercase ... ok
fromRoman should fail with malformed antecedents ... FAIL
fromRoman should fail with repeated pairs of numerals ... FAIL
fromRoman should fail with too many repeated numerals ... FAIL
fromRoman should give known result with known input ... FAIL
toRoman should give known result with known input ... ok #(1)
fromRoman(toRoman(n))==n for all n ... FAIL
toRoman should fail with non-integer input ... ok #(2)
toRoman should fail with negative input ... ok #(3)
toRoman should fail with large input ... ok
toRoman should fail with 0 input ... ok
316 ROZDZIAŁ 15. TESTOWANIE 2
1. toRoman dalej przechodzi testy o znanych wartosciach, co jest pocieszajace. Ponadto
przechodzi wszystkie testy, które przechodził w etapie 2, zatem ostatni
kod niczego nie popsuł.
2. Bardziej ekscytujacy jest fakty, ze teraz nasz program przechodzi wszystkie testy
z niepoprawnym wejsciem. Przechodzi ten test (czyli testNonInteger), poniewaz
kontrolujemy, czy int(n) <> n. Kiedy do funkcji toRoman zostanie przekazana
wartosc nie bedaca liczba całkowita, porównanie int(n) <> n wyłapie
to i wyrzuci wyjatek NotIntegerError, a tego oczekuje test testNonInteger.
3. Program przechodzi ten test (test testNegative), poniewaz w przypadku
prawdziwosci wyrazenia not (0 < n < 4000) zostanie wyrzucony wyjatek
OutOfRangeError, a którego oczekuje test testNegative.
======================================================================
FAIL: fromRoman should only accept uppercase input
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:docbookdippyromanstage3romantest3.py", line 156, in testFromRomanCase
roman3.fromRoman, numeral.lower())
File "c:python21libunittest.py", line 266, in failUnlessRaises
raise self.failureException, excName
AssertionError: InvalidRomanNumeralError
======================================================================
FAIL: fromRoman should fail with malformed antecedents
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:docbookdippyromanstage3romantest3.py", line 133,
in testMalformedAntecedent
self.assertRaises(roman3.InvalidRomanNumeralError, roman3.fromRoman, s)
File "c:python21libunittest.py", line 266, in failUnlessRaises
raise self.failureException, excName
AssertionError: InvalidRomanNumeralError
======================================================================
FAIL: fromRoman should fail with repeated pairs of numerals
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:docbookdippyromanstage3romantest3.py", line 127, in testRepeatedPairs
self.assertRaises(roman3.InvalidRomanNumeralError, roman3.fromRoman, s)
File "c:python21libunittest.py", line 266, in failUnlessRaises
raise self.failureException, excName
AssertionError: InvalidRomanNumeralError
======================================================================
FAIL: fromRoman should fail with too many repeated numerals
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:docbookdippyromanstage3romantest3.py", line 122,
in testTooManyRepeatedNumerals
self.assertRaises(roman3.InvalidRomanNumeralError, roman3.fromRoman, s)
File "c:python21libunittest.py", line 266, in failUnlessRaises
raise self.failureException, excName
15.3. ROMAN.PY, ETAP 3 317
AssertionError: InvalidRomanNumeralError
======================================================================
FAIL: fromRoman should give known result with known input
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:docbookdippyromanstage3romantest3.py", line 99, in testFromRomanKnownValues
self.assertEqual(integer, result)
File "c:python21libunittest.py", line 273, in failUnlessEqual
raise self.failureException, (msg or ’%s != %s’ % (first, second))
AssertionError: 1 != None
======================================================================
FAIL: fromRoman(toRoman(n))==n for all n
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:docbookdippyromanstage3romantest3.py", line 141, in testSanity
self.assertEqual(integer, result)
File "c:python21libunittest.py", line 273, in failUnlessEqual
raise self.failureException, (msg or ’%s != %s’ % (first, second))
AssertionError: 1 != None
----------------------------------------------------------------------
Ran 12 tests in 0.401s
FAILED (failures=6) #(1)
1. Teraz liczba niezaliczonych testów zmniejszyła sie do 6 i wszystkie je powoduje
fromRoman, czyli: test znanych wartosci, trzy testy dotyczace niepoprawnych
argumentów, kontrola wielkosci znaków i kontrola zdroworozsadkowa (czyli
fromRoman(toRoman(n))==n). Oznacza to, ze toRoman przeszedł wszystkie testy,
które mógł przejsc samemu. (Nawala w tescie zdroworozsadkowym, ale test ten
wymaga takze napisania funkcji fromRoman, a to jeszcze nie zostało zrobione.)
Oznacza to, ze musimy przestac juz kodowac toRoman. Juz nie ulepszamy, nie
kombinujemy, bez ekstra “a moze ten”. Stop. Teraz odejdziemy od klawiatury.
Jedna z najistotniejszych spraw jest to, ze rozumowy unit testing mówi tobie, kiedy
przestac kodowac. Kiedy funkcja przechodzi wszystkie unit testy przeznaczone dla niej,
konczymy kodowac te funkcje. Kiedy wszystkie unit test dla całego modułu zostana
zaliczone, przestajemy kodowac moduł.
318 ROZDZIAŁ 15. TESTOWANIE 2
15.4 roman.py, etap 4
Implementacja funkcji toRoman została zakonczona, czas zajac sie funkcja fromRoman.
Dzieki bogatej strukturze danych przechowujacej pewne wartosci w reprezentacji rzymskiej
wraz z ich wartosciami liczbowymi, zadanie to nie bedzie wcale trudniejsze, niz
napisanie funkcji toRoman.
Przykład 14.9. roman4.py
Plik jest dostepny w katalogu in py/roman/stage4/ wewnatrz katalogu examples.
Jesli jeszcze tego nie zrobiliscie, mozecie pobrac ten oraz inne przykłady uzywane
w tej ksiazce stad.
"""Convert to and from Roman numerals"""
#Define exceptions
class RomanError(Exception): pass
class OutOfRangeError(RomanError): pass
class NotIntegerError(RomanError): pass
class InvalidRomanNumeralError(RomanError): pass
#Define digit mapping
romanNumeralMap = ((’M’, 1000),
(’CM’, 900),
(’D’, 500),
(’CD’, 400),
(’C’, 100),
(’XC’, 90),
(’L’, 50),
(’XL’, 40),
(’X’, 10),
(’IX’, 9),
(’V’, 5),
(’IV’, 4),
(’I’, 1))
# toRoman function omitted for clarity (it hasn’t changed)
def fromRoman(s):
"""convert Roman numeral to integer"""
result = 0
index = 0
for numeral, integer in romanNumeralMap:
while s[index:index+len(numeral)] == numeral: #(1)
result += integer
index += len(numeral)
return result
1. Sposób działania jest taki sam jak w toRoman. Iterujemy po reprezentacjach
rzymskich w strukturze danych (bedacej krotka krotek), jednak zamiast dopasowywania
najwiekszej wartosci całkowitej tak czesto, jak to mozliwe, dopasowujemy
“najwyzsza” reprezentacje rzymska tak czesto, jak to mozliwe.
15.4. ROMAN.PY, ETAP 4 319
Przykład 14.10. Jak działa fromRoman
Jesli wciaz nie jestescie pewni, jak działa fromRoman, na koncu petli while dodajcie
instrukcje print:
while s[index:index+len(numeral)] == numeral:
result += integer
index += len(numeral)
print ’found’, numeral, ’of length’, len(numeral), ’, adding’, integer
>>> import roman4
>>> roman4.fromRoman(’MCMLXXII’)
found M , of length 1, adding 1000
found CM , of length 2, adding 900
found L , of length 1, adding 50
found X , of length 1, adding 10
found X , of length 1, adding 10
found I , of length 1, adding 1
found I , of length 1, adding 1
1972
Przykład 14.11. Wyjscie programu romantest4.py testujacego roman4.py
fromRoman should only accept uppercase input ... FAIL
toRoman should always return uppercase ... ok
fromRoman should fail with malformed antecedents ... FAIL
fromRoman should fail with repeated pairs of numerals ... FAIL
fromRoman should fail with too many repeated numerals ... FAIL
fromRoman should give known result with known input ... ok #(1)
toRoman should give known result with known input ... ok
fromRoman(toRoman(n))==n for all n ... ok #(2)
toRoman should fail with non-integer input ... ok
toRoman should fail with negative input ... ok
toRoman should fail with large input ... ok
toRoman should fail with 0 input ... ok
1. Mamy tu dwie interesujace wiadomosci: po pierwsze, fromRoman działa dla poprawnych
danych wejsciowych, przynajmniej dla tych, które sa zdefiniowane w
tescie poprawnych wartosci.
2. Po drugie, test zdroworozsadkowy równiez przeszedł. Wiedzac o tym, ze przeszedł
równiez test znanych wartosci, mozemy byc raczej pewni, ze zarówno fromRoman
jak i toRoman działaja poprawnie dla poprawnych danych wejsciowych. (Nic
nam tego jednak nie gwarantuje; teoretycznie jest mozliwe, ze w funkcji toRoman
ukryty jest jakis bład, przez który dla pewnego zestawu danych wejsciowych generowane
sa niepoprawne reprezentacje rzymskie, natomaist fromRoman zawierac
moze symetryczny bład, który z kolei powoduje, ze dla tych własnie rzymskich
reprezentacji generowane sa niepoprawne wartosci liczbowe. W zaleznosci od
zastosowan waszego kodu, a takze wymagan, jakim ten kod podlega, moze to
stanowic dla was pewien problem; jesli tak jest, dopiszcie wiecej bardziej wszechstronnych
testów tak, aby zmniejszyła sie wasza niepewnosc.)
320 ROZDZIAŁ 15. TESTOWANIE 2
======================================================================
FAIL: fromRoman should only accept uppercase input
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:docbookdippyromanstage4romantest4.py", line 156, in testFromRomanCase
roman4.fromRoman, numeral.lower())
File "c:python21libunittest.py", line 266, in failUnlessRaises
raise self.failureException, excName
AssertionError: InvalidRomanNumeralError
======================================================================
FAIL: fromRoman should fail with malformed antecedents
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:docbookdippyromanstage4romantest4.py", line 133,
in testMalformedAntecedent
self.assertRaises(roman4.InvalidRomanNumeralError, roman4.fromRoman, s)
File "c:python21libunittest.py", line 266, in failUnlessRaises
raise self.failureException, excName
AssertionError: InvalidRomanNumeralError
======================================================================
FAIL: fromRoman should fail with repeated pairs of numerals
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:docbookdippyromanstage4romantest4.py", line 127, in testRepeatedPairs
self.assertRaises(roman4.InvalidRomanNumeralError, roman4.fromRoman, s)
File "c:python21libunittest.py", line 266, in failUnlessRaises
raise self.failureException, excName
AssertionError: InvalidRomanNumeralError
======================================================================
FAIL: fromRoman should fail with too many repeated numerals
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:docbookdippyromanstage4romantest4.py", line 122,
in testTooManyRepeatedNumerals
self.assertRaises(roman4.InvalidRomanNumeralError, roman4.fromRoman, s)
File "c:python21libunittest.py", line 266, in failUnlessRaises
raise self.failureException, excName
AssertionError: InvalidRomanNumeralError
----------------------------------------------------------------------
Ran 12 tests in 1.222s
FAILED (failures=4)
15.5. ROMAN.PY, ETAP 5 321
15.5 roman.py, etap 5
Funkcja fromRoman działa poprawnie dla poprawnych danych wejsciowych, nadszedł
wiec czas na dołozenie ostatniego klocka w naszej układance: napisanie kodu,
dzieki któremu funkcja ta bedzie działała poprawnie równiez dla niepoprawnych danych
wejsciowych. Oznacza to, ze musimy znalezc sposób na ustalenie, czy dany napis
stanowi poprawna rzymska reprezentacje pewnej wartosci. To zadanie jest znacznie
trudniejsze, niz sprawdzenie poprawnosci wartosci liczbowej w funkcji toRoman, jednak
mozemy do tego celu uzyc silnego narzedzia: wyrazen regularnych.
Jesli nie znacie wyrazen regularnych i nie przeczytaliscie jeszcze podrozdziału 7
Wyrazenia regularne, nadszedł własnie doskonały moment, aby to zrobic.
Jak widzielismy w podrozdziale 7.3 Analiza przypadku: Liczby rzymskie, istnieje
kilka prostych reguł, dzieki którym mozna skonstruowac napis reprezentujacy wartosc
liczbowa w zapisie rzymskim, uzywajac liter M, D, C, L, X, V oraz I. Przesledzmy je
po kolei:
1. Znaki mozna dodawac. I to 1, II to 2, III to 3. VI to 6 (dosłownie: “5 i 1”), VII
to 7, a VIII to 8.
2. Liczby składajace sie z jedynki i (byc moze) zer (I, X, C oraz M) — liczby
“dziesiatkowe” — moga byc powtarzane do trzech razy. Przy czwartym nalezy
odjac te wartosc od znaku reprezentujacego liczbe składajaca sie z piatki i (byc
moze) zer — liczbe “piatkowa”. Nie mozna przedstawic liczby 4 jako IIII, nalezy
przedstawic ja jako IV (“1 odjete od 5”). Liczbe 40 zapisujemy jako XL (“10
odjete od 50”), 41 jako XLI, 42 jako XLII, 43 jako XLIII, a 44 jako XLIV (“10
odjete od 50 oraz 1 odjete od 5”).
3. Podobnie tworzymy liczby “dziewiatkowe”: nalezy odejmowac od najblizszej liczby
dziesiatkowej: 8 to VIII, jednak 9 to IX (“1 odjete od 10”), a nie VIIII (poniewaz
I nie moze byc powtórzone wiecej niz trzy razy), zas 90 to XC, a 900 to CM.
4. Liczby “piatkowe” nie moga byc powtarzane. 10 zawsze reprezentowane jest jako
X, a nie VV, 100 jako C, nigdy zas jako LL.
5. Liczby w reprezentacji rzymskiej sa zawsze zapisywane od najwiekszych do najmniejszych
i odczytywane od lewej do prawej, a wiec porzadek znaków ma
ogromne znaczenie. DC to 600; CD to zupełnie inna liczba (400, “100 odjete
od 500”). CI to 101, IC zas nie jest poprawna wartoscia w zapisie rzymskim,
poniewaz nie mozna bezposrednio odjac 1 od 100: nalezałoby zapisac XCIX (“10
odjete od 100 oraz 1 odjete od 10”).
Przykład 14.12. roman5.py
Plik jest dostepny w katalogu in py/roman/stage5/ wewnatrz katalogu examples.
Jesli jeszcze tego nie zrobiliscie, mozecie pobrac ten oraz inne przykłady uzywane
w tej ksiazce stad.
"""Convert to and from Roman numerals"""
import re
#Define exceptions
class RomanError(Exception): pass
class OutOfRangeError(RomanError): pass
322 ROZDZIAŁ 15. TESTOWANIE 2
class NotIntegerError(RomanError): pass
class InvalidRomanNumeralError(RomanError): pass
#Define digit mapping
romanNumeralMap = ((’M’, 1000),
(’CM’, 900),
(’D’, 500),
(’CD’, 400),
(’C’, 100),
(’XC’, 90),
(’L’, 50),
(’XL’, 40),
(’X’, 10),
(’IX’, 9),
(’V’, 5),
(’IV’, 4),
(’I’, 1))
def toRoman(n):
"""convert integer to Roman numeral"""
if not (0 < n < 4000):
raise OutOfRangeError, "number out of range (must be 1..3999)"
if int(n) <> n:
raise NotIntegerError, "non-integers can not be converted"
result = ""
for numeral, integer in romanNumeralMap:
while n >= integer:
result += numeral
n -= integer
return result
#Define pattern to detect valid Roman numerals
romanNumeralPattern = ’^M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)(IX|IV|V?I?I?I?)$’ #(1)
def fromRoman(s):
"""convert Roman numeral to integer"""
if not re.search(romanNumeralPattern, s): #(2)
raise InvalidRomanNumeralError, ’Invalid Roman numeral: %s’ % s
result = 0
index = 0
for numeral, integer in romanNumeralMap:
while s[index:index+len(numeral)] == numeral:
result += integer
index += len(numeral)
return result
1. To kontynuacja wyrazenia, o którym dyskutowalismy w podrozdziale 7.3 Ana15.5.
ROMAN.PY, ETAP 5 323
liza przypadku: Liczby rzymskie. Miejsce “dziesiatki” jest w napisie XC (90), XL
(40) oraz w napisie złozonym z opcjonalnego L oraz nastepujacym po niej opcjonalnym
znaku X powtórzonym od 0 do 3 razy. Miejsce “jedynki” jest w napisie
IX (9), IV (4) oraz przy opcjonalnym V z nastepujacym po niej, opcjonalnym
znakiem I powtórzonym od 0 do 3 razy.
2. Po wpisaniu tej logiki w wyrazenie regularne otrzymamy trywialny kod sprawdzajacy
poprawnosc napisów potencjalnie reprezentujacych liczby rzymskie. Jesli
re.search zwróci obiekt, wyrazenie regularne zostało dopasowane, a wiec dane
wejsciowe sa poprawne; w przeciwnym wypadku, dane wejsciowe sa niepoprawne.
W tym momencie macie prawo byc nieufni wobec tego wielkiego, brzydkiego wyrazenia
regularnego, które ma sie rzekomo dopasowac do wszystkich poprawnych napisów
reprezentujacych liczby rzymskie. Oczywiscie, nie musicie mi wierzyc, spójrzcie zatem
na wyniki testów:
Example 14.13. Output of romantest5.py against roman5.py
fromRoman should only accept uppercase input ... ok #(1)
toRoman should always return uppercase ... ok
fromRoman should fail with malformed antecedents ... ok #(2)
fromRoman should fail with repeated pairs of numerals ... ok #(3)
fromRoman should fail with too many repeated numerals ... ok
fromRoman should give known result with known input ... ok
toRoman should give known result with known input ... ok
fromRoman(toRoman(n))==n for all n ... ok
toRoman should fail with non-integer input ... ok
toRoman should fail with negative input ... ok
toRoman should fail with large input ... ok
toRoman should fail with 0 input ... ok
----------------------------------------------------------------------
Ran 12 tests in 2.864s
OK #(4)
1. Jedna rzecz o jakiej nie wspomniałem w kontekscie wyrazen regularnych to fakt,
ze sa one zalezne od wielkosci znaków. Ze wzgledu na to, ze wyrazenie regularne
romanNumeralPattern zostało zapisane przy uzyciu wielkich liter, sprawdzenie
re.search odrzuci wszystkie napisy, które zawieraja przynajmniej jedna mała
litere. Dlatego tez test wielkich liter przechodzi.
2. Co wiecej, przechodza równiez testy nieprawidłowych danych wejsciowych. Przykładowo
test niepoprawnych poprzedników sprawdza przypadki takie, jak MCMC.
Jak widzimy, wyrazenie regularne nie pasuje do tego napisu, a wiec fromRoman
rzuca wyjatek InvalidRomanNumeralError, i jest to dokładnie taki wyjatek, jakiego
spodziewa sie test niepoprawnych poprzedników, a wiec test ten przechodzi.
3. Rzeczywiscie przechodza wszystkie testy sprawdzajace niepoprawne dane wejsciowe.
Wyrazenie regularne wyłapuje wszystkie przypadki, o jakich myslelismy
podczas przygotowywania naszych przypadków testowych.
324 ROZDZIAŁ 15. TESTOWANIE 2
4. Nagrode najwiekszego rozczarowania roku otrzymuje słówko “OK”, które zostało
wypisane przez moduł unittest w chwili gdy okazało sie, ze wszystkie testy
zakonczyły sie powodzeniem.
Kiedy wszystkie testy przechodza, przestan kodowac.
Rozdział 16
Refaktoryzacja
325
326 ROZDZIAŁ 16. REFAKTORYZACJA
16.1 Obsługa błedów
Mimo wielkiego wysiłku wkładanego w pisanie testów jednostkowych błedy wciaz
sie zdarzaja. Co mam na mysli piszac “bład”? Bład to przypadek testowy, który nie
został jeszcze napisany.
Przykład 15.1. Bład
>>> import roman5
>>> roman5.fromRoman("") #(1)
0
1. Czy pamietasz poprzedni rozdział, w którym okazało sie, ze pusty napis pasuje
do wyrazenia regularnego uzywanego do sprawdzania poprawnosci liczb rzymskich?
Otóz okazuje sie, ze jest to prawda nawet w ostatecznej wersji wyrazenia
regularnego. I to jest własnie bład; pozadanym rezultatem przekazania pustego
napisu, podobnie jak kazdego innego napisu, który nie reprezentuje poprawnej
liczby rzymskiej, jest rzucenie wyjatku InvalidRomanNumeralError.
Po udanym odtworzeniu błedu, ale przed jego naprawieniem, powinno sie napisac
przypadek testowy, który nie działa, uwidaczniajac w ten sposób znaleziony bład.
Przykład 15.2. Testowanie błedu (romantest61.py)
class FromRomanBadInput(unittest.TestCase):
# previous test cases omitted for clarity (they haven’t changed)
def testBlank(self):
"""fromRoman should fail with blank string"""
self.assertRaises(roman.InvalidRomanNumeralError, roman.fromRoman, "") #(1)
1. Sprawa jest prosta. Wywołujemy fromRoman z pustym napisem i upewniamy sie,
ze został rzucony wyjatek InvalidRomanNumeralError. Najtrudniejsza czescia
było znalezienie błedu; teraz, kiedy juz o nim wiemy, testowanie okazuje sie
łatwe.
Mamy juz odpowiedni przypadek testowy, jednak nie bedzie on działał, poniewaz
w kodzie wciaz jest bład:
Przykład 15.3. Wyjscie programu romantest61.py testujacego roman61.py
fromRoman should only accept uppercase input ... ok
toRoman should always return uppercase ... ok
fromRoman should fail with blank string ... FAIL
fromRoman should fail with malformed antecedents ... ok
fromRoman should fail with repeated pairs of numerals ... ok
fromRoman should fail with too many repeated numerals ... ok
fromRoman should give known result with known input ... ok
toRoman should give known result with known input ... ok
fromRoman(toRoman(n))==n for all n ... ok
toRoman should fail with non-integer input ... ok
toRoman should fail with negative input ... ok
toRoman should fail with large input ... ok
toRoman should fail with 0 input ... ok
16.1. OBSŁUGA BŁEDÓW 327
======================================================================
FAIL: fromRoman should fail with blank string
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:docbookdippyromanstage6romantest61.py", line 137, in testBlank
self.assertRaises(roman61.InvalidRomanNumeralError, roman61.fromRoman, "")
File "c:python21libunittest.py", line 266, in failUnlessRaises
raise self.failureException, excName
AssertionError: InvalidRomanNumeralError
----------------------------------------------------------------------
Ran 13 tests in 2.864s
FAILED (failures=1)
Teraz mozemy przystapic do naprawy błedu.
Przykład 15.4. Poprawiane błedu (roman62.py)
Plik jest dostepny w katalogu py/roman/stage6/ znajdujacym sie w katalogu z
przykładami.
def fromRoman(s):
"""convert Roman numeral to integer"""
if not s: #(1)
raise InvalidRomanNumeralError, ’Input can not be blank’
if not re.search(romanNumeralPattern, s):
raise InvalidRomanNumeralError, ’Invalid Roman numeral: %s’ % s
result = 0
index = 0
for numeral, integer in romanNumeralMap:
while s[index:index+len(numeral)] == numeral:
result += integer
index += len(numeral)
return result
1. Potrzebne sa tylko dwie dodatkowe linie kodu: jawne sprawdzenie pustego napisu
oraz wyrazenie raise.
Przykład 15.5. Wyjscie programu romantest62.py testujacego roman62.py
fromRoman should only accept uppercase input ... ok
toRoman should always return uppercase ... ok
fromRoman should fail with blank string ... ok #(1)
fromRoman should fail with malformed antecedents ... ok
fromRoman should fail with repeated pairs of numerals ... ok
fromRoman should fail with too many repeated numerals ... ok
fromRoman should give known result with known input ... ok
toRoman should give known result with known input ... ok
fromRoman(toRoman(n))==n for all n ... ok
toRoman should fail with non-integer input ... ok
toRoman should fail with negative input ... ok
328 ROZDZIAŁ 16. REFAKTORYZACJA
toRoman should fail with large input ... ok
toRoman should fail with 0 input ... ok
----------------------------------------------------------------------
Ran 13 tests in 2.834s
OK #(2)
1. Test pustego napisu przechodzi, a wiec bład udało sie naprawic.
2. Wszystkie pozostałe testy przechodza, co oznacza, ze poprawka błedu nie zepsuła
kodu w innych miejscach. Koniec kodowania.
Ten sposób kodowania nie sprawi, ze znajdowanie błedów stanie sie łatwiejsze.
Proste błedy (takie, jak ten w przykładzie) wymagaja prostych testów jednostkowych;
błedy bardziej złozone beda wymagały testów odpowiednio bardziej złozonych. W
srodowisku, w którym na testowanie kładzie sie duzy nacisk, moze sie poczatkowo wydawac,
ze poprawienie błedu zabiera znacznie wiecej czasu: najpierw nalezy dokładnie
wyrazic w kodzie, na czym polega bład (czyli napisac przypadek testowy), a pózniej
dopiero go poprawic. Nastepnie, jesli przypadek testowy nie przechodzi, nalezy sprawdzic,
czy to poprawka była niewystarczajaca, czy moze kod przypadku testowego został
niepoprawnie zaimplementowany. Jednak w długiej perspektywie takie przełaczanie sie
miedzy kodem i testami niewatpliwie sie opłaca, poniewaz poprawienie błedu za pierwszym
razem jest o wiele bardziej prawdopodobne. Dodatkowo, mozliwosc uruchomienia
wszystkich testów łacznie z dopisanym nowym przypadkiem testowym pozwala łatwo
sprawdzic, czy poprawka błedu nie spowodowała problemów w starym kodzie. Dzisiejszy
test jednostkowy staje sie wiec jutrzejszym testem regresyjnym.
16.2. OBSŁUGA ZMIENIAJACYCH SIE WYMAGAN 329
16.2 Obsługa zmieniajacych sie wymagan
Chocbysmy próbowali przyszpilic swoich klientów do ziemi w celu uzyskania od nich
dokładnych wymagan, uzywajac tak przerazajacych narzedzi tortur, jak nozyce czy
goracy wosk, to i tak te wymagania sie zmienia. Wiekszosc klientów nie wie, czego chce,
dopóki tego nie zobaczy, a nawet jak juz zobaczy, to nie jest w stanie wyartykułowac
tego wystarczajaco precyzyjnie, aby było to uzyteczne. Nawet, gdyby im sie to udało, to
zapewne i tak w kolejnym wydaniu beda chcieli czegos wiecej. Tak wiec lepiej badzmy
przygotowani na aktualizowanie swoich przypadków testowych w miare jak zmieniaja
sie wymagania.
Przypuscmy, na przykład, ze chcielismy rozszerzyc zakres funkcji konwertujacych
liczby rzymskie. Czy pamietacie regułe, która mówi, ze zadna litera nie moze byc
powtórzona wiecej niz trzy razy? Otóz Rzymianie chcieli uczynic wyjatek od tej reguły
tak, aby móc reprezentowac wartosc 4000 stawiajac obok siebie cztery litery M.
Jesli wprowadzimy te zmiane, bedziemy mogli rozszerzyc zakres liczb mozliwych do
przekształcenia na liczbe rzymska z 1..3999 do 1..4999. Najpierw jednak musimy wprowadzic
kilka zmian do przypadków testowych.
Przykład 15.6. Zmiana przypadków testowych przy nowych wymaganiach
(romantest71.py)
Plik jest dostepny w katalogu py/roman/stage7/ znajdujacym sie w katalogu examples.
Jesli jeszcze tego nie zrobiliscie, sciagnijcie ten oraz inne przykłady (http://
diveintopython.org/download/diveintopython-examples-5.4.zip) uzywane w tej
ksiazce.
import roman71
import unittest
class KnownValues(unittest.TestCase):
knownValues = ( (1, ’I’),
(2, ’II’),
(3, ’III’),
(4, ’IV’),
(5, ’V’),
(6, ’VI’),
(7, ’VII’),
(8, ’VIII’),
(9, ’IX’),
(10, ’X’),
(50, ’L’),
(100, ’C’),
(500, ’D’),
(1000, ’M’),
(31, ’XXXI’),
(148, ’CXLVIII’),
(294, ’CCXCIV’),
(312, ’CCCXII’),
(421, ’CDXXI’),
(528, ’DXXVIII’),
(621, ’DCXXI’),
330 ROZDZIAŁ 16. REFAKTORYZACJA
(782, ’DCCLXXXII’),
(870, ’DCCCLXX’),
(941, ’CMXLI’),
(1043, ’MXLIII’),
(1110, ’MCX’),
(1226, ’MCCXXVI’),
(1301, ’MCCCI’),
(1485, ’MCDLXXXV’),
(1509, ’MDIX’),
(1607, ’MDCVII’),
(1754, ’MDCCLIV’),
(1832, ’MDCCCXXXII’),
(1993, ’MCMXCIII’),
(2074, ’MMLXXIV’),
(2152, ’MMCLII’),
(2212, ’MMCCXII’),
(2343, ’MMCCCXLIII’),
(2499, ’MMCDXCIX’),
(2574, ’MMDLXXIV’),
(2646, ’MMDCXLVI’),
(2723, ’MMDCCXXIII’),
(2892, ’MMDCCCXCII’),
(2975, ’MMCMLXXV’),
(3051, ’MMMLI’),
(3185, ’MMMCLXXXV’),
(3250, ’MMMCCL’),
(3313, ’MMMCCCXIII’),
(3408, ’MMMCDVIII’),
(3501, ’MMMDI’),
(3610, ’MMMDCX’),
(3743, ’MMMDCCXLIII’),
(3844, ’MMMDCCCXLIV’),
(3888, ’MMMDCCCLXXXVIII’),
(3940, ’MMMCMXL’),
(3999, ’MMMCMXCIX’),
(4000, ’MMMM’), (4500, ’MMMMD’),
(4888, ’MMMMDCCCLXXXVIII’),
(4999, ’MMMMCMXCIX’))
def testToRomanKnownValues(self):
"""toRoman should give known result with known input"""
for integer, numeral in self.knownValues:
result = roman71.toRoman(integer)
self.assertEqual(numeral, result)
def testFromRomanKnownValues(self):
"""fromRoman should give known result with known input"""
for integer, numeral in self.knownValues:
result = roman71.fromRoman(numeral)
16.2. OBSŁUGA ZMIENIAJACYCH SIE WYMAGAN 331
self.assertEqual(integer, result)
class ToRomanBadInput(unittest.TestCase):
def testTooLarge(self):
"""toRoman should fail with large input"""
self.assertRaises(roman71.OutOfRangeError, roman71.toRoman, 5000) #(2)
def testZero(self):
"""toRoman should fail with 0 input"""
self.assertRaises(roman71.OutOfRangeError, roman71.toRoman, 0)
def testNegative(self):
"""toRoman should fail with negative input"""
self.assertRaises(roman71.OutOfRangeError, roman71.toRoman, -1)
def testNonInteger(self):
"""toRoman should fail with non-integer input"""
self.assertRaises(roman71.NotIntegerError, roman71.toRoman, 0.5)
class FromRomanBadInput(unittest.TestCase):
def testTooManyRepeatedNumerals(self):
"""fromRoman should fail with too many repeated numerals"""
for s in (’MMMMM’, ’DD’, ’CCCC’, ’LL’, ’XXXX’, ’VV’, ’IIII’): #(3)
self.assertRaises(roman71.InvalidRomanNumeralError, roman71.fromRoman, s)
def testRepeatedPairs(self):
"""fromRoman should fail with repeated pairs of numerals"""
for s in (’CMCM’, ’CDCD’, ’XCXC’, ’XLXL’, ’IXIX’, ’IVIV’):
self.assertRaises(roman71.InvalidRomanNumeralError, roman71.fromRoman, s)
def testMalformedAntecedent(self):
"""fromRoman should fail with malformed antecedents"""
for s in (’IIMXCC’, ’VX’, ’DCM’, ’CMM’, ’IXIV’,
’MCMC’, ’XCX’, ’IVI’, ’LM’, ’LD’, ’LC’):
self.assertRaises(roman71.InvalidRomanNumeralError, roman71.fromRoman, s)
def testBlank(self):
"""fromRoman should fail with blank string"""
self.assertRaises(roman71.InvalidRomanNumeralError, roman71.fromRoman, "")
class SanityCheck(unittest.TestCase):
def testSanity(self):
"""fromRoman(toRoman(n))==n for all n"""
for integer in range(1, 5000): #(4)
numeral = roman71.toRoman(integer)
result = roman71.fromRoman(numeral)
self.assertEqual(integer, result)
class CaseCheck(unittest.TestCase):
def testToRomanCase(self):
332 ROZDZIAŁ 16. REFAKTORYZACJA
"""toRoman should always return uppercase"""
for integer in range(1, 5000):
numeral = roman71.toRoman(integer)
self.assertEqual(numeral, numeral.upper())
def testFromRomanCase(self):
"""fromRoman should only accept uppercase input"""
for integer in range(1, 5000):
numeral = roman71.toRoman(integer)
roman71.fromRoman(numeral.upper())
self.assertRaises(roman71.InvalidRomanNumeralError,
roman71.fromRoman, numeral.lower())
if __name__ == "__main__":
unittest.main()
1. Istniejace wartosci nie zmieniaja sie (to wciaz rozsadne wartosci do przetestowania),
jednak musimy dodac kilka do poszerzonego zakresu. Powyzej dodałem
4000 (najkrótszy napis), 4500 (drugi najkrótszy), 4888 (najdłuzszy) oraz 4999
(najwiekszy co do wartosci).
2. Zmieniła sie definicja “duzych danych wejsciowych”. Ten test miał nie przechodzic
dla wartosci 4000 i zgłaszac w takiej sytuacji bład; teraz wartosci z
przedziału 4000-4999 sa poprawne, a jako pierwsza niepoprawna wartosc nalezy
przyjac 5000.
3. Zmieniła sie definicja “zbyt wielu powtórzonych cyfr rzymskich”. Ten test wywoływał
fromRoman z wartoscia ’MMMM’ i spodziewał sie błedu. Obecnie MMMM
jest poprawna liczba rzymska, a wiec nalezy zmienic niepoprawna wartosc na
’MMMMM’.
4. Testy prostego sprawdzenia i sprawdzenia wielkosci liter iteruja po wartosciach z
przedziału od 1 do 3999. Ze wzgledu na poszerzenie tego przedziału rozszerzamy
tez petle w testach tak, aby uwzgledniały wartosci do 4999.
Teraz przypadki testowe odzwierciedlaja juz nowe wymagania, jednak nie uwzglednia
ich jeszcze kod, a wiec mozna sie spodziewac, ze pewne testy nie przejda:
Przykład 15.7. Wyjscie programu romantest71.py testujacego roman71.py
fromRoman should only accept uppercase input ... ERROR #(1)
toRoman should always return uppercase ... ERROR
fromRoman should fail with blank string ... ok
fromRoman should fail with malformed antecedents ... ok
fromRoman should fail with repeated pairs of numerals ... ok
fromRoman should fail with too many repeated numerals ... ok
fromRoman should give known result with known input ... ERROR #(2)
toRoman should give known result with known input ... ERROR #(3)
fromRoman(toRoman(n))==n for all n ... ERROR #(4)
toRoman should fail with non-integer input ... ok
toRoman should fail with negative input ... ok
toRoman should fail with large input ... ok
toRoman should fail with 0 input ... ok
16.2. OBSŁUGA ZMIENIAJACYCH SIE WYMAGAN 333
1. Test sprawdzajacy wielkosc liter nie przechodzi, poniewaz petla uwzglednia wartosci
od 1 do 4999, natomiast toRoman akceptuje wartosci z przedziału od 1 do
3999. Jak tylko licznik petli osiagnie wartosc 4000, test nie przechodzi.
2. Test poprawnych wartosci uzywajacy toRoman nie przechodzi dla napisu ’MMMM’,
poniewaz toRoman wciaz sadzi, ze jest to wartosc niepoprawna.
3. Test poprawnych wartosci uzywajacy toRoman nie przechodzi dla wartosci 4000,
poniewaz toRoman wciaz sadzi, ze jest to wartosc spoza zakresu.
4. Test poprawnosci równiez nie przechodzi dla wartosci 4000, poniewaz toRoman
wciaz sadzi, ze jest to wartosc spoza zakresu.
======================================================================
ERROR: fromRoman should only accept uppercase input
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:docbookdippyromanstage7romantest71.py", line 161, in testFromRomanCase
numeral = roman71.toRoman(integer)
File "roman71.py", line 28, in toRoman
raise OutOfRangeError, "number out of range (must be 1..3999)"
OutOfRangeError: number out of range (must be 1..3999)
======================================================================
ERROR: toRoman should always return uppercase
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:docbookdippyromanstage7romantest71.py", line 155, in testToRomanCase
numeral = roman71.toRoman(integer)
File "roman71.py", line 28, in toRoman
raise OutOfRangeError, "number out of range (must be 1..3999)"
OutOfRangeError: number out of range (must be 1..3999)
======================================================================
ERROR: fromRoman should give known result with known input
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:docbookdippyromanstage7romantest71.py", line 102, in testFromRomanKnownValues
result = roman71.fromRoman(numeral)
File "roman71.py", line 47, in fromRoman
raise InvalidRomanNumeralError, ’Invalid Roman numeral: %s’ % s
InvalidRomanNumeralError: Invalid Roman numeral: MMMM
======================================================================
ERROR: toRoman should give known result with known input
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:docbookdippyromanstage7romantest71.py", line 96, in testToRomanKnownValues
result = roman71.toRoman(integer)
File "roman71.py", line 28, in toRoman
raise OutOfRangeError, "number out of range (must be 1..3999)"
OutOfRangeError: number out of range (must be 1..3999)
======================================================================
ERROR: fromRoman(toRoman(n))==n for all n
334 ROZDZIAŁ 16. REFAKTORYZACJA
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:docbookdippyromanstage7romantest71.py", line 147, in testSanity
numeral = roman71.toRoman(integer)
File "roman71.py", line 28, in toRoman
raise OutOfRangeError, "number out of range (must be 1..3999)"
OutOfRangeError: number out of range (must be 1..3999)
----------------------------------------------------------------------
Ran 13 tests in 2.213s
FAILED (errors=5)
Kiedy juz mamy przypadki testowe, które ze wzgledu na nowe wymagania przestały
przechodzic, mozemy myslec o poprawieniu kodu tak, aby był zgodny z testami
(kiedy zaczyna sie pisac testy jednostkowe, nalezy sie przyzwyczaic do jednej rzeczy:
testowany kod nigdy nie “wyprzedza” przypadków testowych. Zdarza sie, ze kod “nie
nadaza”, co oznacza, ze wciaz jest cos do zrobienia, przy czym jak tylko kod “dogoni”
testy, zadanie jest juz wykonane).
Przykład 15.8. Implementacja nowych wymagan (roman72.py)
Plik jest umieszczony w katalogu py/roman/stage7/ znajdujacym sie w katalogu
examples.
"""Convert to and from Roman numerals"""
import re
#Define exceptions
class RomanError(Exception): pass
class OutOfRangeError(RomanError): pass
class NotIntegerError(RomanError): pass
class InvalidRomanNumeralError(RomanError): pass
#Define digit mapping
romanNumeralMap = ((’M’, 1000),
(’CM’, 900),
(’D’, 500),
(’CD’, 400),
(’C’, 100),
(’XC’, 90),
(’L’, 50),
(’XL’, 40),
(’X’, 10),
(’IX’, 9),
(’V’, 5),
(’IV’, 4),
(’I’, 1))
def toRoman(n):
"""convert integer to Roman numeral"""
if not (0 < n < 5000): raise OutOfRangeError, "number out of range (must be 1..4999)"
16.2. OBSŁUGA ZMIENIAJACYCH SIE WYMAGAN 335
if int(n) <> n:
raise NotIntegerError, "non-integers can not be converted"
result = ""
for numeral, integer in romanNumeralMap:
while n >= integer:
result += numeral
n -= integer
return result
#Define pattern to detect valid Roman numerals
romanNumeralPattern = ’^M?M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)(IX|IV|V?I?I?I?)$’ #(2)
def fromRoman(s):
"""convert Roman numeral to integer"""
if not s:
raise InvalidRomanNumeralError, ’Input can not be blank’
if not re.search(romanNumeralPattern, s):
raise InvalidRomanNumeralError, ’Invalid Roman numeral: %s’ % s
result = 0
index = 0
for numeral, integer in romanNumeralMap:
while s[index:index+len(numeral)] == numeral:
result += integer
index += len(numeral)
return result
1. toRoman wymaga jednej małej zmiany w sprawdzeniu zakresu. Tam, gdzie było
sprawdzenie 0 < n < 4000, powinno byc 0 < n < 5000. Nalezy tez zmienic
tresc komunikatu błedu, tak, aby odzwierciedlał on nowy, akceptowalny zakres
wartosci (1..4999 zamiast 1..3999). Nie trzeba dokonywac zadnych innych zmian
w kodzie funkcji, który juz obsługuje nowe wymaganie. (Kod dodaje ’M’ dla
kazdego pełnego tysiaca; gdy na wejsciu jest wartosc 4000, otrzymamy liczbe
rzymska ’MMMM’. Jedynym powodem tego, ze funkcja nie działała tak wczesniej
było jej jawne zakonczenie przy wartosci przekraczajacej dopuszczalny zakres.
2. Nie trzeba w ogóle zmieniac fromRoman. Jedyna wymagana zmiana dotyczy
wzorca romanNumeralPattern; po blizszym przyjrzeniu sie widac, ze wystarczyło
dodac jeszcze jedno opcjonalne ’M’ w pierwszej czesci wyrazenia regularnego.
Pozwoli to na dopasowanie maksymalnie czterech znaków M zamiast trzech, a
wiec uwzgledni wartosci rzymskie z przedziału do 4999 zamiast do 3999. Obecna
implementacja fromRoman jest bardzo ogólna: wyszukuje ona powtarzajace sie
znaki w zapisie rzymskim, a nastepnie sumuje ich odpowiednie wartosci, nie
przejmujac sie liczba ich wystapien. Funkcja ta nie obsługiwała wczesniej napisu
’MMMM’ wyłacznie dlatego, ze napis taki nie zostałby wczesniej dopasowany do
wyrazenia regularnego.
Mozecie byc odrobine sceptyczni wobec stwierdzenia, ze te dwie małe zmiany to
wszystko, czego potrzebujemy. Nie wierzcie mi na słowo, po prostu to sprawdzcie:
Przykład 15.9. Wyjscie programu romantest72.py testujacego roman72.py
336 ROZDZIAŁ 16. REFAKTORYZACJA
fromRoman should only accept uppercase input ... ok
toRoman should always return uppercase ... ok
fromRoman should fail with blank string ... ok
fromRoman should fail with malformed antecedents ... ok
fromRoman should fail with repeated pairs of numerals ... ok
fromRoman should fail with too many repeated numerals ... ok
fromRoman should give known result with known input ... ok
toRoman should give known result with known input ... ok
fromRoman(toRoman(n))==n for all n ... ok
toRoman should fail with non-integer input ... ok
toRoman should fail with negative input ... ok
toRoman should fail with large input ... ok
toRoman should fail with 0 input ... ok
----------------------------------------------------------------------
Ran 13 tests in 3.685s
OK #(1. Wszytskie testy przechodza. Konczymy kodowanie.
Pełne testowanie jednostkowe oznacza, ze nigdy nie trzeba polegac na słowach
programisty mówiacego: “Zaufaj mi”.
16.3. REFAKTORYZACJA 337
16.3 Refaktoryzacja
Najcenniejsza rzecza, jaka daje testowanie jednostkowe, nie jest uczucie, jakiego
doswiadczamy, kiedy wszystkie testy przechodza, ani nawet uczucie w chwili, gdy ktos
obwinia nas o popsucie swojego kodu, a my jestesmy w stanie udowodnic, ze to nie
nasza wina. Najcenniejsza rzecza w testowaniu jednostkowym jest to, ze daje nam ono
nieskrepowana wolnosc podczas refaktoryzacji.
Refaktoryzacja to proces, który polega na tym, ze bierze sie działajacy kod i zmienia
go tak, aby działał jeszcze lepiej. Zwykle “lepiej” znaczy “szybciej”, choc moze to
równiez znaczyc “przy mniejszym zuzyciu pamieci”, “przy mniejszym zuzyciu przestrzeni
dyskowej” czy nawet “bardziej elegancko”. Czymkolwiek refaktoryzacja jest
dla was, dla waszego projektu czy waszego srodowiska pracy, słuzy ona utrzymaniu
programu w dobrym zdrowiu przez długi czas.
W naszym przypadku “lepiej” znaczy “szybciej”. Dokładnie rzecz ujmujac, funkcja
fromRoman jest wolniejsza niz musiałaby byc ze wzgledu na duze, brzydkie wyrazenie
regularne, którego uzywamy do zweryfikowania, czy napis stanowi poprawna reprezentacje
liczby w notacji rzymskiej. Prawdopodobnie nie opłaca sie całkowicie eliminowac
tego wyrazenia (byłoby to trudne i mogłoby doprowadzic do powstania jeszcze wolniejszego
kodu), jednak mozna uzyskac pewne przyspieszenie dzieki temu, ze wyrazenie
regularne zostanie wstepnie skompilowane.
Przykład 15.10. Kompilacja wyrazenia regularnego
>>> import re
>>> pattern = ’^M?M?M?$’
>>> re.search(pattern, ’M’) #(1)
<SRE_Match object at 01090490>
>>> compiledPattern = re.compile(pattern) #(2)
>>> compiledPattern
<SRE_Pattern object at 00F06E28>
>>> dir(compiledPattern) #(3)
[’findall’, ’match’, ’scanner’, ’search’, ’split’, ’sub’, ’subn’]
>>> compiledPattern.search(’M’) #(4)
<SRE_Match object at 01104928>
1. To składnia, która juz wczesniej widzieliscie: re.search pobiera wyrazenie regularne
jako napis (pattern) oraz napis, do którego wyrazenie bedzie dopasowywane
(’M’). Jesli wyrazenie zostanie dopasowane, funkcja zwróci obiekt match,
który mozna nastepnie odpytac, aby dowiedziec sie, co zostało dopasowane i w
jaki sposób.
2. To jest juz nowa składnia: re.compile pobiera wyrazenie regularne jako napis
i zwraca obiekt pattern. Zauwazmy, ze nie przekazujemy napisu, do którego
bedzie dopasowywane wyrazenie. Kompilacja wyrazenia regularnego nie ma nic
wspólnego z dopasowywaniem wyrazenia do konkretnego napisu (jak np. ’M’);
dotyczy ona wyłacznie samego wyrazenia.
3. Obiekt pattern zwrócony przez funkcje re.compile posiada wiele pozytecznie
wygladajacych funkcji, miedzy innymi kilka takich, które sa dostepne bezposrednio
w module re (np. search czy sub).
4. Wywołujac funkcji search na obiekcie pattern z napisem ’M’ jako parametrem
osiagamy ten sam efekt, co wywołujac re.search z wyrazeniem regularnym i
338 ROZDZIAŁ 16. REFAKTORYZACJA
napisem ’M’ jako parametrami. Z ta róznica, ze osiagamy go o wiele, wiele szybciej.
(W rzeczywistosci funkcja re.search kompiluje wyrazenie regularne i na
obiekcie bedacym wynikiem tej kompilacji wywołuje metode search.)
Kompilacja wyrazen regularnych. Jesli kiedykolwiek bedziecie potrzebowali uzyc
wyrazenia regularnego wiecej niz raz, powinniscie najpierw skompilowac je do obiektu
pattern, a nastepnie wywoływac bezposrednio jego metody
Przykład 15.11.Skompilowane wyrazenie regularne w roman81.py
Plik jest dostepny w katalogu in py/roman/stage8/ wewnatrz katalogu examples.
Jesli jeszcze tego nie zrobiliscie, mozecie pobrac ten oraz inne przykłady uzywane
w tej ksiazce stad.
# toRoman and rest of module omitted for clarity
romanNumeralPattern =
re.compile(’^M?M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)(IX|IV|V?I?I?I?)$’) (1)
def fromRoman(s):
"""convert Roman numeral to integer"""
if not s:
raise InvalidRomanNumeralError, ’Input can not be blank’
if not romanNumeralPattern.search(s): (2)
raise InvalidRomanNumeralError, ’Invalid Roman numeral: %s’ % s
result = 0
index = 0
for numeral, integer in romanNumeralMap:
while s[index:index+len(numeral)] == numeral:
result += integer
index += len(numeral)
return result
1. Wyglada podobnie, choc w rzeczywistosci bardzo duzo sie zmieniło. romanNumeralPattern
nie jest juz napisem; to obiekt pattern, który został zwrócony przez re.compile.
2. Ta linia oznacza, ze na obiekcie romanNumeralPattern mozna bezposrednio wywoływac
metody. Beda one wykonane o wiele szybciej, niz np. podczas kazdorazowego
wywołania re.search. Tutaj wyrazenie regularne zostało skompilowane
dokładnie jeden raz i zapamietane pod nazwa romanNumeralPattern w momencie
pierwszego importu modułu; od tego momentu. ilekroc bedzie wywołana metoda
fromRoman, gotowe wyrazenie bedzie dopasowywane do napisu wejsciowego,
bez zadnych kroków posrednich odbywajacych sie niejawnie.
Wobec tego o ile szybciej działa kod po skompilowaniu wyrazenia regularnego?
Sprawdzcie sami:
Przykład 15.12. Wyjscie programu romantest81.py testujacego roman81.py
............. #(1)
----------------------------------------------------------------------
16.3. REFAKTORYZACJA 339
Ran 13 tests in 3.385s #(2)
OK #(3)
1. Tutaj tylko mała uwaga: tym razem uruchomiłem testy bez podawania opcji -
v, dlatego tez zamiast pełnego napisu komentujacego dla kazdego testu, który
zakonczył sie powodzeniem, została wypisana kropka. (Gdyby test zakonczył sie
niepowodzeniem, zostałaby wypisana litera F, a w przypadku błedu — litera
E. Potencjalne problemy mozna wciaz łatwo zidentyfikowac, poniewaz w razie
niepowodzen lub błedów wypisywana jest zawartosc stosu.)
2. Uruchomienie 13 testów zajeło 3.385 sekund w porównaniu z 3.685 sekund, jakie
zajeły testy bez wczesniejszej kompilacji wyrazenia regularnego. To poprawa w
wysokosci 8%, a warto pamietac, ze przez wiekszosc czasu w testach jednostkowych
wykonywane sa takze inne rzeczy. (Gdy przetestowałem same wyrazenia
regularne, niezaleznie od innych testów, okazało sie, ze kompilacja wyrazenia regularnego
polepszyła czas operacji wyszukiwania srednio o 54%.) Niezle, jak na
taka niewielka poprawke.
3. Och, gdybyscie sie jeszcze zastanawiali, prekompilacja wyrazenia regularnego
niczego nie zepsuła, co własnie udowodnilismy.
Jest jeszcze jedna optymalizacja wydajnosci, która chciałem wypróbowac. Nie powinno
byc niespodzianka, ze przy wysokiej złozonosci składni wyrazen regularnych
istnieje wiecej niz jeden sposób napisania tego samego wyrazenia. W trakcie dyskusji
nad tym rozdziałem, jaka odbyła sie na comp.lang.python ktos zasugerował, zebym
dla powtarzajacych sie opcjonalnych znaków spróbował uzyc składni {m, n}.
Przykład 15.13. roman82.py
Plik jest dostepny w katalogu in py/roman/stage8/ wewnatrz katalogu examples.
Jesli jeszcze tego nie zrobiliscie, mozecie pobrac ten oraz inne przykłady uzywane
w tej ksiazce stad.
# rest of program omitted for clarity
#old version
#romanNumeralPattern =
# re.compile(’^M?M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)(IX|IV|V?I?I?I?)$’)
#new version
romanNumeralPattern =
re.compile(’^M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$’) #(1)
1. Zastapilismy M?M?M?M? wyrazeniem M{0,4}. Obydwa zapisy oznaczaja to
samo: “dopasuj od 0 do 4 znaków M”. Podobnie C?C?C? zostało zastapione
C{0,3} (“dopasuj od 0 do 3 znaków C”) i tak dalej dla X oraz I.
Powyzszy zapis wyrazenia regularnego jest odrobine krótszy (choc nie bardziej
czytelny). Pytanie brzmi: czy jest on szybszy?
Przykład 15.14. Wyjscie programu romantest82.py testujacego roman82.py
340 ROZDZIAŁ 16. REFAKTORYZACJA
.............
----------------------------------------------------------------------
Ran 13 tests in 3.315s #(1)
OK #(2)
1. Przy tej formie wyrazenia regularnego testy jednostkowe działały w sumie 2%
szybciej. Nie brzmi to moze zbyt ekscytujaco, dlatego przypomne, ze wywołania
funkcji wyszukujacej stanowia niewielka czesc wszystkich testów; przez wiekszosc
czasu testy robia co innego. (Gdy niezaleznie od innych testów przetestowałem
wyłacznie wydajnosc wyrazenia regularnego, okazało sie, ze jest ona o 11% wieksza
przy nowej składni). Dzieki prekompilacji wyrazenia regularnego i zmianie
jego składni udało sie poprawic wydajnosc samego wyrazenia o ponad 60%, a
wszystkich testów łacznie o ponad 10%.
2. Znacznie wazniejsze od samego wzrostu wydajnosci jest to, ze moduł wciaz doskonale
działa. To jest własnie wolnosc, o której wspominałem juz wczesniej:
wolnosc poprawiania, zmieniania i przepisywania dowolnego fragmentu kodu i
mozliwosc sprawdzenia, ze zmiany te w miedzyczasie wszystkiego nie popsuły.
Nie chodzi tu o poprawki dla samych poprawek; mielismy bardzo konkretny cel
(“przyspieszyc toRoman”) i bylismy w stanie go zrealizowac bez zbytnich wahan
i troski o to, czy nie wprowadzilismy do kodu nowych błedów.
Chce zrobic jeszcze jedna, ostatnia zmiane i obiecuje, ze na niej skoncze refaktoryzacje
i dam juz temu modułowi spokój. Jak wielokrotnie widzieliscie, wyrazenia
regularne szybko staja sie bardzo nieporzadne i mocno traca na swej czytelnosci. Naprawde
nie chciałbym dostac tego modułu do utrzymania za szesc miesiecy. Oczywiscie,
testy przechodza, wiec mam pewnosc, ze kod działa, jednak jesli nie jestem całkowicie
pewien, w jaki sposób on działa, to bedzie mi trudno dodawac do niego nowe wymagania,
poprawiac błedy czy w inny sposób go utrzymywac. W podrozdziale idzieliscie,
ze Python umozliwia dokładne udokumentowanie logiki kodu.
Przykład 15.15. roman83.py
Plik jest dostepny w katalogu in py/roman/stage8/ wewnatrz katalogu examples.
Jesli jeszcze tego nie zrobiliscie, mozecie pobrac ten oraz inne przykłady uzywane
w tej ksiazce stad.
# rest of program omitted for clarity
#old version
#romanNumeralPattern =
# re.compile(’^M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$’)
#new version
romanNumeralPattern = re.compile(
’’’
^ # beginning of string
M{0,4} # thousands - 0 to 4 M’s
(CM|CD|D?C{0,3}) # hundreds - 900 (CM), 400 (CD), 0-300 (0 to 3 C’s),
# or 500-800 (D, followed by 0 to 3 C’s)
(XC|XL|L?X{0,3}) # tens - 90 (XC), 40 (XL), 0-30 (0 to 3 X’s),
# or 50-80 (L, followed by 0 to 3 X’s)
16.3. REFAKTORYZACJA 341
(IX|IV|V?I{0,3}) # ones - 9 (IX), 4 (IV), 0-3 (0 to 3 I’s),
# or 5-8 (V, followed by 0 to 3 I’s)
$ # end of string
’’’, re.VERBOSE) #(1)
1. Funkcja re.compile moze przyjmowac drugi (opcjonalny) argument, bedacy
zbiorem znaczników kontrolujacych wiele aspektów skompilowanego wyrazenia
regularnego. Powyzej wyspecyfikowalismy znacznik re.VERBOSE, który podpowiada
kompilatorowi jezyka Python, ze wewnatrz wyrazenia regularnego znajduja
sie komentarze. Komentarze te wraz z białymi znakami nie stanowia czesci
wyrazenia regularnego; funkcja re.compile nie bierze ich pod uwage podczas
kompilacji. Dzieki temu zapisowi, choc samo wyrazenie jest identyczne jak poprzednio,
jest ono niewatpliwie znacznie bardziej czytelne.
Przykład 15.16. Wyjscie z programu romantest83.py testujacego roman83.
py
.............
----------------------------------------------------------------------
Ran 13 tests in 3.315s #(1)
OK #(2)
1. Nowa, “rozwlekła” wersja wyrazenia regularnego działa dokładnie tak samo,
jak wersja poprzednia. Rzeczywiscie, skompilowane obiekty wyrazen regularnych
beda identyczne, poniewaz re.compile wyrzuca z wyrazenia dodatkowe znaki,
które umiescilismy tam w charakterze komentarza.
2. Nowa, “rozwlekła” wersja wyrazenia regularnego przechodzi wszystkie testy, tak
jak wersja poprzednia. Nie zmieniło sie nic oprócz tego, ze programista, który
wróci do kodu po szesciu miesiacach bedzie miał mozliwosc zrozumienia, w jaki
sposób działa wyrazenie regularne.
342 ROZDZIAŁ 16. REFAKTORYZACJA
16.4 Postscript
Sprytny czytelnik po przeczytaniu poprzedniego podrozdziału byłby w stanie jeszcze
bardziej polepszyc kod programu. Najwiekszym bowiem problemem (i dziura (?)
wydajnosciowa) programu w obecnym kształcie jest wyrazenie regularne, wymagane
ze wzgledu na to, ze nie ma innego sensownego sposobu weryfikacji poprawnosci liczby
w zapisie rzymskim. Tych liczb jest jednak tylko 5000; dlaczego by wiec nie zbudowac
tablicy przeszukiwania tylko raz, a pózniej po prostu z niej korzystac? Ten pomysł
wyda sie jeszcze lepszy, gdy zdamy sobie sprawe, ze w ogóle nie musimy korzystac
z wyrazen regularnych. Skoro mozna zbudowac tablice przeszukiwan słuzaca do konwersji
wartosci liczbowych w ich rzymska reprezentacje, to mozna równiez zbudowac
tablice odwrotna do przekształcania liczb rzymskich w ich wartosc liczbowa.
Najlepsze zas jest to, ze ów sprytny czytelnik miałby juz do swojej dyspozycji pełen
zestaw testów jednostkowych. Choc zmodyfikowałby połowe kodu w module, to testy
pozostałyby takie same, a wiec mógłby on udowodnic, ze kod po zmianach działa tak
samo, jak wczesniej.
Przykład 15.17. roman9.py
Plik jest dostepny w katalogu in py/roman/stage9/ wewnatrz katalogu examples.
Jesli jeszcze tego nie zrobiliscie, mozecie pobrac ten oraz inne przykłady uzywane
w tej ksiazce stad.
#Define exceptions
class RomanError(Exception): pass
class OutOfRangeError(RomanError): pass
class NotIntegerError(RomanError): pass
class InvalidRomanNumeralError(RomanError): pass
#Roman numerals must be less than 5000
MAX_ROMAN_NUMERAL = 4999
#Define digit mapping
romanNumeralMap = ((’M’, 1000),
(’CM’, 900),
(’D’, 500),
(’CD’, 400),
(’C’, 100),
(’XC’, 90),
(’L’, 50),
(’XL’, 40),
(’X’, 10),
(’IX’, 9),
(’V’, 5),
(’IV’, 4),
(’I’, 1))
#Create tables for fast conversion of roman numerals.
#See fillLookupTables() below.
toRomanTable = [ None ] # Skip an index since Roman numerals have no zero
fromRomanTable = {}
16.4. POSTSCRIPT 343
def toRoman(n):
"""convert integer to Roman numeral"""
if not (0 < n <= MAX_ROMAN_NUMERAL):
raise OutOfRangeError, "number out of range (must be 1..%s)" % MAX_ROMAN_NUMERAL
if int(n) <> n:
raise NotIntegerError, "non-integers can not be converted"
return toRomanTable[n]
def fromRoman(s):
"""convert Roman numeral to integer"""
if not s:
raise InvalidRomanNumeralError, "Input can not be blank"
if not fromRomanTable.has_key(s):
raise InvalidRomanNumeralError, "Invalid Roman numeral: %s" % s
return fromRomanTable[s]
def toRomanDynamic(n):
"""convert integer to Roman numeral using dynamic programming"""
result = ""
for numeral, integer in romanNumeralMap:
if n >= integer:
result = numeral
n -= integer
break
if n > 0:
result += toRomanTable[n]
return result
def fillLookupTables():
"""compute all the possible roman numerals"""
#Save the values in two global tables to convert to and from integers.
for integer in range(1, MAX_ROMAN_NUMERAL + 1):
romanNumber = toRomanDynamic(integer)
toRomanTable.append(romanNumber)
fromRomanTable[romanNumber] = integer
fillLookupTables()
A jak szybki jest taki kod?
Przykład 15.18. Wyjscie programu romantest9.py testujacego roman9.py
.............
----------------------------------------------------------------------
Ran 13 tests in 0.791s
OK
Pamietajmy, ze najlepszym wynikiem, jaki udało nam sie do tej pory uzyskac, był
czas 3.315 sekund dla 13 testów. Oczywiscie, to porównanie nie jest zbyt uczciwe,
poniewaz w tej wersji moduł bedzie sie dłuzej importował (beda generowane tablice
344 ROZDZIAŁ 16. REFAKTORYZACJA
przeszukiwan). Jednak ze wzgledu na to, ze importowanie modułu odbywa sie jednokrotnie,
czas, jaki ono zajmuje, mozna uznac za pomijalny.
Morał z tej historii?
• Prostota jest cnota.
• Szczególnie wtedy, gdy chodzi o wyrazenia regularne.
• A testy jednostkowe daja nam pewnosc i odwage do refaktoryzacji w duzej skali...
nawet, jesli to nie my pisalismy oryginalny kod.
16.5. PODSUMOWANIE 345
16.5 Podsumowanie
Testowanie jednostkowe to bardzo silna koncepcja, która, jesli zostanie poprawnie
wdrozona, moze zarówno zredukowac koszty utrzymywania, jak i zwiekszyc elastycznosc
kazdego trwajacego długo projektu informatycznego. Wazne jest, aby zrozumiec,
ze testowanie jednostkowe nie jest panaceum, Magicznym Rozwiazywaczem Problemów
czy srebrna kula. Napisanie dobrych przypadków testowych jest trudne, a utrzymywanie
ich wymaga ogromnej dyscypliny (szczególnie w sytuacjach, gdy klienci zadaja
natychmiastowych poprawek krytycznych błedów). Testowanie jednostkowe nie zastepuje
równiez innych form testowania, takich jak testy funkcjonalne, testy integracyjne
czy testy akceptacyjne. Jest jednak wykonalne, działa, a kiedy juz zobaczycie je w
działaniu, bedziecie sie zastanawiac, jak w ogóle mogliscie bez nich zyc.
W tym rozdziale poruszylismy wiele spraw; czesc z nich nie dotyczyła wyłacznie
jezyka Python. Biblioteki do testowania jednostkowego istnieja dla wielu róznych jezyków,
a ich uzywanie wymaga jedynie zrozumienia kilku podstawowych koncepcji:
• projektowania przypadków testowych, które sa specyficzne, zautomatyzowane i
niezalezne
• pisania przypadków testowych przed napisaniem testowanego kodu
• pisania przypadków testowych, które uwzgledniaja poprawne dane wejsciowe i
spodziewaja sie poprawnych wyników
• pisania przypadków testowych, które uwzgledniaja niepoprawne dane wejsciowe
i spodziewaja sie odpowiednich niepowodzen
• pisania i aktualizowania przypadków testowych przedstawiajacych błedy lub odzwierciedlajacych
nowe wymagania
• bezwzglednej refaktoryzacji w celu poprawy wydajnosci, skalowalnosci, czytelnosci,
utrzymywalnosci oraz kazdej innej -nosci, która nie jest wystarczajaca
Po przeczytaniu tego rozdziału nie powinniscie miec równiez zadnych problemów z
wykonywaniem zadan specyficznych dla jezyka Python:
• tworzeniem klas pochodnych po unittest.TestCase i pisaniem metod bedacych
szczególnymi przypadkami testowymi
• uzywaniem assertEqual do sprawdzania, czy funkcja zwróciła spodziewana wartosc
• uzywaniem assertRaises do sprawdzania, czy funkcja rzuca spodziewany wyjatek
• wywoływaniem unittest.main() w klauzuli if name w celu uruchomienia
wszystkich przypadków testowych na raz
• uruchamianiem zestawu testów jednostkowych zarówno w trybie normalnym, jak
i rozwlekłym
346 ROZDZIAŁ 16. REFAKTORYZACJA
16.6 Programowanie funkcyjne
Nurkujemy
W rozdziale 13 (“Testowanie”) poznaliscie filozofie testowania jednostkowego. Rozdział
14 (“Testowanie 2”) pozwolił wam zaimplementowac podstawowe testy jednostkowe
w jezyku Python. Rozdział 15 (“Refaktoryzacja”) uswiadomił wam, ze dzieki
testom jednostkowym refaktoryzacja na wielka skale staje sie znacznie prostsza. W
tym zas rozdziale, choc wciaz bedziemy bazowac na wczesniejszych, przykładowych
programach, skupimy sie bardziej na zaawansowanych technikach stosowanych w jezyku
Python, niz na samym testowaniu.
Ponizej przedstawiony jest pełny kod programu bedacego tanim i prostym sposobem
uruchamiania testów regresyjnych. Pobiera on testy jednostkowe, jakie zostały
napisane dla poszczególnych modułów, zbiera je do jednego, wielkiego zestawu testowego
i uruchamia je wszystkie jako całosc. Obecnie, podczas pisania tej ksiazki,
skrypt ten słuzy mi jako czesc procesu budowania; napisałem testy jednostkowe dla
wielu przykładowych programów (nie tylko dla roman.py przedstawionego w rozdziale
13, “Testowanie”), a pierwsza rzecza jaka robi skrypt do automatycznego budowania
jest uruchomienie tego własnie programu, dzieki czemu moge sie upewnic, ze wszystkie
moje przykłady wciaz działaja. Jesli zakonczy sie on niepowodzeniem, wówczas
automatyczne budowanie zostaje natychmiast przerwane. Nie chce publikowac niedziałajacych
przykładów, podobnie jak wy nie chcecie pobierac ich z sieci, a pózniej
długo siedziec, drapac sie po głowie i zastanawiac, dlaczego nie działaja.
Przykład 16.1. regression.py
Jesli jeszcze tego nie zrobiliscie, mozecie pobrac ten oraz inne przykłady uzywane
w tej ksiazce.
"""Regression testing framework
This module will search for scripts in the same directory named
XYZtest.py. Each such script should be a test suite that tests a
module through PyUnit. (As of Python 2.1, PyUnit is included in
the standard library as "unittest".) This script will aggregate all
found test suites into one big test suite and run them all at once.
"""
import sys, os, re, unittest
def regressionTest():
path = os.path.abspath(os.path.dirname(sys.argv[0]))
files = os.listdir(path)
test = re.compile("test.py$", re.IGNORECASE)
files = filter(test.search, files)
filenameToModuleName = lambda f: os.path.splitext(f)[0]
moduleNames = map(filenameToModuleName, files)
modules = map(__import__, moduleNames)
load = unittest.defaultTestLoader.loadTestsFromModule
return unittest.TestSuite(map(load, modules))
if __name__ == "__main__":
16.6. NURKUJEMY 347
unittest.main(defaultTest="regressionTest")
Uruchomienie programu w tym samym katalogu, w którym znajduja sie pozostałe
przykładowe skrypty uzywane w tej ksiazce, spowoduje wyszukanie wszystkich testów
jednostkowych o nazwie moduletest.py, uruchomienie ich wszystkich jako jeden test,
a nastepnie stwierdzenie, czy jako całosc przeszły, czy nie.
Przykład 16.2. Przykładowe wyjscie z programu regression.py
[you@localhost py]$ python regression.py -v
help should fail with no object ... ok #(1)
help should return known result for apihelper ... ok
help should honor collapse argument ... ok
help should honor spacing argument ... ok
buildConnectionString should fail with list input ... ok #(2)
buildConnectionString should fail with string input ... ok
buildConnectionString should fail with tuple input ... ok
buildConnectionString handles empty dictionary ... ok
buildConnectionString returns known result with known input ... ok
fromRoman should only accept uppercase input ... ok #(3)
toRoman should always return uppercase ... ok
fromRoman should fail with blank string ... ok
fromRoman should fail with malformed antecedents ... ok
fromRoman should fail with repeated pairs of numerals ... ok
fromRoman should fail with too many repeated numerals ... ok
fromRoman should give known result with known input ... ok
toRoman should give known result with known input ... ok
fromRoman(toRoman(n))==n for all n ... ok
toRoman should fail with non-integer input ... ok
toRoman should fail with negative input ... ok
toRoman should fail with large input ... ok
toRoman should fail with 0 input ... ok
kgp a ref test ... ok
kgp b ref test ... ok
kgp c ref test ... ok
kgp d ref test ... ok
kgp e ref test ... ok
kgp f ref test ... ok
kgp g ref test ... ok
----------------------------------------------------------------------
Ran 29 tests in 2.799s
OK
1. Pierwszych 5 testów pochodzi z apihelpertest.py, który testuje przykładowy
skrypt z rozdziału 4 (Potega introspekcji).
2. Kolejne 5 testów pochodzi z odbchelpertest.py, który testuje przykładowy
skrypt z rozdziału 2 (Pierwszy program)
348 ROZDZIAŁ 16. REFAKTORYZACJA
3. Pozostałe testy pochodza z romantest.py, zestawu testów, który zgłebialismy
w rozdziale 13, (“Testowanie”).
16.7. ZNAJDOWANIE SCIEZKI 349
16.7 Znajdowanie sciezki
Czasami, kiedy uruchomimy skrypt jezyka Python z linii polecen, chcielibysmy
wiedziec, w jakim miejscu na dysku ten skrypt sie znajduje.
To jeden z tych brzydkich, małych trików, które ciezko wymyslic samemu (o ile
to w ogóle mozliwe), ale za to łatwo zapamietac, jesli juz sie to zobaczy. Kluczem do
tego problemu jest sys.argv. Jak widzieliscie w rozdziale 9 (“Przetwarzanie XML”),
jest to lista przechowujaca argumenty linii polecen. Dodatkowo, lista ta przechowuje
równiez nazwe uruchamianego programu, dokładnie taka, jaka została przekazana w
linii polecen, a na jej podstawie mozna juz ustalic połozenie programu na dysku.
Przykład 16.3. fullpath.py
Jesli jeszcze tego nie zrobiliscie, mozecie pobrac ten oraz inne przykłady uzywane
w tej ksiazce.
import sys, os
print ’sys.argv[0] =’, sys.argv[0] #(1)
pathname = os.path.dirname(sys.argv[0]) #(2)
print ’path =’, pathname
print ’full path =’, os.path.abspath(pathname) #(3)
1. Niezaleznie od tego, w jaki sposób uruchomicie skrypt, sys.argv[0] bedzie zawsze
zawierac nazwe skryptu, w dokładnie takiej postaci, w jakiej pojawiła sie
ona w linii polecen. Jak wkrótce zobaczymy, nazwa moze, choc nie musi, zawierac
informacje o pełnej sciezce.
2. os.path.dirname pobiera napis zawierajacy nazwe pliku i zwraca fragment tego
napisu zawierajacy sciezke do katalogu, w którym plik sie znajduje. Jesli podana
nazwa pliku nie zawiera informacji o sciezce, wywołanie os.path.dirname zwróci
napis pusty.
3. Kluczowa funkcja jest os.path.abspath. Pobiera ona nazwe sciezkowa, która
moze byc czesciowa (wzgledna) lub pusta, i zwraca pełna kwalifikowana nazwe
sciezkowa.
Funkcja os.path.abspath wymaga pewnych wyjasnien. Jest ona bardzo elastyczna
i moze przyjmowac nazwy sciezkowe w dowolnej postaci.
Przykład 16.4. Dalsze wyjasnienia dotyczace os.path.abspath
>>> import os
>>> os.getcwd() #(1)
/home/you
>>> os.path.abspath() #(2)
/home/you
>>> os.path.abspath(’.ssh’) #(3)
/home/you/.ssh
>>> os.path.abspath(’/home/you/.ssh’) #(4)
/home/you/.ssh
>>> os.path.abspath(’.ssh/../foo/’) #(5)
/home/you/foo
350 ROZDZIAŁ 16. REFAKTORYZACJA
1. os.getcwd() zwraca biezacy katalog roboczy.
2. Wywołanie os.path.abspath z napisem pustym zwraca biezacy katalog roboczy,
tak samo jak os.getcwd().
3. Wywołanie os.path.abspath z czesciowa nazwa sciezkowa powoduje skonstruowanie
pełnej kwalifikowanej nazwy sciezkowej w oparciu o biezacy katalog roboczy.
4. Wywołanie os.path.abspath z pełna nazwa sciezkowa zwraca te nazwe.
5. os.path.abspath normalizuje nazwe sciezkowa, która zwraca. Zwróccie uwage,
ze powyzszy przykład bedzie działał nawet wówczas, jesli katalog “foo” nie istnieje.
Funkcja os.path.abspath nigdy nie sprawdza istnienia elementów składowych
sciezki na dysku; dokonuje ona jedynie manipulacji na napisach.
Katalogi i pliki, których nazwy sa przekazywane do os.path.abspath nie musza
istniec w systemie plików.
os.path.abspath nie tylko konstruuje pełne nazwy sciezkowe (sciezki
bezwzgledne), lecz równiez je normalizuje. Oznacza to, ze wywołujac
os.path.abspath(’bin/../local/bin’) z katalogu /usr/ otrzyma sie napis
/usr/local/bin. Normalizacja oznacza tutaj najwieksze mozliwe uproszczenie
sciezki. W celu znormalizowania nazwy sciezkowej bez przekształcania jej w pełna
sciezke nalezy uzyc funkcji os.path.normpath.
Przykład 16.5. Przykładowe wyjscie z programu fullpath.py
[you@localhost py]$ python /home/you/diveintopython/common/py/fullpath.py #(1)
sys.argv[0] = /home/you/diveintopython/common/py/fullpath.py
path = /home/you/diveintopython/common/py
full path = /home/you/diveintopython/common/py
[you@localhost diveintopython]$ python common/py/fullpath.py #(2)
sys.argv[0] = common/py/fullpath.py
path = common/py
full path = /home/you/diveintopython/common/py
[you@localhost diveintopython]$ cd common/py
[you@localhost py]$ python fullpath.py #(3)
sys.argv[0] = fullpath.py
path =
full path = /home/you/diveintopython/common/py
1. W pierwszym przypadku sys.argv[0] zawiera pełna sciezke do skryptu. Mozna
uzyc funkcji os.path.dirname w celu usuniecia nazwy skryptu, otrzymujac pełna
sciezke do katalogu, w którym znajduje sie skrypt. Funkcja os.path.abspath
zwraca dokładnie to samo, co otrzymała na wejsciu.
2. Jesli skrypt jest uruchomiony przy uzyciu sciezki wzglednej, sys.argv[0] w
dalszym ciagu zwraca dokładnie to, co pojawiło sie w linii polecen. Wywołanie
16.7. ZNAJDOWANIE SCIEZKI 351
os.path.dirname zwróci czesciowa nazwe sciezkowa (sciezke wzgledna wzgledem
biezacego katalogu), natomiast os.path.abspath z czesciowej nazwy sciezkowej
skonstruuje pełna sciezke (sciezke bezwzgledna).
3. Jesli skrypt jest uruchomiony z biezacego katalogu bez podawania jakiejkolwiek
sciezki, os.dir.pathname zwróci po prostu pusty napis. Podajac pusty napis do
os.path.abspath otrzymamy sciezke do biezacego katalogu, a tego dokładnie
oczekujemy, poniewaz z tego własnie katalogu uruchamialismy skrypt.
Podobnie jak inne funkcje w modułach os oraz os.path, os.path.abspath jest
funkcja działajaca na wszystkich platformach. Jesli uzywacie systemu operacyjnego
Windows (który w charakterze separatorów elementów sciezki uzywa odwróconych
ukosników) lub MacOS (który uzywa dwukropków) , wyjscie waszych programów bedzie
sie odrobine rózniło od przedstawionego w tej ksiazce, ale przykłady beda nadal
działały. I o to własnie chodzi w module os.
Dodatek. Jeden z czytelników był rozczarowany zaprezentowanym wyzej rozwiazaniem,
poniewaz chciał uruchomic wszystkie testy jednostkowe znajdujace sie w biezacym
katalogu, niekoniecznie zas w katalogu, w którym umieszczony jest program
regression.py. Zasugerował on nastepujace podejscie:
Przykład 16.6. Uruchomienie skryptu z biezacego katalogu
import sys, os, re, unittest
def regressionTest():
path = os.getcwd() #(1)
sys.path.append(path) #(2)
files = os.listdir(path) #(3)
1. Zamiast ustalania sciezki z testami na katalog, w którym znajduje sie obecnie
wykonywany skrypt, ustalamy ja na biezacy katalog roboczy. Bedzie to ten katalog,
w którym bylismy w momencie uruchomienia skryptu, a wiec niekoniecznie
oznacza katalog, w którym znajduje sie skrypt. (Jesli nie chwytasz tego od razu,
przeczytaj to zdanie powoli kilka razy).
2. Dodajemy te sciezke do sciezki wyszukiwania bibliotek jezyka Python, dzieki
czemu w momencie dynamicznego importowania modułów z testami jednostkowymi
Python bedzie mógł je odnalezc. Nie trzeba było tego robic w sytuacji,
w której sciezka z testami była sciezka do uruchomionego skryptu, poniewaz
Python zawsze przeszukuje katalog, w którym znajduje sie uruchomiony skrypt.
3. Pozostała czesc funkcji pozostaje bez zmian.
Dzieki tej technice mozliwe jest powtórne uzycie skryptu regression.py w wielu
projektach. Wystarczy umiescic skrypt w pewnym katalogu wspólnym dla wielu projektów,
a nastepnie, przed jego uruchomieniem, zmienic katalog na katalog projektu,
którego testy chcemy uruchomic. Po uruchomieniu skryptu zostana odnalezione i uruchomione
wszystkie testy projektu, znajdujace sie w katalogu projektu, nie zas testy
znajdujace sie w katalogu wspólnym dla projektów, w którym umieszczony został
skrypt.
352 ROZDZIAŁ 16. REFAKTORYZACJA
16.8 Filtrowanie listy
Jeszcze o filtrowaniu list
Zapoznaliscie sie juz z filtrowaniem list przy uzyciu wyrazen listowych (ang. list
comprehension). Istnieje jeszcze jeden sposób na osiagniecie tego celu, przez wiele osób
uznawany za bardziej wyrazisty.
W jezyku Python istnieje wbudowana funkcja filtrujaca (filter) przyjmujaca dwa
parametry, funkcje oraz liste, i zwracajaca liste 1. Funkcja przekazana jako pierwszy
argument do funkcji filter musi przyjmowac jeden argument, natomiast lista zwrócona
przez funkcje filtrujaca bedzie zawierała te elementy z listy przekazanej do funkcji
filtujacej, dla których funkcja przekazana w pierwszym argumencie zwróciła wartosc
true.
Czy wszystko jasne? To nie takie trudne, jak sie wydaje.
Przykład 16.7. Wprowadzenie do funkcji filter
>>> def odd(n): #(1)
... return n % 2
...
>>> li = [1, 2, 3, 5, 9, 10, 256, -3]
>>> filter(odd, li) #(2)
[1, 3, 5, 9, -3]
>>> [e for e in li if odd(e)] #(3)
[1, 3, 5, 9, -3]
>>> filteredList = []
>>> for n in li: #(4)
... if odd(n):
... filteredList.append(n)
...
>>> filteredList
[1, 3, 5, 9, -3]
1. funkcja odd zwraca True jesli n jest nieparzyste a False w przeciwnym przypadku;
uzywa do tego wbudowanej funkcji modulo (“%”).
2. funkcja filter przyjmuje dwa argumenty: funkcje odd oraz liste li. filter iteruje
po liscie i dla kazdego jej elementu wywołuje odd. Jesli odd zwróci wartosc
true (pamietajcie, ze kazda niezerowa wartosc ma w jezyku Python logiczna wartosc
true), wówczas element jest dodawany do listy wynikowej, w przeciwnym
przypadku jest on pomijany. W rezultacie otrzymujemy liste nieparzystych elementów
z listy oryginalnej, w takiej samej kolejnosci, w jakiej elementy pojawiały
sie na oryginalnej liscie.
3. Jak widzielismy w podrozdziale 4.5 Filtrowanie listy, ten sam cel mozna osiagnac
uzywajac wyrazen listowych.
1Patrzac z technicznego punktu widzenia, drugim argumentem funkcji filter moze byc dowolna
sekwencja, właczajac w to listy, krotki oraz klasy, które funkcjonuja jak listy, poniewaz maja zdefiniowana
metode specjalna getitem . Jesli to mozliwe, funkcja filter zwraca ten sam typ danych,
który otrzymała, a wiec wynikiem filtrowania listy bedzie lista, a wynikiem filtrowania krotki bedzie
krotka. Uwagi te dotycza równiez funkcji map, o której bedzie mowa w nastepnym podrozdziale.
16.8. FILTROWANIE LISTY 353
4. Mozna równiez uzyc petli. W zaleznosci od tego, jakim doswiadczeniem programistycznym
dysponujecie, ten sposób moze sie wam wydawac bardziej “bezposredni”,
jednak uzycie funkcji takich jak filter jest znacznie bardziej wyraziste.
Nie tylko jest prostsze w zapisie, jest równiez łatwiejsze w czytaniu. Czytanie
kodu petli przypomina patrzenie na obraz ze zbyt małej odległosci; widzi sie
detale, jednak potrzeba kilku chwil, by móc odsunac sie na tyle, aby dojrzec całe
dzieło: “Och, to tylko filtrowanie listy!”.
Przykład 16.8. funkcja filter w regression.py
files = os.listdir(path) #(1)
test = re.compile("test.py$", re.IGNORECASE) #(2)
files = filter(test.search, files) #(3)
1. Jak widzielismy w podrozdziale 16.2 Znajdowanie sciezki, sciezka (path) moze
zawierac pełna lub czesciowa nazwe sciezki do katalogu, w którym znajduje sie
własnie wykonywany skrypt lub moze zawierac pusty napis, jesli skrypt został
uruchomiony z biezacego katalogu. Jakkolwiek by nie było, na liscie files znajda
sie nazwy plików z tego samego katalogu, w którym znajduje sie uruchomiony
skrypt.
2. Jest to skompilowane wyrazenie regularne. Jak widzielismy w podrozdziale 15.3
sec:Refaktoryzacja]Refaktoryzacja, jesli to samo wyrazenie ma byc uzywane wiecej
niz raz, warto je skompilowac w celu poprawienia wydajnosci programu.
Obiekt powstały w wyniku kompilacji posiada metode search, która pobiera
jeden argument: napis, do którego powinno sie dopasowac wyrazenie regularne.
Jesli dopasowanie nastapi, metoda search zwróci obiekt klasy Match zawierajacy
informacje o sposobie dopasowania wyrazenia regularnego; w przeciwnym
przypadku zwróci None, czyli zdefiniowana w jezyku Python wartosc null.
3. Metoda search powinna zostac wywołana na obiekcie skompilowanego wyrazenia
regularnego dla kazdego elementu na liscie files. Jesli wyrazenie zostanie
dopasowane do elementu, metoda ta zwróci obiekt klasy Match, który ma wartosc
logiczna true, a wiec element zostanie dołaczony do listy wynikowej zwróconej
przez filtr. Jesli wyrazenie nie zostanie dopasowane, metoda search zwróci wartosc
None, która ma wartosc logiczna false, a wiec dopasowywany element nie
zostanie dołaczony do listy wynikowej.
Notatka historyczna. Wersje jezyka Python wczesniejsze niz 2.0 nie obsługiwały
jeszcze wyrazen listowych, a wiec nie mozna było ich uzyc w celu przefiltrowania listy;
istniała jednak funkcja filter. Nawet po wprowadzeniu wyrazen listowych w 2.0 niektóre
osoby wciaz wola uzywac starej metody filtrujacej filter (oraz towarzyszacej
jej funkcji map, o której powiem jeszcze w tym rozdziale). W chwili obecnej działaja
obydwie te techniki, a wybór jednej z nich jest po prostu kwestia stylu. Toczy sie
dyskusja, czy map i filter nie powinny zostac “przedawnione” w przyszłych wersjach
jezyka, jednak dotychczas ostateczna decyzja nie zapadła.
Przykład 16.9. Filtrowanie przy uzyciu wyrazen listowych
files = os.listdir(path)
test = re.compile("test.py$", re.IGNORECASE)
files = [f for f in files if test.search(f)] #(1)
354 ROZDZIAŁ 16. REFAKTORYZACJA
1. Wykonanie tej linii kodu bedzie miało dokładnie ten sam efekt, co uzycie funkcji
filtrujacej. Który sposób jest bardziej ekspresyjny? Wszystko zalezy od was.
16.9. ODWZOROWYWANIE LISTY 355
16.9 Odwzorowywanie listy
Jeszcze o odwzorowywaniu list
Wiecie juz, w jaki sposób uzyc wyrazen listowych w celu odwzorowania jednej listy
w inna. Mozna to osiagnac równiez w inny sposób, uzywajac wbudowanej funkcji map.
Działa ona podobnie do funkcji filter.
Przykład 16.10. Wprowadzenie do funkcji map
>>> def double(n):
... return n*2
...
>>> li = [1, 2, 3, 5, 9, 10, 256, -3]
>>> map(double, li) #(1)
[2, 4, 6, 10, 18, 20, 512, -6]
>>> [double(n) for n in li] #(2)
[2, 4, 6, 10, 18, 20, 512, -6]
>>> newlist = []
>>> for n in li: #(3)
... newlist.append(double(n))
...
>>> newlist
[2, 4, 6, 10, 18, 20, 512, -6]
1. Funkcja map pobiera jako parametry funkcje oraz liste 1, a zwraca nowa liste,
która powstaje w wyniku wywołania funkcji przekazanej w pierwszym parametrze
dla kazdego elementu listy przekazanej w drugim parametrze. W tym przypadku
kazdy element listy został pomnozony przez 2.
2. Ten sam efekt mozna osiagnac wykorzystujac wyrazenia listowe. Wyrazenia listowe
pojawiły sie w jezyku Python w wersji 2.0; funkcja map istniała w jezyku
od zawsze.
3. Jesli bardzo chcecie myslec jak programista Visual Basica, to moglibyscie równiez
do tego celu uzyc petli.
Przykład 16.11. funkcja map z listami zawierajacymi elementy róznych
typów
>>> li = [5, ’a’, (2, ’b’)]
>>> map(double, li) #(1)
[10, ’aa’, (2, ’b’, 2, ’b’)]
1. Chciałbym zwrócic uwage, ze funkcja, której uzywamy jako argumentu map moze
byc bez problemu zastosowana do list zawierajacych elementy róznych typów,
o ile oczywiscie poprawnie obsługuje kazdy z typów, jakie posiadaja elementy
listy. W tym przypadku funkcja double mnozy swój argument przez 2, a Python
wykona w tym momencie operacje własciwa dla typu tego argumentu. W
przypadku wartosci całkowitych oznacza to pomnozenie wartosci przez 2; w przypadku
napisów oznacza to wydłuzenie napisu o samego siebie; dla krotek oznacza
to utworzenie nowej krotki zawierajacej wszystkie elementy krotki oryginalnej, a
po nich ponownie wszystkie elementy krotki oryginalnej.
1Patrz przypis w poprzednim podrozdziale, Filtrowanie listy
356 ROZDZIAŁ 16. REFAKTORYZACJA
Dobra, koniec zabawy. Popatrzmy na kawałek prawdziwego kodu.
Przykład 16.12. Funkcja map w regression.py
filenameToModuleName = lambda f: os.path.splitext(f)[0] #(1)
moduleNames = map(filenameToModuleName, files) #(2)
1. Jak widzielismy w podrozdziale 4.7 Wyrazenia lambda, lambda pozwala na zdefiniowanie
funkcji w locie. W przykładzie 6.17 “Rozdzielanie sciezek” (w podrozdziale
Praca z katalogami), os.path.splitext pobiera nazwe pliku i zwraca
pare (nazwa, rozszerzenie). A wiec funkcja filenameToModuleName bierze nazwe
pliku jako parametr i odcina od niej rozszerzenie, zwracajac nazwe bez rozszerzenia.
2. Wywołanie map spowoduje wywołanie funkcji filenameToModuleName dla kazdego
elementu z listy files , a w rezultacie zwrócenie listy wartosci, jakie powstały
po kazdym z tych wywołan. Innymi słowy, odcinamy rozszerzenie dla
kazdej nazwy pliku i zbieramy powstałe w ten sposób nazwy bez rozszerzen do
listy moduleNames.
Jak zobaczycie w dalszej czesci rozdziału, tego typu myslenie, które jest mocno
skoncentrowane na przetwarzanych danych, mozna rozciagnac przez wszystkie etapy
pisania kodu, az do ostatecznego celu, jakim jest zdefiniowanie i uruchomienie pojedynczego
zestawu testów jednostkowych zawierajacego testy ze wszystkich poszczególnych
przypadków testowych.
16.10. PROGRAMOWANIE KONCENTRUJACE SIE NA DANYCH 357
16.10 Programowanie koncentrujace sie na danych
W tym momencie zastanawiacie sie zapewne, dlaczego takie podejscie moze byc
uznane za lepsze od podejscia, w którym uzywa sie petli i bezposrednich wywołan
funkcji. I to jest bardzo dobre pytanie. Przede wszystkim jest to kwestia przyjecia
pewnej optyki. Uzycie funkcji map oraz filter zmusza do skoncentrowania sie na
przetwarzanych danych.
W tym przypadku zaczelismy od sytuacji, w której w ogóle nie było zadnych danych;
pierwsza rzecza, jaka zrobilismy, było uzyskanie sciezki do katalogu, w którym
znajdował sie uruchomiony skrypt, a kolejna — uzyskanie na tej podstawie listy plików
znajdujacych sie w tym katalogu. W ten sposób zaczelismy i dzieki tym krokom
zdobylismy dane, na których moglismy dalej pracowac: liste nazw plików.
Wiedzielismy jednak, ze nie interesuja nas wszystkie pliki, a jedynie te, które sa
zestawami testów. Mielismy zbyt duzo danych, a wiec potrzebowalismy je jakos przefiltrowac.
Skad wiedzielismy, które dane zachowac? Potrzebowalismy funkcji, która by
to sprawdzała, a która moglismy przekazac do funkcji filtrujacej. W tym akurat przypadku
uzylismy wyrazenia regularnego, ale koncepcja jest wciaz taka sama, niezaleznie
od tego, w jaki sposób skonstruowana została funkcja sprawdzajaca.
Po tym kroku posiadalismy juz liste nazw plików bedacych zestawami testowymi (i
tylko nimi, poniewaz wszystkie inne pliki zostały odfiltrowane), jednak w rzeczywistosci
potrzebowalismy jedynie liste nazw modułów. Mielismy wszystkie dane, jednak były
one w złym formacie. Zdefiniowalismy wiec funkcje, która przekształcała nazwe pliku
w nazwe modułu i kazdy element z listy nazw plików odwzorowalismy przy pomocy
tej funkcji w nazwe modułu, uzyskujac liste nazw modułów. Z kazdej nazwy pliku
powstała jedna nazwa modułu, a z listy nazw plików powstała lista nazw modułów.
Zamiast funkcji filter moglismy uzyc petli for z instrukcja if. Zamiast funkcji
map moglismy uzyc petli for z wywołaniem funkcji. Jednak uzywanie petli w ten sposób
jest zajeciem czasochłonnym. W najlepszym przypadku stracimy niepotrzebnie
czas, a w najgorszym wprowadzimy brzydkie błedy. Na przykład, odpowiadajac na
pytanie: “czy ten plik jest zestawem testów?” zastanawiamy sie nad logika specyficzna
dla danego zastosowania i zaden jezyk programowania nie wyrazi tego za nas. Jednak
kiedy juz wiemy, jak na takie pytanie odpowiedziec, czy naprawde potrzebujemy tego
kłopotliwego tworzenia nowej, pustej listy, napisania petli for i instrukcji if, a nastepnie
recznego wywoływania funkcji append, aby dodac element, który przeszedł przez
test w warunku if do tej listy, a dodatkowo jeszcze sledzenia, jaka zmienna przechowuje
dane juz przefiltrowane, a jaka te, które dopiero beda filtrowane? Dlaczego nie
mielibysmy po prostu zdefiniowac odpowiedniego warunku, a cała reszte zrobi za nas
Python?
Oczywiscie, moglibysmy byc sprytni i nie tworzyc nowej listy, lecz usuwac niepotrzebne
elementy z listy wejsciowej. Ale juz sie na tym sparzylismy: próba modyfikowania
listy, po której własnie iterujemy, moze powodowac błedy. Usuwamy element,
przechodzimy do nastepnego elementu, i tym samym przeskakujemy przez jakis element.
Czy Python to jeden z tych jezyków, w których usuwanie elementów działa w ten
własnie sposób? Ile czasu zajmie nam ustalenie tego faktu? Czy bedziemy pamietac,
czy taka iteracja jest bezpieczna, czy nie, kiedy bedziemy robic to ponownie? Programisci
traca zbyt wiele czasu i popełniaja wiele błedów podczas zajmowania sie takimi
— czysto przeciez technicznymi — kwestiami, co jest przeciez bezcelowe. Nie posuwa
to pracy nad programem ani o jote, tylko niepotrzebnie zajmuje czas.
Kiedy uczyłem sie jezyka Python po raz pierwszy, stroniłem od wyrazen listowych,
a od funkcji map i filter stroniłem jeszcze dłuzej. Upierałem sie, aby moje zycie było
358 ROZDZIAŁ 16. REFAKTORYZACJA
bardziej skomplikowane, poniewaz przylgnałem do znanego mi sposobu programowania
zorientowanego na kod: uzywałem petli for oraz instrukcji warunkowych. Moje programy
przypominały programy pisane w jezyku Visual Basic, przedstawiały bowiem
dokładnie kazdy krok kazdej operacji w kazdej funkcji. W nich wszystkich pojawiały
sie tez wciaz te same, małe problemy i brzydkie błedy. I nie miało to wiekszego sensu.
Zapomnijmy o tym. Szczegółowe rozpisywanie kodu nie jest wazne. Wazne sa dane.
Dane nie sa trudne, to tylko dane. Jesli mamy ich za duzo, przefiltrujmy je. Jesli nie
sa dokładnie takie, jakich sobie zyczymy, uzyjmy odwzorowania map. Skoncentrujmy
sie na danych, a niepotrzebna prace zostawmy za soba.
16.11. DYNAMICZNE IMPORTOWANIE MODUŁÓW 359
16.11 Dynamiczne importowanie modułów
Dynamiczne importowanie modułów
OK, dosc filozofowania. Pogadajmy o dynamicznym importowaniu modułów.
Najpierw zerknijmy jak normalnie importuje sie moduły. Składnia polecenia import
module sprawdza sciezke w poszukiwaniu nazwanego modułu i importuje go po nazwie.
W ten sposób mozna importowac kilka modułów na raz, podajac nazwy modułów
oddzielone przecinkiem. Z reszta, robilismy juz to w pierwszej linii skryptu z tego
rozdziału.
Przykład 16.13. Importowanie wielu modułów na raz
import sys, os, re, unittest #(1)
1. Importowane sa cztery moduły na raz: sys (funkcje systemowe oraz dostepu do
parametrów przekazywanych z linii polecen), os (wykonywanie funkcji systemowych
takich jak np. listowanie katalogów), re (wyrazenia regularne), oraz unittest
(testy jednostkowe).
A teraz zróbmy to samo, jednak przy uzyciu dynamicznego importowania.
Przykład 16.14. Dynamiczne importowanie modułów
>>> sys = __import__(’sys’) #(1)
>>> os = __import__(’os’)
>>> re = __import__(’re’)
>>> unittest = __import__(’unittest’)
>>> sys #(2)
>>> <module ’sys’ (built-in)>
>>> os
>>> <module ’os’ from ’/usr/local/lib/python2.2/os.pyc’>
1. Wbudowana funkcja import robi to samo co uzycie polecenia import, jednak
jest to funkcja rzeczywista, która przyjmuje ciag znaków jako argument.
2. Zmienna sys staje sie modułem sys, to tak jakby napisac import sys. Zmienna
os staje sie modułem os i tak dalej.
Reasumujac, import importuje moduł, jednak aby tego dokonac, pobiera jako
argument ciag znaków. W tym przypadku moduł, który zaimportowalismy był po
prostu na sztywno zakodowanym ciagiem znaków, jednak nic nie stało na przeszkodzie,
aby była to zmienna lub wynik działania funkcji. Zmienna, pod która podstawiamy
moduł, nie musi sie nazywac tak samo jak nazwa modułu, który importujemy. Równie
dobrze moglibysmy zaimportowac szereg modułów i przypisac je do listy.
Przykład 16.15. Dynamiczne importowanie listy modułów
>>> moduleNames = [’sys’, ’os’, ’re’, ’unittest’] #(1)
>>> moduleNames
[’sys’, ’os’, ’re’, ’unittest’]
>>> modules = map(__import__, moduleNames) #(2)
>>> modules #(3)
[<module ’sys’ (built-in)>,
<module ’os’ from ’c:Python22libos.pyc’>,
360 ROZDZIAŁ 16. REFAKTORYZACJA
<module ’re’ from ’c:Python22libre.pyc’>,
<module ’unittest’ from ’c:Python22libunittest.pyc’>]
>>> modules[0].version #(4)
’2.2.2 (#37, Nov 26 2002, 10:24:37) [MSC 32 bit (Intel)]’
>>> import sys
>>> sys.version
’2.2.2 (#37, Nov 26 2002, 10:24:37) [MSC 32 bit (Intel)]’
1. moduleNames jest po prostu lista ciagów znaków. Nic nadzwyczajnego, za wyjatkiem
tego, ze akurat te ciagi znaków sa nazwami modułów, które moglibysmy
zaimportowac, jesli bysmy chcieli.
2. Wyobrazmy sobie, ze chcielismy je zaimportowac, a dokonalismy tego poprzez
mapowanie funkcji import na liste. Pamietajmy jednak, ze kazdy element
listy (moduleNames) bedzie przekazany jako argument do wywołania raz za razem
funkcji ( import ), dzieki czemu zostanie zbudowana i zwrócona lista wartosci
wynikowych
3. Tak wiec z listy ciagów znaków stworzylismy tak na prawde liste rzeczywistych
modułów. (Nasze sciezki moga sie róznic w zaleznosci od systemu operacyjnego,
na którym zainstalowalismy Pythona, faz ksiezyca i innych takich tam.)
4. Aby upewnic sie, ze sa to tak na prawde moduły, zerknijmy na niektóre ich atrybuty.
Pamietajmy, ze modules[0] jest modułem sys, wiec modules[0].version
odpowiada sys.version. Wszystkie pozostałe atrybuty i metody tych modułów sa
takze dostepne. Nie ma nic niezwykłego w poleceniu import, tak samo jak nie
ma nic magicznego w modułach. Moduły sa obiektami. Wszystko jest obiektem.
Teraz juz powinnismy móc wszystko to poskładac do kupy i rozszyfrowac o co tak
na prawde chodzi w kodzie zamieszczonych tutaj przykładów.
Rozdział 17
Programowanie funkcyjne
361
362 ROZDZIAŁ 17. PROGRAMOWANIE FUNKCYJNE
17.1 Programowanie funkcyjne - wszystko razem
Dowiedzieliscie sie juz wystarczajaco duzo, by móc odczytac pierwszych siedem
linii kodu z przykładu podanego na poczatku rozdziału, w którym odczytywane sa
pliki w katalogu a nastepnie importowane wybrane sposród nich moduły.
Przykład 16.16. Funkcja regressionTest
def regressionTest():
path = os.path.abspath(os.path.dirname(sys.argv[0]))
files = os.listdir(path)
test = re.compile("test.py$", re.IGNORECASE)
files = filter(test.search, files)
filenameToModuleName = lambda f: os.path.splitext(f)[0]
moduleNames = map(filenameToModuleName, files)
modules = map(__import__, moduleNames)
load = unittest.defaultTestLoader.loadTestsFromModule
return unittest.TestSuite(map(load, modules))
Spójrzmy na ten kod w sposób interaktywny, linia po linii. Załózmy, ze katalogiem
biezacym jest c:diveintopythonpy, w którym znajduja sie przykłady dołaczone do tej
ksiazki, z omawianym w tym rozdziale skryptem włacznie. Jak widzielismy w podrozdziale
16.2 Znajdowanie sciezki, nazwa katalogu, w którym znajduje sie skrypt, trafi
do zmiennej path, spróbujmy wiec te czesc zakodowac na sztywno, po czym dopiero
przejsc dalej.
Przykład 16.17. Krok 1: Pobieranie wszystkich plików
>>> import sys, os, re, unittest
>>> path = r’c:diveintopythonpy’
>>> files = os.listdir(path)
>>> files #(1)
[’BaseHTMLProcessor.py’, ’LICENSE.txt’, ’apihelper.py’, ’apihelpertest.py’,
’argecho.py’, ’autosize.py’, ’builddialectexamples.py’, ’dialect.py’,
’fileinfo.py’, ’fullpath.py’, ’kgptest.py’, ’makerealworddoc.py’,
’odbchelper.py’, ’odbchelpertest.py’, ’parsephone.py’, ’piglatin.py’,
’plural.py’, ’pluraltest.py’, ’pyfontify.py’, ’regression.py’, ’roman.py’,
’romantest.py’, ’uncurly.py’, ’unicode2koi8r.py’, ’urllister.py’, ’kgp’,
’plural’, ’roman’, ’colorize.py’]
1. files jest lista nazw wszystkich plików i katalogów znajdujacych sie w katalogu,
z którego pochodzi skrypt. (Jesli wczesniej uruchamialiscie juz jakies przykłady,
na liscie mozecie równiez zauwazyc pliki .pyc)
Przykład 16.18. Krok 2: Filtrowanie w celu znalezienia interesujacych
plików
>>> test = re.compile("test.py$", re.IGNORECASE) #(1)
>>> files = filter(test.search, files) #(2)
>>> files #(3)
[’apihelpertest.py’, ’kgptest.py’, ’odbchelpertest.py’, ’pluraltest.py’,
’romantest.py’]
17.1. PROGRAMOWANIE FUNKCYJNE - WSZYSTKO RAZEM 363
1. Wyrazenie regularne zostanie dopasowane do kazdego napisu zakonczonego na
test.py. Zauwazcie, ze kropka musi zostac poprzedzona sekwencja unikowa; w
wyrazeniach regularnych kropka oznacza “dopasuj dowolny znak”, jednak nam
zalezy na dosłownym dopasowaniu znaku kropki.
2. Skompilowane wyrazenie regularne działa jak funkcja, a wiec mozemy jej uzyc
do przefiltrowania długiej listy nazw plików i katalogów, dzieki czemu uzyskamy
liste nazw, do których zostało dopasowane wyrazenie.
3. Otrzymalismy wiec liste skryptów bedacych testami jednostkowymi, poniewaz
tylko one maja nazwe JAKASNAZWAtest.py.
Przykład 16.19. Krok 3: Odwzorowanie nazw plików na nazwy modułów
>>> filenameToModuleName = lambda f: os.path.splitext(f)[0] #(1)
>>> filenameToModuleName(’romantest.py’) #(2)
’romantest’
>>> filenameToModuleName(’odchelpertest.py’)
’odbchelpertest’
>>> moduleNames = map(filenameToModuleName, files) #(3)
>>> moduleNames #(4)
[’apihelpertest’, ’kgptest’, ’odbchelpertest’, ’pluraltest’, ’romantest’]
1. Jak widzielismy w podrozdziale 4.7 Wyrazenia lambda, lambda pozwala na szybkie
zdefiniowanie jednolinijkowych funkcji w locie. Tutaj funkcja lambda pobiera
nazwe pliku wraz z rozszerzeniem i zwraca czesc nazwy bez rozszerzenia, uzywajac
do tego funkcji os.path.splitext z biblioteki standardowej, która poznalismy
w przykładzie 6.17 “Rozdzielanie sciezek” w podrozdziale Praca z katalogami.
2. filenameToModuleName jest funkcja. W porównaniu ze zwykłymi funkcjami,
które tworzy sie przy pomocy instrukcji def, w funkcjach lambda nie ma niczego
magicznego. Mozemy wywołac filenameToModuleName jak kazda inna funkcje,
a robi ona dokładnie to, czego potrzebujemy: odcina rozszerzenie z napisu przekazanego
jej w parametrze wejsciowym.
3. Tutaj mozemy wywołac te funkcje na kazdej nazwie pliku znajdujacej sie na liscie
plików bedacych testami jednostkowymi. Uzywamy do tego funkcji map.
4. W wyniku otrzymujemy to, czego oczekiwalismy: liste nazw modułów.
Przykład 16.20. Krok 4: Odwzorowanie nazw modułów na moduły
>>> modules = map(__import__, moduleNames) #(1)
>>> modules #(2)
[<module ’apihelpertest’ from ’apihelpertest.py’>,
<module ’kgptest’ from ’kgptest.py’>,
<module ’odbchelpertest’ from ’odbchelpertest.py’>,
<module ’pluraltest’ from ’pluraltest.py’>,
<module ’romantest’ from ’romantest.py’>]
>>> modules[-1] #(3)
<module ’romantest’ from ’romantest.py’>
364 ROZDZIAŁ 17. PROGRAMOWANIE FUNKCYJNE
1. Jak widzielismy w podrozdziale 16.6 Dynamiczne importowanie modułów, w celu
odwzorowania listy nazw modułów (napisów) na własciwe moduły (obiekty wywoływalne,
do których mozna miec dostep jak do jakichkolwiek innych modułów),
mozna uzyc funkcji map oraz import .
2. modules to teraz lista modułów, do których mozna miec taki sam dostep, jak do
jakichkolwiek innych modułów.
3. Ostatnim modułem na liscie jest moduł romantest, tak jak gdybysmy napisali:
import romantest
Przykład 16.21. Krok 5: Ładowanie modułów do zestawu testów
>>> load = unittest.defaultTestLoader.loadTestsFromModule
>>> map(load, modules) #(1)
[<unittest.TestSuite tests=[
<unittest.TestSuite tests=[<apihelpertest.BadInput testMethod=testNoObject>]>,
<unittest.TestSuite tests=[<apihelpertest.KnownValues testMethod=testApiHelper>]>,
<unittest.TestSuite tests=[
<apihelpertest.ParamChecks testMethod=testCollapse>,
<apihelpertest.ParamChecks testMethod=testSpacing>]>,
...
]
]
>>> unittest.TestSuite(map(load, modules)) #(2)
1. To sa prawdziwe obiekty modułów. Nie tylko mamy do nich dostep taki, jak
do innych modułów i mozemy tworzyc instancje klas oraz wywoływac funkcje,
mamy równiez mozliwosc introspekcji (wgladu) w moduł, której mozemy uzyc
przede wszystkim do tego, aby dowiedziec sie, jakie klasy i funkcje dany moduł
posiada. To własnie robi metoda loadTestsFromModule: dokonuje introspekcji, a
nastepnie dla kazdego modułu zwraca obiekt unittest.TestSuite. Kazdy taki
obiekt zawiera liste obiektów unittest.TestSuite, po jednym dla kazdej klasy
dziedziczacej po TestCase zdefiniowanej w module. Kazdy z obiektów na tej
liscie zawiera z kolei liste metod testowych zdefiniowanych w klasie testowej.
2. Na koncu umieszczamy liste obiektów TestSuite wewnatrz jednego, duzego zestawu
testów. Moduł unittest nie ma problemów z przechodzeniem po drzewie
zestawów testowych zagniezdzonych w zestawach testowych; dotrze on do kazdej
metody testowej i ja wywoła, sprawdzajac, czy przeszła, czy nie, a nastepnie
przejdzie do kolejnej metody.
Moduł unittest zwykle przeprowadza za nas proces introspekcji. Czy pamietacie
magiczna funkcje unittest.main(), która wywoływały poszczególne moduły, aby
odpalic wszystkie znajdujace sie w nich testy? Metoda unittest.main() w rzeczywistosci
tworzy instancje klasy unittest.TestProgram, która z kolei tworzy instancje
unittest.defaultTestLoader, słuzaca do załadowania modułu, z którego została wywołana.
(Skad jednak ma referencje do modułu, z którego została wywołana, jesli nie
dostała jej od nas? Otóz dzieki równie magicznemu poleceniu import (’ main ’),
które dynamicznie importuje wykonywany własnie moduł. Mógłbym napisac cała ksiazke
17.1. PROGRAMOWANIE FUNKCYJNE - WSZYSTKO RAZEM 365
na temat trików i technik uzywanych w module unittest, ale chyba bym jej nie skonczył.)
Przykład 16.22. Krok 6: Powiedziec modułowi unittest, aby uzył naszego
zestawu testowego
if __name__ == "__main__":
unittest.main(defaultTest="regressionTest") #(1)
1. Zamiast pozwalac modułowi unittest wykonac cała magie za nas, wiekszosc
zrobilismy sami. Utworzylismy funkcje (regressionTest), która sama importuje
moduły i woła unittest.defaultTestLoader, a nastepnie utworzylismy
duzy zestaw testów. Teraz potrzebujemy jedynie, aby unittest, zamiast szukac
testów i budowac zestaw testowy w standardowy sposób, uruchomił funkcje
regressionTest, która zwróci gotwy do uzycia obiekt testSuite.
366 ROZDZIAŁ 17. PROGRAMOWANIE FUNKCYJNE
17.2 Programowanie funkcyjne - podsumowanie
Program regression.py i wynik jego działania powinien byc teraz całkiem zrozumiały.
Powinniscie tez bez kłopotu wykonywac nastepujace zadania:
• Przekształcanie informacji o sciezce otrzymanej z linii polecen
• Filtrowanie list przy uzyciu metody filter zamiast uzywania wyrazen listowych
• Odwzorowywanie list przy uzyciu metody map zamiast uzywania wyrazen listowych
• Dynamiczne importowanie modułów
Rozdział 18
Funkcje dynamiczne
367
368 ROZDZIAŁ 18. FUNKCJE DYNAMICZNE
18.1 Funkcje dynamiczne
Nurkujemy
Chce teraz opowiedziec o rzeczownikach w liczbie mnogiej. Takze o funkcjach zwracajacych
inne funkcje, o zaawansowanych wyrazeniach regularnych oraz o generatorach,
które pojawiły sie w jezyku Python w wersji 2.3. Zaczne jednak od tego, w jaki
sposób tworzy sie rzeczowniki w liczbie mnogiej.
Jesli jeszcze nie przeczytaliscie rozdziału 7 (Wyrazenia regularne), nadszedł doskonały
moment, aby to zrobic. W tym rozdziale chce szybko przejsc do bardziej
zaawansowanego uzycia wyrazen regularnych, zakładam wiec, ze dobrze rozumiecie
podstawy.
Jezyk angielski jest jezykiem schizofrenicznym, który sporo zapozyczył z innych
jezyków; zasady tworzenia rzeczowników w liczbie mnogiej na podstawie liczby pojedynczej
sa zróznicowane i złozone. Istnieja pewne zasady, jednak istnieja równiez
wyjatki od tych zasad, a nawet wyjatki od tych wyjatków.
Jesli dorastaliscie w kraju, w którym mówi sie po angielsku lub uczyliscie sie angielskiego
w czasie, gdy chodziliscie do szkoły, ponizsze reguły powinny byc wam dobrze
znane:
1. Jesli słowo konczy sie na S, X lub Z, nalezy dodac ES. “Bass” staje sie “basses”,
“fax” staje sie “faxes” a “waltz” staje sie “waltzes”.
2. Jesli słowo konczy sie na dzwieczne H, nalezy dodac ES; jesli konczy sie na nieme
H, nalezy dodac samo S. Co to jest “dzwieczne H”? Takie, które po połaczeniu
z innymi głoskami mozna usłyszec. A wiec “coach” staje sie “coaches” a “rash”
staje sie “rashes”, poniewaz głoski CH i SH sa dzwieczne. Jednak “cheetah” staje
sie “cheetahs”, poniewaz wystepuje tutaj H bezdzwieczne.
3. Jesli słowo konczy sie na Y, które brzmi jak I, nalezy zmienic Y na IES; jesli Y
jest połaczony z głoska, która brzmi inaczej, nalezy dodac S. A wiec “vacancy”
staje sie “vacancies”, ale “day” staje sie “days”.
4. Jesli wszystko zawiedzie, nalezy dodac S i miec nadzieje, ze sie uda.
(Wiem, jest mnóstwo wyjatków. “Man” staje sie “men” a “woman” staje sie “women”,
jednak “human” staje sie “humans”. “Mouse” staje sie “mice”,a “louse” staje
sie “lice”, jednak “house” staje sie “houses”. “Knife” staje sie “knives” a “wife” staje
sie “wives”, jednak “lowlife” staje sie “lowlifes”. Nie mówcie mi nawet o słowach, które
same w sobie oznaczaja liczbe mnoga, jak “sheep”, “deer” czy “haiku”.)
W innych jezykach wyglada to oczywiscie zupełnie inaczej.
Zaprojektujemy wiec moduł, który dla kazdego rzeczownika utworzy odpowiedni
rzeczownik w liczbie mnogiej. Zaczniemy od rzeczowników w jezyku angielskim i od
powyzszych czterech zasad, jednak musimy miec na uwadze, ze obsługiwanie nowych
reguł (a nawet nowych jezyków) jest nieuniknione.
18.2. PLURAL.PY, ETAP 1 369
18.2 plural.py, etap 1
Patrzymy na słowa, które — przynajmniej w jezyku angielskim — składaja sie
z liter. Dysponujemy tez regułami, które mówia, ze musimy znalezc w słowie pewne
kombinacje liter, a nastepnie odpowiednio to słowo zmodyfikowac. Brzmi to dokładnie
jak zadanie dla wyrazen regularnych.
Przykład 17.1. plural1.py
import re
def plural(noun):
if re.search(’[sxz]$’, noun): #(1)
return re.sub(’$’, ’es’, noun) #(2)
elif re.search(’[^aeioudgkprt]h$’, noun):
return re.sub(’$’, ’es’, noun)
elif re.search(’[^aeiou]y$’, noun):
return re.sub(’y$’, ’ies’, noun)
else:
return noun + ’s’
1. Rzeczywiscie, jest to wyrazenie regularne, jednak uzywa ono składni, jakiej w
rozdziale 7 (Wyrazenia regularne) nie widzieliscie. Nawias kwadratowy oznacza:
“dopasuj dokładnie jeden z wymienionych tu znaków”. A wiec [sxz] oznacza “s
albo x, albo z”, ale tylko jeden znak na raz. Znak $ powinien byc wam znany;
dopasowuje sie on do konca napisu. A wiec sprawdzamy tutaj, czy rzeczownik
konczy sie na jedna z liter s, x lub z.
2. Funkcja re.sub dokonuje podstawienia w oparciu o wyrazenie regularne. Przyjrzyjmy
sie jej blizej.
Przykład 17.2. Wprowadzenie funkcji re.sub
>>> import re
>>> re.search(’[abc]’, ’Mark’) #(1)
<_sre.SRE_Match object at 0x001C1FA8>
>>> re.sub(’[abc]’, ’o’, ’Mark’) #(2)
’Mork’
>>> re.sub(’[abc]’, ’o’, ’rock’) #(3)
’rook’
>>> re.sub(’[abc]’, ’o’, ’caps’) #(4)
’oops’
1. Czy napis Mark zawiera jedna z liter a, b lub c? Tak, zawiera a.
2. W porzadku; znajdz a, b lub c i zastap je litera o. Mark zmienia sie w Mork.
3. Ta sama funkcja zmienia rock w rook.
4. Moze sie wydawac, ze ta linia zmieni caps w oaps, jednak dzieje sie inaczej. Funkcja
re.sub zastepuje wszystkie wystapienia, nie tylko pierwsze. Caps zmienia sie
w oops poniewaz zarówno c jak i a zostaja zastapione litera o.
Przykład 17.3. Z powrotem do plural1.py
370 ROZDZIAŁ 18. FUNKCJE DYNAMICZNE
import re
def plural(noun):
if re.search(’[sxz]$’, noun):
return re.sub(’$’, ’es’, noun) #(1)
elif re.search(’[^aeioudgkprt]h$’, noun): #(2)
return re.sub(’$’, ’es’, noun) #(3)
elif re.search(’[^aeiou]y$’, noun):
return re.sub(’y$’, ’ies’, noun)
else:
return noun + ’s’
1. Wrócmy do funkcji plural. Co robimy? Zamieniamy koncówke napisu na “es”.
Innymi słowy, dodajemy “es” do napisu. Moglibysmy osiagnac ten cel uzywajac
dodawania napisów, na przykład stosujac wyrazenie: rzeczownik + “es”, jednak
tutaj wyrazen regularnych bede uzywał do wszystkiego, ze wzgledu na spójnosc
oraz z innych powodów, które zostana wyjasnione w dalszej czesci rozdziału.
2. Patrzcie uwazne, to kolejna nowosc. Znak ˆ znajdujacy sie wewnatrz nawiasów
kwadratowych oznacza cos szczególnego: negacje. [ˆabc] oznacza “dowolny znak
oprócz a, b oraz c”. Wyrazenie [ˆaeioudgkprt] oznacza ”kazdy znak za wyjatkiem
a, e, i, o, u, d, g, k, p, r oraz t. Po tym znaku powinien znalezc sie znak h konczacy
napis. Tutaj szukamy słów konczacych sie na H, które mozna usłyszec.
3. Tutaj podobnie: dopasowujemy słowa konczace sie na Y, przy czym znak stojacy
przed Y musi byc dowolnym znakiem za wyjatkiem a, e, i, o oraz u. Szukamy
słów, które koncza sie na Y i brzmia jak I.
Przykład 17.4. Wiecej na temat negacji w wyrazeniach regularnych
>>> import re
>>> re.search(’[^aeiou]y$’, ’vacancy’) #(1)
<_sre.SRE_Match object at 0x001C1FA8>
>>> re.search(’[^aeiou]y$’, ’boy’) #(2)
>>>
>>> re.search(’[^aeiou]y$’, ’day’)
>>>
>>> re.search(’[^aeiou]y$’, ’pita’) #(3)
>>>
1. Wyrazenie zostanie dopasowane do “vacancy” poniewaz słowo to konczy sie na
cy, a c nie jest a, e, i, o ani u.
2. Nie zostanie dopasowane do “boy”, który konczy sie na oy, a powiedzielismy
wyraznie, ze znakiem stojacym przed y nie moze byc o. “day” nie zostanie dopasowane,
poniewaz konczy sie na ay.
3. “pita” równiez nie zostanie dopasowana, poniewaz nie konczy sie na y.
Przykład 17.5. Wiecej na temat re.sub
18.2. PLURAL.PY, ETAP 1 371
>>> re.sub(’y$’, ’ies’, ’vacancy’) #(1)
’vacancies’
>>> re.sub(’y$’, ’ies’, ’agency’)
’agencies’
>>> re.sub(’([^aeiou])y$’, r’1ies’, ’vacancy’) #(2)
’vacancies’
1. To wyrazenie regularne przekształca “vacancy” w “vacancies” oraz “agency” w
“agencies”, dokładnie tak, jak chcemy. Zauwazmy, ze wyrazenie to przekształciłoby
“boy” w “boies”, gdyby nie fakt, ze w funkcji uzylismy wczesniej re.search,
aby dowiedziec sie, czy powinnismy równiez dokonac podstawienia przy uzyciu
re.sub.
2. Chciałbym nadmienic mimochodem, ze mozliwe jest połaczenie tych dwóch wyrazen
regularnych (jednego, które sprawdza, czy pewna zasada ma zastosowanie,
i drugiego, które faktycznie te zasade stosuje) w jedno. Wygladałoby ono dokładnie
tak. Wiekszosc powinna byc juz wam znana: aby zapamietac znak, który
stoi przed y, uzywamy zapamietanej grupy, o której była mowa w podrozdziale
7.6 (Analiza przypadku: Przetwarzanie numerów telefonów). W podstawianym
napisie pojawia sie nowa składnia, 1, które oznacza: “czy pamietasz grupe numer
1? wstaw ja tutaj”. W tym przypadku jako znak stojacy przed y zostanie
zapamietane c, po czym dokonane zostanie podstawienie c w miejsce c oraz ies
w miejsce y. (Jesli potrzebujemy wiecej niz jednej zapamietanej grupy, uzywamy
2, 3 itd.)
Podstawienia wyrazen regularnych stanowia niezwykle silny mechanizm, a składnia
1 czyni go jeszcze silniejszym. Z drugiej strony przedstawienie całej operacji w
postaci jednego wyrazenia regularnego sprawiłoby, ze stałaby sie ona mało czytelna i
niewiele by miała wspólnego ze sposobem, w jaki na poczatku opisywalismy sposób
konstruowania liczby mnogiej. Utworzylismy reguły takie jak “jesli słowo konczy sie
na S, X lub Z, dodaj ES” i kiedy teraz patrzymy na funkcje plural, widzimy dwie
linijki kodu, które mówia “jesli słowo konczy sie na S, X lub Z, dodaj ES”. Nie mozna
tego zrobic bardziej bezposrednio.
372 ROZDZIAŁ 18. FUNKCJE DYNAMICZNE
18.3 plural.py, etap 2
Dodamy teraz warstwe abstrakcji. Zaczelismy od zdefiniowania listy reguł: jesli
jest tak, wtedy zrób tak, w przeciwnym przypadku idz do nastepnej reguły. Teraz
skomplikujemy pewna czesc programu po to, by móc uproscic inna.
Przykład 17.6. plural2.py
import re
def match_sxz(noun):
return re.search(’[sxz]$’, noun)
def apply_sxz(noun):
return re.sub(’$’, ’es’, noun)
def match_h(noun):
return re.search(’[^aeioudgkprt]h$’, noun)
def apply_h(noun):
return re.sub(’$’, ’es’, noun)
def match_y(noun):
return re.search(’[^aeiou]y$’, noun)
def apply_y(noun):
return re.sub(’y$’, ’ies’, noun)
def match_default(noun):
return 1
def apply_default(noun):
return noun + ’s’
rules = ((match_sxz, apply_sxz),
(match_h, apply_h),
(match_y, apply_y),
(match_default, apply_default)
) #(1)
def plural(noun):
for matchesRule, applyRule in rules: #(2)
if matchesRule(noun): #(3)
return applyRule(noun) #(4)
1. Choc ta wersja jest bardziej skomplikowana (z pewnoscia jest dłuzsza), robi
ona dokładnie to samo: próbuje dopasowac kolejno cztery reguły, a nastepnie,
jesli dopasowanie sie powiedzie, stosuje ona odpowiednie wyrazenie regularne.
Róznica polega na tym, ze kazda reguła dopasowujaca oraz modyfikujaca jest
zdefiniowana w swojej własnej funkcji, przy czym funkcje te zostały zebrane w
zmiennej rules, która jest krotka krotek.
18.3. PLURAL.PY, ETAP 2 373
2. Uzywajac petli for, mozemy z krotki rules wyciagac po dwie reguły na raz
(jedna dopasowujaca i jedna modyfikujaca). Podczas pierwszej iteracji petli for
matchesRule przyjmie wartosc match sxz, a applyRule wartosc apply sxz.
Podczas drugiej iteracji (jesli taka nastapi), matchesRule przyjmie wartosc match h,
a applyRule przyjmie wartosc apply h.
3. Pamietajcie, ze w jezyku Python wszystko jest obiektem, nawet funkcje. Krotka
rules składa sie z dwuelementowych krotek zawierajacych funkcje. Nie sa to nazwy
funkcji, lecz rzeczywiscie funkcje.Wpetli sa one przypisywane do applyRule
oraz matchesRule, które staja sie funkcjami, a wiec obiektami, które mozna wywołac.
W tym miejscu podczas w pierwszej iteracji petli zostanie wykonany kod
równowazny wywołaniu: matches sxz(noun).
4. W tym zas miejscu podczas pierwszej iteracji petli for zostanie wykonany kod
równowazmy wywołaniu apply sxz(noun).
Jesli ten dodatkowy poziom abstrakcji wydaje sie zagmatwany, spróbujmy “odwikłac”
powyzsza funkcje w celu lepszego uwidocznienia równowaznosci. Petla w funkcji
plural jest równowazna nastepujacej petli:
Przykład 17.7. Rozwikływanie funkcji plural
def plural(noun):
if match_sxz(noun):
return apply_sxz(noun)
if match_h(noun):
return apply_h(noun)
if match_y(noun):
return apply_y(noun)
if match_default(noun):
return apply_default(noun)
Zysk jest taki, ze funkcja plural znacznie sie uprosciła. Bierze ona liste reguł zdefiniowanych
w innym miejscu i w sposób bardzo ogólny iteruje po nich: bierze regułe
dopasowujaca; czy reguła pasuje? Jesli tak, wywołuje regułe modyfikujaca. Reguły
moga byc zdefiniowane w innym miejscu, w dowolny sposób. Funkcji plural pochodzenie
reguł nie interesuje.
Zastanówmy sie, czy warto było wprowadzac te warstwe abstrakcji. Raczej nie.
Zastanówmy sie, co musielibysmy zrobic, aby dodac do funkcji nowa regułe. Cóz, w
poprzednim podrozdziale e do funkcji plural nalezałoby dodac instrukcje if. W tym
podrozdziale nalezałoby dodac dwie funkcje, macth foo i apply foo, a nastepnie zaktualizowac
liste reguł wstawiajac je w takim miejscu, zeby w stosunku do innych reguł
zostały one wywołane w odpowiedniej kolejnosci.
Tak naprawde to było tylko wprowadzenie do kolejnego podrozdziału. Idzmy wiec
dalej.
374 ROZDZIAŁ 18. FUNKCJE DYNAMICZNE
18.4 plural.py, etap 3
Zauwazmy, ze definiowanie osobnych, nazwanych funkcji dla kazdej reguły dopasowujacej
i modyfikujacej nie jest tak naprawde konieczne. Nigdy nie wywołujemy tych
funkcji bezposrednio; definiujemy je w krotce rules i wywołujemy je równiez przy
uzyciu tej krotki. Spróbujmy zatem przekształcic je do funkcji anonimowych.
Przykład 17.8. plural3.py
import re
rules =
(
(
lambda word: re.search(’[sxz]$’, word),
lambda word: re.sub(’$’, ’es’, word)
),
(
lambda word: re.search(’[^aeioudgkprt]h$’, word),
lambda word: re.sub(’$’, ’es’, word)
),
(
lambda word: re.search(’[^aeiou]y$’, word),
lambda word: re.sub(’y$’, ’ies’, word)
),
(
lambda word: re.search(’$’, word),
lambda word: re.sub(’$’, ’s’, word)
)
) #(1)
def plural(noun):
for matchesRule, applyRule in rules: #(2)
if matchesRule(noun):
return applyRule(noun)
1. To ten sam zestaw reguł, który widzielismy na etapie 2. Jedyna róznica polega na
tym, ze zamiast definiowac funkcje nazwane, takie jak match sxz czy apply sxz,
właczylismy tresc tych funkcji bezposrednio do zmiennej rules uzywajac funkcji
lambda.
2. Zauwazmy, ze funkcja plural w ogóle sie nie zmieniła. Iteruje ona po zestawie
funkcji reprezentujacych reguły, sprawdza pierwsza regułe, a jesli zwróci ona wartosc
true, wywołuje ona druga regułe i zwraca jej wynik. Dokładnie tak samo,
jak wczesniej. Jedyna róznica polega teraz na tym, ze funkcje z regułami zostały
zdefiniowane “inline”, jako funkcje anonimowe, przy uzyciu funkcji lambda. Jednak
dla funkcji plural sposób zdefiniowania funkcji nie ma zadnego znaczenia;
otrzymuje ona liste reguł i wykonuje na niej swoja prace.
Aby dodac nowa regułe tworzenia liczby mnogiej, wystarczy zdefiniowac nowe
funkcje (regułe dopasowujaca oraz regułe modyfikujaca) bezposrednio w samej krotce
18.4. PLURAL.PY, ETAP 3 375
rules. Teraz jednak, kiedy mamy zdefiniowac reguły wewnatrz krotki, widzimy wyraznie,
ze pojawiły sie liczne niepotrzebne powtórzenia kodu. Mamy bowiem cztery
pary funkcji, a kazda z nich jest napisana według tego samego wzorca. Funkcje dopasowujace
składaja sie z pojedynczego wywołania re.search, a funkcje modyfikujace
— z wywołania re.sub. Spróbujmy zrefaktoryzowac te podobienstwa.
376 ROZDZIAŁ 18. FUNKCJE DYNAMICZNE
18.5 plural.py, etap 4
Aby definiowanie nowych reguł było prostsze, spróbujemy usunac z kodu wystepujace
tam powtórzenia.
Przykład 17.9. plural4.py
import re
def buildMatchAndApplyFunctions((pattern, search, replace)):
matchFunction = lambda word: re.search(pattern, word) #(1)
applyFunction = lambda word: re.sub(search, replace, word) #(2)
return (matchFunction, applyFunction) #(3)
1. buildMatchAndApplyFunctions to funkcja, której zadaniem jest dynamiczne konstruowanie
innych funkcji. Pobiera ona trzy parametry: pattern, search oraz
replace (własciwie pobiera jeden parametr bedacy krotka, ale o tym za chwile),
dzieki którym mozna zbudowac funkcje dopasowujaca przy uzyciu składni lambda
tak, aby pobierała ona jeden parametr (word), a nastepnie wywoływała re.search
z wzorcem (pattern) przekazanym do funkcji buildMatchAndApplyFunctions
oraz z parametrem word przekazanym do własnie budowanej funkcji dopasowujacej.
Ojej.
2. W taki sam sposób odbywa sie budowanie funkcji modyfikujacej. Funkcja modyfikujaca
pobiera jeden parametr i wywołuje re.sub z parametrami search i
replace przekazanymi do funkcji buildMatchAndApplyFunctions oraz parametrem
word przekazanym do własnie budowanej funkcji modyfikujacej. Pokazana
tutaj technika uzywania wartosci zewnetrznych parametrów w funkcjach budowanych
dynamicznie nosi nazwe dopełnien (ang. closures). W gruncie rzeczy,
podczas budowania funkcji modyfikujacej, zdefiniowane zostaja dwie stałe: funkcja
pobiera jeden parametr (word), jednak dodatkowo uzywa ona dwóch innych
wartosci (search oraz replace), które zostaja ustalone w momencie definiowania
funkcji modyfikujacej.
3. Na koncu funkcja buildMatchAndApplyFunctions zwraca krotke zawierajaca
dwie wartosci: dwie własnie utworzone funkcje. Stałe, które zostały zdefiniowane
podczas ich budowania (pattern w matchFunction oraz search i replace w
applyFunction) pozostaja zapamietane w tych funkcjach, nawet po powrocie z
funkcji buildMatchAndApplyFunctions. To szalenie fajna sprawa.
Jesli jest to wciaz niesamowicie zagmatwane (powinno byc, bo rzecz jest złozona),
spróbujmy zobaczyc z bliska, jak tego uzyc — moze sie odrobine wyjasni.
Przykład 17.10. Ciag dalszy plural4.py
patterns =
(
(’[sxz]$’, ’$’, ’es’),
(’[^aeioudgkprt]h$’, ’$’, ’es’),
(’(qu|[^aeiou])y$’, ’y$’, ’ies’),
(’$’, ’$’, ’s’)
) #(1)
rules = map(buildMatchAndApplyFunctions, patterns) #(2)
18.5. PLURAL.PY, ETAP 4 377
1. Nasze reguły tworzenia liczby mnogiej sa teraz zdefiniowane jako seria napisów
(nie funkcji). Pierwszy napis to wyrazenie regularne, które zostanie uzyte w funkcji
re.search w celu zbadania, czy reguła pasuje do zadanego rzeczownika; drugi
i trzeci napis to parametry search oraz replace funkcji re.sub, która zostanie
uzyta w ramach funkcji modyfikujacej do zmiany zadanego rzeczownika w
odpowiednia postac liczby mnogiej.
2. To jest magiczna linijka. Pobiera ona liste napisów jako parametr patterns, a nastepnie
przekształca je w liste funkcji. W jaki sposób? Otóz przez odwzorowanie
listy napisów na liste funkcji przy uzyciu funkcji buildMatchAndApplyFunctions,
która, tak sie akurat składa, pobiera trzy napisy jako parametr i zwraca krotke
zawierajaca dwie funkcje. Oznacza to, ze zmienna rules bedzie miała ostatecznie
taka sama wartosc, jak w poprzednim przykładzie: liste dwuelementowych
krotek, sposród których kazda zawiera dwie funkcje: pierwsza jest funkcja dopasowujaca,
która wywołuje re.search a druga jest funkcja modyfikujaca, która
wywołuje re.sub.
Przysiegam, ze nie zmyslam: zmienna rules bedzie zawierała dokładnie taka sama
liste funkcji, jaka zawierała w poprzednim przykładzie. Odwikłajmy definicje zmiennej
rules i sprawdzmy:
Przykład 17.11. Odwikłanie definicji zmiennej rules
rules =
(
(
lambda word: re.search(’[sxz]$’, word),
lambda word: re.sub(’$’, ’es’, word)
),
(
lambda word: re.search(’[^aeioudgkprt]h$’, word),
lambda word: re.sub(’$’, ’es’, word)
),
(
lambda word: re.search(’[^aeiou]y$’, word),
lambda word: re.sub(’y$’, ’ies’, word)
),
(
lambda word: re.search(’$’, word),
lambda word: re.sub(’$’, ’s’, word)
)
)
Przykład 17.12. Dokonczenie plural4.py
def plural(noun):
for matchesRule, applyRule in rules: #(1)
if matchesRule(noun):
return applyRule(noun)
1. Poniewaz lista reguł zaszyta w zmiennej rules jest dokładnie taka sama, jak
w poprzednim przykładzie, nikogo nie powinno dziwic, ze funkcja plural nie
378 ROZDZIAŁ 18. FUNKCJE DYNAMICZNE
zmieniła sie. Pamietajmy, ze jest ona zupełnie ogólna; pobiera liste funkcji definiujacych
reguły i wywołuje je w podanej kolejnosci. Nie ma dla niej znaczenia,
w jaki sposób reguły zostały zdefiniowane. Na etapie 2 były to osobno zdefiniowane
funkcje nazwane. Na etapie 3 były zdefiniowane jako anonimowe funkcje
lambda. Teraz, na etapie 4, sa one budowane dynamicznie poprzez odwzorowanie
listy napisów przy uzyciu funkcji buildMatchAndApplyFunctions. Nie ma
to znaczenia; funkcja plural działa cały czas tak samo.
Na wypadek, gdyby jeszcze nie bolała was od tego głowa, przyznam sie, ze w definicji
funkcji buildMatchAndApplyFunctions znajduje sie pewna subtelnosc, o której
dotychczas nie mówiłem. Wrócmy na chwile i przyjrzyjmy sie blizej:
Przykład 17.13. Blizsze spotkanie z funkcja buildMatchAndApplyFunctions
def buildMatchAndApplyFunctions((pattern, search, replace)): #(1)
1. Zauwazyliscie podwójne nawiasy? Funkcja ta tak naprawde nie przyjmuje trzech
parametrów; przyjmuje ona dokładnie jeden parametr bedacy trzyelementowa
krotka. W momencie wywoływania funkcji, krotka ta jest rozwijana, a trzy elementy
krotki sa przypisywane do trzech róznych zmiennych o nazwach pattern,
search oraz replace. Jestescie juz zagubieni? Zobaczmy to w działaniu.
Przykład 17.14. Rozwijanie krotek podczas wywoływania funkcji
>>> def foo((a, b, c)):
... print c
... print b
... print a
>>> parameters = (’apple’, ’bear’, ’catnap’)
>>> foo(parameters) #(1)
catnap
bear
apple
1. Poprawnym sposobem wywołania funkcji foo jest przekazanie jej trzyelementowej
krotki. W momencie wywoływania tej funkcji, elementy krotki sa przypisywane
róznym zmiennym lokalnym wewnatrz funkcji foo.
Wrócmy na chwile do naszego programu i sprawdzmy, dlaczego trik w
postaci automatycznego rozwijania krotek był w ogóle potrzebny. Zauwazmy,
ze lista patterns to lista krotek, a kazda krotka posiada trzy elementy.
Wywołanie map(buildMatchAndApplyFunctions, patterns) oznacza, ze funkcja
buildMatchAndApplyFunctions nie zostanie wywołana z trzema parametrami. Uzycie
map do odwzorowania listy przy pomocy funkcji zawsze odbywa sie przez wywołanie
tej funkcji z dokładnie jednym parametrem: kazdym elementem listy. W przypadku listy
patterns, kazdy element listy jest krotka, a wiec buildMatchAndApplyFunctions
zawsze jest wywoływana z krotka, przy czym automatyczne rozwiniecie tej krotki zostało
zastosowane w definicji funkcji buildMatchAndApplyFunctions po to, aby elementy
tej krotki zostały automatycznie przypisane do nazwanych zmiennych, z którymi
mozna dalej pracowac.
18.6. PLURAL.PY, ETAP 5 379
18.6 plural.py, etap 5
Usunelismy juz z kodu powtórzenia i dodalismy wystarczajaco duzo abstrakcji,
aby reguły zamiany rzeczownika na liczbe mnoga znajdowały sie na liscie napisów.
Nastepnym logicznym krokiem bedzie umieszczenie tych napisów (definiujacych reguły)
w osobnym pliku, dzieki czemu bedzie mozna utrzymywac liste reguł niezaleznie
od uzywajacego ja kodu.
Na poczatku utworzymy plik tekstowy zawierajacy reguły. Nie ma tu złozonych
struktur, to po prostu napisy rozdzielone spacjami (lub znakami tabulacji) w trzech
kolumnach. Plik ten nazwiemy rules.en; “en” oznacza “English”, reguły te dotycza bowiem
rzeczowników jezyka angielskiego. Pózniej bedziemy mogli dodac pliki z regułami
dla innych jezyków.
Przykład 17.15. rules.en
[sxz]$ $ es
[^aeioudgkprt]h$ $ es
[^aeiou]y$ y$ ies
$ $ s
Zobaczmy, jak mozemy uzyc tego pliku.
Przykład 17.16. plural5.py
import re
import string
def buildRule((pattern, search, replace)):
return lambda word: re.search(pattern, word) and re.sub(search, replace, word) #(1)
def plural(noun, language=’en’): #(2)
lines = file(’rules.%s’ % language).readlines() #(3)
patterns = map(string.split, lines) #(4)
rules = map(buildRule, patterns) #(5)
for rule in rules:
result = rule(noun) #(6)
if result: return result
1. W dalszym ciagu uzywamy tutaj techniki dopełnien (dynamicznego budowania
funkcji, które uzywaja zmiennych zdefiniowanych na zewnatrz funkcji), jednak
teraz połaczylismy osobne funkcje dopasowujaca oraz modyfikujaca w jedna.
(Powód, dla którego to zrobilismy, stanie sie jasny w nastepnym podrozdziale).
Pozwoli to nam osiagnac ten sam cel, jaki osiagalismy przy uzyciu dwóch funkcji,
bedziemy jedynie musieli, jak zobaczymy za chwile, troche inaczej te nowa funkcje
wywołac.
2. Funkcja plural pobiera teraz opcjonalny parametr, language, który ma domyslna
wartosc en.
3. Parametru language uzywamy do skonstruowania nazwy pliku, nastepnie otwieramy
ten plik i wczytujemy jego zawartosc do listy. Jesli language ma wartosc
en, wówczas zostanie otworzony plik rules.en, wczytana jego zawartosc, podzielona
na podstawie znaków nowego wiersza, i zwrócona w postaci listy. Kazda
linia z pliku zostanie wczytana jako jeden element listy.
380 ROZDZIAŁ 18. FUNKCJE DYNAMICZNE
4. Jak widzielismy wczesniej, kazda linia w pliku zawiera trzy wartosci, które sa
oddzielone białymi znakami (spacja lub znakiem tabulacji, nie ma to znaczenia).
Odwzorowanie powstałej z wczytania pliku listy przy pomocy funkcji string.split
pozwoli na utworzenie nowej listy, której elementami sa trzyelementowe krotki.
Linia taka, jak: “[sxz]$ $ es” zostanie zamieniona w krotke (’[sxz]$’, ’$’, ’es’).
Oznacza to, ze w zmiennej patterns znajda sie trzyelementowe krotki zawierajace
napisy, dokładnie tak, jak to wczesniej, na etapie 4, zakodowalismy na
sztywno.
5. Jesli patterns jest lista krotek, to rules bedzie lista funkcji zdefiniowanych dynamicznie
przy kazdym wywołaniu buildRule.Wywołanie buildRule((’[sxz]$’,
’$’, ’es’)) zwróci funkcje, która pobiera jeden parametr, word. Kiedy zwrócona
funkcja jest wywoływana, zostanie wykonany kod: re.search(’[sxz]$’,
word) and re.sub(’$’, ’es’, word).
6. Poniewaz teraz budujemy funkcje, która łaczy w sobie dopasowanie i modyfikacje,
musimy troche inaczej ja wywołac. Jesli wywołamy te funkcje i ona cos nam
zwróci, to bedzie to forma rzeczownika w liczbie mnogiej; jesli zas nie zwróci
nic (zwróci None), oznacza to, ze reguła dopasowujaca nie zdołała dopasowac
wyrazenia do podanego rzeczownika i nalezy w tej sytuacji spróbowac dopasowac
kolejna regułe.
Poprawa kodu polegała na tym, ze udało nam sie całkowicie wyłaczyc reguły tworzenia
liczby mnogiej do osobnego pliku. Dzieki temu nie tylko bedzie mozna utrzymywac
ten plik niezaleznie od kodu, lecz takze, dzieki wprowadzeniu odpowiedniej
notacji nazewniczej, uzywac funkcji plural z róznymi plikami zawierajacymi reguły
dla róznych jezyków.
Wada tego rozwiazania jest fakt, ze ilekroc chcemy uzyc funkcji plural, musimy
na nowo wczytywac plik z regułami. Myslałem, ze uda mi sie napisac te ksiazke bez
uzywania frazy: “zostawiam to jako zagadnienie jako cwiczenie dla czytelników”, ale nie
moge sie powstrzymac: zbudowanie mechanizmu buforujacego dla zaleznego od jezyka
pliku z regułami, który automatycznie odswieza sie, jesli plik z regułami zmienił sie
miedzy wywołaniami, zostawiam jako cwiczenie dla czytelników. Bawcie sie dobrze!
18.7. PLURAL.PY, ETAP 6 381
18.7 plural.py, etap 6
Teraz jestescie juz gotowi, aby porozmawiac o generatorach.
Przykład 17.17. plural6.py
import re
def rules(language):
for line in file(’rules.%s’ % language):
pattern, search, replace = line.split()
yield lambda word: re.search(pattern, word) and re.sub(search, replace, word)
def plural(noun, language=’en’):
for applyRule in rules(language):
result = applyRule(noun)
if result: return result
Powyzszy kod uzywa generatora. Nie zaczne nawet tłumaczyc, na czym polega ta
technika, dopóki nie przyjrzycie sie najpierw prostszemu przykładowi.
Przykład 17.18. Wprowadzenie do generatorów
>>> def make_counter(x):
... print ’entering make_counter’
... while 1:
... yield x #(1)
... print ’incrementing x’
... x = x + 1
...
>>> counter = make_counter(2) #(2)
>>> counter #(3)
<generator object at 0x001C9C10>
>>> counter.next() #(4)
entering make_counter
2
>>> counter.next() #(5)
incrementing x
3
>>> counter.next() #(6)
incrementing x
4
1. Obecnosc słowa kluczowego yield w definicji make counter oznacza, ze nie jest
to zwykła funkcja. To specjalny rodzaj funkcji, która generuje wartosci przy
kazdym wywołaniu. Mozecie myslec o niej jak o funkcji kontynuujacej swoje
działanie: wywołanie jej zwraca obiekt generatora, który moze zostac uzyty do
generowania kolejnych wartosci x.
2. Aby uzyskac instancje generatora make counter, wystarczy wywołac funkcje
make counter. Zauwazcie, ze nie spowoduje to jeszcze wykonania kodu tej funkcji.
Mozna to stwierdzic na podstawie faktu, ze pierwsza instrukcja w tej funkcji
jest instrukcja print, a nic jeszcze nie zostało wypisane.
382 ROZDZIAŁ 18. FUNKCJE DYNAMICZNE
3. Funkcja make counter zwraca obiekt bedacy generatorem.
4. Kiedy po raz pierwszy wywołamy next() na obiekcie generatora, zostanie wykonany
kod funkcji make counter az do pierwszej instrukcji yield, która spowoduje
zwrócenie pewnej wartosci.Wtym przypadku bedzie to wartosc 2, poniewaz
utworzylismy generator wywołujac make counter(2).
5. Kolejne wywołania next() na obiekcie generatora spowoduja kontynuowanie wykonywania
kodu funkcji od miejsca, w którym wykonywanie funkcji zostało przerwane
az do kolejnego napotkania instrukcji yield. W tym przypadku nastepna
linia kodu oczekujaca na wykonanie jest instrukcja print, która wypisuje tekst
“incrementing x”, a kolejna — instrukcja przypisania x = x + 1, która zwieksza
wartosc x. Nastepnie wchodzimy w kolejny cykl petli while, w którym wykonywana
jest instrukcja yield, zwracajaca biezaca wartosc x (obecnie jest to 3).
6. Kolejne wywołanie counter.next spowoduje wykonanie tych samych instrukcji,
przy czym tym razem x osiagnie wartosc 4. I tak dalej. Poniewaz make counter
zawiera nieskonczona petle, wiec teoretycznie moglibysmy robic to w nieskonczonosc:
generator zwiekszałby wartosc x i wypluwał jego biezaca wartosc. Spójrzmy
jednak na bardziej produktywne przypadki uzycia generatorów.
Przykład 17.19. Uzycie generatorów w miejsce rekurencji
def fibonacci(max):
a, b = 0, 1 #(1)
while a < max:
yield a #(2)
a, b = b, a+b #(3)
1. Ciag Fibonacciego składa sie z liczb, z których kazda jest suma dwóch poprzednich
(za wyjatkiem dwóch pierwszych liczb tego ciagu). Ciag rozpoczynaja liczby
0 i 1, kolejne wartosci rosna powoli, nastepnie róznice miedzy nimi coraz szybciej
sie zwiekszaja. Aby rozpoczac generowanie tego ciagu, potrzebujemy dwóch
zmiennych: a ma wartosc 0, b ma wartosc 1.
2. a to biezaca wartosc w ciagu, wiec zwracamy ja
3. b jest kolejna wartoscia, wiec przypisujemy ja do a, jednoczesnie obliczajac kolejna
wartosc (a+b) i przypisujac ja do b do pózniejszego uzycia. Zauwazcie, ze
dzieje sie to równolegle; jesli a ma wartosc 3 a b ma wartosc 5, wówczas w wyniku
wartosciowania wyrazenia a, b = b, a+b do zmiennej a zostanie przypisana
wartosc 5 (wczesniejsza wartosc b), a do b wartosc 8 (suma poprzednich wartosci
a i b).
Mamy wiec funkcje, która wyrzuca z siebie kolejne wartosci ciagu Fibonacciego.
Oczywiscie moglibysmy to samo osiagnac przy pomocy rekurencji, jednak ten sposób
jest znacznie prostszy w zapisie. No i swietnie sie sprawdza w przypadku petli:
Przykład 17.20. Generatory w petlach
>>> for n in fibonacci(1000): #(1)
... print n, #(2)
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987
18.7. PLURAL.PY, ETAP 6 383
1. Generatory (takie jak fibonacci) moga byc uzywane bezposrednio w petlach.
Petla for utworzy obiekt generatora i bedzie wywoływac na nim metode next(),
przypisujac ja do zmiennej sterujacej petli (n).
2. W kazdym przebiegu petli for n bedzie miało nowa wartosc, zwrócona przez
generator w instrukcji yield funkcji fibonacci, i ta wartosc zostanie wypisana.
Jesli generatorowi fibonacci skoncza sie generowane wartosci (zmienna a
przekroczy max, które w tym przypadku jest równe 1000), petla for zakonczy
działanie.
OK, wrócmy teraz do funkcji plural i sprawdzmy, jak tam został uzyty generator.
Przykład 17.21. Generator tworzacy dynamicznie funkcje
def rules(language):
for line in file(’rules.%s’ % language): #(1)
pattern, search, replace = line.split() #(2)
yield lambda word: re.search(pattern, word) and re.sub(search, replace, word) #(3)
def plural(noun, language=’en’):
for applyRule in rules(language): #(4)
result = applyRule(noun)
if result: return result
1. for line in file(...) to czesto spotykany w jezyku Python idiom słuzacy
wczytaniu po jednej wszystkich linii z pliku. Działa on w ten sposób, ze file
zwraca generator, którego metoda next() zwraca kolejna linie z pliku. To szalenie
fajna sprawa, robie sie cały mokry, kiedy tylko o tym pomysle.
2. Tu nie ma zadnej magii. Pamietajcie, ze kazda linia w pliku z regułami zawiera
trzy wartosci oddzielone białym znakiem, a wiec line.split zwróci trzyelementowa
krotke, a jej trzy wartosci sa tu przypisywane do trzech zmiennych
lokalnych.
3. A teraz nastepuje instrukcja yield. Co zwraca yield? Zwraca ona funkcje zbudowana
dynamicznie przy uzyciu notacji lambda, bedaca w rzeczywistosci domknieciem
(uzywa bowiem zmiennych lokalnych pattern, search oraz replace
jako stałych). Innymi słowy, rules jest generatorem który generuje funkcje reprezentujace
reguły.
4. Jesli rules jest generatorem, to znaczy, ze mozemy uzyc go bezposrednio w petli
for. W pierwszym przebiegu petli wywołanie funkcji rules spowoduje otworzenie
pliku z regułami, przeczytanie pierwszej linijki oraz dynamiczne zbudowanie
funkcji, która dopasowuje i modyfikuje na podstawie pierwszej odczytanej reguły
z pliku, po czym nastapi zwrócenie zbudowanej w ten sposób funkcji w
instrukcji yield. W drugim przebiegu petli for wykonywanie funkcji rules rozpocznie
sie tam, gdzie sie ostatnio zakonczyło (a wiec w srodku petli for line
in file(...)), zostanie wiec odczytana druga linia z pliku z regułami, zostanie
dynamicznie zbudowana inna funkcja dopasowujaca i modyfikujaca na podstawie
zapisanej w pliku reguły, po czym ta funkcja zostanie zwrócona w instrukcji
yield. I tak dalej.
384 ROZDZIAŁ 18. FUNKCJE DYNAMICZNE
Co takiego osiagnelismy w porównaniu z etapem 5? W etapie 5 wczytywalismy cały
plik z regułami i budowalismy liste wszystkich mozliwych reguł zanim nawet wypróbowalismy
pierwsza z nich. Teraz, przy uzyciu generatora, podchodzimy do sprawy w
sposób leniwy: otwieramy plik i wczytujemy pierwsza linie w celu zbudowania funkcji,
jednak jesli ta funkcja zadziała (reguła zostanie dopasowana, a rzeczownik zmodyfikowany),
nie bedziemy niepotrzebnie czytac dalej linii z pliku ani tworzyc jakichkolwiek
innych funkcji.
18.8. PODSUMOWANIE 385
18.8 Funkcje dynamiczne - podsumowanie
W tym rozdziale rozmawialismy o wielu zaawansowanych technikach programowania.
Nalezy pamietac, ze nie wszystkie nadaja sie do stosowania w kazdej sytuacji.
Powinniscie teraz dobrze orientowac sie w nastepujacych technikach:
• Zastepowanie napisów przy pomocy wyrazen regularnych.
• Traktowanie funkcji jak obiektów, przechowywanie ich na listach, przypisywanie
ich do zmiennych i wywoływanie ich przy pomocy tych zmiennych.
• Budowanie funkcji dynamicznych przy uzyciu notacji lambda.
• Budowanie dopełnien — funkcji dynamicznych, których definicja uzywa otaczajacych
je zmiennych w charakterze wartosci stałych.
• Budowanie generatorów — funkcji, które mozna kontynuowac, dzieki którym
realizowana jest pewna przyrostowa logika, a za kazdym ich wywołaniem moze
zostac zwrócona inna wartosc.
Dodawanie abstrakcji, dynamiczne budowanie funkcji, tworzenie domkniec i uzywanie
generatorów moze znacznie uproscic kod, moze sprawic, ze stanie sie on czytelniejszy
i bardziej elastyczny. Moze jednak sprawic, ze ten sam kod stanie sie pózniej
znacznie trudniejszy do debugowania. Do was nalezy znalezienie własciwej równowagi
miedzy prostota i moca.
386 ROZDZIAŁ 18. FUNKCJE DYNAMICZNE
Rozdział 19
Optymalizacja szybkosci
387
388 ROZDZIAŁ 19. OPTYMALIZACJA SZYBKOSCI
19.1 Optymalizacja szybkosci
Optymalizacja szybkosci jest niesamowita sprawa. Tylko dlatego, ze Python jest
jezykiem interpretowanym, nie oznacza, ze nie powinnismy martwic sie o optymalizacje
pod katem szybkosci działania. Jednak nie nalezy sie tym az tak bardzo przejmowac.
Nurkujemy
Istnieje sporo pułapek zwiazanych z optymalizacja kodu, ciezko stwierdzic, od czego
nalezałoby zaczac.
Zacznijmy wiec tutaj: czy jestesmy pewni, ze w ogóle powinnismy sie do tego zabierac?
Czy nasz kod jest na prawde tak kiepski? Czy warto poswiecic na to czas? Ile tak
na prawde czasu podczas działania aplikacji bedzie zajmowało wykonywanie kodu, w
porównaniu z czasem poswieconym na oczekiwanie na odpowiedz zdalnej bazy danych
czy tez akcje uzytkownika?
Po drugie, czy jestesmy pewni, ze skonczylismy kodowanie? Przedwczesna optymalizacja
jest jak nakładanie lodowej polewy na w pół upieczone ciasto. Poswiecamy
godziny czy nawet dni na optymalizacje kodu pod katem wydajnosci, aby w koncu
przekonac sie, ze nie robi on tego, co bysmy chcieli. Stracony czas, robota wyrzucona
w błoto.
Nie chodzi o to, ze optymalizacja kodu jest bezwartosciowa, raczej powinnismy
spojrzec z dystansu na cały system aby nabrac przeswiadczenia, czy czas przeznaczony
na nia jest najlepsza inwestycja. Kazda minuta poswiecona na optymalizacje
kodu jest minuta, której nie poswiecamy na dodawanie nowych funkcjonalnosci, pisanie
dokumentacji, zabawe z naszymi dziecmi czy tez pisanie testów jednostkowych.
A no własnie, testy jednostkowe. Nie powinnismy nawet zaczynac optymalizacji nie
majac pełnego zestawu takich testów. Ostatnia rzecza jakiej bysmy chcieli to pojawienie
sie nowego błedu podczas dłubania w algorytmie.
Majac na uwadze powyzsze porady, zerknijmy na niektóre techniki optymalizacji
kodu Pythona. Nasz kod to implementacja algorytmu Soundex. Soundex był metoda
kategoryzowania nazwisk w spisie ludnosci w Stanach Zjednoczonych na poczatku 20
wieku. Grupował on podobnie brzmiace słowa, przez co naukowcy mieli szanse na
odnalezienie nawet błednie zapisanych nazwisk. Soundex jest do dzis uzywany głównie
z tego samego powodu, lecz oczywisci w tym celu uzywane sa skomputeryzowane bazy
danych. Wiekszosc silników baz danych posiada funkcje Soundex.
Istnieje kilka nieco rózniacych sie od siebie wersji algorytmu Soundex. Ponizej znajduje
sie ta, której uzywamy w tym rozdziale:
1. Wez pierwsza litere nazwy.
2. Przekształc pozostałe litery na cyfry według ponizszej listy:
• B, F, P, oraz V staja sie 1.
• C, G, J, K, Q, S, X, oraz Z staja sie 2.
• D oraz T staja sie 3.
• L staje sie 4.
• M oraz N staja sie 5.
• R staje sie 6.
19.1. NURKUJEMY 389
• Wszystkie pozostałe litery staja sie 9.
1. Usun wszystkie duplikaty.
2. Usun wszystkie dziewiatki.
3. Jesli wynik jest krótszy niz cztery znaki (pierwsza litera plus trzy cyfry), dopełnij
wynik zerami z prawej strony ciagu do czterech znaków.
4. Jezeli wynik jest dłuzszy niz cztery znaki, pozbadz sie wszystkiego po czwartym
znaku.
Na przykład, moje imie, Pilgrim, staje sie P942695. Nie posiada nastepujacych po
sobie duplikatów, wiec nic tutaj nie robimy. Nastepnie usuwamy 9tki pozostawiajac
P4265. Jednak znaków jest za duzo, wiec pomijamy ich nadmiar w wyniku otrzymujac
P426.
Inny przykład: Woo staje sie W99, które staje sie W9, które staje sie W, które
natomiast dopełniamy zerami z prawej strony do czterech znaków aby otrzymac W000.
A oto pierwsze podejscie do funkcji Soundex:
Przykład 18.1. soundex/stage1/soundex1a.py
import string, re
charToSoundex = {"A": "9",
"B": "1",
"C": "2",
"D": "3",
"E": "9",
"F": "1",
"G": "2",
"H": "9",
"I": "9",
"J": "2",
"K": "2",
"L": "4",
"M": "5",
"N": "5",
"O": "9",
"P": "1",
"Q": "2",
"R": "6",
"S": "2",
"T": "3",
"U": "9",
"V": "1",
"W": "9",
"X": "2",
"Y": "9",
"Z": "2"}
def soundex(source):
390 ROZDZIAŁ 19. OPTYMALIZACJA SZYBKOSCI
"convert string to Soundex equivalent"
# Soundex requirements:
# source string must be at least 1 character
# and must consist entirely of letters
allChars = string.uppercase + string.lowercase
if not re.search(’^[%s]+$’ % allChars, source):
return "0000"
# Soundex algorithm:
# 1. make first character uppercase
source = source[0].upper() + source[1:]
# 2. translate all other characters to Soundex digits
digits = source[0]
for s in source[1:]:
s = s.upper()
digits += charToSoundex[s]
# 3. remove consecutive duplicates
digits2 = digits[0]
for d in digits[1:]:
if digits2[-1] != d:
digits2 += d
# 4. remove all "9"s
digits3 = re.sub(’9’, , digits2)
# 5. pad end with "0"s to 4 characters
while len(digits3) < 4:
digits3 += "0"
# 6. return first 4 characters
return digits3[:4]
if __name__ == ’__main__’:
from timeit import Timer
names = (’Woo’, ’Pilgrim’, ’Flingjingwaller’)
for name in names:
statement = "soundex(’%s’)" % name
t = Timer(statement, "from __main__ import soundex")
print name.ljust(15), soundex(name), min(t.repeat())
19.2. KORZYSTANIE Z MODUŁU TIMEIT 391
19.2 Korzystanie z modułu timeit
Najwazniejsza rzecza, jaka powinniscie wiedziec na temat optymalizacji kodu w jezyku
Python jest to, ze nie powinniscie pisac własnej funkcji mierzacej czas wykonania
kodu.
Pomiar czasu wykonania niewielkich fragmentów kodu to zagadnienie niezwykle
złozone. Jak duzo czasu procesora zuzywa wasz komputer podczas działania tego kodu?
Czy istnieja jakies inne procesy działajace w tym samym czasie w tle? Czy jestescie
tego pewni? Na kazdym nowoczesnym komputerze działaja jakies procesy w tle, niektóre
przez cały czas a niektóre okresowo. Zadania w cronie zostaja uruchomione w
dokładnie okreslonych porach; usługi działajace w tle co jakis czas “budza sie”, aby
wykonac rózne pozyteczne prace, takie jak sprawdzenie nowej poczty, połaczenie sie z
serwerami typu “instant messaging”, sprawdzanie, czy istnieja juz aktualizacje zainstalowanych
programów, skanowanie antywirusowe, sprawdzanie, czy w ciagu ostatnich
100 nanosekund do napedu CD została włozona płyta i tak dalej. Zanim rozpoczniecie
testy z pomiarami czasu, wyłaczcie wszystko i odłaczcie komputer od sieci. Nastepnie
wyłaczcie to wszystko, o czym zapomnieliscie za pierwszym razem, pózniej wyłaczcie
usługe, która sprawdza, czy nie przyłaczyliscie sie ponownie do sieci, a nastepnie...
Do tego dochodza jeszcze rózne aspekty wprowadzane przez mechanizm pomiaru
czasu. Czy interpreter jezyka Python zapamietuje raz wyszukane nazwy metod? Czy
zapamietuje bloki skompilowanego kodu? Wyrazenia regularne? Czy w waszym kodzie
objawia sie jakies skutki uboczne, gdy uruchomicie go wiecej niz jeden raz? Nie zapominajcie,
ze mamy tutaj do czynienia z małymi ułamkami sekundy, wiec małe błedy w
mechanizmie pomiaru czasu przełoza sie na nienaprawialne zafałszowanie rezultatów
tych pomiarów.
W społecznosci Pythona funkcjonuje powiedzenie: “Python z bateriami w zestawie”.
Nie piszcie własnego mechanizmu mierzacego czas. Python 2.3 posiada słuzacy
do tego celu doskonały moduł o nazwie timeit.
Przykład 18.2. Wprowadzenie do modułu timeit
>>> import timeit
>>> t = timeit.Timer("soundex.soundex(’Pilgrim’)",
... "import soundex") #(1)
>>> t.timeit() #(2)
8.21683733547
>>> t.repeat(3, 2000000) #(3)
[16.48319309109, 16.46128984923, 16.44203948912]
1. Wmodule timeit zdefiniowana jest jedna klasa, Timer, której konstruktor przyjmuje
dwa argumenty. Obydwa argumenty sa napisami. Pierwszy z nich to instrukcja,
której czas wykonania chcemy zmierzyc; w tym przypadku mierzymy
czas wywołania funkcji soundex z modułu soundex z parametrem ’Pilgrim’.
Drugi argument przekazywany do konstruktora obiektu Timer to instrukcja import,
która ma przygotowac srodowisko do wykonywania instrukcji, której czas trwania
mierzymy. Wewnetrzne działanie timeit polega na przygotowaniu wirtualnego,
wyizolowanego srodowiska, wykonaniu instrukcji przygotowujacych (zaimportowaniu
modułu soundex) oraz kompilacji i wykonaniu instrukcji poddawanej pomiarowi
(wywołanie funkcji soundex).
2. Najłatwiejsza rzecz, jaka mozna zrobic z obiektem klasy Timer, to wywołanie
na nim metody timeit(), która wywołuje podana funkcje milion razy i zwraca
392 ROZDZIAŁ 19. OPTYMALIZACJA SZYBKOSCI
liczbe sekund, jaka zajeło to wywoływanie.
3. Inna wazna metoda obiektu Timer jest repeat(), która pobiera dwa opcjonalne
argumenty. Pierwszy argument to liczba okreslajaca ile razy ma byc powtórzony
cały test; drugi argument to liczba okreslajaca ile razy ma zostac wykonana
mierzona instrukcja w ramach jednego testu. Obydwa argumenty sa opcjonalne,
a ich wartosci domyslne to, odpowiednio, 3 oraz 1000000. Metoda repeat()
zwraca liste zawierajaca czas wykonania kazdego testu wyrazony w sekundach.
Moduł timeit mozna równiez wykorzystac, aby z linii polecen zmierzyc czas trwania
dowolnego programu napisanego w jezyku Python, bez koniecznosci modyfikowania
jego zródeł. Opcje linii polecen dostepne sa w dokumentacji modułu timeit.
Zauwazcie, ze repeat() zwraca liste czasów. Czasy te prawie nigdy nie beda identyczne;
jest to spowodowane faktem, ze interpreter jezyka Python nie zawsze otrzymuje
taka sama ilosc czasu procesora (oraz istnieniem róznych procesów w tle, których nie
tak łatwo sie pozbyc). Wasza pierwsza mysl moze byc taka: “Obliczmy srednia i uzyskajmy
Prawdziwa Wartosc”.
W rzeczywistosci takie podejscie jest prawie na pewno złe. Testy, które trwały
dłuzej, nie trwały dłuzej z powodu odchylen w waszym kodzie albo kodzie interpretera
jezyka; trwały dłuzej, poniewaz w tym samym czasie w systemie działały w tle inne
procesy albo z powodów niezaleznych od interpretera jezyka, których nie mozna było
całkowicie wyeliminowac. Jesli róznice w pomiarach czasu dla róznych testów róznia
sie o wiecej niz kilka procent, wówczas sa one zbyt znaczne, aby ufac takim pomiarom.
W przeciwnym przypadku jako czas trwania testu nalezy przyjac wartosc najmniejsza,
a inne wartosci odrzucic.
Python posiada uzyteczna funkcje min, która zwraca najmniejsza wartosc z podanej
w parametrze listy, a która mozna w tym przypadku wykorzystac:
>>> min(t.repeat(3, 1000000))
8.22203948912
19.3. OPTYMALIZACJA WYRAZEN REGULARNYCH 393
19.3 Optymalizacja wyrazen regularnych
Pierwsza rzecza, jaka musi sprawdzic funkcja Soundex, jest to, czy na wejsciu znajduje
sie niepusty ciag znaków. Jak mozna to najlepiej zrobic?
Jesli odpowiedzieliscie: “uzywajac wyrazen regularnych”, to idziecie do kata i kontemplujecie
tam swoje złe instynkty. “Wyrazenia regularne” prawie nigdy nie stanowia
dobrej odpowiedzi; jesli to mozliwe, nalezy ich raczej unikac. Nie tylko ze wzgledu na
wydajnosc, lecz takze z tego prostego powodu, ze sa one niezwykle trudne w debugowaniu
i w dalszym utrzymaniu. Ale równiez ze wzgledu na wydajnosc.
Ponizszy fragment kodu pochodzi z programu soundex/stage1/soundex1a.py i sprawdza,
czy zmienna source bedaca argumentem funkcji jest napisem zbudowanym wyłacznie
z liter, przy czym zawiera co najmniej jedna litere (nie jest pustym napisem):
allChars = string.uppercase + string.lowercase
if not re.search(’^[%s]+$’ % allChars, source):
return "0000"
Czy soundex1a.py to wydajny program? Dla ułatwienia, w sekcji main skryptu
znajduje sie ponizszy kod, który wywołuje moduł timeit, konstruuje test do pomiaru
złozony z trzech róznych napisów, testuje kazdy napis trzy razy i dla kazdego z napisów
wyswietla minimalny czas:
if __name__ == ’__main__’:
from timeit import Timer
names = (’Woo’, ’Pilgrim’, ’Flingjingwaller’)
for name in names:
statement = "soundex(’%s’)" % name
t = Timer(statement, "from __main__ import soundex")
print name.ljust(15), soundex(name), min(t.repeat())
A wiec czy soundex1a.py uzywajacy wyrazen regularnych jest wydajny?
C:samplessoundexstage1>python soundex1a.py
Woo W000 19.3356647283
Pilgrim P426 24.0772053431
Flingjingwaller F452 35.0463220884
Mozemy sie spodziewac, algorytm ten bedzie działał znacznie dłuzej, jesli zostanie
on wywołany ze znacznie dłuzszymi napisami. Mozemy zrobic wiele, aby zmniejszyc te
szczeline (sprawic, ze funkcja bedzie potrzebowała wzglednie mniej czasu dla dłuzszych
danych wejsciowych), jednak natura tego algorytmu wskazuje na to, ze nie bedzie on
nigdy działał w stałym czasie.
Warto miec na uwadze, ze testujemy reprezentatywna próbke imion. Woo to przypadek
trywialny; to imie zostanie skrócone do jednej litery i uzupełnione zerami. Pilgrim
to srednio złozony przypadek, posiadajacy srednia długosc i zawierajacy zarówno
litery znaczace, jak i te, które zostana zignorowane. Flingjingwaller zas to wyjatkowo
długie imie zawierajace nastepujace po sobie powtórzenia. Inne testy mogłyby równiez
byc przydatne, jednak te trzy pokrywaja duzy zakres przypadków.
Co zatem z wyrazeniem regularnym? Cóz, okazało sie ono nieefektywne. Poniewaz
wyrazenie sprawdza zakresy znaków (duze litery A-Z oraz małe a-z), mozemy uzyc
skróconej składni wyrazen regularnych. Ponizej soundex/stage1/soundex1b.py:
394 ROZDZIAŁ 19. OPTYMALIZACJA SZYBKOSCI
if not re.search(’^[A-Za-z]+$’, source):
return "0000"
Moduł timeit mówi, ze soundex1b.py jest odrobine szybszy, niz soundex1a.py, nie
ma w tym jednak nic, czy mozna by sie ekscytowac:
C:samplessoundexstage1>python soundex1b.py
Woo W000 17.1361133887
Pilgrim P426 21.8201693232
Flingjingwaller F452 32.7262294509
Jak widzielismy w podrozdziale 15.3 [Python/Refaktoryzacja], wyrazenia regularne
moga zostac skompilowane i uzyte powtórnie, dzieki czemu osiaga sie lepsze rezultaty.
Ze wzgledu na to, ze nasze wyrazenie regularne nie zmienia sie miedzy wywołaniami,
kompilujemy je i uzywamy wersji skompilowanej. Ponizej znajduje sie fragment soundex/
stage1/soundex1c.py:
isOnlyChars = re.compile(’^[A-Za-z]+$’).search
def soundex(source):
if not isOnlyChars(source):
return "0000"
Uzycie skompilowanej wersji wyrazenia regularnego jest juz znacznie szybsze:
C:samplessoundexstage1>python soundex1c.py
Woo W000 14.5348347346
Pilgrim P426 19.2784703084
Flingjingwaller F452 30.0893873383
Ale czy to nie jest przypadkiem błedna sciezka? Logika jest przeciez prosta: napis
wejsciowy nie moze byc pusty i musi byc cały złozony z liter. Czy nie byłoby szybciej,
gdybysmy pozbyli sie wyrazenia regularnego i zapisali petle, która sprawdza kazdy
znak?
Ponizej soundex/stage1/soundex1d.py:
if not source:
return "0000"
for c in source:
if not (’A’ <= c <= ’Z’) and not (’a’ <= c <= ’z’):
return "0000"
Okazuje sie, ze ta technika w soundex1d.py nie jest szybsza, niz uzycie skompilowanego
wyrazenia regularnego (ale jest szybsza, niz uzycie nieskompilowanego wyrazenia
regularnego):
C:samplessoundexstage1>python soundex1d.py
Woo W000 15.4065058548
Pilgrim P426 22.2753567842
Flingjingwaller F452 37.5845122774
19.3. OPTYMALIZACJA WYRAZEN REGULARNYCH 395
Dlaczego soundex1d.py nie jest szybszy? Odpowiedz lezy w naturze jezyka Python,
który jest jezykiem interpretowanym. Silnik wyrazen regularnych napisany jest w C
i skompilowany odpowiednio do architektury waszego komputera. Z drugiej strony,
petla napisana jest w jezyku Python i jest uruchamiana przez interpreter jezyka Python.
Choc petla jest stosunkowo prosta, jej prostota nie wystarcza, aby pozbyc sie
narzutu zwiazanego z tym, ze jest ona interpretowana. Wyrazenia regularne nigdy nie
sa dobrym rozwiazaniem... chyba, ze akurat sa.
Okazuje sie, ze Python posiada pewna stara metode w klasie string. Jestescie całkowicie
usprawiedliwieni, jesli o niej nie wiedzieliscie, bowiem nie wspominałem jeszcze
o niej w tej ksiazce. Metoda nazywa sie isalpha() i sprawdza, czy napis składa sie
wyłacznie z liter.
Oto soundex/stage1/soundex1e.py:
if (not source) and (not source.isalpha()):
return "0000"
Czy zyskalismy cos uzywajac tej specyficznej metody w soundex1e.py? Troche zyskalismy.
C:samplessoundexstage1>python soundex1e.py
Woo W000 13.5069504644
Pilgrim P426 18.2199394057
Flingjingwaller F452 28.9975225902
Przykład 18.3. Dotychczas najlepszy rezultat: soundex/stage1/soundex1e.py
import string, re
charToSoundex = {"A": "9",
"B": "1",
"C": "2",
"D": "3",
"E": "9",
"F": "1",
"G": "2",
"H": "9",
"I": "9",
"J": "2",
"K": "2",
"L": "4",
"M": "5",
"N": "5",
"O": "9",
"P": "1",
"Q": "2",
"R": "6",
"S": "2",
"T": "3",
"U": "9",
"V": "1",
"W": "9",
396 ROZDZIAŁ 19. OPTYMALIZACJA SZYBKOSCI
"X": "2",
"Y": "9",
"Z": "2"}
def soundex(source):
if (not source) and (not source.isalpha()):
return "0000"
source = source[0].upper() + source[1:]
digits = source[0]
for s in source[1:]:
s = s.upper()
digits += charToSoundex[s]
digits2 = digits[0]
for d in digits[1:]:
if digits2[-1] != d:
digits2 += d
digits3 = re.sub(’9’, , digits2)
while len(digits3) < 4:
digits3 += "0"
return digits3[:4]
if __name__ == ’__main__’:
from timeit import Timer
names = (’Woo’, ’Pilgrim’, ’Flingjingwaller’)
for name in names:
statement = "soundex(’%s’)" % name
t = Timer(statement, "from __main__ import soundex")
print name.ljust(15), soundex(name), min(t.repeat())
19.4. OPTYMALIZACJA PRZESZUKIWANIA SŁOWNIKA 397
19.4 Optymalizacja przeszukiwania słownika
Kolejnym krokiem w algorytmie Soundex jest przekształcenie znaków w cyfry według
pewnego szczególnego wzorca. Jaki jest najlepszy sposób, aby to zrobic?
Najbardziej oczywiste rozwiazanie polega na zdefiniowaniu słownika, w którym poszczególne
znaki sa kluczami, a wartosciami — odpowiadajace im cyfry, a nastepnie
przeszukiwaniu słownika dla kazdego pojawiajacego sie znaku. Tak własnie działa program
soundex/stage1/soundex1c.py (który osiaga najlepsze jak do tej pory rezultaty
wydajnosciowe):
charToSoundex = {"A": "9",
"B": "1",
"C": "2",
"D": "3",
"E": "9",
"F": "1",
"G": "2",
"H": "9",
"I": "9",
"J": "2",
"K": "2",
"L": "4",
"M": "5",
"N": "5",
"O": "9",
"P": "1",
"Q": "2",
"R": "6",
"S": "2",
"T": "3",
"U": "9",
"V": "1",
"W": "9",
"X": "2",
"Y": "9",
"Z": "2"}
def soundex(source):
# ... input check omitted for brevity ...
source = source[0].upper() + source[1:]
digits = source[0]
for s in source[1:]:
s = s.upper()
digits += charToSoundex[s]
Mierzylismy juz czas wykonania soundex1c.py; oto, jak sie przestawiaja pomiary:
C:samplessoundexstage1>python soundex1c.py
Woo W000 14.5341678901
Pilgrim P426 19.2650071448
Flingjingwaller F452 30.1003563302
398 ROZDZIAŁ 19. OPTYMALIZACJA SZYBKOSCI
Powyzszy kod jest prosty, ale czy stanowi najlepsze mozliwe rozwiazanie? Nieefektywne
wydaje sie w szczególnosci wywoływanie na kazdym znaku metody upper();
zrobilibysmy prawdopodobnie lepiej wywołujac upper() na całym napisie wejsciowym.
Dochodzi do tego kwestia przyrostowego budowania napisu złozonego z cyfr. Takie
przyrostowe budowanie napisu jest niezwykle niewydajne; wewnetrznie interpreter
jezyka musi w kazdym przebiegu petli utworzyc nowy napis, a nastepnie zwolnic stary.
Wiadomo jednak, ze Python swietnie sobie radzi z listami. Moze automatycznie
potraktowac napis jako liste znaków. Liste zas łatwo przekształcic z powrotem do
napisu przy uzyciu metody join().
Ponizej soundex/stage2/soundex2a.py, który konwertuje litery na cyfry przy uzyciu
metody join i notacji lambda:
def soundex(source):
# ...
source = source.upper()
digits = source[0] + "".join(map(lambda c: charToSoundex[c], source[1:]))
Co ciekawe, program soundex2a.py nie jest wcale szybszy:
C:samplessoundexstage2>python soundex2a.py
Woo W000 15.0097526362
Pilgrim P426 19.254806407
Flingjingwaller F452 29.3790847719
Narzut wprowadzony przez funkcje anonimowa utworzona przy pomocy notacji
lambda przekroczył cały zysk wydajnosci, jaki osiagnelismy uzywajac napisu w charakterze
listy znaków.
Program soundex/stage2/soundex2b.py w miejsce notacji lambda uzywa wyrazen
listowych:
source = source.upper()
digits = source[0] + "".join([charToSoundex[c] for c in source[1:]])
Uzycie wyrazen listowych w soundex2b.py jest szybsze niz uzywanie notacji lambda,
ale wciaz nie jest szybsze, niz oryginalny kod (soundex1c.py, w którym napis wynikowy
jest budowany przyrostowo):
C:samplessoundexstage2>python soundex2b.py
Woo W000 13.4221324219
Pilgrim P426 16.4901234654
Flingjingwaller F452 25.8186157738
Nadszedł czas na wprowadzenie radykalnej zmiany w naszym podejsciu. Przeszukiwanie
słownika to narzedzie ogólnego zastosowania. Kluczami w słownikach moga byc
napisy dowolnej długosci (oraz wiele innych typów danych), jednak w tym przypadku
posługiwalismy sie jedynie napisami o długosci jednego znaku, zarówno w charakterze
klucza, jak i wartosci. Okazuje sie, ze jezyk Python posiada specjalizowana funkcje
słuzaca do obsługi dokładnie tej sytuacji, funkcje o nazwie string.maketrans.
Oto program soundex/stage2/soundex2c.py:
19.4. OPTYMALIZACJA PRZESZUKIWANIA SŁOWNIKA 399
allChar = string.uppercase + string.lowercase
charToSoundex = string.maketrans(allChar, "91239129922455912623919292" * 2)
def soundex(source):
# ...
digits = source[0].upper() + source[1:].translate(charToSoundex)
Co własciwie sie tu dzieje? Funkcja string.maketrans tworzy wektor
przekształcen miedzy dwoma napisami: pierwszym i drugim argumentem.
W tym przypadku pierwszym argumentem jest napis ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz,
a drugim napis
9123912992245591262391929291239129922455912623919292. Rozpoznajecie ten
wzorzec? To ten sam, którego uzywalismy w przypadku słownika: A jest przekształcane
do 9, B do 1, C do 2 i tak dalej. Nie jest to jednak słownik; to specjalizowana
struktura danych, do której mamy dostep przy uzyciu metody translate(), przekształcajacej
kazdy znak argumentu w odpowiadajaca mu cyfre, zgodnie z wektorem
przekształcen zdefiniowanym w string.maketrans.
Moduł timeit pokazuje, ze program soundex2c.py jest znacznie szybszy niz ten,
w którym definiowalismy słownik, iterowalismy w petli po napisie wejsciowym i budowalismy
przyrostowo napis wyjsciowy:
C:samplessoundexstage2>python soundex2c.py
Woo W000 11.437645008
Pilgrim P426 13.2825062962
Flingjingwaller F452 18.5570110168
Nie uda sie nam osiagnac o wiele lepszych rezultatów niz pokazany wyzej. Specjalizowana
funkcja jezyka Python robi dokładnie to, czego potrzebujemy; uzywamy wiec
jej i idziemy dalej.
Przykład 18.4. Dotychczas najlepszy rezultat: soundex/stage2/soundex2c.py
import string, re
allChar = string.uppercase + string.lowercase
charToSoundex = string.maketrans(allChar, "91239129922455912623919292" * 2)
isOnlyChars = re.compile(’^[A-Za-z]+$’).search
def soundex(source):
if not isOnlyChars(source):
return "0000"
digits = source[0].upper() + source[1:].translate(charToSoundex)
digits2 = digits[0]
for d in digits[1:]:
if digits2[-1] != d:
digits2 += d
digits3 = re.sub(’9’, , digits2)
while len(digits3) < 4:
digits3 += "0"
return digits3[:4]
if __name__ == ’__main__’:
from timeit import Timer
400 ROZDZIAŁ 19. OPTYMALIZACJA SZYBKOSCI
names = (’Woo’, ’Pilgrim’, ’Flingjingwaller’)
for name in names:
statement = "soundex(’%s’)" % name
t = Timer(statement, "from __main__ import soundex")
print name.ljust(15), soundex(name), min(t.repeat())
19.5. OPTYMALIZACJA OPERACJI NA LISTACH 401
19.5 Optymalizacja operacji na listach
Optymalizacja operacji na listach
Trzecim krokiem a optymalizacji algorytmu soundex jest eliminacja kolejnych powtarzajacych
sie cyfr. Jak najlepiej to zrobic?
Taki kod otrzymalismy dotad, znajduje sie on w soundex/stage2/soundex2c.py:
digits2 = digits[0]
for d in digits[1:]:
if digits2[-1] != d:
digits2 += d
Takie wyniki wydajnosciowe otrzymujemy dla soundex2c.py:
C:samplessoundexstage2>python soundex2c.py
Woo W000 12.6070768771
Pilgrim P426 14.4033353401
Flingjingwaller F452 19.7774882003
Pierwsza rzecza do rozwazenia jest efektywnosc kontrola digits[-1] w kazdej
iteracji petli. Czy indeksowanie listy jest kosztowne? A moze lepiej przechowywac
ostatnia cyfre w oddzielnej zmiennej i sprawdzac nia zamiast listy?
Odpowiedz na to pytanie pomoze nam znalezc soundex/stage3/soundex3a.py:
digits2 =
last_digit =
for d in digits:
if d != last_digit:
digits2 += d
last_digit = d
soundex3a.py nie działa ani troche szybciej niz soundex2c.py, a nawet moze byc
troszeczke wolniejsze (aczkolwiek jest to za mała róznica, aby cos powiedziec pewnie):
C:samplessoundexstage3>python soundex3a.py
Woo W000 11.5346048171
Pilgrim P426 13.3950636184
Flingjingwaller F452 18.6108927252
Dlaczego soundex3a.py nie jest szybsze? Okazuje sie, ze indeksowanie list w Pythonie
jest ekstremalnie efektywne. Powtarzenie dostepu do digits2[-1] nie stanowi w
ogóle problemu. Z innej strony, kiedy manualnie zarzadzamy ostatnia cyfra w oddzielnej
zmiennej, korzystamy z dwóch przypisan do zmiennych dla kazdej przechowywanej
cyfry, a te operacje zastepuja mały koszt zwiazany z korzystania z listy.
Spróbujemy teraz czegos radykalnie innego. Jest mozliwe, aby traktowac dany napis
jako liste znaków, zatem mozna by było wykorzystac wyrazenie listowe, aby przeiterowac
liste znaków. Jednak wystepuje problem zwiazany z tym, ze potrzebujemy dostepu
do poprzedniego znaku w liscie, a to nie jest proste w przypadku prostych wyrazen
listowych.
Jakkolwiek jest mozliwe tworzenie listy liczbowych indeksów za pomoca wbudowanej
funkcji range(), a nastepnie wykorzystac te indeksy do stopniowego przeszukiwania
listy i wkładania kazdego znaku róznego od znaku poprzedzajacego. Dzieki temu
402 ROZDZIAŁ 19. OPTYMALIZACJA SZYBKOSCI
otrzymamy liste znaków, a nastepnie mozemy wykorzystac metode łancucha znaków
join(), a by zrekonstruowac z tego liste.
Ponizej mamy soundex/stage3/soundex3b.py:
digits2 = "".join([digits[i] for i in range(len(digits))
if i == 0 or digits[i-1] != digits[i]])
Czy jest to szybsze? Jednym słowem, nie.
C:samplessoundexstage3>python soundex3b.py
Woo W000 14.2245271396
Pilgrim P426 17.8337165757
Flingjingwaller F452 25.9954005327
Byc moze szybkie techniki powinny sie skupiac wokół łancuchów znaków. Python
moze konwertowac łancuch znaków na liste znaków za pomoca jednego polecenia:
list(’abc’), które zwróci [’a’, ’b’, ’c’]. Ponadto listy moga byc bardzo szybko
modyfikowane w miejscu. Zamiast zwiekszac liczbe tworzonych nowych list (lub łancuchów
znaków) z naszego poczatkowego łancucha, dlaczego by nie przeniesc wszystkich
elementów do pojedynczej listy?
Ponizej przedstawiono soundex/stage3/soundex3c.py, który modyfikuje liste w
miejscu i usuwa kolejno duplujace sie elementy:
digits = list(source[0].upper() + source[1:].translate(charToSoundex))
i=0
for item in digits:
if item==digits[i]: continue
i+=1
digits[i]=item
del digits[i+1:]
digits2 = "".join(digits)
Czy jest to szybsze od soundex3a.py lub soundex3b.py? Nie, w rzeczywistosci jest
to jeszcze wolniejsze:
C:samplessoundexstage3>python soundex3c.py
Woo W000 14.1662554878
Pilgrim P426 16.0397885765
Flingjingwaller F452 22.1789341942
Ciagle nie wykonalismy tutaj zadnego podstepu, z wyjatkiem tego, ze wykorzystalismy
i wypróbowalismy kilka “madrych” technik. Najszybszym kodem, który jak dotad
widzielismy nadal pozostał oryginał, najbardziej prosta metoda (soundex2c.py). Czasami
nie popłaca byc madrym.
Przykład 18.5 Najlepszy wynik do tej pory: soundex/stage2/soundex2c.py
import string, re
allChar = string.uppercase + string.lowercase
charToSoundex = string.maketrans(allChar, "91239129922455912623919292" * 2)
isOnlyChars = re.compile(’^[A-Za-z]+$’).search
19.5. OPTYMALIZACJA OPERACJI NA LISTACH 403
def soundex(source):
if not isOnlyChars(source):
return "0000"
digits = source[0].upper() + source[1:].translate(charToSoundex)
digits2 = digits[0]
for d in digits[1:]:
if digits2[-1] != d:
digits2 += d
digits3 = re.sub(’9’, , digits2)
while len(digits3) < 4:
digits3 += "0"
return digits3[:4]
if __name__ == ’__main__’:
from timeit import Timer
names = (’Woo’, ’Pilgrim’, ’Flingjingwaller’)
for name in names:
statement = "soundex(’%s’)" % name
t = Timer(statement, "from __main__ import soundex")
print name.ljust(15), soundex(name), min(t.repeat())
404 ROZDZIAŁ 19. OPTYMALIZACJA SZYBKOSCI
19.6 Optymalizacja operacji na napisach
Ostatnim krokiem algorytmu Soundex jest dopełnienie krótkich napisów wynikowych
zerami oraz przyciecie napisów zbyt długich. W jaki sposób mozna to zrobic
najlepiej?
Dotychczasowy kod w programie soundex/stage2/soundex2c.py wyglada nastepujaco:
digits3 = re.sub(’9’, ’’, digits2)
while len(digits3) < 4:
digits3 += "0"
return digits3[:4]
Oto rezultaty pomiarów wydajnosci w soundex2c.py:
C:samplessoundexstage2>python soundex2c.py
Woo W000 12.6070768771
Pilgrim P426 14.4033353401
Flingjingwaller F452 19.7774882003
Pierwsza rzecza, jaka nalezy rozwazyc, jest zastapienie wyrazenia regularnego petla.
Oto kod programu soundex/stage4/soundex4a.py:
digits3 = ’’
for d in digits2:
if d != ’9’:
digits3 += d
Czy soundex4a.py jest szybszy? Oczywiscie:
C:samplessoundexstage4>python soundex4a.py
Woo W000 6.62865531792
Pilgrim P426 9.02247576158
Flingjingwaller F452 13.6328416042
Czekajcie chwile. Petla, która usuwa znaki z napisu? Mozemy uzyc do tego prostej
metody z klasy string. Oto soundex/stage4/soundex4b.py:
digits3 = digits2.replace(’9’, )
Czy soundex4b.py jest szybszy? To interesujace pytanie. Zalezy to od dancyh wejsciowych:
C:samplessoundexstage4>python soundex4b.py
Woo W000 6.75477414029
Pilgrim P426 7.56652144337
Flingjingwaller F452 10.8727729362
Dla wiekszosci nazw z programu soundex4b.py metoda z klasy string jest szybsza
niz petla, jest jednak odrobine wolniejsza niz soundex4a.py dla przypadku trywialnego
(dla bardzo krótkiej nazwy). Optymalizacje wydajnosciowe nie zawsze sa jednorodne;
poprawki, które sprawia, ze w pewnych przypadkach program bedzie działał szybciej,
moga sprawic, ze w innych przypadkach ten sam program bedzie działał wolniej. W
19.6. OPTYMALIZACJA OPERACJI NA NAPISACH 405
naszym programie uzyskujemy poprawe dla wiekszosci przypadków, wiec zostawimy
te poprawke, warto jednak na przyszłosc miec te wazna zasade na uwadze.
Na koniec, choc to wcale nie jest sprawa najmniejszej wagi, przesledzmy dwa ostatnie
kroki algorytmu: uzupełnianie zerami krótkich napisów wynikowych oraz przycinanie
napisów zbyt długich do czterech znaków. Kod, który widzicie w soundex4b.py
robi dokładnie to, co trzeba, jednak robi to w bardzo niewydajny sposób. Spójrzmy
na soundex/stage4/soundex4c.py, aby przekonac sie, dlaczego tak sie dzieje.
digits3 += ’000’
return digits3[:4]
Dlaczego potrzebujemy petli while do wyrównania napisu wynikowego? Wiemy
przeciez z góry, ze zamierzamy obciac napis do czterech znaków; wiemy równiez, ze
mamy juz przynajmniej jeden znak (pierwsza litere zmiennej source, która w wyniku
tego algorytmu nie zmienia sie). Oznacza to, ze mozemy po prostu dodac trzy zera
do napisu wyjsciowego, a nastepnie go przyciac. Nie dajcie sie nigdy zbic z tropu
przez dosłowne sformułowanie problemu; czasami wystarczy spojrzec na ten problem
z troche innej perspektywy, a juz pojawia sie prostsze rozwiazanie.
Czy usuwajac petle while zyskalismy na predkosci programu soundex4c.py? Nawet
znacznie:
C:samplessoundexstage4>python soundex4c.py
Woo W000 4.89129791636
Pilgrim P426 7.30642134685
Flingjingwaller F452 10.689832367
Na koniec warto zauwazyc, ze jest jeszcze jedna rzecz, jaka mozna zrobic, aby
przyspieszyc te trzy linijki kodu: mozna przekształcic je do jednej linii. Spójrzmy na
soundex/stage4/soundex4d.py:
return (digits2.replace(’9’, ’’) + ’000’)[:4]
Umieszczenie tego kodu w jednej linii sprawiło, ze stał sie on zaledwie odrobine
szybszy, niz soundex4c.py:
C:samplessoundexstage4>python soundex4d.py
Woo W000 4.93624105857
Pilgrim P426 7.19747593619
Flingjingwaller F452 10.5490700634
Za cene tego niewielkiego wzrostu wydajnosci stał sie przy okazji znacznie mniej
czytelny. Czy warto było to zrobic?Mam nadzieje, ze potraficie to dobrze skomentowac.
Wydajnosc to nie wszystko. Wysiłki zwiazane z optymalizacja kodu musza byc zawsze
równowazone dazeniem do tego, aby kod był czytelny i łatwy w utrzymaniu.
406 ROZDZIAŁ 19. OPTYMALIZACJA SZYBKOSCI
19.7 Optymalizacja szybkosci - podsumowanie
Podsumowanie
Rozdział ten zilustrował kilka waznych aspektów dotyczacych zoptymalizowania
czasu działania programu w Pythonie, jak i w ogólnosci optymalizacji czasu działania.
• Jesli masz wybrac miedzy wyrazeniami regularnymi, a pisanie własnej petli, wybierz
wyrazenia regularne. Wyrazenia regularne sa przekompilowane w C, wiec
beda sie wykonywały bezposrednio przez twój komputer; twoja petla jest pisana
w Pythonie i działa za posrednictwem interpretera Pythona.
• Jesli masz wybrac miedzy wyrazeniami regularnymi, a metodami łancucha znaków,
wybierz metody łancucha znaków. Obydwa sa przekompilowane w C, wiec
wybieramy prostsze wersje.
• Ogólne zastosowanie zwiazane z przeszukiwaniem słowników jest szybkie, jednak
specjalistyczne funkcja takie jak string.maketrans, czy tez metody takie jak
isalpha() sa szybsze. Jesli Python posiada funkcje specjalnie przystosowana dla
ciebie, wykorzystaj ja.
• Nie badz za madry. Czasami najbardziej oczywiste algorytmy sa najszybsze, jakie
bysmy wymyslili.
• Nie mecz sie za bardzo. Szybkosc wykonywania nie jest wszystkim.
Trzeba podkreslic, ze ostatni punkt jest dosyc znaczacy. Przez cały kurs tego rozdziału,
przerobilismy te funkcje tak, aby działała trzy razy szybciej i zaoszczedzilismy
20 sekund na 1 milion wywołan tej funkcji. Wspaniale! Ale pomyslmy teraz: podczas
wykonywania miliona wywołan tej funkcji, jak długo bedziemy musieli czekac na
połaczenie bazy danych? Albo jak długo bedziemy czekac wykonujac operacje wejscia/
wyjscia na dysku? Albo jak długo bedziemy czekac na wejscie uzytkownika? Nie
marnuj za duzo czasu na czasowa optymalizacje jednego algorytmu. Pobaw sie bardziej
nad istotnymi fragmentami kodu, które maja działac szybko, nastepnie popraw
wyłapane błedy, a reszte zostaw w spokoju.
Dodatek A
Informacje o pliku i historia
A.1 Historia
Ta ksiazka została stworzona na polskojezycznej wersji projektu Wikibooks przez
autorów wymienionych ponizej w sekcji Autorzy. Najnowsza wersja podrecznika jest
dostepna pod adresem http://pl.wikibooks.org/wiki/Zanurkuj_w_Pythonie.
A.2 Informacje o pliku PDF i historia
PDF został utworzony przez Derbetha dnia 17 lutego 2008 na podstawie wersji
z 9 lutego 2008 podrecznika na Wikibooks. Wykorzystany został poprawiony program
Wiki2LaTeX autorstwa uzytkownika angielskich Wikibooks, Hagindaza. Wynikowy
kod po recznych poprawkach został przekształcony w ksiazke za pomoca systemu
składu LATEX.
Najnowsza wersja tego PDF-u jest postepna pod adresem http://pl.wikibooks.
org/wiki/Image:Zanurkuj_w_Pythonie.pdf.
A.3 Autorzy
Akira, Ciastek, Derbeth, Diodac, Farin mag, Fred 4, Fservant, JaroslawZabiello,
Jau, Kabturek, KamilaChyla, Kamils, Kangel, Kj, Kocio, Koko, Krzysiu Jarzyna, Kubik,
Lewico, Lwh, MTM, Migol, Myki, Nata, Neverous, Pepson, Pietras1988, Piotr,
Prasuk historyk, Rafał Rawicki, Robwolfe, Rofrol, Roman 92, Salmon, Sasek, Skalee
vel crensh, Sqrll, Sznurek, Tertulian, Turbofart, Tyfus, Warszk, Yurez, Zeo, Zyx i
anonimowi autorzy.
A.4 Grafiki
Autorzy i licencje grafik:
• grafika na okładce: Python molurus z “Fauna of British India” G. A. Boulengera,
zródło Wikimedia Commons, public domain
• logo Wikibooks: zastrzezony znak towarowy,
c & TMAll rights reserved, Wikimedia
Foundation, Inc.
407
408 DODATEK A. INFORMACJE O PLIKU
Dodatek B
Dalsze wykorzystanie tej
ksiazki
B.1 Wstep
Idea Wikibooks jest swobodne dzielenie sie wiedza, dlatego tez uregulowania Wikibooks
maja na celu jak najmniejsze ograniczanie mozliwosci osób korzystajacych
z serwisu i zapewnienie, ze tresci dostepne tutaj beda mogły byc łatwo kopiowane i
wykorzystywane dalej. Prosimy wszystkich odwiedzajacych, aby poswiecili chwile na
zaznajomienie sie z ponizszymi zasadami, by uniknac w przyszłosci nieporozumien.
B.2 Status prawny
Cała zawartosc Wikibooks (o ile w danym miejscu nie jest zaznaczone inaczej;
szczegóły nizej) jest udostepniona na nastepujacych warunkach:
Udziela sie zezwolenia na kopiowanie, rozpowszechnianie i/lub modyfikacje
tresci artykułów polskich Wikibooks zgodnie z zasadami Licencji GNU
Wolnej Dokumentacji (GNU Free Documentation License) w wersji 1.2 lub
dowolnej pózniejszej opublikowanej przez Free Software Foundation; bez
Sekcji Niezmiennych, Tekstu na Przedniej Okładce i bez Tekstu na Tylnej
Okładce. Kopia tekstu licencji znajduje sie na stronie GNU Free Documentation
License.
Zasadniczo oznacza to, ze artykuły pozostana na zawsze dostepne na zasadach
open source i moga byc uzywane przez kazdego z obwarowaniami wyszczególnionymi
ponizej, które zapewniaja, ze artykuły te pozostana wolne.
Grafiki i pliku multimedialne wykorzystane na Wikibooks moga byc udostepnione
na warunkach innych niz GNU FDL. Aby sprawdzic warunki korzystania z grafiki,
nalezy przejsc do jej strony opisu, klikajac na grafice.
B.3 Wykorzystywanie materiałów z Wikibooks
Jesli uzytkownik polskich Wikibooks chce wykorzystac materiały w niej zawarte,
musi to zrobic zgodnie z GNU FDL. Warunki te to w skrócie i uproszczeniu:
409
410 DODATEK B. DALSZE WYKORZYSTANIE TEJ KSIAZKI
Publikacja w Internecie
1. dobrze jest wymienic Wikibooks jako zródło (jest to nieobowiazkowe, lecz jest
dobrym zwyczajem)
2. nalezy podac liste autorów lub wyraznie opisany i funkcjonujacy link do oryginalnej
tresci na Wikibooks lub historii edycji (wypełnia to obowiazek podania
autorów oryginalnego dzieła)
3. trzeba jasno napisac, ze tresc publikowanego dzieła jest objeta licencja GNU
FDL
4. nalezy podac link to tekstu licencji (najlepiej zachowanej na własnym serwerze)
5. postac tekstu nie moze ograniczac mozliwosci jego kopiowania
Druk
1. dobrze jest wymienic Wikibooks jako zródło (jest to nieobowiazkowe, lecz jest
dobrym zwyczajem)
2. nalezy wymienic 5 autorów z listy w rozdziale Autorzy danego podrecznika (w
przypadku braku podobnego rozdziału — lista autorów jest dostepna pod odnosnikiem
“historia” na górze strony). Gdy podrecznik ma mniej niz 5 autorów,
nalezy wymienic wszystkich.
3. trzeba jasno napisac na przynajmniej jednej stronie, ze tresc publikowanego
dzieła jest objeta licencja GNU FDL
4. pełny tekst licencji, w oryginale i bez zmian, musi byc zawarty w ksiazce
5. jesli zostanie wykonanych wiecej niz 100 kopii ksiazki konieczne jest:
(a) dostarczenie płyt CD, DVD, dysków lub innych nosników danych z trescia
ksiazki w formie mozliwa do komputerowego przetwarzania; lub:
(b) dostarczenie linku do strony z czytelna dla komputera forma ksiazki (link
musi byc aktywny przynajmniej przez rok od publikacji; mozna uzyc linku
do spisu tresci danego podrecznika na Wikibooks)
Nie ma wymogu pytania o zgode na wykorzystanie tekstu jakichkolwiek osób z Wikibooks.
Autorzy nie moga zabronic nikomu wykorzystywania ich tekstów zgodnie z
licencja GNU FDL. Mozna korzystac z ksiazek jako całosci albo z ich fragmentów. Materiały
bazujace na tresci z Wikibooks moga byc bez przeszkód sprzedawane; zyskami
nie trzeba dzielic sie z autorami oryginalnego dzieła.
Jezeli w wykorzystanych materiałach z polskich Wikibooks sa tresci objete innymi
niz GNU FDL licencjami, uzytkownik musi spełnic wymagania zawarte w tych licencjach
(dotyczy to szczególnie grafik, które moga miec rózne licencje).
Dodatek C
GNU Free Documentation
License
Version 1.2, November 2002
Copyright
c 2000,2001,2002 Free Software Foundation, Inc.
51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies of this license
document, but changing it is not allowed.
Preamble
The purpose of this License is to make a manual, textbook, or other functional and
useful document “free” in the sense of freedom: to assure everyone the effective freedom
to copy and redistribute it, with or without modifying it, either commercially or noncommercially.
Secondarily, this License preserves for the author and publisher a way
to get credit for their work, while not being considered responsible for modifications
made by others.
This License is a kind of “copyleft”, which means that derivative works of the
document must themselves be free in the same sense. It complements the GNU General
Public License, which is a copyleft license designed for free software.
We have designed this License in order to use it for manuals for free software,
because free software needs free documentation: a free program should come with
manuals providing the same freedoms that the software does. But this License is not
limited to software manuals; it can be used for any textual work, regardless of subject
matter or whether it is published as a printed book. We recommend this License
principally for works whose purpose is instruction or reference.
1. APPLICABILITY AND DEFINITIONS
This License applies to any manual or other work, in any medium, that contains
a notice placed by the copyright holder saying it can be distributed under the terms
of this License. Such a notice grants a world-wide, royalty-free license, unlimited in
duration, to use that work under the conditions stated herein. The “Document”,
below, refers to any such manual or work. Any member of the public is a licensee, and
411
412 DODATEK C. GNU FREE DOCUMENTATION LICENSE
is addressed as “you”. You accept the license if you copy, modify or distribute the
work in a way requiring permission under copyright law.
A “Modified Version” of the Document means any work containing the Document
or a portion of it, either copied verbatim, or with modifications and/or translated
into another language.
A “Secondary Section” is a named appendix or a front-matter section of the
Document that deals exclusively with the relationship of the publishers or authors of
the Document to the Document’s overall subject (or to related matters) and contains
nothing that could fall directly within that overall subject. (Thus, if the Document is
in part a textbook of mathematics, a Secondary Section may not explain any mathematics.)
The relationship could be a matter of historical connection with the subject or
with related matters, or of legal, commercial, philosophical, ethical or political position
regarding them.
The “Invariant Sections” are certain Secondary Sections whose titles are designated,
as being those of Invariant Sections, in the notice that says that the Document
is released under this License. If a section does not fit the above definition of Secondary
then it is not allowed to be designated as Invariant. The Document may contain
zero Invariant Sections. If the Document does not identify any Invariant Sections then
there are none.
The “Cover Texts” are certain short passages of text that are listed, as Front-
Cover Texts or Back-Cover Texts, in the notice that says that the Document is released
under this License. A Front-Cover Text may be at most 5 words, and a Back-Cover
Text may be at most 25 words.
A “Transparent” copy of the Document means a machine-readable copy, represented
in a format whose specification is available to the general public, that is suitable
for revising the document straightforwardly with generic text editors or (for images
composed of pixels) generic paint programs or (for drawings) some widely available
drawing editor, and that is suitable for input to text formatters or for automatic translation
to a variety of formats suitable for input to text formatters. A copy made in
an otherwise Transparent file format whose markup, or absence of markup, has been
arranged to thwart or discourage subsequent modification by readers is not Transparent.
An image format is not Transparent if used for any substantial amount of text.
A copy that is not “Transparent” is called “Opaque”.
Examples of suitable formats for Transparent copies include plain ASCII without
markup, Texinfo input format, LaTeX input format, SGML or XML using a publicly
available DTD, and standard-conforming simple HTML, PostScript or PDF designed
for human modification. Examples of transparent image formats include PNG, XCF
and JPG. Opaque formats include proprietary formats that can be read and edited only
by proprietary word processors, SGML or XML for which the DTD and/or processing
tools are not generally available, and the machine-generated HTML, PostScript or
PDF produced by some word processors for output purposes only.
The “Title Page” means, for a printed book, the title page itself, plus such following
pages as are needed to hold, legibly, the material this License requires to appear
in the title page. For works in formats which do not have any title page as such,
“Title Page” means the text near the most prominent appearance of the work’s title,
preceding the beginning of the body of the text.
A section “Entitled XYZ” means a named subunit of the Document whose title
either is precisely XYZ or contains XYZ in parentheses following text that translates
XYZ in another language. (Here XYZ stands for a specific section name mentioned
below, such as “Acknowledgements”, “Dedications”, “Endorsements”, or
413
“History”.) To “Preserve the Title” of such a section when you modify the Document
means that it remains a section “Entitled XYZ” according to this definition.
The Document may include Warranty Disclaimers next to the notice which states
that this License applies to the Document. These Warranty Disclaimers are considered
to be included by reference in this License, but only as regards disclaiming warranties:
any other implication that these Warranty Disclaimers may have is void and has no
effect on the meaning of this License.
2. VERBATIM COPYING
You may copy and distribute the Document in any medium, either commercially
or noncommercially, provided that this License, the copyright notices, and the license
notice saying this License applies to the Document are reproduced in all copies, and
that you add no other conditions whatsoever to those of this License. You may not
use technical measures to obstruct or control the reading or further copying of the
copies you make or distribute. However, you may accept compensation in exchange
for copies. If you distribute a large enough number of copies you must also follow the
conditions in section 3.
You may also lend copies, under the same conditions stated above, and you may
publicly display copies.
3. COPYING IN QUANTITY
If you publish printed copies (or copies in media that commonly have printed covers)
of the Document, numbering more than 100, and the Document’s license notice requires
Cover Texts, you must enclose the copies in covers that carry, clearly and legibly, all
these Cover Texts: Front-Cover Texts on the front cover, and Back-Cover Texts on
the back cover. Both covers must also clearly and legibly identify you as the publisher
of these copies. The front cover must present the full title with all words of the title
equally prominent and visible. You may add other material on the covers in addition.
Copying with changes limited to the covers, as long as they preserve the title of the
Document and satisfy these conditions, can be treated as verbatim copying in other
respects.
If the required texts for either cover are too voluminous to fit legibly, you should
put the first ones listed (as many as fit reasonably) on the actual cover, and continue
the rest onto adjacent pages.
If you publish or distribute Opaque copies of the Document numbering more than
100, you must either include a machine-readable Transparent copy along with each
Opaque copy, or state in or with each Opaque copy a computer-network location from
which the general network-using public has access to download using public-standard
network protocols a complete Transparent copy of the Document, free of added material.
If you use the latter option, you must take reasonably prudent steps, when you
begin distribution of Opaque copies in quantity, to ensure that this Transparent copy
will remain thus accessible at the stated location until at least one year after the last
time you distribute an Opaque copy (directly or through your agents or retailers) of
that edition to the public.
It is requested, but not required, that you contact the authors of the Document
well before redistributing any large number of copies, to give them a chance to provide
you with an updated version of the Document.
414 DODATEK C. GNU FREE DOCUMENTATION LICENSE
4. MODIFICATIONS
You may copy and distribute a Modified Version of the Document under the conditions
of sections 2 and 3 above, provided that you release the Modified Version under
precisely this License, with the Modified Version filling the role of the Document, thus
licensing distribution and modification of the Modified Version to whoever possesses
a copy of it. In addition, you must do these things in the Modified Version:
A. Use in the Title Page (and on the covers, if any) a title distinct from that of the
Document, and from those of previous versions (which should, if there were any,
be listed in the History section of the Document). You may use the same title as
a previous version if the original publisher of that version gives permission.
B. List on the Title Page, as authors, one or more persons or entities responsible for
authorship of the modifications in the Modified Version, together with at least
five of the principal authors of the Document (all of its principal authors, if it
has fewer than five), unless they release you from this requirement.
C. State on the Title page the name of the publisher of the Modified Version, as
the publisher.
D. Preserve all the copyright notices of the Document.
E. Add an appropriate copyright notice for your modifications adjacent to the other
copyright notices.
F. Include, immediately after the copyright notices, a license notice giving the public
permission to use the Modified Version under the terms of this License, in the
form shown in the Addendum below.
G. Preserve in that license notice the full lists of Invariant Sections and required
Cover Texts given in the Document’s license notice.
H. Include an unaltered copy of this License.
I. Preserve the section Entitled “History”, Preserve its Title, and add to it an
item stating at least the title, year, new authors, and publisher of the Modified
Version as given on the Title Page. If there is no section Entitled “History” in
the Document, create one stating the title, year, authors, and publisher of the
Document as given on its Title Page, then add an item describing the Modified
Version as stated in the previous sentence.
J. Preserve the network location, if any, given in the Document for public access to
a Transparent copy of the Document, and likewise the network locations given in
the Document for previous versions it was based on. These may be placed in the
“History” section. You may omit a network location for a work that was published
at least four years before the Document itself, or if the original publisher of the
version it refers to gives permission.
K. For any section Entitled “Acknowledgements” or “Dedications”, Preserve the
Title of the section, and preserve in the section all the substance and tone of
each of the contributor acknowledgements and/or dedications given therein.
415
L. Preserve all the Invariant Sections of the Document, unaltered in their text and
in their titles. Section numbers or the equivalent are not considered part of the
section titles.
M. Delete any section Entitled “Endorsements”. Such a section may not be included
in the Modified Version.
N. Do not retitle any existing section to be Entitled “Endorsements” or to conflict
in title with any Invariant Section.
O. Preserve any Warranty Disclaimers.
If the Modified Version includes new front-matter sections or appendices that qualify
as Secondary Sections and contain no material copied from the Document, you
may at your option designate some or all of these sections as invariant. To do this,
add their titles to the list of Invariant Sections in the Modified Version’s license notice.
These titles must be distinct from any other section titles.
You may add a section Entitled “Endorsements”, provided it contains nothing but
endorsements of your Modified Version by various parties–for example, statements of
peer review or that the text has been approved by an organization as the authoritative
definition of a standard.
You may add a passage of up to five words as a Front-Cover Text, and a passage
of up to 25 words as a Back-Cover Text, to the end of the list of Cover Texts in
the Modified Version. Only one passage of Front-Cover Text and one of Back-Cover
Text may be added by (or through arrangements made by) any one entity. If the
Document already includes a cover text for the same cover, previously added by you
or by arrangement made by the same entity you are acting on behalf of, you may not
add another; but you may replace the old one, on explicit permission from the previous
publisher that added the old one.
The author(s) and publisher(s) of the Document do not by this License give permission
to use their names for publicity for or to assert or imply endorsement of any
Modified Version.
5. COMBINING DOCUMENTS
You may combine the Document with other documents released under this License,
under the terms defined in section 4 above for modified versions, provided that you
include in the combination all of the Invariant Sections of all of the original documents,
unmodified, and list them all as Invariant Sections of your combined work in its license
notice, and that you preserve all their Warranty Disclaimers.
The combined work need only contain one copy of this License, and multiple identical
Invariant Sections may be replaced with a single copy. If there are multiple Invariant
Sections with the same name but different contents, make the title of each such section
unique by adding at the end of it, in parentheses, the name of the original author or
publisher of that section if known, or else a unique number. Make the same adjustment
to the section titles in the list of Invariant Sections in the license notice of the
combined work.
In the combination, you must combine any sections Entitled “History” in the various
original documents, forming one section Entitled “History”; likewise combine any
sections Entitled “Acknowledgements”, and any sections Entitled “Dedications”. You
must delete all sections Entitled “Endorsements”.
416 DODATEK C. GNU FREE DOCUMENTATION LICENSE
6. COLLECTIONS OF DOCUMENTS
You may make a collection consisting of the Document and other documents released
under this License, and replace the individual copies of this License in the various
documents with a single copy that is included in the collection, provided that you
follow the rules of this License for verbatim copying of each of the documents in all
other respects.
You may extract a single document from such a collection, and distribute it individually
under this License, provided you insert a copy of this License into the extracted
document, and follow this License in all other respects regarding verbatim copying of
that document.
7. AGGREGATION WITH INDEPENDENT
WORKS
A compilation of the Document or its derivatives with other separate and independent
documents or works, in or on a volume of a storage or distribution medium,
is called an “aggregate” if the copyright resulting from the compilation is not used
to limit the legal rights of the compilation’s users beyond what the individual works
permit. When the Document is included in an aggregate, this License does not apply
to the other works in the aggregate which are not themselves derivative works of the
Document.
If the Cover Text requirement of section 3 is applicable to these copies of the
Document, then if the Document is less than one half of the entire aggregate, the
Document’s Cover Texts may be placed on covers that bracket the Document within
the aggregate, or the electronic equivalent of covers if the Document is in electronic
form. Otherwise they must appear on printed covers that bracket the whole aggregate.
8. TRANSLATION
Translation is considered a kind of modification, so you may distribute translations
of the Document under the terms of section 4. Replacing Invariant Sections with
translations requires special permission from their copyright holders, but you may
include translations of some or all Invariant Sections in addition to the original versions
of these Invariant Sections. You may include a translation of this License, and all the
license notices in the Document, and any Warranty Disclaimers, provided that you
also include the original English version of this License and the original versions of
those notices and disclaimers. In case of a disagreement between the translation and
the original version of this License or a notice or disclaimer, the original version will
prevail.
If a section in the Document is Entitled “Acknowledgements”, “Dedications”, or
“History”, the requirement (section 4) to Preserve its Title (section 1) will typically
require changing the actual title.
9. TERMINATION
You may not copy, modify, sublicense, or distribute the Document except as expressly
provided for under this License. Any other attempt to copy, modify, sublicense or
distribute the Document is void, and will automatically terminate your rights under
417
this License. However, parties who have received copies, or rights, from you under this
License will not have their licenses terminated so long as such parties remain in full
compliance.
10. FUTURE REVISIONS OF THIS LICENSE
The Free Software Foundation may publish new, revised versions of the GNU Free
Documentation License from time to time. Such new versions will be similar in spirit
to the present version, but may differ in detail to address new problems or concerns.
See http://www.gnu.org/copyleft/.
Each version of the License is given a distinguishing version number. If the Document
specifies that a particular numbered version of this License “or any later version”
applies to it, you have the option of following the terms and conditions either of that
specified version or of any later version that has been published (not as a draft) by
the Free Software Foundation. If the Document does not specify a version number of
this License, you may choose any version ever published (not as a draft) by the Free
Software Foundation.
ADDENDUM: How to use this License for your
documents
To use this License in a document you have written, include a copy of the License
in the document and put the following copyright and license notices just after the title
page:
Copyright
c YEAR YOUR NAME. Permission is granted to copy, distribute
and/or modify this document under the terms of the GNU Free
Documentation License, Version 1.2 or any later version published by the
Free Software Foundation; with no Invariant Sections, no Front-Cover Texts,
and no Back-Cover Texts. A copy of the license is included in the
section entitled “GNU Free Documentation License”.
If you have Invariant Sections, Front-Cover Texts and Back-Cover Texts, replace
the “with . . . Texts.” line with this:
with the Invariant Sections being LIST THEIR TITLES, with the Front-
Cover Texts being LIST, and with the Back-Cover Texts being LIST.
If you have Invariant Sections without Cover Texts, or some other combination of
the three, merge those two alternatives to suit the situation.
If your document contains nontrivial examples of program code, we recommend
releasing these examples in parallel under your choice of free software license, such as
the GNU General Public License, to permit their use in free software.