Zanurkuj w Pythonie/Wersja do druku
Z Wikibooks, biblioteki wolnych podręczników.
Aktualna, edytowalna wersja tego podręcznika jest dostępna w Wikibooks, bibliotece wolnych podręczników pod adresem
http://pl.wikibooks.org/wiki/Zanurkuj_w_Pythonie
Całość tekstu jest objęta licencją GNU Free Documentation License.
[edytuj] Spis treści
- Instalacja
- Pierwszy program
- Wbudowane typy danych
- Potęga introspekcji
- Obiekty i klasy
- Wyjątki i operacje na plikach
- Wyrażenia regularne
- Przetwarzanie HTML-a
- Przetwarzanie XML-a
- Skrypty i strumienie
- HTTP
- SOAP
- Testowanie jednostkowe
- Testowanie 2
- Refaktoryzacja
- Programowanie funkcyjne
- Funkcje dynamiczne
- Optymalizacja szybkości
- GNU Free Documentation License
[edytuj] O podręczniku
Podręcznik ten powstaje na podstawie książki Dive into Python (w większości jest to tłumaczenie), której autorem jest Mark Pilgrim, a udostępnionej na licencji GNU Free Documentation License.
[edytuj] Autorzy i tłumacze
- Mark Pilgrim (autor książki Dive into Python)
- Warszk
- Piotr Kieć
- Roman Frołow
- Andrzej Saski
- Adam Kubiczek
[edytuj] Kody źródłowe
Kody źródłowe programów przedstawionych w tym podręczniku znajdują się na specjalnej podstronie. Ponieważ źródła są oparte na źródłach z podręcznika "Dive Into Python", zatem do nauki można też wykorzystywać źródła ze strony Dive into Python (nieco się różnią od prezentowanych tutaj, ale funkcjonalność mają podobną).
[edytuj] Instalacja
Witamy w Pythonie. W tym rozdziale zajmiemy się instalacją Pythona.
[edytuj] Który Python jest dla ciebie najlepszy?
Aby móc korzystać z Pythona, najpierw należy go zainstalować. A może już go mamy?
Jeżeli posiadasz konto na jakimkolwiek serwerze, istnieje duże prawdopodobieństwo, że Python jest tam już zainstalowany. Wiele popularnych dystrybucji Linuksa standardowo instaluje ten język programowania. Systemy Mac OS X 10.2 i nowsze posiadają dosyć okrojoną wersję Pythona dostępnego jedynie z poziomu linii poleceń. Zapewne będziesz chciał zainstalować wersję, która da Ci więcej możliwości.
Windows domyślnie nie zawiera żadnej wersji Pythona, ale nie załamuj się! Istnieje wiele sposobów, by w łatwy sposób zainstalować Pythona w tym systemie operacyjnym.
Jak widzisz, wersje Pythona są dostępne na wiele platform i systemów operacyjnych. Możemy zdobyć Pythona zarówno dla Windowsa, Mac OS, Mac OS X, wszystkich wariantów Uniksa, w tym Linuksa czy Solarisa, jak i dla AmigaOS, AROS, OS/2, BeOS, czy też innych systemów, o których najprawdopodobniej nawet nie słyszałeś.
Co najważniejsze, program napisany w Pythonie na jednej platformie, przy zachowaniu niewielkiej dozy ostrożności, zadziała na jakiejkolwiek innej. Możesz na przykład rozwijać swój program pod Windowsem, a następnie przenieść go do Linuksa.
Wracając do pytania rozpoczynającego sekcję, "Który Python jest dla ciebie najlepszy?". Odpowiedź jest jedna: jakikolwiek, który możesz zainstalować na posiadanym komputerze.
[edytuj] Python w Windowsie
W Windowsie mamy parę sposobów zainstalowania Pythona.
Firma ActiveState tworzy instalator Pythona zwany ActivePython. Zawiera on kompletną wersje Pythona, IDE z bardzo dobrym edytorem kodu oraz kilka rozszerzeń dla Windowsa, które zapewniają dostęp do specyficznych dla Windowsa usług, API oraz rejestru.
ActivePython można pobrać nieodpłatnie, ale nie jest produktem Open Source. Wydawany jest kilka miesięcy po wersji oryginalnej.
Drugą opcją jest instalacja "oficjalnej" wersji Pythona, rozprowadzanej przez ludzi, którzy rozwijają ten język. Jest to wersja ogólnodostępna, Open Source i zawsze najnowsza.
[edytuj] Instalacja ActivePythona
Oto procedura instalacji ActivePythona:
- Ściągamy ActivePythona ze strony http://www.activestate.com/Products/ActivePython/.
- Jeżeli używamy Windows 95/98/ME/NT4/2000, będziemy musieli najpierw zainstalować Windows Installer 2.0 dla Windowsa 95/98/Me lub Windows Installer 2.0 dla Windowsa NT4/2000.
- Klikamy dwukrotnie na ściągnięty plik ActivePython-(pobrana wersja)-win32-ix86.msi
- Przechodzimy wszystkie kroki instalatora.
- Po zakończeniu instalacji wybieramy Start->Programy->ActiveState ActivePython 2.2->PythonWin IDE. Zobaczymy wtedy ekran z napisem podobnym do poniższego:
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.
>>>
[edytuj] Instalacja Pythona z Python.org
- Pobieramy z http://www.python.org/download/ najnowszą wersję instalatora dla Windowsa, który oczywiście będzie miał rozszerzenie .exe.
- Klikamy dwukrotnie na instalatorze Python-2.xxx.yyy.msi. Nazwa zależeć będzie od ściągniętej wersji Pytona.
- Jeżeli używamy Windows 95/98/ME/NT4/2000, będziemy musieli najpierw zainstalować Windows Installer 2.0 dla Windowsa 95/98/Me lub Windows Installer 2.0 dla Windowsa NT4/2000.
- Przechodzimy przez wszystkie kroki instalatora.
- Jeżeli nie mamy uprawnień administratora, możemy wybrać Advanced Options, a następnie Non-Admin Install.
- Po zakończeniu instalacji, wybieramy Start->Programy->Python 2.x->IDLE (Python GUI). Zobaczymy ekran z napisem podobnym do poniższego:
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
>>>
[edytuj] Python w Mac OS X
W Mac OS X możemy mieć Pythona na dwa sposoby: instalując go lub nie robiąc tego. Zapewne będziesz chciał go zainstalować.
Mac OS X 10.2 i nowsze domyślnie instalują okrojoną wersję Pythona dostępnego jedynie z linii poleceń. Jeżeli nie przeszkadza Ci praca w linii poleceń, to początkowo taka wersja może Tobie wystarczyć. Jednak nie posiada ona parsera XML, więc jeśli dojdziesz do rozdziału mówiącego na ten temat i tak będziesz musiał zainstalować pełną wersję.
Zamiast więc używać domyślnie zainstalowanej wersji, lepiej będzie od razu zainstalować najnowszą, a która też dostarczy nam wygodną, graficzną powłokę.
[edytuj] Uruchamianie wersji domyślnie zainstalowanej z systemem
- Otwieramy katalog /Applications
- Otwieramy katalog Utilities
- Klikamy dwukrotnie na Terminal, by otworzyć okienko terminala, które zapewni nam dostęp do linii poleceń.
- Wpisujemy polecenie python.
Powinniśmy otrzymać mniej więcej takie coś:
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%
[edytuj] Instalacja najnowszej wersji Pythona
Aby to zrobić postępujemy według poniższych kroków:
- Ściągamy obraz dysku MacPython-OSX z http://homepages.cwi.nl/~jack/macpython/download.html.
- Jeżeli pobrany program nie zostanie uruchomiony przez przeglądarkę, klikamy dwukrotnie na MacPython-OSX-(pobrana wersja).dmg by zamontować obraz dysku w systemie.
- Klikamy dwukrotnie na instalator MacPython-OSX.pkg.
- Instalator poprosi o login i hasło użytkownika z prawami administratora.
- Przechodzimy wszystkie kroki instalatora.
- Po zakończonej instalacji otwieramy katalog /Applications.
- Otwieramy katalog MacPython-2.x.
- Klikamy dwukrotnie na PythonIDE by uruchomić Pythona.
MacPython IDE wyświetli ekran powitalny, a następnie interaktywną powłokę. Jeżeli jednak powłoka się nie pojawi, wybieramy Window->Python Interactive (Cmd-0). Otwarte okienko powinno wyglądać 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, domyślnie zainstalowana wersja Pythona nadal pozostanie w systemie. Podczas uruchamiania skryptów zwróć uwagę z jakiej wersji korzystasz.
[edytuj] Dwie wersje Pythona w Mac OS X
[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%
[edytuj] Instalacja Pythona z MacPortów
Ta metoda jest najlepsza. Należy wpierw pobrać i zainstalować MacPorts (http://www.macports.org). Następnie należy odświeżyć porty
sudo port selfupdate
Potem możemy wyszukiwać interesujące nas pakiety. Np. znalezienie wszystkich pakietów do Pythona 2.5.x:
port search py25
Właściwa instalacja Pythona:
sudo port install python25
Wszystkie programy instalowane tą metodą są składowane w /opt/local. Warto więc dodać do ścieżki PATH /opt/local/bin.
Dobrze jest też doinstalować setuptools, który daje dostęp do pythonowego instalatora pakietów, skryptu easy_install.
sudo port install py25-setuptools
Przydaje się, gdy nie ma w portach pakietu dla naszej wersji Pythona, np. IPythona. Część bibliotek można instalować MacPortami, a resztę za pomocą easy_setup. Na przykład IPythona doinstalujemy za pomocą:
sudo easy_install ipython
Można też aktualizować pakiety:
sudo easy_install -U Pylons
Duże i małe znaki w nazwach pakietów, w wypadku użycia easy_install, nie mają znaczenia.
[edytuj] Python w Mac OS 9
Mac OS 9 nie posiada domyślnie żadnej wersji Pythona, ale samodzielna instalacja jest bardzo prosta.
- Ściągamy plik MacPython23full.bin z http://homepages.cwi.nl/~jack/macpython/download.html.
- Jeżeli plik nie zostanie automatycznie rozpakowany przez przeglądarkę, klikamy dwukrotnie na MacPython23full.bin by to zrobić.
- Klikamy dwukrotnie instalator MacPython23full.
- Przechodzimy wszystkie kroki instalatora.
- Po zakończonej instalacji otwieramy katalog /Applications.
- Otwieramy katalog MacPython-OS9 2.x.
- Kliknij dwukrotnie na Python IDE by uruchomić Pythona.
MacPython IDE wyświetli ekran powitalny, a następnie interaktywną powłokę. Jeżeli jednak powłoka się nie pojawi, wybieramy Window->Python Interactive (Cmd-0). Otwarte okienko powinno wyglądać 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
>>>
[edytuj] Python w dystrybucjach Linuksa
Instalacja z gotowych pakietów binarnych dla konkretnej dystrybucji Linuksa jest stosunkowo prosta. Większość dystrybucji posiada już zainstalowaną wersję Pythona. Możesz także pokusić się o instalację ze źródeł.
Wiele dystrybucji Linuksa zawiera graficzne narzędzia służące do instalacji oprogramowania. My jednak opiszemy, jak to zrobić w konsoli w wybranych dystrybucjach Linuksa.
[edytuj] Python w dystrybucji Red Hat Linux
Możemy zainstalować Pythona wykorzystując 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.
>>> [wciśnij Ctrl+D, żeby wyjść 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.
>>> [wciśnij Ctrl+D, żeby wyjść z programu]
[root@localhost root]# which python2.3 #(3)
/usr/bin/python2.3
- Wpisując polecenie python zostaje uruchomiony Python. Jednak jest to starsza jego wersja, domyślnie zainstalowana wraz z systemem. To nie jest to, czego chcemy.
- Podczas pisania tej książki najnowszą wersją był Python 2.3. Za pomocą polecenia python2.3 uruchomimy nowszą, właśnie zainstalowaną wersje.
- Jest to pełna ścieżka do nowszej wersji Pythona, którą dopiero co zainstalowaliśmy.
[edytuj] Python w dystrybucji Debian
Pythona zainstalujemy wykorzystując 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. >>> [wciśnij Ctrl+D, żeby wyjść z programu]
[edytuj] Python w dystrybucji Mandriva
W konsoli z użytkownika root wpisujemy polecenie:
$ su - Password: [wpisz hasło roota] # urpmi python
[edytuj] Python w dystrybucji Fedora/Fedora Core
Aby zainstalować Pythona w dystrybucji Fedora/Fedora Core należy w konsoli wpisać:
$ su - Password: [wpisz hasło roota] # yum install python
Można też zainstalować Pythona przy instalacji systemu, wybierając pakiety programistyczne.
[edytuj] Python w dystrybucji Gentoo GNU/Linux
W Gentoo do instalacji Pythona możemy użyć programu emerge:
$ su - Password: [wpisz hasło roota] # emerge python
aczkolwiek, jako że narzędzie emerge (należące do pakietu sys-apps/portage) napisane jest w Pythonie, użytkownicy tej dystrybucji dostają Pythona wprost z pudełka.
[edytuj] Python w dystrybucji Arch Linux
Instalacja pythona w dystrybucji Arch Linux jest dziecinnie prosta, ogranicza się do jednego polecenia:
# Musisz podbić uprawniwnia np. za pomocą su lub sudo pacman -S python
Wystarczy potwierdzić chęć instalacji i poczekać chwilę:
rozwiązywanie zależności...
szukanie konfliktów międzypakietowych...
Celuje (1): python-2.6.2-4 [15,01 MB]
Całkowity rozmiar do pobrania: 15,01 MB
Całkowity rozmiar po instalacji: 61,38 MB
Kontynuować instalację? [T/n] t
:: Pobieranie pakietów z extra..
python-2.6.2-4-i686 15,01M 746,6K/s 00:00:20 [###################] 100%
sprawdzanie spójności pakietów...
(1/1) sprawdzanie konfliktów plików [###################] 100%
(1/1) instalowanie python [###################] 100%
Opcjonalne zależności dla python
tk: for IDLE, pynche and modulator
[edytuj] Instalacja ze źródeł
Jeżeli wolimy zainstalować Pythona ze źródeł, będziemy musieli pobrać kod źródłowy z http://www.python.org/ftp/python/. Wybieramy najnowszą wersję (najwyższy numer) i ściągamy plik .tgz, a następnie 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.
>>> [wciśnij Ctrl+D, żeby wyjść z programu]
localhost:~$
[edytuj] Interaktywna powłoka
Teraz kiedy już mamy zainstalowanego Pythona, pewnie się zastanawiamy, co to jest ta interaktywna powłoka (interactive shell), którą uruchomiliśmy?
Powiedzmy tak: Python umożliwia pracę na dwa sposoby. Jest interpreterem skryptów, które możemy uruchomić z linii poleceń lub, jak inne aplikacje, dwukrotnie klikając na ikonce skryptu, a także jest interaktywną powłoką, dzięki której możemy debugować, sprawdzać działanie potrzebnych funkcji czy możliwości Pythona. Możesz nawet potraktować powłokę jako kalkulator!
Uruchom teraz powłokę Pythona w odpowiedni sposób dla twojego systemu i sprawdźmy co ona potrafi:
>>> 1 + 1 #(1) 2 >>> print 'hello world' #(2) hello world >>> x = 1 #(3) >>> y = 2 >>> x + y 3
- Interaktywna powłoka Pythona może wyliczać dowolne wyrażenia matematyczne.
- Potrafi wykonywać dowolne polecenia Pythona.
- Możemy przypisać pewne wartości do zmiennych, które są pamiętane tak długo, jak długo jest uruchomiona nasza powłoka (ale nie dłużej).
[edytuj] Podsumowanie
W tym momencie powinniśmy już mieć zainstalowanego Pythona.
W zależności od platformy możesz mieć zainstalowaną więcej niż jedną wersję Pythona. Jeżeli tak właśnie jest to musisz uważać na ścieżki dostępu do programu. Pisząc tylko python w linii poleceń, może się okazać, że nie uruchamiasz wersji, której akurat potrzebujesz. Być może będziesz musiał podawać pełną ścieżkę dostępu lub odpowiednią nazwę (python2.2, python2.4 itp.)
Gratulujemy i witamy w Pythonie!
[edytuj] Pierwszy Program
Czy dostrzegliście, że większość książek najpierw przedstawia elementarne zasady programowania, a potem opisuje, jak, korzystając z nich, stworzyć kompletny i działający program? My zrobimy inaczej...
[edytuj] Nurkujemy
Oto kompletny, działający program w Pythonie. Prawdopodobnie jest on dla Ciebie całkowicie niezrozumiały, ale nie przejmuj się tym, ponieważ zaraz przeanalizujemy go dokładnie, linia po linii. Przeczytaj go i sprawdź, czy coś jesteś w stanie z niego zrozumieć.
#-*- coding: utf-8 -*-
def buildConnectionString(params):
u"""Tworzy łańcuch znaków na podstawie słownika parametrów.
Zwraca łańcuch 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)
Uruchomimy teraz ten program i zobaczymy, co się stanie.
| W IDE ActivePythona w systemie Windows możemy uruchomić edytowany program wybierając File->Run... (Ctrl-F5). Wynik wyświetlany jest w interaktywnym oknie. |
| W systemach Unix (także w Mac OS X) możesz uruchomić program z linii poleceń poleceniem python odbchelper.py |
W wyniku uruchomienia programu otrzymujemy:
pwd=secret;database=master;uid=sa;server=mpilgrim
[edytuj] Deklarowanie funkcji
Python, podobnie jak wiele innych języków programowania, posiada funkcje, lecz nie definiujemy ich w oddzielnych plikach nagłówkowych (jak np. w C++), czy też nie dzielimy na sekcje interfejsu i implementacji jak w Pascalu. Jeśli potrzebujemy jakiejś funkcji, po prostu ją deklarujemy, na przykład:
def buildConnectionString(params):
Słowo kluczowe def rozpoczyna deklarację funkcji, następnie podajemy nazwę funkcji, a potem, w nawiasach, parametry. Większą liczbę parametrów podajemy po przecinkach.
Jak widać funkcja nie definiuje zwracanego typu. Podczas deklarowania pythonowych funkcji nie określamy, czy mają one zwracać jakąś wartość, a nawet czy mają cokolwiek zwracać. W rzeczywistości każda funkcja zwraca pewną wartość. Jeżeli w funkcji znajduje się instrukcja return, funkcja zwróci określoną wartość, wskazaną za pomocą tej instrukcji. W przeciwnym wypadku (gdy dana funkcja nie posiada instrukcji return) zostanie zwrócona wartość None, czyli tak zwana wartość "pusta", w innych językach często określana jako null lub nil.
Argument params nie ma określonego typu. W Pythonie typów zmiennych nie określamy w sposób jawny. Interpreter sam automatycznie rozpoznaje i śledzi typ zmiennej.
| W Javie, C++ i innych językach statycznie typowanych musimy określić typ danych zwracany przez funkcję, a także typ każdego argumentu. W Pythonie nigdzie tego nie robimy. Bazując na wartości jaka została przypisana zmiennej, Python sam określa jej typ. |
[edytuj] Typy danych w Pythonie a inne języki programowania
Jeśli chodzi o nadawanie typów, języki programowania można podzielić na:
- statycznie typowane
- Są to języki, w których typy są nadawane podczas kompilacji. Wiele tego typu języków programowania wymaga deklarowania wszystkich zmiennych przed ich użyciem, przez podanie ich typu. Przykładami takich języków jest Java, czy też C.
- dynamicznie typowane
- Są to języki, w których typy zmiennych są nadawane podczas działania programu. VBScript i Python są językami dynamicznie typowanymi, ponieważ nadają one typ zmiennej podczas przypisania do niej wartości.
- silnie typowane
- Są to języki, w których między różnymi typami widać wyraźną granicę. Jeśli mamy pewną liczbę całkowitą, to nie możemy jej traktować jak łańcuch znaków bez wcześniejszej konwersji.
- słabo typowane
- Są to języki, w których możemy nie zwracać uwagi na typ zmiennej. Do takich języków zaliczymy VBScript. Możemy w nim, nie wykonując żadnej wyraźnej konwersji, połączyć łańcuch znaków
'12'z liczbą całkowitą3otrzymując łańcuch'123', a następnie potraktować go jako liczbę całkowitą123. Konwersja jest wykonywana automatycznie.
Python jest językiem zarówno dynamicznie typowanym (ponieważ nie wymaga wyraźnej deklaracji typu), jak i silnie typowanym (ponieważ zmienne posiadają wyraźnie ustalone typy, które nie podlegają automatycznej konwersji).
[edytuj] Dokumentowanie funkcji
Funkcje można dokumentować wstawiając notkę dokumentacyjną (ang. doc string).
buildConnectionString
def buildConnectionString(params):
u"""Tworzy łańcuch znaków na podstawie słownika parametrów.
Zwraca łańcuch znaków.
"""
return ";".join(["%s=%s" % (k, v) for k, v in params.items()])
Trzy następujące po sobie cudzysłowy wykorzystuje się do tworzenia ciągu znaków, który może zajmować wiele linii. Wszystko co się znajduje pomiędzy nimi tworzy pojedynczy łańcuch znaków; dotyczy to także znaków powrotu karetki i cudzysłowu. Potrójne cudzysłowy możemy używać wszędzie, ale najczęściej wykorzystuje się je do tworzenia notek dokumentacyjnych.
Za pomocą potrójnych cudzysłowów ("""...""") możemy w łatwy sposób zdefiniować łańcuch znaków zawierający pojedyncze jak i podwójne cudzysłowy, podobnie jak w przypadku qq/../ w Perlu. |
Dzięki przedrostkowi u Python zapamięta ten łańcuch znaków w taki sposób, że będzie potrafił poprawnie zinterpretować polskie litery. Więcej szczegółów na ten temat dowiemy się w dalszej części tej książki.
W powyższym przykładzie wszystko pomiędzy potrójnymi cudzysłowami jest notką dokumentacyjną funkcji, czyli opisem do czego dana funkcja służy i jak ją używać. Notka dokumentacyjna, o ile istnieje, musi znaleźć się pierwsza w definicji funkcji (czyli zaraz po dwukropku). Python nie wymaga, aby funkcja posiadała notkę dokumentacyjną, lecz powinniśmy ją zawsze tworzyć. Ułatwia ona nam, a także innym, zorientować się w programie. Warto zaznaczyć, że notka dokumentacyjna jest dostępna jako atrybut funkcji nawet w trakcie wykonywania programu.
[edytuj] Materiały dodatkowe
Materiały co prawda w języku angielskim, ale na pewno warto je chociaż przejrzeć:
- PEP 257, na temat konwencji notek dokumentacyjnych,
- Python Style Guide, o tym, jak napisać dobrą notkę dokumentacyjną,
[edytuj] Wszystko jest obiektem
Wspomnieliśmy już wcześniej, że funkcje w Pythonie posiadają atrybuty, które są dostępne podczas pracy programu.
Funkcje, podobnie jak wszystko inne w Pythonie, są obiektami.
Otwórz swój ulubiony IDE Pythona i wprowadź następujący kod:
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 łańcuch znaków na podstawie słownika parametrów.
Zwraca łańcuch znaków.
- Pierwsza linia importuje program
odbchelperjako moduł -- kawałek kodu, który możemy używać interaktywnie. (W Rozdziale 4 zobaczymy przykłady programów podzielonych na wiele modułów.) Kiedy już zaimportujemy moduł, możemy odwołać się do jego wszystkich publicznych funkcji, klas oraz atrybutów. Moduły także mogą odwoływać się do jeszcze innych modułów. - Aby wykorzystać jakąś funkcję zdefiniowaną w zaimportowanym module, musimy przed nazwą funkcji dołączyć nazwę modułu. Nie możemy napisać
buildConnectionString, lecz zamiast tego możemy daćodbchelper.buildConnectionString. Jeśli kiedykolwiek korzystaliśmy z klas w Javie, powinniśmy zauważyć pewne podobieństwa. - Tym razem zamiast wywoływać funkcję, poprosiliśmy o dostęp do jednego z jej atrybutów -- atrybut
__doc__. W nim Python przechowuje notkę dokumentacyjną.
import w Pythonie działa podobnie jak require w Perlu. Kiedy zaimportujemy jakiś moduł, odwołujemy się do jego funkcji poprzez modul.funkcja. W Perlu wygląda to troszkę inaczej, piszemy modul::funkcja. |
[edytuj] Ścieżka przeszukiwania modułów
Zanim przejdziemy dalej, należy wspomnieć o ścieżce przeszukiwania modułów. W Pythonie przeglądanych jest kilka miejsc w poszukiwaniu importowanego modułu. Generalnie przeszukiwane są wszystkie katalogi zdefiniowane w sys.path. Jest to lista, którą możemy w łatwy sposób przeglądać i modyfikować w podobny sposób jak inne listy (jak to robić dowiemy się w kolejnych rozdziałach).
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)
- Zaimportowanie modułu
sysspowoduje, że wszystkie jego funkcje i atrybuty stają się dostępne. sys.pathto lista nazw katalogów, które są obecnie przeszukiwane podczas importowania modułu. (Zawartość listy zależna jest od systemu operacyjnego, wersji Pythona i położenia jego instalacji, więc na twoim komputerze może wyglądać nieco inaczej.) Python przeszuka te katalogi (w zadanej kolejności) w poszukiwaniu pliku .py, który nazywa się tak samo jak importowany moduł.- Właściwie to trochę rozminęliśmy się z prawdą. Sytuacja jest bardziej skomplikowana, ponieważ nie wszystkie moduły występują jako pliki z rozszerzeniem .py. Niektóre, tak jak
syssą wbudowane w samego Pythona. Wbudowane moduły zachowują się w ten sam sposób co pozostałe, ale nie mamy bezpośredniego dostępu do ich kodu źródłowego, ponieważ nie są napisane w Pythonie (modułsysnapisany jest w C). - Kiedy dodamy nowy katalog do ścieżki przeszukiwania, Python przy następnych importach przejrzy dodatkowo dodany katalog w poszukiwaniu modułu z rozszerzeniem .py. Nowy katalog będzie znajdował się w ścieżkach szukania tak długo, jak długo uruchomiony będzie interpreter. (Dowiesz się więcej o metodzie
appendi innych metodach list w kolejnym rozdziale).
[edytuj] Co to jest obiekt
W Pythonie wszystko jest obiektem i prawie wszystko posiada metody i atrybuty. Każda funkcja posiada wbudowany atrybut __doc__, który zwraca napis dokumentacyjny zdefiniowany w kodzie funkcji. Moduł sys jest obiektem, który posiada między innymi atrybut path.
W dalszym ciągu nie wyjaśniliśmy jeszcze, co to jest obiekt. Każdy język programowania definiuje "obiekt" w inny sposób. W niektórych językach "obiekt" musi posiadać atrybuty i metody, a w innych wszystkie "obiekty" mogą dzielić się na różne podklasy. W Pythonie jest inaczej, niektóre obiekty nie posiadają ani atrybutów ani metod (więcej o tym w kolejnym rozdziale) i nie wszystkie obiekty dzielą się na podklasy (więcej o tym w rozdziale 5). Wszystko jest obiektem w tym sensie, że może być przypisane do zmiennej albo stanowić argument funkcji (więcej o tym w rozdziale 4).
Ponieważ jest to bardzo ważne, więc powtórzmy to jeszcze raz: wszystko w Pythonie jest obiektem. Łańcuchy znaków to obiekty, listy to obiekty, funkcje to obiekty, a nawet moduły to obiekty...
[edytuj] Materiały dodatkowe
- Python Reference Manual dokładnie opisuje, co to znaczy, że wszystko w Pythonie jest obiektem
- eff-bot podsumowuje obiekty Pythona
[edytuj] Wcięcia kodu
Funkcje w Pythonie nie posiadają sprecyzowanych początków i końców oraz żadnych nawiasów służących do zaznaczania, gdzie funkcja się zaczyna, a gdzie kończy. Jedynym separatorem jest dwukropek (:) i wcięcia kodu.
buildConnectionString
def buildConnectionString(params):
u"""Tworzy łańcuch znaków na podstawie słownika parametrów.
Zwraca łańcuch znaków.
"""
return ";".join(["%s=%s" % (k, v) for k, v in params.items()])
Bloki kodu definiujemy poprzez wcięcia. Przez "blok kodu" rozumiemy funkcje, instrukcje if, pętle for i while i tak dalej. Wstawiając wcięcie zaczynamy blok, a kończymy go przestając wstawiać wcięcia danej wielkości. Nie ma żadnych nawiasów, klamer czy słów kluczowych. Oznacza to, że białe znaki (spacje itp.) mają znaczenie i ich stosowanie musi być konsekwentne. W powyższym przykładzie kod funkcji (włączając w to notkę dokumentacyjną) został wcięty czterema spacjami. Nie musimy stosować konkretnie czterech spacji, jednak musimy być konsekwentni (tzn. jeśli pierwsze wcięcie w funkcji miało 3 spacje, to kolejne wcięcia także muszą mieć 3 spacje). Linia bez wcięcia znajdować się będzie poza funkcją.
if
def silnia(n): #(1)
print 'n =', n #(2)
if n > 1: #(3)
return n * silnia(n - 1)
else: #(4)
print 'koniec'
return 1
- Powyższa funkcja, nazwana
silniaprzyjmuje jeden argument:n. Cały kod wewnątrz funkcji jest wcięty. - Wypisywanie danych (na standardowe wyjście) jest bardzo proste, wystarczy użyć słowa kluczowego
print. Wyrażenieprintmoże przyjąć każdy typ danych, na przykład łańcuchy znaków, liczby całkowite i inne wbudowane typy danych jak słowniki i listy, o których dowiemy się w następnym rozdziale. Możemy nawet drukować na ekran różne wartości w jednej linii. W tym celu podajemy ciąg wartości, które chcemy wyświetlić, oddzielając je przecinkiem. Każda wartość jest wtedy wyświetlana w tej samej linii i oddzielona spacją (znak przecinka nie jest drukowany). Tak więc, kiedy funkcjęsilniawywołamy z argumentem5, na ekranie zobaczymy "n = 5". - Do bloku kodu zaliczamy także instrukcje
if. Jeżeli wyrażenie za instrukcjąifbędzie prawdziwe, to zostanie wykonany wcięty kod znajdujący się zaraz pod instrukcjąif. W przeciwnym wypadku wykonywany jest blokelse. - Oczywiście bloki
iforazelsemogą składać się z większej ilości linii, o ile linie te mają wcięcia z równą ilością spacji. Tutaj blokelsema dwie linie. Python nie wymaga żadnej specjalnej składni dla bloków składających się z wielu linii. Po prostu robimy wcięcia o równej liczbie spacji.
Po początkowych problemach i nietrafionych porównaniach do Fortranu, pogodzisz się z tym i zobaczysz pozytywne cechy wcięć. Jedną z głównych zalet jest to, że wszystkie programy w Pythonie wyglądają podobnie, ponieważ wcięcia kodu są wymagane przez sam język i nie zależą od stylu pisania. Dzięki temu jakikolwiek kod jest prostszy do czytania i zrozumienia.
[edytuj] Materiały dodatkowe
- Python Reference Manual omawia niektóre problemy związane z wcięciami kodu.
- Python Style Guide mówi na temat dobrego stylu tworzenia wcięć.
[edytuj] Testowanie modułów
Moduły Pythona to obiekty, które posiadają kilka przydatnych atrybutów. Możemy ich użyć do łatwego testowania własnych modułów. Oto przykład zastosowania triku if __name__:
if __name__ == "__main__":
Zwróćmy uwagę na dwie ważne sprawy: pierwsza, że instrukcja warunkowa if nie wymaga nawiasów, a druga, że if kończy się dwukropkiem, a w następnych liniach wstawiamy wcięty kod.
Na czym polega zastosowany trik? Moduły to obiekty, a każdy z nich posiada wbudowany atrybut __name__. __name__ zależy od tego, w jaki sposób korzystamy z danego modułu. Jeżeli importujemy moduł, wtedy __name__ jest nazwą pliku modułu bez ścieżki do katalogu, czy rozszerzenia pliku. Możemy także uruchomić bezpośrednio moduł jako samodzielny program, a wtedy __name__ przyjmie domyślną wartość "__main__".
>>> import odbchelper
>>> odbchelper.__name__
'odbchelper'
Wiedząc o tym, możemy zaprojektować test wewnątrz swojego modułu, wykorzystując do tego instrukcję if. Jeśli uruchomisz bezpośrednio dany moduł, atrybut __name__ jest równy "__main__", a więc test zostanie wykonany. Podczas importu tego modułu, __name__ przyjmie inną wartość, więc test się nie uruchomi. Pomoże nam to rozwijać i wyszukiwać błędy w programie (czyli debugować), zanim dany moduł zintegrujemy z większym programem.
| Aby w MacPythonie można było skorzystać z tego triku, należy kliknąć w czarny trójkąt w prawym górnym rogu okna i upewnić się, że zaznaczona jest opcja Run as __main__. |
[edytuj] Materiały dodatkowe
- Python Reference Manual omawia szczegóły dotyczące importowania modułów, istnieje także polskie tłumaczenie.
[edytuj] Wbudowane typy danych
Za moment powrócimy do naszego pierwszego programu, jednak najpierw musimy zrozumieć, czym są słowniki, krotki i listy. Jeśli choć trochę umiesz programować w Perlu, to pewnie masz już pewną wiedzę na temat słowników, czy list, ale pewnie nie wiesz, czym są krotki. Najpierw zajmiemy się jednym z najczęściej używanych typów danych, czyli łańcuchami znaków.
[edytuj] Łańcuchy znaków i unikod
Łańcuchy znaków służą do przechowywania napisu lub pewnych danych bajtowych. W Pythonie, podobnie jak w większości innych języków programowania tworzymy łańcuchy znaków [1] (ang. string) poprzez umieszczenie danego tekstu w cudzysłowach.
>>> text = "Nie za długi tekst" #(1) >>> text 'Nie za d\xc5\x82ugi 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 się w następnej linii' #(5) >>> print text3 Długi tekst, który po przecinku znajdzie się w następnej linii >>> text4 = r'Tutaj znaki specjalne np.\n \t, czy też \x26 nie zostaną zinterpretowane' #(6) >>> print text4 Tutaj znaki specjalne np.\n \t, czy też \x26 nie zostaną zinterpretowane
- W ten sposób stworzyliśmy łańcuch znaków
"Nie za długi tekst", który z kolei przypisaliśmy do zmiennejtext. Zauważmy, że wewnątrz łańcucha mogą się znajdować polskie litery. - Otrzymany w tym miejscu wynik na wyjściu może się nieco różnić na różnych systemach. Jest to zależne od kodowania znaków w systemie operacyjnym, z którego korzystamy. Komputer uruchomiony przez autorów korzystał z kodowania UTF-8. Zauważmy, że litera ł w systemie UTF-8 to dwa bajty
"\xc5\x82". - Wszystko zostało ładnie wypisane. Tam gdzie jest ł widzimy ł, a nie jakieś "krzaki".
- Łańcuch znaków nie musi być deklarowany w podwójnym cudzysłowie, ale może być też to pojedynczy cudzysłów.
- Znaki specjalne wstawiamy dodając odwrotny ukośnik (tzw. backslash) np. \n.
- Możemy także stworzyć łańcuch znaków w tzw. sposób surowy. Aby to uczynić, poprzedzamy łańcuch znaków literą r. Wewnątrz surowego łańcucha znaków odwrotny ukośnik nie jest interpretowany. Można powiedzieć, że znaki specjalne w takim łańcuchu nie są znakami specjalnymi. To co napiszemy, to będziemy mieli.
[edytuj] Unikod
Ponieważ mówimy w języku polskim, piszemy w tym języku, a ponadto czytamy w tym języku, zapewne chcielibyśmy tworzyć programy, które dobrze sobie dają radę ze znakami tego języka. Doskonałym do tego rozwiązaniem jest unikod (ang. unicode). Unikod przechowuje nie tylko polskie znaki, ale jest systemem reprezentowania znaków ze wszystkich języków świata
>>> text = u"Nie za długi tekst" #(1) >>> text u'Nie za d\u0142ugi tekst' #(2) >>> print text Nie za długi tekst
- Aby utworzyć unikodowy napis, dodajemy przedrostek u i tyle.
- Otrzymasz taki sam wynik. Dane przechowywane w unikodzie nie zależą od systemu kodowania, z którego korzysta Twój komputer.
Pamiętamy, jak w poprzednim rozdziale powiedzieliśmy, że do notek dokumentacyjnych został dodany przedrostek u, aby Python potrafił poprawnie zinterpretować polskie znaki. Wtedy właśnie wykorzystaliśmy unikod.
buildConnectionString
def buildConnectionString(params):
u"""Tworzy łańcuch znaków na podstawie słownika parametrów.
Zwraca łańcuch znaków.
"""
[edytuj] Słowniki
Jednym z wbudowanych typów są słowniki (ang. dictionary). Określają one wzajemną relację między kluczem, a wartością.
Słowniki w Pythonie są podobne do haszy w Perlu. W Perlu zmienne przechowujące hasz są reprezentowane zawsze przez początkowy znak %. W Pythonie typ danych jest automatycznie rozpoznawany. Pythonowy słownik przypomina ponadto instancję klasy Hashtable w Javie, a także instancję obiektu Scripting.Dictionary w Visual Basicu. |
[edytuj] 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'
- Najpierw utworzyliśmy nowy słownik z dwoma elementami i przypisaliśmy go do zmiennej
d. Każdy element w słowniku jest parą klucz-wartość, a zbiór elementów jest ograniczony nawiasem klamrowym. 'server'jest kluczem, a skojarzoną z nim wartością jest'mpilgrim', do której odwołujemy się poprzezd["server"].'database'jest kluczem, a skojarzoną z nim wartością jest'master', do której odwołujemy się poprzezd["database"].- Możesz dostać się do wartości za pomocą klucza, ale nie możesz dostać się do klucza za pomocą wartości. Tak więc
d["server"]zwraca'mpilgrim', ale wywołanied["mpilgrim"]sprawi, że zostanie rzucony wyjątek, ponieważ'mpilgrim'nie jest kluczem słownikad.
[edytuj] Modyfikowanie słownika
>>> d {'database': 'master', 'server': 'mpilgrim'} >>> d["database"] = "pubs" #(1) >>> d {'database': 'pubs', 'server': 'mpilgrim'} >>> d["uid"] = "sa" #(2) >>> d {'database': 'pubs', 'uid': 'sa', 'server': 'mpilgrim'}
- Klucze w słowniku nie mogą się powtarzać. Przypisując wartość do istniejącego klucza, będziemy nadpisywać starszą wartość.
- W każdej chwili możesz dodać nową parę klucz-wartość. Składnia jest identyczna do tej, która modyfikuje istniejącą wartość. Czasami może to spowodować pewien problem. Możemy myśleć, że dodaliśmy nową wartość do słownika, ale w rzeczywistości nadpisaliśmy już istniejącą.
Zauważmy, że słownik nie przechowuje kluczy w sposób posortowany. Wydawałoby się, że klucz 'uid' powinien się znaleźć za kluczem 'server', ponieważ litera s jest wcześniej w alfabecie niż u. Jednak tak nie jest. Elementy słownika znajdują się w losowej kolejności.
Pracując ze słownikami pamiętajmy, że wielkość liter kluczy ma znaczenie.
>>> d = {}
>>> d["klucz"] = "wartość"
>>> d["klucz"] = "inna wartość" #(1)
>>> d
{'klucz': 'inna wartość'}
>>> d["Klucz"] = "jeszcze inna wartość" #(2)
>>> d
{'klucz': 'inna wartość', 'Klucz': 'jeszcze inna wartość'}
- Przypisując wartość do istniejącego klucza zamieniamy starą wartość na nową.
- Nie jest to przypisanie do istniejącego klucza, a ponieważ łańcuchy znaków w Pythonie są wrażliwe na wielkość liter, dlatego też
'klucz'nie jest tym samym co'Klucz'. Utworzyliśmy nową parę klucz-wartość w słowniku. Obydwa klucze mogą się wydawać podobne, ale dla Pythona są zupełnie inne.
>>> d {'bazadanych': 'pubs', 'serwer': 'mpilgrim', 'uid': 'sa'} >>> d["licznik"] = 3 #(1) >>> d {'bazadanych': 'pubs', 'serwer': 'mpilgrim', 'licznik': 3, 'uid': 'sa'} >>> d[42] = "douglas" #(2) >>> d {42: 'douglas', 'bazadanych': 'pubs', 'serwer': 'mpilgrim', 'licznik': 3, 'uid': 'sa'}
- Słowniki nie są przeznaczone tylko dla łańcuchów znaków. Wartość w słowniku może być dowolnym typem danych: łańcuchem znaków, liczbą całkowitą, obiektem, a nawet innym słownikiem. W pojedynczym słowniku wszystkie wartości nie muszą być tego samego typu; możemy wstawić do niego wszystko, co chcemy.
- Klucze w słowniku są bardziej restrykcyjne, ale mogą być łańcuchami znaków, liczbami całkowitymi i kilkoma innymi typami. Klucze wewnątrz jednego słownika nie muszą posiadać tego samego typu.
[edytuj] Usuwanie pozycji ze słownika
>>> d {'licznik': 3, 'bazadanych': 'master', 'serwer': 'mpilgrim', 42: 'douglas', 'uid': 'sa'} >>> del d[42] #(1) >>> d {'licznik': 3, 'bazadanych': 'master', 'serwer': 'mpilgrim', 'uid': 'sa'} >>> d.clear() #(2) >>> d {}
- Instrukcja
delkaże usunąć określoną pozycję ze słownika, która jest wskazywana przez podany klucz. - Instrukcja
clearusuwa wszystkie pozycje ze słownika. Zbiór pusty ograniczony przez nawiasy klamrowe oznacza, że słownik nie ma żadnego elementu.
[edytuj] Materiały dodatkowe
- How to Think Like a Computer Scientist uczy o słownikach i pokazuje, jak wykorzystać słowniki do tworzenia rzadkich macierzy.
- W Python Knowledge Base możemy znaleźć wiele przykładów kodów wykorzystujących słowniki.
- Python Cookbook wyjaśnia, jak sortować wartości słownika względem klucza.
- Python Library Reference opisuje wszystkie metody słownika.
[edytuj] Listy
Listy są jednym z najważniejszych typów danych. Można się z nimi spotkać w Visual Basicu. W tym języku są określane jako tablice.
Listy przypominają tablice w Perlu. W Perlu zmienna, która przechowuje listę rozpoczyna się od znaku @, natomiast w Pythonie nazwa może być dowolna, ponieważ Python automatycznie rozpoznaje typ. |
[edytuj] 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'
- Najpierw zdefiniowaliśmy listę pięcioelementową. Zauważmy, że lista zachowuje swój oryginalny porządek i nie jest to przypadkowe. Lista jest uporządkowanym zbiorem elementów ograniczonym nawiasem kwadratowym.
- Lista może być używana tak jak tablica zaczynająca się od 0. Pierwszym elementem niepustej listy o nazwie
lijest zawszeli[0]. - Ostatnim elementem pięcioelementowej listy jest
li[4], ponieważ indeksy są liczone zawsze od 0.
>>> li ['a', 'b', 'mpilgrim', 'z', 'przykład'] >>> li[-1] #(1) 'przykład' >>> li[-3] #(2) 'mpilgrim'
- Za pomocą ujemnych indeksów odnosimy się do elementów idących od końca do początku tzn.
li[-1]oznacza ostatni element,li[-2]przedostatni,li[-3]odnosi się do 3 od końca elementu itd. Ostatnim elementem niepustej listy jest zawszeli[-1]. - Jeśli ciężko ci zrozumieć o co w tym wszystkim chodzi, możesz pomyśleć o tym w ten sposób:
li[-n] == li[len(li) - n].lento funkcja zwracająca ilość elementów listy. Tak więc w tym przypadkuli[-3] == li[5 - 3] == li[2].
>>> 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']
- Możesz pobrać podzbiór listy, który jest nazywany "wycinkiem" (ang. slice), poprzez określenie dwóch indeksów. Zwracaną wartością jest nowa lista zawierająca wszystkie elementy z listy rozpoczynające się od pierwszego wskazywanego indeksu (w tym przypadku
li[1]) i idąc w górę kończy na drugim wskazywanym indeksie, nie dołączając go (w tym przypadkuli[3]). Kolejność elementów względem wcześniejszej listy jest także zachowana. - Możemy także podać ujemną wartość któregoś indeksu. Wycinanie wtedy także dobrze zadziała. Jeśli to pomoże, możemy pomyśleć tak: czytamy listę od lewej do prawej, pierwszy indeks określa pierwszy potrzebny element, a drugi określa element, którego nie chcemy. Zwracana wartość zawiera wszystko między tymi dwoma przedziałami.
- Listy są indeksowane od zera tzn. w tym przypadku
li[0:3]zwraca pierwsze trzy elementy listy, rozpoczynając odli[0], a kończąc nali[2], ale nie dołączającli[3].
>>> 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']
- Jeśli lewy indeks wynosi
0, możemy go opuścić, wartość0jest domyślna.li[:3]jest tym samym, coli[0:3]z poprzedniego przykładu. - Podobnie, jeśli prawy indeks jest długością listy, możemy go pominąć. Tak więc
li[3:]jest tym samym, coli[3:5], ponieważ lista ta posiada pięć elementów. - Zauważmy pewną symetryczność. W pięcioelementowej liście
li[:3]zwraca pierwsze 3 elementy, ali[3:]zwraca dwa ostatnie (a w sumie 3 + 2 = 5). W rzeczywistościli[:n]będzie zwracał zawsze pierwszenelementów, ali[n:]pozostałą liczbę, bez względu na szerokość listy (nmoże być większe od długości listy). - Jeśli obydwa indeksy zostaną pominięte, wszystkie elementy zostaną dołączone. 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 kompletną kopię listy.
[edytuj] 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']
- Dodajemy pojedynczy element do końca listy za pomocą metody
append. - Za pomocą
insertwstawiamy pojedynczy element do listy. Numeryczny argument jest indeksem, pod którym ma się znaleźć wstawiana wartość; reszta elementów, która znajdowała się pod tym indeksem lub miała większy indeks, zostanie przesunięta o jeden indeks dalej. Zauważmy, że elementy w liście nie muszą być unikalne i mogą się powtarzać; w przykładzie mamy dwa oddzielne elementy z wartością'nowy'--li[2]i li[6]. - Za pomocą
extendłączymy listę z inną listą. Nie możemy wywołaćextendz wieloma argumentami, trzeba ją wywoływać z pojedynczym argumentem -- listą. W tym przypadku ta lista ma dwa elementy.
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) 4 >>> li[-1] ['d', 'e', 'f']
- Listy posiadają dwie metody --
extendiappend, które wyglądają na to samo, ale w rzeczywistości są całkowicie różne.extendwymaga jednego argumentu, który musi być listą i dodaje każdy element z tej listy do oryginalnej listy. - Rozpoczęliśmy z listą trójelementową (
'a','b'i'c') i rozszerzyliśmy ją o inne trzy elementy ('d','e'i'f') za pomocąextend, tak więc mamy już sześć elementów. appendwymaga jednego argumentu, który może być dowolnym typem danych. Metoda ta po prostu dodaje dany element na koniec listy. Wywołaliśmyappendz jednym argumentem, który jest listą z trzema elementami.- Teraz oryginalna lista, pierwotnie zawierająca trzy elementy, zawiera ich cztery. Dlaczego cztery? Ponieważ ostatni element przed chwilą do niej wstawiliśmy. Listy mogą wewnątrz przechowywać dowolny typ danych, nawet inne listy. Nie używajmy
append, jeśli zamierzamy listę rozszerzyć o kilka elementów.
[edytuj] 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
indexznajduje pierwsze wystąpienie pewnej wartość w liście i zwraca jego indeks.indexznajduje pierwsze wystąpienie wartości w liście. W tym przykładzie,'nowy'występuje dwa razy -- wli[2]i li[6], ale metodaindexbędzie zawsze zwracać pierwszy indeks, czyli2.- Jeśli wartość nie zostanie znaleziona, Python zgłosi wyjątek. Takie zachowanie nie jest często spotykane w innych językach, w wielu językach w takich przypadkach zostaje zwrócony niepoprawny indeks. Takie zachowanie Pythona jest dosyć dobrym posunięciem, ponieważ umożliwia szybkie wychwycenie błędu w kodzie, a dzięki temu program nie będzie błędnie działał operując na niewłaściwym indeksie.
- Aby sprawdzić czy jakaś wartość jest w liście używamy słowa kluczowego
in, który zwracaTrue, jeśli wartość zostanie znaleziona lubFalsejeśli nie.
Wszystkie powyższe punkty stosowane są w Pythonie 2.2.1 i nowszych, ale obecnie można także używać typu logicznego bool, który może przyjmować wartość True (prawda) lub False (fałsz). Zwróćmy uwagę, że wartości te, tak jak cała składnia języka Python, są wrażliwe na wielkość liter.
[edytuj] 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']
removeusuwa pierwszą występującą wartość w liście.removeusuwa tylko pierwszą występującą wartość. W tym przypadku'nowy'występuje dwa razy, aleli.remove("nowy")usuwa tylko pierwsze wystąpienie.- Jeśli wartość nie zostanie znaleziona w liście, Python wygeneruje wyjątek. Naśladuje on w takich sytuacjach postępowanie metody
index. popjest ciekawą metodą, która wykonuje dwie rzeczy: usuwa ostatni element z listy i zwraca jego wartość. Metoda ta różni się odli[-1]tym, żeli[-1]zwraca jedynie wartość, ale nie zmienia listy, a odli.remove(value)tym, żeli.remove(value)zmienia listę, ale nie zwraca wartości.
[edytuj] Używanie operatorów 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]
- Aby połączyć dwie listy, można też skorzystać z operatora
+. Za pomocąlista = lista + innalistauzyskujemy ten sam wynik, co za pomocąlist.extend(innalista), ale operator+zwraca nową listę, podczas gdyextendzmienia tylko istniejącą listę. Ogólnieextendjest szybszy, szczególnie na dużych listach. - Python posiada także operator
+=. Operacjali += ['dwa']jest równoważnali.extend(['dwa']). Operator+=działa zarówno na listach, łańcuchach znaków jak i może być nadpisany dla dowolnego innego typu danych. - Operator
*zwielokrotnia podaną listę.li = [1, 2] * 3jest równoważne zli = [1, 2] + [1, 2] + [1, 2], które łączy trzy listy w jedną.
[edytuj] Materiały dodatkowe
- How to Think Like a Computer Scientist uczy podstaw związanych z wykorzystywaniem list, a także nawiązuje do przekazywania listy jako argument funkcji.
- Python Tutorial pokazuje, że listy można wykorzystywać jako stos lub kolejkę.
- Python Knowledge Base odpowiada na często zadawane pytania dotyczące list, a także posiada także wiele przykładów kodów źródłowych wykorzystujących listy.
- Python Library Reference opisuje wszystkie metody, które zawiera lista.
[edytuj] Krotki
Krotka (ang. tuple) jest niezmienną listą. Zawartość krotki określamy tylko podczas jej tworzenia. Potem nie możemy już jej zmienić.
>>> 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')
- Krotki definiujemy w identyczny sposób jak listę, lecz z jednym wyjątkiem -- zbiór elementów jest ograniczony w nawiasach okrągłych, zamiast w kwadratowych.
- Podobnie jak w listach, elementy w krotce mają określony porządek. Są one indeksowane od
0, więc pierwszym elementem w niepustej krotce jest zawszet[0]. - Ujemne indeksy idą od końca krotki, tak samo jak w listach.
- Krotki także można wycinać. Kiedy wycinamy listę, dostajemy nową listę. Podobnie, gdy wycinamy krotkę dostajemy nową krotkę.
>>> 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("z") #(3) 3 >>> "z" in t #(4) True
- Nie można dodawać elementów do krotki. Krotki nie posiadają metod typu
append, czy teżextend. - Nie można usuwać elementów z krotki. Nie posiadają one ani metody
removeani metodypop. - Można wyszukiwać elementy w krotce wykorzystując metodę
index. - Można wykorzystać operator
in, aby sprawdzić, czy krotka zawiera dany element.
To w końcu do czego są te krotki przydatne?
- Krotki działają szybciej niż listy. Jeśli definiujemy stały zbiór wartości, który będzie używany tylko do iteracji, skorzystajmy z krotek zamiast z listy.
- Jeśli chcielibyśmy używać danych "zabezpieczonych przed zapisem" (np. po to, żeby program był bezpieczniejszy), wykorzystajmy do tego krotki. Korzystając z krotek, zamiast z list, mamy pewność, że dane w nich zawarte nie zostaną nigdzie zmienione. To trochę tak, jakbyśmy mieli gdzieś ukrytą instrukcję
assertsprawdzającą, czy dane są stałe. W przypadku, gdy nastąpi próba nadpisania pewnych wartości w krotce, program zwróci wyjątek. - Pamiętasz, jak powiedzieliśmy, że klucze w słowniku mogą być łańcuchami znaków, liczbami całkowitymi i "kilkoma innymi typami"? Krotki są jednym z tych "innych typów". W przeciwieństwie do list, mogą one zostać użyte jako klucz w słowniku. Dlaczego? Jest to dosyć skomplikowane. Klucze w słowniku muszą być niezmienne. Krotki same w sobie są niezmienne, jednak jeśli wewnątrz krotki znajdzie się lista, to krotka ta nie będzie mogła zostać użyta jako klucz, ponieważ lista jest zmienna. W takim przypadku krotka nie byłaby słownikowo-bezpieczna. Aby krotka mogła zostać wykorzystana jako klucz, musi ona zawierać wyłącznie łańcuchy znaków, liczby i inne słownikowo-bezpieczne krotki.
- Krotki używane są do formatowania tekstu, co zobaczymy wkrótce.
[edytuj] Materiały dodatkowe
- How to Think Like a Computer Scientist uczy, czym są krotki i pokazuje, jak łączyć je ze sobą.
- Python Knowledge Base pokazuje, jak posortować krotkę.
- Python Tutorial omawia, jak zdefniować krotkę z jednym elementem.
[edytuj] Deklarowanie zmiennych
Wiemy już trochę o słownikach, krotkach i o listach, więc wrócimy do przykładowego kodu przedstawionego w rozdziale drugim, do odbchelper.py.
Podobnie jak większość języków programowania Python posiada zarówno zmienne lokalne jak i globalne, choć nie deklarujemy ich w jakiś wyraźny sposób. Zmienne zostają utworzone, gdy przypisujemy im pewne wartości. Natomiast kiedy zmienna wychodzi poza zasięg, zostaje automatycznie usunięta.
myParams
if __name__ == "__main__":
myParams = {"server":"mpilgrim", \
"database":"master", \
"uid":"sa", \
"pwd":"secret" \
}
Zwróćmy uwagę na wcięcia. Instrukcje warunkowe jako bloki kodu są identyfikowane za pomocą wcięć, podobnie jak funkcje.
Zauważmy też, że dzięki wykorzystaniu backslasha ("\") mogliśmy przypisanie wartości do zmiennej podzielić na kilka linii. Backslashe w Pythonie są specjalnymi znakami, które umożliwiają kontynuację danej instrukcji w następnej linii.
| Kiedy polecenie zostanie podzielone na kilka linii za pomocą znaku kontynuacji ("\"), następna linia może zostać wcięta w dowolny sposób. Python nie weźmie tego wcięcia pod uwagę. |
Ściśle mówiąc, wyrażenia w nawiasach okrągłych, kwadratowych i klamrowych (jak definiowanie słowników) można podzielić na wiele linii bez używania znaku kontynuacji ("\"). Niektórzy zalecają dodawać backslashe nawet wtedy, gdy nie jest to konieczne. Argumentują to tym, że kod staje się wtedy czytelniejszy. Jest to jednak wyłącznie kwestia gustu.
Wcześniej nigdy nie deklarowaliśmy żadnej zmiennej o nazwie myParams, ale właśnie przypisaliśmy do niej wartość. Zachowanie to przypomina trochę VBScript bez instrukcji option explicit. Na szczęście, w przeciwieństwie do VBScript, Python nie pozwala odwoływać się do zmiennych, do których nie zostały wcześniej przypisane żadne wartości. Jeśli spróbujemy to zrobić, Python rzuci wyjątek.
[edytuj] Odwoływanie się do zmiennych
>>> x Traceback (most recent call last): File "<stdin>", line 1, in ? NameError: name 'x' is not defined >>> x = 1 >>> x 1
Kiedyś będziesz za to dziękować...
[edytuj] Wielozmienne przypisania
Jednym z lepszych Pythonowych skrótów jest wielozmienne przypisanie (ang. multi-variable assignment), czyli jednoczesne (za pomocą jednego wyrażenia) przypisywanie kilku wartości do kilku zmiennych.
>>> v = ('a', 'b', 'e')
>>> (x, y, z) = v #(1)
>>> x
'a'
>>> y
'b'
>>> z
'e'
vjest krotką trzech elementów, a(x, y, z)jest krotką trzech zmiennych. Przypisując jedną krotkę do drugiej, przypisaliśmy każdą z wartościvdo odpowiednich zmiennych (w odpowiedniej kolejności).
Może to zostać wykorzystane w wielu sytuacjach. Czasami chcemy przypisać pewnym zmiennym pewien zakres wartości np. od 1 do 10. W języku C możemy utworzyć typy wyliczeniowe (enum) poprzez ręczne utworzenie listy stałych i wartości jakie przechowują. Może to być trochę nudną i czasochłonną robotą, w szczególności gdy wartości są kolejnymi liczbami. W Pythonie możemy wykorzystać wbudowaną funkcję range i wielozmienne przypisanie. W ten sposób z łatwością przypiszemy kolejne wartości do wielu zmiennych.
>>> 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
- Wbudowana funkcja
rangezwraca listę liczb całkowitych. W najprostszej formie funkcja ta bierze górną granicę i zwraca listę liczb od0do podanej granicy (ale już bez niej). (Możemy także ustawić początkową wartość różną niż0, a krok może być inny niż1. Aby otrzymać więcej szczegółów wykorzystaj instrukcjęprint range.__doc__.) PONIEDZIALEK,WTOREK,SRODA,CZWARTEK,PIATEK,SOBOTAiNIEDZIELAsą zmiennymi, które zdefiniowaliśmy. (Ten przykład pochodzi z modułucalendar; nazwy zostały spolszczone. Modułcalendarumożliwia wyświetlanie kalendarzy, podobnie jak to robi Uniksowy program cal. Modułcalendarprzechowuje dla odpowiednich dni tygodnia odpowiednie stałe.)- Teraz każda zmienna ma własną wartość:
PONIEDZIALEKma0,WTOREKma1itd.
Wielozmienne przypisania możemy wykorzystać przy tworzeniu funkcji zwracających wiele wartości w postaci krotki. Zwróconą wartość takiej funkcji możemy potraktować jako normalną krotkę lub też przypisać wszystkie elementy tej krotki do osobnych zmiennych za pomocą wielozmiennego przypisania. Wiele standardowych bibliotek korzysta z tej możliwości np. moduł os, który omówimy w rozdziale 6.
[edytuj] Materiały dodatkowe
- Python Reference Manual pokazuje przykłady, kiedy można pominąć znak kontynuacji linii, a kiedy musisz go wykorzystać.
- How to Think Like a Computer Scientist wyjaśnia, jak za pomocą wielozmiennych przypisań zamienić wartości dwóch zmiennych.
[edytuj] Formatowanie łańcucha znaków
W Pythonie możemy formatować wartości wewnątrz łańcucha znaków. Chociaż możemy tworzyć bardzo skomplikowane wyrażenia, jednak najczęściej w prostych przypadkach wystarczy wykorzystać pole %s, aby wstawić pewien łańcuch znaków wewnątrz innego.
Formatowanie łańcucha znaków w Pythonie używa tej samej składni, co funkcja sprintf w języku C. |
>>> k = "uid"
>>> v = "sa"
>>> "%s=%s" % (k, v) #(1)
'uid=sa'
- Rezultatem wykonania tego wyrażenia jest łańcuch znaków. Pierwsze %s zostało zastąpione wartością znajdującą się w
k, a drugie wystąpienie %s zostało zastąpione wartościąv. Wszystkie inne znaki (w tym przypadku znak równości) pozostały w miejscach, w których były.
Zauważmy, że (k, v) jest krotką. Niedługo zobaczymy, do czego to może być przydatne.
Mogłoby się wydawać, że formatowanie jest jedną z wielu możliwości połączenia łańcuchów znaków, jednak formatowanie łańcucha znaków nie jest tym samym co łączenie łańcuchów.
>>> 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 "Użytkowników: %d" % (userCount, ) #(3) (4) Użytkowników: 6 >>> print "Użytkowników: " + userCount #(5) Traceback (most recent call last): File "<stdin>", line 1, in ? TypeError: cannot concatenate 'str' and 'int' objects
+jest operatorem łączącym łańcuchy znaków.- W tym trywialnym przypadku formatowanie daje ten sam wynik co łączenie.
(userCount, )jest jednoelementową krotką. Składnia ta wygląda trochę dziwnie, jednak takie oznaczenie jest jednoznaczne i wiadomo, że chodzi o krotkę. Zawsze można dołączać przecinek po ostatnim elemencie listy, krotki lub słownika. Jest on jednak wymagany tylko podczas tworzenia jednoelementowej krotki. Jeśli przecinek nie byłby wymagany, Python nie mógłby stwierdzić czy(userCount)ma być krotką, czy też tylko wartością zmiennejuserCount.- Formatowanie łańcucha znaków działa z liczbami całkowitymi. W tym celu używamy %d zamiast %s.
- Spróbowaliśmy połączyć łańcuch znaków z czymś, co nie jest łańcuchem znaków. Został rzucony wyjątek. W przeciwieństwie do formatowania, łączenie łańcucha znaków możemy wykonywać jedynie na innych łańcuchach.
Tak jak sprintf w C, formatowanie tekstu w Pythonie przypomina szwajcarski scyzoryk. Mamy mnóstwo opcji do wyboru, a także wiele pól dla różnych typów wartości.
>>> 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
- Pole formatowania %f traktuje wartość jako liczbę rzeczywistą i pokazuje ją z dokładnością do 6 miejsc po przecinku.
- Modyfikator ".2" pola %f umożliwia pokazywanie wartości rzeczywistej z dokładnością do dwóch miejsc po przecinku.
- Można nawet łączyć modyfikatory. Dodanie modyfikatora + pokazuje plus albo minus przed wartością, w zależności od tego jaki znak ma liczba. Modyfikator ".2" został na swoim miejscu i nadal nakazuje wyświetlanie liczby z dokładnością dwóch miejsc po przecinku.
[edytuj] Materiały dodatkowe
- Python Library Reference wymienia wszystkie opcje formatowania.
- Effective AWK Programming omawia wszystkie opcje formatowania, a także mówi o innych zaawansowanych technikach.
[edytuj] Odwzorowywanie list
Jedną z bardzo użytecznych cech Pythona są wyrażenia listowe (ang. list comprehension), które pozwalają nam w zwięzły sposób odwzorować pewną listę na inną, wykonując na każdym jej elemencie pewną funkcję.
>>> 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]
- Aby zrozumieć o co w tym chodzi, spójrzmy na to od strony prawej do lewej.
lijest listą, którą odwzorowujemy. Python przechodzi po każdym elemencieli, tymczasowo przypisując wartość każdego elementu do zmiennejelement, a następnie wyznacza wartość funkcjielement*2i wstawia wynik do nowej, zwracanej listy. - Wyrażenia listowe nie zmieniają oryginalnej listy.
- Zwracany wynik możemy przypisać do zmiennej, którą odwzorowujemy. Nie spowoduje to żadnych problemów. Python tworzy nową listę w pamięci, a kiedy operacja odwzorowywania listy dobiegnie końca, do zmiennej zostanie przypisana lista znajdująca się w pamięci.
W funkcji buildConnectionString zadeklarowanej w rozdziale 2 skorzystaliśmy z wyrażeń listowych:
["%s=%s" % (k, v) for k, v in params.items()]
Zauważmy, że najpierw wykonujemy funkcję items, znajdującą się w słowniku params. Funkcja ta zwraca wszystkie dane znajdujące się w słowniku w postaci listy krotek.
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')]
- W słowniku metoda
keyszwraca listę wszystkich kluczy. Lista ta nie jest uporządkowana zgodnie z kolejnością, z jaką definiowaliśmy słownik (pamiętamy, że elementy w słowniku są nieuporządkowane). - Metoda
valueszwraca listę wszystkich wartości. Lista ta jest zgodna z porządkiem listy zwracanej przez metodękeys, czyli dla wszystkich wartościxzachodziparams.values()[x] == params[params.keys()[x]]. - Metoda
itemszwraca listę krotek w formie(klucz, wartość). Lista zawiera wszystkie dane ze słownika.
Spójrzmy jeszcze raz na funkcję buildConnectionString. Przyjmuje ona listę params.items(), odwzorowuje ją na nową listę, korzystając dla każdego elementu z formatowania łańcucha znaków. Nowa lista ma tyle samo elementów co params.items(), lecz każdy element nowej listy jest łańcuchem znaków, który zawiera zarówno klucz, jak i skojarzoną z nim wartość ze słownika params.
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']
- Wykonując iteracje po liście
params.items()używamy dwóch zmiennych. Zauważmy, że w ten sposób korzystamy w pętli z wielozmiennego przypisania. Pierwszym elementemparams.items()jest('server', 'mpilgrim'), dlatego też podczas pierwszej iteracji odwzorowywania listy zmiennakbędzie przechowywała wartość'server', avwartość'mpilgrim'. W tym przypadku ignorujemy wartośćv, dołączając tylko wartość znajdującą się wkdo zwracanej listy. Otrzymamy taki sam wynik, gdy wywołamyparams.keys(). - W tym miejscu wykonujemy to samo, ale zamiast zmiennej
vignorujemy zmiennąk. Otrzymamy taki sam wynik, jakbyśmy wywołaliparams.values(). - Dzięki temu, że przerobiliśmy obydwa poprzednie przykłady i skorzystaliśmy z formatowania łańcucha znaków, otrzymaliśmy listę łańcuchów znaków. Każdy łańcuch znaków tej listy zawiera zarówno klucz, jak i wartość pewnego elementu ze słownika. Wynik wygląda podobnie do wyjścia pierwszego programu. Ponadto porządek został taki sam, jaki był w słowniku.
[edytuj] Materiały dodatkowe
- Python Tutorial omawia inny sposób odwzorowywania listy -- za pomocą wbudowanej funkcji
map. - Python Tutorial pokazuje kilka przykładów, jak można korzystać z wyrażeń listowych.
[edytuj] Łączenie listy i dzielenie łańcuchów znaków
Mamy listę, której elementy są w formie klucz=wartość. Załóżmy, że chcielibyśmy połączyć je wszystkie w pojedynczy łańcuch. Aby to zrobić, wykorzystamy metodę join obiektu typu string.
Poniżej został przedstawiony przykład łączenia listy w łańcuch znaków, który wykorzystaliśmy w funkcji buildConnectionString:
return ";".join(["%s=%s" % (k, v) for k, v in params.items()])
Zanim przejdziemy dalej zastanówmy się nad pewną kwestią. Funkcje są obiektami, łańcuchy znaków są obiektami... wszystko jest obiektem. Można by było dojść do wniosku, że także zmienna jest obiektem, ale to akurat nie jest prawdą. Spójrzmy na ten przykład i zauważmy, że łańcuch znaków ";" sam w sobie jest obiektem i z niego można wywołać metodę join. Zmienne są etykietami dla obiektów.
Metoda join łączy elementy listy w jeden łańcuch znaków, a każdy element w zwracanym łańcuchu jest oddzielony od innego separatorem. W naszym przykładzie jest nim ";", lecz może nim być dowolny łańcuch znaków.
>>> 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'
Powyższy łańcuch znaków otrzymaliśmy podczas uruchamiania odbchelper.py.
W Pythonie znajdziemy także metodę analogiczną do metody join, ale która zamiast łączyć, dzieli łańcuch znaków na listę. Jest to funkcja split.
>>> 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']
split, przeciwieństwo funkcjijoin, dzieli łańcuch znaków na wieloelementową listę. Separator (w przykładzie";") nie będzie występował w żadnym elemencie zwracanej listy, zostanie pominięty.- Do funkcji
splitmożemy dodać opcjonalny drugi argument, który określa, na jaką maksymalną liczbę kawałków ma zostać podzielony łańcuch. (O opcjonalnych argumentach w funkcji dowiemy się w następnym rozdziale.)
[edytuj] Materiały dodatkowe
- Python Knowledge Base odpowiada na często zadawane pytania dotyczące łańcuchów znaków, a także posiada wiele przykładów wykorzystywania łańcuchów znaków.
- Python Library Reference wymienia wszystkie metody łańcuchów znaków.
- The Whole Python FAQ wyjaśnia, dlaczego join jest metodą łańcucha znaków zamiast listy.
[edytuj] Kodowanie znaków
W komputerze pewnym znakom odpowiadają pewne liczby, a kodowanie znaków określa, która liczba odpowiada jakiej literze. W łańcuchu znaków każdy symbol zajmuje 8 bitów, co daje nam do dyspozycji tylko 256 różnych symboli. Podstawowym systemem kodowania jest ASCII. Przyporządkowuje on liczbom z zakresu od 0 do 127 znaki alfabetu angielskiego, cyfry i niektóre inne symbole. Pozostałe standardowe systemy kodowania rozszerzają standard ASCII, dlatego znaki z przedziału od 0 do 127 w każdym systemie kodowania są takie same.
>>> 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)
- Funkcja
ordzwraca liczbę, która odpowiada danemu symbolowi. W tym przypadku literze "a" odpowiada liczba 97. - Za pomocą funkcji
chrdowiadujemy się, jaki znak odpowiada danej liczbie. Liczbie 99 odpowiada znak "c". - Procent ("%") odpowiada liczbie 37.
- Każdy symbol odpowiada liczbie z zakresu od 0 do 255. Liczba 261 nie mieści się w jednym bajcie, dlatego wyskoczył nam wyjątek.
- Co prawda liczba 188 mieści się w 8-bitach, ale nie mieści się w standardzie ASCII i dlatego tego symbolu Python nie może jednoznacznie zinterpretować. W systemie kodowania ISO 8859-2 liczba ta odpowiada znakowi "ź", ale w systemie kodowania Windows-1250 (znany też jako CP-1250) znakowi "Ľ".
Każdy edytor tekstu zapisuje tworzone przez nas programy korzystając z jakiegoś kodowania, choćby z samego ASCII. Dobrze jest korzystać z edytora, który daje nam możliwość ustawienia kodowania znaków. Kiedy wiemy, w jakim systemie kodowania został zapisany nasz skrypt, powinniśmy jeszcze o tym poinformować Pythona.
[edytuj] Informowanie Pythona o kodowaniu znaków
Wróćmy do odbchelper.py. Na samym początku dodaliśmy linię [1]:
#-*- coding: utf-8 -*-
W ten sposób ustawiamy kodowanie znaków danego pliku, a nie całego programu (program może się składać z wielu plików). Zresztą, jeśli nie zdefiniujemy kodowania znaków, Python nas o tym uprzedzi:
sys:1: DeprecationWarning: Non-ASCII character '\xc5' in file test.py on line 5
but no encoding declared; see http://www.python.org/peps/pep-0263.html for detils
Jeśli skorzystaliśmy z innego kodowania znaków, zamiast utf-8 oczywiście napiszemy coś innego. Dodając polskie znaki z reguły korzysta się z kodowania UTF-8 lub ISO-8859-2, a czasami w przypadku Windowsa z Windows-1250.
Ale co wtedy, gdy nie mamy możliwości ustawić kodowania znaków i nie wiemy z jakiego korzysta nasz edytor? Można to sprawdzić metodą prób i błędów:
#-*- coding: {tu wstawiamy utf-8, iso-8859-2 lub windows-1250} -*-
print "zażółć gęślą jaźń"
A może pora zmienić edytor?
[edytuj] Unikod jeszcze raz
Jak wiemy, unikod jest systemem reprezentowania znaków ze wszystkich różnych języków świata.
Zaraz powrócimy do Pythona.
Notatka historyczna. Przed powstaniem unikodu istniały oddzielne systemy kodowania znaków dla każdego języka, a co przed chwilą trochę omówiliśmy. Każdy z nich korzystał z tych samych liczb (0-255) do reprezentowania znaków danego języka. Niektóre języki (jak rosyjski) miały wiele sprzecznych standardów reprezentowania tych samych znaków. Inne języki (np. japoński) posiadają tak wiele znaków, że wymagają wielu bajtów, aby zapisać cały zbiór jego znaków. Wymiana dokumentów pomiędzy tymi systemami była trudna, ponieważ komputer nie mógł stwierdzić, z którego systemu kodowania skorzystał autor. Komputer widział tylko liczby, a liczby mogą oznaczać różne rzeczy. Zaczęto się zastanawiać nad przechowywaniem tych dokumentów w tym samym miejscu (np. w tej samej tabeli bazy danych); trzeba było przechowywać informacje o kodowaniu każdego kawałku tekstu, a także trzeba było za każdym razem informować o kodowaniu przekazywanego tekstu. Wtedy też zaczęto myśleć o wielojęzycznych dokumentach, w których znaki z wielu języków znajdują się w tym samym dokumencie. (Wykorzystywały one zazwyczaj kod ucieczki, aby przełączyć tryb kodowania; ciach, jesteś w rosyjskim trybie, więc 241 znaczy to; ciach, jesteś w greckim trybie, więc 241 znaczy coś innego itd.) Unikod został zaprojektowany po to, aby rozwiązywać tego typu problemy.
Aby rozwiązać te problemy, unikod reprezentuje wszystkie znaki jako 2-bajtowe liczby, od 0 do 65535 [2] Każda 2-bajtowa liczba reprezentuje unikalny znak, który jest wykorzystywany w co najmniej jednym języku świata. (Znaki które są wykorzystywane w wielu językach świata, mają ten sam kod numeryczny.) Mamy dokładnie jedną liczbę na znak i dokładnie jeden znak na liczbę. Dane unikodu nigdy nie są dwuznaczne.
7-bitowy ASCII przechowuje wszystkie angielskie znaki za pomocą liczb z zakresu od 0 do 127. (65 jest wielką literą "A", 97 jest małą literą "a" itd.) Język angielski posiada bardzo prosty alfabet, więc może się całkowicie zmieścić w 7-bitowym ASCII. Języki zachodniej Europy jak język francuski, hiszpański, czy też niemiecki, korzystają z systemu kodowania nazwanego ISO-8859-1 (inne określenie 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 "ñ" (241), czy "ü" (252). Także unikod wykorzystuje te same znaki jak 7-bitowy ASCII dla zakresu od 0 do 127, a także 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 języków.
Kiedy korzystamy z danych w postaci unikodu, może zajść potrzeba przekonwertowania danych na jakiś inny system kodowania np. gdy potrzebujemy współdziałać z innym komputerowym systemem, a który oczekuje danych w określonym 1-bajtowym systemie kodowania, czy też wysłać dane na terminal, który nie obsługuje unikodu, czy też do drukarki.
I po tej notatce, powróćmy do Pythona.
>>> ord(u"ą") 261 #(1) >>> print unichr(378) #(2) ź
- W unikodzie polski znak "ą" jednoznacznie odpowiada cyfrze 261.
- Za pomocą funkcji
unichr, dowiadujemy się jakiemu znakowi odpowiada dana liczba. Liczbie 378 odpowiada polska litera "ź". Python automatycznie zakoduje wypisywany napis unikodowy, aby został poprawnie wyświetlony na naszym systemie.
Dlaczego warto korzystać z unikodu? Jest kilka powodów:
- Unikod bardzo dobrze sobie radzi z różnymi międzynarodowymi znakami.
- Reprezentacja unikodowa jest jednoznaczna; jednej liczbie odpowiada dokładnie jeden znak.
- Nie musimy się zamartwiać szczegółami technicznymi np. czy dwa łańcuchy, które ze sobą łączymy są w takim samym systemie kodowania [3].
- Python potrafi właściwie zinterpretować wszystkie znaki (np. co jest literą, co jest białym znakiem, a co jest cyfrą).
- Korzystając z unikodu zapobiegamy wielu problemom.
Dlatego wszędzie, gdzie będziemy korzystali z polskich znaków, będziemy korzystali z unikodu.
[edytuj] Materiały dodatkowe
- PEP 0263 wyjaśnia, w jaki sposób skonfigurować kodowanie kodu źródłowego.
[edytuj] Praca z unikodem
Unikodowymi napisami posługujemy się w identyczny sposób jak normalnymi łańcuchami znaków.
>>> errmsg = u'Nie można otworzyć pliku' #(1) >>> print errmsg #(2) Nie można otworzyć pliku >>> print errmsg + u', brak dostępu.' #(3) Nie można otworzyć pliku, brak dostępu. >>> errmsg.split() #(4) [u'Nie', u'mo\u017cna', u'otworzy\u0107', u'pliku'] >>> print u"Błąd: %s"%errmsg Błąd: Nie można otworzyć pliku
- Tworzymy unikodowy napis i przypisujemy go do zmiennej
errmsg. - Wypisując dowolny unikod, Python go zakoduje w taki sposób, aby był zgodny z kodowaniem znaków wyjścia, a więc dany napis zostanie zawsze wypisany z polskimi znakami.
- Z unikodem operujemy identycznie, jak z innymi łańcuchami znaków. Możemy na przykład dwa unikody ze sobą łączyć.
- Możemy także podzielić unikod na listę.
- Ponadto, podobnie jak w przypadku standardowego łancucha znaków, możemy też unikod formatować.
[edytuj] Unikod a łańcuchy znaków
Funkcjonalność unikodu możemy łączyć ze standardowymi łańcuchami znaków, o ile z operacji tych jasno wynika, co chcemy osiągnąć.
>>> file = 'myfile.txt' >>> errmsg + ' ' + file #(1) u'Nie mo\u017cna otworzy\u0107 pliku myfile.txt' >>> "%s %s"%(errmsg, file) #(2) u'Nie mo\u017cna otworzy\u0107 pliku myfile.txt' >>> errmsg + ', brak dostępu.' #(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łąd: %s"%errmsg #(4) 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)
- Unikod możemy połączyć z łańcuchem znaków. Powstaje nowy napis unikodowy.
- Możemy formatować łańcuch znaków korzystając z unikodowych wartości. Tu także powstaje nowy unikod.
- Python rzucił wyjątek; nie potrafi przekształcić napisu
', brak dostępu'na napis unikodowy. Z unikodem możemy łączyć jedynie łańcuchy znaków w systemie ASCII (czyli zawierające jedynie angielskie litery i kilka innych symboli np. przecinki, kropki itp.). W tym przypadku Python nie wie, z jakiego kodowania korzystamy. - Tutaj mamy analogiczną sytuację. Python nie potrafi przekształcić napisu
'Błąd: %s'na unikod i rzuca wyjątek.
Python zakłada, że kodowaniem wszystkich łańcuchów znaków jest ASCII [4], dlatego jeśli mieszamy tekst unikodowy z łańcuchami znaków, powinniśmy dopilnować, aby łańcuchy znaków zawierały znaki należące do ASCII (czyli nie mogą posiadać polskich znaków).
[edytuj] encode i decode
A co wtedy, gdy chcemy przekształcić unikodowy napis na łańcuch znaków? Łańcuchy znaków są jakoś zakodowane, więc aby stworzyć łańcuch znaków, musimy go na coś zakodować np. ISO 8859-2, czy też UTF-8. W tym celu korzystamy z metody encode unikodu.
encode
>>> errmsg.encode('iso-8859-2') #(1)
'Nie mo\xbfna otworzy\xe6 pliku'
>>> errmsg.encode('utf-8')
'Nie mo\xc5\xbcna otworzy\xc4\x87 pliku' #(2)
- Za pomocą metody
encodeinformujemy Pythona na jakie kodowanie znaków chcemy zakodować dany łańcuch znaków. W tym przypadku otrzymujemy łańcuch znaków zakodowany w systemie ISO 8859-2. - Tutaj otrzymujemy ten sam napis, ale zakodowany w systemie UTF-8.
Operację odwrotną, czyli odkodowania, wykonujemy za pomocą funkcji decode. Na przykład:
decode
>>> msg = errmsg.encode('utf-8') #(1)
>>> msg.decode('utf-8') #(2)
u'Nie mo\u017cna otworzy\u0107 pliku'
>>> print msg.decode('utf-8')
Nie można otworzyć pliku
- W tym miejscu zakodowaliśmy napis
errmsgna UTF-8. - Za pomocą metody
decodeodkodowaliśmy zakodowany łańcuch znaków i zwrócony został unikod.
[edytuj] Podsumowanie
Teraz już powinniśmy wiedzieć w jaki sposób działa program odbchelper.py i zrozumnieć, dlaczego otrzymaliśmy takie wyjście.
def buildConnectionString(params):
u"""Tworzy łańcuchów znaków na podstawie słownika parametrów.
Zwraca łańcuch 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 wyjściu z programu otrzymujemy:
pwd=secret;database=master;uid=sa;server=mpilgrim
Zanim przejdziemy do następnego rozdziału, upewnijmy się czy potrafimy:
- używać IDE Pythona w trybie interaktywnym
- napisać program i uruchamiać go przy pomocy twojego IDE lub z linii poleceń
- tworzyć łańcuchy znaków i napisy unikodowe
- importować moduły i wywoływać funkcje w nich zawarte
- deklarować funkcje, używać napisów dokumentacyjnych (ang. docstring), zmiennych lokalnych, a także używać odpowiednich wcięć
- definiować słowniki, krotki i listy
- dostawać się do atrybutów i metod dowolnego obiektu, włączając w to łańcuchy znaków, listy, słowniki, funkcje i moduły
- łączyć wartości poprzez formatowanie łańcucha znaków
- odwzorowywać listę na inną listę
- dzielić łańcuch znaków na listę i łączyć listę w łańcuch znaków
- poinformować Pythona, z jakiego kodowania znaków korzystamy
- wykorzystywać metody
encodeidecode, aby przekształcić unikod w łańcuch znaków.
[edytuj] Potęga introspekcji
W tym rozdziale dowiemy się o jednej z mocnych stron Pythona -- introspekcji. Jak już wiemy, wszystko w Pythonie jest obiektem, natomiast introspekcja jest kodem, który postrzega funkcje i moduły znajdujące się w pamięci jako obiekty, a także pobiera o nich informacje i operuje nimi.
[edytuj] Nurkujemy
Zacznijmy od kompletnego, działającego programu. Przeglądając kod na pewno rozumiesz już niektóre jego fragmenty. Przy niektórych liniach znajdują się liczby w komentarzach; korzystamy tu z koncepcji, które wykorzystywaliśmy już w rozdziale drugim. Nie przejmuj się, jeżeli nie rozumiesz części tego programu. W rozdziale tym wszystkiego się jeszcze nauczysz.
def info(object, spacing=10, collapse=1): #(1) (2) (3)
u"""Wypisuje metody i ich notki dokumentacyjne.
Argumentem może być moduł, klasa, lista, słownik, czy też łańcuch znaków."""
methodList = [method for method in dir(object) if callable(getattr(object, method))]
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__": #(4) (5)
print info.__doc__
- Ten moduł ma jedną funkcję
info. Zgodnie ze swoją deklaracją wymaga ona trzech argumentów:object,spacingorazcollapse. Dwa ostatnie parametry są opcjonalne, co za chwilę zobaczymy. - Funkcja
infoposiada wieloliniową notkę dokumentacyjną, która opisuje jej zastosowanie. Zauważ, że funkcja nie zwraca żadnej wartości. Ta funkcja będzie wykorzystywana, aby wykonać pewną czynność, a nie żeby otrzymać pewną wartość. - Kod wewnątrz funkcji jest wcięty.
- Sztuczka z
if __name__pozwala wykonać programowi coś użytecznego, kiedy zostanie uruchomiony samodzielnie. Jeśli powyższy kod zaimportujemy jako moduł do innego programu, kod pod tą instrukcją nie zostanie wykonany. W tym wypadku program wypisuje po prostu notkę dokumentacyjną funkcjiinfo. - Instrukcja
ifwykorzystuje==(dwa znaki równości), aby porównać dwie wartości. W instrukcjiifnie musimy korzystać z nawiasów okrągłych.
Funkcja info została zaprojektowana tak, aby ułatwić sobie pracę w IDE Pythona. IDE bierze jakiś obiekt, który posiada funkcje lub metody (jak na przykład moduł zawierający funkcje lub listę, która posiada metody) i wyświetla funkcje i ich notki dokumentacyjne.
>>> 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
Domyślnie wynik jest formatowany tak, by był łatwy do odczytania. Notki dokumentacyjne składające się z wielu linii zamieniane są na jednoliniowe, ale tę opcję możemy zmienić ustawiając wartość 0 dla argumentu collapse. Jeżeli nazwa funkcji jest dłuższa niż 10 znaków, możemy określić inną wartość dla argumentu spacing, by ułatwić sobie czytanie.
>>> import odbchelper >>> info(odbchelper) buildConnectionString Tworzy łańcuchów znaków na podstawie słownika parametrów. Zwraca łańcuch znaków. >>> info(odbchelper, 30) buildConnectionString Tworzy łańcuchów znaków na podstawie słownika parametrów. Zwraca łańcuch znaków. >>> info(odbchelper, 30, 0) buildConnectionString Tworzy łańcuchów znaków na podstawie słownika parametrów. Zwraca łańcuch znaków.
[edytuj] Argumenty opcjonalne i nazwane
W Pythonie argumenty funkcji mogą posiadać wartości domyślne. Jeżeli funkcja zostanie wywołana bez podania pewnego argumentu, argumentowi temu zostanie przypisana jego domyślna wartość. Co więcej możemy podawać argumenty w dowolnej kolejności poprzez użycie ich nazw.
Poniżej przykład funkcji info z dwoma argumentami opcjonalnymi:
def info(object, spacing=10, collapse=1):
spacing oraz collapse są argumentami opcjonalnymi, ponieważ mają przypisane wartości domyślne. Argument object jest wymagany, ponieważ nie posiada wartości domyślnej. Jeżeli info zostanie wywołana tylko z jednym argumentem, spacing przyjmie wartości 10, a collapse wartość 1. Jeżeli wywołamy tę funkcję z dwoma argumentami, jedynie collapse przyjmuje wartość domyślną (czyli 1).
Załóżmy, że chcielibyśmy określić wartość dla collapse, ale dla argumentu spacing chcielibyśmy skorzystać z domyślnej wartości. W większości języków programowania jest to niewykonalne, ponieważ wymagają one od nas wywołania funkcji z trzema argumentami. Na szczęście w Pythonie możemy określać argumenty w dowolnej kolejności poprzez odwołanie się do ich nazw.
infoinfo(odbchelper) #(1) info(odbchelper, 12) #(2) info(odbchelper, collapse=0) #(3) info(spacing=15, object=odbchelper) #(4)
- Kiedy wywołamy tę funkcję z jednym argumentem,
spacingprzyjmie wartość domyślną równą10, acollapsewartość1. - Kiedy podamy dwa argumenty,
collapseprzyjmie wartość domyślną, czyli1. - Tutaj podajemy argument
collapseodwołując się do jego nazwy i określamy wartość, którą chcemy mu przypisać.spacingprzyjmuje wartość domyślną --10. - Nawet wymagany argument (jak
object, który nie posiada wartości domyślnej) może zostać określony poprzez swoją nazwę i może wystąpić na jakimkolwiek miejscu w wywołaniu funkcji.
Takie działanie może się wydawać trochę niejasne, dopóki nie zdamy sobie sprawy, że lista argumentów jest po prostu słownikiem. Gdy wywołujemy daną funkcję w sposób "normalny", czyli bez podawania nazw argumentów, Python dopasowuje wartości do określonych argumentów w takiej kolejności w jakiej zostały zadeklarowane. Najczęściej będziemy wykorzystywali tylko "normalne" wywołania funkcji, ale zawsze mamy możliwość bardziej elastycznego podejścia do określania kolejności argumentów.
[edytuj] Materiały dodatkowe
- Python Tutorial omawia w jaki sposób domyślne wartości są określane, czyli co się stanie, gdy domyślny argument będzie listą lub też pewnym wyrażeniem.
[edytuj] Dwa sposoby importowania modułów
W Pythonie mamy dwa sposoby importowania modułów. Obydwa są przydatne, dlatego też powinniśmy umieć je wykorzystywać. Jednym ze sposobów jest użycie polecenia import module, który mogliśmy zobaczyć w podrozdziale "Wszystko jest obiektem". Istnieje inny sposób, który realizuje tę samą czynność, ale posiada pewne różnice. Poniżej został przedstawiony przykład wykorzystujący instrukcję from module import:
from apihelper import info
Jak widzimy, składnia tego wyrażenia jest bardzo podobna do import module, ale z jedną ważną różnicą: atrybuty i metody danego modułu są importowane bezpośrednio do lokalnej przestrzeni nazw, a więc będą dostępne bezpośrednio i nie musimy określać, z którego modułu korzystamy. Możemy importować określone pozycje albo skorzystać z from module import *, aby zaimportować 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 analogią do import module.* w Javie, a import module w Pythonie przypomina import module w Javie. |
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'>
- Moduł
typesnie posiada żadnych metod. Posiada on jedynie atrybuty określające wszystkie typy zdefiniowane przez Pythona. Zauważmy, że atrybut tego modułu (w tym przypadkuFunctionType) musi być poprzedzony nazwą modułu --types. FunctionTypenie został sam w sobie określony w przestrzeni nazw; istnieje on tylko w kontekście modułutypes.- Za pomocą tego wyrażenia atrybut
FunctionTypez modułutypeszostał zaimportowany bezpośrednio do lokalnej przestrzeni nazw. - Teraz możemy odwoływać się bezpośrednio do
FunctionType, bez odwoływania się dotypes.
Kiedy powinniśmy używać from module import?
- Kiedy często odwołujemy się do atrybutów i metod, a nie chcemy wielokrotnie wpisywać nazwy modułu, wtedy najlepiej wykorzystać
from module import. - Jeśli potrzebujemy selektywnie zaimportować tylko kilka atrybutów lub metod, powinniśmy skorzystać z
from module import. - Jeśli moduł zawiera atrybuty lub metody, które posiadają taką samą nazwę jaka jest w naszym module, powinniśmy wykorzystać
import module, aby uniknąć konfliktu nazw.
W pozostałych przypadkach to kwestia stylu programowania, można spotkać kod napisany obydwoma sposobami.
Używajmy from module import * oszczędnie, ponieważ taki sposób importowania utrudnia określenie, skąd pochodzi dana funkcja lub atrybut, a to z kolei utrudnia debugowanie. |
[edytuj] Materiały dodatkowe
- eff-bot opowie nam więcej na temat różnic między
import moduleafrom module import. - Python Tutorial omawia zaawansowane techniki importu, włączając w to
from module import *.
[edytuj] type, str, dir i inne wbudowane funkcje
Python posiada mały zbiór bardzo użytecznych wbudowanych funkcji. Wszystkie inne funkcje znajdują się w różnych modułach. Była to świadoma decyzja projektowa, aby uniknąć przeładowania rdzenia języka, jak to ma miejsce w przypadku innych języków (jak np. Visual Basic czy Object Pascal).
[edytuj] Funkcja type
Funkcja type zwraca typ danych podanego obiektu. Wszystkie typy znajdują się w module types. Funkcja ta może się okazać przydatna podczas tworzenia funkcji obsługujących kilka typów danych.
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
- Argumentem
typemoże być cokolwiek: stała, łańcuch znaków, lista, słownik, krotka, funkcja, klasa, moduł, wszystkie typy są akceptowane. - Kiedy podamy funkcji
typedowolną zmienną, zostanie zwrócony jej typ. typetakże działa na modułach.- Możemy używać stałych z modułu
types, aby porównywać typy obiektów. Wykorzystuje to funkcjainfo, co wkrótce zobaczymy.
[edytuj] Funkcja str
Funkcja str przekształca dane w łańcuch znaków. Każdy typ danych może zostać przekształcony w łańcuch znaków.
str>>> str(1) #(1) '1' >>> horsemen = ['war', 'pestilence', 'famine'] >>> horsemen ['war', 'pestilence', 'famine'] >>> horsemen.append('Powerbuilder') >>> str(horsemen) #(2) "['war', 'pestilence', 'famine', 'Powerbuilder']" >>> str(odbchelper) #(3) "<module 'odbchelper' from 'c:\\docbook\\dip\\py\\odbchelper.py'>" >>> str(None) #(4) 'None'
- Można było się spodziewać, że
strdziała na tych prostych, podstawowych typach takich jak np. liczby całkowite. Prawie każdy język programowania posiada funkcję konwertującą liczbę całkowitą na łańcuch znaków. - Jakkolwiek funkcja
strdziała na każdym obiekcie dowolnego typu, w tym przypadku jest to lista składająca się z kilku elementów. - Argumentem funkcji
strmoże być także moduł. Zauważmy, że łańcuch reprezentujący moduł zawiera ścieżkę do miejsca, w którym się ten moduł znajduje. Na różnych komputerach może być ona inna. - Subtelnym, lecz ważnym zachowaniem funkcji
strjest to, że argumentem może być nawet wartośćNone(Pythonowej wartości pusta, często określanej w innych językach przeznull). Dla takiego argumentu funkcja zwraca napis'None'. Wkrótce wykorzystamy tę możliwość.
[edytuj] Funkcja unicode
Funkcja unicode pełni tą samą funkcję, co str, ale zamiast łańcucha znaków tworzy unikod.
unicode>>> unicode(1) #(1) u'1' >>> unicode(odbchelper) #(2) u"<module 'odbchelper' from 'c:\\docbook\\dip\\py\\odbchelper.py'>" >>> print unicode(horsemen[0]) u'war' >>> unicode('jeździectwo') #(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('jeździectwo', 'utf-8') #(4) u'je\u017adziectwo'
- Podobnie, jak w przypadku
str, do funkcjiunicodemożemy przekazać dowolny obiekt np. może to być liczba. Przekonwertowaliśmy liczbę na napis unikodowy. - Argumentem funkcji
unicodemoże być także moduł. - Ponieważ litera "ź" nie należy do ASCII, więc Python nie potrafi jej zinterpretować. Zostaje rzucony wyjątek.
- Do funkcji
unicodemożemy przekazać drugi, opcjonalny argumentencoding, który określa, w jakim systemie kodowania jest zakodowany łańcuch znaków. Komputer, na którym został uruchomiony ten przykład, korzystał z kodowania UTF-8, więc przekazany łańcuch znaków także będzie w tym systemie kodowania.
[edytuj] Funkcja dir
Kluczową funkcją wykorzystaną w info jest funkcja dir. Funkcja ta zwraca listę atrybutów i metod pewnego obiektu np. modułu, funkcji, łańcuch znaków, listy, słownika... niemal wszystkiego.
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']
lijest listą, dlatego teżdir(li)zwróci nam listę wszystkich metod, które posiada lista. Zwróćmy uwagę na to, że zwracana lista zawiera nazwy metod w formie łańcucha znaków, a nie metody same w sobie. Metody zaczynające się i kończące dwoma znakami podkreślenia są metodami specjalnymi.djest słownikiem, dlategodir(d)zwróci listę nazw metod słownika. Co najmniej jedna z nich, metodakeys, powinna wyglądać znajomo.- Dzięki temu funkcja ta staje się interesująca.
odbchelperjest modułem, więc za pomocądir(odbchelper)otrzymamy listę nazw atrybutów tego modułu włączając w to wbudowane atrybuty np.__name__, czy też__doc__, a także jakiekolwiek inne np. zdefiniowane przez nas funkcje. W tym przypadkuodbchelperposiada tylko jedną, zdefiniowaną przez nas metodę -- funkcjębuildConnectionStringopisaną w rozdziale "Pierwszy program".
[edytuj] Funkcja callable
Funkcja callable zwraca True, jeśli podany obiekt może być wywoływany, a False w przeciwnym przypadku. Do wywoływalnych obiektów zaliczamy funkcje, metody klas, a nawet same klasy. (Więcej o klasach możemy przeczytać w następnym rozdziale.)
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)
- Nie zaleca się, żeby wykorzystywać funkcje z modułu
string(chociaż wciąż wiele osób używa funkcjijoin), ale moduł ten zawiera wiele przydatnych stałych jak np.string.punctuation, który zawiera wszystkie standardowe znaki przestankowe, więc z niego tutaj skorzystaliśmy. - Funkcja
string.joinłączy listę w łańcuch znaków. string.punctuationnie jest wywoływalny, jest łańcuchem znaków. (Typstringposiada metody, które możemy wywoływać, lecz sam w sobie nie jest wywoływalny.)string.joinmożna wywołać. Jest to funkcja przyjmująca dwa argumenty.- Każdy wywoływalny obiekt może posiadać notkę dokumentacyjną. Kiedy wykonamy funkcję
callablena każdym atrybucie danego obiektu, będziemy mogli potencjalnie określić, którymi atrybutami chcemy się bardziej zainteresować (metody, funkcje, klasy), a które chcemy pominąć (stałe itp.).
[edytuj] Wbudowane funkcje
type, str, unicode, dir i wszystkie pozostałe wbudowane funkcje są umieszczone w specjalnym module o nazwie __builtin__ (nazwa z dwoma znakami podkreślenia przed i po nazwie). Jeśli to pomoże, możemy założyć, że Python automatycznie wykonuje przy starcie polecenie from __builtin__ import *, które bezpośrednio importuje wszystkie wbudowane funkcje do używanej przez nas przestrzeni nazw. Zaletą tego, że funkcje te znajdują się w module, jest to, że możemy dostać informacje o wszystkich wbudowanych funkcjach i atrybutach poprzez moduł __builtin__. Wykorzystajmy funkcje info podając jako argument ten moduł i przejrzyjmy wyświetlony spis. Niektóre z ważniejszych funkcji w module __builtin__ zgłębimy później. (Niektóre z wbudowanych klas błędów np. AttributeError, powinny wyglądać znajomo.).
>>> 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...]
[edytuj] Materiały dodatkowe
[edytuj] Funkcja getattr
Powinniśmy już wiedzieć, że w Pythonie funkcje są obiektami. Ponadto możemy dostać referencję do funkcji bez znajomości jej nazwy przed uruchomieniem programu. W tym celu podczas działania programu należy wykorzystać funkcję getattr.
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'
- Dzięki temu dostaliśmy referencję do metody
pop. Zauważmy, że w ten sposób nie wywołujemy metodypop; aby ją wywołać musielibyśmy wpisać polecenieli.pop(). Otrzymujemy referencję do tej metody. (Adres szesnastkowy wygląda inaczej na różnych komputerach, dlatego wyjścia będą się nieco różnić.) - Operacja ta także zwróciła referencję do metody
pop, lecz tym razem nazwa metody jest określona poprzez łańcuch znaków w argumencie funkcjigetattr.getattrjest bardzo przydatną, wbudowaną funkcją, która zwraca pewien atrybut dowolnego obiektu. Tutaj wykorzystujemy obiekt, który jest listą, a atrybutem jest metodapop. - Dzięki temu przykładowi możemy zobaczyć, jaki duży potencjał kryje się w funkcji
getattr. W tym przypadku zwracaną wartościągetattrjest metoda (referencja do metody). Metodę tę możemy wykonać podobnie, jak byśmy bezpośrednio wywołalili.append("Moe"). Tym razem nie wywołujemy funkcji bezpośrednio, lecz określamy nazwę funkcji za pomocą łańcucha znaków. getattrbez problemu pracuje na słownikach- Teoretycznie
getattrpowinien pracować na krotkach, jednak krotki nie posiadają żadnej metody, dlategogetattrspowoduje wystąpienie wyjątku związanego z brakiem atrybutu o podanej nazwie.
[edytuj] getattr na modułach
getattr działa nie tylko na wbudowanych typach danych. Argumentem tej funkcji może być także moduł.
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
- Polecenie to zwraca nam referencję do funkcji
buildConnectionStringz modułuodbchelper, który przeanalizowaliśmy w Rozdziale 2. - Wykorzystując
getattr, możemy dostać taką samą referencję, do tej samej funkcji. W ogólnościgetattr(obiekt, "atrybut")jest odpowiednikiemobiekt.atrybut. Jeśli obiekt jest modułem, atrybutem może być cokolwiek zdefiniowane w tym module np. funkcja, klasa czy zmienna globalna. - Tę możliwość wykorzystaliśmy w funkcji
info. Obiekt o nazwieobjectzostał przekazany jako argument do funkcjigetattr, ponadto przekazaliśmy nazwę pewnej metody lub funkcji jako zmiennąmethod. - W tym przypadku zmienna
methodprzechowuje nazwę funkcji, co można sprawdzić pobierając typ zwracanej wartości. - Ponieważ zmienna
methodjest funkcją, więc można ją wywoływać. Zatem w wyniku wywołaniacallableotrzymaliśmy wartośćTrue.
[edytuj] getattr jako funkcja pośrednicząca
Funkcja getattr jest powszechnie używana jako funkcja pośrednicząca (ang. dispatcher). Na przykład mamy napisany program, który może wypisywać dane w różnych formatach (np. HTML i PS). Wówczas dla każdego formatu wyjścia możemy zdefiniować odpowiednią funkcję, a podczas wypisywania danych na wyjście getattr będzie nam pośredniczył między tymi funkcjami. Jeśli wydaje się to trochę zagmatwane, zaraz zobaczymy przykład.
Wyobraźmy sobie program, który potrafi wyświetlać statystyki strony w formacie HTML, XML i w czystym tekście. Wybór właściwego formatu może być określony w linii poleceń lub przechowywany w pliku konfiguracyjnym. Moduł statsout definiuje trzy funkcje -- output_html, output_xml i output_text, a wówczas program główny może zdefiniować pojedynczą funkcję, która wypisuje dane na wyjście:
getattr
import statsout
def output(data, format="text"): #(1)
output_function = getattr(statsout, "output_%s" % format) #(2)
return output_function(data) #(3)
- Funkcja
outputwymaga jednego argumentu o nazwiedata, który ma zawierać dane do wypisania na wyjście. Funkcja ta może także przyjąć jeden opcjonalny argumentformat, który określa format wyjścia. Gdy argumentformatnie zostanie określony, przyjmie on wartość"text", a funkcja się zakończy wywołując funkcjęoutput_text, która wypisuje dane na wyjście w postaci czystego tekstu. - Łańcuch znaków
"output_"połączyliśmy z argumentemformat, aby otrzymać nazwę funkcji. Następnie pobraliśmy funkcję o tej nazwie z modułustatsout. Dzięki temu w przyszłości będzie łatwiej rozszerzyć program nie zmieniając funkcji pośredniczącej, aby obsługiwał więcej wyjściowych formatów. W tym celu wystarczy dodać odpowiednią funkcję dostatsoutnp.output_pdfi wywołujemy funkcjęoutputpodając argumentformatjako"pdf". - Teraz możemy wywołać funkcję wyjściową w taki sam sposób jak inne funkcje. Zmienna
output_functionjest referencją do odpowiedniej funkcji wstatsout.
Czy znaleźliśmy błąd w poprzednim przykładzie? Jest to bardzo niestabilne rozwiązanie, ponadto nie ma tu kontroli błędów. Co się stanie gdy użytkownik poda format, który nie zdefiniowaliśmy w statsout? Funkcja getattr rzuci nam wyjątek związany z błędnym argumentem, czyli podaną nazwą funkcji, która nie istnieje w module statsout.
Na szczęście do funkcji getattr możemy podać trzeci, opcjonalny argument, czyli domyślnie zwracaną wartość, gdy podany atrybut nie istnieje.
getattr
import statsout
def output(data, format="text"):
output_function = getattr(statsout, "output_%s" % format, statsout.output_text)
return output_function(data) #(1)
- Ta funkcja już na pewno będzie działała poprawnie, ponieważ podaliśmy trzeci argument w wywołaniu funkcji
getattr. Trzeci argument jest domyślną wartością, która zostanie zwrócona, gdy podany atrybut, czy metoda nie zostanie znaleziona.
Jak mogliśmy zauważyć, funkcja getattr jest niezwykle użyteczna. Jest ona sercem introspekcji. W następnych rozdziałach zobaczymy jeszcze więcej przydatnych przykładów.
[edytuj] Filtrowanie listy
Jak już wiemy, Python ma potężne możliwości odwzorowania list w inne listy poprzez wyrażenia listowe (rozdział "Odwzorowywanie listy"). Wyrażenia listowe możemy też łączyć z mechanizmem filtrowania, dzięki któremu pewne elementy listy są odwzorowywane a pewne pomijane.
Poniżej przedstawiono składnię filtrowania listy:
[wyrażenie odwzorowujące for element in odwzorowywana lista if wyrażenie filtrujące]
Jest to wyrażenie listowe z pewnym rozszerzeniem. Początek wyrażenia jest identyczny, ale końcowa część zaczynająca się od if, jest wyrażeniem filtrującym. Wyrażenie filtrujące może być dowolnym wyrażeniem, które może zostać zinterpretowane jako wyrażenie logiczne. Każdy element dla którego wyrażenie to będzie prawdziwe, zostanie dołączony do wyjściowej listy. Wszystkie inne elementy dla których wyrażenie filtrujące jest fałszywe, zostaną pominięte i nie trafią do wyjściowej 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']
- W tym przykładzie wyrażenie odwzorowujące nie jest skomplikowane (zwraca po prostu wartość każdego elementu), więc skoncentrujmy się na wyrażeniu filtrującym. Kiedy Python przechodzi przez każdy element listy, sprawdza czy wyrażenie filtrujące jest prawdziwe dla tego elementu. Jeśli tak będzie, to Python wykona wyrażenie odwzorowujące na tym elemencie i wstawi odwzorowany element do zwracanej listy. W tym przypadku odfiltrowujemy wszystkie łańcuchy znaków, które mają więcej niż jeden znak, tak więc otrzymujemy listę wszystkich dłuższych napisów.
- Tutaj odfiltrowujemy elementy, które przechowują wartość
"b". Zauważmy, że to wyrażenie listowe odfiltrowuje wszystkie wystąpienia"b", ponieważ za każdym razem, gdy dany element będzie równy"b", wyrażenie filtrujące będzie fałszywe, a zatem wartość ta nie zostanie wstawiona do zwracanej listy. countjest metodą listy, która zwraca ilość wystąpień danej wartości w liście. Można się domyślać, że ten filtr usunie duplikujące się wartości, przez co zostanie zwrócona lista, która zawiera tylko jedną kopię każdej wartości z oryginalnej listy. Jednak tak się nie stanie. Wartości, które pojawiają się dwukrotnie w oryginalnej liście (w tym wypadku"b"i"d") zostaną całkowicie odrzucone. Istnieje możliwość usunięcia duplikatów z listy, jednak filtrowanie listy nie daje nam takiej możliwości.
Wróćmy teraz do apihelper.py, do poniższej linii:
methodList = [method for method in dir(object) if callable(getattr(object, method))]
To wyrażenie listowe wygląda skomplikowanie, a nawet jest skomplikowane, jednak podstawowa struktura jest taka sama. Całe to wyrażenie zwraca listę, która zostaje przypisana do zmiennej methodList. Pierwsza część to część odwzorowująca listę. Wyrażenie odwzorowujące zwraca wartość danego elementu. dir(object) zwraca listę atrybutów i metod obiektu object, czyli jest to po prostu lista, którą odwzorowujemy. Tak więc nową częścią jest tylko wyrażenie filtrujące znajdujące się za instrukcją if.
To wyrażenie nie jest takie straszne, na jakie wygląda. Już poznaliśmy funkcje callable, getattr oraz in. Jak już wiemy z poprzedniego podrozdziału, funkcja getattr(object, method) zwraca obiekt funkcji (czyli referencję do tej funkcji), jeśli object jest modułem, a method jest nazwą funkcji w tym module.
Podsumowując, wyrażenie bierze pewien obiekt (nazwany object). Następnie pobiera listę nazw atrybutów tego obiektu, a także metod i funkcji oraz kilka innych rzeczy. Następnie odrzuca te rzeczy, które nas nie interesują, czyli pobieramy nazwy każdego atrybutu/metody/funkcji, a następnie za pomocą getattr pobieramy referencje do atrybutów o tych nazwach. Potem za pomocą funkcji callable sprawdzamy, czy ten obiekt jest wywoływalny, a dzięki temu dowiadujemy się, czy mamy do czynienia z metodą lub jakąś funkcją. Mogą to być na przykład funkcje wbudowane (np. metoda listy o nazwie pop), czy też funkcje zdefiniowane przez użytkownika (np. funkcja buildConnectionString z modułu odbchelper). Nie musimy natomiast martwić się o inne atrybuty jak np. wbudowany do każdego modułu atrybut __name__ (nie jest on wywoływalny, czyli callable zwróci wartość False).
[edytuj] Materiały dodatkowe
Python Tutorial omawia inny sposób filtrowania listy, za pomocą wbudowanej funkcji filter.
[edytuj] Operatory and i or
Operatory and i or odpowiadają boolowskim operacjom logicznym, jednak nie zwracają one wartości logicznych. Zamiast tego zwracają którąś z podanych wartości.
and>>> 'a' and 'b' #(1) 'b' >>> '' and 'b' #(2) '' >>> 'a' and 'b' and 'c' #(3) 'c'
- Podczas używania
andwartości są oceniane od lewej do prawej.0,'',[],(),{}iNonesą fałszem w kontekście logicznym, natomiast wszystko inne jest prawdą. Cóż, prawie wszystko. Domyślnie instancje klasy w kontekście logicznym są prawdą, ale możesz zdefiniować specjalne metody w swojej klasie, które sprawią, że będzie ona fałszem w kontekście logicznym. Wszystkiego o klasach i specjalnych metodach nauczymy się w rozdziale "Obiekty i klasy". Jeśli wszystkie wartości są prawdą w kontekście logicznym,andzwraca ostatnią wartość. W tym przypadkuandnajpierw bierze'a', co jest prawdą, a potem'b', co też jest prawdą, więc zwraca ostatnią wartość, czyli'b'. - Jeśli jakaś wartość jest fałszywa w kontekście logicznym,
andzwraca pierwszą fałszywą wartość. W tym wypadku''jest pierwszą fałszywą wartością. - Wszystkie wartości są prawdą, tak więc
andzwraca ostatnią wartość,'c'.
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'
- Używając
orwartości są oceniane od lewej do prawej, podobnie jak wand. Jeśli jakaś wartość jest prawdą,orzwraca tą wartość natychmiast. W tym wypadku,'a'jest pierwszą wartością prawdziwą. orwyznacza''jako fałsz, ale potem'b', jako prawdę i zwraca'b'.- Jeśli wszystkie wartości są fałszem,
orzwraca ostatnią wartość.orocenia''jako fałsz, potem[]jako fałsz, potem{}jako fałsz i zwraca{}. - Zauważmy, że
orocenia kolejne wartości od lewej do prawej, dopóki nie znajdzie takiej, która jest prawdą w kontekście logicznym, a pozostałą resztę ignoruje. Tutaj, funkcjasidefxnigdy nie jest wywołana, ponieważ już'a'jest prawdą i'a'zostanie zwrócone natychmiastowo.
Jeśli jesteś osobą programującą w języku C, na pewno znajome jest ci wyrażenie bool ? a : b, z którego otrzymamy a, jeśli bool jest prawdą, lub b w przeciwnym wypadku. Dzięki sposobowi działania operatorów and i or w Pythonie, możemy osiągnąć podobny efekt.
[edytuj] Sztuczka and-or
and-or>>> a = "first" >>> b = "second" >>> 1 and a or b #(1) 'first' >>> 0 and a or b #(2) 'second'
- Ta składnia wygląda podobnie do wyrażenia
bool ? a : bw C. Całe wyrażenie jest oceniane od lewej do prawej, tak więc najpierw określony zostanieand. Czyli1 and 'first'daje'first', potem'first' or 'second'daje'first'. 0 and 'first'daje0, a potem0 or 'second'daje 'second'.
Jakkolwiek te wyrażenia Pythona są po prostu logiką boolowską, a nie specjalną konstrukcją języka. Istnieje jedna bardzo ważna różnica pomiędzy Pythonową sztuczką and-or, a składnią bool ? a : b w C. Jeśli wartość a jest fałszem, wyrażenie to nie będzie działało tak, jakbyśmy chcieli. Można się na tym nieźle przejechać, co zobaczymy w poniższym przykładzie.
and-or
>>> a = ""
>>> b = "second"
>>> 1 and a or b #(1)
'second'
- Ponieważ
ajest pustym napisem, który Python uważa za fałsz w kontekście logicznym, więc1 and ''daje'', a następnie'' or 'second'daje'second'. Ups! To nie to, czego oczekiwaliśmy.
Sztuczka and-or, czyli wyrażenie bool and a or b, nie będzie działało w identyczny sposób, jak wyrażenie w C bool ? a : b, jeśli a będzie fałszem w kontekście logicznym.
Prawdziwą sztuczką kryjącą się za sztuczką and-or, jest upewnienie się, czy wartość 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 będzie a lub b.
and-or>>> a = "" >>> b = "second" >>> (1 and [a] or [b])[0] #(1) ''
- Jako że
[a]jest nie pustą listą, więc nigdy nie będzie fałszem (tylko pusta lista jest fałszem). Nawet gdyajest równe0lub''lub inną wartością dającą fałsz, lista [a] będzie prawdą, ponieważ ma jeden element.
Jak dotąd, ta sztuczka może wydawać się bardziej uciążliwa niż pomocna. Możesz przecież osiągnąć to samo zachowanie instrukcją if, więc po co to całe zamieszanie. Cóż, w wielu przypadkach, wybierasz pomiędzy dwoma stałymi wartościami, więc możesz użyć prostszego zapisu i się nie martwić, ponieważ wiesz, że wartość zawsze będzie prawdą. I nawet kiedy potrzebujesz użyć bardziej skomplikowanej, bezpiecznej formy, istnieją powody, aby i tak czasami korzystać z tej sztuczki. Na przykład, są pewne przypadki w Pythonie gdzie instrukcje if nie są dozwolone np. w wyrażeniach lambda.
[edytuj] Materiały dodatkowe
[edytuj] Wyrażenia lambda
Python za pomocą pewnych wyrażeń pozwala nam zdefiniować jednolinijkowe mini-funkcje. Te tzw. funkcje lambda są zapożyczone z Lispa i mogą być użyte wszędzie tam, gdzie potrzebna jest funkcja.
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
- W ten sposób tworzymy funkcję
lambda, która daje ten sam efekt jak normalna funkcja nad nią. Zwróćmy uwagę na skróconą składnię: nie ma nawiasów wokół listy argumentów, brakuje też słowa kluczowegoreturn(cała funkcja może być tylko jednym wyrażeniem). Funkcja nie posiada również nazwy, ale może być wywołana za pomocą zmiennej, do której zostanie przypisana. - Możemy użyć funkcji
lambdabez przypisywania jej do zmiennej. Może taki sposób korzystania z wyrażeńlambdanie jest zbyt przydatny, ale w ten sposób możemy zobaczyć, że za pomocą tego wyrażenia tworzymy funkcję jednolinijkową.
Podsumowując, funkcja lambda jest funkcją, która pobiera dowolną liczbę argumentów (włączając argumenty opcjonalne) i zwraca wartość, którą otrzymujemy po wykonaniu pojedynczego wyrażenia. Funkcje lambda nie mogą zawierać poleceń i nie mogą zawierać więcej niż jednego wyrażenia. Nie próbujmy upchać zbyt dużo w funkcję lambda; zamiast tego jeśli potrzebujemy coś bardziej złożonego, zdefiniujmy normalną funkcję.
[edytuj] Funkcje lambda w prawdziwym świecie
Poniżej przedstawiamy funkcje lambda wykorzystaną w apihelper.py:
processFunc = collapse and (lambda s: " ".join(s.split())) or (lambda s: s)
Zauważmy, że użyta jest tu prosta forma sztuczki and-or, która jest bezpieczna, ponieważ funkcja lambda jest zawsze prawdą w kontekście logicznym. (To nie znaczy, że funkcja lambda nie może zwracać wartości będącej fałszem. Funkcja jest zawsze prawdą w kontekście logicznym, ale jej zwracana wartość może być czymkolwiek.)
Zauważmy również, że używamy funkcji split bez argumentów. Widzieliśmy już ją użytą z jednym lub dwoma argumentami. Jeśli nie podamy argumentów, wówczas domyślnym separatorem tej funkcji są białe znaki (czyli spacja, znak nowej linii, znak tabulacji itp.).
split bez argumentów>>> s = "this is\na\ttest" #(1) >>> print s this is a test >>> print s.split() #(2) ['this', 'is', 'a', 'test'] >>> print " ".join(s.split()) #(3) 'this is a test'
- Tutaj mamy wieloliniowy napis, zdefiniowany przy pomocy znaków sterujących zamiast użycia trzykrotnych cudzysłowów. \n jest znakiem nowej linii, a \t znakiem tabulacji.
- split bez żadnych argumentów dzieli na białych znakach, a trzy spacje, znak nowej linii i znak tabulacji są białymi znakami.
- Możemy unormować białe znaki poprzez podzielenie napisu za pomocą metody
split, a potem powtórne złączenie metodąjoin, używając pojedynczej spacji jako separatora. To właśnie robi funkcjainfo, aby zwinąć wieloliniowe notki dokumentacyjne w jedną linię.
Co więc właściwie 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 referencją do funkcji, ale zależną od zmiennej collapse. Jeśli collapse jest prawdą, processFunc(string) będzie zwijać białe znaki, a w przeciwnym wypadku processFunc(string) zwróci swój argument niezmieniony.
Aby to zrobić w mniej zaawansowanym języku (np. w Visual Basicu), prawdopodobnie stworzylibyśmy funkcję, która pobiera napis oraz argument collapse i używa instrukcji if, aby określić czy zawijać białe znaki czy też nie, a potem zwracałaby odpowiednią wartość. Takie podejście nie byłoby zbyt efektywne, ponieważ funkcja musiałaby obsłużyć każdy możliwy przypadek. Za każdym jej wywołaniem, musiałaby zdecydować czy zawijać białe znaki zanim dałaby nam to, co chcemy. W Pythonie logikę wyboru możemy wyprowadzić poza funkcję i zdefiniować funkcję lambda, która jest dostosowana do tego, aby dać nam dokładnie to (i tylko to), co chcemy. Takie podejście jest bardziej efektywne, bardziej eleganckie i mniej podatne na błędy typu "Ups! Te argumenty miały być w odwrotnej kolejności...".
[edytuj] Materiały dodatkowe
- Python Knowledge Base omawia, jak wykorzystywać
lambda, aby wywoływać funkcje w sposób niebezpośredni. - Python Tutorial pokazuje, jak dostać się do zewnętrznych zmiennych z wnętrza funkcji
lambda. - The Whole Python FAQ zawiera przykłady, pokazujące, jak można zagmatwać kod funkcji
lambda.
[edytuj] Wszystko razem
Ostatnia linia kodu, jedyna której jeszcze nie rozpracowaliśmy, to ta, która odwala całą robotę. Teraz umieszczamy wszystkie puzzle w jednym miejscu i nadchodzi czas, aby je ułożyć.
To jest najważniejsza część apihelper.py:
print "\n".join(["%s %s" %
(method.ljust(spacing),
processFunc(unicode(getattr(object, method).__doc__)))
for method in methodList])
Zauważmy, że jest to tylko jedne wyrażenie podzielone na wiele linii, ale które nie używa znaku kontynuacji (znaku odwrotnego ukośnika, \). Pamiętasz jak powiedzieliśmy, że pewne instrukcje mogą być rozdzielone na kilka linii bez używania odwrotnego ukośnika? Wyrażenia listowe są jednym z tego typu wyrażeń, ponieważ całe wyrażenie jest zawarte w nawiasach kwadratowych.
Zacznijmy od końca i posuwajmy się w tył. Wyrażenie
for method in methodList
okazuje się być wyrażeniem listowym. Jak wiemy, methodList jest listą wszystkich metod danego obiektu, które nas interesują, więc za pomocą tej pętli przechodzimy tę listę wykorzystując zmienną method.
>>> import odbchelper >>> object = odbchelper #(1) >>> method = 'buildConnectionString' #(2) >>> getattr(object, method) #(3) <function buildConnectionString at 010D6D74> >>> print getattr(object, method).__doc__ #(4) Tworzy łańcuchów znaków na podstawie słownika parametrów. Zwraca łańcuch znaków.
- W funkcji
info,objectjest obiektem do którego otrzymujemy pomoc, a ten obiekt zostaje przekazany jako argument. - Podczas iterowania listy
methodList,methodjest nazwą aktualnej metody. - Używając funkcji
getattrotrzymujemy referencję do funkcjimethodz modułuobject. - Teraz wypisanie notki dokumentacyjnej będzie bardzo proste.
Następnym elementem puzzli jest użycie unicode na notce dokumentacyjnej. Jak sobie przypominamy, unicode jest wbudowaną funkcją, która przekształca dane na unikod, ale notka dokumentacyjna jest zawsze łańcuchem znaków, więc po co ją jeszcze konwertować na unikod? Nie każda funkcja posiada notkę dokumentacyjną, a jeśli ona nie istnieje, atrybut __doc__ ma wartość None.
unicode na notkach dokumentacyjnych?>>> def foo(): print 2 >>> foo() 2 >>> foo.__doc__ #(1) >>> foo.__doc__ == None #(2) True >>> unicode(foo.__doc__) #(3) u'None'
- Możemy łatwo zdefiniować funkcję, która nie posiada notki dokumentacyjnej, tak więc jej atrybut
__doc__ma wartośćNone. Dezorientujące jest to, że jeśli bezpośrednio odwołamy się do atrybutu__doc__, IDE w ogóle nic nie wypisze. Jednak jeśli się trochę zastanowimy nad tym, takie zachowanie IDE ma jednak pewien sens [5] - Możemy sprawdzić, że wartość atrybutu
__doc__aktualnie wynosiNoneprzez porównanie jej bezpośrednio z tą wartością. - W tym przypadku funkcja
unicodeprzyjmuje pustą wartość,Nonei zwraca jej unikodową reprezentację, czyli'None'. li.append.__doc__jest łańcuchem znaków. Zauważmy, że wszystkie angielskie notki dokumentacyjne Pythona korzystają ze znaków ASCII, dlatego możemy spokojnie je przekonwertować do unikodu za pomocą funkcjiunicode.
W SQL musimy skorzystać z IS NULL zamiast z = NULL, aby porównać coś z pustą wartością. W Pythonie możemy użyć albo == None albo is None, lecz is None jest szybsze. |
Teraz kiedy już mamy pewność, że otrzymamy unikod, możemy przekazać otrzymany unikodowy napis do processFunc, którą już zdefiniowaliśmy jako funkcję zwijającą lub niezwijającą białe znaki (w zależności od przekazanego argumentu). Czy już wiemy, dlaczego wykorzystaliśmy unicode? Do przekonwertowania wartości None na reprezentację w postaci unikodowego łańcucha znaków. processFunc przyjmuje argument będący unikodem i wywołuje jego metodę split. Nie zadziałałoby to, gdybyśmy przekazali samo None, ponieważ None nie posiada metody o nazwie split i rzucony zostałby wyjątek. Może się zastanawiasz, dlaczego nie konwertujemy do str? Ponieważ tworzone przez nas notki są napisami unikodowymi, w których nie wszystkie znaki należą do ASCII, a zatem str rzuciłby wyjątek.
Idąc wstecz, widzimy, że ponownie używamy formatowania łańcucha znaków, aby połączyć wartości zwrócone przez processFunc i przez metodę ljust. Jest to metoda łańcucha znaków (dodajmy, że napis unikodowy także jest łańcuchem znaków, tylko nieco o większych możliwościach), której jeszcze nie poznaliśmy.
ljust>>> s = 'buildConnectionString' >>> s.ljust(30) #(1) 'buildConnectionString ' >>> s.ljust(20) #(2) 'buildConnectionString'
ljustwypełnia napis spacjami do zadanej długości. Z tej możliwości korzysta funkcjainfo, aby stworzyć dwie kolumny na wyjściu i aby wszystkie notki dokumentacyjne umieścić w drugiej kolumnie.- Jeśli podana długość jest mniejsza niż długość napisu,
ljustzwróci po prostu napis niezmieniony. Metoda ta nigdy nie obcina łańcucha znaków.
Już prawie skończyliśmy. Mając nazwę metody method uzupełnioną spacjami poprzez ljust i (prawdopodobnie zwiniętą) notkę dokumentacyjną otrzymaną z wywołania processFunc, łączymy je i otrzymujemy pojedynczy napis, łańcuch znaków. Ponieważ odwzorowujemy listę methodList, dostajemy listę złożoną z takich łańcuchów znaków. Używając metody join z napisu "\n", łączymy tę listę w jeden łańcuch znaków, gdzie każdy elementem listy znajduje się w oddzielnej linii i ostatecznie wypisujemy rezultat.
>>> li = ['a', 'b', 'c']
>>> print "\n".join(li) #(1)
a
b
c
- Ta sztuczka może być pomocna do znajdowania błędów, gdy pracujemy na listach, a w Pythonie zawsze pracujemy na listach.
I to już był ostatni element puzzli. Teraz powinieneś zrozumieć ten kod.
print "\n".join(["%s %s" %
(method.ljust(spacing),
processFunc(unicode(getattr(object, method).__doc__)))
for method in methodList])
[edytuj] Podsumowanie
Program apihelper.py i jego wyjście powinno teraz nabrać sensu.
def info(object, spacing=10, collapse=1):
u"""Wypisuje metody i ich notki dokumentacyjne.
Argumentem może być moduł, klasa, lista, słownik, czy też łańcuch 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 wyjścia, 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 następnego rozdziału, upewnijmy się, że nie mamy problemów z wykonywaniem poniższych czynności:
- Definiowanie i wywoływanie funkcji z opcjonalnymi i nazwanymi argumentami
- Importowanie modułów za pomocą
import moduleifrom module import - Używanie
strdo przekształcenia jakiejkolwiek przypadkowej wartości na reprezentację w postaci łańcucha znaków, a także używanieunicodedo przekształcania wartości na unikod. - Używanie
getattrdo dynamicznego otrzymywania referencji do funkcji i innych atrybutów - Tworzenie wyrażeń listowych z filtrowaniem
- Rozpoznawanie sztuczki
and-ori używanie jej w sposób bezpieczny - Definiowanie funkcji
lambda - Przypisywanie funkcji do zmiennych i wywoływanie funkcji przez zmienną. Trudno jest to mocniej zaakcentować, jednak umiejętność wykonywania tego jest niezbędne do lepszego rozumienia Pythona. Zobaczymy bardziej złożone aplikacje opierające się na tej koncepcji w dalszej części książki.
[edytuj] Obiekty i klasy
Rozdział ten zaznajomi nas ze zorientowanym obiektowo programowaniem przy użyciu języka Python.
[edytuj] Nurkujemy
Poniżej znajduje się kompletny program, który oczywiście działa. Czytanie notki dokumentacyjnej modułu, klasy, czy też funkcji jest pomocne w zrozumieniu co dany program właściwie robi i w jaki sposób działa. Jak zwykle, nie martwmy się, że nie możemy wszystkiego zrozumieć. W końcu zasada działania tego programu zostanie dokładnie opisana w dalszej części tego rozdziału.
#-*- coding: utf-8 -*-
u"""Framework do pobierania metadanych specyficznych dla danego typu pliku.
Można utworzyć instancję odpowiedniej klasy podając jej nazwę pliku w konstruktorze.
Zwrócony obiekt zachowuje się jak słownik posiadający parę klucz-wartość
dla każdego 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 użyć funkcji listDirectory, aby pobrać informacje o wszystkich plikach w katalogu.
for info in fileinfo.listDirectory("/music/ap/", [".mp3"]):
...
Framework może być roszerzony poprzez dodanie klas dla poszczególnych typów plików, np.:
HTMLFileInfo, MPGFileInfo, DOCFileInfo. Każda klasa jest całkowicie odpowiedzialna
za właściwe sparsowanie swojego pliku; zobacz przykład MP3FileInfo.
"""
import os
import sys
def stripnulls(data):
u"usuwa białe znaki i nulle"
return data.replace("\00", " ").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:
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 listę obiektów zawierających 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 klasę 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
- Wynik wykonania tego programu zależy od tego jakie pliki mamy na twardym dysku. Aby otrzymać sensowne wyjście, potrzebujemy zmienić ścieżkę, aby wskazywał na katalog, w którym przechowujemy pliki MP3.
W wyniku wykonania tego programu możemy otrzymać podobne wyjście do poniższego. Jest niemal niemożliwe, abyś otrzymał identyczne wyjście.
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
[edytuj] Definiowanie klas
Python jest całkowicie zorientowany obiektowo: możemy definiować własne klasy, dziedziczyć z własnych lub wbudowanych klas, a także tworzyć instancje zdefiniowanych przez siebie klas.
Tworzenie klas w Pythonie jest proste. Podobnie jak z funkcjami, nie używamy oddzielnego interfejsu definicji. Po prostu definiujemy klasę i zaczynamy ją implementować. Klasa w Pythonie rozpoczyna się słowem kluczowym class, po którym następuje nazwa klasy, a następnie w nawiasach okrągłych umieszczamy, z jakich klas dziedziczymy.
class Nicosc(object): #(1)
pass #(2) (3)
- Nazwa tej klasy to
Nicosc, a klasa ta dziedziczy z wbudowanej klasyobject. Nazwy klas są zazwyczaj pisane przy użyciu wielkich liter np.KazdeSlowoOdzieloneWielkaLitera, ale to kwestia konwencji nazewnictwa; nie jest to wymagane. - Klasa ta nie definiuje żadnych metod i atrybutów, ale żeby kod był zgodny ze składnią Pythona, musimy coś umieścić w definicji, tak więc użyliśmy
pass. Jest to zastrzeżone przez Pythona słowo, które mówi interpreterowi "przejdź dalej, nic tu nie ma". Instrukcja ta nie wykonuje żadnej operacji i powinniśmy stosować ją, gdy chcemy zostawić funkcję lub klasę pustą. - Prawdopodobnie zauważyliśmy już, że elementy w klasie są wyszczególnione za pomocą wcięć, podobnie jak kod funkcji, instrukcji warunkowych, pętli itp. Pierwsza nie wcięta instrukcja nie będzie należała już do klasy.
Instrukcja pass w Pythonie jest analogiczna do pustego zbioru nawiasów klamrowych ({}) w Javie lub w języku C++. |
W prawdziwym świecie większość klas definiuje własne metody i atrybuty. Jednak, jak można zobaczyć wyżej, definicja klasy, oprócz swojej nazwy z dodatkiem object w nawiasach, nie musi nic zawierać. Programiści C++ mogą zauważyć, że w Pythonie klasy nie mają wyraźnie sprecyzowanych konstruktorów i destruktorów. Pythonowe klasy mają coś, co przypomina konstruktor -- metodę __init__.
FileInfoclass FileInfo(dict): #(1)
- Jak już wiemy, klasy, z których chcemy dziedziczyć wyszczególniamy w nawiasach okrągłych, które z kolei umieszczamy bezpośrednio po nazwie naszej klasy. Tak więc klasa
FileInfodziedziczy z wbudowanej klasydict, a ta klasa, to po prostu klasa słownika.
Python obsługuje dziedziczenie wielokrotne. Wystarczy w nawiasach okrągłych, umiejscowionych zaraz po nazwie klasy, wstawić nazwy klas, z których chcemy dziedziczyć i oddzielić je przecinkami np. class klasa(klasa1,klasa2).
[edytuj] Inicjalizowanie i implementowanie klasy
Ten przykład przedstawia inicjalizację klasy FileInfo za pomocą metody __init__.
class FileInfo(dict):
u"przechowuje metadane pliku" #(1)
def __init__(self, filename=None): #(2) (3) (4)
- Klasy mogą (a nawet powinny) posiadać także notkę dokumentacyjną, podobnie jak moduły i funkcje.
- Metoda
__init__jest wywoływana bezpośrednio po utworzeniu instancji klasy. Może kusić, aby nazwać ją konstruktorem klasy, co jednak nie jest prawdą. Metoda__init__wygląda podobnie do konstruktora (z reguły__init__jest pierwszą metodą definiowaną dla klasy), działa podobnie (jest pierwszym fragmentem kodu wykonywanego w nowo utworzonej instancji klasy), a nawet podobnie brzmi (słowo "init" sugeruje, że jest to konstruktor). Niestety nie jest to prawda, ponieważ obiekt jest już utworzony przed wywołaniem metody__init__, a my już otrzymujemy poprawną referencję do świeżo utworzonego obiektu. Jednak__init__w Pythonie, jest tym co najbardziej przypomina konstruktor, a ponadto pełni prawie taką samą rolę. - Pierwszym argumentem każdej metody znajdującej się w klasie, włączając w to
__init__, jest zawsze referencja do bieżącej instancji naszej klasy. Według powszechnej konwencji, argument ten jest zawsze nazywanyself. W metodzie__init__selfodwołuje się do właśnie utworzonego obiektu; w innych metodach klasy,selfodwołuje się do instancji, z której wywołaliśmy daną metodę. Mimo, że musimy wyraźnie określić argumentselfpodczas definiowania metody, ale nie określamy go w czasie wywoływania metody; Python dodaje go automatycznie. - Metoda
__init__może posiadać dowolną liczbę argumentów i podobnie jak w funkcjach, argumenty mogą być zdefiniowane z domyślnymi wartościami (w ten sposób stają się argumentami opcjonalnymi). W tym przypadku argumentfilenamema domyślną wartość określoną jakoNone, który jest Pythonową pustą wartością.
FileInfo
class FileInfo(dict):
u"przechowuje metadane pliku"
def __init__(self, filename=None):
dict.__init__(self) #(1)
self["plik"] = filename #(2)
#(3)
- Niektóre języki pseudo-zorientowane obiektowo jak Powerbuilder posiadają koncepcję "rozszerzania" konstruktorów i innych zdarzeń, w których metoda należąca do nadklasy jest wykonywana automatycznie przed metodą podklasy. Python takiego czegoś nie wykonuje; zawsze należy wyraźnie wywołać odpowiednią metodę należącą do przodka klasy.
- Klasa ta działa podobnie jak słownik (w końcu z niego dziedziczymy), co mogliśmy zauważyć po spojrzeniu na tę linię. Przypisaliśmy argument
filenamejako wartość klucza"plik"w naszym obiekcie. - Zauważmy, że metoda
__init__nigdy nie zwraca żadnej wartości.
[edytuj] Kiedy używać self i __init__
Podczas definiowania metody pewnej klasy, musimy wyraźnie wstawić self jako pierwszy argument każdej metody, włączając w to __init__. Kiedy wywołujemy metodę z klasy nadrzędnej, musimy dołączyć argument self, ale jeśli wywołujemy metodę z zewnątrz, nie określamy argumentu self, po prostu go pomijamy. Python automatycznie wstawi odpowiednią referencję za nas. Na początku może się to wydawać trochę namieszane, jednak wynika to z pewnych różnic, o których jeszcze nie wiemy [6].
[edytuj] Tworzenie instancji klasy
Tworzenie instancji klas jest dosyć proste. W tym celu wywołujemy klasę tak jakby była funkcją, dodając odpowiednie argumenty, które są określone w metodzie __init__. Zwracaną wartością będzie zawsze nowo utworzony obiekt.
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'}
- Utworzyliśmy instancję klasy
FileInfo(zdefiniowaną w modulefileinfo) i przypisaliśmy właśnie utworzony obiekt do zmiennejf. Skorzystaliśmy z parametru"/music/_singles/kairo.mp3", a który będzie odpowiadał argumentowifilenamew metodzie__init__klasyFileInfo. - Każda instancja pewnej klasy ma wbudowany atrybut
__class__, który jest klasą danego obiektu. Programiści Javy mogą być zaznajomieni z klasąClass, która posiada metody takie jakgetName, czy teżgetSuperclass, aby pobrać metadane o pewnym obiekcie. W Pythonie ten rodzaj metadadanych, jest dostępny bezpośrednio z obiektu wykorzystując atrybuty takie jak__class__,__name__, czy__bases__. - Możemy pobrać notkę dokumentacyjną w podobny sposób, jak to czyniliśmy w przypadku funkcji czy modułu. Wszystkie instancje klasy współdzielą tę samą notkę dokumentacyjną.
- Pamiętamy, że metoda
__init__przypisuje argumentfilenamedoself["plik"]? To dobrze, w tym miejscu mamy rezultat tej operacji. Argumenty podawane podczas tworzenia instancji pewnej klasy są wysłane do metody__init__(wyłączając pierwszy argument,self. Python zrobił to za nas).
[edytuj] Odśmiecanie pamięci
Jeśli tworzenie nowej instancji jest proste, to jej usuwanie jest jeszcze prostsze. W ogólności nie musimy wyraźnie zwalniać instancji klasy, ponieważ Python robi to automatycznie, gdy wychodzi one poza swój zasięg. W Pythonie rzadko występują wycieki pamięci.
>>> def leakmem():
... f = fileinfo.FileInfo('/music/_singles/kairo.mp3') #(1)
...
>>> for i in range(100):
... leakmem() #(2)
- Za każdym razem, gdy funkcja
leakmemjest wywoływana, zostaje utworzona instancja klasyFileInfo, a ta zostaje przypisana do zmiennejf, która jest lokalną zmienną wewnątrz funkcji. Funkcja ta kończy się bez jakiegokolwiek wyraźnego zwolnienia pamięci zajmowanej przez zmiennąf, a więc spodziewalibyśmy się wycieku pamięci, lecz tak nie będzie. Kiedy funkcja się kończy, lokalna zmiennafwychodzi poza swój zasięg. W tym miejscu nie ma więcej żadnych referencji do nowej instancjiFileInfo, ponieważ nigdzie nie przypisywaliśmy jej do czegoś innego niżf, tak więc Python zniszczy instancję za nas. - Niezależnie od tego, jak wiele razy wywołamy funkcję
leakmem, nigdy nie nastąpi wyciek pamięci, ponieważ za każdym razem kiedy to zrobimy, Python będzie niszczył nowo utworzony obiekt przed wyjściem z funkcjileakmem.
Technicznym terminem tego sposobu odśmiecania pamięci jest "zliczanie odwołań" (zobacz w Wikipedii). Python przechowuje listę referencji do każdej utworzonej instancji. W powyższym przykładzie, mamy tylko jedną referencję do instancji FileInfo -- zmienną f. Kiedy funkcja się kończy, zmienna f wychodzi poza zasięg, więc licznik odwołań zmniejsza się do 0 i Python zniszczy tę instancję automatycznie.
W poprzednich wersjach Pythona występowały sytuacje, gdy zliczanie odwołań zawodziło i Python nie mógł wyczyścić po nas pamięci. Jeśli tworzyliśmy dwie instancje, które odwoływały się do siebie nawzajem (np. instancja listy dwukierunkowej [7], w których każdy węzeł wskazuje na poprzedni i następny znajdujący się w liście), żadna instancja nie była niszczona automatycznie, ponieważ Python uważał (poprawnie), że ciągle mamy referencję do każdej instancji. Od Pythona 2.0 mamy dodatkowy sposób odśmiecania pamięci, nazywany po ang. mark-and-sweep (oznacz i zamiataj, zobacz w Wikipedii), dzięki któremu Python w sprytny sposób wykrywa różne wirtualne blokady i poprawnie czyści cykliczne odwołania.
Podsumowując, w języku tym można po prostu zapomnieć o zarządzaniu pamięcią i pozostawić tę sprawę Pythonowi.
[edytuj] Materiały dodatkowe
- Python Library Reference omawia wbudowane atrybuty podobne do
__class__ - Python Library Reference dokumentuje moduł
gc, który daje niskopoziomową kontrolę nad odśmiecaniem pamięci
[edytuj] Klasa opakowująca UserDict
Wrócimy na chwilę do przeszłości. Za czasów, kiedy nie można było dziedziczyć wbudowanych typów danych np. słownika, powstawały tzw. klasy opakowujące, które pełniły te same funkcję, co typy wbudowane, ale można je było dziedziczyć. Klasą opakowującą dla słownika była klasa UserDict, która nadal jest dostępna wraz z nowymi wersjami Pythona. Przyglądnięcie się implementacji tej klasy może być dla nas cenną lekcją. Zatem zajrzyjmy do kodu źródłowego klasy UserDict, który znajdują się 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 możemy szybko otworzyć dowolny moduł, który znajduje się w ścieżce do bibliotek, gdy wybierzemy File->Locate... (Ctrl-L). |
UserDict
class UserDict: #(1)
def __init__(self, dict=None): #(2)
self.data = {} #(3)
if dict is not None: self.update(dict) #(4) (5)
- Klasa
UserDictnie dziedziczy nic z innych klas. Jednak nie patrzmy się na to, pamiętajmy, żeby zawsze dziedziczyć zobject(lub innego wbudowanego typu), bo wtedy mamy dostęp do dodatkowych możliwości, które dają nam klasy w nowym stylu. - Jak pamiętamy, metoda
__init__jest wywoływana bezpośrednio po utworzeniu instancji klasy. Przy tworzeniu instancji klasyUserDictmożemy zdefiniować początkowe wartości, poprzez przekazanie słownika w argumenciedict. - W Pythonie możemy tworzyć atrybuty danych (zwane polami w Javie i PowerBuilderze). Atrybuty to kawałki danych przechowywane w konkretnej instancji klasy (moglibyśmy je nazwać atrybutami instancji). W tym przypadku każda instancja klasy
UserDictbędzie posiadać atrybutdata. Aby odwołać się do tego pola z kodu spoza klasy, dodajemy z przodu nazwę instancji np.instancja.data; robimy to w identyczny sposób, jak odwołujemy się do funkcji poprzez nazwę modułu, w którym ta funkcja się znajduje. Aby odwołać się do atrybutu danych z wnętrza klasy, używamyself. Zazwyczaj wszystkie atrybuty są inicjalizowane sensownymi wartościami już w metodzie__init__. Jednak nie jest to wymagane, gdyż atrybuty, podobnie jak zmienne lokalne, są tworzone, gdy po raz pierwszy przypisze się do nich jakąś wartość. - Metoda
updatepowiela zachowanie metody słownika: kopiuje wszystkie klucze i wartości z jednego słownika do drugiego. Metoda ta nie czyści słownika docelowego (tego, z którego wywołaliśmy metodę), ale jeśli były tam już jakieś klucze, to zostaną one nadpisane tymi, które są w słowniku źródłowym; pozostałe klucze nie zmienią się. Myślmy oupdatejak o funkcji łączenia, nie kopiowania. - Z tej składni nie korzystaliśmy jeszcze w tej książce. Jest to instrukcja
if, ale zamiast wciętego bloku, który rozpoczynałby się w następnej linii, korzystamy tu z instrukcji, która znajduje się w jednej linii, zaraz za dwukropkiem. Jest to całkowicie poprawna, skrótowa składnia, której możemy używać, jeśli mamy tylko jedną instrukcję w bloku (tak jak pojedyncza instrukcja bez klamer w C++). Możemy albo skorzystać z tej skrótowej składni, albo tworzyć wcięte bloki, jednak nie możemy ich ze sobą łączyć w odniesieniu do tego samego bloku kodu.
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()
clearjest normalną metodą klasy; jest dostępna publicznie i może być wołana przez kogokolwiek w dowolnej chwili. Zauważmy, że wclear, jak we wszystkich metodach klas, pierwszym argumentem jestself. (Pamiętajmy, że nie dodajemyself, gdy wywołujemy metodę; Python robi to za nas.) Zwróćmy uwagę na podstawową cechę tej klasy opakowującej: przechowuje ona prawdziwy słownik w atrybuciedatai definiuje wszystkie metody wbudowanego słownika, a w każdej z tych metod zwraca wynik identyczny do odpowiedniej metody słownika. (Gdybyśmy zapomnieli, metoda słownikaclearczyści cały słownik kasując jego wszystkie klucze i wartości.)- Metoda słownika o nazwie
copyzwraca nowy słownik, który jest dokładną kopią oryginału (mający takie same pary klucz-wartość). Natomiast klasaUserDictnie może po prostu wywołaćself.data.copy, ponieważ ta metoda zwraca wbudowany słownik, a my chcemy zwrócić nową instancję klasy tej samej klasy, jaką maself. - Używamy atrybutu
__class__, żeby sprawdzić, czyselfjest obiektem klasyUserDict; jeśli tak, to jesteśmy w domu, bo wiemy, jak zrobić kopięUserDict: tworzymy nowy obiektUserDicti przekazujemy mu słownik wyciągnięty zself.data, a wtedy możemy od razu zwrócić nowy obiektUserDictnie wykonując nawet instrukcjiimport copyz następnej linii. - Jeśli
self.__class__nie jestUserDict-em, toselfmusi być jakąś podklasąUserDict-a, a w takim przypadku życie wymaga użycia pewnych trików.UserDictnie wie, jak utworzyć dokładną kopię jednego ze swoich potomków. W tym celu możemy np. znając atrybuty zdefiniowane w podklasie, wykonać na nich pętlę, podczas której kopiujemy każdy z tych atrybutów. Na szczęście istnieje moduł, który wykonuje dokładnie to samo, nazywa się oncopy. Nie będziemy się tutaj wdawać w szczegóły (choć jest to wypaśny moduł, jeśli się w niego trochę wgłębimy). Wystarczy wiedzieć, żecopypotrafi kopiować dowolne obiekty, a tu widzimy, jak możemy z niego skorzystać. - Pozostałe metody są bezpośrednimi przekierowaniami, które wywołują wbudowane metody z
self.data.
| Uwaga! Od Pythona 2.2 nie korzystamy z klasy |
[edytuj] Materiały dodatkowe
- Python Library Reference dokumentuje moduł
copy
[edytuj] Pobieranie i ustawianie elementów
Oprócz normalnych metod, jest też kilka (może kilkanaście) metod specjalnych, które można definiować w klasach Pythona. Nie wywołujemy ich bezpośrednio z naszego kodu (jak zwykłe metody). Wywołuje je za nas Python w określonych okolicznościach lub gdy użyjemy określonej składni np. za pomocą metod specjalnych możemy nadpisać operację dodawania, czy też odejmowania.
Z normalnym słownikiem możemy zrobić dużo więcej, niż bezpośrednio wywołać jego metody. Same metody nie wystarczą. Możemy na przykład pobierać i wstawiać elementy dzięki wykorzystaniu odpowiedniej składni, bez jawnego wywoływania metod. Możemy tak robić dzięki metodom specjalnym. Python odpowiednie elementy składni przekształca na odpowiednie wywołania funkcji specjalnych.
__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'
- Metoda specjalna
__getitem__wygląda dość prosto. Ta metoda specjalna pozwala słownikowi zwrócić pewną wartość na podstawie podanego klucza. A jak ta metodę możemy wywołać? Możemy to zrobić bezpośrednio, ale w praktyce nie robimy w ten sposób, byłoby to niezbyt wygodne. Najlepiej pozwolić Pythonowi wywołać tę metodę za nas. - Z takiej składni korzystamy, by dostać pewną wartość ze słownika. W rzeczywistości Python automatycznie przekształca taką składnię na wywołanie metody
f.__getitem__("plik"). Właśnie dlatego__getitem__nazywamy metodą specjalną: nie tylko możemy ją wywołać, ale Python wywołuje tę metodę także za nas, kiedy skorzystamy z odpowiedniej składni.
Istnieje także analogiczna do __getitem__ metoda __setitem__, która zamiast pobierać pewną wartość, zmienia daną wartość korzystając z pewnego klucza.
__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}
- Analogicznie do
__getitem__, możemy za pomocą__setitem__zmienić wartość pewnego klucza znajdującego się w słowniku. Podobnie, jak w przypadku__getitem__nie musimy jej wywoływać w sposób bezpośredni. Python wywoła__setitem__, jeśli tylko użyjemy odpowiedniej składni. - W taki praktyczny sposób korzystamy ze słownika. Za pomocą tej linii kodu Python wywołuje w sposób ukryty
f.__setitem__("gatunek", 32).
__setitem__ jest metodą specjalną, ponieważ Python wywołuje ją za nas, ale ciągle jest metodą klasy. Kiedy definiujemy klasy, możemy definiować pewne metody, nawet jeśli nadklasa ma już zdefiniowaną tę metodę. W ten sposób nadpisujemy (ang. override) metody nadklas. Tyczy się to także metod specjalnych.
Koncepcja ta jest bazą całego szkieletu, który analizujemy w tym rozdziale. Każdy typ plików może posiadać własną klasę obsługi, która wie, w jaki sposób pobrać metadane z konkretnego typu plików. Natychmiast po poznaniu niektórych atrybutów (jak nazwa pliku i położenie), klasa obsługi będzie wiedziała, jak pobrać dalsze metaatrybuty automatycznie. Możemy to zrobić poprzez nadpisanie metody __setitem__, w której sprawdzamy poszczególne klucze i jeśli dany klucz zostanie znaleziony, wykonujemy dodatkowe operacje.
Na przykład MP3FileInfo jest podklasą FileInfo. Kiedy w MP3FileInfo ustawiamy klucz "plik", nie tylko ustawiamy wartość samego klucza "plik" (jak to robi słownik), lecz także zaglądamy do samego pliku, odczytujemy tagi MP3 i tworzymy pełny zbiór kluczy. Poniższy przykład pokazuje, w jaki sposób to działa.
__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)
- Zwróćmy uwagę na kolejność i liczbę argumentów w
__setitem__. Pierwszym argumentem jest instancja danej klasy (argumentself), z której ta metoda została wywołana, następnym argumentem jest klucz (argumentkey), który chcemy ustawić, a trzecim jest wartość (argumentitem), którą chcemy skojarzyć z danym kluczem. Kolejność ta jest ważna, ponieważ Python będzie wywoływał tę metodą w takiej kolejności i z taką liczbą argumentów. (Nazwy argumentów nic nie znaczą, ważna jest ich ilość i kolejność.) - W tym miejscu zawarte jest sedno całej klasy MP3FileInfo: jeśli przypisujemy pewną wartość do klucza "plik", chcemy wykonać dodatkowo pewne operacje.
- Dodatkowe operacje dla klucza "plik" zawarte są w metodzie
__parse. Jest to inna metoda klasyMP3FileInfo. Kiedy wywołujemy metodę__parseużywamy zmiennejself. Gdybyśmy wywołali samo__parse, odnieślibyśmy się do normalnej funkcji, która jest zdefiniowana poza klasą, a tego nie chcemy wykonać. Kiedy natomiast wywołamyself.__parsebędziemy odnosić się do metody znajdującej się wewnątrz klasy. Nie jest to niczym nowym. W identyczny sposób odnosimy się do atrybutów obiektu. - Po wykonaniu tej dodatkowej operacji, chcemy wykonać metodę nadklasy. Pamiętajmy, że Python nigdy nie zrobi tego za nas; musimy zrobić to ręcznie. Zwróćmy uwagę na to, że odwołujemy się do bezpośredniej nadklasy, czyli do
FileInfo, chociaż on nie posiada żadnej metody o nazwie__setitem__. Jednak wszystko jest w porządku, ponieważ Python będzie wędrował po drzewie przodków jeszcze wyżej dopóki nie znajdzie klasy, która posiada metodę, którą wywołujemy. Tak więc ta linia kodu znajdzie i wywoła metodę__setitem__, która jest zdefiniowana w samej wbudowanej klasie słownika, w klasiedict.
|
Kiedy odwołujemy się do danych zawartych w atrybucie instancji, musimy określić nazwę atrybutu np. |
"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'tytu\u0142': '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'tytu\u0142': 'Sidewinder', 'artysta': 'The Cynic Project', 'gatunek': 18}
- Najpierw tworzymy instancję klasy
MP3FileInfobez podawania nazwy pliku. (Możemy tak zrobić, ponieważ argumentfilenamemetody__init__jest opcjonalny.) PonieważMP3FileInfonie posiada własnej metody__init__, Python idzie wyżej po drzewie nadklas i znajduje metodę__init__w klasieFileInfo. Z kolei__init__w tej klasie ręcznie wykonuje metodę__init__w klasiedict, a potem ustawia klucz"plik"na wartość w zmiennejfilename, który wynosiNone, ponieważ pominęliśmy nazwę pliku. Ostateczniemp3filepoczątkowo jest słownikiem (właściwie klasą potomną słownika) z jednym kluczem"plik", którego wartość wynosiNone. - Teraz rozpoczyna się prawdziwa zabawa. Ustawiając klucz
"plik"wmp3filespowoduje wywołanie metody__setitem__klasyMP3FileInfo(a nie słownika, czyli klasydict). Z kolei metoda ta zauważa, że ustawiamy klucz"plik"z prawdziwą wartością (itemjest prawdą w kontekście logicznym) i wywołujeself.__parse. Chociaż jeszcze nie analizowaliśmy działania metody__parse, możemy na podstawie wyjścia zobaczyć, że ustawia ona kilka innych kluczy jak"album", "artysta","gatunek",u"tytuł"(w unikodzie, bo korzystamy z polskich znaków),"rok", czy też"comment". - Kiedy zmienimy klucz
"plik", proces ten zostanie wykonany ponownie. Python wywoła__setitem__, który następnie wywołaself.__parse, a ten ustawi wszystkie inne klucze.
[edytuj] Zaawansowane metody specjalne
W Pythonie, oprócz __getitem__ i __setitem__, są jeszcze inne metody specjalne, . Niektóre z nich pozwalają dodać funkcjonalność, której się nawet nie spodziewamy. Ten przykład pokazuje inne metody specjalne znanej już nam klasy UserDict.
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)
__repr__jest metodą specjalną, która zostanie wywołana, gdy użyjemyrepr(obiekt).reprjest wbudowaną funkcją Pythona, która zwraca reprezentację danego obiektu w postaci łańcucha znaków. Działa dla dowolnych obiektów, nie tylko obiektów klas. Już wielokrotnie używaliśmy tej funkcji, nawet o tym nie wiedząc. Gdy w oknie interaktywnym wpisujemy nazwę zmiennej i naciskamy ENTER, Python używareprdo wyświetlenia wartości zmiennej. Stwórzmy słownikdz jakimiś danymi i wywołajmyrepr(d), żeby się o tym przekonać.- Metoda
__cmp__zostanie wywoływana, gdy porównujemy za pomocą==dwa dowolne obiekty Pythona, nie tylko instancje klas. Aby porównać wbudowane typy danych (i nie tylko), wykorzystywane są pewne reguły np. słowniki są sobie równy, gdy mają dokładnie takie same pary klucz-wartość; łańcuchy znaków są sobie równe, gdy mają taką samą długość i zawierają taki sam ciąg znaków. Dla instancji klas możemy zdefiniować metodę__cmp__i zaimplementować sposób porównania własnoręcznie, a potem używać==do porównywania obiektów klasy. Python wywoła__cmp__za nas. - Metoda
__len__zostanie wywołana, gdy użyjemylen(obiekt).lenjest wbudowaną funkcją Pythona, która zwraca długość obiektu. Ta metoda działa dla dowolnego obiektu, który można uznać za obiekt posiadający jakąś długość. Długość łańcucha znaków jest równa ilości jego znaków, długość słownika, to liczba jego kluczy, a długość listy lub krotki to liczba ich elementów. Dla obiektów klas możesz zdefiniować metodę__len__i samemu zaimplementować obliczanie długości, a następnie możemy używaćlen(obiekt). Python za nas wywoła metodę specjalną__len__. - Metoda
__delitem__zostanie wywołana, gdy użyjemydel obiekt[klucz]. Dzięki tej funkcji możemy usuwać pojedyncze elementy ze słownika. Kiedy użyjemydeldla pewnej instancji, Python wywoła metodę__delitem__za nas.
Metody specjalne sprawiają, że dowolna klasa może przechowywać pary klucz-wartość w ten sposób, jak to robi słownik. W tym celu definiujemy metodę __setitem__. Każda klasa może działać jak sekwencja, dzięki metodzie __getitem__. Obiekty dowolnej klasy, które posiadają metodę __cmp__, mogą być porównywane przy użyciu ==. A jeśli dana klasa reprezentuje coś, co ma pewną długość, nie musimy definiować metody getLength; po prostu definiujemy metodę __len__ i korzystamy z len(obiekt).
Python posiada wiele innych metod specjalnych. Są takie, które pozwalają klasie zachowywać się jak liczby, umożliwiając dodawanie, odejmowanie i inne operacje arytmetyczne na obiektach. (Klasycznym przykładem jest klasa reprezentująca liczby zespolone z częścią rzeczywistą i urojoną, na których możemy wykonywać wszystkie działania). Metoda __call__ pozwala klasie zachowywać się jak funkcja, a dzięki czemu możemy bezpośrednio wywoływać instancję pewnej klasy.
[edytuj] Materiały dodatkowe
[edytuj] Atrybuty klas
Wiemy już, co to są atrybuty, które są częścią konkretnych obiektów. W Python możemy tworzyć też atrybuty klas, czyli zmienne należące do samej klasy (a nie do instancji tej klasy).
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'tytu\u0142': (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'tytu\u0142': (3, 33, <function stripnulls at 0xb7c000d4>),
'artysta': (33, 63, <function stripnulls at 0xb7c000d4>),
'gatunek': (127, 128, <built-in function ord>)}
MP3FileInfojest klasą, nie jest instancją klasy.tagDataMapjest atrybutem klasy i jest dostępny już przed stworzeniem jakiegokolwiek obiektu danej klasy.- Atrybuty klas są dostępne na dwa sposoby: poprzez bezpośrednie odwołanie do klasy lub poprzez jakąkolwiek instancjętej klasy.
Atrybuty klas mogą być używane jako stałe tych klas (w takim celu korzystamy z nich w klasie MP3FileInfo), ale tak naprawdę nie są stałe. Można je zmieniać.
>>> 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
countjest atrybutem klasycounter.__class__jest wbudowanym atrybutem każdego obiektu. Jest to referencja do klasy, której obiektem jestself(w tym wypadku do klasycounter).- Ponieważ
countjest atrybutem klasy, jest dostępny porzez bezpośrednie odwołanie do klasy, przed stworzeniem jakiegokolwiek obiektu. - Kiedy tworzymy instancję tej klasy, automatycznie zostanie wykonana metoda
__init__, która zwiększa atrybut tej klasy o nazwiecounto1. Operacja ta wpływa na klasę samą w sobie, a nie tylko na dopiero co stworzony obiekt. - Stworzenie drugiego obiektu ponownie zwiększy atrybut
count. Zauważmy, że atrybuty klas są wspólne dla klasy i wszystkich jej instancji.
[edytuj] Funkcje prywatne
Jak większość języków programowania, Python posiada koncepcję elementów prywatnych:
- prywatne funkcje, które nie mogą być wywoływane spoza modułów w których są zdefiniowane
- prywatne metody klas, które nie mogą być spoza nich wywołane
- prywatne atrybuty do których nie ma dostępu spoza klasy
Inaczej niż w większości języków, to czy element jest prywatny, czy nie, zależy tylko od jego nazwy.
Jeżeli nazwa funkcji, metody czy atrybutu zaczyna się od dwóch podkreśleń (ale nie kończy się 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 są dostępnych tylko w tej klasie oraz w klasach z niej dziedziczących). Metody mogą być tylko prywatne (dostępne tylko z wnętrza klasy), bądź publiczne (dostępne wszędzie).
W klasie MP3FileInfo istnieją tylko dwie metody: __parse oraz __setitem__. Jak już zostało przedstawione, __setitem__ jest metodą specjalną: zostanie wywołana, gdy skorzystamy ze składni dostępu do słownika bezpośrednio na instancji danej klasy. Jednak ta metoda jest publiczna i można ją wywołać bezpośrednio (nawet z zewnątrz modułu fileinfo), jeśli istnieje jakiś dobry do tego powód. Jednak metoda __parse jest prywatna, ponieważ posiada dwa podkreślenia na początku nazwy.
>>> 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'
- Jeśli spróbujemy wywołać prywatną metodę, Python rzuci nieco mylący wyjątek, który informuje, że metoda nie istnieje. Oczywiście istnieje ona, lecz jest prywatna, więc nie jest możliwe uzyskanie dostępu do niej spoza klasy. Ściśle mówiąc, metody prywatne są dostępne spoza klasy w której były zdefiniowane, jednak nie w taki prosty sposób. Tak naprawdę nic w Pythonie nie jest prywatne, nazwy metod prywatnych są kodowane oraz odkodowane "w locie", aby wyglądały na niedostępne, gdy korzystamy z ich prawdziwych nazw. Możemy jednak wywołać metodę
__parseklasyMP3FileInfoprzy pomocy nazwy_MP3FileInfo.__parse. Informacja ta jest dosyć interesująca, jednak obiecajmy sobie nigdy nie korzystać z niej w prawdziwym kodzie. Metody prywatne są prywatne nie bez powodu, lecz jak wiele rzeczy w Pythonie, ich prywatność jest kwestią konwencji, nie przymusu.
[edytuj] Materiały dodatkowe
- Python Tutorial omawia działanie prywatnych zmiennych od środka
[edytuj] Podsumowanie
To wszystko, jeśli chodzi o triki z obiektami. W rozdziale dwunastym zobaczymy, w jaki sposób wykorzystywać metody specjalne w normalnej aplikacji w której będziemy zajmowali się tworzeniem pośredników (ang. proxy) dla zdalnych usług sieciowych z użyciem getattr.
W następnym rozdziale dalej będziemy używać kodu programu fileinfo.py, aby poznać takie pojęcia jak wyjątki, operacje na plikach oraz pętlę for.
Zanim zanurkujemy w następnym rozdziale, upewnijmy się, że nie mamy problemów z:
- definiowaniem i tworzeniem instancji klasy
- definiowaniem metody __init__ oraz innych metod specjalnych, a także wiem, kiedy są one wywoływane
- dziedziczeniem innych klas (w ten sposób tworząc podklasę danej klasy)
- definiowaniem atrybutów instancji i atrybutów klas, a także rozumiemy różnice między nimi
- definiowaniem prywatnych metod oraz atrybutów
[edytuj] Wyjątki i operacje na plikach
W tym rozdziale zajmiemy się wyjątkami, obiektami pliku, pętlami for oraz modułami os i sys. Jeśli używaliśmy wyjątków w innych językach programowania, możemy tylko szybko przyjrzeć się składni Pythona, która odpowiada za obsługę wyjątków, ale powinniśmy zwrócić uwagę na część, która omawia w jaki sposób zarządzać plikami.
[edytuj] Obsługa wyjątków
Jak wiele innych języków programowania, Python obsługuje wyjątki. Przy pomocy bloków try...except przechwytujemy wyjątki, natomiast raise rzuca wyjątek.
Wyjątki są w Pythonie wszędzie. Praktycznie każdy moduł w bibliotece standardowej Pythona ich używa. Sam interpreter Pythona również rzuca wyjątki w różnych sytuacjach. Już wiele razy widzieliśmy je w tej książce:
- Próba użycia nieistniejącego klucza w słowniku rzuci wyjątek
KeyError - Wyszukiwanie w liście nieistniejącej wartości rzuci wyjątek
ValueError - Wywołanie nieistniejącej metody obiektu rzuci wyjątek
AttributeError - Użycie nieistniejącej zmiennej rzuci wyjątek
NameError - Mieszanie niezgodnych typów danych spowoduje wyjątek
TypeError
W każdym z tych przypadków, gdy używaliśmy IDE Pythona i wystąpił błąd, to został wypisany wyjątek (w zależności od użytego IDE na przykład na czerwono). Jest to tak zwany nieobsłużony wyjątek. Kiedy podczas wykonywania programu został rzucony wyjątek, nie było w nim specjalnego kodu, który by go wykrył i zaznajomił się z nim, dlatego obsługa tego wyjątku zostaje zrzucona na domyślne zachowanie Pythona, które z kolei wypisuje trochę informacji na temat błędu i kończy pracę programu. W przypadku IDE nie jest to wielka przeszkoda, ale wyobraźmy sobie, co by się stało, gdyby podczas wykonywania właściwego programu nastąpiłby taki błąd, a co z kolei spowodowałoby, że program wyłączyłby się.
Jednak efektem wyjątku nie musi być katastrofa programu. Kiedy wyjątki zostaną rzucone, mogą zostać obsłużone. Czasami przyczyną wystąpienia wyjątku jest błąd w kodzie (na przykład próba użycia zmiennej, która nie istnieje), jednak bardzo często wyjątek możemy przewidzieć. Jeśli otwieramy plik, może on nie istnieć. Jeśli łączysz się z bazą danych, może ona być niedostępna lub możemy nie mieć odpowiednich parametrów dostępu np. hasła. Jeśli wiemy, że jakaś linia kodu może wygenerować wyjątek, powinniśmy próbować ją obsłużyć przy pomocy bloku try...except.
>>> fsock = open("/niemapliku", "r") #(1)
Traceback (most recent call last):
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
- Używając wbudowanej funkcji
open, możemy spróbować otworzyć plik do odczytu (więcej o tej funkcji w następnym podrozdziale). Jednak ten plik nie istnieje i dlatego zostanie rzucony wyjątekIOError. Ponieważ nie przechwytujemy tego wyjątku Python po prostu wypisuje trochę danych pomocnych przy znajdywaniu błędu, a potem zakańcza działanie programu. - Próbujemy otworzyć ten sam nieistniejący plik, jednak tym razem robimy to we wnętrzu bloku
try...except. Gdy metodaopenrzuca wyjątekIOError, jesteśmy na to przygotowani. Liniaexcept IOError:przechwytuje ten wyjątek i wykonuje blok kodu, który w tym wypadku wypisuje bardziej przyjazny opis błędu. - Gdy wyjątek zostanie już obsłużony, program wykonuje się dalej w sposób normalny, od pierwszej linii po bloku
try...except. Zauważmy, że ta linia zawsze wypisze tekst "Ta linia zawsze zostanie wypisana", niezależnie, czy wyjątek wystąpi, czy też nie. Jeśli naprawdę mielibyśmy taki plik na dysku, to i tak ta linia zostałaby wykonana.
Wyjątki mogą wydawać się nieprzyjazne (w końcu, jeśli nie przechwycimy wyjątku, program zostanie przerwany), jednak pomyślmy, z jakiej innej alternatywy moglibyśmy skorzystać. Czy chcielibyśmy dostać bezużyteczny obiekt, który przestawia nieistniejący plik? I tak musielibyśmy sprawdzić jego poprawność w jakiś sposób, a jeśli byśmy tego nie zrobili, to nasz program wykonałby jakieś dziwne, nieprzewidywalne operacje, których byśmy się nie spodziewali. Nie byłaby to wcale dobra zabawa. Z wyjątkami błędy występują natychmiast i możemy je obsługiwać w standardowy sposób u źródła problemu.
[edytuj] Wykorzystanie wyjątków do innych celów
Jest wiele innych sposobów wykorzystania wyjątków, oprócz do obsługi błędów. Dobrym przykładem jest importowanie modułów Pythona, sprawdzając czy nastąpił wyjątek. Jeśli moduł nie istnieje zostanie rzucony wyjątek ImportError. Dzięki temu możemy zdefiniować wiele poziomów funkcjonalności, które zależą od modułów dostępnych w czasie wykonania, a dzięki temu możemy wspierać różnorodne platformy (kod zależny od platformy jest podzielony na oddzielne moduły).
Możemy też zdefiniować własne wyjątki, tworząc klasę, która dziedziczy z wbudowanej klasy Exception, a następnie możemy rzucać wyjątki przy pomocy polecenia raise. Możemy zajrzeć do części "materiały dodatkowe", aby dowiedzieć się więcej na ten temat.
Następny przykład pokazuje, w jaki sposób wykorzystywać wyjątki, aby obsłużyć funkcjonalność zdefiniowaną jedynie dla konkretnej platformy. Kod pochodzi z modułu getpass, który jest modułem opakowującym, którym umożliwia pobranie hasła od użytkownika. Pobieranie hasła jest całkowicie różne na platformach UNIX, Windows, czy Mac OS, ale kod ten obsługuje wszystkie te różnice.
# 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
termiosjest modułem określonym dla UNIX-a, który dostarcza niskopoziomową kontrolę nad terminalem wejścia. Jeśli moduł ten jest niedostępny (ponieważ, nie ma tego na naszym systemie, ponieważ system tego nie obsługuje), importowanie nawali, a Python rzuci wyjątekImportError, który przechwycimy.- OK, nie mamy
termios, więc spróbujmy zmsvcrt, który jest modułem charakterystycznym dla systemu Windows, a dostarcza on API do wielu przydatnych funkcji dla tego systemu. Jeśli to także nie zadziała, Python rzuci wyjątekImportError, który także przechwycimy. - Jeśli pierwsze dwa nie zadziałają, próbujemy zaimportować funkcję z
EasyDialogs, która jest w module określonym dla Mac OS-a, który dostarcza funkcje przeznaczone dla wyskakujących okien dialogowych różnego typu. I ponownie, jeśli moduł nie istnieje, Python rzuci wyjątekImportError, który też przechwytujemy. - Żaden z tych modułów, które są przeznaczone dla konkretnej platformy, nie są dostępne (jest to możliwe, ponieważ Python został przeportowany na wiele różnych platform), więc musimy powrócić do domyślnej funkcji do pobierania hasła (która jest zdefiniowana gdzieś w module
getpass). Zauważmy, co robimy: przypisujemy funkcjędefault_getpassdo zmiennejgetpass. Jeśli czytaliśmy oficjalną dokumentacjęgetpass, mówi ona, że modułgetpassdefiniuje funkcjęgetpass. Wykonuje to poprzez powiązaniegetpassz odpowiednią funkcją, która zależy od naszej platformy. Kiedy wywołujemy funkcjęgetpass, tak naprawdę wywołujemy funkcję określoną dla konkretnej platformy, którą określił za nas powyższy kod. Nie musimy się martwić, na jakiej platformie uruchamiamy nasz kod -- wywołujemy tylkogetpass, a funkcja ta wykona zawsze odpowiednią czynność. - Blok
try...except, podobnie jak instrukcjaif, może posiadać klauzuleelse. Jeśli żaden wyjątek nie zostanie rzucony podczas wykonywania blokutry, spowoduje to wywołanie klauzulielse. W tym przypadku oznacza to, że importfrom EasyDialogs import AskPasswordzadziałał, a więc możemy powiązaćgetpassz funkcjąAskPassword. Każdy inny bloktry...exceptw przedstawionym kodzie posiada podobną klauzulęelse, która pozwala, gdy zostanie zaimportowany działający moduł, przypisać dogetpassodpowiednią funkcję.
[edytuj] Materiały dodatkowe
- Python Tutorial mówi na temat definiowania, rzucania własnych wyjątków i jednoczesnej obsługi wielu wyjątków
- Python Library Reference opisuje wszystkie wbudowane wyjątki
- Python Library Reference dokumentuje moduł
getpass. - Python Library Reference dokumentuje moduł
traceback, który zapewnia niskopoziomowy dostęp do atrybutów wyjątków, po tym, jak wyjątek zostanie rzucony. - Python Reference Manual omawia bardziej technicznie blok
try...except.
[edytuj] Praca z obiektami plików
Python posiada wbudowaną funkcję open, służącą do otwierania plików z dysku. open zwraca obiekt pliku posiadający metody i atrybuty, dzięki którym możemy dostać się do pliku i wykonywać na nim pewne operacje.
>>> 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'
- Metoda
openprzyjmuje do trzech argumentów: nazwę pliku, tryb i argument buforowania. Tylko pierwszy z nich, nazwa pliku, jest wymagany; pozostałe dwa są opcjonalne. Jeśli nie są podane, plik zostanie otwarty do odczytu w trybie tekstowym. Tutaj otworzyliśmy plik do odczytu w trybie binarnym. (print open.__doc__da nam świetne objaśnienie wszystkich możliwych trybów.) - Metoda
openzwraca obiekt (w tym momencie nie powinno to już być zaskoczeniem). Obiekt pliku ma kilka użytecznych atrybutów. - Atrybut
modeobiektu pliku mówi nam, w jakim trybie został on otwarty. - Atrybut
namezwraca ścieżkę do pliku, który jest dostępny z tego obiektu.
[edytuj] Czytanie z pliku
Otworzywszy plik, będziemy chcieli odczytać z niego informacje, tak jak pokazano to w następnym przykładzie.
>>> 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 \037' >>> f.tell() #(5) 7543037
- Obiekt pliku przechowuje stan otwartego pliku. Metoda
tellzwraca aktualną pozycję w otwartym pliku. Z uwagi na to, że nie robiliśmy jeszcze nic z tym plikiem, aktualna pozycja to0, czyli początek pliku. - Metoda
seekobiektu pliku służy do poruszania się po otwartym pliku. Jej drugi argument określa znaczenie pierwszego argumentu; jeśli argument drugi wynosi0, oznacza to, że pierwszy argument odnosi się do pozycji bezwzględnej (czyli licząc od początku pliku),1oznacza przeskok do innej pozycji względem pozycji aktualnej (licząc od pozycji aktualnej),2oznacza przeskok do danej pozycji względem końca pliku. Jako że tagi MP3, o które nam chodzi, przechowywane są na końcu pliku, korzystamy z opcji2i przeskakujemy do pozycji oddalonej o 128 bajtów od końca pliku. - Metoda
tellpotwierdza, że rzeczywiście zmieniliśmy pozycję pliku. - Metoda
readczyta określoną liczbę bajtów z otwartego pliku i zwraca dane w postaci łańcucha znaków, które zostały odczytane. Opcjonalny argument określa maksymalną liczbę bajtów do odczytu. Jeśli nie zostanie podany argument,readbędzie czytał do końca pliku. (W tym przypadku moglibyśmy użyć samegoread(), ponieważ wiemy dokładnie w jakiej pozycji w pliku jesteśmy i w rzeczywistości odczytujemy ostanie 128 bajtów.) Odczytane dane przypisujemy do zmiennejtagData, a bieżąca pozycja zostaje uaktualniana na podstawie ilości odczytanych bajtów. - Metoda
tellpotwierdza, że zmieniła się bieżąca pozycja. Jeśli pokusimy się o wykonanie obliczenia, zauważymy, że po odczytaniu128bajtów aktualna pozycja wzrosła o128.
[edytuj] Zamykanie pliku
Otwarte pliki zajmują zasoby systemu, a inne aplikacje czasami mogą nie mieć do nich dostępu (zależy to od trybu otwarcia pliku), dlatego bardzo ważne jest zamykanie plików tak szybko, jak tylko skończymy na nich pracę.
>>> 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 ? 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)
- Atrybut
closedobiektu pliku mówi, czy plik jest otwarty, czy też nie. W tym przypadku plik jeszcze jest otwarty (closedjest równeFalse). - Aby zamknąć plik należy wywołać metodę
closeobiektu pliku. Zwalnia to blokadę, która nałożona była na plik (jeśli była nałożona), oczyszcza buforowane dane (jeśli jakiekolwiek dane w nim występują), które system nie zdążył jeszcze rzeczywiście zapisać, a następnie zwalnia zasoby. - Atrybut
closedpotwierdza, że plik jest zamknięty. - To że plik został zamknięty, nie oznacza od razu, że obiekt przestaje istnieć. Zmienna
fbędzie istnieć póki nie wyjdzie poza swój zasięg, lub nie zostanie skasowana ręcznie. Aczkolwiek żadna z metod służących do operowania na otwartym pliku, nie będzie działać od momentu jego zamknięcia; wszystkie rzucają wyjątek. - Wywołanie
closena pliku uprzednio zamkniętym nie zwraca wyjątku; w przypadku błędu cicho sobie z nim radzi.
[edytuj] Błędy wejścia/wyjścia
Zrozumienie kodu fileinfo.py z poprzedniego rozdziału, nie powinno już być problemem. Kolejny przykład pokazuje, jak bezpiecznie otwierać i zamykać pliki oraz jak należycie obchodzić się z błędami.
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
- Ponieważ otwieranie pliku i czytanie z niego jest ryzykowne, a także operacje te mogą rzucić wyjątek, cały ten kod jest ujęty w blok
try...except. (Hej, czy zestandaryzowane wcięcia nie są świetne? To moment, w którym zaczynasz je doceniać.) - Funkcja
openmoże rzucić wyjątekIOError. (Plik może nie istnieć.) - Funkcja
seekmoże rzucić wyjątekIOError. (Plik może być mniejszy niż 128 bajtów.) - Funkcja
readmoże rzucić wyjątekIOError. (Być może dysk posiada uszkodzony sektor, albo plik znajduje się na dysku sieciowym, a sieć właśnie przestała działać.) - Nowość: blok
try...finally. Nawet po udanym otworzeniu pliku przezopenchcemy być całkowicie pewni, że zostanie on zamknięty niezależnie od tego, czy metodyseekireadrzucą wyjątki, czy też nie. Właśnie do takich rzeczy służy bloktry...finally: kod z blokufinallyzostanie zawsze wykonany, nawet jeśli jakaś instrukcja blokutryrzuci wyjątek. Należy o tym myśleć jak o kodzie wykonywanym na zakończenie operacji, niezależnie od tego co działo się wcześniej. - Nareszcie poradzimy sobie z wyjątkiem
IOError. Może to być wyjątek wywołany przez którąkolwiek z funkcjiopen,seek, czyread. Tym razem nie jest to dla nas istotne, gdyż jedyną rzeczą, którą zrobimy to zignorowanie tego wyjątku i kontynuowanie dalszej pracy programu. (Pamiętajmy,passjest wyrażeniem Pythona, które nic nie robi.) Takie coś jest całkowicie dozwolone; to że przechwyciliśmy dany wyjątek, nie oznacza, że musimy z nim cokolwiek robić. Wyjątek zostanie potraktowany tak, jakby został obsłużony, a kod będzie normalnie kontynuowany od następnej linijki kodu po blokutry...except.
[edytuj] Zapisywanie do pliku
Jak można przypuszczać, istnieje również możliwość zapisywania danych do plików w sposób bardzo podobny do odczytywania. Wyróżniamy dwa podstawowe tryby otwierania plików:
- w trybie "append", w którym dane będą dodawane na końcu pliku
- w trybie "write", w którym plik zostanie nadpisany.
Oba tryby, jeśli plik nie będzie istniał, utworzą go automatycznie, dlatego nie ma potrzeby na fikuśne działania typu "jeśli dany plik jeszcze nie istnieje, utwórz nowy pusty plik, aby móc go otworzyć po raz pierwszy". Po prostu otwieramy plik i zaczynamy do niego zapisywać dane.
>>> 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
- Zaczynamy odważnie: tworzymy nowy lub nadpisujemy istniejący plik test.log, a następnie otwieramy do zapisu. (Drugi argument
"w"oznacza otwieranie pliku do zapisu.) Tak, jest to dokładnie tak niebezpieczne, jak brzmi. Miejmy nadzieję, że poprzednia zawartość pliku nie była istota, bo już jej nie ma. - Dane do nowo otwartego pliku dodajemy za pomocą metody
writeobiektu zwróconego przezopen. - Ten jednowierszowiec otwiera plik, czyta jego zawartość i drukuje na ekran.
- Przypadkiem wiemy, że test.log istnieje (w końcu właśnie skończyliśmy do niego pisać), więc możemy go otworzyć i dodawać dane. (Argument
"a"oznacza otwieranie pliku w trybie dodawania danych na koniec pliku.) Właściwie moglibyśmy to zrobić nawet wtedy, gdyby plik nie istniał, ponieważ otworzenie pliku w trybie"a"spowoduje jego powstanie, jeśli będzie to potrzebne. Otworzenie w trybie"a"nigdy nie uszkodzi aktualnej zawartości pliku. - Jak widać, zarówno pierwotnie zapisane, jak i dopisane dane, aktualnie znajdują się w pliku test.log. Należy zauważyć, że znaki końca linii nie są uwzględnione. Jako że nie zapisywaliśmy znaków końca linii w żadnym z przypadków, plik ich nie zawiera. Znaki końca linii możemy zapisać za pomocą symbolu
"\n". Z uwagi na fakt, iż tego nie zrobiliśmy, całość danych w pliku wylądowała w jednej linijce.
[edytuj] Materiały dodatkowe
- Python Tutorial opisuje, jak czytać i zapisywać pliki, w tym także, jak czytać pliki linia po linii lub jak wczytać wszystkie linie na raz do listy
- Python Knowledge Base odpowiada na najczęściej zadawane pytania dotyczące plików
- Python Library Reference omawia wszystkie metody obiektu pliku.
[edytuj] Pętla for
Podobnie jak wiele innych języków, Python posiada pętlę for. Jedynym powodem, dla którego nie widzieliśmy tej pętli wcześniej jest to, że Python posiada tyle innych użytecznych rzeczy, że nie potrzebujemy jej aż tak często.
Wiele języków programowania nie posiada typu danych o takich możliwościach, jakie daje lista w Pythonie, dlatego też w tych językach trzeba wykonywać dużo manualnej pracy, trzeba określać początek, koniec i krok, aby przejść zakres liczb całkowitych, znaków lub innych iteracyjnych jednostek. Jednak pythonowa pętla for, przechodzi całą listę w identyczny sposób, jak ma to miejsce w wyrażeniach listowych.
for>>> li = ['a', 'b', 'e'] >>> for s in li: #(1) ... print s #(2) a b e >>> print "\n".join(li) #(3) a b e
- Składnia pętli
forjest bardzo podobna do składni wyrażenia listowego.lijest listą, a zmiennasbędzie przyjmować kolejne wartości elementów tej listy podczas wykonywania pętli, zaczynając od elementu pierwszego. - Podobnie jak wyrażenie
ifi inne bloki tworzone za pomocą wcięć, pętlaformoże posiadać dowolną liczbę linii kodu. - To główna przyczyna, dla której jeszcze nie widzieliśmy pętli
for. Po prostu jej nie potrzebowaliśmy. Jest to zadziwiające, jak często trzeba wykorzystywać pętleforw innych językach, podczas gdy tak naprawdę potrzebujemy metodyjoin, czy też wyrażenia listowego.
Wykonanie "normalnej" (zgodnie ze standardami Visual Basica), licznikowej pętli for jest także bardzo proste.
>>> 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) ... print li[i] a b c d e
- Jak zobaczyliśmy w przykładzie 3.23, "Przypisywanie kolejnych wartości", funkcja
rangetworzy listę liczb całkowitych, a tę listę będziemy chcieli przejść za pomocą pętli. Powyższy kod być może wygląda trochę dziwacznie, lecz bywa przydatny do tworzenia pętli licznikowej. - Nigdy tego nie róbmy. Jest to visualbasicowy styl myślenia. Skończmy z nim. Powinno się iterować listę, jak pokazano to w poprzednim przykładzie.
Pętle for nie zostały stworzone do tworzenia prostych pętli licznikowych. Służą one raczej do przechodzenia po wszystkich elementach w danym obiekcie. Poniżej pokazano przykład, jak można iterować po słowniku za pomocą pętli for:
>>> import os >>> for k, v in os.environ.items(): #(1) (2) ... print "%s=%s" % (k, v) USERPROFILE=C:\Documents and Settings\mpilgrim 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 Settings\mpilgrim OS=Windows_NT COMPUTERNAME=MPILGRIM USERNAME=mpilgrim [...ciach...]
os.environjest słownikiem zmiennych środowiskowych zdefiniowanych w systemie operacyjnym. W Windowsie mamy tutaj zmienne użytkownika i systemu dostępne z MS-DOS-a. W Uniksie mamy tu zmienne wyeksportowane w twojej powłoce przez skrypty startowe. W systemie Mac OS nie ma czegoś takiego jak zmienne środowiskowe, więc słownik ten jest pusty.os.environ.items()zwraca listę krotek w postaci[(klucz1, wartosc1), (klucz2, wartosc2), ...]. Pętlaforiteruje po tej liście. W pierwszym przebiegu pętli, przypisuje ona wartośćklucz1do zmiennejk, a wartośćwartosc1dov, więck = "USERPROFILE", av = "C:\Documents and Settings\mpilgrim". W drugim przebiegu pętli,kprzyjmuje wartość drugiego klucza, czyli"OS", avbierze odpowiadającą temu kluczowi wartość, czyli"Windows_NT".- Za pomocą wielozmiennego przypisania i wyrażeń listowych, możemy zastąpić całą pętle
forjednym wyrażeniem. Z której metody będziemy korzystać w kodzie, jest kwestią stylu pisania. Niektórym się to może podobać, ponieważ za pomocą wyrażenia listowego jasno stwierdzamy, że to co robimy to odwzorowywanie słownika na listę, a następnie łączymy otrzymaną listę w jeden napis. Inni z kolei preferują pisanie z użyciem pętlifor. Otrzymane wyjście programu jest identyczne w obydwu przypadkach, jakkolwiek wersja w tym przykładzie jest nieco szybsza, ponieważ tutaj tylko raz wykorzystujemy instrukcjęprint.
Spójrzmy teraz na pętlę for w MP3FileInfo z przykładu fileinfo.py wprowadzonego w rozdziale 5.
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)
tagDataMapjest atrybutem klasy, który definiuje tagi, jakie będziemy szukali w pliku MP3. Tagi są przechowywane w polach o ustalonej długości. Ponieważ czytamy ostatnie 128 bajtów pliku, bajty od 3 do 32 z przeczytanych danych są zawsze tytułem utworu, bajty od 33 do 62 są zawsze nazwą artysty, od 63 do 92 mamy nazwę albumu itd. Zauważmy, żetagDataMapjest słownikiem krotek, a każda krotka przechowuje dwie liczby całkowite i jedną referencję do funkcji.- Wygląda to skomplikowanie, jednak takie nie jest. Struktura zmiennych w pętli
forodpowiada strukturze elementów listy zwróconej poprzez metodęitems. Pamiętamy, żeitemszwraca listę krotek w formie(klucz, wartosc). Jeśli pierwszym elementem tej listy jest(u"tytuł", (3, 33, <function stripnulls at 0xb7c91f7c>)), tak więc podczas pierwszego przebiegu pętli,tagjest równeu"tytuł",startprzyjmuje wartość3,endwartość33, aparseFunczawiera funkcjęstripnulls. - Kiedy już wydobędziemy wszystkie parametry tagów pliku MP3, zapisanie danych odnoszących się do określonych tagów będzie proste. W tym celu wycinamy napis
tagdataod wartości w zmiennejstartdo wartości w zmiennejend, aby pobrać aktualne dane dla tego tagu, a następnie wywołujemy funkcjęparseFunc, aby przetworzyć te dane, a potem przypisujemy zwróconą wartość do kluczatagw słownikuself(ściślej,selfjest instancją podklasy słownika). Po przejściu wszystkich elementów wtagDataMap,selfbędzie przechowywał wartości dla wszystkich tagów, a my będziemy mogli się dowiedzieć o utworze, co tylko będziemy chcieli.
[edytuj] Korzystanie z sys.modules
Moduły, podobnie jak wszystko inne w Pythonie, są obiektami. Jeśli wcześniej zaimportowaliśmy pewien moduł, możemy pobrać do niego referencję za pośrednictwem globalnego słownika sys.modules.
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
- Moduł
syszawiera informacje dotyczące systemu jak np. wersję uruchomionego Pythona (sys.versionlubsys.version_info) i opcje dotyczące systemu np. maksymalna dozwolona głębokość rekurencji (sys.getrecursionlimit()isys.setrecursionlimit()). sys.modulesjest słownikiem zawierającym wszystkie moduły, które zostały zaimportowane od czasu startu Pythona. W słowniku tym kluczem jest nazwa danego modułu, a wartością jest obiekt tego modułu. Dodajmy, że jest tu więcej modułów niż nasz program zaimportował. Python wczytuje niektóre moduły podczas startu, a jeśli używasz IDE Pythona ,sys.moduleszawiera wszystkie moduły zaimportowane przez wszystkie programy uruchomione wewnątrz IDE.
Poniższy przykład pokazuje, jak wykorzystywać sys.modules.
sys.modules>>> import fileinfo #(1) >>> print '\n'.join(sys.modules.keys()) win32api os.path os fileinfo exceptions __main__ ntpath nt sys __builtin__ site signal UserDict stat >>> fileinfo <module 'fileinfo' from 'fileinfo.pyc'> >>> sys.modules["fileinfo"] #(2) <module 'fileinfo' from 'fileinfo.pyc'>
- Podczas importowania nowych modułów, zostają one dodane do
sys.modules. To tłumaczy dlaczego ponowne zaimportowanie tego samego modułu jest bardzo szybkie. Otóż Python aktualnie posiada wczytany i zapamiętany moduł wsys.modules, więc za drugim razem kiedy importujemy moduł, Python spogląda po prostu tylko do tego słownika. - Podając nazwę wcześniej zaimportowanego modułu (w postaci łańcucha znaków), możemy pobrać referencję do samego modułu poprzez bezpośrednie wykorzystanie słownika
sys.modules.
Kolejny przykład pokazuje, jak wykorzystywać atrybut klasy __module__ razem ze słownikiem sys.modules, aby pobrać referencję do modułu, w którym ta klasa jest zdefiniowana.
__module__>>> from fileinfo import MP3FileInfo >>> MP3FileInfo.__module__ #(1) 'fileinfo' >>> sys.modules[MP3FileInfo.__module__] #(2) <module 'fileinfo' from 'fileinfo.pyc'>
- Każda klasa Pythona posiada wbudowany atrybut klasy, jakim jest
__module__, a który przechowuje nazwę modułu, w którym dana klasa jest zdefiniowana. - Łącząc to z
sys.modules, możemy pobrać referencję do modułu, w którym ta klasa jest zdefiniowana.
Teraz już jesteś przygotowany do tego, aby zobaczyć w jaki sposób sys.modules jest wykorzystywany w fileinfo.py, czyli przykładowym programie wykorzystanym w rozdziale 5. Poniższy przykład przedstawia fragment kodu.
sys.modules w fileinfo.py
def getFileInfoClass(filename, module=sys.modules[FileInfo.__module__]): #(1)
u"zwraca klasę 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)
- Jest to funkcja z dwoma argumentami. Argument
filenamejest wymagany, alemodulejest argumentem opcjonalnym i domyślnie wskazuje na moduł, który zawiera klasęFileInfo. Wygląda to nieefektywnie, ponieważ może się wydawać, że Python wykonuje wyrażeniesys.modulesza każdym razem, gdy funkcja zostaje wywołana. Tak na prawdę, Python wykonuje domyślne wyrażenia tylko raz, podczas pierwszego zaimportowania modułu. Jak zobaczymy później, nigdy nie wywołamy tej funkcji z argumentemmodule, więc argumentmodulesłuży nam raczej jako stała na poziomie tej funkcji. - Funkcji tej przyjrzymy się później, po tym, jak zanurkujemy w module
os. Na razie zaufaj, że linia ta sprawia, żesubclassprzechowuje nazwę klasy np.MP3FileInfo. - Już wiemy, że funkcja
getattrzwraca nam referencje do obiektu poprzez nazwę.hasattrjest funkcją uzupełniającą, która sprawdza, czy obiekt posiada określony atrybut. W tym przypadku sprawdzamy, czy moduł posiada określoną klasę (funkcja ta działa na dowolnym obiekcie i dowolnym atrybucie, podobnie jakgetattr). Ten kod możemy na język polski przetłumaczyć w ten sposób: „Jeśli ten moduł posiada klasę o nazwie zawartej w zmiennejsubclass, to ją zwróć, w przeciwnym wypadku zwróć klasęFileInfo”.
[edytuj] Materiały dodatkowe
- Python Tutorial omawia dokładnie, kiedy i jak domyślne argumenty są wyznaczane
- Python Library Reference dokumentuje moduł
sys
[edytuj] Praca z katalogami
Moduł os.path zawiera kilka funkcji służących do manipulacji plikami i katalogami (w systemie Windows nazywanymi folderami). Przyjrzymy się teraz obsłudze ścieżek i odczytywaniu zawartości katalogów.
>>> import os
>>> os.path.join("c:\\music\\ap\\", "mahadeva.mp3") #(1) (2)
'c:\\music\\ap\\mahadeva.mp3'
>>> os.path.join("c:\\music\\ap", "mahadeva.mp3") #(3)
'c:\\music\\ap\\mahadeva.mp3'
>>> os.path.expanduser("~") #(4)
'c:\\Documents and Settings\\mpilgrim\\My Documents'
>>> os.path.join(os.path.expanduser("~"), "Python") #(5)
'c:\\Documents and Settings\\mpilgrim\\My Documents\\Python'
os.pathjest referencją do modułu, a ten moduł zależy od platformy z jakiej korzystamy. Tak jakgetpassniweluje różnice między platformami ustawiającgetpassna funkcję odpowiednią dla naszego systemu, takosustawiapathna moduł specyficzny dla konkretnej platformy.- Funkcja
joinmodułuos.pathtworzy ścieżkę dostępu do pliku z jednej lub kilku ścieżek częściowych. W tym przypadku po prostu łączy dwa łańcuchy znaków. (Zauważmy, że w Windowsie musimy używać podwójnych ukośników.) - W tym, trochę bardziej skomplikowanym, przypadku,
joindopisze dodatkowy ukośnik do ścieżki przed dołączeniem do niej nazwy pliku. Nie musimy pisać małej, głupiej funkcjiaddSlashIfNecessary, ponieważ mądrzy ludzie zrobili już to za nas. expanduserrozwinie w ścieżce znak~na ścieżkę katalogu domowego aktualnie zalogowanego użytkownika. Ta funkcja działa w każdym systemie, w którym użytkownicy mają swoje katalogi domowe, między innymi w systemach Windows, UNIX i Mac OS X, ale w systemie Mac OS nie otrzymujemy żadnych efektów.- Używając tych technik, możemy łatwo tworzyć ścieżki do plików i katalogów wewnątrz katalogu domowego.
>>> os.path.split("c:\\music\\ap\\mahadeva.mp3") #(1)
('c:\\music\\ap', 'mahadeva.mp3')
>>> (filepath, filename) = os.path.split("c:\\music\\ap\\mahadeva.mp3") #(2)
>>> filepath #(3)
'c:\\music\\ap'
>>> filename #(4)
'mahadeva.mp3'
>>> (shortname, extension) = os.path.splitext(filename) #(5)
>>> shortname
'mahadeva'
>>> extension
'.mp3'
- Funkcja
splitdzieli pełną ścieżkę i zwraca krotkę, która zawiera ścieżkę do katalogu i nazwę pliku. Pamiętasz, jak mówiliśmy, że można używać wielozmiennego przypisania do zwracania kilku wartości z funkcji?splitjest taką właśnie funkcją. - Przypisujesz wynik działania funkcji
splitdo krotki dwóch zmiennych. Każda zmienna będzie teraz zawierać wartość odpowiedniego elementu krotki zwróconej przez funkcjęsplit. - Pierwsza zmienna,
filepath, zawiera pierwszy element zwróconej listy -- ścieżkę pliku. - Druga zmienna,
filename, zawiera drugi element listy -- nazwę pliku. - Moduł
os.pathzawiera też funkcjęsplitext, która zwraca krotkę zawierającą właściwą nazwę pliku i jego rozszerzenie. Używamy tej samej techniki, co poprzednio, do przypisania każdej części do osobnej zmiennej.
>>> 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']
- Funkcja
listdirpobiera ścieżkę do katalogu i zwraca listę jego zawartości. listdirzwraca zarówno pliki jak i katalogi, bez wskazania które są którymi.- Możemy użyć filtrowania listy i funkcji
isfilemodułuos.path, aby oddzielić pliki od katalogów.isfileprzyjmuje ścieżkę do pliku i zwracaTrue, jeśli reprezentuje ona plik alboFalsew innym przypadku. W przykładzie używamyos.path.join, aby uzyskać pełną ścieżkę, aleisfilepracuje też ze ścieżkami względnymi wobec bieżącego katalogu. Możemy użyćos.getcwd()aby pobrać bieżący katalog. os.pathzawiera też funkcjęisdir, która zwracaTrue, jeśli ścieżka reprezentuje katalog iFalsew innym przypadku. Możemy jej użyć do uzyskania listy podkatalogów.
def listDirectory(directory, fileExtList):
u"zwraca listę obiektów zawierających 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)
os.listdir(directory)zwraca listę wszystkich plików i podkatalogów w katalogudirectory.- Iterując po liście z użyciem zmiennej
f, wykorzystujemyos.path.normcase(f), aby znormalizować wielkość liter zgodnie z domyślną wielkością liter w systemem operacyjnym. Funkcjanormcasejest użyteczną, prostą funkcją, która stanowi równoważnik pomiędzy systemami operacyjnymi, w których wielkość liter w nazwie pliku nie ma znaczenia, w którym np. mahadeva.mp3 i mahadeva.MP3 są takimi samymi plikami. Na przykład w Windowsie i Mac OS,normcasebędzie konwertował całą nazwę pliku na małe litery, a w systemach kompatybilnych z UNIX-em funkcja ta będzie zwracała niezmienioną nazwę pliku. - Iterując ponownie po liście z użyciem
f, wykorzystujemyos.path.splitext(f), aby podzielić nazwę pliku na nazwę i jej rozszerzenie. - Dla każdego pliku sprawdzamy, czy rozszerzenie jest w liście plików, o które nam chodzi (czyli
fileExtList, która została przekazana dolistDirectory). - Dla każdego pliku, który nas interesuje, wykorzystujemy
os.path.join(directory, f), aby skonstruować pełną ścieżkę pliku i zwrócić listę zawierającą pełne ścieżki.
Jest jeszcze inna metoda dostania się do zawartości katalogu. Metoda ta jest bardzo potężna i używa zestawu symboli wieloznacznych (ang. wildcard), z którymi można się spotkać pracując w linii poleceń.
glob
>>> 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\\_singles\\a_time_long_forgotten_con.mp3',
'c:\\music\\_singles\\hellraiser.mp3',
'c:\\music\\_singles\\kairo.mp3',
'c:\\music\\_singles\\long_way_home1.mp3',
'c:\\music\\_singles\\sidewinder.mp3',
'c:\\music\\_singles\\spinning.mp3']
>>> glob.glob('c:\\music\\_singles\\s*.mp3') #(3)
['c:\\music\\_singles\\sidewinder.mp3',
'c:\\music\\_singles\\spinning.mp3']
>>> glob.glob('c:\\music\\*\\*.mp3') #(4)
- Jak wcześniej powiedzieliśmy,
os.listdirpobiera ścieżkę do katalogu i zwraca wszystkie pliki i podkatalogi, które się w nim znajdują. - Z drugiej strony, moduł
globna podstawie podanego wyrażenia składającego się z symboli wieloznacznych, zwraca pełne ścieżki wszystkich plików, które spełniają te wyrażenie. Tutaj wyrażenie jest ścieżką do katalogu plus"*.mp3", który będzie dopasowywał wszystkie pliki .mp3. Dodajmy, że każdy element zwracanej listy jest już pełną ścieżką do pliku. - Jeśli chcemy znaleźć wszystkie pliki w określonym katalogu, gdzie nazwa zaczyna się od
"s", a kończy na ".mp3", możemy to zrobić w ten sposób. - Teraz rozważ taki scenariusz: mamy katalog z muzyką z kilkoma podkatalogami, wewnątrz których są pliki .mp3. Możemy pobrać listę wszystkich tych plików za pomocą jednego wywołania
glob, wykorzystując połączenie dwóch wyrażeń. Pierwszym jest "*.mp3" (wyszukuje pliki .mp3), a drugim są same w sobie ścieżki do katalogów, aby przetworzyć każdy podkatalog w c:\music. Ta prosto wyglądająca funkcja daje nam niesamowite możliwości!
[edytuj] Materiały dodatkowe
- Python Knowledge Base odpowiada na najczęściej zadawane pytanie na temat modułu
os - Python Library Reference dokumentuje moduł
osi modułos.path
[edytuj] Wszystko razem
Jeszcze raz ułożymy wszystkie puzzle domina w jednym miejscu. Już poznaliśmy, w jaki sposób działa każda linia kodu. Powrócimy do tego jeszcze raz i zobaczymy, jak to wszystko jest ze sobą dopasowane.
listDirectory
def listDirectory(directory, fileExtList): #(1)
u"zwraca listę obiektów zawierających 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 klasę 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)
listDirectoryjest główną atrakcją tego modułu. Przyjmuje ona na wejściu katalog (np. c:\music\_singles\) i listę interesujących nas rozszerzeń plików (jak np.['.mp3']), a następnie zwraca listę instancji klas, które są podklasami słownika, a przechowują metadane na temat każdego interesującego nas pliku w tym katalogu. I to wszystko jest wykonywane za pomocą kilku prostych linii kodu.- Jak dowiedzieliśmy się w poprzednim podrozdziale, ta linia kodu zwraca listę pełnych ścieżek wszystkich plików z danego katalogu, które mają interesujące nas rozszerzenie (podane w argumencie
fileExtList). - Starzy programiści Pascala znają zagnieżdżone funkcje (funkcje wewnątrz funkcji), ale większość ludzi jest zdziwionych, gdy mówi się im, że Python je wspiera. Zagnieżdżona funkcja
getFileInfoClassmoże być wywołana tylko z funkcji, w której jest zadeklarowana, czyli zlistDirectory. Jak w przypadku każdej innej funkcji, nie musimy przejmować się deklaracją interfejsu, ani niczym innym. Po prostu definiujemy funkcję i implementujamy ją. - Teraz, gdy już znamy moduł
os, ta linia powinna nabrać sensu. Pobiera ona rozszerzenie pliku (os.path.splitext(filename)[1]), przekształca je do dużych liter (.upper()), odcina kropkę ([1:]) i tworzy nazwę klasy używając łańcucha formatującego. c:\music\ap\mahadeva.mp3 zostaje przekształcone na .mp3, potem na .MP3, a następnie na MP3 i na końcu otrzymujemy MP3FileInfo. - Mając nazwę klasy obsługującej ten plik, sprawdzamy czy tak klasa istnieje w tym module. Jeśli tak, zwracamy tę klasę, jeśli nie -- klasę bazową
FileInfo. To bardzo ważne: zwracamy klasę. Nie zwracamy obiektu, ale klasę samą w sobie. - Dla każdego pliku z listy
fileListwywołujemygetFileInfoClassz nazwą pliku(f). WywołaniegetFileInfoClass(f)zwraca klasę. Dokładnie nie wiadomo jaką, ale to nam nie przeszkadza. Potem tworzymy obiekt tej klasy (jaka by ona nie była) i przekazujemy nazwę pliku (znówf) do jej metody__init__. Jak pamiętamy z wcześniejszych rozdziałów, metoda__init__klasyFileInfoustawia wartośćself["name"], co powoduje wywołanie__setitem__klasy pochodnej, czyliMP3FileInfo, żeby odpowiednio przetworzyć plik i wyciągnąć jego metadane. Robimy to wszystko dla każdego interesującego pliku i zwracamy listę obiektów wynikowych.
Zauważmy, że metoda listDirectory jest bardzo ogólna. Nie wie w żaden sposób, z jakimi typami plików będzie pracować, ani jakie klasy są zdefiniowane do obsługi tych plików. Zaczyna pracę od przejrzenia katalogu, w poszukiwaniu plików do przetwarzania, a potem analizuje swój moduł, żeby sprawdzić, jakie klasy obsługi (np. MP3FileInfo) są zdefiniowane. Możemy rozszerzyć ten program, żeby obsługiwać inne typy plików definiując klasy o odpowiednich nazwach: HTMLFileInfo dla plików HTML, DOCFileInfo dla plików Worda itp. listDirectory, bez potrzeby modyfikacji kodu tej funkcji, obsłuży je wszystkie, zrzucając całe przetwarzanie na odpowiednie klasy i zbierając otrzymane wyniki.
[edytuj] Podsumowanie
Program fileinfo.py wprowadzony w rozdziale 5 powinien już być zrozumiały.
u"""Framework do pobierania matedanych specyficznych dla danego typu pliku.
Można utworzyć instancję odpowiedniej klasy podając jej nazwę pliku w konstruktorze.
Zwrócony obiekt zachowuje się jak słownik posiadający parę klucz-wartość
dla każdego 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 użyć funkcji listDirectory, aby pobrać informacje o wszystkich plikach w katalogu.
for info in fileinfo.listDirectory("/music/ap/", [".mp3"]):
...
Framework może być roszerzony poprzez dodanie klas dla poszczególnych typów plików, np.:
HTMLFileInfo, MPGFileInfo, DOCFileInfo. Każda klasa jest całkowicie odpowiedzialna
za właściwe sparsowanie swojego pliku; zobacz przykład MP3FileInfo.
"""
import os
import sys
def stripnulls(data):
u"usuwa białe znaki i nulle"
return data.replace("\00", " ").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:
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 listę obiektów zawierających 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 klasę 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 następnym rozdziale, upewnijmy się, że nie mamy problemów z:
- przechwytywaniem wyjątków za pomocą
try...except - chronieniem zewnętrznych zasobów za pomocą
try...finally - czytaniem plików
- korzystaniem z wielozmiennych przypisań w pętli
for - Wykorzystywaniem modułu
osdo niezależnego od platformy zarządzania plikami - Dynamicznym tworzeniem instancji klas nieznanych typów poprzez traktowanie klas jak obiektów
[edytuj] Wyrażenia regularne
Wyrażenia regularne są bardzo użytecznymi, a zarazem standardowymi środkami wyszukiwania, zamiany i przetwarzania tekstu wykorzystując skomplikowane wzorce. Jeśli wykorzystywaliśmy już wyrażenia regularne w innych językach (np. w Perlu), to pewnie zauważymy, że składnia jest bardzo podobna, ponadto możemy przeczytać podsumowanie modułu re, aby przeglądnąć dostępne funkcje i ich argumenty.
[edytuj] Nurkujemy
Łańcuchy znaków mają metody, które służą wyszukiwaniu (index, find i count), zmienianiu (replace) i przetwarzaniu (split), ale są one ograniczone do najprostszych przypadków. Metody te wyszukują pojedynczy, zapisany na stałe ciąg znaków i zawsze uwzględniają wielkość liter. Aby wyszukać coś niezależnie od wielkości liter w łańcuchu s, musimy użyć s.lower() lub s.upper() i upewnić się, że nasz tekst do wyszukania ma odpowiednią wielkość liter. Metody służące do zamiany i podziału mają takie same ograniczenia.
Jeśli to, co próbujemy zrobić jest możliwe przy użyciu metod łańcucha znaków, powinniśmy ich użyć. Są szybkie, proste i czytelne. Jeśli jednak okazuje się, że używamy wielu różnych metod i instrukcji if do obsługi przypadków szczególnych albo jeśli łączysz je z użyciem split, join i wyrażeń listowych w dziwny i nieczytelny sposób, możemy być zmuszeni do przejścia na wyrażenia regularne.
Pomimo, że składnia wyrażeń regularnych jest zwarta i niepodobna do normalnego kodu, wynik może być czytelniejszy niż użycie długiego ciągu metod łańcucha znaków. Są nawet sposoby dodawania komentarzy wewnątrz wyrażeń regularnych, aby były one praktycznie samodokumentujące się.
[edytuj] Analiza przypadku: Adresy ulic
Ta seria przykładów została zainspirowana problemami z prawdziwego życia. Kilka lat temu gdzieś nie w Polsce, zaszła potrzeba oczyszczenia i ustandaryzowania adresów ulic zaimportowanych ze starego systemu, zanim zostaną zaimportowane do nowego. (Zauważmy, że nie jest to jakiś wyimaginowany przykład. Może on się okazać przydatny.) Poniższy przykład przybliży nas do tego problemu.
>>> 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.'
- Naszym celem jest ustandaryzowanie adresów ulic, więc skrótem od
'ROAD'jest'RD.'. Na pierwszy rzut oka wydaje się, że po prostu można wykorzystać metodę łańcucha znaków, jaką jestreplace. Zakładamy, że wszystkie dane zapisane są za pomocą wielkich liter, więc nie powinno być problemów wynikających z niedopasowania, ze względu na wielkość liter. Wyszukujemy stały napis, jakim jest'ROAD'. Jest to bardzo płytki przykład, więcs.replacepoprawnie zadziała. - Życie niestety jest trochę bardziej skomplikowane, o czym dość szybko można się przekonać. Problem w tym przypadku polega na tym, że
'ROAD'występuje w adresie dwukrotnie: raz jako część nazwy ulicy ('BROAD') i drugi raz jako oddzielne słowo. Metodareplaceznajduje te dwa wystąpienia i ślepo je zamienia, niszcząc adres. - Aby rozwiązać problem z adresami, gdzie podciąg
'ROAD'występuje kilka razy, możemy wykorzystać taki pomysł: tylko szukamy i zamieniamy'ROAD'w ostatnich czterech znakach adresu (czylis[-4:]), a zostawiamy pozostałą część (czyli s[:-4]). Jednak, jak możemy zresztą zobaczyć, takie rozwiązanie jest dosyć niewygodne. Na przykład polecenie, które chcemy wykorzystać, zależy od długość zamienianego napisu (jeśli chcemy zamienić'STREET'na'ST.', wykorzystamys[:-6]is[-6:].replace(...)). Chciałoby się do tego wrócić za sześć miesięcy i to debugować? Pewnie nie. - Nadszedł odpowiedni czas, aby przejść do wyrażeń regularnych. W Pythonie cała funkcjonalność wyrażeń regularnych zawarta jest w module
re. - Spójrzmy na pierwszy parametr,
'ROAD$'. Jest to proste wyrażenie regularne, które dopasuje'ROAD'tylko wtedy, gdy wystąpi on na końcu tekstu. Znak $ znaczy "koniec napisu". (Mamy także analogiczny znak, znak daszka ^, który znaczy "początek napisu".) - Korzystając z funkcji
re.sub, przeszukujemy napissi podciąg pasujący do wyrażenia regularnego'ROAD$'zamieniamy na'RD.'. Dzięki temu wyrażeniu dopasowujemy'ROAD'na końcu napisus, lecz napis'ROAD'nie zostanie dopasowany w słowie'BROAD', ponieważ znajduje się on w środku napisus.
Wracając do historii z porządkowaniem adresów, okazało się, że poprzedni przykład, dopasowujący 'ROAD' na końcu adresu, nie był poprawny, ponieważ nie wszystkie adresy dołączały 'ROAD' na końcu adresu. Niektóre adresy kończyły się tylko nazwą ulicy. Wiele razy to wyrażenie zadziałało poprawnie, jednak gdy mieliśmy do czynienia z ulicą 'BROAD', wówczas wyrażenie regularne dopasowywało 'ROAD' na końcu napisu jako część słowa 'BROAD', a takiego wyniku nie oczekiwaliśmy.
>>> 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'\bROAD\b', 'RD.', s) #(4)
'100 BROAD RD. APT. 3'
- W istocie chcieliśmy odnaleźć
'ROAD'znajdujące się na końcu napisu i będące samodzielnym słowem, a nie częścią dłuższego wyrazu. By opisać coś takiego za pomocą wyrażeń regularnych korzystamy z\b, które znaczy tyle co "tutaj musi znajdować się początek lub koniec wyrazu". W Pythonie jest to nieco skomplikowane przez fakt, iż znaki specjalne (takie jak np.\) muszą być poprzedzone właśnie znakiem\. Zjawisko to określane jest czasem plagą ukośników (ang. backslash plague) i wydaje się być jednym z powodów łatwiejszego korzystania z wyrażeń regularnych w Perlu niż w Pythonie. Z drugiej jednak strony, w Perlu składnia wyrażeń regularnych wymieszana jest ze składnią języka, co utrudnia stwierdzenie czy błąd tkwi w naszym wyrażeniu regularnym, czy w błędnym użyciu składni języka. - Eleganckim obejściem problemu plagi ukośników jest wykorzystywanie tzw. surowych napisów (ang. raw string), które opisywaliśmy w rozdziale 3, poprzez umieszczanie przed napisami litery r. Python jest w ten sposób informowany o tym, iż żaden ze znaków specjalnych w tym napisie nie ma być interpretowany;
'\t'odpowiada znakowi tab, jednakr'\t'oznacza tyle, co litera t poprzedzona znakiem \. Przy wykorzystaniu wyrażeń regularnych zalecane jest stosowanie surowych napisów; w innym wypadku wyrażenia szybko stają się niezwykle skomplikowane (a przecież już ze swej natury nie są proste). - Cóż... Niestety wkrótce okazuje się, iż istnieje więcej przypadków przeczących logice naszego postępowania. W tym przypadku
'ROAD'było samodzielnym słowem, jednak znajdowało się w środku napisu, ponieważ na jego końcu umieszczony był jeszcze numer mieszkania. Z tego powodu nasze bieżące wyrażenie nie zostało odnalezione, funkcjare.subniczego nie zamieniła, a co za tym idzie napis został zwrócony w pierwotnej postaci (co nie było naszym celem). - Aby rozwiązać ten problem wystarczy zamienić w wyrażeniu regularnym $ na kolejne \b. Teraz będzie ono pasować do każdego samodzielnego słowa
'ROAD', niezależnie od jego pozycji w napisie.
[edytuj] Analiza przypadku: Liczby rzymskie
Najprawdopodobniej spotkaliśmy się już gdzieś z liczbami rzymskimi. Można je spotkać w starszych filmach oglądanych w telewizji (np. "Copyright MCMXLVI" zamiast "Copyright 1946") lub na ścianach bibliotek, czy uniwersytetów (napisy typu "założone w MDCCCLXXXVIII" zamiast "założone w 1888 roku"). Mogliśmy je także zobaczyć na przykład w referencjach bibliograficznych. Ten system reprezentowania liczb sięga czasów starożytnego Rzymu (stąd nazwa).
W liczbach rzymskich wykorzystuje się siedem znaków, które na różne sposoby się powtarza i łączy, aby zapisać pewną liczbę:
- I = 1
- V = 5
- X = 10
- L = 50
- C = 100
- D = 500
- M = 1000
Poniżej znajdują się podstawowe zasady konstruowania liczb rzymskich:
- Znaki są 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 dziesiątek (I, X, C i M) mogą się powtarzać do trzech razy. Za czwartym należy odjąć od następnego większego znaku piątek. Nie można zapisać liczby 4 jako IIII. Zamiast tego napiszemy IV ("o 1 mniej niż 5"). Liczba 40 zapisujemy jako XL (o 10 mniej niż 50), 41 jako XLI, 42 jako XLII, 43 jako XLIII, a potem 44 jako XLIV (o 10 mniej niż 50, a potem o 1 mniej niż 5).
- Podobnie w przypadku 9. Musimy odejmować od wyższego znaku dziesiątek: 8 to VIII, lecz 9 zapiszemy jako IX (o 1 mniej niż 10), a nie jako VIIII (ponieważ znak nie może się powtarzać cztery razy). Liczba 90 to XC, a 900 zapiszemy jako CM.
- Znaki piątek nie mogą się powtarzać. Liczba 10 jest zawsze reprezentowana przez X, nigdy przez VV. Liczba 100 to zawsze C, nigdy LL.
- Liczby rzymskie są zawsze pisane od najwyższych do najniższych i czytane od lewej do prawej, więc porządek znaków jest bardzo ważny. DC to 600, jednak CD jest kompletnie inną liczbą (400, ponieważ o 100 mniej niż 500). CI to 101, jednak IC nie jest żadną poprawną liczbą rzymską (nie możemy bezpośrednio odejmować 1 od 100, musimy to zapisać jako XCIX, o 10 mniej niż 100, dodać 1 mniej niż 10).
[edytuj] Sprawdzamy tysiące
Jak sprawdzić, czy jakiś łańcuch znaków jest liczbą rzymską? Spróbujmy sprawdzać cyfra po cyfrze. Jako, że liczby rzymskie są zapisywane zawsze od najwyższych do najniższych, zacznijmy od tych najwyższych: tysięcy. Dla liczby 1000 i większych, tysiące zapisywane są przez serię liter M.
>>> 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>
- Ten wzorzec składa się z 3 części:
- ^, które umieszczone jest w celu dopasowania jedynie początku łańcucha. Gdybyśmy go nie umieścili, wzorzec pasowałby do każdego wystąpienia znaków M, czego przecież nie chcemy. Chcemy, aby dopasowane zostały jedynie znaki M znajdujące się na początku łańcucha, o ile w ogóle istnieją.
- M?, które ma wykryć, czy istnieje pojedyncza litera M. Jako, że powtarzamy to trzykrotnie, dopasujemy od zera do trzech liter M w szeregu.
- $, w celu dopasowania wzorca do końca łańcucha. Gdy połączymy to ze znakiem ^ na początku, otrzymamy wzorzec, który musi pasować do całego łańcucha, bez żadnych znaków przed czy po serii znaków M.
- Sednem modułu
rejest funkcjasearch, która jako argumenty przyjmuje wyrażenie regularne (wzorzec) i łańcuch znaków ('M'), a następnie próbuje go dopasować do wzorca. Gdy zostanie dopasowany,searchzwraca obiekt który posiada wiele metod, które opisują dopasowanie. Jeśli nie uda się dopasować,searchzwracaNone, co jest Pythonową pustą wartością i nic nie oznacza. Na razie jedyne, co nas interesuje, to czy wzorzec został dopasowany, czy nie, a co możemy stwierdzić przez sprawdzenie, co zwróciła funkcjasearch.'M'pasuje do wzorca, gdyż pierwsze opcjonalneMzostało dopasowane, a drugie i trzecie zostało zignorowane. 'MM'pasuje, gdyż pierwsze i drugie opcjonalne M zostało dopasowane, a trzecie zignorowane.'MMM'również pasuje do wzorca, gdyż wszystkie trzy opcjonalne wystąpienia M we wzorcu zostały dopasowane.'MMMM'nie pasuje, gdyż pomimo dopasowania pierwszych trzech opcjonalnych znaków M, za trzecim wzorzec wymaga, aby łańcuch się skończył, a w naszym łańcuchu znaków znajduje się kolejna litera M. Tak więcsearchzwraca wartośćNone.- Co ciekawe, pusty łańcuch też pasuje do wzorca, gdyż wszystkie wystąpienia M są opcjonalne.
[edytuj] Sprawdzamy setki
Setki są nieco trudniejsze, ponieważ schemat zapisu nie jest aż tak prosty jak w wypadku tysięcy. Mamy więc następujące możliwości:
- 100 = C
- 200 = CC
- 300 = CCC
- 400 = CD
- 500 = D
- 600 = DC
- 700 = DCC
- 800 = DCCC
- 900 = CM
Wynika z tego, że mamy 4 wzorce:
- CM
- CD
- Zero do trzech wystąpień C (zero, gdyż może nie być żadnej setki)
- D, po którym następuje zero do trzech C
Ostatnie dwa wzorce możemy połączyć w opcjonalne D, a za nim od zera do trzech C.
Poniższy przykład ilustruje jak sprawdzać setki w liczbach Rzymskich.
>>> 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>
- Ten wzorzec zaczyna się tak samo jak poprzedni, rozpoczynając sprawdzanie od początku łańcucha (^), potem sprawdzając tysiące (M?M?M?). Tutaj zaczyna się nowa część, która definiuje 3 alternatywne wzorce rozdzielone pionową kreską (|): CM, CD, i D?C?C?C? (opcjonalne D, po którym następuje od zera do trzech opcjonalnych znaków C). Analizator wyrażeń regularnych sprawdza każdy ze wzorców w kolejności od lewej do prawej, wybiera pierwszy pasujący i ignoruje resztę.
'MCM'pasuje, gdyż pierwsza litera M pasuje, drugie i trzecie M jest ignorowane, i CM pasuje (gdyż CD oraz D?C?C?C? nie są nawet sprawdzane). MCM to rzymska liczba 1900.'MD'pasuje, ponieważ pierwsze M pasuje, drugie i trzecie M z wzorca jest ignorowane, oraz D pasuje do wzorca D?C?C?C? (wystąpienia znaku C jest opcjonalne, więc analizator je ignoruje). MD to rzymska liczba 1500.'MMMCCC'pasuje, gdyż pasują wszystkie trzy pierwsze znaki M, a fragment D?C?C?C? we wzorcu pasuje do CCC (D jest opcjonalne). MMMCCC to 3300.'MCMC'nie pasuje, Pierwsze M pasuje, CM również, ale $ już nie, gdyż nasz łańcuch zamiast się skończyć, ma kolejną literę C. Nie została ona dopasowana do wzorca D?C?C?C?, gdyż został on wykluczony przez wystąpienie wzorca CM.- Co ciekawe, pusty łańcuch znaków dalej pasuje do naszego wzorca, gdyż wszystkie znaki M są opcjonalne, tak jak każdy ze znaków we wzorcu D?C?C?C?.
Uff! Widzimy, jak szybko wyrażenia regularne stają się brzydkie? A jak na razie wprowadziliśmy do niego tylko tysiące i setki. Ale jeśli dokładnie śledziliśmy cały ten rozdział, dziesiątki i jednostki nie powinny stanowić dla Ciebie problemu, ponieważ wzór jest identyczny. A teraz zajmiemy się inną metodą wyrażania wzorca.
[edytuj] Składnia {n, m}
W poprzednim podrozdziale poznaliśmy wzorce, w których ten sam znak mógł się powtarzać co najwyżej trzy razy. Tutaj przedstawimy inny sposób zapisania takiego wyrażenia, a który wydaje się być bardziej czytelny. Najpierw spójrzmy na metody wykorzystane w poprzednim przykładzie.
>>> 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)
- Instrukcja ta dopasowuje początek napisu, a następnie pierwszą literę M, lecz nie dopasowuje drugiego i trzeciego M (wszystko jest w porządku, ponieważ są one opcjonalne), a następnie koniec napisu.
- Tutaj zostaje dopasowany początek napisu, a następnie pierwsze i drugie opcjonalne M, jednak nie zostaje dopasowane trzecie M (ale wszystko jest w ok, ponieważ jest to opcjonalne), ale zostaje dopasowany koniec napisu.
- Zostanie dopasowany początek napisu, a następnie wszystkie opcjonalne M, a potem koniec napisu.
- Dopasowany zostanie początek napisu, następnie wszystkie opcjonalne M, jednak koniec tekstu nie zostanie dopasowany, ponieważ pozostanie jedno niedopasowane M, dlatego też nic nie zostanie dopasowane, a operacja zwróci
None.
>>> 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)
- Ten wzorzec mówi: "dopasuj początek napisu, potem od zera do trzech znaków M, a następnie koniec napisu". 0 i 3 może być dowolną liczbą. Jeśli chcielibyśmy dopasować co najmniej jeden, lecz nie więcej niż trzy znaki M, powinienniśmy napisać M{1,3}.
- Dopasowujemy początek napisu, potem jeden znak M z trzech możliwych, a następnie koniec napisu.
- Tutaj zostaje dopasowany początek napisu, następnie dwa M z trzech możliwych, a następnie koniec napisu.
- Zostanie dopasowany początek napisu, potem trzy znaki M z trzech możliwych, a następnie koniec napisu.
- W tym miejscu dopasowujemy początek napisu, potem trzy znaki M z pośród trzech możliwych, lecz nie dopasujemy końca napisu. To wyrażenie regularne pozwala wykorzystać tylko trzy litery M, zanim dojdzie do końca napisu, a my mamy cztery, więc ten wzorzec niczego nie dopasuje i zwróci
None.
[edytuj] Sprawdzanie dziesiątek i jedności
Teraz rozszerzmy wyrażenie wykrywające liczby rzymskie, żeby odnajdywało też dziesiątki i jedności. Ten przykład pokazuje sprawdzanie dziesiątek.
>>> 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)
- To dopasuje początek łańcucha, potem pierwsze opcjonalne M, dalej CM i XL, a potem koniec łańcucha. Zapamiętajmy, że składnia (A|B|C) oznacza "dopasuj dokładnie jedno z A, B lub C". W tym wypadku dopasowaliśmy XL, więc ignorujemy XC i L?X?X?X? i przechodzimy do końca łańcucha. MCMXL to 1940.
- Tutaj dopasowujemy początek łańcucha, pierwsze opcjonalne M, potem CM i L?X?X?X?. Z tego ostatniego elementu dopasowane zostaje tylko L, a opcjonalne X zostają pominięte. Potem przechodzimy na koniec łańcucha. MCML to 1950.
- Dopasowuje początek napisu, potem pierwsze opcjonalne M, następnie CM, potem opcjonalne L i pierwsze opcjonalne X, pomijając drugie i trzecie opcjonalne X, a następnie dopasowuje koniec napisu. MCMLX jest rzymską reprezentacją liczby 1960.
- Tutaj zostanie dopasowany początek napisu, następnie pierwsze opcjonalne M, potem CM, następnie opcjonalne L, wszystkie trzy opcjonalne znaki X i w końcu dopasowany zostanie koniec napisu. MCMLXXX jest rzymską reprezentacją liczby 1980.
- To dopasuje początek napisu, następnie pierwsze opcjonalne M, potem CM, opcjonalne L, wszystkie trzy opcjonalne znaki X, jednak nie może dopasować końca napisu, ponieważ pozostał jeszcze jeden niewliczony znak X. Zatem cały wzorzec nie zostanie dopasowany, więc zostanie zwrócone
None. MCMLXXXX nie jest poprawną liczbą rzymską.
Poniżej przedstawiono podobne wyrażenie, które dodatkowo sprawdza jedności. Oszczędzimy 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 będzie wyglądało to wyrażenie wykorzystując składnię {n,m}? Zobaczmy na poniższy przykład.
>>> 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>
- Dopasowany zostanie początek napisu, potem jeden z możliwych czterech znaków M i następnie D?C{0,3}. Z kolei D?C{0,3} dopasuje opcjonalny znak D i zero z trzech możliwych znaków C. Idąc dalej, dopasowany zostanie L?X{0,3} poprzez dopasowanie opcjonalnego znaku L i zero z możliwych trzech znaków X. Następnie dopasowujemy V?I{0,3} dodając opcjonalne V i zero z trzech możliwych znaków I, a ostatecznie dopasowujemy koniec napisu. MDLV jest rzymską reprezentacją liczby 1555.
- To dopasuje początek napisu, następnie dwa z czterech możliwych znaków M, a potem D?C{0,3} z D i jednym z trzech możliwych znaków C. Dalej dopasujemy L?X{0,3} z L i jednym z trzech możliwych znaków X, a następnie V?I{0,3} z V i jednym z trzech możliwych znaków I, a w końcu koniec napisu. MMDCLXVI jest reprezentacją liczby 2666.
- Tutaj dopasowany zostanie początek napisu, a potem trzy z trzech znaków M, a następnie D?C{0,3} ze znakiem D i trzema z trzech możliwych znaków C. Potem dopasujemy L?X{0,3} z L i trzema z trzech znaków X, następnie V?I{0,3} z V i trzema z trzech możliwych znaków I, a ostatecznie koniec napisu. MMMDCCCLXXXVIII jest reprezentacją liczby 3888 i ponadto jest najdłuższą liczbą Rzymską, którą można zapisać bez rozszerzonej składni.
- Obserwuj dokładnie. Dopasujemy początek napisu, potem zero z czterech M, następnie dopasowujemy D?C{0,3} pomijając opcjonalne D i dopasowując zero z trzech znaków C. Następnie dopasowujemy L?X{0,3} pomijając opcjonalne L i dopasowując zero z trzech znaków X, a potem dopasowujemy V?I{0,3} pomijając opcjonalne V, ale dopasowując jeden z trzech możliwych I. Ostatecznie dopasowujemy koniec napisu.
Jeśli prześledziłeś to wszystko i zrozumiałeś to za pierwszym razem, jesteś bardzo bystry. Teraz wyobraźmy sobie sytuację, że próbujemy zrozumieć jakieś inne wyrażenie regularne, które znajduje się w centralnej, krytycznej funkcji wielkiego programu. Albo wyobraź sobie nawet, że wracasz do swojego wyrażenia regularnego po kilku miesiącach. Sytuacje takie mogą nie wyglądać ciekawie...
W następnym podrozdziale dowiemy się o alternatywnej składni, która pomaga zrozumieć i zarządzać wyrażeniami.
[edytuj] Rozwlekłe wyrażenia regularne
Jak na razie, mieliśmy do czynienia z czymś, co nazywam "zwięzłymi" wyrażeniami regularnymi. Jak pewnie zauważyliśmy, są one trudne do odczytania i nawet, jeśli już je rozszyfrujemy, nie ma gwarancji, że zrobimy to za np. sześć miesięcy. To, czego potrzebujemy, to dokumentacja w ich treści.
Python pozwala na to przez tworzenie rozwlekłych wyrażeń regularnych (ang. verbose regular expressions). Różnią się one od zwięzłych dwoma rzeczami:
- Białe znaki są ignorowane. Spacje, znaki tabulacji, znaki nowej linii nie są dopasowywane jako spacje, znaki tabulacji lub znaki nowej linii. Znaki te nie są w ogóle dopasowywane. (Jeśli byśmy chcieli jednak dopasować któryś z nich, musisz poprzedzić je odwrotnym ukośnikiem (\).)
- Komentarze są ignorowane. Komentarz w rozwlekłym wyrażeniu regularnym wygląda dokładnie tak samo, jak w kodzie Pythona: zaczyna się od # i leci aż do końca linii. W tym przypadku jest to komentarz w wieloliniowym łańcuchu znaków, a nie w kodzie źródłowym, ale zasada działania jest taka sama.
Łatwiej będzie to zrozumieć jeśli skorzystamy z przykładu. Skorzystajmy ze zwięzłego wyrażenia regularnego, które utworzyliśmy wcześniej i zróbmy z niego rozwlekłe. Ten przykład pokazuje jak.
>>> pattern = """
^ # początek łańcucha znaków
M{0,3} # tysiące - 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}) # dziesiątki - 90 (XC), 40 (XL), 0-30 (0 do 3 X),
# lub 50-80 (L, a po nim 0 do 3 X)
(IX|IV|V?I{0,3}) # jedności - 9 (IX), 4 (IV), 0-3 (0 do 3 I),
# lub 5-8 (V, a po nim 0 do 3 I)
$ # koniec łańcucha 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)
- Najważniejszą rzeczą o której należy pamiętać, gdy korzystamy z rozwlekłych wyrażeń regularnych jest to, że musimy przekazać dodatkowy argument:
re.VERBOSE. Jest to stała zdefiniowana w modulere, która sygnalizuje, że wyrażenie powinno być traktowane jako rozwlekłe. Jak widzimy, ten wzorzec ma mnóstwo białych znaków (które są ignorowane) i kilka komentarzy (które też są ignorowane). Gdy usuniemy białe znaki i komentarze, to pozostanie dokładnie to samo wyrażenie regularne, jakie otrzymaliśmy w poprzednim przykładzie, ale o wiele mniej czytelne. (Zauważmy, że co prawda łańcuch znaków posiada polskie znaki, ale nie tworzymy go w unikodzie, ponieważ i tak te znaki nie mają dla nas żadnego znaczenia, ponieważ są w komentarzach.) - To dopasowuje początek łańcucha, potem jedno z trzech możliwych M, potem CM, L i trzy z trzech możliwych X, a następnie IX i koniec łańcucha.
- To dopasowuje początek łańcucha, potem trzy z trzech możliwych M, dalej D, trzy z trzech możliwych C, L z trzema możliwymi X, potem V z trzema możliwymi I i na koniec koniec łańcucha.
- Tutaj nie udało się dopasować niczego. Czemu? Ponieważ nie przekazaliśmy flagi
re.VERBOSE, więc funkcjare.searchtraktuje to wyrażenie regularne jako zwięzłe, z dużą ilością białych znaków i kratek. Python nie rozpoznaje samodzielnie, czy każemy mu dopasować zwięzłe, czy może rozwlekłe wyrażenie regularne i przyjmuje, że każde jest zwięzłe, chyba że wyraźnie wskażemy, że tak nie jest.
[edytuj] Analiza przypadku: Przetwarzanie numerów telefonów
Do tej pory koncentrowaliśmy się na dopasowywaniu całych wzorców. Albo pasowały, albo nie. Ale wyrażenia regularne są dużo potężniejsze. Gdy zostaną dopasowane, można wyciągnąć z nich wybrane kawałki i dzięki temu sprawdzić, co i gdzie zostało dopasowane.
Oto kolejny przykład z życia wzięty, z jakim można się spotkać: przetwarzanie amerykańskich numerów telefonów. Klient chciał móc wprowadzać numer w dowolnej formie w jednym polu, ale potem chciał, żeby przechowywać oddzielnie numer kierunkowy, numer w dwóch częściach i opcjonalny numer wewnętrzny w bazie danych firmy. W Internecie można znaleźć wiele takich wyrażeń regularnych, ale żadne z nich nie jest aż tak bardzo restrykcyjne.
Oto przykłady numerów telefonów, jakie program miał przetwarzać:
- 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 duże zróżnicowanie! W każdym z tych przypadków musimy wiedzieć, że numerem kierunkowym jest 800, pierwszą częścią numeru jest 555, drugą 1212, a numerem wewnętrznym (jeśli istnieje) - 1234.
Spróbujmy rozwiązać ten problem. Poniższy przykład pokazuje pierwszy krok.
>>> 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)
>>>
- Zawsze odczytujemy wyrażenie regularne od lewej do prawej. Tutaj dopasowujemy początek łańcucha znaków, potem (\d{3}). Co to takiego te (\d{3})? {3} oznacza "dopasuj dokładnie 3 wystąpienia" (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 zapamiętaj je jako grupę, o którą można zapytać później". Następnie mamy dopasować myślnik. Dalej dopasuj kolejną grupę dokładnie trzech cyfr, a następnie kolejny myślnik, i ostatnią grupę tym razem czterech cyfr. Na koniec dopasuje koniec łańcucha znaków.
- Aby otrzymać grupy, które zapamięta moduł przetwarzania wyrażeń regularnych, należy skorzystać z metody
groups()obiektu zwracanego przez funkcjęsearch. Zwróci ona krotkę z ilością elementów równą ilości grup zdefiniowanych w wyrażeniu regularnym. W tym przypadku mamy trzy grupy: dwie po 3 cyfry i ostatnią czterocyfrową. - To jednak nie jest rozwiązaniem naszego problemu, bo nie dopasowuje numeru telefonu z numerem wewnętrznym. Musimy więc je rozszerzyć.
>>> 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)
>>>
- To wyrażenie regularne jest praktycznie identyczne z wcześniejszym. Tak jak wcześniej, dopasowujemy początek łańcucha, potem zapamiętywaną grupę trzech cyfr, myślnik, zapamiętywaną grupę trzech cyfr, myślnik i zapamiętywaną grupę czterech cyfr. Nową częścią jest kolejny myślnik i zapamiętywana grupa jednej lub więcej cyfr. Na końcu jak w poprzednim przykładzie dopasowujemy koniec łańcucha.
- Metoda
groups()zwraca teraz krotkę czterech elementów, ponieważ wyrażenie regularne definiuje teraz cztery grupy do zapamiętania. - Niestety nie jest to wersja ostateczna, gdyż zakładamy, że każda część numeru telefonu jest rozdzielona myślnikiem. Co jeśli będą rozdzielone spacją, kropką albo przecinkiem? Potrzebujemy bardziej ogólnego rozwiązania.
- Ups! Nie tylko to wyrażenie nie robi wszystkiego co powinno, ale cofnęliśmy się wstecz, gdyż teraz nie dopasowuje ono numerów bez numeru wewnętrznego. To nie jest to co chcieliśmy; jeśli w numerze jest podany numer wewnętrzny, to chcemy go znać, ale jeśli go nie ma, to i tak chcemy znać inne części numeru telefonu.
Następny przykład pokazuje wyrażenie regularne, które obsługuje różne separatory między częściami numeru telefonu.
>>> 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')
>>> phonePattern.search('80055512121234') #(4)
>>>
>>> phonePattern.search('800-555-1212') #(5)
>>>
- Teraz dopasowujemy początek łańcucha, grupę trzech cyfr, potem \D+... zaraz, zaraz, co to jest? \D dopasowuje dowolny znak, który nie jest cyfrą, a + oznacza "jeden lub więcej". Więc \D+ dopasowuje jeden lub więcej znaków nie będących cyfrą. Korzystamy z niego, aby dopasować różne separatory, nie tylko myślniki.
- Korzystanie z \D+ zamiast z - pozwala na dopasowywanie numerów telefonów ze spacjami w roli separatora części.
- Oczywiście myślniki też działają.
- Niestety, to dalej nie jest ostateczna wersja, ponieważ nie obsługuje ona braku jakichkolwiek separatorów.
- No i dalej nie rozwiązany pozostał problem możliwości braku numeru wewnętrznego. Mamy więc dwa problemy do rozwiązania, ale w obu przypadkach rozwiążemy problem tą samą techniką.
Następny przykład pokazuje wyrażenie regularne pasujące także do numeru 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)
>>>
- Jedyna zmiana jakiej dokonaliśmy od ostatniego kroku to zamiana wszystkich + na *. Zamiast \D+ pomiędzy częściami numeru telefonu dopasowujemy teraz \D*. Pamiętasz, że + oznacza "1 lub więcej"? * oznacza "0 lub więcej". Tak więc teraz jesteśmy w stanie przetworzyć numer nawet bez separatorów.
- Nareszcie działa! Dlaczego? Dopasowany został początek łańcucha, grupa 3 cyfr (800), potem zero znaków nienumerycznych, potem znowu zapamiętywana grupa 3 cyfr (555), znowu zero znaków nienumerycznych, zapamiętywana grupa 4 cyfr (1212), zero znaków nienumerycznych, numer wewnętrzny (1234) i nareszcie koniec łańcucha.
- Inne odmiany też działają np. numer rozdzielony kropkami ze spacją i x-em przed numerem wewnętrznym.
- Wreszcie udało się też rozwiązać problem z brakiem numeru wewnętrznego. Tak czy siak groups() zwraca nam krotkę z 4 elementami, ale ostatni jest tutaj pusty.
- Niestety jeszcze nie skończyliśmy. Co tutaj jest nie tak? Przed numerem kierunkowym znajduje się dodatkowy znak "(", a nasze wyrażenie zakłada, że numer kierunkowy znajduje się na samym przodzie. Nie ma problemu, możemy zastosować tę samą metodę, co do znaków rozdzielających.
Następny przykład pokazuje jak sobie radzić ze znakami wiodącymi w numerach telefonów.
>>> 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)
>>>
- Wzorzec w tym przykładzie jest taki sam jak w poprzednim, z wyjątkiem tego, że teraz na początku łańcucha dopasowujemy \D* przed pierwszą zapamiętywaną grupą (numerem kierunkowym). Zauważ że tych znaków nie zapamiętujemy (nie są one w nawiasie). Jeśli je napotkamy, to ignorujemy je i przechodzimy do numeru kierunkowego.
- Teraz udało się przetworzyć numer telefonu z nawiasem otwierającym na początku. (Zamykający był już wcześniej obsługiwany; był traktowany jako nienumeryczny znak pasujący do teraz drugiego \D*.)
- Tak na wszelki wypadek sprawdzamy czy nie popsuliśmy czegoś. Jako, że początkowy znak jest całkowicie opcjonalny, następuje dopasowanie w dokładnie taki sam sposób jak w poprzednim przykładzie.
- W tym miejscu wyrażenia regularne sprawiają, że chce się człowiekowi rozbić bardzo dużym młotem monitor. Dlaczego to nie pasuje? Wszystko za sprawą 1 przed numerem kierunkowym (numer kierunkowy USA), a przecież przyjęliśmy, że na początku mogą być tylko nienumeryczne znaki. Ech...
Cofnijmy się na chwilę. Jak na razie wszystkie wyrażenia dopasowywaliśmy od początku łańcucha. Ale teraz widać wyraźnie, że na początku naszego łańcucha mamy nieokreśloną liczbę znaków których kompletnie nie potrzebujemy. Po co mamy więc dopasowywać początek łańcucha? Jeśli tego nie zrobimy, to przecież pominie on tyle znaków ile mu się uda, a przecież o to nam chodzi. Takie podejście prezentuje następny przykład.
>>> 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')
- Zauważ, że brakuje ^ w tym wyrażeniu regularnym, Teraz już nie dopasowujemy początku łańcucha, bo przecież nikt nie powiedział, że wyrażenie musi pasować do całego łańcucha, a nie do fragmentu. Mechanizm wyrażeń regularnych sam zadba o namierzenie miejsca do którego ono pasuje (o ile w ogóle).
- Teraz nareszcie pasuje numer ze znakami na początku (w tym cyframi) i dowolnymi, jakimikolwiek separatorami w środku.
- Na wszelki wypadek sprawdzamy i to. Działa!
- To też działa.
Widzimy, jak szybko wyrażenia regularne wymykają się spod kontroli? Rzućmy okiem na jedną z poprzednich przykładów. Widzimy różnice pomiędzy nim i następnym?
Póki jeszcze rozumiemy to, co napisaliśmy, przekształćmy to w rozwlekłe wyrażenie regularne, żeby nie zapomnieć, co odpowiada za co i dlaczego.
>>> phonePattern = re.compile(r'''
# nie dopasowuj początku łańcucha, numer może się zacząć gdziekolwiek
(\d{3}) # numer kierunkowy - 3 cyfry (np. '800')
\D* # opcjonalny nienumeryczny separator
(\d{3}) # pierwsza część numeru - 3 cyfry (np. '555')
\D* # opcjonalny separator
(\d{4}) # druga część numeru (np. '1212')
\D* # opcjonalny separator
(\d*) # numer wewnętrzny jest opcjonalny i może mieć dowolną długość
$ # koniec łańcucha
''', 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', '')
- Pomijając fakt, że jest ono podzielone na wiele linii, to wyrażenie jest dokładnie takie samo jak po ostatnim kroku, więc nie jest niespodzianką, że dalej działa jak powinno.
- Jeszcze jedna próba. Tak, działa! Skończone!
[edytuj] Podsumowanie
To, co przedstawiliśmy tutaj, to zaledwie wierzchołek góry lodowej, odnośnie tego, co potrafią wyrażenia regularne. Innymi słowy, mimo że jesteśmy teraz nimi przytłoczeni, uwierzmy, że jeszcze nic nie widzieliśmy.
Powinieneś już być zaznajomiony z poniższymi technikami:
- ^ dopasowuje początek napisu.
- $ dopasowuje koniec napisu.
- \b dopasowuje początek lub koniec słowa.
- \d dopasowuje dowolną cyfrę.
- \D dopasowuje dowolny znak, który nie jest cyfrą.
- x? dopasowuje opcjonalny znak x (innymi słowy, dopasowuje x zero lub jeden raz).
- x* dopasowuje x zero lub więcej razy.
- x+ dopasowuje x jeden lub więcej razy.
- x{n,m} dopasowuje znak x co najmniej n razy, lecz nie więcej niż m razy.
- (a|b|c) dopasowuje a albo b albo c.
- (x) generalnie jest to zapamiętana grupa. Można otrzymać wartość, która została dopasowana, wykorzystując metodę groups() obiektu zwróconego przez re.search.
Wyrażenia regularne dają potężne możliwości, lecz nie zawsze są poprawnym rozwiązaniem do każdego problemu. Powinno się więcej o nich poczytać, aby dowiedzieć się, kiedy będą one odpowiednie podczas rozwiązywania pewnych problemów, a kiedy mogą raczej powodować problemy, niż je rozwiązywać.
"Niektórzy ludzie, kiedy napotkają problem, myślą: 'Wiem, użyję wyrażeń regularnych'. I teraz mają dwa problemy."
-- Jamie Zawinski[edytuj] Przetwarzanie HTML-a
[edytuj] Nurkujemy
Na comp.lang.python często można zobaczyć pytania w stylu "jak można znaleźć wszystkie nagłówki/obrazki/linki w moim dokumencie HTML?", "jak mogę sparsować/przetłumaczyć/przerobić tekst mojego dokumentu HTML tak, aby zostawić znaczniki w spokoju?" lub też "jak mogę natychmiastowo dodać/usunąć/zacytować atrybuty z wszystkich znaczników mojego dokumentu HTML?". Rozdział ten odpowiada na wszystkie te pytania.
Poniżej przedstawiono w dwóch częściach całkowicie działający program. Pierwsza część, BaseHTMLProcessor.py jest ogólnym narzędziem, które przetwarza pliki HTML przechodząc przez wszystkie znaczniki i bloki tekstowe. Druga część, dialect.py, jest przykładem tego, jak wykorzystać BaseHTMLProcessor.py, aby przetłumaczyć tekst dokumentu HTML, lecz przy tym zostawiając znaczniki w spokoju. Przeczytaj notki dokumentacyjne i komentarze w celu zorientowania się, co się tutaj właściwie dzieje. Duża część tego kodu wygląda jak czarna magia, ponieważ nie jest oczywiste w jaki sposób dowolna z metod klasy jest wywoływana. Jednak nie martw się, wszystko zostanie wyjaśnione w odpowiednim czasie.
#-*- 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 każdego początkowego znacznika
# attrs jest listą krotek (atrybut, wartość)
# np. dla <pre class="screen"> będziemy mieli tag="pre", attrs=[("class", "screen")]
# Chcielibyśmy zrekonstruować oryginalne znaczniki i atrybuty, ale
# może się zdarzyć, że umieścimy w cudzysłowach wartości, które nie były zacytowane
# w źródle dokumentu, a także możemy zmienić rodzaj cudzysłowów w wartości danego
# atrybutu (pojedyncze cudzysłowy lub podwójne).
# Dodajmy, że niepoprawnie osadzony kod nie-HTML-owy (np. kod JavaScript)
# może zostać źle sparsowany przez klasę bazową, a to spowoduje błąd wykonania skryptu.
# Cały nie-HTML musi być umieszczony w komentarzu HTML-a (<!-- kod -->),
# aby parser zostawił ten niezmieniony (korzystając z handle_comment).
strattrs = "".join([' %s="%s"' % (key, value) for key, value in attrs])
self.pieces.append("<%(tag)s%(strattrs)s>" % locals())
def unknown_endtag(self, tag):
# wywoływane dla każdego znacznika końcowego np. dla </pre>, tag będzie równy "pre"
# Rekonstruuje oryginalny znacznik końcowy w wyjściowym dokumencie
self.pieces.append("</%(tag)s>" % locals())
def handle_charref(self, ref):
# wywoływane jest dla każdego odwołania znakowego np. dla " ", ref będzie równe "160"
# Rekonstruuje oryginalne odwołanie znakowe.
self.pieces.append("&#%(ref)s;" % locals())
def handle_entityref(self, ref):
# wywoływane jest dla każdego odwołania do encji np. dla "©", ref będzie równe "copy"
# Rekonstruuje oryginalne odwołanie do encji.
self.pieces.append("&%(ref)s" % locals())
# standardowe encje HTML-a są zakończone średnikiem; pozostałe encje (encje spoza HTML-a)
# nie są
if htmlentitydefs.entitydefs.has_key(ref):
self.pieces.append(";")
def handle_data(self, text):
# wywoływane dla każdego bloku czystego teksu np. dla danych spoza dowolnego
# znacznika, w których nie występują żadne 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 każdego komentarza np. <!-- wpis kod JavaScript w tym miejscu -->
# Rekonstruuje oryginalny komentarz.
# Jest to szczególnie ważne, gdy dokument zawiera kod przeznaczony
# dla przeglądarki (np. kod Javascript) wewnątrz komentarza, dzięki temu
# parser może przejść przez ten kod bez zakłóceń;
# więcej szczegółów w komentarzu metody unknown_starttag.
self.pieces.append("<!--%(text)s-->" % locals())
def handle_pi(self, text):
# wywoływane dla każdej instrukcji przetwarzania np. <?instruction>
# Rekonstruuje oryginalną instrukcję przetwarzania
self.pieces.append("<?%(text)s>" % locals())
def handle_decl(self, text):
# wywoływane dla deklaracji typu dokumentu, jeśli występuje, np.
# <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
# "http://www.w3.org/TR/html4/loose.dtd">
# Rekonstruuje oryginalną deklarację typu dokumentu
self.pieces.append("<!%(text)s>" % locals())
def output(self):
u"""Zwraca przetworzony HTML jako pojedynczy łańcuch znaków"""
return "".join(self.pieces)
if __name__ == "__main__":
for k, v in globals().items():
print k, "=", v
#-*- 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 każdego znacznika <pre> w źródle HTML
# Zwiększa licznik trybu dosłowności verbatim, a następnie
# obsługuje ten znacznik normalnie
self.verbatim += 1
self.unknown_starttag("pre", attrs)
def end_pre(self):
# wywoływane dla każdego znacznika </pre>
# Zmiejsza licznik trybu dosłowności verbatim
self.unknown_endtag("pre")
self.verbatim -= 1
def handle_data(self, text):
# metoda nadpisana
# wywoływane dla każdego bloku tekstu w źródle
# Jeśli jest w trybie dosłownym, zapisuje tekst niezmieniony;
# inaczej przetwarza tekst za pomocą szeregu podstawień
self.pieces.append(self.verbatim and text or self.process(text))
def process(self, text):
# wywoływane z handle_data
# Przetwarza każdy blok wykonując serie podstawień
# za pomocą wyrażeń regularnych (podstawienia są definiowane przez klasy pochodne)
for fromPattern, toPattern in self.subs:
text = re.sub(fromPattern, toPattern, text)
return text
class ChefDialectizer(Dialectizer):
u"""konwertuje HTML na mowę szwedzkiego szefa kuchni
oparte na klasycznym chef.x, copyright (c) 1992, 1993 John Hagerman
"""
subs = ((r'a([nu])', r'u\1'),
(r'A([nu])', r'U\1'),
(r'a\B', r'e'),
(r'A\B', r'E'),
(r'en\b', r'ee'),
(r'\Bew', r'oo'),
(r'\Be\b', r'e-a'),
(r'\be', r'i'),
(r'\bE', r'I'),
(r'\Bf', r'ff'),
(r'\Bir', r'ur'),
(r'(\w*?)i(\w*?)$', r'\1ee\2'),
(r'\bow', r'oo'),
(r'\bo', r'oo'),
(r'\bO', r'Oo'),
(r'the', r'zee'),
(r'The', r'Zee'),
(r'th\b', 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 mowę Elmer Fudda"""
subs = ((r'[rl]', r'w'),
(r'qu', r'qw'),
(r'th\b', r'f'),
(r'th', r'd'),
(r'n[.]', r'n, uh-hah-hah-hah.'))
class OldeDialectizer(Dialectizer):
u"""konwertuje HTML na pozorowany język średnioangielski"""
subs = ((r'i([bcdfghjklmnpqrstvwxyz])e\b', r'y\1'),
(r'i([bcdfghjklmnpqrstvwxyz])e', r'y\1\1e'),
(r'ick\b', r'yk'),
(r'ia([bcdfghjklmnpqrstvwxyz])', r'e\1e'),
(r'e[ea]([bcdfghjklmnpqrstvwxyz])', r'e\1e'),
(r'([bcdfghjklmnpqrstvwxyz])y', r'\1ee'),
(r'([bcdfghjklmnpqrstvwxyz])er', r'\1re'),
(r'([aeiou])re\b', r'\1r'),
(r'ia([bcdfghjklmnpqrstvwxyz])', r'i\1e'),
(r'tion\b', r'cioun'),
(r'ion\b', r'ioun'),
(r'aid', r'ayde'),
(r'ai', r'ey'),
(r'ay\b', 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've\b', r'veth'),
(r'se\b', r'e'),
(r"'s\b", r'es'),
(r'ic\b', r'ick'),
(r'ics\b', r'icc'),
(r'ical\b', r'ick'),
(r'tle\b', r'til'),
(r'll\b', r'l'),
(r'ould\b', r'olde'),
(r'own\b', r'oune'),
(r'un\b', r'onne'),
(r'rry\b', r'rye'),
(r'est\b', r'este'),
(r'pt\b', r'pte'),
(r'th\b', r'the'),
(r'ch\b', r'che'),
(r'ss\b', r'sse'),
(r'([wybdp])\b', r'\1e'),
(r'([rnt])\b', r'\1\1e'),
(r'from', r'fro'),
(r'when', r'whan'))
def translate(url, dialectName="chef"):
u"""pobiera plik na podstawie URL-a
i tłumaczy korzystając 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]
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")
Uruchamiając ten skrypt, przetłumaczymy podrozdział 3.2, z książki "Dive Into Python", na pozorowany szwedzki kuchmistrza z Muppetów, udawany język Elmer Fudda (z kreskówek Królik Bugs) i pozorowany język średnioangielski (luźno oparty na "Chaucer's The Canterbury Tales"). Jeśli spojrzymy na źródło HTML wyjściowej strony, zobaczymy, że znaczniki i atrybuty zostały nietknięte, lecz tekst między znacznikami został "przetłumaczony" na udawany język. Jeśli przyglądniemy się jeszcze bardziej, zobaczymy, że tylko tytuły i akapity zostały przetłumaczone. Przedstawione kody i wyniki działania programu zostały niezmienione.
<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>
[edytuj] 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 częścią standardowej biblioteki Pythona.
Kluczem do zrozumienia tego rozdziału jest uświadomienie sobie, że HTML to nie tylko tekst, jest to tekst z pewną strukturą. Struktura ta powstaje z mniej lub bardziej hierarchicznych sekwencji znaczników początkowych i znaczników końcowych. Zazwyczaj nie pracujemy z HTML-em w sposób strukturalny, raczej tekstowo w edytorze tekstu lub wizualnie w przeglądarce internetowej, czy innym narzędziu. sgmllib.py prezentuje HTML strukturalnie.
sgmllib.py zawiera jedną ważną klasę: SGMLParser. SGMLParser rozbiera HTML na użyteczne kawałki takie jak znaczniki początkowe i znaczniki końcowe. Jak tylko udaje mu się rozebrać jakieś dane na przydatne kawałki, wywołuję odpowiednią metodę, w zależności co zostało znalezione. Żeby wykorzystać parser, tworzymy podklasę SGMLParser-a i nadpisujemy te metody. Mówiąc, że sgmllib.py prezentuje HTML strukturalnie, mieliśmy na myśli to, że struktura dokumentu HTML jest określana poprzez wywoływane metody, a także argumenty przekazywane do tych metod.
SGMLParser parsuje HTML na 8 rodzajów danych i wykonuje odpowiednie metody dla każdego z nich:
- Znacznik początkowy
- Znacznik HTML, który rozpoczyna blok np. <html>, <head>, <body> lub <pre> lub samodzielne znaczniki jak <br> lub <img>. Kiedy odnajdzie znacznik tagname, to
SGMLParserbędzie szukał metod o nazwiestart_tagnamelubdo_tagname. Na przykład, jeśli odnajdzie znacznik <pre>, to będzie szukał metodstart_prelubdo_pre. Jeśli je znajdzie,SGMLParserwywoła te metody z listą atrybutów tego znacznika. W przeciwnym wypadku wywołaunknown_starttagz nazwą znacznika i listą atrybutów. - Znacznik końcowy
- Znacznik HTML, który kończy blok np. </html>, </head>, </body> lub </pre>. Kiedy odnajdzie znacznik końcowy,
SGMLParserbędzie szukał metody o nazwieend_tagname. Jeśli ją znajdzie, wywoła tę metodę, jeśli nie, wywoła metodęunknown_endtagz nazwą znacznika. - Odwołania znakowe
- Znak specjalny, do którego dowołujemy się podając jego dziesiętny lub szesnastkowy odpowiednik np.  . Kiedy odwołanie znakowe zostanie odnalezione,
SGMLParserwywołahandle_charrefz tekstem dziesiętnego lub szesnastkowego odpowiednika znaku. - Odwołanie do encji
- Encja HTML to np. ©. Kiedy zostanie znaleziona,
SGMLParserwywołujehandle_entityrefz nazwą encji. - Komentarz
- Element HTML, który jest ograniczony przez <!-- ... -->. Kiedy zostanie znaleziony,
SGMLParserwywołujehandle_commentz zawartością komentarza. - Instrukcje przetwarzania
- Instrukcje przetwarzania HTML są ograniczone przez <? ... >. Kiedy zostaną odnalezione,
SGMLParserwywołujehandle_piz zawartością instrukcji przetwarzania. - Deklaracja
- Deklaracja HTML np. typu dokumentu (DOCTYPE), jest ograniczona przez <! ... >. Kiedy zostanie znaleziona,
SGMLParserwywołujehandle_declz zawartością deklaracji. - Dane tekstowe
- Bloki tekstu. Wszystko inne, co się nie mieści w innych 7 kategoriach. Kiedy zostaną one znalezione,
SGMLParserwywołahandle_dataz tekstem.
sgmllib.py posiada zestaw testów, które to ilustrują. Możemy uruchomić sgmllib.py, podając w linii poleceń nazwę pliku, a będzie on wyświetlał 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 będą po prostu wyświetlać swoje argumenty.
|
W ActivePython IDE pod Windows możemy określić argumenty linii poleceń za pomocą "Run script". Różne argumenty oddzielamy spacją. |
c:\python23\lib> type "c:\downloads\diveintopython\html\toc\index.html" <!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 treści angielskiej wersji tej książki, w HTML-u. Oczywiście ścieżki do plików możesz mieć trochę inne. (Angielską wersję tej książki, w formacie HTML, możesz znaleźć na http://diveintopython.org/.)
Uruchomiając to za pomocą zestawu testów sgmllib.py, zobaczymy:
c:\python23\lib> python sgmllib.py "c:\downloads\diveintopython\html\toc\index.html" data: '\n\n' 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 ' [...ciach...]
Taki jest plan reszty tego rozdziału:
- Dziedziczymy po
SGMLParser, aby stworzyć klasy, które wydobywają interesujące dane z dokumentu HTML. - Dziedziczymy po
SGMLParser, aby stworzyć podklasęBaseHTMLProcessor, która nadpisuje wszystkie 8 metod obsługi i wykorzystujemy je, aby zrekonstruować oryginalny dokument HTML z otrzymywanych kawałków. - Dziedziczymy po
BaseHTMLProcessor, aby utworzyćDialectizer, który dodaje kilka metod w celu specjalnego przetworzenia określonych znaczników HTML. Ponadto nadpisuje metodęhandle_data, aby zapewnić możliwość przetwarzania bloków tekstowych pomiędzy znacznikami HTML. - Dziedziczymy po
Dialectizer, aby stworzyć klasy, które definiują zasady przetwarzania tekstu wykorzystane wDialectizer.handle_data. - Piszemy zestaw testów, które korzystają z prawdziwej strony internetowej, http://diveintopython.org/, i ją przetwarzają.
Przy okazji dowiemy się, czym jest locals i globals, a także jak formatować łańcuchy znaków za pomocą słowników.
[edytuj] Wyciąganie danych z dokumentu HTML
Aby wyciągnąć dane z dokumentu HTML, tworzymy podklasę klasy SGMLParser i definiujemy dla encji lub każdego znacznika, który nas interesuje, odpowiednią metodę.
Pierwszym krokiem do wydobycia danych z dokumentu HTML jest zdobycie jakiegoś dokumentu. Jeśli posiadamy jakiś dokument HTML na swoim twardym dysku, możemy wykorzystać funkcje do obsługi plików, aby go odczytać, jednak prawdziwa zabawa rozpoczyna się, gdy weźmiemy HTML z istniejącej strony internetowej.
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"> </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...]
- Moduł
urllibjest częścią standardowej biblioteki Pythona. Zawiera on funkcje służące do pobierania informacji o danych, a także pobierania samych danych z internetu na podstawie adresu URL (głównie strony internetowe). - Najprostszym sposobem wykorzystania
urllib-a jest pobranie całego tekstu strony internetowej przy pomocy funkcjiurlopen. Otworzenie URL-a jest równie proste, jak otworzenie pliku. Zwracana wartość funkcjiurlopenprzypomina normalny obiekt pliku i posiada niektóre analogiczne metody do obiektu pliku. - Najprostszą czynnością, którą możemy wykonać na obiekcie zwróconym przez
urlopen, jest wywołanieread. Metoda ta odczyta cały HTML strony internetowej i zwróci go w postaci łańcucha znaków. Obiekt ten posiada także metodęreadlines, która czyta tekst linia po linii, dodając kolejne linie do listy. - Kiedy skończymy pracę na tym obiekcie, powinniśmy go jeszcze zamknąć za pomocą
close, podobnie jak normalny plik. - Mamy kompletny dokument HTML w postaci łańcucha znaków, pobrany ze strony domowej http://diveintopython.org/ i jesteśmy przygotowani do tego, aby go sparsować.
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)
resetjest wywoływany przez metodę__init__SGMLParser-a, a także można go wywołać ręcznie już po utworzeniu instancji parsera. Zatem, jeśli potrzebujemy powtórnie zainicjalizować instancję parsera, który był wcześniej używany, zrobimy to za pomocąreset(nie przez__init__). Nie ma potrzeby tworzenia nowego obiektu.- Zawsze, kiedy parser odnajdzie znacznik <a>, wywoła metodę
start_a. Znacznik może posiadać atrybuthref, a także inne jak na przykładname, czytitle. Parametrattrsjest listą krotek[(atrybut1, wartość1), (atrybut2, wartość2), ...]. Znacznik ten może być także samym<a>, poprawnym (lecz bezużytecznym) znacznikiem HTML, a w tym przypadkuattrsbędzie pustą listą. - Możemy stwierdzić, czy znacznik
<a>posiada atrybut href, za pomocą prostego wielozmiennego wyrażenie listowego. - Porównywanie napisów (np.
k=='href') jest zawsze wrażliwe na wielkość liter, lecz w tym przypadku takie użycie jest bezpieczne, ponieważSGMLParserkonwertuje podczas tworzeniaattrsnazwy atrybutów na małe litery.
>>> 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 ...]
- Wywołujemy metodę
feedzdefiniowaną wSGMLParser, aby "nakarmić" parser przekazując mu kod HTML-a. Metoda ta przyjmuje łańcuch znaków, którym w tym przypadku będzie wartość zwrócona przezusock.read(). - Podobnie jak pliki, powinniśmy zamknąć swoje obiekty URL, kiedy już nie będą ci potrzebne.
- Powinieneś także zamknąć obiekt parsera, lecz z innego powodu. Podczas czytania danych przekazujemy je do parsera, lecz metoda
feednie gwarantuje, że wszystkie przekazane dane, zostały przetworzone. Parser może te dane zbuforować i czekać na dalszą porcję danych. Kiedy wywołamyclose, mamy pewność, że bufor zostanie opróżniony i wszystko zostanie całkowicie sparsowane. - Ponieważ parser został zamknięty, więc parsowanie zostało zakończone i
parser.urlszawiera listę wszystkich URL-i, do których linki zawiera dokument HTML. (Twoje wyjście może wyglądać inaczej, ponieważ z biegiem czasu linki mogły ulec zmianie.)
[edytuj] Wprowadzenie do BaseHTMLProcessor.py
SGMLParser nie tworzy niczego samodzielnie. On po prostu parsuje, parsuje i parsuje i wywołuje metodę dla każdej interesującej rzeczy jaką znajdzie, ale te metody nie wykonują niczego. SGMLParser jest konsumentem HTML-a: bierze HTML-a i rozkłada go na małe, strukturalne części. Jak już widzieliśmy w poprzednim podrozdziale, możemy dziedziczyć po klasie SGMLParser, aby zdefiniować klasy, które przechwycą poszczególne znaczniki i jakoś to pożytecznie wykorzystają, np. stworzą listę odnośników na danej stronie internetowej. Teraz pójdziemy krok dalej i zdefiniujemy klasę, która przechwyci wszystko, co zgłosi SGMLParser i zrekonstruuje kompletny dokument HTML. Używając terminologii technicznej nasza klasa będzie 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.
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("" % locals())
def handle_pi(self, text): #(8)
self.pieces.append("<?%(text)s>" % locals())
def handle_decl(self, text):
self.pieces.append("<!%(text)s>" % locals())
reset, wołany przezSGMLParser.__init__, inicjalizujeself.piecesjako pustą listę przed wywołaniem metody klasy przodka.self.piecesjest atrybutem, który będzie przechowywał części konstruowanego dokumentu HTML. Każda metoda będzie rekonstruować HTML parsowany przezSGMLParseri każda z tych metod będzie dodawać jakiś tekst doself.pieces. Zauważmy, żeself.piecesjest listą. Moglibyśmy ulec pokusie, aby zdefiniować ten atrybut jako obiekt łańcucha znaków i po prostu dołączać do niego kolejne kawałki tekstu. To także by działało, ale Python jest dużo bardziej wydajny pracując z listami. [8]- Ponieważ
BaseHTMLProcessornie definiuje żadnej metody dla poszczególnych znaczników (jak np. metodastart_awURLLister),SGMLParserbędzie wywoływał dla każdego początkowego znacznika metodęunknown_starttag. Ta metoda przyjmuje na wejściu znacznik (argumenttag) i listę par postaci nazwa atrybutu/wartość atrybutu (argumentattrs), a następnie rekonstruuje oryginalnego HTML-a i dodaje doself.pieces. Napis formatujący jest tutaj nieco dziwny; rozwikłamy to później w tym rozdziale (a także tą dziwnie wyglądającą funkcjęlocals). - Rekonstrukcja znaczników końcowych jest dużo prostsza; po prostu pobieramy nazwę znacznika i opakowujemy nawiasami ostrymi </...>.
- Gdy
SGMLParsernapotka odwołanie znakowe wywołuje metodęhandle_charrefi przekazuje jej samą wartość odwołania. Jeśli dokument HTML zawiera ,refprzyjmie wartość160. Rekonstrukcja oryginalnego kompletnego odwołania znakowego wymaga po prostu dodania znaków &#...;. - 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łaściwie, jak wskazał na to pewien czytelnik, jest to nieco bardziej skomplikowane. Tylko niektóre standardowe encje HTML-a kończą się znakiem średnika; inne podobnie wyglądające encje już nie. Na szczęście dla nas zbiór standardowych encji HTML-a zdefiniowany jest w Pythonie w słowniku w module o nazwie
htmlentitydefs. Stąd ta dodatkowa instrukcjaif.) - Bloki tekstu są po prostu dołączane do
self.piecesbez żadnych zmian, w postaci dosłownej. - Komentarze HTML-a opakowywane są znakami <!--...-->.
- Instrukcje przetwarzania wstawiane są pomiędzy znakami <?...>.
BaseHTMLProcessor i jego metoda output
def output(self): #(1)
u"""Zwraca przetworzony HTML jako pojedynczy łańcuch znaków"""
return "".join(self.pieces)
- To jest jedyna metoda, która nie jest wołana przez klasę przodka, czyli klasę
SGMLParser. Ponieważ pozostałe metody umieszczają swoje zrekonstruowane kawałki HTML-a wself.pieces, ta funkcja jest potrzebna, aby połączyć wszystkie te kawałki w jeden napis. Gdyż jak już wspomniano wcześniej Python jest świetny w obsłudze list i z reguły mierny w obsłudze napisów, kompletny napis wyjściowy tworzony jest tylko wtedy, gdy ktoś o to wyraźnie poprosi.
[edytuj] Materiały dodatkowe
- W3C omawia odwołania znakowe i encje.
- Python Library Reference potwierdza Twoje podejrzenia, iż moduł
htmlentitydefsjest dokładnie tym na co wygląda.
[edytuj] locals i globals
Odejdźmy teraz na minutkę od przetwarzania HTML-a. Porozmawiajmy o tym, jak Python obchodzi się ze zmiennymi. Python posiada dwie wbudowane funkcje, locals i globals, które pozwalają nam uzyskać w słownikowy sposób dostęp do zmiennych lokalnych i globalnych.
Pamiętasz locals? Pierwszy raz mogliśmy ją zobaczyć 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 możesz jeszcze się uczyć o locals. Najpierw, musisz nauczyć się, czym są przestrzenie nazw. Przedstawimy teraz trochę suchego materiału, lecz ważnego, dlatego też zachowaj uwagę.
Python korzysta z czegoś, co się nazywa przestrzenią nazw (ang. namespace), aby śledzić zmienne. Przestrzeń nazw jest właściwie słownikiem, gdzie kluczami są nazwy zmiennych, a wartościami słownika są wartości tych zmiennych. Możemy dostać się do przestrzeni nazw, jak do Pythonowego słownika, co zresztą zobaczymy za chwilkę.
Z dowolnego miejsca Pythonowego programu mamy dostęp do kilku przestrzeni nazw. Każda funkcja posiada własną przestrzeń nazw, nazywaną lokalną przestrzenią nazw, a która śledzi zmienne funkcji, włączając w to jej argumenty i lokalnie zdefiniowane zmienne. Każdy moduł posiada własną przestrzeń nazw, nazwaną globalną przestrzenią nazw, a która śledzi zmienne modułu, włączając w to funkcje, klasy i inne zaimportowane moduły, a także zmienne zdefiniowane w tym module i stałe. Jest także wbudowana przestrzeń nazw, dostępna z każdego modułu, a która przechowuje funkcje wbudowane i wyjątki.
Kiedy pewna linia kodu pyta się o wartość zmiennej x, Python przeszuka wszystkie przestrzenie nazw, aby ją znaleźć, w poniższym porządku:
- lokalna przestrzeń nazw -- określona dla bieżącej funkcji lub metody pewnej klasy. Jeśli funkcja definiuje jakąś lokalną zmienną
x, Python wykorzysta ją i zakończy szukanie. - przestrzeni nazw, w której dana funkcja została zagnieżdżona i przestrzeniach nazw, które znajdują się wyżej w "zagnieżdżonej" hierarchii.
- globalna przestrzeń nazw -- określona dla bieżącego modułu. Jeśli moduł definiuje zmienną lub klasę o nazwie
x, Python wykorzysta ją i zakończy szukanie. - wbudowana przestrzeń nazw -- globalna dla wszystkich modułów. Ponieważ jest to ostatnia deska ratunku, Python przyjmie, że
xjest nazwą wbudowanej funkcji lub zmiennej.
Jeśli Python nie znajdzie x w żadnej z tych przestrzeni nazw, poddaje się i wyrzuca wyjątek NameError z wiadomością "name 'x' is not defined", którą zobaczyliśmy w przykładzie 3.21, lecz nie jesteśmy w stanie ocenić, jak Python zadziała, zanim dostaniemy ten błąd.
Zmieszałeś się? Nie panikuj! Jest to naprawdę wypaśne. Podobnie, jak wiele rzeczy w Pythonie, przestrzenie nazw są bezpośrednio dostępne podczas wykonywania programu. Jak? Do lokalnej przestrzeni nazw mamy dostęp poprzez wbudowaną funkcję locals, a globalna (na poziomie modułu) przestrzeń nazw jest dostępna poprzez wbudowaną funkcję globals.
locals>>> def foo(arg): #(1) ... x = 1 ... print locals() ... >>> foo(7) #(2) {'arg': 7, 'x': 1} >>> foo('bar') #(3) {'arg': 'bar', 'x': 1}
- Funkcja
fooposiada dwie zmienne w swojej lokalnej przestrzeni nazw:arg, której wartość jest przekazana do funkcji, a takżex, która jest zdefiniowana wewnątrz funkcji. localszwraca słownik par nazwa/wartość. Kluczami słownika są nazwy zmiennych w postaci napisów. Wartościami słownika są bieżące wartości tych zmiennych. Zatem wywołującfooz7, wypiszemy słownik zawierający dwie lokalne zmienne tej funkcji, czyliarg(o wartości7) ix(o wartości1).- Pamiętaj, Python jest dynamicznie typowany, dlatego też możemy w prosty sposób jako argument
argprzekazać napis. Funkcja (a także wywołanielocals) będą nadal działać jak należy.localsdziała z wszystkimi zmiennymi dowolnych typów danych.
To co locals robi dla lokalnej (należącej do funkcji) przestrzeni nazw, globals robi dla globalnej (modułu) przestrzeni nazw. globals jest bardziej ekscytujące, ponieważ przestrzeń nazw modułu jest bardziej pasjonująca [9]. Przestrzeń nazw modułu nie tylko przechowuje zmienne i stałe na poziomie tego modułu, lecz także funkcje i klasy zdefiniowane w tym module. Ponadto dołączone do tego jest cokolwiek, co zostało zaimportowane do tego modułu.
Pamiętasz różnicę między from module import, a import module? Za pomocą import module, zaimportujemy sam moduł, który zachowa własną przestrzeń nazw, a to jest przyczyną, dlaczego musimy odwołać się do nazwy modułu, aby dostać się do jakiejś funkcji lub atrybutu (pisząc module.function). Z kolei za pomocą from module import rzeczywiście importujemy do własnej przestrzeni nazw określoną funkcje i atrybuty z innego modułu, a dzięki temu odwołujemy się do niego bezpośrednio, bez wskazywania modułu, z którego one pochodzą. Dzięki funkcji globals możemy zobaczyć, że rzeczywiście tak jest.
Spójrzmy na poniższy blok kodu, który znajduje się na dole BaseHTMLProcessor.py.
globals
if __name__ == "__main__":
for k, v in globals().items(): #(1)
print k, "=", v
- Na wypadek gdyby wydawało Ci się to straszne, to pamiętaj, że widzieliśmy to już wcześniej. Funkcja
globalszwraca słownik, następnie iterujemy go wykorzystując metodęitemsi wielozmienne przypisanie. Jedyną nową rzeczą jest funkcjaglobals.
Teraz, uruchamiając skrypt z linii poleceń otrzymamy takie wyjście (twoje wyjście może się nieco różnić, jest zależne od systemu i miejsca instalacji Pythona):
c:\docbook\dip\py> python BaseHTMLProcessor.py SGMLParser = sgmllib.SGMLParser #(1) htmlentitydefs = <module 'htmlentitydefs' from 'C:\Python23\lib\htmlentitydefs.py'> #(2) BaseHTMLProcessor = __main__.BaseHTMLProcessor #(3) __name__ = __main__ #(4) [...ciach ...]
SGMLParserzostał zaimportowany zsgmllib, wykorzystującfrom module import. Oznacza to, że został zaimportowany bezpośrednio do przestrzeni nazw modułu i w tym też miejscu jest.- W przeciwieństwie do
SGMLParsera,htmlentitydefszostał zaimportowany wykorzystując instrukcjęimport. Oznacza to, że modułhtmlentitydefssam w sobie jest przestrzenią nazw, ale zmiennaentitydefswewnątrzhtmlentitydefsjuż nie. - Moduł ten definiuje jedną klasę,
BaseHTMLProcessori oto ona. Dodajmy, że ta wartość jest klasą samą w sobie, a nie jakąś specyficzną instancją tej klasy. - Pamiętasz trik
if __name__? Kiedy uruchamiamy moduł (zamiast importować go z innego modułu), to wbudowany atrybut__name__ma specjalną wartość,"__main__". Ponieważ uruchomiliśmy ten moduł jako skrypt z linii poleceń, wartość__name__wynosi"__main__", dlatego też zostanie wykonany mały kod testowy, który wypisujeglobals.
Poniżej pokażemy inną ważną różnicę między funkcjami locals i globals, a o której powinieniśmy się dowiedzieć, zanim nas to ukąsi. Jakkolwiek to i tak Ciebie ukąsi, ale przynajmniej będziesz pamiętał, że była o tym mowa w tym podręczniku.
locals jest tylko do odczytu, a globals już 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)
- Ponieważ
foozostało wywołane z argumentem3, więc zostanie wypisane{'arg': 3, 'x': 1}. Nie powinno to być zaskoczeniem. localsjest funkcją zwracającą słownik i w tym miejscu zmieniamy wartość w tym słowniku. Możemy myśleć, że wartość zmiennejxzostanie zmieniona na2, jednak tak nie będzie.localswłaściwie nie zwraca lokalnej przestrzeni nazw, zwraca jego kopię. Zatem zmieniając ją, nie zmieniamy wartości zmiennych w lokalnej przestrzeni nazw.- Zostanie wypisane
x= 1, a niex= 2. - Po tym, jak zostaliśmy poparzeni przez
locals, możemy myśleć, że ta operacja nie zmieni wartościz, ale w rzeczywistości zmieni. W skutek wewnętrznych różnic implementacyjnych [10],globalszwraca aktualną, globalną przestrzeń nazw, a nie jej kopię; całkowicie odwrotne zachowanie w stosunku dolocals. Tak więc dowolna zmiana zwróconego przezglobalssłownika bezpośrednio wpływa na zmienne globalne. - Wypisze
z= 8, a niez= 7.
[edytuj] Formatowanie napisów w oparciu o słowniki
Dlaczego uczyliśmy się na temat funkcji locals i globals? Ponieważ teraz możemy się nauczyć formatowania napisów w oparciu o słowniki. Jak już mówiliśmy, regularne formatowanie napisów umożliwia w łatwy sposób wstawianie wartości do napisów. Wartości są wyszczególnione w krotce i w odpowiednim porządku wstawione do napisu, gdzie występuje pole formatujące. O ile jest to skuteczne, nie zawsze tworzy kod łatwy do czytania, zwłaszcza, gdy zostaje wstawianych wiele wartości. Żeby zrozumieć o co chodzi, nie wystarczy po prostu jednorazowo prześledzić napis; trzeba ciągle skakać między czytanym napisem, a czytaną krotką wartości.
Mamy tutaj alternatywną metodę formatowania napisu, wykorzystującą słowniki zamiast krotek.
>>> 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'
- Zamiast korzystać z krotki wartości, formujemy napis formatujący, który korzysta ze słownika
params. Ponadto zamiast prostego pola%sw napisie, pole zawiera nazwę w nawiasach okrągłych. Nazwa ta jest wykorzystana jako klucz w słownikuparamsi zostaje zastąpione odpowiednią wartością,secret, w miejscu wystąpienia pola%(pwd)s. - Takie formatowanie może posiadać dowolną liczbę odwołań do kluczy. Każdy klucz musi istnieć w podanym słowniku, ponieważ inaczej formatowanie zakończy się niepowodzeniem i zostanie rzucony wyjątek
KeyError. - Możemy nawet wykorzystać ten sam klucz kilka razy. Każde wystąpienie zostanie zastąpione odpowiednią wartością.
Zatem dlaczego używać formatowania napisu w oparciu o słowniki? Może to wyglądać na nadmierne wmieszanie słownika z kluczami i wartościami, aby wykonać proste formatowanie napisu. W rzeczywistości jest bardzo przydatne, kiedy już się ma słownik z kluczami o sensownych nazwach i wartościach, jak np. locals.
def handle_comment(self, text):
self.pieces.append("<!--%(text)s-->" % locals()) #(1)
- Formatowanie za pomocą słowników jest powszechnie używane z wbudowaną funkcję
locals. Oznacza to, że możemy wykorzystywać nazwy zmiennych lokalnych wewnątrz napisu formatującego (w tym przypadkutext, który został przykazany jako argument do metody klasy) i każda nazwa zmiennej zostanie zastąpiona jej wartością. Jeślitextprzechowuje wartość'Początek stopki', formatowany napis"<!--%(text)s-->" % locals()zostanie wygenerowany jako'<!--Początek stopki-->'.
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)
- Kiedy metoda ta zostaje wywołana,
attrsjest listą krotek postaci klucz/wartość, podobnie jak zwrócona wartość metody słownikaitems, a to oznacza, że możemy wykorzystać wielozmienne przypisanie, aby wykonać na niej iterację. Powinniśmy już być zaznajomieni z tymi operacjami, ale występuje ich tutaj sporo, więc prześledźmy je po kolei:- Przypuśćmy, że
attrswynosi [('href', 'index.html'), ('title', 'Idź do strony domowej')]. - W pierwszym przebiegu odwzorowywania listy,
keyprzyjmie wartość'href', avalueweźmie wartość'index.html'. - Formatowanie napisu
' %s="%s"' % (key, value)przekształci się na' href="index.html"'. Napis ten będzie pierwszym elementem zwróconej listy. - W drugim przebiegu,
keyprzyjmie wartość'title', avaluewartość'Idź do strony domowej'. - Formatowanie napisu przekształci to na
' title="Idź do strony domowej"'. - Po wykonaniu wyrażenia listowego zwrócona lista będzie przechowywała te dwa wygenerowane napisy, a
strattrsbędzie połączeniem obydwu tych elementów, czyli będzie przechowywał' href="index.html" title="Go to home page"'.
- Przypuśćmy, że
- Teraz formatując napis za pomocą słownika, wstawiamy wartość zmiennej
tagistrattrsdo napisu. Zatem jeślitagwynosił'a', w ostateczności otrzymamy wynik'<a href="index.html" title="Idź do strony domowej'">'i to następnie dodajemy doself.pieces.
[edytuj] Dodawanie cudzysłowów do wartości atrybutów
Dość powszechnym pytaniem na comp.lang.python jest "Mam kilka dokumentów HTML z wartościami atrybutów bez cudzysłowów i chciałbym odpowiednio te cudzysłowy dodać. Jak mogę to zrobić?" [11] (Przeważnie wynika to z dołączenia do projektu nowego kierownika, będącego wyznawcą HTML-owych standardów i bezwzględnie wymagającego, aby wszystkie strony bezbłędnie przechodziły kontrolę HTML-owych walidatorów. Wartości atrybutów bez cudzysłowów są powszechnym naruszeniem HTML-wego standardu.) Niezależnie od powodu, uzupełnienie cudzysłowów jest łatwe przy pomocy klasy BaseHTMLProcessor.
BaseHTMLProcessor konsumuje HTML-a (ponieważ jest potomkiem klasy SGMLParser) i produkuje równoważny HTML, ale ten wyjściowy HTML nie jest identyczny z wejściowym. Znaczniki i nazwy atrybutów zostaną zapisane małymi literami, nawet jeśli wcześniej były dużymi lub wymieszanymi, a wartości atrybutów zostaną zamknięte w podwójnych cudzysłowach, nawet jeśli wcześniej były otoczone pojedynczymi cudzysłowami lub nie miały żadnych cudzysłowów. To jest taki efekt uboczny, z którego możemy tu skorzystać.
>>> 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 treści</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>
<li><a href="toc.html">Spis treści</a></li>
<li><a href="history.html">Historia zmian</a></li>
</body>
</html>
- Zauważmy, że wartości atrybutów href w znacznikach <a> nie są ograniczone cudzysłowami. (Jednocześnie zauważmy, że używamy potrójnych cudzysłowów do czegoś innego niż notki dokumentacyjnej i to bezpośrednio w IDE. Są one bardzo użyteczne.)
- "Karmimy" parser.
- Używając funkcji
outputzdefiniowanej w klasieBaseHTMLProcessor, otrzymujemy wyjście jako pojedynczy kompletny łańcuch znaków ze wszystkimi wartościami atrybutów w cudzysłowach. Pomyślmy, jak wiele właściwie się tutaj działo:SGMLParsersparsował cały dokument HTML, podzielił go na znaczniki, odwołania, dane tekstowe itp.;BaseHTMLProcessorużył tych elementów do zrekonstruowania części HTML-a (które nadal są składowane wparser.pieces, jeśli chcesz je zobaczyć); na końcu wywołaliśmyparser.output, która to metoda połączyła wszystkie części HTML-a w jeden napis.
Python/Wprowadzanie do dialect.py
[edytuj] Wszystko razem
Nadszedł czas, aby połączyć w całość wiedzę, którą zdobyliśmy do tej pory.
translate, część 1
def translate(url, dialectName="chef"): #(1)
import urllib #(2)
sock = urllib.urlopen(url) #(3)
htmlSource = sock.read()
sock.close()
- Funkcja
translateprzyjmuje opcjonalny argumentdialectName, który jest łańcuchem znaków określającym używany dialekt. Zaraz zobaczymy, jak to jest wykorzystywane. - Moment, tam jest ważna instrukcja w tej funkcji! Jest to w pełni dozwolone w Pythonie działanie. Instrukcja
importużywaliśmy zwykle na samym początku programu, aby zaimportowany moduł był dostępny w dowolnym miejscu. Ale możemy także importować moduły w samej funkcji, przez co będą one dostępne tylko z jej poziomu. Jeżeli jakiegoś moduły potrzebujemy użyć tylko w jednej funkcji, jest to najlepszy sposób aby zachować modularność twojego programu. (Docenisz to, gdy okaże się, że twój weekendowy hack wyrósł na ważące 800 linii dzieło sztuki, a ty właśnie zdecydujesz się podzielić to na mniejsze części). - Tutaj otwieramy połączenie i do zmiennej
htmlSourcepobieramy źródło HTML spod wskazanego adresu URL.
translate, część 2: coraz ciekawiej
parserName = "%sDialectizer" % dialectName.capitalize() #(1)
parserClass = globals()[parserName] #(2)
parser = parserClass() #(3)
capitalizejest metodą łańcucha znaków, z którą się jeszcze nie spotkaliśmy; zmienia ona pierwszy znak na wielką literę, a wszystkie pozostałe znaki na małe litery. W połączeniu z prostym formatowaniem napisu, nazwa dialektu zamieniana jest na nazwę odpowiadającej mu klasy. JeżelidialectNamema wartość'chef',parserNameprzyjmie wartość'ChefDialectizer'.- W tym miejscu mamy nazwę klasy (w zmiennej
parserName) oraz dostęp do globalnej przestrzeni nazw, poprzez słownikglobals(). Łącząc obie informacje dostajemy referencje do klasy o określonej nazwie. (Pamiętajmy, że klasy są obiektami i mogą być przypisane do zmiennej, jak każdy inny obiekt). JeżeliparserNamema wartość'ChefDialectizer',parserClassbędzie klasąChefDialectizer. - Ostatecznie, mając obiekt klasy (
parserClass) chcemy zainicjować tę klasę. Wiemy już jak zrobić –- po prostu wywołujemy klasę w taki sposób, jakby była to funkcja. Fakt, że klasa jest przechowywana w lokalnej zmiennej nie robi żadnej różnicy, po prostu wywołujemy lokalną zmienną jak funkcję, i na wyjściu wyskakuje instancja klasy. JeżeliparserClassjest klasąChefDialectizer, parser będzie instancją klasyChefDialectizer.
Zastanawiasz się, ponieważ istnieją tylko 3 klasy Dialectizer, dlaczego by nie użyć po prostu instrukcji case? (W porządku, w Pythonie nie ma instrukcji case, ale zawsze można użyć serii instrukcji if). Z jednego powodu: elastyczności programu. Funkcja translate nie ma pojęcia, jak wiele zdefiniowaliśmy podklas Dialectizer-a. Wyobraźmy sobie, że definiujemy jutro nową klasę FooDialectizer -– funkcja translate zadziała bez przeróbek.
Nawet lepiej – wyobraźmy sobie, że umieszczasz klasę FooDialectizer w osobnym module i importujesz ją poprzez from module import. Jak wcześniej mogliśmy się przekonać, taka operacja dołączy to do globals(), więc funkcja translate nadal będzie działać prawidłowo bez konieczności dokonywania modyfikacji, nawet wtedy, gdy FooDialectizer znajdzie się w oddzielnym pliku.
Teraz wyobraźmy sobie, że nazwa dialektu pochodzi skądś spoza programu, może z bazy danych lub z wartości wprowadzonej przez użytkownika. Możemy użyć jakąkolwiek ilość pythonowych skryptów po stronie serwera, aby dynamicznie generować strony internetowe; taka funkcja mogłaby przekazać URL i nazwę dialektu (oba w postaci łańcucha znaków) w zapytania żądania strony internetowej, i zwrócić "przetłumaczoną" stronę.
Na koniec wyobraźmy sobie framework Dialectizer z wbudowaną obsługą plug-inów. Możemy umieścić każdą podklasę Dialectizer-a w osobnym pliku pozostawiając jedynie w pliku dialect.py funkcję translate. Jeżeli zachowasz stały schemat nazewnictwa klas, funkcja translate może dynamicznie importować potrzebną klasę z odpowiedniego pliku, jedynie na podstawie podanej nazwy dialektu. (Dynamicznego importowanie omówimy to w dalszej części tego podręcznika). Aby dodać nowy dialekt, wystarczy, że utworzymy odpowiednio nazwany plik (np. foodialect.py zawierający klasę FooDialectizer) w katalogu z plug-inami. Wywołując funkcję translate z nazwą dialektu 'foo', odnajdzie ona moduł foodialect.py i automatycznie zaimportuje klasę FooDialectizer.
translate, część 3
parser.feed(htmlSource) #(1)
parser.close() #(2)
return parser.output() #(3)
- Po tym całym wyobrażaniu sobie, co robiło się już nudne, mamy funkcję
feed, która przeprowadza całą transformację. Ponieważ całe źródło HTML-a mamy w jednym łańcuchu znaków, więc funkcjęfeedwywołujemy tylko raz. Oczywiście możemy wywoływać ją dowolną ilość razy, a parser za każdym razem przeprowadzi transformację. Na przykład, jeżeli obawiamy się o zużycie pamięci (albo wiemy, że będziemy parsowali naprawdę wielkie strony HTML), możemy umieścić tą funkcję w pętli, w której będziemy odczytywał tylko kawałek HTML-a i karmił nim parser. Efekty będą takie same. - Ponieważ funkcja
feedwykorzystuje wewnętrzny bufor, powinniśmy zawsze po zakończeniu operacji wywołać funkcjęclose()parsera (nawet jeżeli przesłaliśmy parserowi całość za jednym razem). W przeciwnym wypadku możemy stwierdzić, że w otrzymanym wyniku brakuje kilku ostatnich bajtów. - Pamiętajmy, że funkcja
output, którą zdefiniowaliśmy samodzielnie w klasieBaseHTMLProcessor, łączy wszystkie zbuforowane kawałki i zwraca całość w postaci pojedynczego łańcucha znaków.
I właśnie w taki sposób, "przetłumaczyliśmy" stronę internetową, podając jedynie jej adres URL i nazwę dialektu.
[edytuj] Podsumowanie
Python dostarcza potężne narzędzie do operowania na HTML-u - bibliotekę sgmllib.py, która obudowuje kod HTML w model obiektowy. Możemy używać tego narzędzia na wiele sposobów:
- parsując HTML w poszukiwaniu specyficznych informacji
- gromadząc wyniki, np. tak jak to robi URL lister.
- modyfikując strukturę w dowolny sposób, np. dodawać cudzysłowy do atrybutów
- transformując HTML w inny format, poprzez manipulowanie tekstem bez ruszania znaczników, np. tak jak nasz
Dialectizer
Po tych wszystkich przykładach, powinniśmy umieć wykonywać wszystkie z tych operacji:
- Używać odpowiednio
locals()iglobals(), aby dostać się do przestrzeni nazw - Formatować łańcuchy w oparciu o słowniki
[edytuj] Przetwarzanie XML-a
[edytuj] Nurkujemy
Kolejne dwa rozdziały są na temat przetwarzania XML-a w Pythonie. Będzie to przydatne, jeśli już wiesz, jak wyglądają dokumenty XML, a które są wykonane ze strukturalnych znaczników określających hierarchię elementów itp. Jeśli nic z tego nie rozumiesz, możesz przeczytać coś na ten temat na Wikipedii.
Nawet jeśli nie interesuje Ciebie temat XML-a i tak dobrze by było przeczytać te rozdziały, ponieważ omawiają one wiele ważnych tematów jak pakiety, argumenty linii poleceń, a także jak wykorzystywać getattr jako pośrednik metod.
Bycie magistrem filozofii nie jest wymagane, chociaż jeśli kiedyś spotkaliśmy się z tekstami napisanymi przez Immanuel Kanta, lepiej zrozumiemy przykładowy program, niż jeśli byłbyś specjalistą w czymś 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, że czyta przez chwilę dokument XML i wywołuje dla każdego odnalezionego elementu odpowiednie metody. (Jeśli przeczytaliśmy rozdział 8, powinno to wyglądać znajomo, ponieważ w taki sposób pracuje moduł sgmllib.) Inny jest nazywany DOM (Document Object Model), a pracuje w ten sposób, że jednorazowo czyta cały dokument XML i tworzy wewnętrzną reprezentację, wykorzystując klasy Pythona powiązane w strukturę drzewa. Python posiada standardowe moduły do obydwu sposobów parsowania, ale rozdział ten opisze tylko, jak wykorzystywać DOM.
Poniżej znajduje się kompletny program Pythona, który generuje pseudolosowe wyjście oparte na gramatyce bezkontekstowej zdefiniowanej w formacie XML. Nie przejmujmy się, jeśli nie zrozumieliśmy, co to znaczy. Będziemy głębiej badać zarówno wejście programu, jak i jego wyjście w tym i następnym rozdziale.
u"""Generator Kanta dla Pythona
Generuje pseudofilozofię opartą na gramatyce bezkontekstowej
Użycie: python kgp.py [options] [source]
Opcje:
-g ..., --grammar=... używa określonego pliku gramatyki lub adres URL
-h, --help wyświetla ten komunikat pomocy
-d wyświetla informacje debugowania podczas parsowania
Przykłady:
kgp.py generuje kilka akapitów z filozofią Kanta
kgp.py -g husserl.xml generuje kilka akapitów z filozofią Husserla
kpg.py "<xref id='paragraph'/>" generuje akapit Kanta
kgp.py template.xml czyta template.xml, aby określić, co ma generować
"""
from xml.dom import minidom
import random
import toolbox
import sys
import getopt
_debug = 0
class NoSourceError(Exception): pass
class KantGenerator(object):
u"""generuje pseudofilozofię opartą 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 źródło wejścia, zwraca sparsowany dokument XML
- adres URL z plikiem XML ("http://diveintopython.org/kant.xml")
- nazwę lokalnego pliku XML ("~/diveintopython/common/py/kant.xml")
- standardowe wejście ("-")
- bieżący dokument XML w postaci łańcucha znaków
"""
sock = toolbox.openAnything(source)
xmldoc = minidom.parse(sock).documentElement
sock.close()
return xmldoc
def loadGrammar(self, grammar):
u"""wczytuje gramatykę bezkontekstową"""
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 źródło source"""
self.source = self._load(source)
def getDefaultSource(self):
u"""zgaduje domyślne źródło bieżącej gramatyki
Domyślnym źródłem będzie jeden z <ref>-ów, do którego nic się
nie odwołuje. Może brzmi to skomplikowanie, ale tak napr