Podstawy Pythona

Python obecnie jest jednym z najbardziej popularnych języków programowania. Za jego suksesem stoi między innymi przejrzysty i prosty kod źródłowy, otwarte źródła i łatwe możliwości rozszerzania bibliotek. Z pewnością Python jest jednym z najbardziej przyjaznych języków do nauki programowania.

Praca z Pythonem

Istnieje kilka sposobów wygodnej pracy z Pythonem, w zależności od upodobań, rodzaju problemu i dostępnych narzędzi

  1. Bezpośrednia praca z interpreterem (REPL - read-eval-print loop), w tym przypadku wygodniejszy od standardowego interpretera jest ipython,

  2. Pisanie skryptów w ulubionym edytorze (gnome-text-editor, vim, emacs, nano) w pliku "program.py" oraz uruchamianie w terminalu poleceniem python program.py

  3. Używanie notatnika jupyter (lub jego wersji Google-Colab) - odpowiedników notatników w Mathematice

  4. Używanie środowiska programistycznego (np. VS Code) z odpowiednimi wtyczkami do obsługi Pythona

Interpreter

Python jest językiem interpretowanym. Oznacza to, że programów nie trzeba kompilować, a specjalny program - interpreter - tłumaczy na bieżąco polecenia na język maszynowy. Bazowy interpreter można uruchomić poleceniem

$ python

Ale nie jest to jedyna opcja. Bardziej zaawansowany interpreter, który ma podobne możliwości, jak Mathematica lub Matlab, to:

$ ipython

Jeszcze inna wersja to Jupyter Notebook, który działa w przeglądarkach i umożliwia tworzenie notatek - dokumentów, zawierających kod i tekst. Odmianą tego projektu jest Google Colab. Instrukcje w tych intepreterach można wydawać po kolei i śledzić kolejne wyniki (tzw. REPL - Read Evaluate Print Loop, ten sam schemat używa Mathematica, Matlab, itd.). Na przykład po uruchomieniu interpretera ipython zobaczymy coś następującego

In   [1]: a = 1

In   [2]: b = 5

In   [3]: a**2 + b**2
Out[3]: 26

W ten sposób można szybko wykonywać obliczenia, sprawdzać działanie fragmentów kodu itd.

Ale program w pythonie może mieć również postać pliku z rozszerzeniem *.py, który jest wywoływany przez interpreter:

$ python moj_program.py

Uwaga! Na starszych komputerach można spotkać ciągle Pythona2, który ma inną składnię niż Python3. W tych wypadkach często równolegle jest zainstalowany Python3 i można go uruchomić poleceniem python3.

Zmienne

Python jest językiem dynamicznym i sam rozpoznaje typ zmiennej:

a = 1
x = 12.345

Ta sama zmienna może za chwilę zmienić się w coś zupełnie innego:

a = "Łańcuch znaków"

i nie będzie to błędem.

Pomimo tego trzeba pamiętać, że zmienne jednak mają swój typ i może to być typ prosty (int, float, bool, string) lub złożony (tuple, dictionary...) i wpływa to na sposób działania kodu.

>>> a = 5
>>> b = "Ala"
>>> a * b
>>> a = True
>>> a * b

Typy proste, o których na razie musimy wiedzieć, to liczby całkowite (int), logiczne (bool) i liczby zmiennoprzecinkowe (float). Z typów złożonych przyda nam się lista:

a = [3, 5, 8]

która jest po prostu uporządkowaną sekwencją jakichkolwiek obiektów. Mogą być to liczby, napisy, inne listy:

a = [12.3, [0, 1, 2], "Ala ma kota", []]

Ważne jest to, że możemy się odwoływać do kolejnych elementów za pomocą indeksów (zaczynających się od zera dla pierwszego elementu).

>>> a[0]
12.3
>>> a[1]
[0, 1, 2]

Ciekawostką są możliwości indeksowania. Ujemne liczby oznaczają liczenie elementów od końca, więc:

a[-1]

to ostatni wyraz. Wybierać można również pewien zakres elementów - poprzez składnię:

a[0:2]

przy czym początek należy do podzbioru, a koniec nie, czyli w podanym przykładzie podzbiór będzie miał elementy a[0] i a[1].

Bieżącą długość listy można sprawdzić poleceniem len :

n = len(a)

Wypisywanie na ekranie

Służy do tego instrukcja print (), która - jak łatwo się domyśleć - wypisuje coś na ekranie. Jej składnia jest bardzo prosta: można podawać jej tekst lub liczby, a kolejne elementy oddziela się przecinkami. Print sam wstawia enter na końcu i spacje pomiędzy elementami!

print ("x =", x)

Ciekawe są możliwości formatowania liczb i innych zmiennych za pomocą specjalnej składni. Powiedzmy, że chcemy wypisać wynik z dokładnością do 3 miejsc po przecinku, ale tak, aby cały napis mieścił się na 8 znakach i był wyjustowany w prawą stronę.

print('{: >8.3f}'.format(10 / 3))

Nawiasy klamrowe mówią pythonowi, że tu ma wstawić coś, co będziemy formatować. Po dwukropku podajemy spację, którą wstawi w puste miejsca (można użyć innych znaków), znak > mówiący, że napis jest wyjustowany na prawo (inne możliwości to < i ^) oraz 8.3f, który mówi, że pól jest 8, miejsc po przecinku 3, a liczba jest typu float. Uwaga: wynik dzielenia 10 / 3 zostanie automatycznie zrzutowany na typ float!

Instrukcje kontrolne

Instrukcje kontrolne - to wszelkie elementy języka, które pozwalają na zmiany zachowania się kodu w zależności od wyników obliczeń, wprowadzonych danych, powtarzanie operacji itd.

Polecenie if

if x > 0:
    print("x jest dodatni")
elif x < 0:
    print("x jest ujemny")
else:
    print("x jest zerem")

Przy okazji zwróćmy uwagę na kilka istotnych cech składni pythona:

  • brak nawiasów

  • brak średników na końcach linii

  • bloki są definiowane przez wcięcia

  • na końcu linii z warunkiem jest dwukropek.

Wcięcia wymuszają poprawne formatowanie kodu. Ważne jest, aby wcięć dokonywać za pomocą ustalonej liczby spacji, np. 4. W edytorach można ustawić automatyczne wcinanie kodu, więc nie trzeba ich liczyć ręcznie. Podobnie można ustawić, aby edytor wpisywał odpowiednią liczbę spacji po naciśnięciu klawisza tabulatora. Możliwe jest też stosowanie znaku tabulacji jako wcięcia, ale ważne jest, że nie można ze sobą mieszać spacji i tabulatorów!

Instrukcja while

x = 1
y = 8
while x < 100:
    print (x)
    if x % y == 0:
        break
    x += 1
else:
    print (x, 'nie dzieli się przez', y)

W pętli while operacje wewnątrz są powtarzane, dopóki spełniony jest warunek. Warto zwrócić uwagę na to, że cały blok 'while' musi być wcięty, a instrukcja wewnątrz warunku 'if' - podwójnie wcięta (jest to odpowiednik nawiasów w nawiasach).

Instrukcje += i break mają odpowiedno znaczenie sumowania (x += 1 jest tożsame z x = x + 1) oraz wyskoczenia z pętli. Ciekawostką jest instrukcja else przy pętli while. Będzie ona wykonana tylko wtedy, gdy while zakończy działanie na skutek spełnienia warunku. Jeżeli z pętli while wyskoczymy poleceniem break, to kod w bloku else nie będzie wykonany.

Instrukcja for

Instrukcja for w pythonie wygląda następująco

for i in [0, 2, 4, 6]:
    print (i)

W pętli for także możemy stosować polecenie else, na takich samych zasadach jak w pętli while.

Dla tej pętli podajemy zbiór, po którym ma się poruszać iterator (tutaj i) i będzie to robić po kolei, chyba, że jej przerwiemy poleceniem break. Ponieważ często chcemy poruszać się po pewnym zakresie, na przykład od 1 do 10, w pythonie mamy polecenie range(), które taką listę zrobi od ręki. Ważne, aby pamiętać, że pierwsza podana wartość będzie użyta, a ostatnia jest górną granicą ostrą, czyli jej samej nie będzie. Poniższy kod pokaże nam tabliczkę mnożenia do 10.

for i in range (1, 11):
    for j in range (1, 11):
        print ( '{: >4d}'.format(i * j) , end='')
    print ()

Formatowanie napisów

W powyższym przykładzie użyte zostało bardziej zaawansowane formatowanie wypisywanych na ekranie wyników. Użyta została tu metoda, należąca do obiektu typu ciąg znaków, format. Jest to bardzo elastyczne polecenie, pozwalające na zdefiniowanie wielu aspektów napisu. Najważniejsze dla nas informacje to:

  • W ciągu znaków używamy nawiasów {}, które oznaczają kolejno podawane później w poleceniu format wartości

  • W nawiasach {:} pod dwukropku możemy podać formatowanie danej wartości

    • {:d} liczba całkowita

    • {:f} liczba zmiennoprzecinkowa

    • {:e} liczba zmiennoprzecinkowa w formacie "naukowym"

    • {:s} ciąg znaków

  • Dodatkowo możemy podać szerokość napisu, dokładność (liczbę miejsc po przecinku oraz wyjustowanie

    • {: ^8.3f} wyjustowanie do środka spacjami, szerokość 8 znaków, 3 miejsca po przecinku

    • {:0>4d} wyjustowanie w prawo zerami, szerokość 4 znaki

Można zwrócić też uwagę na słówko end='' w instrukcji print, które powoduje, że nie będzie wstawiany znak nowej linii (domyślne działanie), którą chcemy wstawić dopiero na koniec dziesiątki - i stąd puste print (), dające tylko enter.

Funkcje

Jeżeli stworzymy w programie jakąś procedurę, którą będziemy często powtarzać, zamiast kopiować i wklejać fragment kodu, warto jest stworzyć z niej funkcję (zwaną także procedurą lub subrutyną). Funkcję definiujemy w następujący sposób

def pitagoras(a, b):
    c = a**2 + b**2
    return c**(1/2)

Zgodnie ze wcześniejszą składnią stosujemy dwukropek i wcięcie. Jeżeli nie użyjemy słówka return, funkcja automatycznie zwróci wartość None (nic).

Zmienne wewnątrz funkcji są widoczne tylko w niej, nawet jeżeli nazywają się tak samo jak zmienne globalne. Na przykład

def f(x):
    x = x + 1
    print('f:', x)
    return x**3

x = 1
y = f(x)
print('main:', x)

rezultatem działania będzie

f: 2
main: 1

Pomimo iż dodajemy jedynkę do x wewnątrz funkcji, jest to inny x (lokalny) niż ten, który deklarujemy w głównej części programu! Dlatego po wyjściu z funkcji x globalny jest dalej równy 1.

Uwaga! Inaczej będą się zachowywały listy. Z powodu sposobu w jaki Python przekazuje zmienne do funkcji, lista zmodyfikowana wewnątrz funkcji, po wyjściu z niej nadal będzie zmodyfikowana. Jeżeli chcemy, aby lista nie ulegała zmianom, nie wystarczy przypisać wartość listy nowej zmiennej (bo wskazuje ona ciągle na ten sam obiekt!). Musimy zrobić jej kopię (np. wewnątrz funkcji B = A.copy()).

def f(A):
    A[0] += 1
    print('f:', A)

# To jest niewłaściwe podejście
def g(A):
    B = A
    B[0] += 1
    print('f:', B)

def h(A):
    B = A.copy()
    B[0] += 1
    print('g:', B)

A = [0, 0, 0]
f(A)
print('main:', A)

rezultatem działania będzie

f: [1, 0, 0]
main: [1, 0, 0]
g: [2, 0, 0]
main: [2, 0, 0]
h: [3, 0, 0]
main: [2, 0, 0]

Zarówno f, jak i g modyfikują oryginalną listę A. Dopiero podejście w funkcji h pokazuje jak uniknąć tego efektu, jeżeli taki mamy zamiar.

Operacje I/O

Pliki

Funkcja open zwraca obiekt powiązany z plikiem, w zależności od trybu otwarcia może być on do odczytu (r), pisania (w, istniejący plik zostaje skasowany), dopisywania (a), odczytu i zapisu (r+). Dodanie litery b do trybu oznacza tryb binarny, w innym przypadku jest to tryb tekstowy.

data_file = open('data.txt', 'r')

Funkcja read służy do czytania danych, read() wczyta cały plik, read(size) wczyta size bajtów.

data_file.read(10)

Funkcja readline wczytuje jedną linię tekstu (łącznie ze znakiem końca linii).

data_file.readline()
    for line in data_file:
        print(line, end='')

write służy do pisania w pliku, natomiast seek i tell odpowiednio przesuwają i zwracają położenie bieżącej pozycji w pliku.

data_file.write('{} {} {} \n'.format(0, 0, 1)
    data_file.seek(10)
    data_file.tell()

Błędy

Podczas pisania programów prędzej czy później popełnimy jakiś błąd i działanie skończy się komunikatem o problemie. Ważne jest, aby umieć odpowiednio odczytywać, naprawiać lub radzić sobie ze zgłoszonym problemem w inny sposób.

Oto prosty program z drobnym błędem

l = [0, 1, 2]
    if l > 0:
        l.pop()

i próba uruchomienia

Traceback (most recent call last):
      File "test.py", line 2, in <module>
        if l > 0:
    TypeError: '>' not supported between instances of 'list' and 'int'

Jest to typowy komunikat błędu. Po pierwsze mówi nam o pliku (test.py) w którym nastąpił problem. Dalej mamy numer linii kodu, w której wystąpił błąd (numer 2), jej treść (if l > 0:) oraz typ błędu, który w tym przypadku oznacza: nie można porównać listy i liczby za pomocą operatora ">".

Wszelkie błędy są zwracane w postaci wyjątków (Exception), należących do odpowiednich klas o czym niżej. Powyżej mamy błąd typu (TypeError). Program wcale nie musi przerywać działania w takiej sytuacji, możliwa jest odpowiednia obsługa wyjątku. Na przykład poniższy kod podczas czytania pliku zigoruje przypadki, kiedy napisu w danej linii nie daje się zinterpretować jako liczby całkowitej.

data_file = open('data.txt')
    counter = 0
    for line in data_file:
        try:
            counter += int(line)
        except TypeError:
            continue
    print(counter)

Programowanie obiektowe

  • Podejście obiektowe jest próbą opisu skomplikowanych systemów za pomocą abstrakcji.

  • Tworzymy obiekty, z którymi się komunikujemy (i które mogą się komunikować między sobą), które realizują zadania dzięki wewnętrznym metodom.

  • Problem dzielimy na pewne zadania, które przypisujemy obiektom i w pewnym sensie nie interesuje nas jak dany obiekt rozwiąże zadanie, o ile robi to zgodnie z oczekiwaniami. Niepotrzebne informacje są ukryte wewnątrz obiektu.

  • Każdy obiekt należy do pewnej klasy.

  • Każdy obiekt ma swoje zmienne i procedury.

  • Obiekty można ponownie używać w innych programach.

  • Wewnętrzną strukturę obiektów można modyfikować, uzupełniać i poprawiać, o ile nie wpływa to na widziane z zewnątrz własności.

  • Obiekty mogą dziedziczyć (łączyć się hierachicznie) po bardziej ogólnych klasach (np. samochód, czy rower jest bardziej szczegółową realizacją klasy pojazd).

  • Obiekty mogą zawierać inne obiekty (np. samochód zawiera koła, czujnik prędkości itd.).

  • W Pythonie wszyscy członkowie klasy są publiczni, a wszystkie funkcje są wirtualne (według terminologii C++).

Klasy

Poniższy przykład tworzy dwie klasy obiektów matematycznych: Point oraz Circle. Do zdefiniowania punktu na płaszczyźnie potrzebujemy jego współrzędnych. Tworzymy też metodę tej klasy, distance, która mierzy odległość między dwoma punktami. Druga klasa - Circle - wymaga podania położenia środka okręgu oraz jego promienia. Środek okręgu jest punktem, więc możemy użyć obiektu poprzednio stworzonej klasy Point (taka konstrukcja nazywa się kompozycją).

import math

    class Point:

        def __init__(self, x, y):
            self.x = x
            self.y = y

        def distance(self, point):
            return math.sqrt((self.x - point.x)**2 + (self.y - point.y)**2)

    class Circle:

        def __init__(self, x, y, r):
            self.center = Point(x, y)
            self.r = r

        def distance_to_perimeter(self, point):
            return math.sqrt((self.center.x - point.x)**2 + 
                             (self.center.y - point.y)**2) - self.r

    p1 = Point(0, 0)
    p2 = Point(1, 2)
    c1 = Circle(1, 1, 1)
    print(p1.distance(p2))
    print(c1.distance_to_perimeter(p2))
    print(p1.distance(c1.center))

Gdybyśmy chcieli stworzyć więcej kształtów geometrycznych i nasz kod miałby elastycznie z nimi pracować możnaby rozważyć utworzenie klasy Shape, po której dziedziczą kolejne elementy. Powiedzmy, że chcemy, żeby każdy kształt miał jakiś kolor. Możemy wtedy zrobić tak.

import math

class Shape:
    def __init__(self, color="black"):
        self.color = color

class Point:

    def __init__(self, x, y):
        self.x = x
        self.y = y


class Circle(Shape):

    def __init__(self, x, y, r, color="black"):
        super().__init__(color)
        self.center = Point(x, y)
        self.r = r


class Triangle(Shape):

    def __init__(self, points, color="black"):
        super().__init__(color)
        self.points = points


c1 = Circle(1, 1, 1)
c2 = Circle(0, 0, 1, "red")
t1 = Triangle([Point(0, 0), Point(1, 1), Point(0, 1)], "green")

print(c1.color)
print(c2.color)
print(t1.color)

Jeżeli wpadnie nam do głowy, że oprócz koloru przydałby się np. krój linii (kreski, kropki, itp.) teraz wystarczy dołożyć taki element do macierzystej klasy Shape i wszystkie dziedziczące po niej klasy będą taką cechę posiadały.

Wyjątki - jeszcze raz

Znając mechanizm dziedziczenia może na chwilę wrócić do sprawy wyjątków, zwracanych w przypadku gdy coś w kodzie poszło źle. Nowe wyjątki, obsługujące nasze własne przypadki, można tworzyć korzystając z właśnie z mechanizmu dziedziczenia. Wszystkie wyjątki w Pythonie dziedziczą bowiem po bazowej klasie Exception i zaczynając od niej można dodawać kolejne klasy.

class NuclearError(Exception):

    def __init__(self, msg = ''):
        self.msg = msg

    def __str__(self):
        return self.msg

try: 
    if temp > temp_max:
        raise NuclearError('Max temp reached')
except NuclearError as err:
    print(err)
CC BY-SA 4.0 Krzysztof Miernik. Last modified: December 07, 2023. Website built with Franklin.jl and the Julia programming language.