|
|
|
Z tego rodziału warto co najwyżej przeczytać część dotyczącą tego systemu operacyjnego, którego się samemu używa.
Opisujemy zestaw dla Windows i Linuksa, który jest używany na zajęciach na Neuroinformatyce i Fizyce Medycznej. Obecnie (rok szkolny 2009/10) korzystamy z Pythona w wersji 2.6 bo do niej są stabilne moduły numeryczne.
Proszę zwrócić uwagę aby w instalowanym zestawie znalazło się środowisko IDLE. Jest to Zintegrowane środowisko programistyczne (ang. IDE — Integrated Development Environment). Będziemy z niego korzystać na zajęciach. Jest ono domyślnie instalowane z Pythonem pod Windowsami i pod Mac OS X. Również użytkownicy Linuksa i BSD mogą zainstalować IDLE.
W następnej sekcji jest opisane jak używać IDLE. W poszukiwaniu szczegółów, zapoznaj się z dokumentacją IDLE.
Istnieją wiele sposobów instalacji Pythona pod Windows:
Jeśli chcesz używać Pythona w wierszu poleceń, musisz odpowiednio ustawić zmienną PATH.
W Windows 2000, XP, 2003 otwórz Panel Sterowania → System → Zaawansowne → Zmienne środowiskowe. W części Zmienne systemowe kliknij na zmiennej PATH, wybierz Edytuj i dodaj ;C:\Python25 na samym końcu. Oczywiście powinieneś wpisać poprawną nazwę katalogu, w którym Python jest zainstalowany.
W starszych wersjach Windows dodaj linijkę PATH=%PATH%;C:\Python25 do pliku C:\AUTOEXEC.BAT i zrestartuj system. W Windows NT użyj pliku AUTOEXEC.NT.
Jeśli używasz dystrybucji w rodzaju Ubuntu, Fedora, Mandriva czy {wstaw własną}, albo BSD, np. FreeBSD, prawdopodobnie Python jest już zainstalowany w Twoim systemie.
Aby to sprawdzić, otwórz okno terminala (np. konsole lub gnome-terminal) i wprowadź polecenie python -V (zwróć uwagę na wielkie V):
$ python -V Python 2.5.4
Jeżeli widzisz informacje na temat wersji podobne do powyższych, oznacza to, że Python jest już zainstalowany.
Jednak jeśli dostaniesz coś w rodzaju:
$ python -V -bash: python: nie znaleziono polecenia
to nie masz go zainstalowanego. Jest to mało prawdopodobne, ale możliwe. W takim przypadku pozostają Ci następujące możliwości instalacji Pythona w systemie:
Python jest domyślnie zainstalowany w Mac OS X 10.3 i wyższych. Jeśli chcesz zainstalować nowszą wersję Pythona, użyj MacPorts:
Jeśli masz starszą wersji Mac OS X, odwiedź oficjalną stronę MacPython, pobierz plik DMG dla posiadanej wersji systemu, podepnij obraz dyskietki i uruchom instalator.
W systemie Windows instalacja Pythona jest prosta i polega na pobraniu instalatora i dwukrotnym kliknięciu na nim. W systemie linuksowym najprawdopodobniej już miałeś zainstalowanego Pythona, a nawet jeśli nie, możesz go zainstalować za pomocą programu zarządzającego pakietami charakterystycznego dla Twojej dystrybucji. Również w Mac OS X Python już był zainstalowany w Twoim systemie. Od tej chwili zakładamy więc, że masz zainstalowanego Pythona.
W pierwszym kroku nauczymy się uruchamiać w Pythonie tradycyjny* program „Witaj świecie!". Dzięki temu nauczysz się pisać w języku Python, a także zapisywać i uruchamiać swoje pythonowe programy.
*Jest taka tradycja, że za każdym razem, kiedy uczysz się nowego języka programowania, pierwszym programem jest „Witaj świecie!” (w oryginale „Hello World”) — wszystko, co robi, to tylko wypisanie tekstu „Witaj świecie!”.
Są dwa sposoby uruchamiania programów pythonowych: użycie linii poleceń interpretera lub użycie pliku źródłowego. Zobaczmy, w jaki sposób korzystać z tych metod.
Python jest interpreterem poleceń. Mamy dwie możliwości wydawania owych poleceń:
Uruchom interpreter Pythona, wpisując python w powłoce (ang. shell) systemowej. Jeśli używasz Linuksa lub BSD, otwórz w tym celu program w rodzaju konsole czy terminal.
W przypadku Mac OS X otwórz Terminal (znajdziesz go w: Finder → Aplikacje → Użytki → Terminal), a jeśli używasz Windows, to uruchom (Start → Uruchom...) cmd lub (w przypadku starszych wersji Windows) command albo po prostu znajdź odpowiednią pozycję w menu.
Zauważ, że w przypadku Windows musisz zadbać o poprawnie ustawioną zmienną PATH.
Użytkownicy Windows mogą chcieć również używać IDLE. Uzyskasz do niego dostęp przez:
Start → Wszystkie programy → Python 2.x → IDLE (Python GUI).
Teraz wpisz print 'Witaj świecie!' i wciśnij Enter. Na ekranie powinien pojawić się napis Witaj świecie!
$ python Python 2.5.4 (r254:67916, Mar 13 2009, 18:11:09) [GCC 4.1.2] on linux2 Type "help", "copyright", "credits" or "license" for more information. >>> print 'Witaj świecie!' Witaj świecie! >>>
Uwaga:
Zobacz, Python natychmiast zwraca wynik! To, co właśnie wprowadziłeś, to pojedyncze polecenie Pythona. Używamy print (ang. drukuj, wypisz), aby wypisać to, co mu podamy. W naszym przypadku podaliśmy tekst Witaj świecie! i został on natychmiast wyświetlony na ekranie.
Jak wyjść z interpretera?
Wróćmy wreszcie do programowania.
Uruchom swój edytor (u nas idle), wpisz poniższy program i zapisz go pod nazwą witaj.py.
Aby zrobić to w idle, wybierz File → New Window i wpisz poniższy program, po czym kliknij File → Save.
# Nazwa pliku: witaj.py print 'Witaj świecie!'
Uruchom program, otwierając powłokę, przechodząc do odpowiedniego katalogu i wykonując w nim polecenie python witaj.py.
W idle, wybierz w menu Run → Run Module lub użyj skrótu klawiaturowego F5.
Wynik powinien przypominać:
$ python witaj.py Witaj świecie!
Gratuluję, jeśli wynik jest podobny powyższego! Właśnie uruchomiłeś swój pierwszy program w Pythonie.
Jeśli wystąpił błąd, przepisz program tak, żeby wyglądał dokładnie tak, jak powyższy i spróbuj uruchomić go jeszcze raz. Zwróć uwagę na to, że Python rozróżnia wielkość liter, np. print znaczy co innego, niż Print (zauważ wielkie P w drugim). Upewnij się także, że nie ma żadnych spacji ani też znaków tabulacji na początku jakiejkolwiek linii (za moment dowiesz się, dlaczego jest to takie jest istotne).
Zacznijmy od przeanalizowania pierwszej linii naszego programu. Jest to tak zwany komentarz. Wszystko, co znajduje się na prawo od znaku # jest pomijane w trakcie wykonywania programu i przydaje się przede wszystkim jako notatki pozostawione dla przyszłego czytelnika programu. Python nie interpretuje komentarzy.
Program w Pythonie możesz uruchomić wpisując polecenie, nazwę interpretera i pliku, czyli tak jak w naszym poprzednim przykładzie, python witaj.py.
Uwaga o komentarzach
W naszym pierwszym programie po komentarzach następuje polecenie Pythona, które wypisuje na ekran słowa Witaj świecie!. print jest instrukcją (w nowym Pythonie 3.x jest to już funkcja i używa się jej nieco inaczej, ale nie będziemy się tym teraz martwić), a dostarczone przez nas 'Witaj świecie!' jest napisem (ang. string). Wyjaśnimy tę terminologię później.
Przez znaki niewidoczne rozumiemy spację, tabulator itp. Rozpoczęcie od nich linii tekstu nazywamy wcięciem linii. Znaki niewidoczne znajdujące się na początku linii są w języku Python bardzo istotne. Spacje i znaki tabulacji na początku linii są brane pod uwagę przy określaniu stopnia wcięcia danej linii, co z kolei pozwala Pythonowi grupować polecenia. Polecenia, które są tak samo wcięte tworzą blok poleceń.
Powinieneś również zapamiętać, że nieprawidłowe wcięcia pociągają za sobą czasem trudne do znalezienia błędy. Spójrz na ten przykład:
#!/usr/bin/env python # -*- coding: utf-8 -*- # Nazwa pliku: wciecia.py i = 5 print 'Wartość zmiennej to ', i # Błąd! Zauważ spację na początku linii print 'Powtarzam, wartość zmiennej to ', i
Próbując uruchomić powyższy program, prawdopodobnie otrzymasz następujący błąd:
File "wciecia.py", line 4
print 'Wartość zmiennej to ', i # Błąd! Zauważ spację na początku linii
^
IndentationError: unexpected indent
Python podpowiada, że we wskazanej linii znajduje się nieoczekiwane wcięcie. Nie można dowolnie zaczynać nowych bloków poleceń. Sytuacje, w których możesz rozpoczynać nowe bloki, zostaną opisane w następnych rozdziałach.
Miłą konsekwencją stosowania wcięć jest to, że w ten sposób Python zmusza nas do pisania bardziej schludnego kodu, który jest bardziej przejrzysty dla czytelnika.
Wybierz jeden z powyższych sposobów stosowania wcięć i stale używaj tylko tego sposobu.
Proszę wypróbować samodzielnie powyższy przykład
Można spowodować, by program w Pythonie zachowywał się jak każdy inny program, tzn. żeby można było go uruchomić podając jedynie jego nazwę. Zob. TI:Wykonywalne programy w Pythonie.
Jeśli szybko musisz znaleźć opis funkcji lub polecenia Pythona, możesz skorzystać z wbudowanej pomocy (ang. help). Przydaje się to szczególnie podczas używania wiersza poleceń interpretera. Dla przykładu możesz wykonać help('string'), a uzyskasz pomoc odnośnie napisów.
W podobny sposób możesz uzyskać informacje na temat niemal wszystkiego, co dotyczy Pythona. By dowiedzieć się więcej o samej pomocy, użyj help().
$ python Python 2.5.4 (r254:67916, Mar 13 2009, 18:11:09) [GCC 4.1.2] on linux2 Type "help", "copyright", "credits" or "license" for more information. >>> help('print')
Zauważ, że musimy użyć apostrofów i zapytać o 'print', bo print jest tzw. słowem kluczowym i nie może być użyte jako argument do help.
Odnosi się to wyłącznie do użytkowników Linuksa/BSD/Mac OS X, ale użytkownicy Windows również mogą być zainteresowani znaczeniem pierwszej linii programu.
Jeśli chcemy, żeby nasz program w Pythonie zachowywał się jak normalny program, czyli żeby można go było uruchomić wpisując jedynie ścieżkę do pliku, musimy wykonać dwa kroki.
Przede wszystkim na początku pliku (a dokładnie w pierwszej linijce) musimy napisać jakiego interpretera należy system ma użyć do interpretacji tego programu. W tym celu, tak jak w przykładach w tej książce, należy zapisać ścieżkę do interpretera po znakach '#!'.
#!/usr/bin/python <treść programu>
Po drugie,
musimy nadać użytkownikom pliku tzw. prawo do wykonywania, poprzez użycie polecenia chmod.
$ chmod +x witaj.py $ ./witaj.py Witaj !
Komendy chmod używamy, aby zmienić tryb (ang. change mode) pliku przez nadanie prawa wykonywania (execute) wszystkim użytkownikom systemu. Potem wykonujemy program poprzez bezpośrednie podanie lokalizacji pliku - używamy ./ by wskazać, że program znajduje się w aktualnym katalogu.
Jak wynika z powyższego opisu, informacja dla systemu o tym jakiego typu (w jakim języku) jest program, jest tak naprawdę zawarta w jego pierwszej linijce. W takim razie, do czego służy końcówka nazwy programu, '.py'? Okazuje się, że jest to informacja dla użytkownika...
można zmienić nazwę pliku na zwykłe witaj i uruchamiać go przez ./witaj, a program wciąż będzie działać, bo system wie, że musi uruchomić program z użyciem danego interpretera.
Potrafisz już uruchamiać program, jeśli znasz jego dokładne położenie. Co jednak zrobić, gdy chcesz mieć możliwość uruchomienia programu z każdej lokalizacji? Możesz to osiągnąć, umieszczając program w jednym z katalogów zawartych w zmiennej środowiskowej PATH (ścieżka dostępu).
Zawsze, gdy próbujesz uruchomić program przez podanie jego nazwy, system szuka tego programu w każdym z katalogów wymienionych w zmiennej PATH i, w przypadku sukcesu, uruchamia program. Można sprawić, by program był zewsząd dostępny, poprzez umieszczenie go w jednym z tych katalogów.
# Kopiowanie programu do katalogu obecnego w zmiennej PATH $ echo $PATH /usr/local/bin:/usr/bin:/bin:/usr/X11R6/bin:/home/swaroop/bin $ cp witaj.py /home/swaroop/bin/ $ witaj Witaj !
Możemy wyświetlić zawartość zmiennej PATH, używając komendy echo i umieszczając $ przed nazwą zmiennej. Znak $ wskazuje powłoce, że chcemy otrzymać wartość danej zmiennej. Widzimy, że katalog /home/swaroop/bin jest obecny w PATH. Zwróć uwagę, że swaroop jest nazwą użytkownika w systemie. Jaki masz katalog domowy możesz łatwo sprawdzić:
$ cd ~ $ pwd
Jeżeli nie masz jeszcze odpowiedniego prywatnego katalogu w zmiennej PATH, możesz oczywiście tę zmienną rozszerzyć o pożądany katalog. Możesz to zrobić przez wpisanie w powłoce:
export PATH=$PATH:/home/swaroop/mojkatalog
gdzie /home/swaroop/mojkatalog jest katalogiem, który chcesz dodać do zmiennej PATH.
W tym przykładzie dodajemy katalog do zmiennej PATH. Używamy $PATH, by otrzymać dotychczasową wartość i ustawiamy nową wartość przez dodanie pełnej ścieżki do pożądanego katalogu. Zwróć uwagę, że katalogi w zmiennej PATH oddzielane są dwukropkami :, więc my też jeden dodaliśmy. Komenda export oznacza, że programy uruchamiane z aktualnej powłoki powinny móc używać nowej wartości zmiennej PATH.
PATH i kiedy gdziekolwiek po prostu wpiszemy witaj, system odnajdzie nasz program i uruchomi go, korzystając oczywiście z interpretera.
Ważną rzeczą jest zwrócenie uwagi na fakt, że dzięki temu nasz program stał się jakby częścią systemu operacyjnego.
Przykładem stałej dosłownej jest:
Nazywa się ją dosłowną, bo jest dosłowna, tzn. zawsze używasz jej wartości w sposób dosłowny. Przykładowo liczba 2 zawsze reprezentuje samą siebie i nic innego. Jest stałą, ponieważ jej wartości nie można zmienić. Stąd wszystkie stałe tego typu nazywa się stałymi dosłownymi.
Proszę skorzystać z linii poleceń pythona jak ze standardowego kalkulatora.
Wpisywane przez nas liczby to stałe dosłowne. Proszę zwrócić uwagę na wyniki dzielenia 1/2 i 1.0/2
Liczby w Pythonie dzielimy na trzy główne typy: całkowite (ang. integers), zmiennoprzecinkowe (ang. floating point albo krócej: float) i zespolone (ang. complex).
W przypadku liczb zmiennoprzecinkowych część znajdująca się przed znakiem E nazywana jest mantysą. Po znaku E znajduje się wykładnik. Wartość liczby to mantysa razy 10 podniesione do danego wykładnika.
| 52.3E–4 | = 52,3·10–4 |
| –5 + 4j | = –5 + 4i |
| 2.3 – 4.6j | = 2,3 – 4,6i |
| 1j | = i |
Napis (ang. string) to po prostu ciąg znaków.
Napis można zapisać w programie używając apostrofów
'Możecie mnie cytować.'Wszystkie znaki (litery, odstępy, tabulatory) są zachowane w niezmienionej postaci.
Napisy w cudzysłowach zapisuje się tak samo jak w apostrofach.
"Zażółcić gęślą jaźń."Różnica jest taka, że w napisie w apostrofach można łatwo zawrzeć cudzysłów, a w napisie w cudzysłowach, apostrof. Później, w trakcie używania napisu, nie ma żadnego znaczenia to, w jaki sposób został on zapisany.
Za pomocą potrójnych cudzysłowów lub apostrofów możesz oznaczać napisy wielolinijkowe. Co więcej, wewnątrz nich możesz swobodnie używać zarówno cudzysłowów jak i apostrofów. Najlepiej zilustruje to przykład:
'''Litwo, ojczyzno moja, ty jesteś jak zdrowie. Ile cię trzeba cenić, ten tylko się dowie, kto cię stracił. '''
Wyobraź sobie, że chcesz zapisać napis, który zawiera apostrof ('), na przykład Nie lubię Harry'ego. Nie można zapisać 'Nie lubię Harry'ego', bo Python nie zrozumie, gdzie napis ma koniec. Musimy więc wyraźnie zaznaczyć, że ten konkretny apostrof pomiędzy y a e nie oznacza końca napisu. Tutaj z pomocą przychodzą nam właśnie sekwencje specjalne. Za ich pomocą możemy zapisać ów apostrof jako \', z ukośnikiem wstecznym. Zatem nasz napis, poprawnie zapisany, wygląda tak: 'Nie lubię Harry\'ego'.
W tym konkretnym wypadku lepiej jednak jest użyć cudzysłowów do zaznaczenia granic napisu: "Nie lubię Harry'ego". Po to właśnie istnieje możliwość używania zarówno cudzysłowów jak i apostrofów.
A co w przypadku napisów wielolinijkowych? Można użyć potrójnych cudzysłowów, jak pokazaliśmy wyżej. Można także użyć znaków ucieczki, aby zapisać znak specjalny — znak nowej linii. Robimy to w następujący sposób: To jest pierwszy wiersz\nTo jest drugi wiersz.
Oprócz \n istnieje więcej znaków specjalnych, ale w miarę często używa się tylko jeszcze jednego, czyli znaku tabulacji \t. Znak tabulacji zachowuje się trochę jak 8 lub 4 spacje.
Możemy także zrobić rzecz odwrotną, a mianowicie zapisać napis niezawierający znaków nowej linii w wielu wierszach w programie:
"To jest pierwsze zdanie. \
A to drugie."Powyższy przykład jest równoważny napisowi "To jest pierwsze zdanie. A to drugie.". Zauważ, że umieściliśmy spację przed ukośnikiem wstecznym — w przeciwnym wypadku A występowałoby w napisie bezpośrednio po kropce.
Jeżeli musisz stworzyć napis, który nie zawiera żadnych sekwencji specjalnych, a zawiera fragmenty ze wstecznymi ukośnikami, przydatną opcją są napisy surowe (ang. raw). Tworzymy je, poprzedzając napis literą r lub R. Przykład: r"Nowe linie zapisujemy za pomocą \n.".
Unikod (ang. Unicode) jest zestandaryzowanym sposobem zapisu tekstu w różnych językach. Jeśli chcesz zapisać tekst np. w języku polskim, prawdopodobnie wiesz, że najlepiej jest użyć edytora z obsługą unikodu. Python również daje Ci możliwość użycia Unikodu: wystarczy przy zapisie napisu jako przedrostek dodać u lub U, na przykład: u"Ten tekst zapisaliśmy w Unikodzie.". Niezależnie od tego, z jakiego kodowania korzysta dany komputer, nie będzie on miał problemu z poprawnym odczytaniem tekstu zapisanego w unikodzie, więc używaj unikodowego zapisu napisów dla tekstów, które zawierają np. polskie znaki, a mogą być odczytywane przez różne komputery.
W systemie linuks możemy zadeklarować chęć korzystania ze znaków w kodowaniu UTF. Do tego służy nam linijka # -*- coding: utf-8 -*- na początku kodu, która informuje interpreter, że w kodzie mogą się pojawić znaki niestandardowe (teksty zostaną zakodowane w systemie UTF-8). W systemie Windows musimy dopisywać literkę u przed ciągiem znaków zawierających polskie litery. W Linuksie owe u nie przeszkadza, więc chcąc mieć przenośny kod powinniśmy to u dopisywać.
Oznacza to, że jeżeli już raz utworzyłeś napis, nie możesz go zmienić.
Jeżeli umieścimy dwa napisy obok siebie, Python automatycznie je połączy. Spójrz na przykład: 'Jak ' 'Cię zwą?' zostanie automatycznie przekonwertowane do 'Jak Cię zwą?'.
Używanie tylko stałych dosłownych może szybko stać się nudne — potrzebujemy sposobu przechowywania informacji i manipulowania nimi. Do tego właśnie służą zmienne. Są dokładnie tym, co mówi nam ich nazwa, czyli mogą ulegać zmianom, a to oznacza, że możesz w nich przechowywać cokolwiek. Zmienne to nic innego, tylko fragmenty pamięci Twojego komputera, w których przechowywana jest jakaś informacja. W przeciwieństwie do stałych, musisz mieć możliwość dostępu do tych danych, tak więc zmiennym nadajemy nazwy. Najpierw tylko zobaczmy, jakie nazwy możesz nadać zmiennym.
Zmienne są przykładami identyfikatorów. Identyfikatory to nazwy, które nadajemy czemuś do zidentyfikowania tego. Tworząc identyfikatory w Pythonie, musisz trzymać się kilku zasad:
Aby nadać zmiennej jakąś wartość trzeba wartość przypisać do zmiennej. Robi się to przy pomocy operatora = (operatora przypisania). Zwróć uwagę, że jest to zupełnie inny sens znaku „=” niż w matematyce.
Proszę w linii komend wytworzyć na próbę prawidłowe i nieprawidłowe nazwy zmiennych.
Aby użyć zmiennej, wystarczy przydzielić jej jakąś wartość. Nie musimy tej zmiennej deklarować ani nadawać jej konkretnego typu danych.
Teraz zobaczymy, jak w programach używać zmiennych razem ze stałymi dosłownymi.
Zapisz poniższy przykład i uruchom program.
#!/usr/bin/env python # -*- coding: utf-8 -*- # Nazwa pliku: zmienne.py i = 5 print i i = i + 1 print i s = '''To jest napis wielolinijkowy. To jest drugi wiersz.''' print s
$ python zmienne.py 5 6 To jest napis wielolinijkowy. To jest drugi wiersz.
Najpierw przypisujemy stałą dosłowną 5 do zmiennej i za pomocą operatora przypisania (=). Linia ta nazywa się poleceniem, ponieważ zleca ona Pythonowi wykonanie czegoś: w tym przypadku łączymy nazwę zmiennej i z wartością 5.
Następnie, również za pomocą polecenia, wypisujemy wartość zmiennej i na ekran, używając print.
Później dodajemy 1 do wartości przechowywanej w zmiennej i i zapisujemy nowo obliczoną wartość z powrotem do zmiennej i. Potem wypisujemy wartość zmiennej i i, jak się spodziewaliśmy, dostajemy 6.
Analogicznie postępujemy ze zmienną s, której przydzielamy wartość dosłowną napisu i wypisujemy ją.
Napisać program witaj ze zmienną (tzn. niech imię będzie zmienną, a program będzie wypisywał Witaj, imię).
W czasie wykonywania programu możemy podawać mu dane. Jedną z funkcji służących do wprowadzania danych przez użytkownika w czasie wykonywania programu jest raw_input(). Z funkcji tej można skorzystać np. tak: a = raw_input(napis, który program ma wyświetlić). Takie wywołanie funkcji powoduje wyświetlenie w konsoli podanego w argumencie napisu. Następnie funkcja czeka, aż użytkownik napisze coś i zakończy wprowadzanie danych naciśnięciem Enter. Wprowadzony przez użytkownika napis przypisywany jest do zmiennej a.
Napisać program witaj, który najpierw zapyta użytkownika o imię a następnie wypisze Witaj, imię).
Zmienne przechowują wartości różnego rodzaju: są to typy danych. Podstawowe typy danych już poznaliśmy, są to liczby i napisy. W następnych rozdziałach dowiemy się, jak tworzyć nasze własne typy danych za pomocą klas.
Od teraz zapamiętaj, że Python traktuje wszystkie rzeczy użyte w programie jako obiekty. Rozumiemy to w ogólnym sensie. Zamiast mówić "to coś", mówimy "ten obiekt".
Python jest silnie zorientowany obiektowo, co oznacza, że wszystko jest obiektem, włącznie z liczbami, napisami i funkcjami.
Większość poleceń, jak będziesz w przyszłości pisał, będzie zawierała wyrażenia (ang. expressions). Prostym przykładem wyrażenia może być 2 + 3. Wyrażenie możemy podzielić na operatory i operandy.
Operatory to funkcjonalności, które coś robią i są reprezentowane przez symbole, jak +, albo przez specjalne słowa kluczowe. Operatory, jak sama nazwa wskazuje, muszą na czymś operować — tym czymś są operandy. W naszym przypadku operandami są 2 i 3. Spójrz na tabelę przedstawiającą operatory i ich użycie.
Zanim jednak to zrobisz, pozwól, że przypomnę Ci, że możesz obliczyć wartość wyrażenia, bezpośrednio używając linii poleceń interpretera. Spójrz na przykład:
>>> 2 + 3 5 >>> 3 * 5 15 >>>
Proszę przetestować działnie wybranych operatorów z poniższej tabeli
Uwaga: nie musisz od razu przyswajać sobie całej poniższej tabeli. Jest ona duża, ale po to, żeby wszystkie informacje na ten temat były zebrane w jednym miejscu. Można będzie do niej wracać w razie potrzeby, w trakcie późniejszej nauki.
| Operator | Nazwa | Wyjaśnienie | Przykłady | |
|---|---|---|---|---|
| o p e r a c j e a l g e b r a i c z n e |
+ | Plus | Dodaje dwa obiekty. | 3 + 5 daje 8.'a' + 'b' daje 'ab'. |
| – | Minus | Daje liczbę przeciwną do danej liczby bądź odejmuje jedną liczbę od drugiej. | –5.2 daje liczbę ujemną.
50 – 24 daje 26. | |
| * | Mnożenie | Daje iloczyn dwóch liczb bądź zwraca napis powtórzony zadaną liczbę razy. | 2 * 3 daje 6.
'la' * 3 daje 'lalala'. | |
| ** | Potęgowanie | Zwraca x do potęgi y. | 3 ** 4 daje 81 (czyli 3 * 3 * 3 * 3). | |
| / | Dzielenie | Dzieli x przez y. | 4 / 3 daje 1 (ponieważ dzielimy dwie liczby całkowite).
4.0 / 3 oraz 4 / 3.0 daje 1.3333333333333333. | |
| // | Dzielenie całkowite (ang. floor division) | Zwraca wynik dzielenia całkowitego. | 4 // 3.0 daje 1. | |
| % | Dzielenie modulo | Zwraca wynik z dzielenia modulo, czyli po prostu resztę z dzielenia. | 8 % 3 daje 2.
-25.5 % 2.25 daje 1.5. | |
| o p e r a c j e b i t o w e |
<< | Przesunięcie bitowe (ang. shift) w lewo | Przesuwa daną liczbę o zadaną liczbę bitów w lewo (pamiętaj, że liczby są zapisywane w pamięci postaci dwójkowej, czyli przez 0 i 1). | 2 << 2 daje 8. 2 to 10 w zapisie dwójkowym. Przesunięcie w lewo o 2 bity daje 1000, czyli 8 w zapisie dziesiętnym. |
| >> | Przesunięcie bitowe w prawo | Przesuwa daną liczbę o zadaną liczbę bitów w prawo. | 11 >> 1 daje 5. 11 w zapisie dwójkowym to 1011, czyli przesuwając o 1 bit w prawo dostajemy 101, czyli 5 w zapisie dziesiętnym. | |
| & | Iloczyn bitowy (bitowe „i”, ang. bitwise AND) | Zwraca iloczyn bitowy wszystkich odpowiadających bitów podanych liczb. | 5 & 3 daje 1. | |
| | | Suma (alternatywa) bitowa (bitowe „lub”, ang. bitwise OR) | Zwraca sumę bitową wszystkich odpowiadających bitów podanych liczb. | 5 | 3 daje 7. | |
| ^ | Bitowa różnica symetryczna (bitowe „albo”, ang. bitwise XOR) | Zwraca bitową różnicę symetryczną wszystkich odpowiadających bitów podanych liczb. | 5 ^ 3 daje 6. | |
| ~ | Odwrócenie wszystkich bitów (ang. bitwise invert) | Odwrócenie wszystkich bitów x daje –(x+1). | ~5 daje –6. | |
| p r z y r ó w n a n i a |
< | Mniejsze niż | Zwraca wartość logiczną zdania „x mniejsze od y”. Wszystkie operatory porównania zwracają True (prawda) lub False (fałsz). Zwróć uwagę na wielkie litery na początku. | 5 < 3 daje False, a 3 < 5 daje True.
Porównania mogą być dowolnie łączone: 3 < 5 < 7 daje True. |
| > | Większe niż | Zwraca wartość logiczną zdania „x większe od y”. | 5 > 3 daje True. | |
| <= | Mniejsze lub równe | Zwraca wartość logiczną zdania „x mniejsze lub równe y”. | x = 3; y = 6
x <= y daje True. | |
| >= | Większe lub równe | Zwraca wartość logiczną zdania „x większe lub równe y”. | x = 4; y = 3
x >= 3 daje True. | |
| == | Równe | Zwraca wartość logiczną zdania „x równe y”. | x = 2; y = 2; x == y daje True.
x = 'str'; y = 'stR'; x == y daje False. x = 'str'; y = 'str'; x == y daje True. | |
| != | Nie jest równe | Zwraca wartość logiczną zdania „x nie jest równe y”. | x = 2; y = 3
x != y daje True. | |
| o p e r c j e l o g i c z n e |
not | Zaprzeczenie logiczne (logiczne „nie”, ang. boolean NOT) | Jeśli x jest prawdą, zwracane jest False. Jeśli x jest fałszem, zwracane jest True. | x = True; not x daje False. |
| and | Iloczyn logiczny (logiczne „i”, ang. boolean AND) | x and y zwraca x, jeżeli x jest fałszem, w przeciwnym wypadku zwraca y | x = False; y = sin(30)
x and y daje False, ponieważ x jest fałszem. W tym przypadku Python nie oblicza wartości y, ponieważ w trakcie wykonywania iloczynu okazuje się, że lewa strona wyrażenia jest nieprawdą, co oznacza, że na pewno całe wyrażenie jest nieprawdą, niezależnie od wartości logicznej y. Nazywa się to warunkowym obliczaniem wartości wyrażenia (ang. short-circuit evaluation). | |
| or | Suma logiczna (logiczne „lub”, ang. boolean OR) | Jeśli x jest prawdą, zwracane jest x, w przeciwnym wypadku zwracane jest y. | x = 3; y = False
x or y zwraca 3. Warunkowe wyliczanie wartości również tutaj ma zastosowanie. | |
| o p e r c j e n a z b i o r a c h |
| | Suma zbiorów | Zwraca zbiór wszystkich elementów które są w pierwszym zbiorze lub są w drugim zbiorze. | set([1,3,5]) | set([7,3]) daje set([1,3,5,7]). |
| − | Różnica zbiorów | Zwraca zbiór elementów które są w pierwszym zbiorze i nie są w drugim zbiorze. | set([1,3,5]) − set([7,3]) daje set([1,5]). | |
| & | Przecięcie (część wspólna, iloczyn) zbiorów | Zwraca zbiór elementów które są w pierwszym zbiorze i są w drugim zbiorze. | set([1,3,5]) & set([7,3]) daje set([3]). | |
| ^ | Elementy unikalne | Zwraca zbiór zawierający elementy nie będące wspólne dla dwu zbiorów. | set([1,3,5]) ^ set([7,3]) daje set([1,5,7]). | |
| in | Sprawdzenie czy jest elementem. | Zwraca wartość logiczną zdania „x jest elementem zbioru A”. | 3 in set([1,3,5]) daje True. | |
| not in | Sprawdzenie czy nie jest elementem | Zwraca wartość logiczną zdania „x nie jest elementem zbioru A”. | 3 not in set([1,3,5]) daje False. | |
| < | Sprawdzenie czy jest podzbiorem | Zwraca wartość logiczną zdania „A jest podzbiorem zbioru B”. | set([1,3]) < set([7,3]) daje False. set([1,3]) < set([1,7,3]) daje True. | |
| > | Sprawdzenie czy jest nadzbiorem | Zwraca wartość logiczną zdania „A jest nadzbiorem zbioru B”. | set([1,5,3]) > set([7,3]) daje False. set([1,5,3]) > set([1,3]) daje True. | |
| == | Sprawdzenie czy są jednakowe | Zwraca wartość logiczną twierdzenia, że każdy element pierwszego zbioru jest elementem drugiego zbioru i każdy element drugiego zbioru jest elementem pierwszego. | set([1,3,5]) == set([7,3]) daje False. set([1,3,5]) == set([5,3,1]) daje True. | |
| != | Sprawdzenie czy nie są jednakowe | Zwraca wartość logiczną twierdzenia, że pierwszy zbiór nie jest jednakowy z drugim. | set([1,3,5]) != set([7,3]) daje True. |
Nie ma obowiązku przyswojenia całej tabeli operatorów naraz — jej zawartość jest po kolei omawiana w następnych rozdziałach: operacje algebraiczne za moment, operacje logiczne i przyrównania w rozdziale o wykonaniu warunkowym, a operacje bitowe w rozdziale o operacjach bitowych.
Jeśli mamy wyrażenie typu 2 + 3 * 4, co jest wykonywane najpierw: dodawanie czy mnożenie? Znając reguły matematyczne, możemy stwierdzić, ze mnożenie powinno być wykonane w pierwszej kolejności, co oznacza, że operator mnożenia ma wyższy priorytet (ang. precedence) od operatora dodawania. O kolejności wykonywania operacji decyduje tabela priorytetów operatorów.
Jeśli chcesz zmienić kolejność wykonywania działań, również musisz użyć nawiasów. Aby dodawanie zostało wykonane przed mnożeniem, należy napisać na przykład (2 + 3) * 4.
Czasem używa się nawiasów, mimo że nie powoduje to zmiany kolejności wykonywania. Przykładowo można napisać 2 + (3 * 4), żeby podkreślić, że mnożenie jest wykonywane wcześniej. Nie należy z tym przesadzać.
Poniższa tabela daje nam spojrzenie na priorytety operatorów w Pythonie, od najwyższego (wykonywanego najpierw) do najniższego (wykonywany w ostatniej kolejności). W praktyce wygląda to tak, że w wyrażeniu najpierw wykonane zostaną operacje położone wyżej w tabeli, a dopiero potem te z niższych wierszy.
Poniższa tabela jest dość obszerna (pochodzi z dokumentacji Pythona), aby dać pełne spojrzenie na to zagadnienie. Priorytety operatorów są dobrane tak, by były zgodne z przyzwyczajeniami wziętymi z matematyki, a także by można były popularne wyrażenia zapisać bez nawiasów. O tym, czy należy użyć nawiasów w sytuacji gdy nie są konieczne, decyduje czytelność. Jeśli program jest bardziej zrozumiały z nawiasami, to należy ich użyć.
| Operator | Opis |
|---|---|
| (), [], {}, ` ` | wszystkie formy naturalnie zagnieżdzone: konstrukcja krotki, listy i słownika, przekształcenie w napis |
| f(argumenty ...), x[indeks:indeks], x[indeks], x.atrybut | wywołanie funkcji, pobranie wycinka, wydobycie elementu, dostęp do atrybutu. |
| ** | potęgowanie |
| ~x, +x, –x | inwersja bitowa, liczba, liczba przeciwna (operatory jednoargumentowe). |
| *, /, //, % | mnożenie, dzielenie, dzielenie całkowite i dzielenie modulo |
| +, – | dodawanie i odejmowanie (operatory dwuargumentowe) |
| <<, >> | przesunięcia bitowe |
| & | iloczyn bitowy |
| ^ | bitowa różnica symetryczna |
| | | suma bitowa |
| is, is not, in, not in, <, <=, >, >=, !=, == | sprawdzanie identyczności, sprawdzanie przynależności, porównania. |
| not x | zaprzeczenie logiczne |
| and | iloczyn logiczny |
| or | suma logiczna |
| lambda | wyrażenie lambda |
Operatory, z którymi się jeszcze nie zapoznaliśmy, zostaną wyjaśnione w następnych rozdziałach.
Operatory mające taki sam priorytet znajdują się w jednym wierszu powyższej tabeli. Przykładowo, + i – mają taki sam priorytet.
Weźmy wyrażenie -x**1/2. Jak zostanie ono zinterpretowane?
W tym wyrażeniu występują trzy operatory, -, ** i /, oraz trzy operandy, x, 1 i 2. Jeśli zajrzymy do tabelki powyżej, to zobaczymy, że idąc od góry, najpierw pojawia się potęgowanie, potem negacja, a jeszcze później dzielenie.
Wobec tego właściwa kolejność wykonywania jest taka: (-(x**1))/2.
Załóżmy, że chcemy sprawdzić, czy rozwiązania równania kwadratowego, x1, x2, są poprawne i ułożone rosnąco.
delta < 0 or a*x1**2 + b*x1 + c == 0 and a*x2**2 + b*x2 + c == 0 and x1 <= x2
Wyrażenie nie jest zbyt przejrzyste, więc w prawdziwym programie lepiej byłoby tak nie pisać. Niemniej jest poprawne, co widać w poniższym równoważnym zapisie.
( ( delta < 0 ) or ( ((a*x1**2 + b*x1 + c) == 0 ) and ((a*x2**2 + b*x2 + c) == 0 ) and (x1 <= x2) ) )
Szczegółowo:
Operatory są zazwyczaj łączone od lewej do prawej, co oznacza, że operacje z tym samym pierwszeństwem są wykonywane od lewej do prawej. Przykładowo 2 - 3 - 4 jest wykonywane jak (2 - 3) - 4, czyli daje wynik −5.
Wyjątkiem w stosunku do łączności lewostronnej jest operator potęgowania, który jest łączony od prawej do lewej, np. 3**3**3 daje
, a nie
. Czyli potęgowanie w Pythonie zachowuje się tak jak normalnie w matematyce.
x1 < 0 < x2 # lub równoważnie x1 < 0 and 0 < x2
Przypisanie do wielu zmiennych jest traktowane jak przypisanie do wszystkich tych zmiennych tej samej wartości. Np. a = b = c to to samo co a = c i b = c.
Proszę przećwiczyć różne kombinacje operatorów i nawiasów.
Wyrażenia są kombinacjami zmiennych, stałych i łączących je operatorów. Przykładem może być wzór na obliczanie pola lub obwodu jakiejś figury geometrycznej.
Proszę przepisać i uruchomić poniższy przykład
#!/usr/bin/env python # -*- coding: utf-8 -*- # Nazwa pliku: wyrazenia.py dlugosc = 5 szerokosc = 2 pole = dlugosc * szerokosc print 'Pole wynosi', pole print 'Obwód wynosi', 2 * (dlugosc + szerokosc)
$ python wyrazenia.py Pole wynosi 10 Obwód wynosi 14
Długość i szerokość prostokąta są przechowywane w zmiennych o tych samych nazwach. Użyjemy tych zmiennych, aby za pomocą wyrażeń obliczyć pole i obwód tego prostokąta. Przechowujemy wynik wyrażenia dlugosc * szerokosc w zmiennej pole, aby wypisać jej wartość za pomocą polecenia print. W przypadku obwodu bezpośrednio używamy wyrażenia 2 * (dlugosc + szerokosc) w poleceniu print.
Zwróć uwagę na to, że Python jest napisany tak, by ułatwić życie programiście. Mimo, że nie umieściliśmy spacji pomiędzy 'Pole wynosi', a zmienną pole, Python wstawił ją tam za nas. Oddzielenie wypisywanych wartości spacjami to najpowszechniejsza potrzeba programisty, więc tak się dzieje, jeśli poda on pare rzeczy do wypisania.
Proszę napisać program do obliczania pola i obwodu koła.
W programach, które widzieliśmy dotychczas, zawsze występowały ciągi wyrażeń, które Python wiernie odtwarzał w podanej kolejności. Co zrobić, gdy chcemy zmienić tę kolejność? Na przykład chcemy, żeby program podejmował decyzje i robił różne rzeczy, zależnie od sytuacji, jak na przykład wypisywał „Dzień dobry” albo „Dobry wieczór” zależnie od pory dnia.
Uzyskuje się to kontrolując przepływ programu za pomocą wyrażeń, które powodują przeskok i wykonanie instrukcji w innej kolejności niż ta, w której są napisane. Najważniejsze z takich wyrażeń to if (wykonanie warunkowe, przedstawione poniżej) oraz for i while (wykonanie w pętli, przedstawione w następnym rozdziale).
Konstrukcja if jest używana do wyboru jednego z dwóch bloków w zależności od pewnego warunku. Poniżej przedstawiamy składnię tej konstrukcji najpierw w dwóch szczególnych, ale często występujących przypadkac, a na końcu w pełnej wersji. Proszę zwrócić uwagę na dwukropki i wcięcia. Fragment kodu o wyróżniony jednakowym wcięciem nazywamy blokiem.
if warunek:
blok kodu,
który ma być wykonany
jeśli warunek jest prawdziwyif warunek: blok kodu, który ma być wykonany jeśli warunek jest prawdziwy else: blok kodu, który ma być wykonany jeśli warunek jest fałszywy
if warunek1: blok kodu, który ma być wykonany jeśli warunek1 jest prawdziwy elif warunek2: blok kodu, który ma być wykonany jeśli warunek2 jest prawdziwy else: blok kodu, który ma być wykonany jeśli każdy z powyższych warunków jest fałszywy
Chcemy napisać program realizujący następujący algorytm:
Algorytm ten można w pythonie zaimplementować następująco:
#!/usr/bin/python # -*- coding: utf-8 -*- temperatura = 11 if temperatura > 0: print "jest ciepło!"
Python wykonuje wyrażenie będące warunkiem (temperatura > 0). Ponieważ warunek jest prawdziwy, uruchamiamy blok operacji bezpośrednio pod if.
Zmodyfikujmy nieco nasz algorytm:
Algorytm ten można zaimplementować w następujący sposób:
#!/usr/bin/python # -*- coding: utf-8 -*- temperatura = 11 if temperatura > 0: print "jest ciepło!" else: print "uwaga, przymrozek!"
Program został wzbogacony o nowy blok poprzedzony instrukcją else. Jeśli zmienilibyśmy wartość przypisaną do zmiennej temperatura i warunek był fałszywy, wykonałby się właśnie ten blok (pod else).
Sekcja else jest opcjonalna — jeśli jej nie ma, a warunek jest fałszywy, to cała konstrukcja if nie powoduje wykonania żadnych instrukcji (poza wykonaniem warunku).
Proszę napisać program realizujący algorytm:
Uwaga 1: proszę przeanalizować sekwencję warunków
Uwaga: W tym i kolejnym zadaniu przydać się może funkcja raw_input. Dla przypomnieinia: ta funkcja wczytuje jedną linijkę „z klawiatury“ Wywołujemy funkcję raw_input przekazując jej jako argument napis, który zostaje wypisany na ekran jako zachęta dla użytkownika. Po tym jak użytkownik wpisze coś i naciśnie Enter, funkcja raw_input zwraca to, co zostało wpisane, jako napis. Przetwarzamy go na liczbę całkowitą za pomocą int, a następnie zapisujemy w zmiennej. Podsumowując:
licba_wprowadzona = int(raw_input('Podaj liczbę: ' ) )
Algorytm zagadki jest następujący:
Zwróćmy uwagę na fakt, że jeżeli pierwszy i drugi warunek nie jest spełniony (jest fałszywy) to warunek ostatni jest jedyną możliwością i nie musimy go już sprawdzać.
Proszę zaimplementować powyższy algorytm.
Możliwe rozwiązanie (ukryte):
Jak wiadomo, logika rządzi się swoimi prawami. Pozwalają one na ścisłe wyliczanie wartości logicznej zdań — czyli po prostu określenie, czy zdanie złożone z prostszych zdań jest prawdziwe czy też fałszywe. Ponieważ występują tylko dwie wartości i można dla nich pisać równania, to zasady logiki nazywamy algebrą dwuwartościową, lub częściej algebrą Boole'a, od nazwiska osoby która pierwsza sformalizowała ten rachunek.
Dwie wartości, prawdę i fałsz, oznaczamy często jako 1 i 0. W Pythonie jako wartość oznaczającą prawdę można użyć liczby 1 albo specjalnego obiektu True, natomiast fałsz — liczby 0 lub obiektu False. Te specjalne obiekty nazywane są po angielsku booleans, co należy rozumieć jako wartości Boole'a. Nie są niezbędne i zostały wprowadzone po to, by podkreślić kiedy używamy algebry dwuwartościowej, a kiedy zwykłej.
Jeśli mamy dwa zdania a oraz b, ich koniunkcją nazywamy wyrażenie, które jest prawdziwe gdy obywa zdania są prawdziwe. W matematyce oznacza się to przez
. W Pythonie operatorem koniunkcji jest and.
Możemy wypisać działanie operatora na każdą możliwą parę wartości logicznych:
| wartości zdań | wartość ich koniunkcji | |
|---|---|---|
| a | b | a and b |
| True | True | True |
| True | False | False |
| False | True | False |
| False | False | False |
Koniunkcję nazywa się często iloczynem logicznym, bo jeśli oznaczymy prawdę i fałsz przez 1 i 0, to zwykłe mnożenie tych dwóch liczb daje taki sam wynik jak koniunkcja.
Jeśli mamy dwa zdania a oraz b, ich alternatywą nazywamy wyrażenie, które jest prawdziwe gdy przynajmniej jedno z tych zdań jest prawdziwe. W matematyce oznacza się to przez
. W Pythonie operatorem alternatywy jest or.
Ponownie, najłatwiej przedstawić działanie operatora w tabelce ze wszystkimi możliwymi parami wartości logicznych:
| wartości zdań | wartość ich alternatywy | |
|---|---|---|
| a | b | a or b |
| True | True | True |
| True | False | True |
| False | True | True |
| False | False | False |
Alternatywę nazywa się często sumą logiczną, bo jeśli ponownie oznaczymy prawdę i fałsz przez 1 i 0, to zwykłe dodawanie tych dwóch liczb daje taki sam wynik jak koniunkcja, za wyjątkiem 1 + 1. W wypadku sumy logicznej dwóch zdań prawdziwych wynik i tak jest 1, bo w algebrze Boole'a po prostu nie ma większej liczby.
Jeśli mamy zdanie a, jego zaprzeczeniem nazywamy wyrażenie, które jest prawdziwe tylko gdy to zdanie jest fałszywe.
Tutaj tabelka możliwości jest krótsza, bo mamy tylko jedno zdanie:
| wartość zdania | wartość jego zaprzeczenia |
|---|---|
| a | not a |
| True | False |
| False | True |
Kiedy używamy operatorów przyrównania (==, !=, <=, >=, <, >), w wyniku otrzymujemy jedną z dwóch wartości logicznych True / False. Większość innych wyrażeń nie jest prawdziwa lub fałszywa w sensie matematycznym. Na przykład napis "Hello, World" nie ma wartości logicznej w sensie matematycznym. Niemniej w Pythonie, jak też w wielu innych językach programowania, wszystkie obiekty mają wartość logiczną określaną zgodnie z pewnymi ustalonymi regułami. Pozwala to wykorzystać dowolne wyrażenie jako warunek w poleceniu sterujących wykonaniem programu, jak if czy while, bo każde wyrażenie zwraca jakiś obiekt.
W przypadku obiektów które nie są po prostu True ani False, to czy dany obiekt zostanie zinterpretowany jako prawdziwy, czy też jako fałszywy, rządzi się paroma prostymi regułami:
TO DO: http://Link?
Kiedy piszemy if temperatura > 0:... to jest jasne, jak należy rozumieć warunek — jest on spełniony jeśli temperatura jest dodatnia. Wartością wyrażenia stanowiącego ten warunek, tak samo jak każdego wyrażenia z dowolnym z operatorów przyrównania, jest wartość logiczna True lub False. Jeśli jako warunek napiszemy po prostu temperatura, to to wyrażenie daje w wyniku wartość zmiennej temperatura, czyli zapewne liczbę. Na podstawie podanych powyżej reguł możemy określić, że taki warunek jest równoważny warunkowi temperatura != 0.
Jeśli chcemy sprawdzić wartość logiczną, możemy konwersję na wartość logiczną wykonać explicite. Robimy to za pomocą wyrażenia bool(coś).
$ python >>> bool("Hello, World") True >>> bool("") False >>> bool(1) True >>> bool(120) True >>> bool(-120) True >>> bool(0.5) True >>> bool(0) False >>> bool(0.0) False >>> bool(True) True >>> bool(False) False
Jeśli napiszemy wyrażenie zawierające operatory and lub or, często możemy określić wartość logiczną całego wyrażenia bez znajomości wartości logicznej drugiego operandu. Pozwala to na pominięcie części obliczeń, tzw. wykonanie warunkowe (ang. short-circuiting). Mówiąc bardziej konkretnie,
W przedstawionych powyżej tabelkach przedstawiającej działanie operatorów and lub or na wartości logiczne True i False argumentami były wszystkie możliwe kombinacje tych dwóch wartości. Później zostały przedstawione reguły pozwalająca na nadanie wartości logicznej każdemu wyrażeniu. W przypadku gdy jako argumentu w koniunkcji lub alternatywie użyje się obiektu, który nie jest wartością logiczną True albo False, to wynik wyrażenia logicznego też może nie być wartością logiczną.
Tabelki działania operatorów pozostają w mocy, ale aby uwzględnić działanie na nie-booleans i wykonanie warunkowe, do wykonywania operatorów logicznych używa się następujących reguł:
Wyniki działania tych reguł dla wartości True i False dają to co powinny. Natomiast w innych przypadkach pozwalają na budowanie pseudo-zdań logicznych. Np. jeśli prosimy użytkownika o wpisanie pewnego napisu, ale w przypadku gdy wpisał napis pusty chcemy użyć wartości domyślnej, możemy wykorzystać alternatywę:
print (napis or "(podałeś pusty napis)") # napis o długości 0 jest fałszywy, # w takim wypadku zostanie użyty drugi operand
Przedstawione powyżej reguły obliczania koniunkcji i alternatywy są równoważne następującemu przepisowi:
Proszę napisać program do rozwiązywania równania kwadratowego.
Wyrażenie
while warunek: blok
służy do konstrukcji bloku poleceń pętli wykonywanej warunkowo. Jak w przypadku każdego bloku w Pythonie, do bloku po while zaliczają się te linijki, które są wyrównane do tej samej kolumny (mają takie samo wcięcie). Python najpierw sprawdza czy warunek jest spełniony, i jeśli tak, to wykonuje wszystkie wyrażenia zawarte w bloku. Następnie ponownie sprawdza warunek, i jeśli nadal jest spełniony, to ponownie wykonuje wszystkie wyrażenia zawarte w bloku. Pętla jest wykonywana tak długo, jak długo warunek jest prawdziwy.
#!/usr/bin/python # -*- coding: utf-8 -*- liczba = 23 dzialaj = True while dzialaj: strzal = int(raw_input('Wpisz liczbę całkowitą: ')) if strzal == liczba: print 'Gratulacje, zgadłeś ją!' print '(Ale nic nie wygrałeś.)' dzialaj = False # To sprawia, że warunek przestaje być # spełniony elif strzal < liczba: print 'Nie, szukana liczba jest większa od podanej.' else: print 'Nie, szukana liczba jest mniejsza od podanej.' print 'Koniec programu.'
$ python while.py Wpisz liczbę całkowitą: 50 Nie, ta liczba jest mniejsza od szukanej. Wpisz liczbę całkowitą: 22 Nie, ta liczba jest większa od szukanej. Wpisz liczbę całkowitą: 23 Gratulacje, zgadłeś ją! (Ale nic nie wygrałeś.) Koniec programu.
W tym programie gramy w zgadywankę, dopóki użytkownik nie poda prawidłowej odpowiedzi.
Nie ma konieczności ponownego uruchamiania programu po każdym strzale (por. poprzedni rodział). Przesunęliśmy wyrażenia raw_input oraz if do wnętrza pętli while i ustawiliśmy zmienną dzialaj na wartość True przed pętlą while. Najpierw sprawdzamy wartość zmiennej dzialaj. Jeśli jest to True przystępujemy do wykonywania bloku while. Po zakończeniu pracy tego bloku, warunek jest sprawdzany ponownie (w tym wypadku wartość zmiennej dzialaj). Jeżeli jest prawdziwy, pętla uruchamia się ponownie, w przeciwnym wypadku idziemy dalej.
Standardowe wykonanie pętli — warunek spełniony, wykonujemy cały blok, warunek spełniony, wykonujemy cały blok, ..., warunek niespełniony, koniec — nie zawsze jest pożądane. Pętlę można zarówno po prostu przerwać, jak też pominąć pozostałą do wykonania resztę bloku i rozpocząć następny obieg pętli.
Wyrażenia break używamy, aby wyrwać się z pętli, czyli zakończyć jej wykonywanie natychmiast. Przykład powyżej można w sposób równoważny, ale krótszy i czytelniejszy, zapisać z użyciem break.
#!/usr/bin/python # -*- coding: utf-8 -*- liczba = 23 while True: strzal = int(raw_input('Wpisz liczbę całkowitą: ')) if strzal == liczba: print 'Gratulacje, zgadłeś ją!\n(Ale nic nie wygrałeś.)' break elif strzal < liczba: print 'Nie, szukana liczba jest większa od podanej.' else: print 'Nie, szukana liczba jest mniejsza od podanej.' print '\nKoniec programu.'
Wyrażenia continue używamy, aby nakazać Pythonowi ominąć pozostałe wyrażenia w bloku pętli i kontynuować od następnej iteracji tej pętli.
Jeszcze raz ten sam program, tym razem z wykorzystaniem continue:
#!/usr/bin/python # -*- coding: utf-8 -*- # nazwa pliku: zgadywanka.py liczba = 23 while True: strzal = int(raw_input('Wpisz liczbę całkowitą: ')) if strzal > liczba: print 'Nie, ta liczba jest większa od szukanej.' continue if strzal < liczba: print 'Nie, ta liczba jest mniejsza od szukanej.' continue print 'Gratulacje, w końcu ją zgadłeś!' print '(Ale znowu nic nie wygrałeś.)' break print '\nKoniec programu.'
Wyrażenia break i continue są uniwersalne — mogą zostać użyte zarówno w pętli while jak i w pętli for, opisanej poniżej.
#!/usr/bin/env python # -*- coding: utf-8 -*- # Nazwa pliku: continue.py while True: s = raw_input('Wpisz coś: ') if s == 'quit': break if len(s) < 3: print 'Za krótkie.' continue print 'Wpis jest wystarczającej długości.' print 'Liczba słów wpisu:', len(s.split()), '.'
$ python continue.py Wpisz coś: a Za krótkie. Wpisz coś: 12 Za krótkie. Wpisz coś: abc cde Wpis jest wystarczającej długości. Liczba słów wpisu: 2. Wpisz coś: quit
W tym programie prosimy użytkownika o napis, ale zajmujemy się nim jedynie, gdy ma przynajmniej 3 znaki długości. (Zob. też #Jaką długość ma napis zawierający polskie litery? poniżej) Aby poznać długość napisu używamy wbudowanej funkcji len. Jeżeli jest on krótszy niż 3 znaki, wypisujemy komunikat Za krótkie., następnie wyrażenie continue powoduje pominięcie resztę poleceń w bloku pętli. Jeżeli jest on nie krótszy niż 3 znaki, wszystkie pozostałe linie w bloku pętli zostają wykonane.
Wymyśl algorytm, a następnie napisz program, który sumuje liczby dodatnie podawane przez użytkownika.
Zmodyfikuj poprzedni algorytm tak, aby zliczał on ile liczb użytkownik wprowadził. Zatem chcemy mieć algorytm, który sumuje liczby dodatnie podawane przez użytkownika i zapamiętuje ile liczb zostało wprowadzonych.
Zmodyfikuj poprzedni algorytm tak, że pobiera on od użytkownika ciąg liczb, i wypisuje długość i średnią tego ciągu. Przyjmij, że użytkownik wpisał wszystkie liczby wtedy, kiedy na pytanie o następną liczbę,poda liczbę ujemną. po prostu naciśnie Enter, czyli wpisze pustą linijkę.
Zmodyfikuj poprzedni algorytm tak, że pobiera on od użytkownika ciąg liczb, i wypisuje długość i średnią tego ciągu. Przyjmij, że użytkownik wpisał wszystkie liczby wtedy, kiedy na pytanie o następną liczbę po prostu naciśnie Enter, czyli wpisze pustą linijkę.
Wskazówka: Fragment algorytmu potrzebny do obsługi wczytywania:
Przedstawiona powyżej pętla while jest pierwowzorem pętli — na dodatek wygląda i działa niemalże tak samo w różnych językach programowania. Niemniej, na co dzień, używa się raczej innej pętli, bardziej zwięzłej i wygodniejszej w użyciu.
for zmienna in sekwencja: blok
Pętla for służy do wykonania tego samego bloku operacji dla każdego elementu z sekwencji. Oznacza to, że liczba wykonań bloku jest równa długości sekwencji. Zmienna przyjmuje po kolei wartości wszystkich elementów sekwencji. Jednokrotne wykonanie bloku pętli dla aktualnej wartości zmiennej nazywamy iteracją. W żargonie mówimy, że pętla for iteruje po sekwencji obiektów.
Najprostszym przykładem jest wykonanie tej samej operacji dla sekwencji liczb. W pythonie najprostszym przykładem sekwencji jest lista liczb. Można ją wytworzyć wpisując "ręcznie" liczby rozdzielone przecinkami w nawiasie kwadratowym. W powłoce pythona proszę wypróbować następujący kod:
a = [2, 3, 4, 5] print 'sekwencja a:', a
Wygodniejszym sposobem wygenerowania potrzebnej sekwencji liczb jest funkcja range(a,b). Proszę wypróbować:
a = range(2,6) print 'sekwencja a:', a
# cała sekwencja na raz print 'sekwencja:', [2, 3, 4, 5] # sekwencja element po elemencie for i in [2, 3, 4, 5]: print 'element sekwencji:', i
$ python for.py sekwencja: [2, 3, 4, 5] element sekwencji: 2 element sekwencji: 3 element sekwencji: 4 element sekwencji: 5
W tym programie wypisujemy listę liczb na dwa sposoby, za pierwszym razem od razu w całości, a za drugim razem element po elemencie.
Pętla for iteruje po liście w ten sposób, że przypisuje zmiennej i kolejny element listy, a następnie wykonuje blok wyrażeń.
Napisz dwa programy, które wypisują liczby całkowite od 1 do 10. W pierwszym wykorzystaj pętlę while, a w drugim pętlę for.
Napisz dwa programy, które obliczają sumę liczb całkowitych od 1 do 10. W pierwszym wykorzystaj pętlę while, a w drugim pętlę for.
Napisz program który pyta użytkownika o liczbę, a następnie oblicza silnię tej liczby. Wykorzystaj pętlę for.
Oblicz sumę sześcianów liczb naturalnych od 0 do 100.
Napisz program który sprawdzi, sześciany ilu liczb naturalnych (od 0) trzeba zsumować, by uzyskać liczbę większą niż 106.
Cechą wspólną wielu różnych kolekcji obiektów jest możliwość ponumerowania w określony sposób elementów i iteracji po nich, czyli dostępu do każdego z nich po kolei. Dzięki temu możemy np. iterować po sekwencji znaków, albo po sekwencji linii w pliku, albo po sekwencji liczb.
Najprostszy przykład sekwencji to lista wpisana bezpośrednio w tekście programu.
Drugi przykład, to ciąg znaków. Naturalnie myślimy o napisie jako o sekwencji znaków/liter.
Trzeci przykład, to funkcja range(a, b) generująca sekwencję liczb od a do b.
Sekwencje uzyskujemy też jako wynik wywołania wielu innych funkcji.
Sekwencję liczb generujemy używając wbudowanej funkcji range. Podajemy dwie liczby, a funkcja range zwraca nam sekwencję liczb począwszy od pierwszej, a kończąc przed drugą z nich. Na przykład range(2,6) tworzy sekwencję [2,3,4,5]. Pierwszy argument range jest opcjonalny, jeśli go się nie poda, to sekwencja zaczyna się od 0. Na przykład wywołanie range(N) zwraca sekwencję N elementów: od 0 do N−1.
Domyślnie, range wytwarza sekwencję liczb zwiększających się co 1. Trzeci, opcjonalny argument range jest wartością o jaką będą zwiększane kolejne liczby w sekwencji. Na przykład range(1,5,2) zwróci [1,3].
Funkcja range tworzy od razu całą wymaganą listę. Jeśli lista jest długa, jest to niepotrzebny narzut, bo możemy przerwać wykonywanie pętli przedwcześnie, nie używając wszystkich elementów. Dlatego w wyrażeniu for należy używać zamiennika xrange, który zachowuje się tak samo, ale generuje swoje elementy w miarę potrzeby w trakcie wykonywania pętli.
Potrzebujemy ciągu pierwszych 100 liczb parzystych. Piszemy
range(0, 2*100, 2)
Metoda split dzieli ciąg znaków na słowa — tj. zwraca listę ciągów znaków dzieląc w miejscach gdzie występują spacje.
# Plik slowa.py for slowo in 'ala ma kota'.split(): print slowo
$ python slowa.py ala ma kota
Pętla for...in działa dla każdej sekwencji. Tutaj mieliśmy sekwencje liczb, znaków lub napisów, ale tak naprawdę możemy użyć dowolnego rodzaju sekwencji z dowolnym rodzajem obiektów! Przyjrzymy się temu w następnych rozdziałach.
Pythonowa pętla for radykalnie różni się od pętli for w C/C++. Programujący w C# zauważą, że Pythonowa pętla for jest podobna do pętli foreach w C#. Programujący w Java zauważą, ze jest podobna do pętli for(Object i : Iterable) w Java 1.5. W C/C++ pisze się for(int i = 0; i < 5; i++), zaś w Pythonie jedynie for i in range(0, 5).
Blok else jest wywoływany gdy warunek pętli while przyjmuje wartość False lub gdy pętla for for przebiegnie całą sekwencję. To może się wydarzyć nawet za pierwszym razem, jeśli warunek jest od początku fałszywy albo sekwencja pusta. Jeżeli istnieje sekcja else dla pętli while, jest ona zawsze uruchamiana, z wyjątkiem sytuacji, gdy wyjdziesz z pętli poleceniem break.
Chcemy sprawdzić co jest mniejsze: liczba 10 czy piąta w kolejności liczba podzielna przez 3?
# Co jest mniejsze: 10 czy piąta w kolejności liczba podzielna przez 3? # plik wyścig.py podzielnych = 0 i = 1 while i < 10: if i % 3 == 0: print i, "jest podzielna przez 3" podzielnych += 1 if podzielnych == 5: print "piąta liczba podzielna przez 3 jest mniejsza" break i += 1 else: print "doszliśmy do 10" print "10 jest mniejsze"
$ python wyścig.py 3 jest podzielna przez 3 6 jest podzielna przez 3 9 jest podzielna przez 3 doszliśmy do 10 10 jest mniejsze
Zauważ, że gdy wyjdziesz z pętli for albo while używając break, blok else nie zostanie wywołany.
Zobaczmy jeszcze jeden przykład, wydawałoby się niemalże trywialny, na wykorzystanie pętli while...
#!/usr/bin/python # -*- coding: utf-8 -*- # Nazwa pliku: break.py while True: s = raw_input('Wpisz coś: ') if s == 'quit': break else: print 'Długość tego ciągu znaków to:', len(s) print 'Koniec programu.'
$ python break.py Wpisz coś: Programming is fun Długość tego ciągu znaków to: 18 Wpisz coś: When the work is done Długość tego ciągu znaków to: 21 Wpisz coś: if you wanna make your work also fun: Długość tego ciągu znaków to: 37 Wpisz coś: use Python! Długość tego ciągu znaków to: 15 Wpisz coś: quit Koniec programu.
W tym programie wielokrotnie prosimy użytkownika o napisanie czegokolwiek i wypisujemy długość tego ciągu znaków na ekranie. Dodaliśmy specjalny warunek stopu poprzez sprawdzanie, czy użytkownik wpisał quit. Jeżeli tak, zatrzymujemy pętlę poleceniem break i wykonujemy końcową część programu.
Długość ciągu znaków sprawdzamy wbudowaną funkcją len.
I czy jest w tym coś podchwytliwego? Spróbujmy jeszcze raz:
$ python break.py Wpisz coś: pyton Długość tego ciągu znaków to: 5 Wpisz coś: wąż Długość tego ciągu znaków to: 5 # dlaczego akurat 5 a nie 3?
Jeśli zapytasz o długość ciągu znaków zawierającego polskie znaki diakrytyczne („ą”, „ć”, „ę”, „ł”, „ń”, „ó”, „ś”, „ź”, „ż” oraz „Ą”, i inne wielkie), uzyskana odpowiedź może różnić się od oczekiwań!
$ python >>> print len('aaa') 3 >>> print len('ąęó') 6 #wynik może być różny w zależności od systemu: zazwyczaj 6 lub 3 lub 12
Aby wyjaśnić co się tutaj dzieje, trzeba by zacząć od podstaw, czyli stron kodowych i standardu Unicode. Temat jest ciekawy, ale by wyjaśnić powyższy wynik, wystarczy powiedzieć, że ciąg znaków 'ąęó' jest rozumiany przez Pythona jako ciąg znaków z bardzo ograniczonego zestawu ASCII. Jeśli używasz w miarę współczesnego systemu operacyjnego, to taka sekwencja znaków zostaje podana Pythonowi jako ciąg znaków ze znacznie szerszego zestawu Unicode. Znaki ASCII są zapisywanie po jednym na bajt, a że znaków Unicode jest znacznie więcej, to potrzeba na nie więcej miejsca. Ile dokładnie miejsca potrzeba na polskie znaki diakrytyczne zależy od systemu, ale najczęściej są to dwa bajty na każdy. Ostatecznie Python otrzymuje ciąg bajtów, które usiłuje interpretować jako jednobajtowe znaki ASCII. Rezultat jest taki, że otrzymujemy ilość bajtów zajmowanych przez ciąg znaków, a nie długość tego ciągu.
Nie tylko zliczanie znaków działa źle, pętla for też się gubi:
$ python >>> for znak in 'ąęó': ... print znak ... � # znak oznaczający 'zepsuty' znak � � � � �
Aby zaradzić temu zamieszaniu (np. bo nie chcemy żeby program dawał inny wynik w zależności od systemu na którym jest wykonywany) mamy proste wyjście. Musimy poinformować Pythona, że dany ciąg znaków jest ciągiem znaków Unicode. W tym celu ciąg przed otwierającym cudzysłowem wstawiamy literkę 'u':
$ python >>> print len(u'aaa') 3 >>> print len(u'ąęó') 3 # zgodnie z oczekiwaniem >>> for znak in u'ąęó': ... print znak ... ą ę ó
Niestety takie rozwiązanie jest pracochłonne dla programisty, a na dodatek łatwo o pomyłkę, gdy gdzieś zapomnimy przedrostka. Nowe wersje Pythona (3.0 i późniejsze) rozwiązują ten problem prościej — ciągi znaków to domyślnie Unicode i nie ma w ogóle przedrostka 'u'.
Zobaczyliśmy, jak używać trzech wyrażeń kontroli przepływu — if, while oraz for, razem z powiazanymi z nimi poleceniami break i continue. Są one jednymi z najczęściej używanych części Pythona i w związku z tym obeznanie się z nimi jest niezbędne.
W następnym rozdziale dowiemy się jak tworzyć funkcje i nimi operować.
Napisz program który zapyta użytkownika o liczbę, a następnie wyliczy dziesiąty element (wyraz a9) ciągu an = 3,9 · an–1 · (1 – an–1) zaczynając od wprowadzonej przez użytkownika liczby jako a0.
Potem popraw program tak, by sprawdzał czy wprowadzona liczba zawiera się w przedziale [0, 1] i pytał ponownie, aż do uzyskania liczby z tego przedziału.
Napisz program który wypisze elegancką tabliczkę mnożenia od 1 do 10.
Np.
1 2 3 4 5 6 7 8 9 10 2 4 6 8 10 12 14 16 18 20 3 6 9 12 15 18 21 24 27 30 4 8 12 16 20 24 28 32 36 40 5 10 15 20 25 30 35 40 45 50 6 12 18 24 30 36 42 48 54 60 7 14 21 28 35 42 49 56 63 70 8 16 24 32 40 48 56 64 72 80 9 18 27 36 45 54 63 72 81 90 10 20 30 40 50 60 70 80 90 100
Potem zmodyfikuj swój program tak, żeby użytkownik mógł podać zakres w jakim tabliczka ma być narysowana.
Odpowiedz na list nieszczęśliwego użytkownika Pythona.
Witam, uczę się Pythona i mam kłopot z takim kawałkiem programu:
a = raw_input('Podaj liczbę: ') if a > 0: print 'dodatnia' elif a == 0: print 'zero' elif a < 0: print 'ujemna'
Program działa, ale zawsze pisze "dodatnia" czy wpiszę liczbę dodatnią, ujemną czy zero. Wydaje mi się, że rozwiązanie problemu jest proste, ale nie mogę go znaleźć ;-)
Z góry dziękuję
Funkcje to fragmenty programu, które można używać wielokrotnie. Możemy nadać nazwę jakiejś grupie instrukcji i uruchamiać ją używając tej nazwy w dowolnym miejscu programu tyle razy ile chcemy. Operację taką nazywamy wywołaniem funkcji. Tak naprawdę używaliśmy już wielu wbudowanych funkcji takich jak len czy range. Funkcje są prawdopodobnie najważniejszym składnikiem budującym każdy program (w dowolnym języku programowania), musimy więc poznać je dokładniej.
Funkcje definiujemy w programie używając słowa kluczowego def. Po tym słowie następuje nazwa funkcji oraz para nawiasów, które mogą zawierać nazwy zmiennych (parametrów funkcji), a linia kończy się dwukropkiem. Parametry służą do przekazywania funkcji różnych wartości tzw. wejściowych i uzyskiwania zależnych od nich wyników. Potem następuje blok linii programu, które tworzą funkcję. Poniżej jest przedstawiony przykład definicji i wywołania funkcji.
#!/usr/bin/python # -*- coding: utf-8 -*- # Nazwa pliku: function1.py # początek definicji funkcji def przywitajSie(): print 'Sie ma!' # instrukcje tworzące funkcję # koniec definicji funkcji przywitajSie() # wywołanie (użycie) funkcji przywitajSie() # ponowne wywołanie funkcji
Sie ma! Sie ma!
Definiujemy funkcję o nazwie przywitajSie w sposób opisany powyżej. Ponieważ funkcja ta nie używa żadnych parametrów, nie ma więc żadnych zmiennych wewnątrz nawiasów. Zauważ, że możemy wywołać tę samą funkcję dwukrotnie co oznacza, że nie musimy dwa razy pisać tego fragmentu programu.
Jak wiemy, funkcja może mieć parametry wywołania. Są to wartości, które dostarczamy funkcji, i których możemy użyć wewnątrz funkcji. Parametry zachowują się tak samo jak zmienne z tą różnicą, że ich wartości są nadawane w momencie wywołania funkcji.
Parametry wpisuje się w nawiasach podczas definiowania funkcji oddzielając je przecinkami. Kiedy wywołujemy funkcję, ich wartości podajemy w taki sam sposób. Uwaga: nazwy, które podaliśmy podczas definiowania funkcji nazywamy parametrami, ale wartości podane przy wywołaniu funkcji nazywamy argumentami.
#!/usr/bin/python # -*- coding: utf-8 -*- # Nazwa pliku: func_param.py def printMax(a, b): if a > b: print a, 'jest większe' elif a == b: print a, 'jest równe', b else: print b, 'jest większe' printMax(3, 4) # bezpośrednio podane wartości x = 5 y = 7 printMax(x, y) # zmienne podane jako argumenty
4 jest większe 7 jest większe
Definiujemy tutaj funkcję o nazwie printMax, która używa dwóch parametrów o nazwach a i b. Funkcja ma znaleźć większą z liczb używając instrukcji if..else a następnie wypisać, która liczba jest większa. Podczas pierwszego użycia funkcji printMax podaliśmy bezpośrednio liczby jako argumenty. Przy drugim wywołaniu jako argumentów użyliśmy zmiennych. Instrukcja printMax(x,y) przypisuje wartość pierwszego argumentu (zmiennej x) do parametru a oraz wartość drugiego argumentu (zmiennej y) do parametru b. Samo wyznaczenie większej liczby i jej wypisanie odbywa się wewnątrz funkcji tak samo za każdym razem.
Jeśli wewnątrz funkcji zadeklarujemy zmienne, nie mają one żadnego związku ze zmiennymi poza funkcją, nawet jeśli mają takie same nazwy. Mówimy, że nazwy zmiennych wewnątrz funkcji są lokalne dla tej funkcji. Obszar, w którym możemy danej zmiennej używać nazywamy zasięgiem zmiennej. Każda zmienna zdefiniowana w funkcji ma zasięg poczynając od miejsca, w którym po raz pierwszy przypiszemy jej wartość, aż do końca funkcji.
#!/usr/bin/python # -*- coding: utf-8 -*- # Nazwa pliku: func_local.py x = 50 def func(x): print 'x wynosi', x x = 2 print 'Zmieniono lokalne x na', x func(x) print 'x ciągle wynosi', x
x wynosi 50 Zmieniono lokalne x na 2 x ciągle wynosi 50
Kiedy po raz pierwszy wypisujemy wartość zmiennej x, w pierwszej linii funkcji func, użyta zostaje wartość parametru zadeklarowanego w bloku głównym, powyżej definicji funkcji.
Następnie przypisujemy zmiennej x wartość 2. Nazwa x jest lokalna dla funkcji, tak więc gdy zmieniamy wartość x wewnątrz funkcji, zmienna x zdefiniowana w bloku głównym pozostaje niezmieniona.
W momencie ostatniego wywołania polecenia print wypisujemy wartość zmiennej x zdefiniowanej w bloku głównym potwierdzając, że nie została ona zmieniona przez przypisanie nowej wartości lokalnej zmiennej x wewnątrz poprzednio wywołanej funkcji.
Wewnątrz funkcji, odczytywać zmienne zdefiniowane na wyższym poziomie można bez problemu. Jeśli natomiast chcemy przypisywać wartość do takiej zmiennej, zdefiniowanej poza zakresem danej funkcji, to trzeba tą zmienną określić jako globalną. Służy do tego deklaracja global. Bez użycia tej deklaracji nie jest możliwe przypisanie wartości zmiennej zdefiniowanej na zewnątrz funkcji — jedyny efekt takiego przypisania to utworzenie lokalnej zmiennej o tej nazwie, która zasłania zmienną globalną.
Należy powiedzieć precyzyjnie, co tutaj znaczy słowo globalna. W Pythonie zmienną globalną nazywa się zmienną zdefiniowaną bezpośrednio w jakimś module, a nie wewnątrz funkcji czy klasy w tym module. Takie znaczenie jest zupełnie inne niż np. w C, gdzie zmienna globalna o danej nazwie w całym programie może być tylko jedna.
#!/usr/bin/python # -*- coding: utf-8 -*- # Nazwa pliku: func_global.py x = 50 def func(): global x print 'x wynosi', x x = 2 print 'Zmieniono zewnętrzny x na', x func() print 'x wynosi teraz', x
x wynosi 50 Zmieniono zewnętrzny x na 2 x wynosi teraz 2
Deklaracja global została użyta do poinformowania, że chodzi nam o globalną, zewnętrzną zmienną x. Jeśli teraz przypisujemy wartość zmiennej x wewnątrz funkcji, zmieniamy wartość x z głównego bloku.
Można określić więcej niż jedną zmienną globalną w tej samej deklaracji global, na przykład: global x, y, z.
Wyskakując na moment do rozdziału o modułach, należy wspomnieć, że do do zmiennych zdefiniowanych „gdzie indziej“ można się odwoływać podając nazwę modułu który je zawiera. Na przykład zmienna argv zdefiniowana w module sys jest z poza tego modułu dostępna jako sys.argv. Dlatego atrybut global służy do dostępu do zmiennych zdefiniowanych na poziomie modułu, ale tylko z wewnątrz tego modułu.
W przypadku pewnych funkcji nie zawsze chcemy podawać argumenty odpowiadające wszystkim parametrom określonych w definicji funkcji. Parametry mogą być „opcjonalne“. Nie musimy wtedy ich podawać w wyłaniu funkcji, i jeśli tego nie zrobimy zostaną użyte wartości domyślne. Wartości domyślne definiujemy dopisując w definicji funkcji po nazwie parametru znak = i wartość domyślną.
#!/usr/bin/python # -*- coding: utf-8 -*- # Nazwa pliku: func_default.py def napisz(wiadomosc, razy=1): print wiadomosc * razy napisz('Dzień') napisz('dobry', 5)
Dzień dobrydobrydobrydobrydobry
Funkcja o nazwie napisz ma za zadanie wypisać tekst tyle razy ile podano. Jeśli nie podamy ile razy, wtedy domyślnie tekst wypisywany jest jeden raz. Osiągamy to podając wartość domyślną argumentu dla parametru razy równą 1.
Podczas pierwszego wywołania napisz podajemy tylko tekst i wypisywany jest on jeden raz. Przy drugim wywołaniu napisz podajemy zarówno tekst jak i argument 5 mówiący, że chcemy napisać tekst 5 razy.
Jeśli chcemy skorzystać z funkcji o dużej liczbie parametrów, wygodne może być jawne użycie nazw parametrów. Działa to tak, że w wywołaniu funkcji podajemy zarówno nazwę parametru, jak i, tak jak poprzednio, właściwy argument. Podanie wartości danego parametru poprzez taki nazwany argument (po angielsku argumenty takie nazywa się keyword arguments), powoduje, że jego wartość w wywołaniu funkcji jest ustalona i zostaje on wyłączony z normalnego przypisywania argumentów do parametrów według kolejności.
Rozpoznawanie argumentów po nazwie, a nie kolejności, ma dwie zalety:
#!/usr/bin/python # -*- coding: utf-8 -*- # Nazwa pliku: func_key.py def func(a, b=5, c=10): print 'a wynosi', a, 'oraz b wynosi', b, 'oraz c wynosi', c func(3, 7) func(25, c=24) func(c=50, a=100)
a wynosi 3 oraz b wynosi 7 oraz c wynosi 10 a wynosi 25 oraz b wynosi 5 oraz c wynosi 24 a wynosi 100 oraz b wynosi 5 oraz c wynosi 50
Funkcja o nazwie func ma jeden parametr bez wartości domyślnej, oraz dwa parametry posiadające wartości domyślne.
Użycie argumentu nazwanego nazwą która nie odpowiada żadnemu parametrowi jest błędem.
Czasami wygodnie byłoby zdefiniować funkcję, która przyjmowałaby dowolną liczbę parametrów, tak jak polecenie print. Można to uzyskać dodając na końcu listy parametrów zmienną, do której zostanie wpisana lista „nadmiarowych“ argumentów. Ten specjalny parametr jest oznaczony przez gwiazdkę przed jego nazwą.
>>> def powypisuj(rozdzielacz, pierwsze, *reszta) ... """Wypisuje podane słowa używając pierwszego argumentu ... jako rozdzielacza.""" ... print pierwsze ... for slowo in reszta: ... print rozdzielacz, slowo, ... ... >>> powypisuj('-x-', 'ala', 'ma', 'kota') ala-x-ma-x-kota >>> powypisuj('-x-', 'ala') ala >>> powypisuj('-x-') Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: powypisuj() takes at least 2 arguments (1 given)
Argumenty nazwane nie mogą występować tylko na końcu listy argumentów, lub patrząc od drugiej strony, argumenty nienazwane muszą znajdować się na początku. Podane argumenty są przyporządkowywane parametrom w ten sposób, że najpierw argumenty bez zadanej nazwy parametru zostają przydzielone do odpowiednich parametrów w kolejności, a potem argumenty nazwane zostają przydzielone do odpowiednich parametrów według nazwy. Tak więc, jeśli argumentów jest więcej niż parametrów „bez gwiazki“, to trafią one do zmiennej „z gwiazdką“.
Błędem jest jeśli jakiemuś parametrowi zostaną przyporządkowane dwa argumenty &mdash na przykład jeden zwykły, a drugi nazwany.
Istnieje jeszcze jeden sposób przekazywania zmiennej liczby parametrów, w formie słownika. Ponieważ słowniki będą omówione dopiero w rozdziale o strukturach danych, to reszta tego podrozdziału może być niezrozumiała. Jeśli tak, to nie przejmuj się i najpierw poczytaj o słownikach.
Funkcja może mieć jeszcze jeden specjalny parametr — poprzedzony dwoma gwiazkami, zwyczajowo nazywający się **kwargs. W wywołaniu takiej funkcji można użyć argumentów nazwanych, których nazwy nie odpowiadają żadnym parametrom funkcji. Te argumenty zostają wstawione do słownika, który zostaje przekazany jako parametr kwargs do funkcji. W tym słowniku, nazwy argumentów są kluczami, a ich wartości wartościami przypisanymi do tych kluczy.
>>> def f(*args, **kwargs): ... print 'args =', args ... print 'kwargs =', kwargs ... >>> f(1, 2, 3, a=4, b=5, c=6) args = (1, 2, 3) kwargs = {'a': 4, 'b': 5, 'c': 6}
Dotychczas rozważane funkcje wykonywały pewne polecenia ale nie oczekiwaliśmy od nich aby zwracały jakieś wartości (tzn. nie potrzebowaliśmy aby wartości wyliczone albo obiekty wytworzone w funkcji były dostępne w miejscu, z którego wywołaliśmy funkcję).
Polecenie return jest używane do powrotu z funkcji czyli natychmiastowego przerwania jej wykonania i zwrócenia wartości.
#!/usr/bin/python # -*- coding: utf-8 -*- # Nazwa pliku: func_return.py def maksimum(x, y): if x > y: return x else: return y print maksimum(2, 3)
3Funkcja maksimum zwraca większy ze swoich parametrów, czyli liczb przekazanych do funkcji. Do znalezienia większej z liczb użyta jest instrukcja if..else, po znalezieniu ta wartość jest zwracana.
Każda funkcja domyślnie zawiera instrukcję return None na końcu. Można to zaobserwować wywołując print jakasFunkcja(), gdzie funkcja jakasFunkcja nie używa instrukcji return, na przykład:
>>> def jakasFunkcja(): ... pass ... >>> print jakasFunkcja() None
Instrukcja pass jest w języku Python używana do oznaczenia pustego bloku instrukcji.
Uwaga:
Istnieje wbudowana funkcja max, która wyszukuje większą z liczb. Jeśli tylko to możliwe należy używać funkcji wbudowanych, a nie pisać własne.
W Pythonie istnieje wygodna możliwość uzupełniania programu o objaśnienia–dokumentację. W ogólności programista może umieszczać swoje uwagi jako komentarze (wykorzystując znak #), które są po prostu ignorowane w trakcie wykonywania programu, lub jako napisy wstawione na początku definicji funkcji (wykorzystując znaki normalnie używane do ograniczania napisów, czyli ' i "). Ta druga forma ma tę przewagę, że mimo iż w trakcie normalnego wykonywania programu jest pomijana podobnie jak zwykłe komentarze, to te napisy zostają także zachowane od specjalnej zmiennej i można je obejrzeć. W trybie interaktywnej pracy z Pythonem służy do tego funkcja help.
Linie tekstu z dokumentacją nazywają się po angielsku documentation strings, co skraca się do docstrings. Przez docstring rozumie się tylko taki napis umiejscowiony na początku funkcji (lub klasy czy modułu, mechanizm jest ten sam) stanowiący dokumentację.
#!/usr/bin/python # -*- coding: utf-8 -*- # Nazwa pliku: func_doc.py def printMax(x, y): '''Konwertuje obydwa obiekty na int i wypisuje większą z dwóch liczb. Niepowodzenie konwersji powoduje rzucenie wyjątku ValueError. ''' x = int(x) # konwersja do liczby całkowitej (integer), jeśli możliwa y = int(y) if x > y: print x, 'jest większa' else: print y, 'jest większa' printMax(3, 5) print(printMax.__doc__)
5 jest większa Konwertuje obydwa obiekty na int i wypisuje większą z dwóch liczb. Niepowodzenie konwersji powoduje rzucenie wyjątku ValueError.
Napis w pierwszym logicznym wierszu funkcji staje się docstring-iem dla tej funkcji. Dodajmy, że DocStrings również stosują się do modułów i klas, o których będzie mowa później.
Konwencją stosowaną dla dokumentacji jest napis w kilku wierszach. Pierwszy wiersz zaczyna się wielką literą i kończy kropką. Następny wiersz jest pusty, a w trzecim wierszu rozpoczynamy dokładniejsze objaśnienia. Jest bardzo wskazane, aby stosować się do tej konwencji opisując napisane przez siebie funkcje.
Możemy odczytać dosctring jakiejś funkcji (na przyklad printMax) używając jej atrybutu (czyli nazwy należącej do funkcji) __doc__ (zauważ użycie po dwóch znaków podkreślenia). Atrybuty są częścią obiektów, którymi są też funkcje. O obiektach będzie mowa w następnych rozdziałach.
Używając pomocy help() w Pythonie robimy użytek z docstringow. Funkcja help() pobiera z funkcji jej atrybut __doc__ i wypisuje go. Możesz to wypróbować na powyższej funkcji — napisz w swoim programie help(printMax). Pamiętaj, żeby nacisnąć klawisz q, żeby zakończyć pomoc help.
Automatyczne narzędzia programistyczne pobierają dokumentację z twoich programów właśnie w taki sposób. Dlatego dobrze jest używać docstrings do każdej napisanej funkcji. Komenda pydoc, dostępna w dystrybucji języka Python działa podobnie do funkcji help() używając docstrings.
Nawet krótka dokumentacja funkcji może być bardzo pomocna. Co powinno się w niej znaleźć?
Docstrings to ważne narzędzie, którego warto używać, bo dzięki dokumentacji program jest łatwiejszy do zrozumienia.
Dowiedziałeś się, jak używać wielokrotnie fragmentu kodu programu dzieląc go na funkcje. A w jaki sposób można używać funkcji zdefiniowanych w innym miejscu? Jak pewnie zgadłeś, odpowiedzią są moduły.
Są różne metody pisania modułów, ale najprościej jest stworzyć plik z rozszerzeniem .py, który będzie zawierał funkcje i zmienne.
Moduły do Pythona można pisać nie tylko w Pythonie. Można, na przykład, napisać moduł do wydajnych obliczeń numerycznych w bardziej wydajnym języku C i po skompilowaniu używać go w swoim pythonowym kodzie.
Aby użyć zawartości modułu, należy go zaimportować. Dotyczy to również modułów ze standardowej biblioteki Pythona, od czego właśnie zaczniemy.
#!/usr/bin/env python # -*- coding: utf-8 -*- # Nazwa pliku: uzycie_sys.py import sys print 'Polecenia linii komend to:' for i in sys.argv: print i print '\n\nPYTHONPATH to:',sys.path,'\n'
$ python uzycie_sys.py my jestesmy argumentami Polecenia linii komend to: uzycie_sys.py my jestesmy argumentami PYTHONPATH to: ['/home/Swaroop', '/usr/lib/python25.zip', '/usr/lib/python2.5', '/usr/lib/python2.5/plat-linux2', '/usr/lib/python2.5/lib-tk', '/usr/lib/python2 .5/lib-dynload', '/usr/local/lib/python2.5/site-packages', '/usr/lib/python2.5/s ite-packages', '/usr/lib/python2.5/site-packages/Numeric', '/usr/lib/python2.5/s ite-packages/PIL', '/usr/lib/python2.5/site-packages/gst-0.10', '/var/lib/python -support/python2.5', '/usr/lib/python2.5/site-packages/gtk-2.0', '/var/lib/pytho n-support/python2.5/gtk-2.0']
PYTHONPATH to lista katalogów, w których Python poszukuje modułów. W IDLE listę taką można obejrzeć klikając: File->Path Browser.
Najpierw importujemy moduł sys używając polecenia import sys, czyli mówimy Pythonowi, że chcemy go używać. Moduł sys zawiera polecenia związane z Pythonem i jego środowiskiem, czyli systemem.
Podczas wykonywania polecenia import sys Python szuka pliku lub katalogu o odpowiedniej nazwie (m.in. zaczynającej się od sys). Aby operacja się powiodła, moduł musi być w jednym z katalogów wymienionych w zmiennej sys.path.
Po znalezieniu modułu treść jego głównego bloku zostaje wykonana, a funkcjonalność dostarczana przez moduł staje się dostępna w programie. Opisana inicjalizacja modułu następuje tylko raz, podczas jego pierwszego importu w danym programie.
Do zmiennych i funkcji zdefiniowanych w module dostajemy się korzystając z notacji z kropką. Na przykład, zmienna argv w module sys jest dostępna jako (sys.argv), dzięki czemu wiadomo, że ta nazwa jest częścią modułu sys. Zaletą tej notacji jest możliwość zdefiniowania zmiennych o tej samej nazwie w różnych modułach bez utraty jednoznaczności. Możemy mieć wiele zmiennych argv w różnych modułach, i jednoznacznie się do niech odwoływać.
Zmienna sys.argv jest listą ciągów znaków. (Listy poznamy dokładniej w następnym rozdziale.) Dokładniej mówiąc, zawiera listę argumentów, z którymi program został wywołany, czyli nazwę pliku zawierającego program oraz to, co użytkownik programu wpisał po nazwie w wywołaniu z wiersza poleceń.
Gdy wpisujemy python uzycie_sys.py my jestesmy argumentami, uruchamiamy moduł uzycie_sys.py za pomocą aplikacji python wraz z argumentami my jestesmy argumentami. Python przechowuje dla nas treść linii komend w zmiennej sys.argv.
Pamiętaj, że nazwa uruchamianego skryptu jest zawsze na początku listy sys.argv. Dlatego w omawianym przykładzie mamy następujące przypisania:
Zauważ, że Python numeruje od 0, nie od 1.
sys.path zawiera listę katalogów, z których są importowane moduły. Zobacz, że pierwsza pozycja to folder, w którym został uruchomiony program. Folder, w którym został uruchomiony program zostaje automatycznie wstawiony na początek listy, natomiast pozostałe jej elementy są takie same niezależnie od miejsca wywołania. Oznacza to, że możesz łatwo importować moduły znajdujące się w katalogu bieżącym. Inne moduły są dostępne tylko w wypadku gdy znajdują się w którymś z katalogów wymienionych w ścieżce.
Przy okazji, możesz w każdej chwili podejrzeć katalog, w którym jesteś wpisując w Pythonie print os.getcwd() (oczywiście najpierw musisz zaimportować moduł os).
Importowanie modułu to dosyć czasochłonna operacja, więc Python używa pewnych sztuczek, aby ją przyspieszyć. Jedną z nich jest tworzenie plików skompilowanych do kodu bajtowego — pewnej formy pośredniej z rozszerzeniem .pyc. Pliki .pyc są niezależne od platformy. Plik .pyc jest przydatny, gdy masz zamiar zaimportować dany moduł po raz kolejny, używając innego programu - o wiele szybciej, gdyż część pracy potrzebnej do zaimportowania została już wykonana.
Jeżeli chcesz bezpośrednio zaimportować zmienną argv do swojego programu (aby nie pisać ciągle sys.), możesz użyć wyrażenia from sys import argv. Jeżeli chcesz zaimportować wszystko, co znajduje się w module sys, możesz użyć wyrażenia from sys import *. To działa z każdym modułem.
Tak naprawdę, powinieneś normalnie używać formy import ..., i odwoływać się do zmiennych z innych modułów przez nazwę ich modułu. Formy from...import... powinieneś używać wtedy, gdy dana nazwa będzie używana bardzo często i kłopotliwe byłoby używanie pełnej ścieżki. Formy z gwiazdką nie powinieneś używać w programach, bo powoduje zaśmiecenie przestrzeni nazw. Niemniej jest ona użyteczna w trybie interaktywnym, gdzie naprawdę nie chcemy pisać długich nazw.
Istnieje też możliwość zaimportowania modułu pod inną nazwę. Konstrukcja import nazwa_modulu as inna_nazwa spowoduje zaimportowanie modułu, z tym, że dostęp do jego zmiennych i funkcji będzie się odbywał przez inna_nazwa. . Może to być przydatne jeśli chcemy sobie oszczędzić pisania (na ogół inna_nazwa wybierana jest jako znacznie krótsza niż oryginalna nazwa_modulu)
Każdy moduł posiada zmienną zawierającą jego nazwę (zazwyczaj). Najczęściej używa się tej zmiennej wtedy, gdy chcemy się dowiedzieć, czy moduł został zaimportowany, czy uruchomiony jako program. Jak już wcześniej wspomniano, gdy moduł zostaje zaimportowany po raz pierwszy, jego kod zostaje wykonany. W przypadku definicji funkcji i klas ich wykonanie oznacza po prostu zdefiniowanie tych funkcji i klas. Polecenia zapisane w treści funkcji nie zostają wykonane w momencie definicji funkcji, lecz dopiero później, w momencie wywołania funkcji. Podobnie jest dla klas, czyli definicja klasy nie powoduje stworzenia obiektu. Natomiast wyrażenia znajdujące się poza definicjami funkcji i klasy zostają wykonane od razu. Często chcemy, by zostałe one wykonane tylko wtedy, gdy uruchamiamy moduł jako program.
Zmienna __name__ zawiera nazwę modułu. Wyjątkiem jest sytuacja gdy został on uruchomiony samodzielnie, jako program. Wówczas zawiera napis '__main__'. Dzięki temu możemy rozróżnić dwa sposoby wywołania modułu i podjąć odpowiednie decyzje.
#!/usr/bin/env python # -*- coding: utf-8 -*- # Nazwa pliku: nazwa.py if __name__ == '__main__': print 'Ten program jest uruchomiony samodzielnie.' else: print 'Zostałem zaimportowany z innego modułu.'
$ python nazwa.py Ten program jest uruchomiony samodzielnie. $ python >>> import nazwa Zostałem zaimportowany z innego modułu. >>>
Każdy moduł Pythona ma zdefiniowaną własną nazwę. Jeżeli jest nią '__main__', oznacza to, że moduł działa samodzielnie, a wtedy możemy podjąć odpowiednie działania.
Tworzenie własnych modułów jest proste, robisz to cały czas! A to dlatego, że każdy program w Pythonie jest także modułem. Ty musisz tylko zadbać, żeby miał rozszerzenie .py. Ten przykład powinien wszystko wyjaśnić.
Przykład:
#!/usr/bin/env python # -*- coding: utf-8 -*- # Nazwa pliku: mojmodul.py def mowczesc(): print 'Cześć, tu mówi mojmodul.' __version__ = '0.1' # Koniec modułu mojmodul.py.
Oto przykładowy moduł. Jak widać, nie ma tu nic szczególnie różniącego go od zwykłego programu w Pythonie. Następnie zobaczymy, jak go użyć w innych naszych programach.
Pamiętaj, że moduł powinien być umieszczony w tym samym katalogu co program, który z niego korzysta, lub też w jednym z katalogów wpisanych w sys.path.
#!/usr/bin/env python # -*- coding: utf-8 -*- # Nazwa pliku: mojmodul_demo.py import mojmodul mojmodul.mowczesc() print 'Wersja', mojmodul.__version__
Rezultat:
$ python mojmodul_demo.py Cześć, tu mówi mojmodul. Wersja 0.1
Zauważ, że używamy tego samego zapisu z kropkami przy uzyskiwaniu dostępu do elementów modułu. Python robi dobry użytek z tej samej notacji nadając temu swoisty „pythonowy” styl, dzięki czemu nie musimy wciąż poznawać coraz to nowych metod pracy.
Oto wersja z użyciem from...import...:
#!/usr/bin/env python # -*- coding: utf-8 -*- # Nazwa pliku: mojmodul_demo2.py from mojmodul import mowczesc, __version__ mowczesc() print 'Wersja', __version__
Rezultat mojmodul_demo2.py jest taki sam jak mojmodul_demo.py.
Ta forma jest przykładem bardzo złego stylu. Zauważ, że jeżeli nazwa __version__ już istniała wcześniej w module, który importuje mojmodul, powstanie konflikt nazw. Na dodatek jest to bardzo prawdopodobne, ponieważ zmienna o nazwie __version__ jest standardowo używana po przechowywania wersji modułu. W momencie kiedy plik mojmodul_demo zostanie wydłużony, łatwo o pomyłkę, bo normalnie __version__ to wersja bieżącego modułu, a tutaj to wersja modułu mojmodul. Stąd zawsze lepiej użyć wyrażenia import w taki sposób, by ograniczyć liczbę importowanych nazw.
Możesz także użyć:
from mojmodul import *
To spowoduje zaimportowanie prawie wszystkich nazw, jak na przykład mowczesc, ale ominie __version__, gdyż zaczyna się ona od podwójnego podkreślenia. Ta wersja jest jeszcze brzydsza.
Możesz użyć wbudowanej funkcji dir, aby wypisać nazwy zdefiniowane w pewnym obiekcie. Na przykład w module takie nazwy wskazują na funkcje, klasy i zmienne w nim zadeklarowane.
Gdy dir() zostaje wywołane z argumentem, to działa na nim. W wypadku wywołania dir() bez argumentu, działa ono na przestrzeni nazw, z której została wywołana.
Przykład:
$ python >>> import sys # Zdobądź listę atrybutów, w tym wypadku dla modułu sys. >>> dir(sys) ['__displayhook__', '__doc__', '__excepthook__', '__name__', '__package__', '__s tderr__', '__stdin__', '__stdout__', '_clear_type_cache', '_current_frames', '_g etframe', 'api_version', 'argv', 'builtin_module_names', 'byteorder', 'call_trac ing', 'callstats', 'copyright', 'displayhook', 'dont_write_bytecode', 'exc_clear ', 'exc_info', 'exc_type', 'excepthook', 'exec_prefix', 'executable', 'exit', 'f lags', 'float_info', 'getcheckinterval', 'getdefaultencoding', 'getdlopenflags', 'getfilesystemencoding', 'getprofile', 'getrecursionlimit', 'getrefcount', 'get sizeof', 'gettrace', 'hexversion', 'maxint', 'maxsize', 'maxunicode', 'meta_path ', 'modules', 'path', 'path_hooks', 'path_importer_cache', 'platform', 'prefix', 'ps1', 'ps2', 'py3kwarning', 'pydebug', 'setcheckinterval', 'setdlopenflags', ' setprofile', 'setrecursionlimit', 'settrace', 'stderr', 'stdin', 'stdout', 'subv ersion', 'version', 'version_info', 'warnoptions'] >>> dir() # Zdobądź listę atrybutów dla aktualnego modułu. ['__builtins__', '__doc__', '__name__', '__package__', 'sys'] >>> a = 5 # Stwórz nową zmienną "a". >>> dir() ['__builtins__', '__doc__', '__name__', '__package__', 'a', 'sys'] >>> del a # Usuń nazwę. >>> dir() ['__builtins__', '__doc__', '__name__', '__package__', 'sys'] >>>
Na początku sprawdzamy działanie dir na zaimportowanym module sys. Widać długą listę jego atrybutów.
Następnie używamy funkcji dir bez parametrów. Domyślnie zwraca ona listę atrybutów aktualnego modułu. Zauważ, że lista zaimportowanych modułów jest też częścią wyniku.
W celu ujrzenia dir w akcji, deklarujmy nową zmienną a, przypisujemy jej wartość, a następnie sprawdzamy, że na liście pojawiła nazwa naszej nowej zmiennej. Usuwamy ją poleceniem del, czego efekt widać po kolejnym użyciu dir.
Uwaga do del — to polecenie usuwa zmienną/nazwę (w tym wypadku del a), w efekcie później nie da się odnieść do tej nazwy, tak jakby nigdy wcześniej nie istniała.
Pamiętaj, że funkcja dir() działa z każdym obiektem. Na przykład możesz napisać dir(max), aby poznać atrybuty funkcji max, albo dir(str), aby poznać atrybuty klasy str.
Właśnie zacząłeś dogłębnie poznawać hierarchię elementów twoich programów. Zmienne zazwyczaj znajdują się w funkcjach. Funkcje oraz zmienne globalne — w modułach. A co gdy chcesz zarządzać modułami? W tym miejscu na scenę wkraczają paczki.
Paczki to katalogi z modułami oraz ze specjalnym plikiem __init__.py, który informuje Pythona, że ten katalog jest specjalnie przeznaczony właśnie do przechowywania modułów.
Powiedzmy, że chcesz stworzyć paczkę o nazwie swiat zawierającą paczki azja, afryka itd., zaś w nich na przykład indie czy madagaskar.
Oto, jak powinna wyglądać twoja struktura katalogów:
jakiś katalog wymieniony w sys.path
└── swiat/
├── __init__.py
├── azja/
│ ├── __init__.py
│ ├── indie.py
│ └── chiny.py
└── afryka/
├── __init__.py
└── madagaskar.py
Paczki są wygodnym sposobem segregacji modułów. Zobaczysz wiele przykładów ich użycia w bibliotece standardowej.
Tak jak funkcje są częściami programu wielokrotnego użytku, tak moduły to programy wielokrotnego użytku. Paczki są odrębną hierarchią organizacji modułów. Standardowa biblioteka Pythona jest przykładem zestawu paczek i modułów.
Zobaczyliśmy jak użyć tych modułów i utworzyć swoje własne.
Następnie poznamy pewne interesujące koncepty zwane strukturami danych.
Stwórz moduł fib3 zawierający trzy funkcje zwracające n-tą liczbę Fibonacciego — obliczoną rekurencyjnie z definicji, w pętli, oraz ze wzoru Bineta. Sam moduł oraz funkcje muszą mieć docstringi.
Moduł ma też zawierać bezargumentową funkcję test, której zadaniem jest sprawdzenie poprawności działania wszystkich trzech funkcji i wypisanie odpowiedniego komunikatu. Test powinien być wykonany dla pewnych ustalonych wartości, w szczególności tych granicznych, czyli 0, 1, 2 i innych. Przydatna może być ich lista na http://pl.wikisource.org/wiki/Ciąg_Fibonacciego.
Następnie napisz moduł fib3-czas, który wykorzystuje moduł fib3 i moduł time aby porównać szybkość wykonywania wszystkich trzech algorytmów. Wynik powienien zostać wypisany w postaci tabelki
n T1/ms T2/ms T3/ms 1 0.5 0.3 0.7 2 0.5 0.5 1.0 ... ... ... ...
Struktury danych to typy danych posiadających pewną wewnętrzną organizację/strukturę. Struktury danych tworzymy łącząc w określony sposób prostsze typy danych. Przykładem może być książka telefoniczna, gdzie odpowiednio organizując i łącząc proste typy: napis (zawierający nazwisko) i int (zawierający numer telefonu) tworzymy nowy typ danych określający jak jest zbudowany pojedynczy wpisy do książki telefonicznej.
W Pythonie istnieją cztery podstawowe struktury danych — lista, krotka, słownik i zbiór. Tak naprawdę są to klasy — odpowiednio list, tuple, dict, set. Jeśli chcesz wiedzieć więcej o klasach i obiektach, to zajrzyj do rozdziału Programowanie zorientowane obiektowo. Niemniej, aby poprawnie używać obiektów, wystarczy wiedzieć, że obiekty pewnej klasy mają wspólne zachowanie zdefiniowane przez tę klasę. Oznacza to między innymi, że obiekty mają pewien zbiór metod, czyli funkcji które można uruchamiać pisząć obiekt.metoda().
Lista to struktura, która zawiera uporządkowaną sekwencję obiektów. Tych obiektów może być zero, wtedy lista jest pusta, lub dowolnie dużo. Ten sam obiekt może występować na liście wielokrotnie. Dwie najważniejsze operacje, jakie można wykonać na liście, to:
Łatwo sobie to wyobrazić za pomocą listy zakupów, na której masz zapisane rzeczy do kupienia. Jedyna różnica jest taka, że na liście zakupów zazwyczaj rzeczy wpisuje się jedną pod drugą, zaś w Pythonie listę zapisuje się rozdzieląc przecinkami obiekty.
Najprostszym sposobem uzyskania listy jest właśnie zapisanie w treści programu ciągu „rzeczy“ rozdzielonych przecinkami, wszystko objęte nawiasami kwadratowymi. Innym sposobem jest wywołanie funkcji, która zwróci listę.
Do listy można dodawać elementy i je z niej usuwać. Te operacje zmieniają listę, więc mówimy, że lista jest zmiennym typem danych.
#!/usr/bin/env python # -*- coding: utf-8 -*- # Nazwa pliku: lista.py # To moja lista zakupów: lista = ['jabłko', 'mango', 'marchew', 'kiwi'] print 'Mam', len(lista), 'rzeczy do kupienia.' print 'Te rzeczy to:', for i in lista: print i, print '\nMuszę jeszcze kupić ryż.' lista.append('ryż') print 'Teraz moja lista to:', lista print 'Posortuję moją listę.' lista.sort() print 'Posortowana lista to:', lista print 'Pierwsza rzecz, jaką muszę kupić, to', lista[0] rzecz = lista[0] del lista[0] print 'Kupiłem', rzecz print 'Moja lista teraz to:', lista
Mam 4 rzeczy do kupienia. Te rzeczy to: jabłko mango marchew kiwi Muszę jeszcze kupić ryż. Teraz moja lista to: ['jabłko', 'mango', 'marchew', 'kiwi', 'ryż'] Posortuję moją listę. Posortowana lista to: ['jabłko', 'kiwi', 'mango', 'marchew', 'ryż'] Pierwsza rzecz, jaką muszę kupić, to jabłko Kupiłem jabłko Moja lista teraz to: ['kiwi', 'mango', 'marchew', 'ryż']
Zmienna lista jest listą zakupów kogoś wybierającego się do sklepu. Przechowujemy tam jedynie słowa oznaczające przedmioty do kupienia, ale tak naprawdę lista może zawierać dowolne obiekty, np. liczby czy inne listy.
Użyliśmy też pętli for...in... do przejścia po wszystkich elementach listy. W tym momencie można zauważyć, że lista jest przykładem sekwencji. Ale sekwencje poznasz trochę później.
Zauważ, że na końcu polecenia print użyliśmy przecinka. Powoduje to, że Python zakończy wypisywanie spacją, a nie przejściem do nowej linii.
Następnie dodaliśmy obiekt do listy metodą append, po czym sprawdziliśmy, czy to podziałało, po prostu każąc wypisać nową zawartość listy na ekran.
Po tym wszystkim posortowaliśmy listę za pomocą metody sort. Ważne jest to, że wynik powstaje przez przestawianie obiektów wewnątrz tej samej listy. Innymi słowy, żaden nowy obiekt nie zostaje stworzony, zmienia się tylko kolejność elementów na liście, co nazywamy sortowaniem w miejscu.
Zamiast sortowania w miejscu, można by stworzyć nową listę zawierającą te same elementy w innej kolejności. Do wykonania takiej operacji można wykorzystać funkcję sorted:
posortowana = sorted(lista)
Z tego przykładu widać, że lista jest typem zmiennym, czyli obiekty tej klasy można modyfikować. W przypadku innych typów, jak krotka (tuple) czy napis (str), dysponujemy tylko tym drugim sposobem sortowania. Mówimy, że są to typy niezmienne, przez co rozumiemy, że obiekt tej klasy, raz stworzony, nie może być zmieniony.
Gdy kupiliśmy już jakąś rzecz z listy, chcemy tę pozycję usunąć. Używamy do tego polecenia del — tutaj chcemy wyrzucić pierwszy (czyli w Pythonie zerowy) element, więc piszemy del lista[0].
Poniżej prezentujemy metody dla obiektów klasy list. Niech nasza przykładowa lista nazywa się L:
L.append(x)
Dodaje element x na koniec listy L. Taki sam efekt daje wyrażenie: L[len(L):] = [x].
L.extend(L2)
Wydłuża listę L dodając na jej końcu wszystkie elementy listy L2. Taki sam efekt daje wyrażenie: L[len(L):] = L2.
L.insert(i, x)
Wstawia element x do listy L na pozycji i. Dotychczasowe elementy listy L o indeksach od i do ostatniego mają teraz indeksy zwiększone o 1. Na przykład L.insert(0, x) wstawia element x na początek listy L natomiast L.insert(len(L), x) dodaje element na koniec listy.
L.remove(x)
Usuwa z listy L pierwszy napotkany element o wartości x. Uwaga: Jeśli takiego elementu nie ma funkcja generuje błąd!
L.pop(i)
Usuwa z listy L element o indeksie i i zwraca jego wartość. Jeśli nie podamy argumetu i to domyślnie funkcja zostanie zastosowana do ostatniego elementu listy.
L.index(x)
Zwraca pierwszy napotkany na liście L element o wartości x. Uwaga: Jeśli takiego elementu nie ma funkcja generuje błąd!
L.count(x)
Zwraca liczbę wystąpień elementu o wartości x na liście L.
L.sort()
Sortuje elementy listy L w miejscu.
L.reverse()
Odwraca kolejność elementów na liście L w miejscu.
>>> L = [66.25, 333, 333, 1, 1234.5] >>> print L.count(333), L.count(66.25), L.count('x') 2 1 0 >>> L.insert(2, -1) >>> L.append(333) >>> L [66.25, 333, -1, 333, 1, 1234.5, 333] >>> L.index(333) 1 >>> L.remove(333) >>> L [66.25, -1, 333, 1, 1234.5, 333] >>> L.reverse() >>> L [333, 1234.5, 1, 333, -1, 66.25] >>> L.sort() >>> L [-1, 1, 66.25, 333, 333, 1234.5]
Napisz program, który wczytuje wyniki pomiaru pewnej wielkości, czyli po prostu listę liczb. Niech użytkownik wpisuje liczby podając po jednej w każdej linijce „standardowego wejścia” — czyli na przykład wpisując je z klawiatury. Pusta linijka niech oznacza koniec danych.
Po wczytaniu wszystkich liczb, wypisz je w kolejności rosnącej, wraz z odchyleniem każdej liczby od średniej. Następnie wypisz nieobciążoną estymatę wariancji i odchylenia standardowego średniej całej sekwencji.
Przykład:
Wpisz liczby: 1 3 2.5 Dzięki. Posortowane: 1 -1.167 2.5 0.333 3 0.833 wariancja=1.083 odchylenie=0.601
Wskazówka:
oraz
oraz
Przez outliers rozumie się punkty odstające od reszty danych. W przypadku pomiarów takie punkty mogą na przykład pochodzić z błędnie przeprowadzonych pomiarów — jeśli mierzymy temperaturę termoparą i wszystkie pomiary temperatury dały ok. 30 mV, a tylko jeden 0 mV, to może w tym jednym pomiarze termopara odłączyła się od woltomierza? Niemniej zwyczajny statystyczny rozrzut wyników też może skutkować pomiarami leżącymi daleko od średniej, tyle że z małym prawdopodobieństwem.
W przypadku rozkładu Gaussa 99,7% wyników (średnio) pada w przedziale [μ−3σ, μ+3σ], gdzie μ to średnia, a σ to odchylenie standardowe. Napisz program, który tak jak w poprzednim przykładzie wczyta listę liczb, a następnie wypisze te z nich, które leżą poza przedziałem 3σ. Dla każdej z wypisywanych liczb wypisz też jej kolejność na liście. Na końcu napisz jaki procent liczb znalazł się poza przedziałem 3σ.
W poprzednim przykładzie liczby były wprowadzane przez użytkownika programu. Wprowadzanie liczb szybko robi się męczące. Na szczęście można użyć generatora liczb losowych, żeby „wprowadzić” liczby.
Generator liczb pseudolosowych zazwyczaj zwraca po prostu liczby z pewnego przedziału — np. pomiędzy 0 a 1 — zgodnie z rozkładem płaskim. Innymi słowy, wylosowanie liczby z dowolnego miejsca w całym przedziale jest tak samo prawdopodobne. Takim generatorem jest funkcja random.random.
W naszym przykładzie chcemy mieć liczby które przypominają wyniki pomiarów — czyli mają raczej rozkład normalny niż płaski. Jeśli dysponujemy generatorem liczb losowych płaskich, to bez problemu można „przerobić” go na generator liczb z dowolnego rozkładu jeśli tylko znamy dystrybuantę tego rozkładu. Na szczęście ten krok został już za nas wykonany i możemy po prostu skorzystać z funkcji random.normalvariate.
Funkcja normalvariate(mu, sigma) z modułu random ma dwa parametry — środek rozkładu i jego szerokość. Zwraca pojedynczą liczbę wylosowaną zgodnie z rozkładem normalnym.
Zmodyfikuj program z poprzedniego przykładu tak, by zamiast wczytywać liczby, generował je automatycznie. Od użytkownika pobierz tylko liczbę liczb n, i pożądane parametry μ, σ, a następnie posłuż się funkcją random.normalvariate by wygenerować n liczb z rozkładu normalnego.
Przykład:
>>> python outliers2.py N= 100 # 100 wpisane przez użytkownika μ= 3 # 3 wpisane przez użytkownika σ= 1 # 1 wpisane przez użytkownika liczby: 2.93313883, 4.27443167, 3.02747082, 3.74820712, 1.67385955, 3.91314826, 2.85697077, 3.72747457, 0.83560025, 2.60574017, ... 3.90344627, 2.07056563, 2.01952891, 2.81624163, -0.469027334 outliers: 100: -0.469027334 poza 3σ: 1%
Pamiętaj, że oczekiwana częstość występowania liczb spoza przedziału 3σ to tylko 3‰.
Krotka to niemodyfikowalna lista.
Podobnie jak listy, krotki można wygodnie tworzyć, wykorzystując specjalną notację: krotkę definiuje się przez wypisanie jej elementów oddzielonych przecinkami. Często, dla czytelności, warto zamknąć definicję krotki w nawias.
Krotki używa się w sytuacji, gdy chcemy przechowywać niezmienną sekwencję obiektów.
#!/usr/bin/env python # -*- coding: utf-8 -*- # Nazwa pliku: krotka.py zoo = ('pyton', 'słoń', 'pingwin') # Pamiętaj, że nawiasy są opcjonalne. print 'Liczba zwierząt w zoo:', len(zoo) nowe_zoo = ('małpa', 'wielbłąd', zoo) print 'Liczba klatek w nowym zoo:', len(nowe_zoo) print 'Wszystkie zwierzęta w nowym zoo to:', nowe_zoo print 'Zwierzęta sprowadzone ze starego zoo to:', nowe_zoo[2] print 'Ostatnim zwierzęciem sprowadzonym ze starego zoo jest', nowe_zoo[2][2] print 'Liczba zwierząt w nowym zoo:',len(nowe_zoo)-1+len(nowe_zoo[2])
Liczba zwierząt w zoo: 3 Liczba klatek w nowym zoo: 3 Wszystkie zwierzęta w nowym zoo to: ('małpa', 'wielbłąd', ('pyton', 'słoń', 'pingwin')) Zwierzęta sprowadzone ze starego zoo to: ('pyton', 'słoń', 'pingwin') Ostatnim zwierzęciem sprowadzonym ze starego zoo jest pingwin Liczba zwierząt w nowym zoo: 5
Zmienna zoo odnosi się do krotki złożonej ze zwierząt. Jak widać, dzięki funkcji len możemy sprawdzić długość krotki. To dodatkowo pokazuje, że krotka jest sekwencją.
Z powodu zamknięcia starego zoo, przenosimy zwierzęta do nowego. Dlatego też krotka nowe_zoo zawiera kilka zwierząt, które były już tam wcześniej, a także zwierzęta przeniesione ze starego zoo. Wracając do rzeczywistości, zauważ, że krotka w krotce nie traci swojej tożsamości.
Możemy odnieść się do pojedynczego elementu krotki poprzez podanie pozycji tego elementu w nawiasach kwadratowych, zupełnie jak przy listach. Nazywamy to operatorem indeksowania. Uzyskujemy trzeci element w krotce nowe_zoo przez wpisanie nowe_zoo[2] oraz trzeci element w trzecim elemencie tej krotki dzięki nowe_zoo[2][2].
Pomimo, że są opcjonalne, warto często użyć nawiasów, by podkreślić, że tworzy się krotkę. Np.
a = (1, 2, 3) # zamiast a = 1, 2, 3
Z tych dwóch równoważnych wersji, pierwsza wydaje się czytelniejsza.
Odmienna sytuacja występuje w przypadku wypisywania zmiennych. Wyrażenia print 1,2,3 oraz print (1,2,3) mają różne znaczenia. Za pierwszym razem uzyskamy trzy cyfry oddzielone przecinkami, a za drugim krotkę (czyli te same trzy liczby oddzielone przecinkami, ale jeszcze dodatkowo otoczone nawiasami).
pusta = ()
Krotka jednoelementowa to już większy problem. Jeśli napiszesz wyrażenie zawierające pojedynczą zmienną w nawiasach okrągłych, to (teoretycznie), można by to rozumieć jako
Z tych dwóch możliwości twórcy języka wybrali tą drugą. Dlatego aby stworzyć krotkę, należy użyć przecinka po pierwszym (i jedynym) jej elemencie.
Na przykład:samotnik = (2, )
Listy w listach nie tracą swojej tożsamości. Nie są one spłaszczane, jak w Perlu. To samo odnosi się do krotki w krotce, krotki w liście, listy w krotce itd. Dla Pythona to po prostu obiekty przechowywane w innym obiekcie, to wszystko.
Listy, krotki i napisy określamy mianem sekwencji. Kolejność elementów w sekwencji jest ustalona i elementy te są ponumerowane. Jest to cecha wspólna wszystkich sekwencji. Numer elementu w sekwencji jest nazywany indeksem. Podstawową operacją dla sekwencji jest pobranie wartości elementu o zadanym indeksie. Ponadto możemy sprawdzić czy dany obiekt jest elementem danej sekwencji (operatory in i not in).
Wymienione wcześniej trzy typy sekwencji — lista, krotka i ciąg znaków, mogą być dodatkowo pocięte, dzięki czemu uzyskujemy wycinek, czyli pod-sekwencję zawierającą część obiektów z oryginalnej sekwencji.
Wybieranie elementów przez indeksowanie lub wycinanie pokażemy dokładniej na przykładzie napisu. Napisy są również przykładem sekwencji elementów (liter).
Indeksując napis odwołujemy się do wybranego znaku. Znaki mogą być liczone od początku napisu (pierwszy ma numer 0) lub od jego końca (ostatni ma numer –1).
| 0 | 1 | 2 | ... | –2 | –1 | |||||||||
| ↓ | ↓ | ↓ | ↓ | ↓ | ↓ | |||||||||
| tekst = | " | S | T | R | I | N | G | " | ||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| ↑ | ↑ | |||||||||||||
| [ | ] |
Na powyższym rysunku czerwone indeksy są liczone od początku napisu, a niebieskie od jego końca.
Teraz przykłady:
| tekst[0] | → | "S" |
| tekst[1] | → | "T" |
| tekst[2] | → | "R" |
| tekst[−2] | → | "N" |
| tekst[−0] | → | "S" |
| tekst[20] | → | IndexError |
Nie ma indeksu −0, bo w Pythonie −0 == 0, więc −0 również wskazuje na początek napisu.
Numery wskazujące na miejsca poza końcem napisu lub przed jego początkiem są błędne, Python zwróci informację o błędzie, tzw. IndexError.
>>> tekst[0] 's' >>> tekst[1] 't' >>> tekst[2] 'r' >>> tekst[3] 'i' >>> tekst[4] 'n' >>> tekst[5] 'g'
>>> tekst = "string" >>> tekst[-1] 'g' >>> tekst[-2] 'n' >>> tekst[-3] 'i' >>> tekst[-5] 't' >>> tekst[-4] 'r' >>> tekst[-5] 't' >>> tekst[-6] 's'
Pobranie wycinka sekwencji polega na wybraniu jej fragmentu — podsekwencji. Miejsca początku i końca zakresu z którego pobieramy wycinek liczone są podobnie jak przypadku indeksowania. Istotna różnica jest taka, że wskazujemy podsekwencję, a nie pojedynczy element — i to w sposób szczególny. Przedział który wskazujemy jako [a:b], jest zamknięty od strony a, a otwarty od strony b.
Lewy i prawy kraniec wycinka podajemy oddzielając indeksy dwukropkiem :. Każdą z tych wartości można pominąć, w skrajnym przypadku zostawiając tylko dwukropek. Jeśli dany indeks pominiemy, zostaną użyte wartości domyślne:
Taka konwencja ma pewne zalety, w szczególności dla dowolnego indeksu a prawdą jest, że
sekwencja[:a] + sekwencja[a:] == sekwencja
Konstruując wycinek możemy pobierać co n-ty element napisu. Robi się to podając wartość kroku n po kolejnym dwukropku. Domyślnie n ma wartość 1.
Teraz przykłady:
| tekst[1:4] | → | "TRI" |
| tekst[:5] | → | "STRIN" |
| tekst[:–1] | → | "STRIN" |
| tekst[–4:] | → | "RING" |
| tekst[:] | → | "STRING" (tzw. pełny wycinek, czyli kopia sekwencji) |
| tekst[::–1] | → | "GNIRTS" |
| tekst[−10:−2] | → | "STRI" |
Numery wskazujące na miejsca poza rozmiarem napisu nie są błędne, Python wybierze tylko istniejące znaki.
#!/usr/bin/env python # -*- coding: utf-8 -*- # Nazwa pliku: sekwencja.py lista = ['jabłko', 'mango', 'marchew', 'kiwi'] imie = 'Juliusz' # Indeksowanie print 'Rzecz 0 to', lista[0] print 'Rzecz 1 to', lista[1] print 'Rzecz 2 to', lista[2] print 'Rzecz 3 to', lista[3] print 'Rzecz -1 to', lista[-1] print 'Rzecz -2 to', lista[-2] print 'Litera 0 to', imie[0] # Cięcie listy. print 'Rzeczy od 1 do 3 to', lista[1:3] print 'Rzeczy od 2 do końca to', lista[2:] print 'Rzeczy od 1 do -1 to', lista[1:-1] print 'Rzeczy od początku do końca to', lista[:] # Cięcie ciągu znaków. print 'Litery od 1 do 3 to', imie[1:3] print 'Litery od 2 do końca to', imie[2:] print 'Litery od 1 do -1 to', imie[1:-1] print 'Litery od początku do końca to', imie[:]
Rzecz 0 to jabłko Rzecz 1 to mango Rzecz 2 to marchew Rzecz 3 to kiwi Rzecz -1 to kiwi Rzecz -2 to marchew Litera 0 to J Rzeczy od 1 do 3 to ['mango', 'marchew'] Rzeczy od 2 do końca to ['marchew', 'kiwi'] Rzeczy od 1 do -1 to ['mango', 'marchew'] Rzeczy od początku do końca to ['jabłko', 'mango', 'marchew', 'kiwi'] Litery od 1 do 3 to ul Litery od 2 do końca to liusz Litery od 1 do -1 to ulius Litery od początku do końca to Juliusz
Najpierw używamy indeksów do pobrania wartości poszczególnych elementów sekwencji. Nawiasy kwadratowe są operatorem indeksowania — gdy przy sekwencji podasz liczbę w nawiasach kwadratowych to uzyskasz element z danej pozycji. Pamiętaj, że w Pythonie, podobnie jak w C i wielu innych językach programowania, sekwencje czy tablice numeruje się od 0, dlatego lista[0] to pierwszy element, a lista[3] to czwarty.
Indeks może też być ujemny, wtedy liczenie zaczyna się od końca sekwencji. Dlatego lista[-1] to ostatni element, a lista[-2] to przedostatni.
Operacja pobierania wycinka jest wykonywana przez podanie obiektu do pocięcia, a następnie dwóch liczb w nawiasie kwadratowym, przedzielonych dwukropkiem.
Pierwsza liczba (przed dwukropkiem) w operacji pobrania wycinka oznacza pozycję startową, zaś druga (za dwukropkiem) wyznacza dokąd cięcie ma zostać wykonane. Jeżeli nie ma pierwszej liczby, to Python zacznie wycinać od początku. Gdy nie ma drugiej, to wytnie aż do końca. Zauważ, że cięcie zaczyna się równo z pozycją startową, ale kończy się przed pozycją końcową. To znaczy, że pozycja startowa jest zawarta w wyciętym fragmencie, ale pozycja końcowa już nie jest.
W związku z tym, lista[1:3] zwraca elementy 1 i 2, ale nie zwraca już trzeciego, zaś lista[:] (tzw. pełny wycinek) zwraca kopię sekwencji.
Możesz także podać trzeci argument, którym jest krok cięcia (domyślnie 1).
>>> lista = ['jabłko', 'mango', 'marchew', 'kiwi'] >>> lista[::1] ['jabłko', 'mango', 'marchew', 'kiwi'] >>> lista[::2] ['jabłko', 'marchew'] >>> lista[::3] ['jabłko', 'kiwi'] >>> lista[::-1] ['kiwi', 'marchew', 'mango', 'jabłko']
Jak widzisz, gdy krok wynosi 2, uzyskujemy elementy numer 0, 2, ..., zaś gdy wynosi 3, uzyskujemy elementy numer 0, 3, ... itd.
Wypróbuj różne kombinacje używając interpretera w trybie interaktywnym. Najlepsze jest to, że możesz dokładnie to samo robić na każdym typie sekwencji, czy to lista, czy krotka, czy ciąg znaków!
Wytwórz listę liczb naturalnych od 1 do 20. Następnie pobierz z tej sekwencji wycinek zawierający wyłącznie liczby parzyste oraz wycinek zawierający liczby nieparzyste od ostatniej do pierwszej.
Wybierz co trzecią liczbę zaczynając od 10 i nie przekraczając 20.
Masz pewien napis zawierający poszukiwane hasło w nawiasach klamrowych.
napis = "dane_użytkownika = {tajne hasło} login=użytkownik "
Napisz funkcję która weźmie napis jako parametr i zwróci samo hasło bez nawiasów klamrowych.
Gdy tworzysz obiekt i przypisujesz go do zmiennej, zmienna jedynie odnosi się do tego obiektu, a nie reprezentuje go. Nazywamy to związywaniem (ang. binding) nazwy z obiektem. Tak więc, nazwa zmiennej jedynie wskazuje miejsce w pamięci komputera, w którym znajdują się określone dane. Pociąga to za sobą pewne konsekwencje, które ilustruje poniższy przykład.
#!/usr/bin/env python # -*- coding: utf-8 -*- # Nazwa pliku: odniesienie.py print 'Proste Przypisywanie' lista = ['jabłko', 'mango', 'marchew', 'kiwi'] mojalista = lista # mojalista to tylko inna nazwa dla tej samej zmiennej! del lista[0] # Kupiłem pierwszą rzecz, więc ją usuwam z listy. print 'Lista zakupów:', lista print 'Moja lista:', mojalista # Obydwie listy będą zawierać dokładnie to samo, czyli trzy pozycje. # W żadnej z nich nie pojawi się "jabłko", bo dotyczą tego samego obiektu. print 'Kopiowanie za pomocą pełnego wycinka' mojalista = lista[:] # Stwórz kopię za pomocą pełnego wycinka... del mojalista[0] # Usuń pierwszą rzecz... print 'Lista zakupów:', lista print 'Moja lista:', mojalista # Teraz te listy będą się różniły.
Proste Przypisywanie Lista zakupów: ['mango', 'marchew', 'kiwi'] Moja lista: ['mango', 'marchew', 'kiwi'] Kopiowanie za pomocą pełnego wycinka Lista zakupów: ['mango', 'marchew', 'kiwi'] Moja lista: ['marchew', 'kiwi']
Pamiętaj, że jak chcesz zrobić kopię jakiejś złożonej zmiennej (nie prostej, jak ciąg znaków), to musisz przypisać mu pełny wycinek. Jeżeli zamiast tego po prostu przypiszesz zmiennej inną nazwę, obydwie nazwy będą się odnosić do tego samego obiektu.
Już wcześniej omówiliśmy dogłębnie ciągi znaków, więc co jeszcze można dodać? Cóż... Czy wiedziałeś, że to też są obiekty i również mają swoje metody, jak na przykład sprawdzanie, czy w danym tekście jest określone słowo?
Wszystkie ciągi znaków, jakie używasz, mają przypisaną klasę str. Poniżej zaprezentowane jest użycie kilku ciekawych metod tej klasy. Więcej informacji znajdziesz wywołując help(str).
#!/usr/bin/env python # -*- coding: utf-8 -*- # Nazwa pliku: str_metody.py imie = 'Aleksander' # To obiekt klasy str. if imie.startswith('Ale'): print 'Tak, to imię zaczyna się od "Ale"' if 'a' in imie: print 'Tak, to imię zawiera literę "a"' if imie.find('san') != -1: print 'Tak, w tym imieniu jest ciąg "san"' separator = ' do ' mojalista = ['Brazylii', 'Rosji', 'Indii', 'Chin'] print 'Wielka podróż z', separator.join(mojalista)
$ python str_metody.py Tak, to imię zaczyna się od "Ale" Tak, to imię zawiera literę "a" Tak, w tym imieniu jest ciąg "san" Wielka podróż z Brazylii do Rosji do Indii do Chin
Tu widzimy wiele metod w akcji. Metoda startswith sprawdza, czy tekst się zaczyna od podanego ciągu znaków. Operator in sprawdza, czy dany ciąg znaków znajduje się w tym tekście.
Metoda find sprawdza pozycję podanego ciągu znaków w tekście. Zwraca ona -1, gdy nic nie znajdzie. Klasa str ma też ciekawą metodę join, która łączy elementy sekwencji w jeden długi ciąg znaków, używając podanego ciągu jako separatora.
W poprzednim rozdziale przedstawione były struktury danych zawierające elementy w pewnym określonym porządku — listy, krotki, napisy. W przypadku struktur danych takich jak słowniki czy zbiory elementy nie mają określonej kolejności. Nie znaczy to, że nie można wykonać operacji dla każdego elementu zbioru jak dla zwykłej sekwencji, tylko że kolejność elementów w zbiorze nie jest dobrze określona.
O słowniku wygodnie jest myśleć jako o nieposortowanym zbiorze par klucz:wartość, przy czym klucz musi być unikalny. W słowniku kojarzymy klucze (nazwy) z wartościami (szczegółami). Przykładem słownika jest książka adresowa, w której możesz znaleźć czyjś adres lub telefon znając dane tej osoby.
Kluczem może być tylko obiekt niezmienny (na przykład napis czy krotka), ale wartości mogą być dowolne (napisy, krotki, listy, słowniki, liczby, ...). Tutaj niezmienność krotek znajduje swoje zastosowanie.
Do tworzenia słowników służy następująca notacja:
s = {klucz1 : wartość1, klucz2 : wartość2}
Między kluczem a wartością jest dwukropek, zaś między parami są przecinki. Wszystko jest zamknięte w nawiasach klamrowych.
Pamiętaj, że klucze w słowniku nie są w żaden sposób posegregowane. Jeżeli chcesz mieć je poukładane w jakimś szczególnym porządku, musisz ich listę samemu posortować.
Słowniki to po angielsku dictionary, zaś klasa nazywa się krótko dict.
Słownik to funkcja w sensie matematycznym — ze zbioru kluczy w i na zbiór wartości.
#!/usr/bin/env python # -*- coding: utf-8 -*- # Nazwa pliku: slownik.py # "ka" to skrót od "k"siążka "a"dresowa ka = { 'Jan' : 'jasiek@jasiowo.pl', 'Tomek' : 'tomek@cba.gov', 'Ela' : 'ela@zzzz.com', 'Kasia' : 'katarzyna@hopla.us' } print "Adres Kasi:", ka['Kasia'] # Usuwanie pary klucz-wartość. del ka['Kasia'] print u'\nKontaktów w książce adresowej jest {0}.\n'.format(len(ka)) # iterowanie przez pary słownika for imie, adres in ka.items(): print '{0} ma adres {1}'.format(imie, adres) # Dodawanie pary klucz-wartość. ka['Wojtek'] = 'wojtas@kumple.pl' if 'Wojtek' in ka: print "\nAdres Wojtka:", ka['Wojtek'] # pobranie listy kluczy w słowniku l = ka.keys() print l # sprawdzenie czy klucz jest w słowniku jest = 'Tomek' in ka print jest
Adres Kasi: katarzyna@hopla.us Kontaktów w książce adresowej jest 3. Jan ma adres jasiek@jasiowo.pl Ela ma adres ela@zzzz.com Tomek ma adres tomek@cba.gov Adres Wojtka: wojtas@kumple.pl ['Jan', 'Wojtek', 'Ela', 'Tomek'] True
Tworzymy słownik ka używając omówionej już wcześniej notacji. Następnie docieramy do jednej z wartości używając nazwiska jako klucza. Operację pobrania elementu zapisujemy podobnie jak w przypadku indeksowania sekwencji. Niemniej pary w słowniku nie są uporządkowane i jako „indeksu” do słownika można użyć tylko jeden z kluczy.
Możemy usuwać wpisy ze słownika za pomocą polecenia del — po prostu określamy słownik i klucz, który razem z odpowiednią wartością ma zostać usunięty. Samej wartości nie musimy wcale znać przy tej operacji.
Następnie używamy metody items, która zwraca nam pary w postaci krotek, z których każda składa się z dwóch elementów — pierwszy to klucz, a drugi to wartość. Dzięki for...in... przypisujemy te pary do zmiennych, odpowiednio imie i adres, po czym wypisujemy je w bloku for. Do formatowania napisu wykorzystujemy tu metodę format. Jej argumentem jest krotka. Za numery w nawiasach klamrowych metoda format podstawia element krotki o tym indeksie. Możemy dodać nową parę klucz-wartość po prostu używając operatora indeksowania do oznaczenia klucza i przypisania mu wartości, tak jak zrobiliśmy to dla Wojtka w powyższym przykładzie.
Możemy sprawdzić czy dany klucz istnieje w słowniku za pomocą operatora in.
Jeśli chcesz poznać wszystkie metody dostępne dla klasy słowników, wpisz help(dict) albo zajrzyj do pomocy na sieci.
Tak jak zostało to wcześniej wspomniane, słowniki służą do przekazywania nadmiarowych argumentów nazwanych do funkcji. Jako klucze słownika zostają użyte nazwy (nieistniejących) parametrów w postaci napisów. Klucze wskazują z kolei na wartości argumentów.
def wypisz(separator,*wyrazy, **dodatkowe_informacje): print separator # ten argument MUSI wystąpić w wywołaniu funkcji print wyrazy # krotka zawierająca dodatkowe, NIENAZWANE argumenty print dodatkowe_informacje # słownik zawierający dodatkowe NAZWANE argumenty for w in wyrazy: print w, separator, print for klucz,wartosc in dodatkowe_informacje.items(): print klucz, wartosc wypisz(';', 'Być','albo' ,'nie być', sztuka='Hamlet', autor='Szekspir')
; ('By\xc4\x87', 'albo', 'nie by\xc4\x87') {'sztuka': 'Hamlet', 'autor': 'Szekspir'} Być ; albo ; nie być ; sztuka Hamlet autor Szekspir
Zob. też bardziej zaawansowane przykłady w rozdziale o wyjątkach.
Bardziej ambitne zadanie wymagające inteligentnego użycia słowników: anagramy.
Zbiory to nieuporządkowane zestawy prostych obiektów. Używamy ich, gdy istotny jest tylko fakt występowania elementu, a nie jego położenie albo liczba powtórzeń.
Zbiory możesz testować pod kątem występowania danego elementu, sprawdzać czy to jest podzbiór innego zbioru, szukać części wspólnej zbiorów i tak dalej.
>>> kraje = set(['Brazylia', 'Rosja', 'Indie']) >>> kraje.__contains__('Indie') True >>> kraje.__contains__('USA') False >>> kraje2 = kraje.copy() >>> kraje2.add('Chiny') >>> kraje2.issuperset(kraje) True >>> kraje.issubset(kraje2) True >>> kraje.remove('Rosja') >>> kraje.intersection(kraje2) set(['Brazylia', 'Indie'])
Lista operatorów działających na zbiory jest przedstawiona w tabeli operatorów w drugim rozdziale.
Te same operacje co powyżej można zwięźlej zapisać korzystając z operatorów zamiast metod.
>>> kraje = set(['Brazylia', 'Rosja', 'Indie']) >>> 'Indie' in kraje True >>> 'USA' in kraje False >>> kraje2 = kraje | set(['Chiny']) >>> kraje2 > kraje True >>> kraje < kraje2 True >>> kraje.remove('Rosja') >>> kraje & kraje2 set(['Brazylia', 'Indie'])
Zbiór — struktura danych języka Python — posiada własności i można na nim wykonywać operacje takie jak dla zbioru w sensie obiektu matematycznego.
Ten przykład nie wymaga omawiania, gdyż użyte w nim są jedynie proste techniki matematyczne uczone w szkole. Dla porządku dodamy znaczenie używanych tu słów z języka angielskiego: set — zbiór, superset — nadzbiór, subset — podzbiór, intersection — część wspólna zbiorów.
Przestudiowaliśmy dokładnie różne wbudowane struktury danych Pythona. Będą one niezbędne przy pisaniu programów bardziej złożonych programów.
Moduł Numpy jest podstawowym zestawem narzędzi dla języka Python umożliwiającym zaawansowane obliczenia matematyczne, w szczególności do zastosowań naukowych (tzw. obliczenia numeryczne, jak mnożenie i dodawanie macierzy, diagonalizacja czy odwrócenie, całkowanie, rozwiązywanie równań, itd.). Daje on nam do dyspozycji specjalizowane typy danych, operacje i funkcje, których nie ma w typowej instalacji Pythona. Natomiast moduł Scipy pozwala na dostęp do bardziej złożonych i różnorodnych algorytmów wykorzystujących bazę zdefiniowaną w Numpy.
Przedstawimy tutaj tylko wstęp do Numpy. Wynika to z faktu, że opisanie licznych funkcji dostępnych w bibliotece Numpy jest ogromną pracą, która zupełnie nie ma sensu — równie dobrze można zajrzeć bezpośrednio do źródła, http://docs.scipy.org/doc/numpy/reference/.
Najważniejszym obiektem, na którym bazuje pakiet Numpy i szereg pakietów z niego korzystających jest klasa ndarray wprowadzająca obiekty array. Obiekty array możemy traktować jako uniwersalne pojemniki na dane w postaci macierzy (czyli wektorów lub tablic). W porównaniu ze standardowymi typami sekwencji Pythonowych (lista, krotka) jest kilka różnic w operowaniu tymi obiektami:
Najprostszym sposobem stworzenia macierzy Numpy jest wywołanie funkcji array z argumentem w postaci listy liczb. Jeśli zamiast listy liczb użyjemy listy zawierającej inne listy (tzw. listy zagnieżdżone), to otrzymamy macierz wielowymiarową. Na przykład jeśli listy są podwójnie zagnieżdzone, to otrzymujemy macierz dwuwymiarową (tablicę).
# przykład wykorzystania Numpy >>> import numpy >>> A = numpy.array([1, 3, 7, 2, 8]) array([1, 3, 7, 2, 8]) >>> B = numpy.array([[1, 2, 3], [4, 5, 6]]) >>> B array([[1, 2, 3], [4, 5, 6]]) >>> B.transpose() array([[1, 4], [2, 5], [3, 6]])
Innym sposobem tworzenia macierzy jest funkcja numpy.arange, która działa analogicznie do range, tyle tylko, że zwraca macierz zamiast listy. Argumenty są takie same:
>>> numpy.arange(1000000) array([ 0, 1, 2, ..., 999997, 999998, 999999])
Jak było już wspomniane, w przypadku macierzy array typowe operacje matematyczne możemy przeprowadzić dla wszystkich elementów macierzy przy użyciu jednego operatora lub funkcji. Zachowanie takie jest odmienne niż w przypadku list czy innych sekwencji Pythona. Jeśli chcielibyśmy na przykład pomnożyć wszystkie elementy listy L przez liczbę a, musimy użyć pętli:
L = [1, 3, 5, 2, 3, 1] for i in L: L[i]=L[i]*a
Natomiast mnożenie wszystkich elementów macierzy M przez liczbę a wygląda tak:
M = numpy.array([1, 3, 5, 2, 3, 1]) M = M*a
Operacje wykonywane od razu na całych macierzach mają wiele zalet. Kod programu jest prostszy i krótszy, przez co mniej podatny na błędy. Poza tym nie musimy przejmować się konkretną realizacją danej operacji — robi to za nas funkcja pakietu Numpy, która jest specjalnie optymalizowana, żeby działała jak najszybciej.
Dostęp do elementów (i pod-macierzy) jest możliwy poprzez wykorzystanie notacji indeksowej (macierz[i]) jak i wycinkowej (macierz[i:j]).
Dostęp do pojedynczego elementu:
>>> A = array([[1, 2, 3],[4,5,6]]) >>> A array([[1, 2, 3], [4, 5, 6]]) >>> A[0][2] # podobnie jak w Pythonie,numeracja od 0 3 >>> A[0, 2] 3
Indeksy dotyczące poszczególnych wymiarów można oddzielić przecinkami.
Macierz A jest tablicą dwuwymiarową, i sposób numerowania zawartych w niej obiektów jest następujący: pierwszy indeks przebiega pierwszy wymiar (wybiera wiersz), drugi indeks przebiega drugi wymiar (wybiera kolumnę).
Dostęp do pod-macierzy:
>>> A[1] # wiersz 1 array([4, 5, 6]) >>> A[1, :] # wiersz 1, wszystkie kolumny array([4, 5, 6]) >>> A[:, 1] # wszystkie wiersze, kolumna 1 array([2, 5])
Jak widać, ograniczenie się do pojedynczego punktu w danym wymiarze, powoduje degenerację tego wymiaru. Uzyskuje się macierz, w której liczba wymiarów jest mniejsza o jeden.
>>> A[:, 1:] array([[2, 3], [5, 6]])
W pierwszym wymiarze (wiersze) bierzemy wszystko, natomiast w drugim od 1 do końca. Efektywnie wycinamy kolumnę 0.
Do wybrania elementów z macierzy można tez użyć innej macierzy. Może to być
Uwaga: W wyniku dostajemy macierz jedno wierszową.
>>> print A [[1 2 3] [4 5 6]] >>> print A > 2 [[False False True] [ True True True]] >>> print A[A > 2] [3 4 5 6] >>> print A[A % 2 == 0] [2 4 6]
Przejście od Pythona do Numpy oznacza odejście od obiektowości. Oczywiście takie stwierdzenie można natychmiast skontrować:
numpy.random.random((10,10)).var()
jest typowym przykładem notacji obiektowej, i generalnie ma cechy obiektowości (ukrywanie detali implementacji, polimorfizm). Odejście od obiektowości występuje tylko na poziomie indywidualnych elementów — liczb.
W Pythonie, tak jak w zasadniczej większości języków programowania, operacje na liczbach są ostatecznie wykonywane przez procesor, w identyczny sposób niezależnie od języka programowania. A procesor, jak wiadomo, umie wykonywać tylko proste operacje. Przez to liczby, które się mu podaje by wykonać na nich działania, są w ściśle określonym formacie, niezależnym od języka programowania. Tak więc obiekt w Pythonie, np. liczba zmiennoprzecinkowa, zawiera pewne meta-informacje o tym obiekcie (jak reference-count, czyli liczba użytkowników obiektu, i typ, czyli przypisanie do klasy) oraz właściwe dane, w formacie oczekiwanym przez procesor.
Rysunki powyżej przedstawiają schematycznie zmienną w Pythonie (typu float) i pojedynczy element macierzy numpy.ndarray. W przypadku architektury 32-bitowej, liczba float ma 4 bajty (32 bity), a cały obiekt 12 bajtów, czyli 96 bitów).
Odejście od obiektowości w Numpy oznacza zatracenie obiektowości indywidualnych elementów macierzy, natomiast sam obiekt numpy.ndarray bardzo silnie wykorzystuje notację i właściwości podejścia obiektowego. Indywidualne elementy macierzy muszą być tego samego typu — oznacza to ten sam rozmiar w bajtach oraz identyczną interpretację i zachowanie każdego elementu.
Można powiedzieć, że numpy.ndarray rezygnuje z części możliwości na rzecz wydajności, przed wszystkim różnorodności typów przedstawionej jak na rysunku poniżej.
Każda liczba w Pythonie jest indywidualnym obiektem. Liczby w Numpy takie nie są. Niemniej, kiedy wybierzemy jeden element z macierzy (np. używając []), to otrzymujemy liczbę-obiekt. Numpy automatycznie tworzy nowe obiekty do przechowywania liczb które mają być użyte poza Numpy.
Pakiet Numpy wprowadza też szczególne wartości dla przechowywania nietypowych wyników obliczeń. Należą tutaj takie wartości jak:
Jakkolwiek wartości te nie są dostępne w standardowym Pythonie, są one zestandaryzowane i opisane w normie IEEE-754; zapisane w pliku binarnym będą poprawnie interpretowane przez inne programy stosujące się do tej normy.
Pierwsza przyczyna, zazwyczaj najmniej istotna, to wydajność. Jeśli mamy pomnożyć 100 elementów, to szybkość operacji na pojedynczym elemencie nie ma znaczenia. Podobnie jest z rozmiarem pojedynczego elementu. Jeśli elementów jest 106, to również wtedy narzut nie ma większego znaczenia. Policzmy: 1000000 razy 12 bajtów, to 12 MB. Typowy komputer ma obecnie 1-4 GB pamięci, czyli używamy od 1,2% do 0,27% dostępnej pamięci — jaki problem? Dopiero gdy miejsce zajmowane przez dane jest tego samego rzędu co całość dostępnej pamięci, to czy pojedyncza komórka zajmuje 8 czy 16 bajtów, zaczyna mieć znaczenie.
Druga przyczyna, istotna ze względu na przyjemność pracy, to notacja obiektowa i infixowa. Ta pierwsza to oczywiście „notacja z kropką” — dostęp do metod i atrybutów na obiekcie. Jej użycie, zwłaszcza w połączeniu z dopełnieniem TAB-em upraszcza pisanie. Przykład notacji obiektowej:
a.transpose().min() # zamiast numpy.min(numpy.transpose(a))
Ta druga (infixowa) to stara dobra „notacja matematyczna” — umiejscowienie operatorów dwuargumentowych pomiędzy obiektami na które działają. Przykład notacji infixowej:
a + b*c # zamiast numpy.add(a, numpy.multiply(b, c))
Oczywiście notacja obiektowa i infixowa jest używane wszędzie w Pythonie, ale warto wspomnieć, że Numpy od niej nie odchodzi. Niemniej Numpy odchodzi od Pythonowej interpretacji niektórych działań. W Pythonie takie operacje jak mnożenie list wywodzą się z działań na ciągach znaków. W obliczeniach numerycznych podstawą są działania na elementach, tak więc w Numpy wszystkie operatory domyślnie działają na indywidualnych parach elementów.
Trzecia przyczyna, chyba najważniejsza, to biblioteka funkcji numerycznych. Odejście od obiektowości danych pozwala na eksport wartości i komunikację z bibliotekami napisanymi w zupełnie innych językach programowania. Na przykład Scipy może korzystać z biblioteki LAPACK (Linear Algebra PACKage, napisanej w Fortranie 77). To że funkcje napisane w różnych językach mogą wymieniać się danymi w pamięci bez skomplikowanego tłumaczenia danych, wynika z faktu, że tak jak to w poprzednim podrozdziale było opisane, ostatecznie wszystkie liczby są w formacie akceptowanym przez procesor.
Możliwość użycia kodu napisanego w C czy Fortranie pozwala na wykorzystanie starych, zoptymalizowanych, sprawdzonych rozwiązań.
Normalnie programista Pythona takie detale, jak ile bitów ma zajmować zmienna, pozostawia całkowicie w gestii interpretera. Niemniej w przypadku obliczeń numerycznych często potrzebna jest silniejsza kontrola. Numpy daje możliwość dokładnej kontroli formatu danych, czyli odejście od pomocniczości powszechnej w Pythonie, pozwalając jednocześnie na gładkie łączenie obliczeń na macierzach z Numpy i normalnych obiektach pythonowych.
Zob. zadania z operacji na macierzach.
Pakiet Pylab/Matplotlib bazuje na pakiecie numerycznym Numpy i korzysta z obiektów w nim zawartych. Pokażemy, jak z jego pomocą rysować różnorodne wykresy prezentujące graficznie przetwarzane dane i wyniki obliczeń. Zamiast wyliczać zawartość pakietu pokażemy ich użyteczność na przykładach. Zaczniemy od prostych i będziemy po drodze omawiać zastosowane w nich konstrukcje.
Prześledźmy działanie poniższego programu:
import pylab x = [1,2,3] y = [4,6,5] pylab.plot(x,y) pylab.show()
Aby skorzystać z pakietu graficznego Pylab importujemy go do naszego programu poleceniem import.
Wytwarzamy dwie listy x i y zawierające ciągi liczb 1, 2, 3 oraz 4, 6, 5.
Funkcja plot rysuje wykres i umieszcza na nim punkty o współrzędnych zawartych w listach przekazanych jej jako argumenty. Pierwszy argument zawiera współrzędne x-owe kolejnych punktów, a drugi argument współrzędne y-owe kolejnych punktów wykresu. Ponieważ listy mają po trzy elementy, tak więc wykres zawierać będzie trzy punkty o współrzędnych (1, 4), (2, 6) oraz (3, 5). Domyślnie punkty na wykresie łączone są ze sobą niebieską linią ciągłą.
Po wywołaniu funkcji plot wykres nie pokazuje się jeszcze na ekranie. Aby go pokazać, używamy funkcji show. Wykres pojawia się na ekranie w osobnym oknie, a Python czeka z wykonywaniem kolejnych instrukcji do momentu zamknięcia okna z wykresem.
W okienku wykresu mamy kilka guzików (po lewej stronie na dole). Służą one do manipulowania wyglądem rysunku. Guzikiem z krzyżykiem możemy zmniejszać/zwiększać skalę na osiach (wciskając prawy guzik myszy i przesuwając kursor po obrazku) oraz przesuwać cały wykres (wciskając lewy guzik myszy i przesuwając kursor po obrazku). Guzik z okienkiem i strzałkami pozwala także zmieniać rozmiar i położenie osi wykresu wewnątrz okna wybierając właściwe wartości. Guzik z domkiem przywraca wyjściowe ustawienia rysunku.
import pylab as p x = p.arange(0.0, 2.0, 0.01) y = p.sin(2.0*p.pi*x) p.plot(x,y) p.show()
Pobieramy do użycia pakiet Pylab pod nazwą p.
Funkcja arange jest podobna do standardowej funkcji range wytwarzającej określone sekwencje liczb w postaci listy. Funkcja arange zamiast listy wytwarza macierz zawierającą ciąg liczb zmiennoprzecinkowych zaczynający się od pierwszego podanego argumentu funkcji arange (u nas 0.0), a kończący się przed drugim argumentem (tradycyjnie, ciąg wynikowy nie zawiera wartości podanej jako drugi argument, u nas 2.0). Różnica między elementami wytworzonego ciągu domyślnie wynosi 1, ale jeśli podamy funkcji arange trzeci argument, to definiuje on nową różnicę ciągu, u nas wynosi on 0.01.
Tak więc zmienna x jest macierzą-wektorem zawierającą ciąg liczb od 0 do 1,99 co 0,01 (czyli 0, 0,01, 0,02, ..., 1,98, 1,99).
Funkcja sin służy do obliczania wartości funkcji sinus dla argumentu podanego w radianach. A co u nas jest argumentem tej funkcji? Wyrażenie będące argumentem zawiera mnożenie liczby 2.0 przez pi (pochodzące z pakietu Pylab), a następnie mnożenie wyniku przez macierz x. Zmienna pi zawiera wartość matematycznej stałej π ≈ 3,1415926. Mnożenie liczby i macierzy, jak wiemy z poprzedniego punktu, daje w wyniku macierz. Oznacza to, że argumentem funkcji sin jest nie liczba, ale macierz! Taka możliwość jest przewidziana przez twórców pakietu Numpy; wynikiem wywołania funkcji jest wtedy również macierz. Jest ona tej samej długości co macierz będąca argumentem wywołania funkcji.
Tak więc zmienna y zawiera ciąg wartości funkcji sinus policzonych dla wartości zawartych w zmiennej x pomnożonych każda przez 2π (czyli sin(2π·0), sin(2π·0,01), sin(2π·0,02), ..., sin(2π·1,98), sin(2π·1,99)).
Funkcja plot(x,y) narysuje zestaw punktów o współrzędnych (0, sin(2π·0)), (0,01, sin(2π·0,01)), (0,02, sin(2π·0,02)), ..., (1,98, sin(2π·1,98)), (1,99, sin(2π·1,99)) połączonych niebieską linią.
import pylab as p x = p.arange(0.0, 2.0, 0.01) y = p.sin(2.0*p.pi*x) p.plot(x,y,'r:',linewidth=6) p.xlabel('Czas') p.ylabel('Pozycja') p.title('Nasz pierwszy wykres') p.grid(True) p.show()
W porównaniu z poprzednim przykładem pojawiło się na wykresie kilka drobnych zmian i „ozdobników”.
W funkcji plot pojawiły się dwa nowe parametry:
Dodaliśmy też wywołania funkcji xlabel i ylabel. Ich argumentami są napisy, które pojawią się jako opisy osi, odpowiednio poziomej i pionowej. Wywołanie funkcji title wypisuje przekazany jej napis jako tytuł całego wykresu.
Funkcja grid dorysowuje siatkę prostokątną na wykresie w wybranych punktach opisujących wartości na osiach wykresu. Punkty, w których wybierane są wartości opisane na osiach (ang. tick) są wybierane automatycznie (oczywiście jeśli chcemy, możemy zmieniać ich położenie i opisy odpowiednią funkcją, powiemy o tym później).
import pylab as p x = p.arange(0.0, 2.0, 0.01) y1 = p.sin(2.0*p.pi*x) y2 = p.cos(2.0*p.pi*x) p.plot(x,y1,'r:',x,y2,'g') p.legend(('dane y1','dane y2')) p.xlabel('Czas') p.ylabel('Pozycja') p.title('Wykres ') p.grid(True) p.show()
W jednym układzie współrzędnych możemy narysować wiele wykresów. Robimy to podając w jednym poleceniu p.plot kolejno zestawy parametrów opisujące poszczególne linie: współrzędne x, współrzędne y, sposób wykreślania linii. Aby łatwo identyfikować linie można dodać legendę poleceniem legend(). Sposób kontrolowania wyglądu i położenia legendy: help(p.legend) (oczywiście po zaimportowaniu modułu: import pylab as p )
import pylab as p x = p.arange(0.0, 2.0, 0.01) y1 = p.sin(2.0*p.pi*x) y2 = p.cos(2.0*p.pi*x) y = y1*y2 l1, = p.plot(x,y,'b') l2,l3 = p.plot(x,y1,'r:',x,y2,'g') p.legend((l2,l3,l1),('dane y1','dane y2','y1*y2')) p.xlabel('Czas') p.ylabel('Pozycja') p.title('Wykres ') p.grid(True) p.show()
Wykresy możemy dodawać do współrzędnych kolejnymi poleceniami p.plot. Funkcja p.plot zwraca listę linii. Notacja l1, = p.plot(x,y,'b') wydobywa z listy pierwszą linię (Gdyby po l1 nie było przecinka to l1 byłoby listą zawierającą jeden obiekt klasy linia ).
Dzięki nazwaniu poszczególnych obiektów linii możemy kontrolować ich kolejność (i obecność) na legendzie.
import pylab as p zliczenia = p.array([0, 1, 2, 2, 3, 3, 3, 3, 3, 3, 4, 4, 4, 5, 7]) p.hist(zliczenia) p.show()
Do zmiennej zliczenia przypisujemy macierz z ręcznie podanymi wartościami.
Zakres zmienności badanych zliczeń (odkładany na osi X) dzielimy na przedziały (ang. bin) o jednakowej „szerokości”; domyślnie będzie ich 10. Funkcja p.hist() zlicza wystąpienia wartości w binach i rysuje histogram. Funkcja ta zwraca krotkę (array ze zliczeniami, array z binami, lista zawierająca prostokąty, które histogram rysuje, tzw. obiekty Patch).
Pełna lista parametrów tej funkcji jest następująca:
hist(x, bins=10, range=None, normed=False, cumulative=False,
bottom=None, histtype='bar', align='mid',
orientation='vertical', rwidth=None, log=False, **kwargs)
Poniżej opisujemy znaczenie najistotniejszych parametrów.
Pozostałe argumenty kontrolują wygląd i układ rysunku. Ich znaczenie można poznać z dokumentacji help(p.hist)
Wyjaśnienie działania znajduje się w komentarzach do programu:
import pylab as p import numpy mi, sigma = 100, 15 x = mi + sigma * numpy.random.randn(10000) # numpy.random.randn zwraca array z liczbami pseudolosowymi # pochodzącymi z rozkładu normalnego o średniej 0 i wariancji 1 # przemnożenie przez odchylenie standandardowe sigma i dodanie śreniej mi # transformuje rozkład do rozkładu normalnego o średniej mi i wariancji sigma**2 n, bins, patches = p.hist(x, 50, normed=True, facecolor='green', alpha=0.75) # Tu w jawny sposób odbieramy zwracane przez p.hist obiekty # Zmieniamy też: # - ilość binów na 50 # - normujemy histogram do jedności # - ustalamy kolor prostokątów na zielony # - ustawiamy przezroczystość prostokątów na 0.75 bincenters = 0.5*(bins[1:]+bins[:-1]) # wytwarzamy array z centrami binów korzystając z granic binów # zwróconych przez p.hist w macierzy bins y = p.normpdf( bincenters, mi, sigma) # obliczamy wartości w normalnym rozkładzie gęstości prawdopodobieństwa # o średniej mi i wariancji sigma**2 dla wartości bincenters l = p.plot(bincenters, y, 'r--', linewidth=1) # do histogramu dorysowujemy linię p.show()
import pylab as p p.figure() teta = p.arange(0,3*p.pi,0.001) r = teta p.polar(teta, r, color='red') p.figure() r = p.arange(0,1,0.001) teta = -2*2*p.pi*r p.polar(teta, r, color='#ee8d18', lw=3) p.show()
Dotychczasowe wykresy wykonywane funkcją plot rysowane były w kartezjańskim układzie współrzędnych, gdzie podawaliśmy położenie punktów (x, y) jako ich odległości od środka układu w kierunku poziomym i pionowym. W układzie biegunowym położenie punktów podajemy w postaci (r, θ), które opisują odległość od środka układu i kąt (prostej łączącej punkt i środek układu) z osią poziomą.
Narysuj wykresy krzywych, które zdefiniowane są w układzie biegunowym następującymi zależnościami:
, dla 0 < ε < 1.
import pylab as p p.ion() a=1.0 teta = p.arange(0,2*p.pi,0.1) r = a*p.cos(3*teta) linia, = p.polar(teta, r, color='red') for fi in p.arange(0,p.pi,0.01): r = a*p.cos(3*teta+fi) linia.set_ydata(r) p.draw()
Uwaga: ta metoda animowania nie działa w Windows.
Pod linuksem ten typ animacji działa i można go zrozumieć w następujący sposób. Funkcja p.ion() przełącza system graficzny w tryb interaktywny. Wywołanie funkcji p.polar wytwarza obiekt linia dany równaniem r(θ) = acos(3θ). Następnie w każdej iteracji pętli obliczamy nowy zestaw punktów definiujących linię dla aktualnej wartości fi, zgodnie z równaniem r(θ) = acos(3θ + φ). Wyliczone wartości r podstawiamy do obiektu linia. Funkcja p.draw() powoduje wyczyszczenie poprzedniej wersji rysunku i odrysowanie aktualnej reprezentacji obiektu linia.
import pylab as p X = p.array([[1, 3, 2], [2, 2.4, 3], [2, 3, 1], [1, 1, 2], [3, 2, 2]]) p.figure() p.imshow(X) p.figure() p.pcolor(X) p.figure() p.imshow(X, interpolation='nearest') p.colorbar() p.show()
Do rysowania prostych wykresów w przestrzeni trójwymiarowej można użyć dodatku do modułu matplotlib mpl_toolkits.mplot3d. Poniższy przykład demonstruje jego użycie.
from mpl_toolkits.mplot3d import Axes3D from matplotlib import cm import pylab as p import numpy as np fig = p.figure() # wytwarzamy obiekt typu figura ax = Axes3D(fig) # wytwarzamy obiekt osie 3D X = np.arange(-15, 15, 0.3) # wektor opisujący oś X Y = np.arange(-15, 15, 0.3) # wektor opisujący oś Y X, Y = np.meshgrid(X, Y) # zamieniamy wektory osi w siatkę, której węzły są #zadane przez odpowiadające sobie elementy w macierzach X i Y # dla każdego węzła obliczamy jego odległość od początku układu współrzędnych R = np.sqrt(X**2 + Y**2) # dla każdego węzła obliczamy wartość funkcji Z Z = np.sin(R)/R # generujemy rysunek powierzchni # parametry <tt>rstride=2, cstride=2</tt> służą do próbkowania siatki (tu bierzemy co drugi węzeł), # parametr <tt>cmap=cm.jet</tt> definiuje jaką mapę kolorów zastosować ax.plot_surface(X, Y, Z, rstride=2, cstride=2,cmap=cm.jet) # pokazujemy gotowy rysunek p.show()
import pylab as p p.subplot(1,2,1) p.plot([1,2,3],[2,7,8]) p.subplot(1,2,2) p.plot([1,2,3],[4,7,2]) p.show()
import pylab as p ''' Ruch jednostajnie przyspieszony: demonstracja subplotów i całkowania metodą Eulera''' dt=0.1 # definiujemy krok czasu # -- mniejszy krok czasu większa dokładność całkowania t=p.arange(0,10,dt) # wytwarzamy oś czasu # definiujemy parametry początkowe dla rozważanego ruchu x0=1 v0=0.5 a0=0.5 # analityczne całkowanie ruchu ze stałym przyspieszeniem # daje następujące wyrażenia: a=a0*p.ones(len(t),) # przyspieszenie v=v0 + a*t # prędkość x=x0 + v0*t + a*t**2/2.0 # położenie # rysujemy wykresy # przyspieszenia, prędkości i położenia od czasu # uzyskane przez całkowanie analityczne p.subplot(3,1,1) p.plot(t,a) p.subplot(3,1,2) p.plot(t,v) p.subplot(3,1,3) p.plot(t,x) # obliczamy numerycznie wartości prędkości w kolejnych chwilach czasu v_num = p.zeros(len(t),) v_num[0]=v0 for i in range(1,len(t)): v_num[i]=v_num[i-1]+a0*dt # obliczamy numerycznie wartości położenia w kolejnych chwilach czasu x_num = p.zeros(len(t),) x_num[0]=x0 for i in range(1,len(t)): x_num[i]=x_num[i-1]+v_num[i-1]*dt # do wykresów uzyskanych analitycznie dorysowujemy wyniki numeryczne p.subplot(3,1,2) p.plot(t,v_num,'r') p.subplot(3,1,3) p.plot(t,x_num,'r') p.show()
Proszę zrobić program wykreślający krzywą y(x), gdzie x = Asin(ωat + φa) zaś y = Bsin(ωbt + φb).
Twój program, jeśli ma robić coś użytecznego, zazwyczaj musi komunikować się z użytkownikiem. Na przykład, na podstawie wprowadzonych przez użytkownika danych program wykonuje pewne obliczenia i następnie wypisuje wynik, przekazując go tym samym użytkownikowi. Ten moment kiedy informacja zostaje wprodzona do programu, lub kiedy program zapisuje wyniki swojego działania gdzieś indziej niż w swojej pamięci, nazywamy operacjami wejścia/wyjścia.
W najprostszym wypadku komunikację z otoczeniem można wykonać przy użyciu funkcji raw_input i wyrażenia print, które odpowiednio wczytują linijkę i wypisują linijkę tekstu.
W bardziej zaawansowanej wersji komunikacji z otoczeniem, program odczytuje i zapisuje informację w plikach na dysku. Pozwala to na przechowywanie danych w sposób trwały — pliku w przeciwieństwie do napisów na ekranie przeżywają wyłączenie i włączenie komputera. Tworzenie plików, odczytywanie i zmiana ich zawartości to podstawowe narzędzie, które zostanie przedstawione w tym rozdziale.
#!/usr/bin/python # -*- coding:utf-8 -*- # user_input.py def reversed(text): return text[::-1] def palindrom(text): return text == reversed(text) napis = input('Wprowadź napis: ') if palindrom(napis): print("Tak, to palindrom") else: print("Nie, to zwykłe słowo")
$ python user_input.py Wprowadź napis: abrakadabra Nie, to zwykłe słowo $ python user_input.py Wprowadź napis: kajak Tak, to palindrom $ python user_input.py Wprowadź napis: 1234321 Tak, to palindrom
Funkcja reversed zwraca swój argument w odwrotnej kolejności wykorzystując zaawansowane indeksowanie. Poprzednio była mowa o indeksowaniu z dwoma argumentami [a:b] — czyli braniu podsekwencji od pozycji a do pozycji b.
Tutaj pierwsze dwa parametry są pomięte, czyli przyjmują wartości domyślne. Jako trzeci parametr, czyli krok, podajemy –1. Ujemny krok oznacza przejście od końca do początku, w ten sposób otrzymujemy sekwencję w odwrotnej kolejności.
Funkcja raw_input używa podanego argumentu jako zachęty dla użytkownika i czeka na jego odpowiedź. Po tym użytkownik coś wpisze, a tak naprawdę po tym jak naciśnie klawisz Enter, funkcja raw_input kończy swoje działanie i zwraca wpisany tekst.
Program porównuje wpisany tekst z jego odwrotnością. Porównywanie sekwencji oznacza przyrównanie ich element po elemencie, czyli w tym wypadku litera po literze. W wypadku gdy porównanie nie znalazło różnic, możemy oznajmić, że tekst jest słowem lustrzanym.
Różne typy plików opisane są w materiałach do wykładu. Dostęp do plików na dysku uzyskuje się w Pythonie poprzez klasę file, która reprezentuje otwarty plik. Plik otwieramy wywołując funkcję open podając nazwę pliku. Otwarcie pliku oznacza, że system operacyjny zezwolił nam na dostęp do niego, i możemy wykonywać operacje odczytu i zapisu.
Część pierwsza użycia pliku:
#!/usr/bin/python # -*- coding: utf-8 -*- tekst = '''\ Litwo! Ojczyzno moja! ty jesteś jak zdrowie; Ile cię trzeba cenić, ten tylko się dowie, Kto cię stracił. Dziś piękność twą w całej ozdobie Widzę i opisuję, bo tęsknię po tobie. ''' f1 = open('wiersz.txt', 'w') # 'w' oznacza, że będziemy pisać do pliku f1.write(tekst) f1.close() # 'wypchnięcie' zmian do pliku przez jego zamknięcie
$ python zapis_pliku.pyCzęść druga użycia pliku:
#!/usr/bin/python # -*- coding: utf-8 -*- f2 = open('wiersz.txt') # brak drugiego argumentu oznacza 'tylko odczyt' for wers in f2: print wers,
$ python odczyt_pliku.py Litwo! Ojczyzno moja! ty jesteś jak zdrowie; Ile cię trzeba cenić, ten tylko się dowie, Kto cię stracił. Dziś piękność twą w całej ozdobie Widzę i opisuję, bo tęsknię po tobie.
Tworzymy plik wiersz.txt otwierając go w trybie do zapisu (z argumentem 'w'). Jeśli plik jeszcze nie istniał, to zostaje stworzony pusty, a jeśli już istniał, to jego zawartość zostaje wykasowana w momencie otwarcia. Do tego pliku zapisujemy tekst jako treść.
Następnie otwieramy ten sam plik ponownie, tym razem w trybie do odczytu. Następnie linijka po linijce odczytujemy plik i wypisujemy każdą linijkę.
Program jest bardzo prosty, ale jest tu parę drobiazgów, na które warto zwrócić uwagę:
Pliki mogą być otwierane w różny sposób — mówimy, że są otweirane w danym trybie. Dostępne tryby to:
Jeśli pominiemy tryb to domyślnie przyjmuje on wartość r (tylko do odczytu).
W niektórych systemach operacyjnych rozróżniane są pliki tekstowe i binarne. Każdy plik można otworzyć w jednym i w drugim trybie, efektem jest nieco inna obsługa plików przez program.
Niemniej otwieranie plików binarnych w trybie tekstowym nie jest dobrym pomysłem.
Różnica między dwoma trybami jest taka, że w trybie „tekstowym” biblioteka obsługująca odczytywanie z i zapisywanie do pliku w szczególny sposób obsługuje znaki końca linii. W przypadku trybu „binarnego” taka specjalne traktowanie nie ma miejsca i program dostaje dokładnie takie dane jakie odczytał z pliku.
Jak w pliku tekstowym jest zapisany „koniec linii”? Na kartce początek nowego wiersza oznacza się poprzez umieszczenie pierwszej litery blisko lewego krańca tekstu. W pliku coś takiego nie jest możliwe — bo każdy plik, również tekstowy jest po prostu ciągiem bajtów. Sytuacja jest podobna do cytowania wierszy w felietonach w gazecie, gdzie znaki nowej linii oznacza się używając kreski:
To jest pierwsza linijka/To jest druga linijka.
W plikach do rozdzielania wierszy używa się pewnej ustalonej sekwencji. Kiedy program natrafi na taki bajt czy bajty w pliku zawierającym tekst, to rozumie się, że jest to koniec poprzedniej i początek następnej linijki.
Wszystko byłoby dobrze, gdyby nie fakt, że w różnych systemach operacyjnych używa się różnych sekwencji do oznaczenia końca linii. W systemach UNIXowych (takich jak Linux czy BSD) używa się pojedynczego bajtu o wartości 0x0a. Na Macintoshach też używa się jednego bajtu, ale o wartości 0x10. Natomiast w systemach Microsoftu używa się dwóch bajtów — 0x0d 0x0a.
Aby programista nie musiał pamiętać tych wartości, w kodzie Pythona, tak samo jako w C, Perlu i wielu innych językach, \n i \r oznaczają odpowiednio 0x0d i 0x0a. Na dodatek jeśli otworzy się plik w trybie tekstowym, po odczytaniu danych z pliku, wszystkie specyficzne dla danego systemu operacyjnego znaki końca linii są zamieniane na 0x0a. Pozwala to na wygodną obsługę plików tekstowych.
Proszę pobrać plik http://brain.fuw.edu.pl/~jarek/SYGNALY/TF/c4spin.txt.
import pylab import numpy fi = open('c4spin.txt') x=[] for linijka in fi: x.append(int(linijka)) signal = numpy.array(x) pylab.plot(signal/20) pylab.show()
Otwieramy plik tekstowy c4spin.txt. Każda linijka w tym pliku zawiera jedną próbkę sygnału EEG. W pętli kolejno odczytujemy z pliku linie tekstu, interpretujemy je jako liczby całkowite (int), a następnie dodajemy na koniec listy x. Po zakończeniu odczytu z pliku zamykamy go. Dla większej wygody operacji na sygnale dobrze jest przekształcić listę w tablicę modułu numpy. Teraz łatwo możemy np. skalibrować sygnał. Załóżmy, że dane pochodziły z przetwornika analogowo-cyfrowego, o którym wiemy, że na napięcie 1 µV na jego wejściu daje na wyjściu liczbę 20. Zatem żeby wyskalować nasz sygnał w µV trzeba podzielić go przez 20.
W poprzednim przykładzie próbki były wczytywane w pętli — ale jest też funkcja która robi dokładnie to samo.
import pylab import numpy signal = numpy.loadtxt('c4spin.txt') pylab.plot(signal/20) pylab.show()
Te dane mogą być też przechowywane np. w postaci binarnej jako dwubajtowe liczby całkowite (typu short, plik http://brain.fuw.edu.pl/~jarek/SYGNALY/c4spin.bin) lub np. czterobajtowe liczby zmiennoprzecinkowe (typu single, plik http://brain.fuw.edu.pl/~jarek/SYGNALY/c4spinf.bin).
Proszę podejrzeć wszystkie trzy pliki w powłoce systemu poleceniem less nazwa_pliku.
Następnie proszę napisać program rysujący jednakowe wykresy wczytując dane z każdego z trzech plików.
Wskazówka: pliki binarne wczytuj funkcją fromfile z modułu numpy.
Proszę pobrać plik http://brain.fuw.edu.pl/~jarek/SYGNALY/prawahj12 — są tam zapisane dane: 24 kanały pomiaru (próbkowane z częstością 128 Hz), w każdym po 1024 punkty pomiarowe, eksperyment powtórzono 57 razy, zapisano w pliku binarnym jako multipleksowane szesnastobitowe liczby całkowite (short).
Z pliku proszę wybrać kanał 1 i wykreślić pierwsze powtórzenie.
import pylab import numpy x=numpy.fromfile('prawahj12','short') signal = x[0::24] pylab.plot(signal[:1024]) pylab.show()
Inny sposób:
import pylab as p import numpy x=numpy.fromfile('prawahj12','short') x=numpy.reshape(x,(57,1024,24)) p.plot(x[0,:,0]) p.show()
Ten sam plik co w poprzednim przykładzie: http://brain.fuw.edu.pl/~jarek/SYGNALY/prawahj12 — 24 kanały, 128 Hz, 57 powtórzeń po 1024 punkty, format liczb: szesnastobitowe całkowite. Z pliku tego wybieramy kanał 1 i rysujemy na wykresie pierwsze powtórzenie.
import pylab import numpy x = numpy.fromfile('prawahj12','short') signal = x[0::24] pylab.plot(signal[:1024]) pylab.show() x.reshape(-1, 24) pylab.figure() pylab.plot(x[0:1024,0]) pylab.show()
Standardowa biblioteka Pythona zawiera moduł pickle (słowo to oznacza sposób konserwowania m. in. ogórków). Służy on do zapisywania dowolnych obiektów Pythona w pliku, i odwrotnie, do późniejszego oczytywania i rekreacji obiektów. W języku angielskim mówi się o persistent storage („pamięci trwałej”), by podkreślić, że obiekty są zachowane w sposób trwały, w przeciwieństwie do obiektów w programie, które znikają po jego zakończeniu.
#!/usr/bin/python # -*- coding:utf-8; -*- # Filename: pickling.py import pickle # the name of the file where we will store the object trwala_lista_zakupow = 'lista_zakupow.data' # lista rzeczy do kupienia lista_zakupow = ['jabłka', 'ziemniaki', 'pomidory'] # zapisanie do pliku f = open(trwala_lista_zakupow, 'wb') pickle.dump(lista_zakupow, f) f.close() # usunięcie zmiennej 'nie-trwałej' del lista_zakupow # odzyskanie zmiennej f = open(trwala_lista_zakupow, 'rb') lista_zakupow = pickle.load(f) for rzecz in lista_zakupow: print rzecz
$ python pickling.py
jabłka
ziemniaki
pomidoryAby zapisać dane „zakonserwowane” do pliku, otwieramy plik w trybie do zapisu. Parametr 'wb' oznacza write-binary, czyli dodatkowo mówimy, że dane mają być zapisane do pliku dokładnie tak, jak je podajemy. W przypadku wersji Python 2.x brak tego parametru nie ma znaczenia, ale nie szkodzi. Natomiast w nowszych wersjach Pythona (3.x), jest on potrzebny. Właściwego zapisu dokonujemy funkcją pickle.dump
Aby odczytać „zakonserwowane” dane z pliku, otwieramy plik w trybie do odczytu. Funkcja pickle.load pozwala nam odczytać dokładnie to co zapisaliśmy.
W module numpy dostępnych jest kilka funkcji obsługujących wygodny zapis i odczytywanie obiektów, a w szczególności macierzy: save,savetxt. Poniżej opiszemy nieco dokładniej funkcję numpy.savez. Jej składnia jest następująca:
numpy.savez(plik, *argumenty, **argumenty_nazwane)
Funkcja ta umożliwia zapis kilku macierzy do jednego spakowanego archiwum w formacie .npz. Jeśli wywołamy funkcję z argumentami nazwanymi nazwy to nazwy te zostaną zapisane razem z zawartością macierzy. Jeśli nie podamy nazw, tzn wywołamy funkcję numpy.savez tylko z lista zmiennych do zapisania to zostaną one zapisane z domyślnymi nazwami arr_0, arr_1, ...
Parametry:
Format pliku .npz to spakowane archiwum. Każdy plik w tym archiwum przechowuje jedna z zapisywanych zmiennych w formacie .npy i ma nazwę zmiennej, którą przechowuje.
Do wczytywania plików .npz służy funkcja numpy.load. Obiekt, który ona zwraca zawiera pole files zawierające listę dostępnych kluczy. Klucze te mogą być użyte do indeksowania przechowywanych w tym obiekcie zmiennych.
import numpy as n x = n.arange(10) y = n.sin(x) #użycie savez z *argumentami, macierze zapisywane są z domyślnymi nazwami n.savez('plik_testowy', x, y) wczytane = n.load('plik_testowy1.npz') wczytane.files ['arr_1', 'arr_0'] wczytane['arr_0'] array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) #użycie savez z **argumentami_nazwanymi, macierze zapisywane są z nazwami podanymi jako klucze. n.savez('plik_testowy2', x=x, y=y) wczytane = n.load('plik_testowy2.npz') wczytane.files ['y', 'x'] wczytane['x'] array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
W pracy z programem Matlab firmy Mathworks można zachować zmienne z bieżącego środowiska pracy do pliku. Możliwości są bardzo podobne do numpy.savez. Format jest natomiast zupełnie inny.
Pakiet Scipy pozwala na zapis i odczyt takich plików, mają one zwyczajową końcówkę .mat.
import scipy.io as io D = io.loadmat('test1.mat') # D zawiera słownik {zmienna:wartość} io.savemat('test2.mat', {'nowa_zmienna': 666})
Scipy nie obsługuje wszystkich wersji format plików matlabowych i nie wszystkie struktury danych z Matlaba daje się jednoznacznie i czysto przełożyć na struktury Pythonowe, więc funkcje loadmat i savemat mają sporo opcji regulujących ten proces. (zob. help(io.loadmat) lub docs.scipy.org)
Powyżej zostały przedstawione dwa mechanizmy zachowywania danych w postaci obiektów bezpośrednio do plików. Matlabowy format '.mat' jest przewidziany do przechowywania struktur danych z Matlaba, a ponieważ menażeria obiektów w Pythonie jest bogatsza, to nie wszystko da się zachować. Niemniej założenie jest takie samo — by zapisać obiekty jako ciąg bajtów w pliku. Przecież już normalnie można dane odczytywać z i zapisywać do plików, więc kiedy programista powinien zdecydować się na picklowanie?
Kiedy zapisujemy dane do pliku, używamy pewnego określonego formatu. Takim formatem może być np. XML, czy jakaś odmiana tzw. CSV (Comma Seperated Values). Takim formatem jest też pickle czy mat-file. Istotna różnica jest taka, że w przypadku formatów przeznaczonego do przechowywania stanu z danego języka, nie ma osobnej warstwy tłumaczącej, która bierze dane odczytane z pliku i wywołuje funkcje konstruujące obiekty. Ponieważ dane są w formacie tak blisko powiązanym z gotowymi obiektami, to sam moduł pickle może zrekonstruować obiekty.
To którego formatu należy użyć, zależy od celu w jakim dane zapisujemy i od tego z kim chcemy te dane wymieniać.
O formacie pickle można powiedzieć, że:
Wynika to z tego, że zarówno implementacja jest dostępna tylko na Pythona, jak i z tego, że sam format jest bezpośrednio związany z pythonowymi strukturami danych.
Wynika to z tego, że wersja Pythona za rok czy dwa na pewno te dane przeczyta, ale nie ma pewności czy za 20 lat się uda.
Można pokrótce powiedzieć, że picklowanie to użyteczny mechanizm roboczy, na teraz, do komunikacji w określonym ekosystemie programów. Do komunikacji z innymi ludźmi, czy przetrzymywania danych archiwalnych, pickle się nie nadaje.
Czasami w trakcie wykonywania operacji przez program występuje sytuacja, która uniemożliwia jej wykonanie. Typowe sytuacje wymagające specjalnych reakcji programu to:
Problem obsługi sytuacji wyjątkowych jest dodatkowo skomplikowany przez fakt, że często sytuacja wymagająca specjalnego obsłużenia występuje w funkcji, która została wywołana przez inną funkcję, która została wywołana przez inną funkcję,... Niemniej, właściwym miejscem na reakcję, często nie jest miejsce stwierdzenia błędu, gdzieś głęboko w wywołaniu funkcji wykonującej jedno specjalizowane zadanie, a wręcz przeciwnie, bardzo płytko, w miejscu sterowania całym wykonaniem programu.
Jako przykład rozważmy podany wcześniej błąd otwarcia pliku. Jeśli taki błąd zostanie napotkany przez program, którego zadaniem było odczytać z dysku i wyświetlić jedno zdjęcie, to właściwą reakcją będzie wypisanie informacji o błędzie i zakończenie działania programu. Jeśli taki błąd wystąpi w działaniu przeglądarki ściągającej pliki ze zdalnego serwera i przechowującej tymczasowo pliki na dysku, to właściwą reakcją będzie ponowne ściągnięcie zdjęcia ze zdalnego serwera, bez informowania użytkownika lub przerywania pracy.
Potrzebny jest zatem mechanizm przekazywania informacji o błędzie wzdłuż łańcucha wywołań funkcji. Jednym z takich mechanizmów są tzw. wyjątki.
Zasygnalizowanie w programie sytuacji nietypowej, która nie powinna się normalnie wydarzyć (czyli sytuacji wyjątkowej) nazywa się „rzuceniem” lub „podniesieniem” wyjątku. Informacje precyzujące zaistniałą sytuację są zapamiętywane. W tym celu zostaje stworzony obiekt nazywany wyjątkiem (exception). Dzieje się to wszystko z wykorzystaniem instrukcji raise:
raise <typ-wyjątku>(argumenty precyzujące sytuację)
Na przykład żeby zasygnalizować, że funkcja została wywołana z nieodpowiednim argumentem (np. liczbą ujemną gdy oczekiwaliśmy dodatniej), można rzucić wyjątek typu ValueError (zgodnie z konwencją używany w takiej sytuacji).
raise ValueError('liczba ujemna')
Wyjątek może też zostać rzucony nie przez program, a przez samego Pythona (interpreter programu). Np. gdy program spróbuje wykonać niedozwolone dzielenie przez 0, zostaje automatycznie rzucony wyjątek typu ZeroDivisionError.
Jeśli w czasie wykonywania pewnej funkcji zostanie rzucony wyjątek (niezależnie czy explicite, czy automatycznie), to wykonywanie tej funkcji zostanie przerwane i program powróci do miejsca, z którego ta funkcja została wywołana tzn. do funkcji o jeden poziom wyżej. Wykonywanie kolejnych funkcji jest przerywane, aż dojdziemy do najwyższego poziomu. Tam, jako domyślna reakcja na błąd, zostaje wypisana informacja o wystąpieniu wyjątku (jego typ, miejsce wystąpienia, jak też dodatkowe informacje) i program kończy działanie.
Domyślna reakcja Pythona na błąd nie zawsze jest pożądana. Jak wynika z przykładu z przeglądarką internetową, czasami właściwą reakcją nie jest przerwanie działania, ale podjęcie działań zaradczych (załadowanie zdjęcia z oryginalnego serwera) i kontynuowanie wykonywania programu. Mechanizm kaskadowego przerywania funkcji (i w konsekwencji całego programu) w momencie wystąpienia wyjątku może być kontrolowany. Służy do tego konstrukcja try...except. W bloku programu pomiędzy try: a except wpisujemy fragment kodu, który może rzucić wyjątek. W bloku programu występującym po except: opisujemy co należy zrobić jeśli pojawi się konkretny typ wyjątku. Podstawowa składnia jest następująca:
try: <operacje-mogące-rzucić-wyjątek> except <typ-wyjątku> as <nazwa-zmiennej>: <operacje-zaradcze>
W przykładzie z przeglądarką internetową:
def wyswietl_obrazek(nazwa): try: obrazek = wczytaj_z_przechowalni(nazwa) except BladOdczytuZPrzechowalni as opis: zapisz_do_dziennika('nie udało się odczytać z przechowalni:' + nazwa, opis) obrazek = wczytaj_z_serwera(nazwa) wyswietl(obrazek)
Jeśli funkcja wczytaj_z_przechowalni() rzuci wyjątek typu BladOdczytuZPrzechowalni, jesteśmy przygotowani na jego obsługę i wywołujemy wczytaj_z_serwera(). Jeśli operacja wczytaj_z_przechowalni() powiedzie się, to wczytaj_z_serwera() nie zostanie użyte. Niezależnie od tego którym sposobem uzyskaliśmy obrazek, zostaje on w końcu wyświetlony przy użyciu funkcji wyswietl(). Jak już wspominaliśmy, wyjątek jest obiektem. W konstrukcji except <typ-wyjątku> as <nazwa-zmiennej>: wiążemy ten przechwycony obiekt z nazwą nazwa-zmiennej. Dzięki temu możemy z tym obiektem coś zrobić. W naszym przykładzie funkcja zapisz_do_dziennika() jako jeden z argumentów dostaje przechwycony wyjątek aby odnotować fakt i okoliczności jego wystąpienia w pliku dziennika.
Rzucanie wyjątków jest faktycznie bardzo proste i zostało powyżej opisane w sposób w miarę kompletny. Natomiast obsługa wyjątków jest bardziej rozbudowana i dopuszcza następujące typy reakcji:
[przypis: o BaseException i przyjaciołach]
try: <operacje-mogące-rzucić-wyjątek> except <typ-wyjątku> as <nazwa-zmiennej>: # po pierwsze <operacje-zaradcze> # Tutaj przechwytujemy wyjątek jednego określonego typu. # Istnieje też starsza notacja (Python <= 2.5 ?), gdzie # zamiast 'as' używa się przecinka. Jej wadą mniejsza # ekspresyjność i możliwość pomylenia sytuacji gdy # chcielibyśmy przechwycić dwa różne typy wyjątków (patrz # poniżej) bez zachowywania wyjątku do zmiennej, z sytuacją # taką jak tutaj, gdzie chcemy przechwycić wyjątek określonego # typu i zachować go do zmiennej. except <typ-wyjątku-1>, <typ-wyjątku-2> as <nazwa-zmiennej>: # po drugie <operacje-zaradcze> # Tutaj przechwytujemy wyjątek typu typ-wyjątku-1 # lub typ-wyjątku-2. except Exception: # po trzecie <operacje-zaradcze> # Tutaj przechwytujemy wyjątek każdego podtypu Exception. else: # po czwarte <operacje-wykonywane-w-przypadku-powodzenia> # Tutaj należy wpisać kod który jest kontynuacją # <operacji-mogących-rzucić-wyjątek>. Napisanie go w tym miejscu # powoduje, że wyjątek rzucony tutaj nie zostanie złapany. finally: # po piąte <operacje-wykonywane-zawsze> # Tutaj należy wpisać działania takie jak zamknięcie plików # i zwolnienie blokad, które muszą być wykonane niezależnie # od tego czy operacja się powiodła czy też nie. # Innym mechanizmem, który może często zastąpić taki blok # else, jest konstrukcja 'with'.
W wypadku gdy wykorzystamy gołe except:, powinniśmy zachować dużą dozę ostrożności. W ten sposób przechwycimy np. wyjątki typu ValueError związane z niewłaściwym wywołaniem funkcji gdzieś w kodzie. Ale przechwycimy także wszystkie inne wyjątki, nawet takie których się nie spodziewaliśmy. W tym miejscu często programiści wypisują komunikat o błędzie, a następnie ponownie rzucają ten sam wyjątek aby był dostępny dla kolejnych w hierarchii funkcji (zob. przykład w podręczniku Pythona).
Przykład wykonywalny:
# -*- coding: utf-8 -*- # plik exc1.py import random, time try: print 'Zaczynam obliczenia... ', time.sleep(3 * random.random()) print 'obliczenia trwają... ', wybor = random.randrange(0, 5) if wybor == 0: print 'wykonamy dzielenie przez 0' print 1 / 0 print 'udało się :)' elif wybor == 1: print 'rzucimy wyjątek dzielenia przez zero samemu' raise ZeroDivisionError('jak mogłaś?') print 'znowu się udało :)' elif wybor == 2: print 'rzucimy jeszcze inny wyjątek' raise ValueError('wylosowałeś krótką słomkę') print 'udało się ponownie :)' else: print '...obliczenia zakończone!' except ValueError as e: print "Wiem co było źle podczas obliczeń:", e raise except ArithmeticError as e: # obejmuje ZeroDivisionError print "Coś było źle podczas obliczeń." import sys print "Napotkałem taki błąd:", sys.exc_info()[1] raise # użycie tej instrukcji wytwarza sytuację wyjątkową taką, # jaka zdarzyła się ostatnio # czyli rzuca wyjątek taki, jaki właśnie przechwyciliśmy else: print "Obliczenia przebiegły pomyślnie." finally: print "Koniec bloku try-except-else-finally." print "What's next?"
Jeśli zachowamy ten przykład do pliku exc1.py to możemy wykonywać go wielokrotnie i patrzeć na wypisywane komunikaty. W szczególności te zakończone śmieszkiem nigdy nie zostaną wypisane :). Z drugiej strony komunikat o końcu bloku zostanie wypisany zawsze.
Nie wszystkie języki programowania mają mechanizm wyjątków, nie zawsze też wyjątki są właściwym sposobem obsługi błędów. W zależności od sytuacji wykorzystywane jest parę mechanizmów które warto znać. Są one pokrótce opisane poniżej.
Ustalamy, że pewna szczególna wartość oznacza błąd. Np. w przypadku wczytywaniu obrazków, wynik poprawny jest jakimś obiektem. Możemy wobec tego umówić się, że wynik None oznacza błąd — niemożność wczytania obrazka.
Po wykonaniu operacji mogącej zwrócić wartość szczególną aby zasygnalizować błąd, trzeba sprawdzić czy uzyskaliśmy normalny wynik, czy też nie.
def wyswietl_obrazek(nazwa): obrazek = wczytaj_z_przechowalni(nazwa) if obrazek is None: zapisz_do_dziennika('nie udało się odczytać z przechowalni:' + nazwa) obrazek = wczytaj_z_serwera(nazwa) wyswietl(obrazek)
def wyswietl_obrazek(nazwa): obrazek = wczytaj_z_przechowalni(nazwa) if obrazek.kod == PUSTY_OBRAZEK: zapisz_do_dziennika('obrazka nie ma w przechowalni:' + nazwa) obrazek = wczytaj_z_serwera(nazwa) elif obrazek.kod == ZLY_OBRAZEK: zapisz_do_dziennika('obrazek z przechowalni jest niepoprawny:' + nazwa) obrazek = wczytaj_z_serwera(nazwa) wyswietl(obrazek)
Ten typ zwracania informacji o błędzie jest używany zwłaszcza wtedy, gdy jest potrzebne dużo możliwych wartości świadczących o niepowodzeniu, a tylko jedna świadcząca o powodzeniu operacji. Powłoka UNIXowa używa konwencji takiej, że programy zwracają 0 jeśli udało im się wykonać poprawnie, a każda inna wartość oznacza błąd. Możliwych błędów jest dużo: brak pliku na którym miała być wykonana operacja, brak pozwolenia, przerwanie operacji przez użytkownika, ...
Najbardziej znane użycie tego mechanizmu to zmienna errno używana przy wywoływaniu funkcji systemowych w języku C (takich jak open do otwierania plików czy execve do wywołania innego programu). W wypadku niepowodzenia, wywołanie funkcji zwraca szczególną wartość (–1), a do globalnej zmiennej errno (od numer błędu, ang. error number) zostaje zapisany kod błędu. Program może następnie zajrzeć do tablicy opisów błędów i wydrukować komunikat dla użytkownika.
Wadą tej metody jest to, że zmienna errno jest jedna. Bezpośrednio po wywołaniu funkcji, przed wywołaniem jakiejkolwiek funkcji systemowej, musimy ją zapisać w inne miejsce, tak by nie uległa nadpisaniu w przypadku kolejnego błędu. Generalnie taka obsługa błędów jest bardzo natrętna — po każdym wywołaniu funkcji systemowej musi być parę linijek sprawdzających, czy wartość zwrócona przez funkcję nie świadczy o błędzie, i ewentualnie jaki jest to błąd.
Takie zachowanie — wyświetlenie komunikatu i zakończeniu programu — jest bardzo złym wyjściem gdy zostanie użyte w bibliotece lub kodzie który może być wywołany z więcej niż jednego miejsca. Wynika to z tego, że takie „zaszycie” zachowania w miejscu wykrycia błędu silnie ogranicza możliwości użycia danej funkcji.
Niemniej, jeśli piszemy program od początku do końca i nie zależy nam na ponownym wykorzystaniu tego kodu, to jest to rozwiązanie proste, zwięzłe i szybkie.
def wyswietl_obrazek(nazwa): "Odczytaj obrazek z dysku i wyświetl. Niepowodzenie kończy program." obrazek = wczytaj_z_dysku(nazwa) if obrazek is None: import sys print 'nie udało się odczytać:' + nazwa) sys.exit(1) wyswietl(obrazek)
W tym przykładzie użyte są dwie konwencje — funkcja wczytaj_z_dysku w przypadku błędu zwraca wartość szczególną (None), natomiast funkcja wywołująca wyswietl_obrazek w reakcji na ten błąd kończy program.
Zastosowanie tego sposobu obsługi błędów ma jeszcze jeden aspekt, który w praktyce okazuje się kłopotliwy, niezależnie od tego, czy zakończeniu programu jest właściwym wyjściem. Przed zakończeniem programu informujemy wypisując komunikat o błędzie na standardowe wyjście. Co się stanie jeśli w pewnym momencie dodamy interfejs graficzny do naszego programu który pozwoli na wywołanie programu nie z konsoli, ale z graficznego menu? Komunikaty wypisywane na konsolę ulegną zagubieniu, bo w sytuacji użycia graficznego interfejsu użytkownika (GUI), właściwym mechanizmem komunikacji z użytkownikiem jest wyświetlenie okienka. Programista dodający GUI musiałby przejść program i zmienić wszystkie miejsca gdzie print zostaje użyte do wypisania błędu, lub też spowodować, że wszystkie użycia print prowadzą do wyświetlenia komunikatu również w GUI.
Bardzo ogólnym mechanizmem obsługi błędów jest oddelegowanie obsługi z powrotem do strony wywołującej. W momencie wywołania funkcji bibliotecznej, jako parametr podajemy funkcję (z ang. handler), która zostanie wywołana w wypadku wystąpienia błędu. Decyzja o sposobie obsługi błędu zostaje wtedy podjęta wewnątrz funkcji handler — np. nie ma żadnych ograniczeń na to jak (i czy) komunikat o błędzie będzie wypisany. Funkcja handler zwraca kod, na podstawie którego działanie funkcji w której wystąpił błąd jest kontynuowane lub przerywane.
Silnym plusem jest tutaj możliwość kontynuowania obliczeń. Silnym minusem jest inwazyjność tej metody i konieczność napisania wyspecjalizowanej funkcji do obsługi błędów.
Jako przykład mogą posłużyć powiązane funkcje numpy.seterr i numpy.seterrcall. Pierwsza z nich może być użyta do ustawienia jako obsługę błędów wywołania funkcji zarejestrowanej przy pomocy drugiej z nich. Funkcja zarejestrowana jako obsługa błędów otrzymuje w momencie wywołania informację o błędzie i może wypisać komunikat i ewentualnie rzucić wyjątek. Jeśli wyjątek nie zostanie rzucony, obliczenia są kontynuowane.
Python jest językiem w którym mechanizm wyjątków jest szczególnie szeroko wykorzystywany. Wynika to po części z tego, że konstrukcja try...except jest wydajna, tak że nie ma sensu (z punktu szybkości działania programu), prewencyjnie sprawdzać czy jakiś warunek jest spełniony, by uniknąć przechwytywania wyjątku. Jest to ujęte zwięźle w zaleceniu dla pythonistów
Easier to ask for forgiveness than permission
Rozpowszechnione użycie wyjątków ma też tę zaletę, że pominięcie obsługi błędów prowadzi, w przypadku niepoprawnego działania programu, do jego szybkiego zakończenia i wypisania ciągu wywołań funkcji w programie (ang. stack trace). Nie jest to może rozwiązanie najbardziej eleganckie, często też komunikat jest niezrozumiały dla nie-programistów, ale generalnie jest to znacznie lepsze wyjście niż dalsze wykonywanie programu.
Jak każdy ogólny mechanizm, wyjątki nie są panaceum, tylko jednym z mechanizmów do użycia w połączeniu z innymi w zależności od sytuacji. Program przeznaczony dla końcowego użytkownika powinien gdzieś na wysokim poziomie zawierać blok try...except przechwytujący większość wyjątków. Użytkownik powinien zostać poinformowany o zaistniałej sytuacji w możliwie łagodnych słowach. Następnie powinien zostać użyty inny z wymienionych mechanizmów, zapewne po prostu zakończenie programu.
Podobnie jeśli mamy program wykonujący długotrwałe obliczenia numeryczne, i w jednym z 10000 punktów siatki obliczenia zakończą się rzuceniem wyjątku, to zakończenie działania programu zapewne nie jest rozwiązaniem preferowanym przez użytkownika. Należy zapewne zapisać wartość szczególną (NaN ?), wypisać komunikat, i kontynuować obliczenia.
Załóżmy, że chcemy w celu optymalizacji wykonywać funkcję obliczeniową tylko raz dla każdego zestawu argumentów.
def _funkcja(argument): "Ta funkcja symuluje długotrwałe, trudne symuluacje." import time, random time.sleep(3) wynik = random.random() return wynik _cache = {} def funkcja(argument): try: return _cache[argument] except KeyError: wynik = _cache[argument] = _funkcja(argument) return wynik
Tutaj przedrostek _, zgodnie z powszechnie przyjętą w Pythonie konwencją, oznacza funkcję lub zmienną prywatną — element implementacji, do którego nie należy odwoływać się spoza tego modułu. Interfejs publiczny to funkcja funkcja.
Załóżmy, że chcemy zwrócić wartość ze słownika, jeśli tam jest, a w przeciwnym wypadku obliczyć domyślną wartość dla danego klucza.
Tak nie należy (wersja LOOK BEFORE YOU LEAP — spójrz zanim skoczysz):
if key in dictionary: return dictionary[key] else: return compute_default_value_for(key)
To, że tak nie należy, wynika z faktu, że jeśli słownik jest wystarczająco duży, to najbardziej pracochłonna operacja zostanie wykonana dwa razy. Tą operacją jest przeszukanie słownika, by znaleźć odpowiedni klucz — niezależnie czy się uda go znaleźć, czy nie, zajmuje to tyle samo czasu.
Należy po prostu spróbować (wersja EASIER TO ASK FOR FORGIVENESS THAN PERMISSION — łatwiej prosić o wybaczenie niż o pozwolenie):
try: return dictionary[key] except KeyError: return compute_default_value_for(key)
Załóżmy, że napisaliśmy prosty programik liczący linijki komentarza (zaczynające się od znaku #) w pliku.
# komentarze.py import sys file = open(sys.argv[1]) count = 0 for line in file: count += line.startswith('#') print 'liczba linijek z komentarzem =', count
Czy ten program ma jakąkolwiek obsługę błędów? Explicite nie, ale dzięki temu, że funkcje wbudowane w Pythona (funkcje biblioteczne) rzucają wyjątki w przypadku niepowodzenia, program zachowa się poprawnie w przypadku wystąpienia różnego rodzaju sytuacji nieprzewidzianych. Nawet jeśli komunikat nie będzie doszlifowany, to będzie zawierał informacje pozwalające (programiście) na rozwiązanie problemu. Zobaczmy:
# wywołanie poprawne $ python komentarze.py uni1.py liczba linijek z komentarzem = 1 # wywołanie na pliku którego nie ma $ python komentarze.py xxx.py Traceback (most recent call last): File "komentarze.py", line 3, in <module> file = open(sys.argv[1]) IOError: [Errno 2] No such file or directory: 'xxx.py' # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ # wywołanie na pliku którego nie wolno odczytać $ python komentarze.py /etc/shadow Traceback (most recent call last): File "komentarze.py", line 3, in <module> file = open(sys.argv[1]) IOError: [Errno 13] Permission denied: '/etc/shadow' # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ # wywołanie bez wymaganego argumentu $ python komentarze.py Traceback (most recent call last): File "komentarze.py", line 3, in <module> file = open(sys.argv[1]) IndexError: list index out of range
Widać, że program wypisuje wynik tylko wtedy, gdy ten wynik jest poprawny.
Nawet w ostatnim przypadku, gdy komunikat jest mniej jasny, można łatwo się domyślić (jeśli się wie że sys.argv to lista argumentów programu), że lista argumentów programu jest za krótka.
Oczywiście znacznie lepiej, by programista sprawdził, czy użytkownik podał właściwą listę argumentów do programu. W szczególności w obecnej wersji, nadmiarowe argumenty są ignorowane, co stoi w sprzeczności z zasadą, że errors should never pass silently, czyli że żadne błędy nie powinny uchodzić uwadze. Niemniej, wydaje mi się, że ten przykład ładnie pokazuje elegancję wyjątków.
W części o chwytaniu wyjątków został pokazany blok finally. Do czego on służy? Kiedy należy go użyć?
Komendy zawarte w bloku finally są wykonywane zawsze, niezależnie od tego, czy wyjątek został rzucony, czy nie. Pozwala to na zawarcie w tym bloku operacji typu „sprzątanie po sobie”, które muszą zawsze zostać wykonane. Typowy przykład to sytuacja, gdzie procesy działające równolegle muszą się ze sobą synchronizować i wykorzystują do tego celu blokadę. Może to być np. pusty plik na dysku, który zostaje stworzony przed rozpoczęciem działań i skasowany po ich zakończeniu, tzw. lock-file.
import sys, time def create(lockfile): while True: try: os.open(lockfile, os.O_CREAT|os.O_EXCL|os.O_RDONLY) break except OSError as e: if e.errno != os.errno.EEXIST: raise print 'file is locked!, waiting' time.sleep(1) def remove(lockfile): os.unlink(lockfile) def do_work(datafile): lockfile = datafile + '.lock' create(lockfile) try: do_some_work_on(datafile) finally: remove(lockfile) if __name__ == '__main__' do_work(sys.argv[1])
Jak widać, ten program jest napisany tak, że jeśli w momencie wywołania będzie istniał plik lockfile, to program w nieskończoność będzie czekał na jego zniknięcie. Dlatego kluczowe jest, by uruchomienie programu zawsze zostało zakończone skasowaniem pliku lockfile. Programista nie przywidywał wystąpienia żadnego konkretnego błędu w wywołaniu do_some_work_on, wyjątek rzucony w trakcie wykonywania spowoduje zakończeniu programu i wypisanie ciągu wywołań w programie. Niemniej, nawet przed takim smutnym zakończeniem, plik lockfile zostanie usunięty.
Jako nieco łatwiejszy przykład możemy rozważyć sytuację, gdy chcemy zamknąć otwarty plik zaraz po zakończeniu pewnego ciągu instrukcji. W przykładzie powyżej (zob. #Pliki) zamykaliśmy plik explicite, używając jego metody close. Jest to jak najbardziej wystarczające, bo chodzi nam o jego zamknięcie tylko dlatego, by przy okazji wypchnąć zmiany w nim dokonane i móc go znowu za chwilę otworzyć i odczytać nową zawartość. Normalnie plik i tak zostałby zamknięty w momencie zakończenia programu. Wobec tego nie ma sensu przejmować się wystąpieniem wyjątku w operacjach wykonywanych na otwartym pliku (czyli pojedynczej operacji os.write(wierszyk)), bo on i tak uniemożliwiłby wykonanie dalszej części programu. Ale załóżmy, że chcemy, by metoda close została wykonana niezależnie od tego, co zdarzy się w trakcie os.write. Możemy zawrzeć wywołanie close w bloku finally:
f1 = open('wierszyk.txt', 'w') try: f1.write(wierszyk) finally: f1.close()
Nowsze wersje Pythona (≥ 2.5) zawierają konstrukcję with pozwalającą na wygodne skorzystanie z predefiniowanych operacji do wywołania przed i po jakimś bloku kodu. Istotne jest to, że tak jak w przypadku bloku finally, operacje kończące są wywoływane nawet w wypadku wystąpienia wyjątku.
Operacja close na pliku wywoływana w bloku finally jest bardzo powszechna, chociaż może nie tak jak iteracja po linijkach w pliku. Dlatego klasa file definiuje ją jako standardową operację „sprzątającą”.
Przykład powyżej przepisany z wykorzystaniem konstrukcji with:
with open('wierszyk.txt', 'w') as f1: f1.write(wierszyk) # po wyjściu z tego bloku plik zostanie samoczynnie zamknięty
W nowych programach należy postępować w ten właśnie sposób.
W jaki sposób klasa file definiuje, co ma zostać wywołane na końcu bloku with? Wykorzystuje context manager protocol opisany w PEP 0343.
Programowanie obiektowe, czy szerzej programowanie zorientowane obiektowo, jest pewnym sposobem organizacji kodu. Pomysł polega na umieszczeniu kodu obsługującego dane blisko tych danych. W jakim sensie „blisko”? Nie fizycznej bliskości, czyli nie w sensie bliskiego umiejscowienia w pamięci, ani niekoniecznie w sensie „blisko” w tekście programu. Chodzi raczej o bliskość logiczną, podkreśloną przez wygodną notację. Co to wszystko dokładnie znaczy łatwiej będzie opisać
po wprowadzeniu odpowiednich pojęć.
Zmienne przechowujące dane zawarte w obiekcie nazywa się polami, natomiast funkcje związane z obiektem, nazywa się metodami.
Ważną cechą obiektów jest to, że dzielą się one na grupy charakteryzujące się podobnym zachowaniem. Wynika to z tego, że każdy obiekt ma swoją klasę, lub bardziej poprawnie gramatycznie, jest pewnej klasy.
Klasa jest opisem, definicją. Obiekt jest konkretną zmienną zbudowaną i zachowującą się zgodnie z definicją klasy.
Gdy tworzymy zmienną i przypisując jej, powiedzmy, liczbę 5, to tworzymy obiekt o nazwie i, klasy int.
i = 5Ważnym elementem używania obiektów jest notacja obiektowa. Do pól i metod obiektów dostajemy się pisząc nazwę zmiennej dowiązanej do obiektu, kropkę i nazwę atrybutu obiektu.
Na przykład dla klasy list istnieje metoda append, która pozwala na dopisywanie elementów na końcu każdej listy. Wywołanie mojalista.append('rzecz') doda napis 'rzecz' na koniec listy mojalista.
Jako przykład pól rozważmy liczbę: obiekt klasy int.
>>> a = 1 >>> print 'a = ', a.numerator, '/', a.denominator a = 1 / 1 >>> print 'a = ', a.real, ' + i * ', a.imag a = 1 + i * 0 >>> print 'a jest typu', a.__class__.__name__ a jest typu int
Jak widać, obiekt klasy int ma (przynajmniej) cztery pola — licznik (ang. numerator) i mianownik (ang. denominator) oraz część rzeczywistą (ang. real) i urojoną (ang. imaginary).
Notację z kropką stosuje się tak samo by uzyskać dostęp do pól jak i do metod, bo tak naprawdę różnica między polami a metodami jest znacznie mniejsza niż się na pierwszy rzut oka wydaje.
Zacznijmy od przykładu:
class Wektor(object): "Dwuwymiarowy wektor" def __init__(self, x, y): self.a = x self.b = y print "wektor został stworzony!" def dlugosc(self): "Zwraca długość wektora." return (self.a ** 2 + self.b ** 2) ** 0.5 w1 = Wektor(5, 7) # pisze: wektor został stworzony! print w1.dlugosc() # pisze: 8.6...
Nasza nowa klasa Wektor ma dwie metody: Wektor.__init__ i Wektor.dlugosc. Nazwa i znaczenie pierwszej z nich będzie wyjaśnione poniżej, na razie powiemy tylko, że jej zadaniem jest zapisanie odpowiednich wartości w polach obiektu i że zostaje ona wywołana automatycznie w momencie tworzenia obiektu. Natomiast funkcja dlugosc oblicza długość wektora korzystając ze zmiennych przechowywanych w polach wektora. Jak to zostało pokazane w przykładzie, dysponując obiektem, metodę można wywołać dzięki notacji z kropką.
Pewne funkcje zostają wywołane w sposób automatyczny. Aby podkreślić fakt, że dana funkcja może zostać wywołana automatycznie, mają one nazwy zaczynające i kończące się dwoma podkreśleniami. W Pythonie są trzy różne kategorie nazw zaczynających się od podkreślenia, ale tak naprawdę nie mają one ze sobą dużo wspólnego. Najważniejsze są właśnie funkcje wywoływane automatycznie opisanej powyżej, które mają po parze podkreśleń na początku i na końcu. Oprócz tego występują zmienne prywatne, zaczynające się od jednego lub dwóch podkreśleń. Ich przeznaczeniem jest przechowanie zmiennych, do których nie należy się bezpośrednio odwoływać spoza klasy czy modułu.
Ważną funkcją specjalną jest metoda __init__. Python sam wywołuje ją automatycznie kiedy tworzymy nowy obiekt danego typu. Programista nie musi podać nazwy funkcji do wywołania, bo interpreter wie, że do konstrukcji obiektu służy metoda o tej właśnie nazwie. W tym właśnie sensie metoda __init__ jest specjalna. Metoda ta powinna wykonywać wszystkie operacje potrzebne do zainicjowania nowego obiektu, w szczególności powinna ona nadawać wartości jego polom. Pod innymi względami jest ona zupełnie zwyczajna, w szczególności można ją wywołać drugi i trzeci raz podając explicite jej nazwę (jeśli chcemy zmienić nasz wektor, na przykład wydłużyć dwa razy to możemy napisać w1.__init__(10, 14)).
Oprócz wcześniej wspomnianej metody __init__ przydatna jest metoda __str__. Służy ona do wytwarzania tekstowej reprezentacji obiektu. Jest automatycznie wywoływana np. przez polecenie print. Można ją dodać do naszego przykładowego wektora i zostanie ona automatycznie użyta do uzyskania reprezentacji tekstowej wektora kiedy tylko zajdzie taka potrzeba.
class Wektor(object): "Dwuwymiarowy wektor" def __init__(self, x, y): self.a = x self.b = y print "wektor został stworzony!" def dlugosc(self): "Zwraca długość wektora." return (self.a ** 2 + self.b ** 2) ** 0.5 def __str__(self): """Zwraca napis, reprezentację tekstową obiektu. Ta metoda jest wywoływana automatycznie, gdy ktoś napisze str(wektor) lub print wektor """ return "Wektor x={0.a} y={0.b}".format(self) w1 = Wektor(5, 7) print w1 # wypisuje: Wektor x=5 y=7
Operatory dwuargumentowe są kolejnym typem metod specjalnych.
Wracając do naszgo przykładowego Wektora: jeśli zechcemy dodać dwa Wektory korzystając ze znaku +, nie będzie to wykonalne, otrzymamy TypeError. Stanie się to możliwe dopiero kiedy napiszemy metodę __add__, czyli implementację operatora + dla klasy Wektor.
Aby zrozumieć działanie operatora dwuargumentowego, trzeba wiedzieć, że Python po napotkaniu x+y wywołuje metodę x.__add__(y) (mówimy, że wykonanie operatora zostaje delegowane do metody). Zatem implementując operator dwuargumentowy musimy napisać funkcję, której pierwszym argumentem jest obiekt znajdujący się na lewo od operatora (jest to aktualny obiekt i mamy dostęp do niego przez self) zaś drugim argumentem jest obiekt znajdujący się na prawo od operatora. Poniżej prezentujemy przykład implementacji dodawania dla rozważnej klasy Wektor:
class Wektor(object): # # dotychczasowy kod klasy wektor # def __add__(self, other): """Zwraca nowy wektor, sumę self i other. Ta metoda jest wywoływana automatycznie, gdy ktoś napisze w1 + w2 """ return Wektor(self.a + other.a, self.b + other.b) w1=Wektor(5,7) print w1 + w1 # wypisuje: Wektor x=10 y=14
Funkcji specjalnych jest sporo, bo tabela operatorów jest długa, a należy pamiętać, że dochodzą jeszcze operatory skrócone +=, -=, ... Każdy z nich ma odpowiadającą mu funkcję specjalną, którą można napisać by umożliwić wykonywanie tej operacji na obiektach danej klasy. Wszystkie funkcje specjalne są opisane w dokumentacji języka, http://docs.python.org/dev/reference/datamodel.html#special-method-names. W trakcie pracy w interaktywnym interpreterze przydatny jest moduł operator, który zawiera funkcje specjalne do arytmetyki na liczbach. Można skorzystać z jego dokumentacji, która zawiera opisy funkcji specjalnych do operacji matematycznych (import operator; help(operator)).
Aby stworzyć obiekt, trzeba wcześniej mieć klasę. Samo polecenie stworzenia obiektu wygląda identycznie jak wywołanie funkcji. W naszym przykładzie
instancja_wektora = Wektor(11, -4)
Bezpośrednio po utworzeniu obiektu, zmienne wewnątrz obiektu (pola) jeszcze nie istnieją. Dodanie ich do obiektu przez przypisanie im wartości jest zadaniem funkcji zwanych konstruktorami. Po stworzeniu obiektu zostają wywołane jego konstruktory, czyli metody __new__ i __init__. W obiektach klas stworzonych przez użytkownika języka ich rola się dubluje, o pierwszej zapominamy, i należy używać tylko drugiej z nich.
Przechodząc do inicjalizacji obiektu, następuje ona przez wywołanie funkcji __init__ z argumentami przekazanymi przy „wywołaniu” klasy opisanym w poprzednim punkcie.
Rozważmy szczegółowo inicjalizację Wektora wyrażeniem Wektor(11, -4).
Powoduje to wywołanie funkcji Wektor.__init__ z trzema argumentami:
Na początku funkcji __init__ nasz obiekt jest wydmuszką, która nawet nie ma swoich pól a i b. Ma jedynie metody zdefiniowane w klasie, ale nie można by ich użyć bo nie ma danych potrzebnych do ich wykonania.
Ponieważ Wektor po prostu przechowuje podane wartości współrzędnych to są one zapisanywane do zmiennych self.a i self.b bez żadnej obróbki. Gdybyśmy chcieli nałożyć jakieś warunki, np. aby wektor zawierał się w I ćwiartce, to w funkcji __init__ powinniśmy dodać test na współrzędne i rzucić ValueError w wypadku gdy nasze żądania co do argumentów inicjalizujących Wektor nie zostały spełnione.
Po zakończeniu działania funkcji __init__ wektor ma nowo stworzone zmienne self.a i self.b, oraz metody __init__, dlugosc, i inne zdefiniowane w klasie. Metody są takie same dla każdego obiektu, są więc przechowywane w klasie, natomiast zmienne obiektu są przechowywane razem z nim, ze względu na to, że każdy wektor może mieć inne współrzędne. Taki podział działa automatycznie w ten sposób, że Python najpierw poszukuje atrybutu, pola lub metody, w obiekcie, a dopiero potem w jego klasie. Są też możliwe bardzie wyrafinowane przypisania pól i metod do obiektów, ale nie będziemy o nich tutaj pisać. [staticmethod, classmethod, funkcje w obiektach, zmienne klasowe]
Teraz obiekt jest gotowy do użycia. Aby było to możliwe, musi być przypisany do jakiejś zmiennej. Po tym jak żadne zmienne nie wskazują na obiekt, przestaje on być potrzebny i będzie później zniszczony. Innymi słowy, jeśli chcemy żeby nasz obiekt żył szczęśliwie, to musimy go mieć przypisanego przez cały czas do przynajmniej jednej zmiennej, dzięki temu wiemy, że nie zostanie zniszczony.
Python niszczy obiekty kiedy stają się niepotrzebne. Niepotrzebnym jest obiekt, który nie jest przypisany do żadnej nazwy. Obiekt ma wbudowany licznik referencji. Przypisanie obiektu do dowolnej zmiennej zwiększa ten licznik o 1, a usunięcie zmiennej (skasowanie zmiennej przez del, przypisanie do zmiennej innego obiektu lub „zniknięcie” zmiennej po zakończeniu funkcji) — zmniejsza o 1.
Obiekt żyje co najmniej tak długo, jak długo jego licznik referencji jest większy od 0.
Zanim obiekt zostanie zniszczony, zostanie wywołana jego metoda __del__. Jej zadaniem jest wykonanie działań takich jak zamknięcie plików, które muszą być zamknięte wraz ze zniszczeniem obiektu. Niemniej, moment destrukcji obiektu jest trudny do przewidzenia, więc mechanizm __del__ jest bardzo zawodny. Nie należy go używać.
Podstawą zrozumienia tego, jak to wszystko działa, jest to, że w Pythonie prawie wszystko jest obiektem. Zmienna jest nazwą, która wskazuje na obiekt, czyli mówiąc bardziej precyzyjnie, kluczem w pewnym słowniku wewnątrz Pythona (zwanym namespace), który wiąże nazwy z obiektami.
Poniżej szczegółowo przedstawiona jest operacją przypisania dwóch zmiennych.
Stwórzmy pierwszy obiekt (klasy Obiekt) i zachowajmy go w zmiennej nazwa.
nazwa = Obiekt('niebieski')
Teraz przypiszmy obiektowi drugą nazwę.
nowa_nazwa = nazwa
Teraz stwórzmy nowy obiekt (klasy Obiekt) i zachowajmy go w zmiennej nazwa.
nazwa = Obiekt('zielony')
Teraz użyjmy operatora del.
del nowa_nazwaJak widać, operator del usuwa zmienną, nie obiekt na który ta zmienna wskazuje.
Co się stanie z obiektem, na który wskazywała nazwa? W tym momencie jego licznik referencji (ang. reference count), czyli sposobów na który można dotrzeć do obiektu, wynosi 0. Obiekt zostanie niedługo zlikwidowany.
# -*- coding: utf-8 -*- class Wektor(object): """Dwuwymiarowy wektor.""" _ile_nas = 0 def __init__(self, a, b): self.a = a self.b = b Wektor._ile_nas += 1 def dlugosc(self): """Zwraca liczbę, długość Wektora.""" return (self.a**2 + self.b**2)**0.5 def obroc(self): """Odwraca wektor w miejscu, czyli zamienia wartości jego współrzędnych na przeciwne. """ self.a *= -1 self.b *= -1 def obrocony(self): """Zwraca nowy wektor odwrócony.""" return Wektor(-self.a, -self.b) def __str__(self): """Zwraca reprezentację Wektora w formie (x, y)""" return "(" + str(self.a) + ", " + str(self.b) + ")" def __repr__(self): """Zwraca reprezentacje Wektora w formie <Wektor(x, y) @id>""" return "<Wektor{0} @{1}>".format(self.__str__(), id(self)) def __add__(self, other): """Zwraca nowy Wektor będący sumą self i other.""" print "dodawanie", self, other return Wektor(self.a + other.a, self.b + other.b) def dodawanie(self, other): """Zwraca nowy Wektor będący sumą self i other.""" print "dodawanie'", self, other return Wektor(self.a + other.a, self.b + other.b) def __mul__(self, other): """Zwraca liczbę będącą iloczynem skalarnym self i other.""" print "mnozenie", self, other return self.a * other.a + self.b * other.b def __del__(self): Wektor._ile_nas -= 1
Plik rozpoczyna się standardowym nagłówkiem informującym o kodowaniu — dzięki temu możemy użyć polskie literki w napisach w programie bez ryzyka, że zostaną wyświetlone jako krzaczki pod innym systemem. Informacja o kodowaniu jest zapisana w komentarzu (zaczynającym się od #).
Wszystkie nowe klasy powinny pochodzić od typu object lub innej klasy. Jeśli nie chcemy zmieniać działania innej klasy tylko „zacząć od zera” to powinniśmy zrobić to tak jak w tym przykładzie.
Na początku klasy, podobnie jak na początku funkcji, możemy dodać docstring. Ten napis zostanie wyświetlony jeśli ktoś napisze help(Wektor).
Dodamy zmienną klasową liczącą stworzone wektory. Ta zmienna, w przeciwieństwie do zwykłych pól, występuje tylko raz i jest przypisana do samej klasy, a nie obiektów tej klasy. Pozwala to na przechowywanie informacji wspólnych dla wszystkich obiektów, w tym wypadku ich całkowitej liczby.
Metoda __init__ ma za zadanie stworzyć pola obiektu. Aktualizujemy również licznik stworzonych wektorów.
Metoda dlugosc wykorzystuje pola obiektu (self.a i self.b),
aby wykonać niezbędne obliczenia i zwrócić długość wektora.
Dwa sposoby wykonania operacji: w miejscu i ze stworzeniem nowego obiektu.
Nazwy w formie niedokonanej i dokonanej są pewną konwencją — podobnie sort sortuje listę zmieniając kolejność jej elementów, a sorted zwraca nowy obiekt z posortowanymi elementami.
Metody __str__ i __repr__ wytwarzają napis będący reprezentacją Wektora — zwięzłą, przeznaczoną do wypisywania wyników dla użytkownika, oraz nieco bardziej dokładną zawierającą informacje przydatne dla programisty. Dobrze, by __repr__ zwracało napis pozwalający na skopiowanie i wykorzystanie jako fragment programu.
Dwa sposoby wykonania operacji arytmetycznej: metoda o specjalnej nazwie wywoływana automatycznie przy wykonywaniu operacji +, oraz metoda wywoływana explicité przez podanie nazwy.
Metoda __mul__ realizuje mnożenie dwóch Wektorów. Metoda ta jest wywoływana automatycznie po użyciu operatora *. Wykorzystując różne obiekty można łatwo zrealizować mnożenie dla różnych typów. Niestety, jest tylko jeden operator mnożenia i nie można wygodnie zrealizować różnych mnożeń (np. wektorowego i skalarnego) dla jednego typu.
Metoda __del__ jest wywoływana automatycznie przy destrukcji obiektu. Normalnie nie jest specjalnie przydatna, ale tutaj używamy jej do aktualizacji licznika wystąpień obiektów tej klasy.
Dziedziczeniem nazywamy sytację, w której definiujemy nową klasę jako rozwinięcie istniejącej. Tę nową klasę nazywamy dzieckiem, a starą rodzicem. Jeszcze inne, alternatywne, nazewnictwo mówi o podklasach i nadklasach. Pochodzenie tych słów będzie wyjaśnione poniżej.
Załóżmy, że mamy klasę B, która dziedziczy po klasie A. Dziedziczenie oznacza, że metody i pola dostępne w klasie-rodzicu A, są również dostępne w klasie-dziecku B. Innymi słowy, wszystkie obiekty klasy B mają takie same atrybuty jak obiekty klasy A. Powoduje to, że w ogólności, tam gdzie mogliśmy użyć obiektu klasy A, możemy również użyć obiektu klasy B. Oznacza to, że obiekty typu B są też obiektami typu A, czyli B jest podtypem czy też podklasą A. Symetrycznie, A jest nadklasą B.
Sytuację kiedy obiektów dwóch różnych klas mają wspólny zestaw metod i pól i można je używać zamiennie nazywamy polimorfizmem.
Polimorfizm jest częstszy, niż by się na pierwszy rzut oka mogło wydawać. Na przykład różne rodzaje liczb (klasy int, float czy complex) stosujemy w tym samym miejscu w zasadzie bez zastanowienia. Ponieważ możemy dodać dwie liczby zmienno-przecinkowe pisząc 1.5 + 2.0, to oczekujemy też, że podobna opercja będzie możliwa i da identyczny wynik jeśli za jedną z nich podstawimy liczbę całkowitą pisząć 1.5 + 2.
Polimorfizm jest ważny, gdyż znacznie ułatwia wykorzystanie obiektów różnych klas. Wygodniej jest myśleć o operacji dodawania zdefiniowanej dla wszystkich liczb, niż o opercji dodawania liczb zmienno-przecinkowych, operacji dodawania liczb całkowitych, operacji dodawania liczb zespolonych, itd.
Dziedziczenie jest mechanizmem, który pozwala na uzyskanie polimorfizmu w bardzo łatwy sposób. Ponieważ klasy potomne na wstępie otrzymują komplet metod i pól rodzica, to możliwość podstawienia obiektu klasy dziecka za rodzica otrzymuje się automatycznie.
W Pythonie dziedziczenie nie jest jednym sposobem na uzyskanie wymienności obiektów różnych klas. Tym co decyduje, jakiego typu obiekt możemy wykorzystać np. jako argument funkcji, nie jest jego klasa, tylko zestaw pól i metod obiektu wykorzystywanych w funkcji. Popatrzmy na przykład
>>> class Hello(object): ... def hello(self): return 'hello' >>> class Bye2(object): ... def hello(self): return 'bye, bye' >>> def print_greeting(obj): ... print type(obj), obj.hello() >>> print_greeting(Hello()) <class '__main__.Hello'> hello >>> print_greeting(Bye2()) <class '__main__.Bye2'> bye, bye
Mamy tutaj obiekty dwóch różnych klas, i przekazujemy je do tej samej funkcji print_greeting. Od obiektu przekazanego jako parametr, funkcja wymaga tylko tyle, by miał on metodę hello. Taki "leniwy polimorfizm" nazywa się (ang. duck-typing), co oznacza, że definiujemy klasy które niekoniecznie mają wspólnego przodka, a jedynie definiują wszystkie atrybuty potrzebne do wykonania danej operacji.
W typowym przypadku, klasy które definiujemy mają jednego tylko rodzica. Niemniej, nie musi tak być. Jeśli podamy więcej niż jednego rodzica, to obiekty naszej nowej klasy odziedziczą atrybuty po wszystkich rodzicach.
>>> class A(object): ... def a(self): ... print 'A' >>> class B(object): ... def b(self): ... print 'B' >>> class C(A, B): ... def c(self): ... print 'C' >>> c = C() >>> c.a() A >>> c.b() B >>> c.c() C
W przypadku gdy zażądamy dostępu do metody czy pola, które występuje tylko w jednym z rodziców, to sytuacja jest prosta — przeszukiwany jest najpierw pierwszy rodzic, potem drugi,... dopóki nie natrafimy na atrybut o żądanej nazwie.
W sytuacji kiedy zażądamy dostępu do atrybutu, dostępnego u więcej niż jednego rodzica, to decyduje kolejność dziedziczenia. W przykładzie powyżej, najpierw przeszukiwane jest C, potem A, potem B, a na końcu object.
Wielodziedziczenie jest bardzo ciekawą techniką programistyczną, lecz niestety w praktyce w miarę skomplikowaną. Problemem jest to, że implementacje wszystkich klas w hierarchii muszą ze sobą współgrać. W szczególności, gdyby jeden z rodziców miał metodę o pewnej nazwie, a drugi z rodziców przypadkowo miałby pod taką samą nazwą zdefiniowaną metodę, która wykonuje zupełnie inną operację, to poprzez dziedziczenie po obydwu rodzicach, możemy np. doprowadzić do sytuacji, że omyłkowo wywołamy metodę z pierwszego rodzica chcąc wywołać metodę z pierwszego rodzica. Musimy sprawdzać nie tylko czy pożądana metoda jest zdefiniowana, ale przypadkiem również czy nie jest zdefiniowana inna metoda o tej samej nazwie. Takie ścisłe zespolenie różnych klas nie jest dobre, i często lepiej wykorzystać kompozycję zamiast (wielo-)dziedziczenia.
Wiemy już, że jeśli zdefiniowaliśmy w klasie-dziecku metodę o danej nazwie, to zastępuje ona metodę o takiej samej nazwie zdefiniowaną w klasie-rodzicu. Niemniej, czasami chcielibyśmy wywołać metodę nadrzędną, czyli tą odziedziczoną po rodzicu. Możemy to zrobić na dwa sposoby: podając explicite nazwę klasy rodzica, lub wykorzystując funkcję super. To pierwsze rozwiązanie jest nieco prostsze, to drugie nieco lepsze.
W pierwszym rozwiązaniu, do metody rodzicielskiej odwołujemy się tak samo jak do każdej innej funkcji, podając ścieżkę do niej.
>>> class Welcome(object): ... def hello(self): ... return 'Hello' >>> class WarmWelcome(Welcome): ... def hello(self): ... return Welcome.hello(self) + ", you're welcome" >>> print Welcome().hello() Hello >>> print WarmWelcome().hello() Hello, you're welcome
W drugim rozwiązaniu, do metody rodzicielskiej odwołujemy się poprzez pomocniczy obiekt zwracany przez super. Funkcja super bieże dwa argumenty — klasę od której zaczynamy poszukiwania (czyli generalnie klasę wewnątrz której definicji wywołujemy super) oraz argument self. Brzmi to trochę karkołomnie, ale w praktyce nie jest specjalnie skomplikowane.
>>> class HeartyWelcome(Welcome): ... def hello(self): ... return super(HeartyWelcome, self).hello() + ", you're heartily welcome" >>> print HeartyWelcome().hello() Hello, you're heartily welcome
W takich prostych przypadkach jak ten, i jeden, i drugi sposób odwołania się do Welcome.hello będzie skuteczny. Można nawet powiedzieć, że pierwszy jest bardziej klarowny. Niemniej, ze względu na to, że w bardziej skomplikowanych przypadkach drugi jest lepszy, dla spójności (i celem wyrabiania dobrych nawyków), lepiej stosować ten drugi.
Czym różnią się te dwa sposoby? Wykorzystując super podajemy nazwę bieżącej klasy. Wykorzystując bezpośrednie odwołanie podajemy nazwę klasy macierzystej. W przypadku gdy zmieniamy hierarchię dziedziczenia, np. czyniąc HeartyWelcome dzieckiem Welcome, nie musimy zmieniać wszystkich odwołań do odziedziczonych metod. Jeśli odwoływaliśmy się do metod rodzica przez super, to automatycznie zaczniemy się odwoływać do metod nowego rodzica. Jest to szczególnie ważne, gdy klasa-rodzic jest zdefiniowana w innym module. Wykorzystanie super zmniejsza prawdopodobieństwo pomyłek przy przerabianiu kodu.
Po drugie, funkcja super zachowuje się w szczególny sposób w sytuacji wielokrotnego dziedziczenia (czyli w sytuacji, gdy dana klasa ma więcej niż jednego rodzica). O ile każda z metod w hierarchii wywołuje metodę "nadrzędną" przez super, to wszystkie metody w hierarchii zostaną wywołane w pewnym ściśle określonym porządku.
>>> class A(object): ... def __init__(self): ... super(A, self).__init__() ... print 'init A' ... >>> class B(object): ... def __init__(self): ... super(B, self).__init__() ... print 'init B' ... >>> class C(A, B): ... def __init__(self): ... super(C, self).__init__() ... print 'init C' ... >>> c = C() init B init A init C
Zauważmy, że A.__init__ wywołuje B.__init__, mimo że klasa A zupełnie nic o klasie B nie wie. Niemniej, z punktu widzenia klasy C, to by obydwoje jej rodzice zostali poprawnie zainicjalizowani, jest absolutnie kluczowe.
Po trzecie, nie musimy przekazywać self jako pierwszego argumentu wywołania metody. Oczywiście niewiele na tym zyskujemy, bo musimy przekazać self jako argument funkcji super. Możnaby pomyśleć, że takie przekazywanie oczywistych informacji, np. tego wewnątrz jakiej klasy metoda jest zdefiniowana, jest niepotrzebne i interpreter Pythona mógłby sam to stwierdzić. Tak się dzieje, ale dopiero w Pythonie 3. Piszemy po prostu super().
Przykładowe klasy definiowane powyżej zawsze dziedziczą po czymś. Często jest to klasa object. Niemniej wydawałoby się, że to dziedzicznie nie jest do niczego potrzebne (bo faktycznie klasa object nie definiuje żadnych pól, a jedynie parę metod typu __str__, do których rzadko się odwołujemy z klas potomnych). Dziedziczenie po object jest potrzebne z innych przyczyn.
W Pythonie 2 mamy dwa rodzaje klas — tak zwane (ang. old-style classes) oraz (ang. new-style classes). Te drugie otrzymujemy, gdy definiujemy klasy dziedziące bo object, bezpośrednio lub pośrednio, tak jak w przykładach w tym skrycie. Te pierwsze otrzymujemy, gdy definiujemy klasy gołe, nie dziedziczące po niczym, lub klasy dziedziczące po klasach starego typu. Klasy starego typu są reliktem historii i pochodzą z czasów, gdy klasy definiowane przez użytkownika tworzyły zupełnie oddzielną hierarchię od typów macierzystych języka. Ta dychotomia jest od dawna zniesiona, ale oba typy klas różnią się nieco zachowaniem, o czym poniżej, i ze względu na potrzebę zachowania poprawności działania istniejącego kodu, można definiować klasy zarówno starego jak i nowego typu. W Pythonie 3 klasy starego typu już nie istnieją. Z tego względu w Pythonie 3, niezależnie od tego, czy się w jawny sposób zadeklaruje dziedziczenie po object, czy też nie, takie dziedziczenie jest automatyczne i zawsze otrzymuje się klasę nowego typu.
W przypadku klas starego i nowego typu, nieco inaczej zachowuje się operator super. Przy rekurencyjnych wywołaniach metody nadrzędnej, w klasach nowego typu przechodzimy wszystkich rodziców w pewnym porządku. W klasach starego typu możemy jednego rodzica odwiedzić więcej niż raz. [2] [3]
W klasach nowego typu możemy definiować tzw. deskryptory. Deskryptory pozwalają na definiowanie specjalnego typu atrybutów. Dostęp do atrybutów zdefiniowanych przez deskryptory do których dostęp wygląda tak samo jak dostęp do zwykłych statycznych pól, ale faktycznie powoduje wywołanie metody na deskryptorze. Użycie deskryptora pozwala np. na zastąpenie zwykłego pola przez parę funkcji (ang. getter)/(ang. setter), czyli np. dodanie sprawdzania poprawności wartości wstawianej do pola, bez zmiany sposobu dostępu do tego pola. Bywa to niezwykle wygodne w przypadku przerabiania kodu. Ta funkcjonalność jest opisana tutaj.
Niemniej, deskryptory działają tylko w przypadku klas nowego typu. Podobnych drobnych niekompatybilności między klasami starego i nowego typu jest więcej. Generalnie, są one w miarę subtelne i naprawdę trudno jest je wszystkie pamiętać. Oznacza to, że jeśli nie chcemy mieć niespodzianek, powinniśmy zacisnąć zęby i zawsze dopisywać te osiem znaków potrzebnych po to, by definiować klasy nowego typu. Jako dodatkowy bonus, nasz kod jest znacznie łatwiej przerobić na Pythona 3, gdzie klasy nowego typu nazywają się po prostu klasami.
Rozwiązanie układu równań liniowych:

import numpy as np A = np.array([[1, 0, 3], [2, 1, 5], [4, 8, 8]]) b = np.array([3, 2, 1]) x = np.linalg.solve(A, b) print x
[-12.75 1.25 5.25]
Nasz wyjściowy układ równań liniowych możemy zapisać w postaci macierzowej Ax = b:
. Macierz A konstruujemy ze współczynników mnożących zmienne x1, x2 i x3 w poszczególnych równaniach — każdy wiersz macierzy A opisuje jedno równanie, każda jej kolumna opisuje kolejno jedną z niewiadomych zmiennych xi. Po pomnożeniu A przez wektor x, którego każdy wiersz zawiera odpowiednią niewiadomą, otrzymamy lewą stronę naszego układu równań. Prawą stronę tworzy wektor b, którego wiersze utworzone są odpowiednio z prawych stron poszczególnych równań układu.
Funkcja solve modułu Numpy zwraca nam listę zawierającą poszukiwane przez nas rozwiązanie układu równań: x1 = −12,75, x2 = 1,25, x3 = 5,25.
Możemy zauważyć, że rozwiązanie układu równań można formalnie uzyskać mnożąc obie strony równania (lewostronnie) przez macierz odwrotną do A:
W poniższym przykładzie do odwrócenia macierzy A stosujemy funkcję inv z modułu numpy.linalg. Mnożenie macierzy realizuje zaś funkcja dot.
# -*- coding: utf-8 -*- import numpy as np A = np.array([[1, 0, 3], [2, 1, 5], [4, 8, 8]]) b = np.array([3, 2, 1]) A1 = np.linalg.inv(A) print "Macierz odwrotna do A:" print A1 print "Sprawdzenie macierzy odwrotnej:" print np.dot(A1, A) x = np.dot(A1, b) print "Rozwiązanie:" print x
Macierz odwrotna do A: [[-8. 6. -0.75] [ 1. -1. 0.25] [ 3. -2. 0.25]] Sprawdzenie macierzy odwrotnej: [[ 1.00000000e+00 1.77635684e-15 1.77635684e-15] [ -1.11022302e-16 1.00000000e+00 -2.22044605e-16] [ -2.22044605e-16 -4.44089210e-16 1.00000000e+00]] Rozwiązanie: [-12.75 1.25 5.25]
W takich obliczeniach jak te opisywane w niniejszym rozdziale, występuje potrzeba definiowania króciusieńkich funkcji. Okazuje się, że można je definiować na dwa sposoby:
Po słowie kluczowym lambda podajemy nazwy, które będą pełnić rolę zmiennych w naszej funkcji, a po dwukropku wypisujemy wyrażenie (używając podanych przed dwukropkiem nazw jako zmiennych).
>>> def f(x): return x**3 ... >>> print f <function f at 0x1c285f0> >>> f1 = f >>> print f1 <function f at 0x1c285f0> >>> g = lambda x: x**3 >>> print g <function <lambda> at 0x1c286e0>
W tym przykładzie, funkcje f, f1 i g działają tak samo — biorą jeden argument i zwracają jego sześcian. Różnica jest taka, że funkcja f „wie” że nazywa się „f”, co widać w napisie wypisywanym po print f czy print f1. Natomiast funkcja g nazywa się „<lambda>”, tak samo jak wszystkie inne funkcje zdefiniowane jako wyrażenie λ, czyli nie ma swojej nazwy. Zmienna g jest tylko dowiązaniem do obiektu funkcji, tak samo jak zmienna f1.
Funkcja zdefiniowana jako wyrażenie λ jest równoważna funkcji definiowanej zwyczajnie zwracającej to samo wyrażnie. Niemniej, większości rzeczy które można zrobić w funkcji, nie można zrobić w wyrażeniu λ ze względu na ograniczenia składni:
Wyrażenia λ są wygodne ze względu na to, że wymagają mniej stukania w klawisze.
import scipy.optimize as so def func(x): return x**3 + 3. * x - 0.3 print so.fsolve(func, 0.5) print so.fsolve(lambda x: x**3 + 3*x - 0.3, 0.5)
0.099669956223525716 0.099669956223525716
Funkcja fsolve z modułu scipy.optimize poszukuje miejsc zerowych funkcji, czyli takich wartości x, dla których f(x) = 0. Załóżmy, że chcemy rozwiązać równanie:
x3 + 3x = 0,3.
Po przepisaniu tego równania do postaci
x3 + 3x − 0,3 = 0
widzimy, że rozwiązanie tego równania jest równoważne poszukiwaniu miejsc zerowych funkcji:
f(x) = x3 + 3x − 0,3.
Funkcja fsolve oczekuje badanej funkcji jako pierwszego argumentu. Możemy tu przekazać nazwę funkcji osobno zdefiniowanej, jak na przykład func. Jeśli chcemy szukać miejsc zerowych funkcji, którą da się zapisać prostym wyrażeniem złożonym z operacji na dostępnych wielkościach i funkcjach możemy użyć jako argumentu wyrażenia lambda. Wyrażenie to zastępuje „pełną” definicję funkcji w momencie, gdy tak naprawdę taka pełna definicja nie jest nam potrzebna. W podanym przykładzie szukaną funkcję przekazaliśmy dwoma sposobami — raz z użyciem nazwy uprzednio zdefiniowanej funkcji func, a raz z użyciem wyrażenia lambda.
Drugim argumentem tej funkcji jest punkt startowy do poszukiwania miejsca zerowego. Powinniśmy najpierw zgrubnie oszacować położenie poszukiwanego miejsca zerowego i wpisać je tutaj. W zależności od funkcji parametr ten jest mniej lub bardziej istotny dla znalezienia poprawnego wyniku. Jest on zaś szczególnie istotny, jeśli badana funkcja ma więcej niż jedno miejsce zerowe (w tym przypadku wskazane może być wykreślenie funkcji).
# -*- coding: utf-8 -*- import scipy.integrate as si import numpy as np def func(x): return x**3/(np.exp(x)-1) print 'Całka od',0,'do',1,'wynosi',si.quad(func,0,1) print 'Całka od',0,'do','+∞','wynosi',si.quad(func,0,np.inf) print 'Całka od',0,'do','π','wynosi',si.quad(lambda x: np.sin(x),0,np.pi) print 'Całka od',0,'do',1,'wynosi',si.quad(lambda x: x,0,1)
Całka od 0 do 1 wynosi (0.22480518802593821, 2.4958389580158414e-15) Całka od 0 do +∞ wynosi (6.4939394022668298, 2.6284700289248249e-09) Całka od 0 do π wynosi (2.0, 2.2204460492503131e-14) Całka od 0 do 1 wynosi (0.5, 5.5511151231257827e-15)
Do numerycznego obliczania całek oznaczonych używamy funkcji quad z modułu scipy.integrate. Funkcja ta jako pierwszego argumentu wywołania oczekuje funkcji, którą będziemy całkować. Podobnie jak w poprzednim przykładzie, możemy tu przekazać nazwę funkcji osobno zdefiniowanej (func) albo wyrażenia lambda.
Drugi i trzeci argument funkcji quad to początek i koniec zakresu całkowania. Zauważ, że oprócz „normalnych” liczb możliwe są zakresy nieskończone, z użyciem stałej inf z modułu numpy. Rezultatem funkcji quad jest krotka dwuelementowa. Pierwszy jej element to wynik całkowania, natomiast drugi określa dokładność, z jaką udało się wynik ten policzyć.
Rozwiązanie równania różniczkowego
z warunkiem początkowym y(t = 0) = 1.
import scipy.integrate as si import numpy as np import pylab as p df = lambda y, t: y X = np.linspace(0, 5, 51) # 51 punktów od 0 do 5 włącznie wynik = si.odeint(df, 1, X) p.plot(X, np.zeros_like(X), 'ro', X, wynik, 'go', X, numpy.exp(X), '--') p.show()
Poszukujemy funkcji y(t) spełniającej nasze równanie różniczkowe
. Funkcja odeint (skrót ODE pochodzi z angielskiego określenia ordinary differential equation czyli równanie różniczkowe zwyczajne) oblicza wartości poszukiwanej przez nas funkcji w punktach podanych jako jej trzeci argument.
Z teorii wiemy, że równanie to spełnia funkcja y(t) = exp(t) (spełnia je również funkcja y(t)≡0, ale ze względu na przyjęty warunek początkowy ta funkcja nie może być naszym rozwiązaniem).
Poniższy przykład ilustruje jak można rozwiązać równanie różniczkowe zwyczajne wyższego rzędu. Jednocześnie będzie to przykład na rozwiązywanie układów równań różniczkowych.
Rozważmy masę m umieszczoną na sprężynie o stałej sprężystości k. Siła działająca na masę zależna jest od jej wychylenia x z położenia równowagi i wynosi −kx. Równanie ruchu tej masy to (kropka nad symbolem zmiennej oznacza jej różniczkowanie po czasie: jedna kropka — pierwszą pochodną, dwie kropki — drugą pochodną):
Dzieląc to równanie stronami przez m otrzymujemy standardowe równanie oscylatora harmonicznego o częstości
:
Funkcje całkujące równania różniczkowe w SciPy radzą sobie tylko z równaniami i układami równań pierwszego rzędu. Musimy zatem przepisać nasze równanie na układ równań pierwszego rzędu. Można to zrobić wprowadzając dodatkową zmienną. Tą zmienną jest
(prędkość masy).
Teraz nasz układ wygląda tak:
# -*- coding: utf-8 -*- from scipy.integrate import odeint import numpy as np import pylab as p def prawa_strona_rownania(w, t, params): ''' Argumenty: w: wektor stanu (u nas x, v) t: czas params: wektor parametrów (u nas omega_kwadrat) W wektorze f funkcja zwraca obliczone dla danego wektora stanu wartości prawej strony równania ''' x, v = w # dla czytelności równania wypakowuję zmienne z wektora "w" omega_kwadrat, = params # i podobnie z parametrami "params" # poniżej do tablicy f w kolejnych wierszach wpisuję # kolejne prawe strony równań stanowiących układ f = [v, # wartość pochodnej dx/dt -omega_kwadrat * x] # wartość pochodnej dv/dt return f t = np.linspace(0, 5, 51) params = [2] w = [0, 3] # warunek początkowy (t=0) dla x i v print "Wektor stanu w chwili początkowej: ", print prawa_strona_rownania(w, t[0], params) # argumentami odeint są: # - nazwa funkcji, # - wektor stanu początkowego, # - wektor zawierający chwile czasu, dla których ma być zwrócony stan układu # - krotka zawierająca dodatkowe parametry, które mają być przekazane do funkcji # opisującej prawe strony równań wynik = odeint(prawa_strona_rownania, w, t, args=(params,) ) x = wynik[:, 0] v = wynik[:, 1] p.plot(t,x, t,v) p.legend(('x', 'v')) p.grid(True) p.show()
Funkcja odeint oczekuje przynajmniej trzech parametrów: funkcji obliczającej pochodną poszukiwanej funkcji (albo pochodne poszukiwanych funkcji w przypadku układu równań), warunku początkowego dla szukanych funkcji oraz sekwencji punktów czasu, w których będzie obliczone rozwiązanie. W naszym przypadku będą to:
, czyli
;
U nas jest jeszcze czwarty parametr (nazwany, o nazwie args). Powinna być to krotka zawierająca dodatkowe parametry, jakich może potrzebować funkcja obliczająca wartości pochodnych. U nas jest taki jeden dodatkowy parametr: ω2, która w naszym programie jest przechowywana w zmiennej params. Tworzymy więc z niej krotkę jednoelementową i przekazujemy ją funkcji odeint jako czwarty parametr. Zostanie ona użyta podczas liczenia prawej strony równań rozwiązywanego układu, czyli w funkcji prawa_strona_rownania.
Otrzymujemy wykres zmiennych x i v czyli położenia i prędkości masy zaczepionej na sprężynie. Ponieważ zastosowaliśmy warunek początkowy x(0) = 0, v(0) = 3, więc w chwili początkowej masa mija punkt równowagi x=0 z prędkością 3 jednostek. Otrzymujemy drgania z amplitudą teoretycznie 3/ω = 2,12 jednostek.
import pylab as p import numpy as np import scipy.fftpack as sf x = np.loadtxt('c4spin.txt') p.subplot(2,1,1) p.plot(x) p.subplot(2,1,2) z=sf.fft(x) widmo = np.abs(z[:len(z)/2]) p.plot(widmo) p.show()
Moduł scipy.fftpack dostarcza narzędzi do liczenia transformaty Fouriera. Transformata Fouriera pozwala wyznaczyć skład częstotliwościowy sygnału. W powyższym przykładzie wczytujemy dane z pliku tekstowego poleceniem np.loadtxt('c4spin.txt'). Dane te interpretujemy jako kolejne próbki pewnego sygnału.
Na górnym panelu rysunku wykreślamy przebieg sygnału w czasie. Funkcja sf.fft(x) oblicza transformatę Fouriera sygnału x. Zmienna z zawiera pełną informację o transformacie Fouriera sygnału x — jest to wektor liczb zespolonych. Aby uzyskać informację o częstościach zawartych w sygnale x trzeba wziąć wartość bezwzględną liczb z. Jeśli ponadto sygnał x składa się z liczb rzeczywistych (a tak jest w naszym przypadku) to wynikowy wektor z zawiera pełną informację o składzie częstotliwościowym w swojej pierwszej połowie. Dlatego do zmiennej widmo przepisujemy tylko wycinek z[:len(z)/2]. Na dolnym panelu wyrysowujemy zmienną widmo.
How to Think Like a Computer Scientist (Learning with Python), Allan Downey, Jeffrey Elkner, Chris Meyers
Non-Programmer's Tutorial for Python 2.0, Josh Cogliati and contributors
Python Programming: An Introduction to Computer Science, John M. Zelle
http://www.python.org.pl/kursy,jezyka.html
A Byte of Python, C. H. Svaroop
Guide to Numpy a.k.a. The Numpy Book, Travis Oliphant
http://docs.python.org !!! a zwłaszcza tutorial i library reference
http://wordaligned.org/articles/essential-python-reading-list
http://python.net/~goodger/projects/pycon/2007/idiomatic/ i linki tamże
książki o Pythonie na wikibooksach: http://en.wikibooks.org/wiki/Python, w szczególności
Podręcznik „Programowanie z Pythonem“ powstał na podstawie
W stosunku do polskiego tłumaczenia Dominika Kozaczko i in. materiał tutaj jest częściowo kopią, częściowo materiałem zmienionym, natomiast fragmenty są zupełnie nowe.
Nowe rozdziały i wiele poprawek zostały napisane przez Jarosława Żygierewicza, Maćka Kamińskiego, Zbyszka J-Szmeka.
Dodatkowo wykorzystane materiały:
Całość podręcznika jest udostępniona na licencji Creative Commons Uznanie autorstwa-Na tych samych zasadach 3.0 Polska.