1. Cel wprowadzenia ¶
Ten krótki kurs nie ma, bynajmniej, na celu nauki języka Python, tzn. systematycznego wprowadzenia do języka, począwszy od postaw, a skończywszy na zaawansowanych tematach. Kurs ma raczej pokazać jak w łatwy sposób wykonać większość zadań z przedmiotu Metody numeryczne.
Z tego też powodu dla osób znających język Python to wprowadzenie może być dość dziwne, gdyż część tematów została potraktowana w najlepszym wypadku powierzchownie. W szczególności nie będziemy zastanawiać się nad zbiorami, słownikami i krotkami, czy też nad kwestią programowania obiektowego.
Podstawowe rzeczy, na których nam zależy to:
- typy zmiennych w Pythonie oraz instrukcje sterujące,
- listy, odwoływanie się do nich i ich tworzenie (w szczególności za pomocą list składanych - list comprehensions)
- tworzenie najprostych wykresów za pomocą pakietu matplotlib,
- pakiet numpy, czyli łatwe tworzenie macierzy oraz praca na nich
2. Środowisko ¶
W trakcie zajęć bedziemy korzystać z Wirtualnego Laboratorium Fizyki (WLK) https://wlk.fizyka.pw.edu.pl/. Po zalogowaniu należy
- wybrać Ubuntu 20.04 jako system, z którego będziemy korzystać,
- otworzyć terminal dokonać eksportu dwóch zmiennych w linii komend wpisując i zatwierdzając:
export JUPYTER_RUNTIME_DIR=/tmp/test
orazexport JUPYTER_CONFIG_DIR=/tmp/test
- uruchomić środowisko Jupyter wpisując w linii komend
jupyter-notebook
W efekcie tych działan powinno pojawić się okno przeglądarki z panelem środwiska Jupyter (UWAGA! To może trochę potrwać).
W tym momencie albo uruchamiamy isniejący notatnik (pliki z rozszerzeniem ipynb) albo klikamy w prawym górnym rogu na rozwijaną listę New i wybieramy Python3.
Pojawi się wtedy nowa zakładka z notatnikiem. W prawym górnym rogu wyświetli się napis Kernel starting, please wait... i gdy zniknie, jesteśmy w stanie już pracować.
UWAGA! Za każdym razem gdy zamkniemy terminal ustawione przez nas zmienne znikną. Żeby uniknąc każdorazowego exportowania zmiennych, możemy:
- stworzyć plik o nazwie np.
zmienne_jupyter
, wpisać do niego te dwie linijki i uruchamiać go komendąsource ./zmienne_jupyter
, - dołożyć te dwie linijki na koniec pliku
bashrc
, wtedy zmienne będą się wczytywać automatycznie.
Oczywiście, za każdym razem należy uruchomić środowisko komendą jupyter-notebook
.
3. Kalkulator ¶
Nasz notatnik może zostać potraktowany jak wielki kalkulator. W tym celu musimy jedynie wpisać do komórki instrukcję taką jak
1 + 1
i zatwierdzić ją kombinacją SHIFT+ENTER
1 + 1
2
Żeby kalkulator miał głębszy sens, potrzebne są funkcje matematyczne. Te akurat nie są domyślnie uruchamiane w Pythonie - potrzebujemy do tego odrębnej biblioteki, które noszą tu nazwę "modułów". Uruchamianie modułów może odbywać się na kilka sposobów (UWAGA! można to przeczytać później):
import nazwa_modułu
- importujemy cały moduł i następnie odwołujemy się do jego funkcji poprzeznazwa_modułu.nazwa_funkcji
import nazwa_modułu as inna_nazwa
- działa tak samo jak powyżej (czyli odwołujemy się jakoinna_nazwa.nazwa_funkcji
, ale jest stosowane zwykle wtedy, gdy nazwa modułu jest bardzo długafrom nazwa_modułu import nazwa_funkcji
- w ten sposób nie musimy podawać już nazwy modułu, po prostu używamy wszędzienazwa_funkcji()
,from nazwa_modułu import *
- importujemy wszystkie funkcje z modułu - nie jest to uznawane za najlepszy sposób w przypadku długich kodów, ciężko się wtedy zorientować, czy dana funkcja została definiowana przez użytkownika, czy pochodzi z modułu.
W naszym przypadku skorzystamy z modułu math
, za pomoca pierwszej opcji; beziemy mogli się dzięki temu odwoływac do takich funkcji jak math.sin()
czy math.exp()
:
import math
math.sin(10) + math.cos(10)
-1.383092639965822
Warto przy tym wiedzieć, że w module są też popularne stałe np. math.pi
, jak również math.inf
(czyli nieskończoność), zauważając, że ta ostatnia działa w wyrażeniach artymetycznych na zasadzie granicy, tzn:
math.exp(-math.inf)
0.0
gdyż $\lim_\limits{x \rightarrow \infty} \mathbf{e}^{-x} = 0$
4. Przypisanie¶
Jedną z najważnijszych instrukcji jest przypisanie, czyli ogólnie zrecz mówiąc nadanie zmiennej wartości. W języku Python (w odróżnieniu od np. języka C) nie deklarujemy uprzednio typu zmiennej - jest on narzucany poprzez jej przypisanie. Wpisanie nazwy zmiennej zatwierdzenie SHIFT+ENTER wypisuje jej zawartość. Przy okazji możemy się przekonać, że pojedyncza komórka może pomiescić więcej niż jedną linię kodu - do kolejnej linii przechodzimy za pomocą ENTER i dopiero całość zatwierdzamy SHIFT+ENTER:
x = 1
x
1
x = 128
x
128
Wypisywanie zawartości zmiennej poprzez jej podanie jest jednym sposobem; innym jest użycie funkcji print()
, w której po przecinkach możemy podać kolejne zmienne, wypisując je jednocześnie. Możemy też jednocześnie przypisać kilka zmiennych, podając je po przecinku po lewej stronie przypisania, a po prawej, również po przecinku, podać wartości:
x, y = 2, 128
print(x, y)
2 128
5. Typy zmiennych ¶
Na naszych zajęciach będziemy wykorzystywać zmienne całkowie (int), zmiennoprzecinkowe (float) oraz logiczne (bool). Dodatkowo często korzysta sie także z typu tekstowego (str).
5.1 Zmienne całkowite i zmiennoprzecinkowe ¶
Generalnie kwestia, czy liczba jest całkowita czy zmiennoprzecinkowa nie będzie raczej spędzała nam snu z powiek - szczególnie, że konwersja pomiędzy tymi typami odbywa się automatycznie, np. jeśli podzielimy dwie liczby całkowite przez siebie, to otrzymamy liczbę zmiennoprzecinkową. Sprawdzimy to poniżej wykorzystując funkcję type()
:
x, y = 1, 2
z = x / y
print(x, y, type(x), type(y))
print(z, type(z))
1 2 <class 'int'> <class 'int'> 0.5 <class 'float'>
Oprócz standardowych operacji (+, -, *, /) można również skorzystać z operatora potęgowania (**) a także dzielenia bez reszty (//) czy też reszty z dzielenia (%):
2 ** 3
8
10 // 3
3
10 % 3
1
Wyjście funkcji print()
możemy modyfikować używając modyfikatorów:
print(f'To jest liczba z z dokładnością do 2 miejsc po przecinku {z:1.2f}, a tak wygląda w notacji naukowej {z:1.2E}')
To jest liczba z z dokładnością do 2 miejsc po przecinku 0.50, a tak wygląda w notacji naukowej 5.00E-01
5.2 Zmienne logiczne ¶
Wartości logiczne są reprezntowane przez dwie stałe True i False. Możemy je oczywiście po prostu przypisać do zmiennych (i sprawdzić, czy zmienna jest typu logicznego):
x = True
type(x)
bool
natomiast najczęsciej będą one efektem sprawdzania warunków (patrz dalej). Na zmiennych logicznych możemy wykonywac operacje negacji (not), koniunkcji (and) czy alternatywy (or):
x, y = True, False
print(not x)
print(x or y)
print(x and y)
False True False
Należy odróżnić operator not od wykrzyknika wykorzystywanego przy sprawdzaniu nierówności, tj. jako wyrażenie != (nie równa się):
10 != 9
True
5.3 Zmienne tekstowe ¶
Ostatnim typem są zmienne tekstowe (będące rodzajem sekwencji, patrz poniżej); w Pythonie nie ma rozróżnienia pomiędzy pojedynczym znakiem i łańcuchem, składającym się z wielu znaków:
x, y = "a", 'napis'
print(type(x), type(y))
<class 'str'> <class 'str'>
6. Struktury danych ¶
Python posiada dość szeroką gamę struktur danych, w których łatwo się na początku pogubić. Ogólnie, na potrzeby zajęć, możemy je podzielić na:
- sekwencje: listy (lists), krotki (tuple, tuples), zakresy (ciągi, range); do tej części należą także zmienne tekstowe (strings), które są sekwencjami znaków,
- zbiory (sets),
- słowniki (dictionaries).
Większość czasu poświęcimy na omówienie list, gdyż właśnie te obiekty będą dla najbardziej przydatne podczas zajęć.
6.1 Listy ¶
Stosując odpowiedniki z języka C, możemy powiedzieć, że jest to tablica (również wielowymiarowa) wyposażona w pewne możliwości listy jednokierunkowej z języka C.
Lista to uprządkowana struktura danych (czyli kolejność jej elemntów ma znaczenie). Jest ona modyfikowalna i, co może być dość zaskakujące, nie musi zawierać elementów tego samego typu.
Z punktu widzenia zapisu jesto lista wartości (elementów) rozdzielonych przecinkami ujęta w nawiasy kwadratowe.
Listę możemy zainicjować podając jej elementy "z palca", np:
l = [3, -1, 8, 10, 2, -2.8]
l
[3, -1, 8, 10, 2, -2.8]
Elementy listy są indeksowane od zera, a indeksy podajemy w nawiasach kwadratowych:
l[0]
3
Nie jesteśmy ograniczeni do podawania pojedynczych indeksów - możemy użyć operatora wycinania (wykrawania, slice) czyli dwukropka : lub dwukropków :: aby określić przedział indeksów, np. x[0:2] oznacza, że chcemy wypisać dwa pierwsze elementy listy, a x[2:8:2] wypisuje elemnty listy pod indeksami 2, 4 i 6:
l[0:2]
[3, -1]
l[1:5:2]
[-1, 10]
Brak indeksu po lewej stronie oznacza to samo co 0, a po prawej - co koniec listy. W efekcie zapis x[:] to po prostu cała lista, a np. x[::2], to co drugi element listy:
l[:3]
[3, -1, 8]
l[4:]
[2, -2.8]
l[:]
[3, -1, 8, 10, 2, -2.8]
l[::2]
[3, 8, 2]
Listy możemy również indeksować od tyłu, podając znak minus, tzn. x[-2]
oznacza, że wypisujemy drugi od końca element listy x
. Oczywiście, możemy jednocześnie używać indeksów dodatnich i ujemnych:
l[-2]
2
l[1:-2]
[-1, 8, 10]
Tak jak została to na wstepie wspomniane, listy nie muszą być obiektem jednorodnym (choć zwykle są):
l1 = [True, 1, "abc", 2, -128.567]
l1
[True, 1, 'abc', 2, -128.567]
6.1.1 Zmiana zawartości listy¶
Podając konkretne indeksy, jesteśmy w stanie podmienić aktualną zawartość listy, np:
x = [1, 2, 3]
x[1] = 5
x
[1, 5, 3]
Możemy przy tym korzystać z operatora slice, aby od razu zmienić całą część listy:
x = [1, 2, 3, 4, 5]
x[1:3] = [128, 129]
x
[1, 128, 129, 4, 5]
6.1.2 Metody związane z listami¶
Oprócz korzystania z list podobnie jak z tablic w języku C, możemy używac także funkcje bardziej kojarzących się z listami jednokierunkowymi. Funkcje te zapisywane są w postaci metod tzn nazwa_listy.funkcja(argument)
i daja np. możliwość dodania elementu na koniec listy nazwa_listy.append(nowy_element)
, wstawienia elementu w konkretnym miejscu w liście nazwa_listy.insert(indeks, nowy_element)
czy usunięcia ostatniego (lub dowlonego) elementu listy nazwa_listy.pop(indeks)
:
x = [10, 1, 20, 30, 22]
x.append(18)
x
[10, 1, 20, 30, 22, 18]
x.insert(2, 333)
x
[10, 1, 333, 20, 30, 22, 18]
x.pop()
18
x
[10, 1, 333, 20, 30, 22]
x.pop(4)
30
6.1.3 Kopiowanie list¶
W wielu przypadkach potrzebujemy stworzyć kopię danej listy i dalej na niej pracować, zachowując oryginalną listę nietkniętną. Wykonanie operacji przypisania a=b
na poziomie list tworzy połączenie pomiedzy tymi obiektami, tzn każda zmiana elementów będzie widoczna w obu listach:
a = [1, 2, 3]
b = a
print(a, b)
[1, 2, 3] [1, 2, 3]
a[0] = -100
b[2] = 128
print(a, b)
[-100, 2, 128] [-100, 2, 128]
Należy przy tym pamiętać, że jeśli stworzymy listę na nowo, to połączenie zostanie zerwane:
b = [1, 2, 3]
print(a, b)
[-100, 2, 128] [1, 2, 3]
Aby otrzymać kopię zgodną z naszymi pierwotnymi założeniami (tzn. niezależną od oryginału) należy skorzystać z metody copy()
:
a = [1, 2, 3]
b = a.copy()
a[0] = 128
print(a, b)
[128, 2, 3] [1, 2, 3]
Uwaga! Dla list zagnieżdżonych (patrz poniżej) sytuacja się komplikuje.
6.1.4 Listy zagnieżdżone¶
Częściowo można je traktować jak tablice wielowymiarowe w języku C. Odwołujemy się do nich podobnie, pierwszy nawias kwadratowy odnosi się do pierwszego poziomu zagnieżdżenia, drugi do drugiego etc:
A = [[1, 2, 3], [4, 5, 6, [7, 8]]]
A
[[1, 2, 3], [4, 5, 6, [7, 8]]]
print(A[0], A[1])
[1, 2, 3] [4, 5, 6, [7, 8]]
print(A[1][3], A[1][3][0])
[7, 8] 7
6.2 Zakresy (range) ¶
Zakresy, realizowane poprzez funkcję range()
są bardzo przydatnymi obiektami, gdyż, jak sama nazwa wskazuje, umożliwiają stworzenie ciągu liczb (będziemy później z nich korzystać przy wykonywaniu pętli). Ogólna idea jest taka, że range()
tworzy sekwencje - nie jest to lista sensu stricto - ale możemy się do niej odwoływać podobnie jak do listy, z tą różnicą, że nie jest ona modyfikowalna. Wywołanie funkcji range(10)
tworzy sekwencję liczb od 0 do 9, natomiast range(-5, 5, 2)
stworzy ciąg liczb od -5 do 5 z krokiem 2:
s = range(10)
s
range(0, 10)
s[5]
5
Jeśli tego potrzebujemy, możemy zrzutować obiekt typu range na listę:
list(range(0, 10))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
7. Instrukcje sterujące (flow control): pętle, warunki, funkcje ¶
Ciężko sobie wyobrazić język bez instrukcji sterujących, aczkolwiek zminimalizowanie ich wykorzystania jest możliwe, jak się wkrótce przekonamy. W szczególności zwykle potrzebujmy następujących konstrukcji:
- pętli - najczęściej
for
orazwhile
, - instrukcji warunkowych
if
else
, - funkcji.
Opis warto zacząć od tego, że w Pythonie aby wskazać blok intrukcji używamy dwukropka oraz wcięcia - nie ma do intrukcji zamykających blok ani też klamer. Pętle, instrukcje warunkowe czy funkcje mają tę samą strukturę: zaczynają się od słowa kluczowego, następnie pojawia się warunek lub nazwa, następnie dwukropek, po którym musimy zrobić wcięcie (tabulatorem) i tam wstawić ciało pętli, warunku czy funkcji.
7.1 Pętla for
¶
Pętle for różnią się od typowych konstrukcji np. w języku C - wykonujemy ją przechodząc element po elemencie sekwencji lub zbioru. Najłatwiej odwołac się do dopiero co poznanych zakresów:
for i in range(10):
print(i)
0 1 2 3 4 5 6 7 8 9
W ten sam sposób możemy wypisać elementy dowolonej listy:
x = [2, 4, 128, -5]
for i in x:
print(i)
2 4 128 -5
Oczywiście moglibyśmy zrobić to bardziej "a la C", tzn np. wkorzystać funkcję len()
to wyznaczenia długości listy, a następnie odwołać się do indeksów:
for i in range(len(x)):
print(i, x[i])
0 2 1 4 2 128 3 -5
ale jest to uznawane za bardzo brzydki i "niepythonowy" sposób. Jeśli istotnie potrzebujemy się dostać zarówno do wartości jak i do indeksu, możemy wykorzystać funkcję enumerate()
, która stworzy krotkę (czyli niemodyfikowalną listę), zawierającą obie te rzeczy. Możemy ją rozpakować do dwóch zmiennych, otrzymując indeks i wartość:
for indeks, wartosc in enumerate(x):
print(indeks, wartosc)
0 2 1 4 2 128 3 -5
7.2 Pętla while
¶
Pętle while
dużo bardziej przypominają konstrukcję w C - podajemy warunek, który musi być prawdą, aby pętla się wykonywała. Może to być np. typowa inkrementacja (przy okazji, w Pythonie mamy operatory +=
, -=
etc natomiast NIE MA operatorów ++
czy --
):
x = 0
while x < 5:
x += 1
x
5
W Pythonie nie ma pętli do...while
. Oczywiście, można ją bez problemu "emulować", ustawiając taki warunek, żeby wykonała się co najmniej raz (co jest ideą pętli do...while
).
7.3 Instrukcja warunkowa ¶
Instrukcja warunkowa zaczyna się od słowa kluczowego if
, po którem następuje warunek i dwukropek, a następnie (we wcięciu) to, co ma zostać wykonane, w przypadku, gdy warunek okaże się prawdziwy. Jeśli chcemy umieścić kolejny warunek, korzystamy z elif
, i wreszcie, aby zaznaczyć pozostałe przypadki, używamy else
. Poniższy przykłąd wykorzystuje funkcję input
, która wczytuje dane wejściowe - ponieważ na wyjściu otrzymujemy obiekt tekstowy, musimy go jeszcze zrzutowac na liczbę całkowitą (UWAGA! ten kod nie jest dobrze napisany, gdyż "wywali" się, jeśli podamy coś innego niż liczbę całkowitą. Oczywiście, można to zrobić "dobrze", ale wymaga to zastosowania wyjątków etc):
x = int(input())
if x < 0:
print("To liczba ujemna!")
elif x == 0:
print("Wpisałaś/eś zero!")
else:
print("Logicznie rozumując, x jest dodatnie!")
Wpisałaś/eś zero!
W Pythonie mamy także operator trójargumentowy (warunkowy, ternary operator), który ma postać wartość_gdy_prawda
if
warunek
else
wartość_gdy_fałsz
, przy czym bez problemu możemy przypisac efekt działąnia tego opertaora do zmiennej:
x = 5
x = 1 if x >= 0 else -1
y = -5
y = 1 if y >= 0 else -1
print(x, y)
1 -1
8. Funkcje ¶
Ostatni przykład to dobra ilustracja po co są nam potrzebne funkcje - w powyższym przypadku dwa razy tworzyliśmy wyrażenie, które odpowiada funkcji Heavside'a $\theta(x)$. Zamiast tego moglibyśmy stworzyć funkcję i wywołać ją dwa razy, dla różnych argumentów.
Funkcję zaczynamy od słowa kluczowego def
, następnie podajemy w nawiasie argument (argumenty) funkcji oraz dwukropek. Jeśli funkcja ma zwracać musimy uzyć słowa kluczowego (w zasadzie funkcji) return
:
def heaviside(x):
return 1 if x >= 0 else -1
print(heaviside(5), heaviside(-5))
1 -1
Nie ma zasadniczo ograniczeń na argumenty oraz wartości funkcji - może ona przymować liczby, listy etc i zwracać dowolne rzeczy:
def dziwna_funkcja(x):
if type(x) == int:
return x ** 2
elif type(x) == str:
return "Mamy tu napis!"
else:
return x
print(dziwna_funkcja(10))
print(dziwna_funkcja('Python'))
print(dziwna_funkcja([0, 1, 2, 3]))
100 Mamy tu napis! [0, 1, 2, 3]
8.1 Funkcje anonimowe (lambda) ¶
Jak sama nazwa wskazuje, funkcje anonimowe... nie posiadają nazwy. Są to najczęściej dość krótkie wyrażenia, bez których można się w wielu przypadkach obyć, ale które bardzo upraszczają kod. Składnia wyrażeń lambda jest następująca lambda
argument(y)
:
ciało_funkcji
. Możemy takie wyrażenie wywołać w następujący sposób:
(lambda x, y: x + y)(2, 4)
6
możemy także przypisac wyrażenie lambda do zmiennej, co spowoduje, że będziemy mieli praktycznie do czynienia ze zwykłą funkcją. Jest często wykorzystywane do stworzenia krótkich, prostych funkcji:
f = lambda x, y: x + y
f(2, 4)
6
Uwaga! Funkcje w połączeniu z wyrażeniam i lambda umożliwiają tworzenie "funkcji funkcji"
9. Funkcje map() i filter() oraz listy składane (list comprehensions) ¶
Wykorzystanie operatora :
oraz funkcji range()
wskazuje na podobieństwo Pythona do takich języków skryptowych jak np. Matlab. Z drugiej strony, z dotychczasowych rozważań wynika, że aby stworzyć nową listę y
, w której każdy element będzie podwojoną wartością odpowiedniego elementu z listy x
, musimy się uciec do pętli:
x = range(10)
y = []
for i in x:
y.append(2*i)
y
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
9.1 Funkcja map() ¶
Jest to dość "brzydki" zapis. Zamiast tego można wykorzystać funkcję map(f, lista)
, która, jak wskazuje jej nazwa, dokonuje mapowania pomiędzy wejściową listą a pewną funkcją f()
, którą podamy. Funkcja map()
zwraca obiekt typu map
, stąd zwykle dobrze jest go zrzutować na listę za pomocą funkcji list()
. Możemy jawnie zdefiniować wspomnianą funkcję:
def f(x):
return 2*x
l = range(10)
y = list(map(f, l))
y
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
W przypadku, gdy funkcja jest prosta, możemy skorzystać z omawianych wcześniej funkcji anonimowych:
l = range(10)
y = list(map(lambda x: 2*x, l))
y
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
9.2 Funkcja filter() ¶
Podobnie przedstawia się sprawa testowania warunków dla listy. Wyobraźmy sobie, że chcemy pobrać z listy l
jedynie te elementy, które są większe niż 5. Zamiast tworzenia karkołomych konstrukcji z użyciem pętli lub funkcji map()
(co jest możliwe), korzystamy z funkcji filter(funkcja_warunkowa, lista)
:
l = range(10)
y = list(filter(lambda x: x > 5, l))
y
[6, 7, 8, 9]
def f(x, n):
return x > n
n = 3
list(filter(lambda x: f(x, n), range(10)))
[4, 5, 6, 7, 8, 9]
9.3 List comprehension ¶
Istnieje jeszcze prostszy sposób, a na pewno bardziej elegancki i chyba też częściej wykorzystywany, aby stworzyć listę, w której znajdą się elementy poddane funkcji. Są to tzw. listy składane - to wyrażenie nie jest zbyt popularne w jęzku polskim i częściej korzysta się z oryginału, czyli list comprehension. Jest konstrukcja, która składa się z nawiasów kwadratowych oraz jednolinijkowej pętli for [
wyrażenie
for
iterator
in
lista
]
Poniższy kod stworzy po prostu taką samą listę jak wejściowa:
l = range(10)
y = [x for x in l]
y
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
natomiast odpowiednia modyfikacja wypisze nam kwadraty:
y = [x**2 for x in l]
y
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
Oczywiście, możemy też zdefiniowac wcześniej funkcję lub wyrażenie lambda:
def f(x):
return math.sin(x) - math.cos(x)
g = lambda x: x**2 - 2*x + 3
y1 = [f(x) for x in l]
y2 = [g(x) for x in l]
print(y1)
print(y2)
[-1.0, 0.30116867893975674, 1.325444263372824, 1.1311125046603125, -0.10315887444431626, -1.2425864601263648, -1.2395857848492917, -0.09691565562451554, 1.1348582804319953, 1.3232487471264336] [3, 2, 3, 6, 11, 18, 27, 38, 51, 66]
10. Wykresy ¶
W Pythonie nie ma natywnej funkcji tworzącej wykresy - musimy skorzystać z jednej z bibliotek (modułów). Jedną z bardziej popularnych jest matplotlib, ale nie oznacza to, że jest to "jedyna słuszna opcja". Jeśli ktoś pracuje bardziej z danymi typu ramki danych (czyli korzysta z modułu pandas) to dużo lepszą opcją jest seaborn. My jednak skorzystamy z tej pierwszej opcji. W tym celu uruchomimy podmoduł matplotlib.pyplot i nazwiemy go (standardowo) plt, żeby nie męczyć się długą nazwą.
Idea najprostyszych wykresów 2D jest dość trywialna - korzystamy z funkcji plot, podając jako oddzielne argumenty listy zawierające współrzędne x oraz y:
import matplotlib.pyplot as plt
x = range(10)
y = [i**2 for i in x]
plt.plot(x, y)
[<matplotlib.lines.Line2D at 0x7f390da8bc40>]
Zamiast funkcji plot()
możemy korzystać ze scatter()
, która prezentuje dane za pomocą punktów, aczkolwiek to samo możemy uzyskać wstawiając opcję 'o'
w funkcji plot()
po serii danych. Aby ustawić kolor w funkcji plot()
, korzystamy c opcji c="..."
, podczas gdy rozmiarem punktów sterujemy za pomocą ms=...
. Typ linii możemy modyfikowac dzięki ls="..."
. Kolejne serie danych można dodać wywołując kolejne funkcje plot()
w ramach jednej komórki.
Warto też wspomnieć, że dodanie opcji label="..."
umożliwi później skorzystanie z funkcji legend()
, wyświetlającej legendę do rysunku, natomiast axhline()
i axvline()
, odpowiednio, dostarczą linie poziome i pionowe, a xlabel()
i ylabel()
podpisy osi.
x = range(-5, 6)
plt.plot(x, [x**2 for x in x], "o", c = "red", ms = 10, label = "punkty")
plt.plot(x, x, label = "linia")
plt.axhline(0, c = "gray", ls = "--")
plt.axvline(0, c = "gray", ls = "--")
plt.xlabel("x")
plt.ylabel(r"$f(x)=x^2$, $g(x)=x$")
plt.legend()
<matplotlib.legend.Legend at 0x7f390b9b2110>
11. Moduł numpy ¶
Ponieważ nasze zajęcia w dużej mierze będą dotyczyły przekształceń wektorowych i macierzowych, zalecane jest wykorzystanie funkcji bezpośrednio dedykowanych do takich operacji. Mowa tu o module numpy, którego podstawową cechą jest to, że pozwala na wykonywnie operacji element po elemencie, bez odwoływania się do pętli. Innymi słowy, np. dodanie liczby 1 do każdego elementu wektora v
może zostać zapisane w kodzie jako v + 1
.
Niezbędne jest przy tym przekształcenie natywnego obiektu Pythona (czyli listy) do obiektu moduły numpy (czyli macierzy - array) za pomocą funkcji np.array()
.
import numpy as np
v = range(-5, 5)
u = np.array(v)
u
array([-5, -4, -3, -2, -1, 0, 1, 2, 3, 4])
11.1 Wektory ¶
W tym momencie możemy już wykonać wspomniane operacje, czyli np. dodać skalar 1 do każdego elementu $u$:
u + 1
array([-4, -3, -2, -1, 0, 1, 2, 3, 4, 5])
lub np. pomnożyć każdy element wektora $u$ przez 1.28:
u * 1.28
array([-6.4 , -5.12, -3.84, -2.56, -1.28, 0. , 1.28, 2.56, 3.84, 5.12])
u[0:5]
array([-5, -4, -3, -2, -1])
Jeśli mamy dwa zgodne obikety wektorowe (tj. o tej samej długości) możemy je dodać do siebie:
s = np.array(range(5, -5, -1))
s
array([ 5, 4, 3, 2, 1, 0, -1, -2, -3, -4])
u + s
array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
11.2 Macierze ¶
Rozróżnienie pomiedzy wektorami i macierzami jest w tym wypadku sztuczne: de facto każdy obiekt stworzony przez funkcję np.array()
jest po prostu macierzą.
A = np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
[10, 11, 12]])
A
array([[ 1, 2, 3], [ 4, 5, 6], [ 7, 8, 9], [10, 11, 12]])
Podobnie jak w przypadku wektorów, możemy bez problemu dodawać wartości do wszystkich elementów macierzy, mnożyć
A + 1
array([[ 2, 3, 4], [ 5, 6, 7], [ 8, 9, 10], [11, 12, 13]])
2 * A
array([[ 2, 4, 6], [ 8, 10, 12], [14, 16, 18], [20, 22, 24]])
Kluczową kwestią jest to, że macierze numpy są inaczej indeksowane niż listy: zamiast zapisu A[i][j]
, mamy tu notację A[i,j]
. Oznacza to, że bez problemu możemy wypisać np pierwszą kolumnę macierzy A
:
A[:,0]
array([ 1, 4, 7, 10])
dwa pierwsze wiersze:
A[0:2,:]
array([[1, 2, 3], [4, 5, 6]])
albo podmacierz powstałą przez
A[[0, 2],[0, 2]]
array([1, 9])
Sytuacja robi się trochę bardziej skomplikowana, jeśli chcemy wypisać podmacierz, składającą się z dowolnych wierszy i kolumn (np. 0 i 2 wiersz, oraz 0 i 2 kolumna). Musimy wtedy albo posłużyć się poniższą strukturą:
A[[0, 2],:][:,[0,2]]
array([[1, 3], [7, 9]])
11.3 Ufunc i wykonywanie operacji element po elemencie, wektoryzacja ¶
W pakiecie numpy instnieje cały zestaw funkcji nazywanych ufunc (universal functions), których cechą jest to, że wykonają nasze operacje element po elemencie. W szczególności będą to funkcje matematyczne, znane nam z pakietu math, takie jak exp()
czy sin()
. Używanie wersji z pakietu numpy uwalnia nas od konieczności korzystania z list comprehensions:
x = np.arange(0, 2, 0.1)
np.exp(x)
array([1. , 1.10517092, 1.22140276, 1.34985881, 1.4918247 , 1.64872127, 1.8221188 , 2.01375271, 2.22554093, 2.45960311, 2.71828183, 3.00416602, 3.32011692, 3.66929667, 4.05519997, 4.48168907, 4.95303242, 5.47394739, 6.04964746, 6.68589444])
W powyższym przypadku użycie math.exp(x)
spowodowałoby wyświetlenie komunikatu błędu. Możemy stworzyć własne funkcje, opierając się na innych funkcja wykonywanych element po elemencie:
def sinh(x):
return 0.5*(np.exp(x) - np.exp(-x))
x = np.arange(-4, 4, 0.1)
plt.plot(x, sinh(x))
[<matplotlib.lines.Line2D at 0x7f390b716080>]
Uwaga! Niektóre funkcje wymagają Wektoryzacji
11.4 Funkcje modułu numpy ¶
Moduł numpy ma całą gamę przydatnych funkcji oraz właściwości przydatnych do obsługi macierzy:
- pobieranie rozmiaru macierzy
A.shape
A = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]])
nx, ny = A.shape
print(nx, ny)
4 3
- wykonanie transpozycji macierzy
A.T
A.T
array([[ 1, 4, 7, 10], [ 2, 5, 8, 11], [ 3, 6, 9, 12]])
- funkcja
arange()
, która podobnie to funkcjirange()
tworzy zakres, ale przymuje również wartości niecałkowite:
np.arange(-5, 5, 0.5)
array([-5. , -4.5, -4. , -3.5, -3. , -2.5, -2. , -1.5, -1. , -0.5, 0. , 0.5, 1. , 1.5, 2. , 2.5, 3. , 3.5, 4. , 4.5])
- funkcja
linspace()
dzieląca przedział na podaną liczbę podprzedziałów:
np.linspace(0, 5, 11)
array([0. , 0.5, 1. , 1.5, 2. , 2.5, 3. , 3.5, 4. , 4.5, 5. ])
- funkcja
reshape()
, która zmienia rozmiar macierzy - bardzo wygodna przy połączeniu zarange()
, możemy np, stworzyć ciąg liczb od 1 do 9 i wstawić je do macierzy 3 na 3:
np.arange(1, 10).reshape(3,3)
array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
- operator @ wykonujący mnożenie macierzowe:
A = np.arange(1, 7).reshape(2, 3)
B = np.arange(6, 0, -1).reshape(3, 2)
A @ B
array([[20, 14], [56, 41]])
Warto przy tym pamiętać, że zwykły operator mnożenia np. A * B
skutkuje mnożeniem element po elemencie, więc np.
A * A
array([[ 1, 4, 9], [16, 25, 36]])
- funkcja
outer()
realizująca iloczyn kartezjański, czyli mnożąca "każdy z każdym":
np.outer(np.arange(1, 11), np.arange(1, 11))
array([[ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], [ 2, 4, 6, 8, 10, 12, 14, 16, 18, 20], [ 3, 6, 9, 12, 15, 18, 21, 24, 27, 30], [ 4, 8, 12, 16, 20, 24, 28, 32, 36, 40], [ 5, 10, 15, 20, 25, 30, 35, 40, 45, 50], [ 6, 12, 18, 24, 30, 36, 42, 48, 54, 60], [ 7, 14, 21, 28, 35, 42, 49, 56, 63, 70], [ 8, 16, 24, 32, 40, 48, 56, 64, 72, 80], [ 9, 18, 27, 36, 45, 54, 63, 72, 81, 90], [ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100]])
- funkcja
pad()
, która ma naprawdę skomplikowaną składnie: pozwala dołożyć do istniejącej macierzy wartości, przy czym możemy wskazać ile wierszy i kolumn zarówno przed jak i po ma zostać wypelnione. Poniższy przykład dokłada przez macierzą A jeden wiersz i dwie kolumny zer, a po niej dwa wiersze i trzy kolumny ósemek.
A = np.arange(1.0, 10).reshape(3, 3)
np.pad(A, ((1,2), (2,3)), constant_values=(0, 8))
array([[0., 0., 0., 0., 0., 8., 8., 8.], [0., 0., 1., 2., 3., 8., 8., 8.], [0., 0., 4., 5., 6., 8., 8., 8.], [0., 0., 7., 8., 9., 8., 8., 8.], [0., 0., 8., 8., 8., 8., 8., 8.], [0., 0., 8., 8., 8., 8., 8., 8.]])
Najczęściej chcemy po prostu dołożyć zera, wtedy składnia się upraszcza (w tym przypadku chcemy dołozyć 2 kolumny i 2 rzędy zer, ale tylko za macierzą:
np.pad(A, (0,2))
array([[1., 2., 3., 0., 0.], [4., 5., 6., 0., 0.], [7., 8., 9., 0., 0.], [0., 0., 0., 0., 0.], [0., 0., 0., 0., 0.]])
np.pad(A, (0,2))
array([[1., 2., 3., 0., 0.], [4., 5., 6., 0., 0.], [7., 8., 9., 0., 0.], [0., 0., 0., 0., 0.], [0., 0., 0., 0., 0.]])
- funkcja
tile()
, która powtarza daną macierz (również pojednyczą liczbę) tak, aby stworzyć macierz o określonym rozmiarze:
np.tile(128, (2, 3))
array([[128, 128, 128], [128, 128, 128]])
- wreszcie funkcje
ones()
izeros()
, które są szczególnym przypadkiemtile()
orazeye()
, tworząca macierz z diagonalą (w tym wypadku jest to macierz kwadratowa, więc podajemy tylko jedną wartość):
np.ones((3, 3))
array([[1., 1., 1.], [1., 1., 1.], [1., 1., 1.]])
np.zeros((2, 3))
array([[0., 0., 0.], [0., 0., 0.]])
np.eye(4)
array([[1., 0., 0., 0.], [0., 1., 0., 0.], [0., 0., 1., 0.], [0., 0., 0., 1.]])
Z pewnością w trakcie zajęć często będzie wykorzystywana funkcja append()
, która dokleja do istniejącej macierzy $\mathbf{A}$ wiersze append(..., axis = 0)
lub kolumny append(..., axis = 1)
. Należy przy tym pamiętać, aby doklejny obiekt był faktycznie kolumną lub wierszem, a nie wektorem (jednowymiarowym). Dla przykładu, mamy do dyspozycji macierz $\mathbf{A}$
A = np.array([[1, 2], [3, 4]])
A
array([[1, 2], [3, 4]])
i chielibyśmy do niej dokleić wektor $\mathbf{b} = (-1, -1)$ jako wiersz lub kolumnę. Naiwne zapisanie $\mathbf{b}$ jako b=np.array([-1, -1])
i użycie funkcji append()
zwróci komunikat o błędzie. Zamiast tego musimy "ubrać" wektor w podwójne nawiasy kwadratowe i dopiero wykonac zamierzone operacje (oczywiście w przypadku doklejania kolumny, musimy najpierw dokonać w tym przypadku operacji transpozycji):
b = np.array([[-1, -1]])
np.append(A, b, axis = 0)
array([[ 1, 2], [ 3, 4], [-1, -1]])
np.append(A, b.T, axis = 1)
array([[ 1, 2, -1], [ 3, 4, -1]])
11.5 Podmoduł random ¶
Moduł numpy ma cały zestaw funkcji do losowania wartości z różnych rozkładów. Odwołujemy się do nich za pomocą np.random.nazwa_funkcji()
. I tak np. uniform(a, b, n)
umozliwia wylosowanie $n$ wartości z rozkładu jednorodnego z przedziału $[a,b)$, a normal(mu, sigma, n)
- $n$ wartości z rozkładu Gaussa o średniej $\mu$ i odchyleniu $\sigma$, przy czym $n$ może być liczbą, ale równiez wymiarami macierzy:
np.random.uniform(0, 1, 10)
array([0.76464566, 0.97390915, 0.4973259 , 0.32639541, 0.51436316, 0.38323547, 0.65851253, 0.39933521, 0.01736986, 0.96790883])
np.random.normal(0, 1, (4, 4))
array([[-0.50200595, 1.02398044, -0.77097715, -1.1703033 ], [-0.33630453, -0.61697951, -1.20115121, -1.53741149], [-0.49933072, 0.93399376, 0.91053656, 0.69549816], [ 0.20995195, -0.63409547, 0.46317876, 0.39503581]])
11.6 Indeksowanie logiczne ¶
Jednym z kluczowych udogodnień związanych z pakietem numpy jest indeksowanie wektorów i macierzy za pomocą wartości logicznych. Innymi słowy, oprócz odwoływania się do elementów wektorów jako $A[0]$ etc, możemy przekazać wektor wartość logicznych długości rozpatrywanego wektora.
Przykład: dysponujemy wektorem pierwszych dzisięciu liczb naturalnych i chcemy wypisać tylko te liczby, które są większe niż 5.
x = np.array(range(1, 11))
x
array([ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
ind = x > 5
ind
array([False, False, False, False, False, True, True, True, True, True])
x[ind]
array([ 6, 7, 8, 9, 10])
Oczywiście, nie musimy dokonywać pośredniego kroku stworzenia wektora ind - możemy po prostu wykonać:
x[x > 5]
array([ 6, 7, 8, 9, 10])
Przykład: wylosuj 10000 punktów z rozkladu jednorodnego w przestrzeni dwuwymiarowej i narysuj je, zaznaczając jednym kolorem takie punkty, które mieszczą się w kole o promieniu 1, a innym te, które tego warunku nie spełniają.
# Losowanie współrzędnej x oraz y
x = np.random.uniform(-2, 2, 10000)
y = np.random.uniform(-2, 2, 10000)
# Warunek na to, żeby punkty leżały w kole o promieniu 1
ind = x**2 + y**2 < 1
# Stworzenie rysunku o równych proporcjach
fig = plt.figure(figsize = (3,3))
# Rysowanie punktów "zewnętrznych"
plt.scatter(x[np.invert(ind)], y[np.invert(ind)], s = .5)
# Rysowanie punktów "wewnętrznych"
plt.scatter(x[ind], y[ind], s = .5)
<matplotlib.collections.PathCollection at 0x7f390b7799f0>
12. Dodatkowe uwagi ¶
12.1 Użycie operatorów dodawania oraz mnożenia w przypadku list i zmiennych tekstowych ¶
Tak jak zostało wspomniane, napisy w Pythonie są sekwencją, tak więc możemy się do nich odwoływać tak, jak do zwykłej listy, czyli oprzez idenksy i operator slice:
l = "To jest napis"
l[3:7]
'jest'
W obu przypadkach (listy i napisy) operatory dodawania i mnożenia mają dość specyficzne działanie: operator + skleja ze sobą struktury a operator * powiela je:
l = "ma"
k = "tka"
l + k
'matka'
x = [1, 2, 3]
4*x
[1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3]
12.2 Tworzenie kopii list zagnieżdżonych oraz kopii macierzy w numpy ¶
W przypadku obiektów zagnieżdżonych kopiowanie za pomocą copy()
nie jest wystarczające, gdyż odnosi się jedynie do "pierwszego poziomu" i zmiany na niższym poziomie zagnieżdżenia będą widoczone w obu obiektach. Stąd też funkcja copy()
nosi nazwę "płytkiej" kopii. Aby faktycznie uzyskać kopię niezależną musimy odwołać się do funkcji deepcopy()
- jak sama nazwa wskazuje, tworzy ona głęboką kopię. W tym celu musimy uruchomić moduł copy:
import copy
A = [[1, 2, 3], [4, 5, 6, [7, 8]]]
B = A.copy()
C = copy.deepcopy(A)
B[0][0] = 128
C[0][0] = -11
print(A, "\n", B, "\n", C, "\n")
[[128, 2, 3], [4, 5, 6, [7, 8]]] [[128, 2, 3], [4, 5, 6, [7, 8]]] [[-11, 2, 3], [4, 5, 6, [7, 8]]]
Ten "problem" nie występuje dla modułu numpy - nawet w przypadku wielowymiarowych macierzy wystarczy użyć funkcji copy()
:
A = np.random.uniform(0, 1, [3, 3, 3])
A
array([[[0.8651204 , 0.71752124, 0.41805215], [0.07589229, 0.5840822 , 0.74278958], [0.69851995, 0.72389972, 0.73241555]], [[0.97709257, 0.67899621, 0.51116979], [0.68695573, 0.1122271 , 0.09161973], [0.27548366, 0.5830295 , 0.57456545]], [[0.17569447, 0.47641883, 0.29310266], [0.61282061, 0.83264516, 0.55279941], [0.76962148, 0.56127908, 0.2766145 ]]])
B = A.copy()
B[2,2,2] = -100
A
array([[[0.8651204 , 0.71752124, 0.41805215], [0.07589229, 0.5840822 , 0.74278958], [0.69851995, 0.72389972, 0.73241555]], [[0.97709257, 0.67899621, 0.51116979], [0.68695573, 0.1122271 , 0.09161973], [0.27548366, 0.5830295 , 0.57456545]], [[0.17569447, 0.47641883, 0.29310266], [0.61282061, 0.83264516, 0.55279941], [0.76962148, 0.56127908, 0.2766145 ]]])
12.3 Funkcje anonimowe w tworzeniu funkcji funkcji ¶
Funkcje anonimowe są dość wygodne, gdy chcemy stworzyć funkcje wyższych rzędów, tzn. funkcje funkcji. Skonstruujmy np. następującą funkcję:
def funkcja_potegowa(alfa):
return lambda x: x ** alfa
Jeśli teraz wywołamy funkcję funkcja_potęgowa
z pewnym argumentem i przypiszemy wynik do zmiennej, to stworzmy funkcję, która będzie zawsze podnosiła argument do danej potęgi:
kwadrat = funkcja_potegowa(2.0)
print(kwadrat(4), kwadrat(10))
16.0 100.0
szescian = funkcja_potegowa(3.0)
print(szescian(4), szescian(10))
64.0 1000.0
12.4 Wektoryzacja funkcji w numpy ¶
Należy pamiętać, że nie każda funkcja, którą stworzymy, jest wykonywana element po elemencie: jeśli w ramach naszej funkcji będziemy tworzyć wektory czy tablice, to schemat nie zadziała. Rozważmy np. funkcję, która otrzymuje jako argument liczbę naturalną $n$ i liczy sumę ciągu arytmetycznego:
def fsum(n):
return np.sum(np.arange(1, n + 1))
fsum(10)
55
Wywołanie tej funkcji na obiekcie typu array da komunikat błędu. Aby policzyć funkcję fsum()
dla każdego elementu wektora musimy ją zwektoryzować za pomocą funkcji vectorize()
z pakietu numpy:
l = np.arange(1, 10)
v_fsum = np.vectorize(fsum)
v_fsum(l)
array([ 1, 3, 6, 10, 15, 21, 28, 36, 45])