TI:Programowanie z Pythonem/Wersja do druku

Z edu
brain.fuw.edu.pl/edu / TI:Programowanie z Pythonem> Programowanie z Pythonem/Wersja do druku
KL grafika.png KAPITAŁ LUDZKI
NARODOWA STRATEGIA SPÓJNOŚCI
Universitas Varsoviensis orzelek trans 116x128.png
UNIA EUROPEJSKA
EUROPEJSKI
FUNDUSZ SPOŁECZNY
European flag.svg
Projekt Fizyka wobec wyzwań XXI w. współfinansowany jest przez Unię Europejską ze środków Europejskiego Funduszu Społecznego w ramach Programu Operacyjnego Kapitał Ludzki.
Niniejszy podręcznik został stworzony dla studentów I roku neuroinformatykifizyki medycznej na Wydziale Fizyki Uniwersytetu Warszawskiego.
Powstanie tego podręcznika było możliwe w znacznej części dzięki wspaniałomyślności różnych osób udostępniających wyniki swojej pracy na licencji CC BY-SA i umożliwiającym tym samym swobodne kopiowanie, tłumaczenie i łączenie materiałów z różnych źródeł. Lista autorów, podziękowania i odnośniki do oryginalnych źródeł znajdują się poniżej. Prace nad podręcznikiem są częściowo finansowane z grantu przyznanego w ramach programu „Kapitał Ludzki“.
Edycja tego podręcznika jest możliwa na stronie http://brain.fuw.edu.pl/edu/PzP. Zapraszamy do wyłapywania błędów i nanoszenia poprawek.


If our basic tool, the language in which we design and code our programs, is also complicated, the language itself becomes part of the problem rather than part of its solution.
C.A.R. Hoare „The Emperor's Old Clothes“


Języki programowania powinny ułatwiać swoje użycie w poprawny sposób, a nie zapobiegać niepoprawnemu.
Kees Koster


Spis treści

Instalacja Pythona

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.

Na większość systemów

Można pobrać wersję akademicką ze strony firmy Entought: https://store.enthought.com/#canopy-academic

Dla użytkowników Windows

Istnieją wiele sposobów instalacji Pythona pod Windows:

  • gotowe dystrybucje zawierające zestaw narzędzi i pakietów dedykowanych do konkretnych zastosowań np. dla nas niezły jest Python(x,y)
  • wybieranie programu podstawowego i potem doinstalowywanie potrzebnych modułów np tak: Odwiedź dział download na stronie Python.org i pobierz najnowszy instalator Pythona 2.x dla Windows. Instalacja wygląda podobnie, jak w przypadku innych programów dla Windows.
Uwaga
Nie odznaczaj żadnych opcjonalnych komponentów podczas instalacji! Część z nich może być bardzo pomocna, szczególnie IDLE.
  • Pobierz i zainstaluj Active Python. Jest to instalator Pythona przygotowany przez ActiveState. Zawiera wiele użytecznych rzeczy, jak np. PythonWin, który może się przydać użytkownikom Windows.

Używanie Pythona w wierszu poleceń 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.

Dla użytkowników Linuksa i BSD

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.

Ikonka terminala z gnome

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
Uwaga
$ jest znakiem zachęty konsoli. Może się różnić w zależności od ustawień systemu, więc przyjmijmy, że odtąd będę używał znaku $ na oznaczenie znaku zachęty.

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:

  • Zainstalować pakiet binarny używając do tego programu zarządzającego pakietami w swoim systemie (np. apt-get w Ubuntu/Debianie, yum w Fedorze, urpmi w Mandrivie, pkg_add we FreeBSD, itd.). Zauważ, że potrzebne będzie połączenie z internetem.
  • Możesz też pobrać paczki binarne (np. DEB czy RPM), skopiować je na swój komputer i zainstalować.
  • Możesz skompilować źródła Pythona 2.x i zainstalować je. Instrukcja kompilacji jest dołączona do źródeł. Zauważ, że aby móc używać proponowanego w tej książce IDLE (więcej w następnym rozdziale), musisz skompilować Pythona z obsługą Tcl/Tk.

Dla użytkowników Mac OS X

Python jest domyślnie zainstalowany w Mac OS X 10.3 i wyższych. Jeśli chcesz zainstalować nowszą wersję Pythona, użyj MacPorts:

  • Zainstaluj MacPorts.
  • By otrzymać listę pakietów wykonaj sudo port search python — w momencie pisania najnowszą dostępną wersją Pythona 2.x jest Python 2.6.1 występujący w MacPorts jako python26.
  • Wykonaj sudo port install python26.

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.

Podsumowanie

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.


Pierwsze kroki

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.


Praca z Pythonem

Python jest interpreterem poleceń. Mamy dwie możliwości wydawania owych poleceń:

  • interaktywnie z linii poleceń,
  • zapisując polecenia w pliku tekstowym i wykonując je jako program.

Używanie linii poleceń interpretera

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:

Napis: >>> oznacza, że Python oczekuje na wprowadzenie polecenia. Z ang. nazywa się to prompt.

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?

  • Jeśli używasz IDLE bądź powłoki systemowej Linuksa/BSD/Mac OS X, wciśnij po prostu Ctrl-D.
  • Jeśli używasz Pythona w linii poleceń Windows, naciśnij Ctrl-Z, a następnie Enter.
Python dump.png

Pliki źródłowe

Wróćmy wreszcie do programowania.

Jak pisać programy w Pythonie?
Od tego momentu nasza standardowa procedura zapisu i uruchamianiu programów w Pythonie będzie wyglądała następująco:
  1. Otwórz swój ulubiony edytor → na naszych zajęciach IDLE :-)
  2. Wprowadź/Skopiuj kod programu podany w przykładzie.
  3. Zapisz plik pod nazwą podaną w komentarzu. Przyjmijmy, że programy Pythona będziemy zapisywali z rozszerzeniem .py.
  4. Teraz możesz uruchomić program jedną z poniższych metod:
  • w powłoce systemowej za pomocą polecenia python nazwa.py, gdzie nazwa jest nazwą pliku
  • użyj klawisza F5 w edytorze IDLE
  • wpisz execfile('nazwa.py') w lini poleceń interpretera, aby uruchomić program.



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).

Jak to działa?

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

Komentarze są ważne, ponieważ pomagają wprowadzać szczegółowe notatki związane z programem. Powinny one wyjaśniać, co program wykonuje, albo też informować o rzeczach, które czekają na poprawienie/dokończenie/zrobienie. Mogą się one okazać bardzo przydatne dla osób, które będą czytały Twój program. Również Tobie z pewnością się przydadzą, kiedy na przykład będziesz czytał swój program sześć miesięcy po jego napisaniu.

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.

Wcięcia

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.

Jak używać wcięć?
Nie używaj mieszaniny tabulatorów i spacji do stosowania wcięć, ponieważ nie działa to prawidłowo na wszystkich platformach. Zdecydowanie polecam żebyś używał pojedynczego tabulatora lub czterech spacji na każdy jeden stopień wcięcia.

Wybierz jeden z powyższych sposobów stosowania wcięć i stale używaj tylko tego sposobu.


Programy wykonywalne w Pythonie (na potem)

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.

Uzyskiwanie pomocy

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.

Uwaga
Aby zakończyć pomoc, wciśnij q.

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.

Podsumowanie i ćwiczenia



Wykonywalne programy w Pythonie

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.

Dodanie do ścieżki

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.

Możemy więc skopiować nasz program do katalogu obecnego w zmiennej 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.

Uwaga
Względem Pythona program, skrypt i oprogramowanie oznaczają dokładnie to samo.

Stałe i zmienne

Stałe dosłowne

Przykładem stałej dosłownej jest:

  • liczba, np. 5, 1.23, 9.25e-3
  • napis w rodzaju 'To jest napis' albo "To też jest napis!".

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.


Wpisywane przez nas liczby to stałe dosłowne. Proszę zwrócić uwagę na wyniki dzielenia 1/2 i 1.0/2

Liczby

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).

  • Przykładami liczb całkowitych są 2 i 1025.
  • Przykłady liczb zmiennoprzecinkowych to: 3.23 i 52.3E–4.

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

  • Jednostkę urojoną zapisujemy jako j po liczbie, tak więc
–5 + 4j = –5 + 4i
2.3 – 4.6j = 2,3 – 4,6i
1j = i


Napisy

Napis (ang. string) to po prostu ciąg znaków.


Apostrof (')

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.

Cudzysłów (")

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.

Potrójny cudzysłów lub apostrof (""" lub ''')

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ł.
'''

Sekwencje specjalne

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.

Napisy surowe

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.".

Napisy Unikodowe

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ć.

Napisy są niezmienne (ang. immutable)

Oznacza to, że jeżeli już raz utworzyłeś napis, nie możesz go zmienić.

Automatyczne łączenie napisów zapisanych obok siebie

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ą?'.

Zmienne

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.

Nazewnictwo identyfikatorów

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:

  • Pierwszym znakiem identyfikatora musi być mała lub wielka litera alfabetu łacińskiego (więc polskie znaki są niedopuszczalne) albo podkreślnik (_).
  • Pozostałe znaki mogą zawierać małe lub wielkie litery alfabetu łacińskiego, podkreślniki oraz cyfry (0-9).
  • Wielkość znaków w identyfikatorze jest ważna. Stąd mojanazwa i mojaNazwa to zupełnie co innego. Zwróć uwagę na wielkie N w drugim przykładzie.
  • Przykłady poprawnych identyfikatorów to: i, __moja_nazwa, nazwa_23, a1b2_c3.
  • Przykłady niepoprawnych identyfikatorów to: 2nazwy, nazwa ze spacjami, moja-nazwa.

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.


Przykład użycia zmiennych i stałych dosłownych

Teraz zobaczymy, jak w programach używać zmiennych razem ze stałymi dosłownymi.


Jak to działa?

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ą.


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.


Typy danych

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.

Obiekty

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".

Operatory

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
>>>


Tabela operatorów

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.

Operatory i ich użycie
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.

Kolejność wykonywania

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ć.

Priorytety operatorów
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.



Łączność operatorów

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 3^{\left(3^3\right)}, a nie \left(3^3\right)^3. Czyli potęgowanie w Pythonie zachowuje się tak jak normalnie w matematyce.

Przyrównania (==, <=, <=, >=, <, >, !=) w zasadzie są łączne lewostronnie, ale w praktyce w przypadku zwykłych obiektów wynik nie zależy od kolejności wykonania. Jeśli wielokrotne przyrównania są połączone w ciąg, to wartością takiego wyrażenia jest iloczyn logiczny wszystkich przyrównań.
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.



Wyrażenia

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.




Spodziewany wynik
$ python wyrazenia.py
Pole wynosi 10
Obwód wynosi 14

Jak to działa?

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.



Wykonanie warunkowe

Wprowadzenie

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

Składnia

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.

  • Przypadek I: wykonanie warunkowe bloku kodu:
if warunek:
    blok kodu,
    który ma być wykonany
    jeśli warunek jest prawdziwy
  • Przypadek II: wykonanie warunkowe jednego z dwóch bloków kodu:
if 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
  • Przypadek ogólny: kaskada warunków
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

Przykład 1

Chcemy napisać program realizujący następujący algorytm:

  1. ustal zmienną temperatura na pewną wartość np. 11
  2. Jeśli temperatura jest dodatnia wypisz komunikat: "jest ciepło!"

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.

Przykład 2

Zmodyfikujmy nieco nasz algorytm:

  1. ustal zmienną temperatura na pewną wartość np. 11
  2. Jeśli temperatura jest dodatnia wypisz komunikat: "jest ciepło!"
  3. W przeciwnym razie wypisz komunikat: "uwaga, przymrozek!"

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).

Zadanie

Proszę napisać program realizujący algorytm:

  1. poproś użytkownika po podanie temperatury
  2. przypisz podaną przez użytkownika wartość do zmiennej temperatura
  3. Jeśli temperatura jest powyżej 30 wypisz komunikat: "upał"
  4. jeśli jest powyżej 15 dodatnia wypisz komunikat: "jest ciepło!"
  5. W przeciwnym razie wypisz komunikat: "uwaga, chłodno!"

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ę: ' ) )

Zadanie: Zagadka

Algorytm zagadki jest następujący:

  1. przypisz do zmiennej liczba wartość 23
  2. pobierz od użytkownika liczbę całkowitą i przypisz ja do zmiennej strzal
  3. Jeśli wartości zmiennych liczba i strzal są równe to wypisz gratulacje
  4. jeśli wartość zmiennej liczba jest większa niż wartość zmiennej strzal to wypisz informację "Nie, szukana liczba jest większa od podanej"
  5. jeśli wartość zmiennej liczba jest mniejsza niż wartość zmiennej strzal to wypisz informację "Nie, szukana liczba jest mniejsza od podanej"

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):

Wartości logiczne

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.

Operacje logiczne

Koniunkcja

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 a \wedge b. 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.

Alternatywa

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 a \vee b. 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.

Zaprzeczenie

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

Wartość logiczna wyrażeń w Pythonie

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:

  1. w przypadku liczb, liczba 0 jest fałszywa, wszystkie pozostałe są prawdziwe
  2. w przypadku sekwencji (np. napisów) i innych kolekcji, tylko te puste, o długości 0, są fałszywe
  3. pozostałe obiekty są prawdziwe (o ile ich twórca nie podjął specjalnych działań by mogły mieć różne wartości logiczne)

TO DO: http://Link?


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

Obliczanie wartości wyrażeń logicznych w Pythonie

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 przypadku operatora and, jeśli pierwszy wyraz jest fałszywy, to drugi nie jest obliczany bo wiadomo, że całe wyrażenie jest fałszywe,
  • w przypadku operatora or, jeśli pierwszy wyraz jest prawdziwy, to drugi nie jest obliczany, bo wiadomo, że całe wyrażenie jest prawdziwe.

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ł:

x and y — jeżeli x jest fałszem, zwraca x, w przeciwnym wypadku zwraca y
x or y — jeżeli x jest prawdą, zwraca x, w przeciwnym wypadku zwraca y

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:

  1. Zaczynamy od obliczenia wartości lewego operandu.
  2. Jeśli wartość logiczna lewego operandu nie pozwala na rozstrzygnięcie wartości logicznej całego wyrażenia, obliczamy wartość prawego operandu.
  3. Jako wynik zwracamy ostatni obliczony operand, czyli ten który rozstrzyga o wartości logicznej całego wyrażenia.





Pętle

Pętla while

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.


Wyrażenia break i continue

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.

Pętla while.svg

Wyrażenie break

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.


Wyrażenie continue

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:


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.







Sekwencje i pętla for

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









Wiele różnych typów sekwencji

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.

Funkcje range i xrange

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.


Funkcja split

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.


Podsumowanie

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.

Blok else po pętli

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.


Przykład:

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"
Rezultat:
$ 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.

Pętle i litery z polskimi znakami diakrytycznymi

Zobaczmy jeszcze jeden przykład, wydawałoby się niemalże trywialny, na wykorzystanie pętli while...


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?

Jaką długość ma napis zawierający polskie litery?

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ń!


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:


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':


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'.

Podsumowanie

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ć.

Ćwiczenia





Funkcje

Wprowadzenie

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.


Parametry funkcji

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.


Zmienne lokalne

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.



Dostęp do zmiennych globalnych

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.


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.

Domyślne wartości argumentów

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ą.


Zaawansowane sposoby przekazywania argumentów

Argumenty nazwane

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:

  1. użycie funkcji jest prostsze, bo nie musimy się martwić o kolejność argumentów
  2. możemy podać wartości tylko tych argumentów, których chcemy (pod warunkiem, że parametry które pomieliśmy mają określone wartości domyślne).


Użycie argumentu nazwanego nazwą która nie odpowiada żadnemu parametrowi jest błędem.

Zmienna liczba parametrów

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ą.


W jaki sposób argumenty zostają przyporządkowane?

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.

Zmienna liczba argumentów nazwanych

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.



Polecenie return

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.


Uwaga
instrukcja return bez podanej wartości jest równoważna instrukcji return None. None jest obiektem języka Python reprezentującym brak wartości. Jest on używany na przykład do zaznaczenia, że jakaś zmienna nie ma wartości.

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:


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.

Opisy wbudowane w funkcje

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ę.


Używanie DocString

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.

Co powinna zawierać dokumentacja funkcji

Nawet krótka dokumentacja funkcji może być bardzo pomocna. Co powinno się w niej znaleźć?

  1. Jednolinijkowy opis działania wykonywanego przez funkcję: „Mnoży wektorowo argumenty i zwraca ślad wyniku.”
  2. Nie ma potrzeby pisać, czym jest opisywany obiekt (w tym przypadku funkcją), ani duplikować listy parametrów, bo użytkownik i tak to wie z opisu wygenerowanego automatycznie: „To jest funkcja która ma parametry a, b, c.”
  3. Dobrze jest napisać co funkcja zwraca, jeśli coś zwraca.
  4. Jeśli funkcja ma być wykorzystywana przez innych jako funkcja biblioteczna, to opis musi być znacznie dłuższy — powinien zawierać głębsze wyjaśnienie tego co funkcja robi, jeśli to jest istotne jakie ma ograniczenia i warunki działania.
  5. W przypadku funcji złożonych obliczeniowo, dobrze jest napisać jak czas wykonywania funkcji rośnie wraz ze wzrostem rozmiaru argumentów wejściowych.
  6. Dobrze jest podać przykład użycia funkcji i oczekiwanego wyniku. Zob. http://docs.python.org/library/doctest.html .

Docstrings to ważne narzędzie, którego warto używać, bo dzięki dokumentacji program jest łatwiejszy do zrozumienia.

Ćwiczenia



Moduły

Wprowadzenie

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.

Przykład:
#!/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'
Rezultat:
$ 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.

Jak to działa?

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:

  • uzycie_sys.py jako sys.argv[0],
  • my jako sys.argv[1],
  • jestesmy jako sys.argv[2]
  • argumentami jako sys.argv[3].

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).

Pliki pośrednie .pyc

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.

Uwaga
Pliki .pyc są zazwyczaj tworzone w tym samym folderze, co odpowiednie pliki .py. Jeżeli Python nie ma pozwolenia na zapis w tym folderze, pliki .pyc nie zostaną utworzone.

Konstrukcja from ... import ...

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.

Konstrukcja import ... as ...

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)

Samoidentyfikacja modułu

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.

Przykład:
#!/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.'
Rezultat:
$ python nazwa.py
Ten program jest uruchomiony samodzielnie.
 
$ python
>>> import nazwa
Zostałem zaimportowany z innego modułu.
>>>

Jak to działa?

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

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

Jak to działa?

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.

Funkcja dir

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']
>>>

Jak to działa?

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.

Paczki

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.

Podsumowanie

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.

Zadania

Liczby Fibonacciego

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 — sekwencje

Show me your flow charts and conceal your tables and I shall continue to be mystified, show me your tables and I won't usually need your flow charts; they'll be obvious.
Brooks, The Mythical Man-Month [1]


Wprowadzenie

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

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:

  1. sekwencyjne działania na obiektach od zerowego do ostatniego. Dzięki temu można użyć listy do iterowania pętli. Wyrażenie for element in lista: rozpoczyna pętlę, w której dostępny jest kolejno każdy element listy.
  2. wydobycie z listy obiektu o danym indeksie. Używamy to tego operatora indeksowania []

Ł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.

Przykład użycia listy

#!/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
Rezultat:
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ż']
Jak to działa?

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)
Istotna różnica jest taka, że posortowana jest nowym obiektem, a stara lista pozostaje nienaruszona.

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].

Przydatne metody klasy lista

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]

Zadanie: wariancja i odchylenie standardowe średniej

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:

wariancja \sigma^2 = \frac{1}{N -1} \sum_{i=1}^{N} (x_i - \mu)^2

oraz

odchylenie standardowe \sigma = \sqrt{\frac{1}{{N -1}}{\sum_{i=1}^{N} (x_i - \mu)^2}}

oraz

odchylenie standardowe średniej \bar\sigma = \frac{1}{\sqrt{N}}\sqrt{\frac{1}{{N -1}}{\sum_{i=1}^{N} (x_i - \mu)^2}}

Zadanie: outliers

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σ.

Zadanie: outliers 2

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 to tylko 3‰.

Krotka

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.

Przykład:
#!/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])
Rezultat:
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
Jak to działa?

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].

Notacja „krotkowa”

Nawiasy

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).

Krotka pusta lub z 1 elementem

Krotkę pustą tworzy się za pomocą pustej pary nawiasów:
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

  1. krotkę jednoelementową
  2. jedno-elementowe wyrażenie wzięte w nawias

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, )


Sekwencje

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).


Indeksy

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.

Przykład — indeksy dla napisu "string"

Indeksy dodatnie
>>> tekst[0]
's'
>>> tekst[1]
't'
>>> tekst[2]
'r'
>>> tekst[3]
'i'
>>> tekst[4]
'n'
>>> tekst[5]
'g'
Indeksy ujemne
>>> tekst = "string"
>>> tekst[-1]
'g'
>>> tekst[-2]
'n'
>>> tekst[-3]
'i'
>>> tekst[-5]
't'
>>> tekst[-4]
'r'
>>> tekst[-5]
't'
>>> tekst[-6]
's'

Wycinki

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:

  • domyślne położenie początku wycinka to początek sekwencji
  • domyślne położenie końca wycinka to koniec całej sekwencji

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.



Przykład drugi:
#!/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[:]
Rezultat:
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

Jak to działa?

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!





Odniesienia

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.

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.
Rezultat:
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.

Uwaga dla programujących w Perl
Pamiętaj, że przypisanie listy do zmiennej nie tworzy kopii! Zawsze do skopiowania sekwencji potrzebne jest pobranie pełnego wycinka.

Więcej o ciągach znaków

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).

Przykład:
#!/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)
Rezultat:
$ 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

Jak to działa?

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.


Struktury danych — słowniki i zbiory

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.

Słownik

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.

Przykład:
#!/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
Rezultat:
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

Jak to działa?

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.

Słowniki i krotki w przekazywaniu argumentów w wywołaniu funkcji

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.


Dodatkowe zadania i przykłady

Zob. też bardziej zaawansowane przykłady w rozdziale o wyjątkach.

Bardziej ambitne zadanie wymagające inteligentnego użycia słowników: anagramy.

Zbiór

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.

Przykład:
>>> 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'])


Jak to działa?

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.

Podsumowanie

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.


Numpy

Numpylogo.png

Pakiet Numpy

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:

  1. obiekty przechowywane w macierzy array muszą być wszystkie tego samego typu;
  2. obiekty array zachowują swój rozmiar; przy zmianie rozmiaru takiego obiektu powstaje nowy obiekt, a obiekt sprzed zmiany zostaje usunięty;
  3. obiekty array wyposażone są w bogaty zestaw funkcji operujących na wszystkich przechowywanych w obiekcie danych, specjalnie optymalizowanych do przetwarzania dużych ilości danych. Jak to działa zostanie zaprezentowane poniżej.

Tworzenie

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:

  1. indeks początkowy [opcjonalnie, domyślnie 0]
  2. indeks następny po końcowym
  3. krok [opcjonalnie, domyślnie 1]
>>> 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.

Inne
zob.numpy.mgrid, numpy.ogrid, numpy.linspace, numpy.zeros, numpy.ones, numpy.r_.

Wydobywanie danych

Pojedyncze liczby

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.svg

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ę).

Pod-macierze

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.

Indeksowanie macierzy macierzami

Do wybrania elementów z macierzy można tez użyć innej macierzy. Może to być

  • macierz liczb — wówczas są one traktowane jako indeksy. Wybieramy te elementy, które uzyskalibyśmy indeksując każdym z indeksów z osobna
  • macierz wartości logicznych (boolean) rozmiaru macierzy z danymi. Wybieramy te elementy, którym odpowiada True w macierzy indeksującej.

Uwaga: W wyniku dostajemy macierz jedno wierszową.

Przykład
>>> 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]
Więcej: http://docs.scipy.org/doc/numpy/user/basics.indexing.html


Porównanie liczb w Numpy i liczb w Pythonie

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.

Float32python.png
Float32.png

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).

lista czterech liczb w Pythonie
macierz czterech liczb w Numpy

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.

lista może zawierać różne obiekty
„Eksport” danych z Numpy do Pythona

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.

Specjalne wartości liczbowe

Pakiet Numpy wprowadza też szczególne wartości dla przechowywania nietypowych wyników obliczeń. Należą tutaj takie wartości jak:

  • inf opisująca wartość nieskończoną. Są dostępne również jej następujące warianty: PINF odpowiada wartości +∞, natomiast NINF wartości −∞. Do sprawdzenia czy badana zmienna x zawiera „normalną” czy nieskończoną wartość używamy funkcji isfinite(x) pakietu Numpy. Zwraca ona False w przypadku napotkania wartości nieskończonej w zmiennej x.
  • NaN opisującą nie-liczbę (przechowywaną w zmiennej liczbowej, NaN to skrót od angielskiego not a number), wartość, która nie reprezentuje żadnej liczby. Wartość taka pojawia się w przypadku próby wykonania pewnych niedozwolonych operacji matematycznych lub sygnalizuje wystąpienie wyniku takiej operacji. Warto tutaj zauważyć, że porównanie numpy.NaN == numpy.NaN daje wynik False. Aby sprawdzić czy mamy do czynienia z taką wartością używamy funkcji isnan pakietu Numpy.

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.

Dlaczego warto używać Numpy?

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ń.

Podsumowanie

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.


Zadanka

Zob. zadania z operacji na macierzach.


Matplotlib

Wprowadzenie do pakietu Pylab/Matplotlib na przykładach

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.

Wykres funkcji y = f(x)

Prześledźmy działanie poniższego programu:

import pylab
x = [1,2,3]
y = [4,6,5]
pylab.plot(x,y)
pylab.show()
Rezultat

Wykres1.gif

Jak to działa?

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.

Rysujemy wykres funkcji sinus

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()
Rezultat

Wykres2.gif

Jak to działa?

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ą.

Ulepszamy wykres

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()
Rezultat

Wykres3.gif

Jak to działa?

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:

  1. 'r:' — ten parametr steruje wyglądem rysowanej linii wykresu. Pierwsza litera tego napisu określa kolor linii (na przykład r: czerwony, b: niebieski, g: zielony, y: żółty, k: czarny). Drugi znak napisu określa wygląd samej linii (np. -: ciągła, :: kropkowana, o: okrągłe punkty bez linii, +: krzyżyki bez linii, itd.).
  2. linewidth=6 — ten parametr zmienia grubość rysowanej linii.

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).

Kilka wykresów we wspólnych osiach

Pierwsza wersja:

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()

Rezultat:

Wykres 2 linie.svg

Jak to działa?

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 )

Druga wersja:

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()

Rezultat:

Wykres 3 linie.svg

Jak to działa?

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.

Histogram (diagram liczebności)

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()

Rezultat:

Hist1.gif

Jak to działa ?

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.

  • Możemy kontrolować albo ilość binów jeśli bins jest liczbą, albo położenie binów (może być nierównomierne) jeśli bins jest sekwencją.
  • range kontroluje zakres przeliczanych danych. Domyślnie jest to (x.min(), x.max()). Jeśli podamy inne wartości to dane mniejsze od dolnego zakresu i większe od górnego zakresu nie zostaną uwzględnione.
  • normed kontroluje czy zliczenia w binach mają być unormowane do jedności — może to być przydatne do wykreślania np. rozkładów gęstości prawdopodobieństwa.
  • Jeżeli parametr cumulative ma wartość True to kolejne biny mają do swojej własnej ilości zliczeń dodawaną sumę zliczeń wszystkich dotychczasowych (dla mniejszych wartości x) binów. W ten sposób możemy na przykład narysować dystrybuantę rozkładu prawdopodobieństwa.

Pozostałe argumenty kontrolują wygląd i układ rysunku. Ich znaczenie można poznać z dokumentacji help(p.hist)

Bardziej zaawansowany przykład:

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()

Rezultat:

Hist2.gif

Wykres biegunowy

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()

Rezultat:

Polar1a+2.gif


Prosta animacja

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()

Jak to działa

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.


Wizualizacja zawartości macierzy dwuwymiarowej

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()

Rezultat:

Matrix1+2+3.png

Powierzchnia dwuwymiarowa w przestrzeni trójwymiarowej

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()

Rezultat:

Wykres3D.svg

Jak to działa?

Wiele wykresów w jednym oknie

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()

Rezultat:

Multi1.gif






Wejście i wyjście

Wprowadzenie

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.

Wprowadzanie danych przez użytkownika

Przykład:
#!/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")
Wynik:
$ 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

Jak to działa?

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.


Pliki

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.

Przykład:

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
Rezultat:
$ python zapis_pliku.py

Część 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,
Rezultat:
$ 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.
Jak to działa

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ę:

  1. Zmienna tekst jest wielo-linijkowym ciągiem znaków stworzonym przez użycie potrójnego apostrofu ''' jako ogranicznika.
  2. Bezpośrednio po otwarciu tego napisu jest użyta kontynuacja linii \, po to, by napis miał naturalne wcięcia (czyli ich brak). Gdyby znaku kontynuacji nie było, napis zawierałby dodatkową pustą linjkę na samym początku.
  3. Po zapisaniu zmian „wypychamy” je na dysk przez użycie metody close. Normalnie operacje na plikach są wykonywane dopiero wtedy, gdy uzbiera się ich większa ilość. Niemniej, tutaj zaraz chcemy zacząć odczytywać ten sam plik i musimy się upewnić, że nasze zmiany naprawdę znalazły się w pliku zanim zaczniemy ten plik czytać .
  4. Iteracja po obiekcie file oznacza iterację po linijkach, co tutaj wykorzystujemy.
  5. Każda linijka (zmienna wers) jest zakończona swoim znakiem nowej linii i dlatego wywołujemy print tak, by nie dodało nowego znaku końca linii, czyli z , na końcu.
  6. Gdybyśmy nie wywołali close, to zmiany w pliku i tak zostałyby zapisane automatycznie najpóźniej w momencie zakończenia programu. Gdy program kończy swoje działanie, wszystkie zmiany są zapisywane i pliki są zamykane przez system operacyjny.

Więcej o otwieraniu plików

Pliki mogą być otwierane w różny sposób — mówimy, że są otweirane w danym trybie. Dostępne tryby to:

  • r (ang. read) otwarcie do czytania
  • w (ang. write) otwarcie do pisania, jeśli plik już istnieje to domyślnie kasujemy jego zawartość
  • a (ang. append) otwarcie do dopisywania. W tym trybie jeśli pliku nie było to zostanie utworzony, a jeśli był to nasze wpisy są dodawane na końcu.

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.

Przykłady



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.



Pickle'owanie i podobne czynności

O module pickle

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.

Przykład
#!/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
pomidory
Jak to działa

Aby 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.

Zapisywanie danych w numpy

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:

plik
napis zawierający nazwę pliku lub obiekt plik (wytworzony np. przez wywołanie funkcji open). Do nazwy pliku zostanie dodane rozszerzenie .npz (chyba, że już wcześniej nazwa pliku je zawierała)
argumenty
wszystkie pozostałe argumenty na liście po nazwie pliku są traktowane jako zmienne do zapisania, mogą to być też dowolne wyrażenia. Argumenty przekazane na tej liście będą zapisane z domyślnymi nazwami: arr_0, arr_1, itd.
argumenty_nazwane
argumenty te przekazywane są jako pary klucz=wartość. Zmienna wartość zostanie zapisana razem z nazwą klucz


Uwaga

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.


Pliki matlabowe

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)


Porównanie danych „świeżych” i „konserwowych”

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:

  • Służy tylko do wymiany pomiędzy różnymi programami w Pythonie.

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.

  • Można go używać do wymiany pomiędzy różnymi wersjami Pythona, ale
  • nie nadaje się do naprawdę trwałego przechowywania 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.

  • Jeśli pickluje się obiekty, które nie należą do podstawowego zestawu takiego jak napisy, liczby, listy, słowniki, to zachowane dane są silnie związane z używaną przez nas hierarchią klas.

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.


dane.txt
mymat.txt



Wyjątki i inne mechanizmy obsługi błędów

Informowanie o błędach

Czasami w trakcie wykonywania operacji przez program występuje sytuacja, która uniemożliwia jej wykonanie. Typowe sytuacje wymagające specjalnych reakcji programu to:

  1. błąd w korzystaniu z zewnętrznych zasobów np.:
    • program miał za zadanie otworzyć plik o określonej nazwie, a okazało się, że takiego pliku nie ma
    • program miał pobrać stronę www ze zdalnego serwera, a nie udaje się nawiązać połączenia
  2. niemożność poprawnego wykonania
    • program miał za zadanie znaleźć długość ósmej linijki w pliku, a plik ma tylko siedem linijek
    • program miał obliczyć pierwiastek liczby podanej przez użytkownika, a podana liczba jest ujemna (i program nie jest świadomy istnienia liczb zespolonych)
  3. pogwałcenie wewnętrznej spójności programu: w miarę jak program się rozrasta rośnie prawdopodobieństwo, że wywołamy funkcję w sposób niezgodny z jej wymaganiami
    • np. funkcja znajdująca liczbę w tablicy oczekuje tablicy posortowanej, a wywołamy ją z tablicą nieposortowaną
    • np. zmienna opisująca promień koła ma wartość ujemną

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.

Jak działają wyjątki w Pythonie — rzucanie

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.

Obsługa wyjątków w Pythonie — chwytanie

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:

  • Po pierwsze, możemy chwytać na raz wiele różnych typów wyjątków i obsługiwać je w ten sam sposób.
  • Po drugie, możemy chwytać wiele różnych typów wyjątków i obsługiwać je w różny sposób.
  • Po trzecie, możemy chwytać wyjątki wszystkich typów.
  • Po czwarte, możemy napisać kod, który zostanie wykonany jeśli nie został rzucony wyjątek, ale poza obszarem łapania wyjątków.
  • Po piąte, możemy napisać kod który zostanie wykonany niezależnie od tego czy program wykonał się normalnie czy został rzucony wyjątek.
 [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.

Hierarchia wyjątków

Exception-hierarchy.svg

Inne sposoby

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.

Zwracanie zarezerwowanej wartości

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)

Zwracanie wielu zarezerwowanych wartości

  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, ...

Zwracanie jednej zarezerwowanej wartości i przechowywanie informacji w zmiennej globalnej

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.

Natychmiastowe kończenie programu

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.

Wywoływanie funkcji do obsługi błędów

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.

Wyjątki kontra inne mechanizmy błędów

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.

Przykłady

Przechowywanie wyniku długotrwałych obliczeń

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.

Zaglądanie do słownika

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)

Dostęp do pliku

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.

Sprzątanie po sobie

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()

Sprzątanie zestandaryzowane

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 zorientowane obiektowo

Entia non sunt multiplicanda praeter necessitatem.
William Ockham


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ęć.


Szybkie wprowadzenie do obiektów i klas

Obiekt

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
definiuje wspólne własności i zachowanie obiektów. Klasa zawiera funkcje przeznaczone do używania z obiektami tej klasy — metody.

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 = 5

Notacja obiektowa

Waż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.

Jak w Pythonie tworzy się nowe klasy?

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...
Jak to działa?

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ą.

Zmienna self
W definicji funkcji dlugosc podany jest jeden parametr — self. Niemniej, w momencie wywołania, piszemy w1.dlugosc(), nie podając żadnych argumentów. Wewnątrz metod, zmienna self odnosi się do samego obiektu. Dzięki temu możliwy jest dostęp do pól obiektu, np. self.a. W momencie wywołania metody obiektu, zostaje on automatycznie wstawiony jako pierwszy argument metody i użytkownik podaje o jeden mniej argument niż metoda wymaga.

Specjalne metody w Pythonie

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.

Metoda __init__

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)).

Metoda __str__

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

Operatory dwuargumentowe są kolejnym typem metod specjalnych.

  • Ich nazwy zawierają po dwa znaki podkreślenia na początku i na końcu.
  • Wywoływane są gdy w kodzie pomiędzy dwoma obiektami pojawia się symbol operatora, np. +
  • Z każdym operatorem związana jest domyślna nazwa metody, która definiuje co programista przewidywał zrobić gdy np. dodawał do siebie dwa wektory

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

Inne funkcje specjalne

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)).

Życie obiektu

Stworzenie w wyniku wywołania klasy

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)

Inicjalizacja zawartości obiektu

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____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:

  1. self → nowo stworzony obiekt, jeszcze bez pól
  2. a → 11, argument przekazany w wywołaniu
  3. b → −4, argument przekazany w wywołaniu

Na początku funkcji __init__ nasz obiekt jest wydmuszką, która nawet nie ma swoich pól ab. 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.aself.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.aself.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]

Życie obiektu

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.

Śmierć obiektu

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ć.

Przykład: historia życia dwóch obiektów

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')
Obiekt z nazwą.png

Teraz przypiszmy obiektowi drugą nazwę.

  nowa_nazwa = nazwa
Obiekt z nazwą i nazwą.png


Teraz stwórzmy nowy obiekt (klasy Obiekt) i zachowajmy go w zmiennej nazwa.

  nazwa = Obiekt('zielony')
Obiekty dwa.png

Teraz użyjmy operatora del.

  del nowa_nazwa
Obiekty przed usunięciem.png

Jak 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.

Przykład: kompletna definicja klasy Wektor

# -*- 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.aself.b),
aby wykonać niezbędne obliczenia i zwrócić długość wektora.

Dwa sposoby wykonania operacji: w miejscu i ze stworzeniem nowego obiektu.

  1. obroc zmienia obiekt
  2. obrocony tworzy nowy, stary pozostawiajac bez zmian

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____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.

  1. __add__ pozwala na dodawanie dwóch Wektorów. Tworzony jest nowy obiekt.
  2. dodawanie robi dokładnie to samo, tylko sposób wywołania jest inny.

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.

Dziedziecznie

Simple inheritance.svg

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.

Duck-typing

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.

Wielodziedziczenie

Diamond inheritance.svg

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.


Funkcja super

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.

Diamond inheritance+super.svg
>>> 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().

Stare i nowe klasy w Pythonie 2

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.


Wybrane zagadnienia numeryczne

Rozwiązywanie równań liniowych

Przykład:

Rozwiązanie układu równań liniowych:


\left\{ \begin{array}{l}
x_1+3x_3=3\\
2x_1+x_2+5x_3=2\\
4x_1+8x_2+8x_3=1
\end{array} \right.
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
Rezultat
[-12.75   1.25   5.25]
Jak to działa?

Nasz wyjściowy układ równań liniowych możemy zapisać w postaci macierzowej Ax = b:


\begin{array}{cccc}
\left( \begin{array}{ccc}1 & 0 & 3\\2 & 1 & 5\\4 & 8 & 8\end{array} \right)&
\left( \begin{array}{c}x_1\\x_2\\x_3\end{array} \right)&
=&
\left( \begin{array}{c}3\\2\\1\end{array} \right)\\
\ &\ &\ &\ \\
A&x&\ &b \end{array}
.

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.

Inne podejście

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:


\begin{array}{l}
Ax=b\quad\quad\quad\quad\quad\quad \mid A^{-1}\cdot \\
\underbrace{A^{-1}\cdot A}_{=1} \cdot x=A^{-1} \cdot b \\
x=A^{-1}b
\end{array}



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
Rezultat
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]


Wyrażenia lambda

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:

  1. zwyczajnie, z wykorzystaniem słowa kluczowego def,
  2. jako tzw. wyrażenie λ, z wykorzystaniem słowa kluczowego lambda.

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:

  1. Wyrażenie λ musi być pojedynczym wyrażeniem.
  2. Wyrażenie λ nie może zawierać docstringa.

Wyrażenia λ są wygodne ze względu na to, że wymagają mniej stukania w klawisze.

Rozwiązywanie równań dowolnych

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)
Rezultat:
0.099669956223525716
0.099669956223525716
Jak to działa?

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).



Numeryczne obliczanie całek oznaczonych

Przykład:
# -*- 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)
Rezultat:
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)
Jak to działa?

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ązywanie równań różniczkowych

Przykład:

Rozwiązanie równania różniczkowego \frac{dy(t)}{dt}=y 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()
Rezultat

ODE1.png

Jak to działa?

Poszukujemy funkcji y(t) spełniającej nasze równanie różniczkowe y^\prime=y. 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).

Równania wyższych stopni i układy równań różniczkowych

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ą):


m\ddot{x} = - k x

Dzieląc to równanie stronami przez m otrzymujemy standardowe równanie oscylatora harmonicznego o częstości \omega = \sqrt{\frac{k}{m}} :


\ddot{x} = - \omega^2 x

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 v = \dot{x} (prędkość masy). Teraz nasz układ wygląda tak:


\left\{ \begin{array}{l}
\dot{x} = v \\
\dot{v} = - \omega^2 x
\end{array}
\right.
# -*- 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:

  1. nazwa funkcji obliczającej pochodne — prawa_strona_rownania, zwraca wektor [\dot{x},\ \dot{v}], czyli [v,\ -\omega ^2 x];
  2. warunek początkowy — lista w = [0, 3], czyli [x(0), v(0)];
  3. sekwencja punktów czasu — wektor t = np.linspace(0,5,51), czyli [0, 0,1, 0,2,... 4,9, 5].

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.


Rezultat

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.

ODE2.png

Transformata Fouriera

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()
http://brain.fuw.edu.pl/~jarek/SYGNALY/TF/c4spin.txt
Rezultat

FFT.gif

Jak to działa

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.



Książki o Pythonie

Bookstack.svg

Nauka Pythona od podstaw

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

  • + linki tamże

http://ocw.mit.edu/OcwWeb/Electrical-Engineering-and-Computer-Science/6-00Fall-2007/Readings/index.htm

  • + linki tamże

Python Programming wiki

A Byte of Python, C. H. Svaroop

Numpy i Scipy

Guide to Numpy a.k.a. The Numpy Book, Travis Oliphant

Numpy User Guide

Różne

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/linki tamże

książki o Pythonie na wikibooksach: http://en.wikibooks.org/wiki/Python, w szczególności

Książki o Linuxie

Lemmling Cartoon penguin.svg

W formie martwego drzewa

  • Peter Norton, Peter Norton's Guide to UNIX

dostępny w BUWie

W wersji elektronicznej

O skrypcie

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.

CC-88x31.png
Osobiste