Python

krótkie notatki służące pomocy w zaczęciu z programowaniem w języku Python.

O Pythonie

Python to język programowania ogólnego zastosowania cechujący się bogactwem bibliotek (czyli mamy sporo "gotowców") i czytelną, zwięzłą składnią (dzięki czemu jest relatywnie łatwy w nauce).

Należy do języków z mechanizmem automatycznego zarządzania pamięcią (co również ułatwia naukę).

Jest językiem interpretowanym (nie wymaga kompilacji przed uruchomieniem) co ułatwia przenoszenie aplikacji w nim napisanych na różne platformy (Windows, Linux, MacOS...) kosztem wydajności.

Cechy te sprawiają, że jest to jeden z najpopularniejszych języków i często jest polecany osobom zaczynającym naukę programowania. Przyczynił się też do popularyzacji zastosowań AI i jest szeroko wykorzystywane w nauce, statystyce.

Subiektywne zestawienie zastosowań Pythona

Jak każdy inny język programowania Python ma swoje mocne i słabsze strony. Poniżej znajdziesz tabelkę z moją subiektywną oceną w jakich dziedzinach sprawdza się dobrze, średnio, słabo.

Zastosowanie Dobre Średnie Niewskazane
uczenie maszynowe / AI x
inżynieria i analiza danych x
backend aplikacji internetowych x
automatyzacja (skrypty, DevOps etc.) x
web scraping x
aplikacje "stacjonarne" x
aplikacje mobilne x x
programowanie mikrokontrolerów x
gry x
frontend x
aplikacje systemowe x

Zmienne i typy podstawowe

Deklaracja zmiennych

W Pythonie bezpiecznie jest myśleć o zmiennych jak o etykietach wskazujących nam na jakąś zawartość (formalnie przyjmujemy, że oba te elementy są częścią zmiennej - jednak wg. mnie zaproponowane podejście ułatwia na początku uniknięcie wielu błędów). Ta sama zawartość może mieć wiele etykiet (czyli kilka zmiennych może wskazywać na te same dane - warto jednak takich sytuacji unikać).

Zmienne deklarujemy wg. poniższego schematu:

nazwa_zmiennej = zawartość

Znak równości służy więc do przypisania nazwy (którą umieszczamy po lewej stronie znaku) do jakiegoś obiektu (który umieszczamy po prawej stronie znaku) np.

moj_zwierzak="pies"

W powyższym przykladzie moj_zwierzak to nazwa zmiennej (którą będziemy wykorzystywać w programie) a "pies" to zawartość.

Tworząc nazwę MUSIMY przestrzegać kilku zasad:

Dobrą praktyką jest:

Dynamiczne typy zmiennych

Zwróć uwagę, że raz zadeklarowana zmienna MOŻE zmienić swój typ - np. coś co wskazywało na liczbę chwilę później może wskazywać na tekst. Czyli w Pythonie taka operacja wykona się poprawnie:

moja_zmienna = 1
moja_zmienna = "pies"

W wielu innych językach (tzw. statycznie typowanych) taki zapis byłby niepoprawny - jeśli raz przypisalibyśmy do zmiennej np. liczbę to przez cały cykl życia aplikacji trzeba by było się tego trzymać.

Mimo tej możliwości którą daje Python zazwyczaj dobrą praktyką jest trzymanie pod utworzoną zmienną jednego typu danych.

Print

print(zawartość_do_wyświetlenia)

np.

zdanie = "To jakiś tekst"

spowoduje wyświetlenie następującej zawartości w terminalu:

To jakiś tekst

Należy pamiętać o pisaniu polecenia z małej litery - czyli print jest ok, podczas gdy np. Print nie zostanie rozpoznane (w większości języków programowania wielkość liter ma znaczenie).

Input

nazwa_zmiennej = input ("opcjonalny_komunikat dla użytkownika")

np.

imie_gracza = input("Wprowadz imię: ")

Typy podstawowe

Typy określają wspólne cechy obiektów, metody z nimi związane. Dzięki nim programiści wiedzą jakie operacje na nich są dozwolone (a jakie nie). Są więc bardzo przydatnym środkiem komunikacji przy pisaniu aplikacji.

Python obsługuje następujące typy podstawowe:

Liczby całkowite (integer)

Liczy naturalne (1, 2, 3, 4...) i przeciwne do nich (-1, -2, -3) oraz zero.

Przypisując do zmiennej liczbę całkowitą po prostu podajemy ją po znaku równości np.

moja_liczba = 12

Liczby zmiennoprzecinkowe (float)

Liczby zapisane z "częścią ułamkową". Ważne - część ułamkowa zapisana jest po KROPCE, nie po przecinku.

moja_liczba = 22.15

Ciągi znaków (string)

Seria znaków zapisana między cudzysłowami pojedynczymi, lub podwójnymi (nie można ich jednak mieszać!) np.

moj_tekst = "To tylko tekst."

Wartości logiczne (bool)

Wartości prawda / fałsz zapisane jako True / False (istotna jest wielkość znaków!).

np.

gra_gotowa =  True

Podstawowe operacje

Podstawowe operatory to:

dodawanie + 

odejmowanie -

mnożenie *

dzielenie / 

modulo % (reszta z dzielenia)

Można ich używać na:

integer float string bool
+ tak tak tak tak
- tak tak nie tak
* tak tak tak, ale... tak
/ tak tak nie -
% tak tak nie -

W przypadku ciągu znaków (string) możemy wykonać operację mnożenia przez liczbę - powtórzy nam to dany tekst wskazaną ilość razy. Nie można jednak mnożyć ciągu znaków przez ciąg znaków.

Wartości logiczne True / False są równoważne 1 i 0 - stąd też da się wykonać operacje dzielenia, czy modulo... z założeniem, że nie dzielimy przez 0 ;) .

Odnośniki

„3.12.5 Documentation”. Dostęp 18 sierpień 2024. https://docs.python.org/3/.

Viafore, Patrick. Robust Python: write clean and maintainable code. First edition. Beijing [China]; Boston [MA]: O’Reilly, 2021.

„Zmienna (informatyka)”. W Wikipedia, wolna encyklopedia, 15 kwiecień 2024. https://pl.wikipedia.org/w/index.php?title=Zmienna_(informatyka)&oldid=73515366.

„Magic Python: Mutable vs Immutable and How To Copy Objects”. Dostęp 18 sierpień 2024 https://alexkataev.medium.com/magic-python-mutable-vs-immutable-and-how-to-copy-objects-908bffb811fa

Podejmowanie decyzji

Porównania

Żebyśmy mogli podjąć w programie jakąś decyzję musimy najpierw sformułować pytanie na które da się jednoznacznie odpowiedzieć tak/nie (prawda / fałsz) .

Najprostszym podejściem do tego jest wykonanie jakiegoś porównania np. czy 12 jest większe od 6.

Podstawowe porównania

Operator Pytanie Przykład
== równe? x == y
!= nierówne? x != y
> większe? x > y
>= większe lub równe? x >= y
< mniejsze? x < y
<= mniejsze lub równe? x <= y

W Pythonie jest podwójny znak równości (==) służy do porównania czy dane wartości są równe, podczas gdy pojedynczy = służy do przypisania wartości.

Porównania tekstów

O dziwo w Pythonie porównanie tekstów są dozwolone np.

"pies" > "kot"

Takie porównanie zwróci nam wartość True.

Jak Python doszedł do takiego wniosku? Każdy znak jest jakąś liczbą w ściśle określonej tablicy znaków (w tym wypadku unicode). Python porównuje numer danej litery dla każdego znaku w tekście i na tej podstawie podejmuje decyzję.

Warunki i bloki kodu

Mając przygotowane pytanie na które da się odpowiedzieć tak/nie, prawda/fałsz możemy wykonać jakąś akcję wg schematu:

A = 12
B = 6

jeżeli A > B:
    wykonaj coś1
a jeżeli A == B:
    wykonaj coś2
w przeciwnym wypadku: (jeśli żaden inny warunek nie został spełniony)
    wykonaj coś3

Tłumacząc to na Pythona wygląda to tak:

# to pseudokod - nie zadziała bo nie poinformowaliśmy Pythona co oznacza zrob_cos :D
A = 12
B = 6

if A > B:
    zrob_cos(1)
elif A==B:
    zrob_cos(2)
else:
    zrob_cos(3)

Należy pamiętać o wcięciach(!) - decydują one o tym co Python wykona po podjęciu decyzji np. w powyższym przykładzie jeśli A jest większe od B to uruchomi się jedynie funkcja (fikcyjna) zrob_cos(1).

np.

wiek_uzytkownika = 12

if wiek_uzytkownika < 18 :
    print("Jesteś niepełnoletni/-a")
elif wiek_uzytkownika > 65 :
    print("Hmmm... Senior?")
else:
    print("Pełnoletni.")

Czemu warto korzystać z elif

Teoretycznie można by było w powyższym przykładzie użyć tylko i wyłącznie if - użytkownik programu nie zauważyłby raczej różnicy.

Dla komputera jednak różnica jest spora - używając tylko if wykonałby każde z porównań niezależnie od wyniku poprzedniego.

Jeśli korzystamy z elif / else porównania wykonują się do momentu uzyskania w którymś odpowiedzi Prawda (True) - pozostałe wtedy są pomijane. Dzięki temu minimalizujemy ilość niepotrzebnie wykonywanych operacji.

Korzystając z if bez elif należy też uważać na ew. dodanie sekcji else - w takiej sytuacji else będzie się odnosic wyłącznie(!) do ostatniego warunku (ostatniego if-a).

Stąd też jeśli wykonujemy jakieś porównania, które są z sobą powiązane (zależą od siebie) należy korzystać z elif i else.

Wcięcia w edytorach kodu

Edytoru kodu starają się pomóc użytkownikom z wcięciami - w oczywistych miejscach od razu je dla nas robią. Oprócz tego w większości z nich są one oznaczana pionową linią:

Struktury danych

Listy i krotki

W obu wypadkach:

ALE...

Listy (list)

moje_ksiazki = ["Blade Runner", "Hobbit", "Unicorn Project"]

Krotki (tuple)

menu_kategorie = ("przystawki", "zupy", "desery")

Indeksy

Chcąc "wyciągnąć" konkretny element będący ich częścią wpisujemy identyfikator w nawiasach kwadratowych po nazwie zmiennej .

nazwa_zmiennej[indeks] np.

moja_lista = ["cos1", "cos2", "cos3", "cos4" ]

print(moja_lista[2])

WAŻNE!!! Indeksy są liczone od 0 nie od 1 . Stąd też pierwszy element na powyższej liście ma indeks 0, ostatni 3.

Możliwe jest też sięgnięcie "wstecz" np. ostatni element można wskazać przez użycie indeksu -1.

"Cięcie" list i krotek

Chcąc wyświetlić zakres elementów listy / krotki należy wpisać w nawiasie kwadratowym element startowy i element końcowy oddzielone dwukropkiem.

moja_zmienna[start:koniec]

np.

print(moja_lista[1:3])

Jeśli jest potrzeba "przeskakiwania niektórych wartości to można ten schemat rozszerzyć:

moja_lista[start:koniec:krok]

Należy to czytać - "wybierz elementy z moja_lista, zaczynając od start, zatrzymując się PRZED koniec (czyli element o tym indeksie nie wejdzie w wybrany zakres) i przeskakując co skok.

Co ciekawe przy tych "wycinkach" (slices) list/krotek podanie indeksów poza zakresem etc NIE spowoduje błędu - stworzy po prostu pustą listę/krotkę.

lista_a = [0, 1, 2, 3]
wycinek = lista_a[8:12] # to nie spowoduje błędu!
print(wycinek)
# wynikiem będzie:
# []
# ... czyli powstała pusta lista

Metody dla list i krotek

Metody to specjalne funkcje "wbudowane" w dany obiekt. Wywołuje się je poprzez wpisanie nazwy zmiennej, postawienie kropki i wstawienie parametrów do metody w nawiasach okrągłych (jeśli nie przekazujemy żadnych zostawiamy same nawiasy okrągłe).

krotka lista opis
.count() .count() zlicza ilość wystąpień przekazanego parametru
.index() .index() podaje pierwszy indeks pod którym jest przekazana wartość
.append() dodaj argument do listy
.pop() usuń element o podanym indeksie z listy(lub ostatni element)
.sort() sortuje elementy
.copy() zwraca kopię obiektu

Słowniki i zbiory

Zarówno słowniki jak i zbiory przechowują "klucze", które nie mogą się powtarzać. Jednak w przypadku słowników z kluczem jest powiązana wartość.

Klucze muszą być wartością na której da się wykonać tzw. funkcję skrótu (są "hashable"). W tym wypadku chodzi o to, że muszą to być obiekty, które nie są modyfikowalne (np. ciągi znaków, liczby).

Słowniki (dict)

Umożliwiają szybkie odnalezienie jakiejś zawartości z pomocą wartości klucza przypisanego do niej.

Deklarujemy je poprzez użycie nawiasów klamrowych, a w nich pary klucz, wartość rozdzielonych dwukropkiem.

**moj_slownik={klucz:wartość} **

np.

menu ={
    "zupa" : "ogórkowa",
    "lody" : "waniliowe",
    "sok" : "pomarańczowy"
    }

Chcąc wyświetlić wartość przechowywaną pod konkretnym kluczem działamy podobnie jak z listą.

Różnica polega na tym, że w nawiasach kwadratowych podajemy nie indeks, tylko klucz np.

print(menu["zupa"])

Jeśli kluczem jest np. jakiś tekst będący nazwą potrawy a wartością lista zawierająca jej składniki to trzymamy się tego przez cały słownik.

Przy konstruowaniu słownika warto być konsekwentnym - nie wrzucamy jako kolejnego elementu np. klucza z jakąś liczbą (niech to będzie chociażby numer strony naszej ulubionej książki kucharskiej) zestawionego z krotką przechowującą kaloryczność składników jako wartość.

Zwracajmy też uwagę na to czy faktycznie w naszym programie korzystamy z tych słownikowych kluczy - jeśli nie, to jest spora szansa, że powinniśmy skorzystać z innej struktury danych.

Słowniki - dodawanie elementów

Wystarczy przypisać jakąś wartość pod dany klucz - nie potrzeba używać żadnej specjalnej mentody. Jeśli dany klucz istnieje to wartość przechowywana pod nim zostanie podmieniona, jeśli nie to do słownika zostanie dodana nowa para klucz:wartość.

nazwa_zmiennej[klucz] = wartosc

np. żeby dodać do słownika menu nowy klucz "przystawki" z jakąś wartością można:

menu ={
    "zupa" : "ogórkowa",
    "lody" : "waniliowe",
    "sok" : "pomarańczowy"
    }

menu["przystawki"] = "sałatka grecka"

Słowniki - usuwanie elementów

Żeby usunąć element ze słownika można skorzystać z metody .pop(), podając w nawiasie okrągłym klucz elementu do usunięcia.

Np. chcąc usunąć parę "zupa" : "ogórkowa" z słownika menu wyglądałoby to tak:

menu.pop("zupa")

Słowniki - metody

słownik opis
.keys() zwraca listę kluczy słownika
.values() zwraca listę wartości słownika
.pop() usuwa element o podanym kluczu
.items() zwraca listę krotek dla każdej pary klucz:wartość
.get() zwraca wartość dla danego klucza

Zbiory (set)

Wyglądają podobnie do listy. Mają jednak jedną cenną cechę:

nie posiadają duplikatów wartości.

Czyli jeśli dwa razy spróbujemy do zbioru dodać np. słowo "kot" to ten drugi wpis zostanie usunięty.

Nie mają też indeksów (!).

Bezpiecznie jest myśleć o nich jako o takich dziwnych słownikach, które mają jedynie klucze.

Pewnie dlatego też, podobnie jak słowniki, deklaruje się je z użyciem nawiasów klamrowych:

moj_zbior={unikat1, unikat2, unikat3}

Właśnie ten brak powtórzeń jest często wykorzystywany - np. zamieniamy listę na zbiór żeby pozbyć się duplikatów.

Zamiana listy na zbiór... i z powrotem :)

ksiazki = ["Endymion", "Hobbit", "Fundacja", "Endymion"]
zbior_ksiazek =  set(ksiazki)
ksiazki_bez_powtorzen =  list(zbior_ksiazek)

Które typy są modyfikowalne (mutable)?

typ modyfikowalny niemodyfikowalny
liczba całk. tak
liczba zmiennop. tak
wart.log tak
ciąg znaków tak
krotka tak
lista tak
słownik tak
zbiór tak

Odnośniki

„3.12.5 Documentation”. Dostęp 18 sierpień 2024. https://docs.python.org/3/.

Viafore, Patrick. Robust Python: write clean and maintainable code. First edition. Beijing [China]; Boston [MA]: O’Reilly, 2021.

Modyfikacja zmiennej

Niektóre zmienne przekazywane są po wartości - co oznacza, że wartość jednej zmiennej jest KOPIOWANA do drugiej. Dotyczy to wszystkich typów, które są niemodyfikowalne - czyli np. liczb, ciągu znaków (teksty - string), wartości logicznych, krotek.

np.

a = 4       # liczba jest "niemodyfikowalna" sama z siebie
b = a       # do zmiennej b jest kopiowana 4 przechowywana pod a
b = b + 2   # do wartości przechowywanej pod b dodawane jest 2
print(a)
print(b)
# wynikiem jest:
# 4
# 6
# wynika to z tego, że b miało wartość SKOPIOWANĄ z a

W Pythonie istnieją też typy, które podlegają modyfikacji - np. listy czy słowniki. W ich wypadku to przekazanie nie robi kopii - przekazywana jest tzw. referencja do obiektu (okropny anglicyzm). Czyli zamiast kopiować wartość przekazywany jest wskaźnik informujący gdzie w pamięci komputera przechowywana jest interesująca nas wartość.

a = [4,2]       # liczba jest "niemodyfikowalna" sama z siebie
b = a       # zmienna b przechowuje ten sam wskaźnik do obiektu co a
b.pop()   # z listy na którą wskazuje b usuwana jest ostatnia wartość 
print(a)
print(b)
# wynikiem jest:
# [4]
# [4]
# wynika to z tego, że b i a wskazują na tę samą listę (nie jest robiona kopia)

Skopiowanie wartości

W przypadku gdy mamy do czynienia z obiektem niemodyfikowalnym (np. krotką) jedyne co Python może zrobić to "przekleić" naszą etykietę (zmienną) na nowy obiekt. Jeśli ten pierwotny nie będzie miał na sobie innej etykietki (nie wskazuje na niego inna zmienna) to zostanie on automatycznie usunięty (przez tzw. garbage collector).

Zmiana zawartości

W przypadku gdy obiekt jest modyfikowalny (np. lista) i dołożymy do niej kolejny element zmienna nie będzie musiała przeskoczyć na nowy obiekt z nową zawartością - po prostu zostanie dodany nowy element (zmieni się zawartość obiektu, etykieta będzie wskazywać ten sam obiekt).

... a czemu to istotne?

Powody są co najmniej dwa:

  1. Należy pamiętać, że gdy zmieniamy jakiś obiekt modyfikowalny na który wskazuje kilka zmiennych to zmienimy wartości dla każdej z nich.
  2. Tworzenie obiektów (rezerwowanie miejsca w pamięci dla nich) to proces względnie czasochłonny - stąd też proces kopiowania całego obiektu i dodawania do niego jakiegoś dodatkowego elementu (czyli wariant z obiektem niemodyfikowalnym) może spowolnić działanie tworzonej aplikacji.

-> stąd też tak istotne jest świadome dobieranie struktur danych :) .

Co jest modyfikowalne, a co nie?

typ modyfikowalny niemodyfikowalny
liczba całk. tak
liczba zmiennop. tak
wart.log tak
ciąg znaków tak
krotka tak
lista tak
słownik tak
zbiór tak

Pętle, moduł żółwia

Do wykonywania powtarzalnych czynności używamy w programowaniu pętli.

Pętla for

Wykonuje blok kodu tyle razy ile mamy jakiś elementów w sekwencji (np. tyle razy ile mamy liter w wyrazie, elementów w liście) / pozostałych obiektach iterowalnych

np.

for litera in "Robisz.to":
	print(litera)
for element in ["a", "b", "c"]:
	print(element)

Na początku najważniejsze (i prostsze do zrozumienia) jest przyjęcie, że obiektem dla pętli for są sekwencje / inne iterowalne elementy - ciągi znaków, listy, krotki, range.

Nie ma obowiązku wykorzystywania elementu sekwencji w powtarzalnym bloku. Jeśli używamy go jedynie do przemieszczania się po sekwencji to dobrą praktyką jest nazwanie go "_" (czyli użycie znaku podłogi).

Ogólny schemat korzystania z tej pętli:

Na powyższym obrazku słowo "sekwencja" jest użyte w potocznym rozumieniu. Tak jak wspomniano wyżej chodzi o każdy iterowalny element. Na start można jednak nie zawracać sobie głowy tymi subtelnościami - dla zainterowanych link w odnośnikach.

Pętla while

"... wykonuj tak długo aż"

np.

i = 0

while i < 10:
	print(i)
	i += 1

Przy każdym "obrocie" pętli zwiększamy "i" o 1, gdy osiągnie 10 warunek "i < 10" zwróci False i pętla przestanie się wykonywać.

Break

Jeśli chcemy wyjść z pętli wcześniej (w przypadku for zanim dojdziemy do ostatniego elementu, w przypadku while zanim warunek zwróci False) możemy skorzystać z break.

i = 0

while True:
	if i < 10:
		break
	print(i)
	i += 1

Funkcja range

Jeśli nie mamy jakiejś gotowej sekwencji z odpowiednią ilością elementów to z pomocą może przyjść nam funkcja range.

W najprostszej postaci konstruujemy ją tak:

range(ilośc_elementów)

np. chcąc wyświetlić napis "Hej" 3 razy możemy zrobić to tak:

for _ in range(3):
	print(Hej)

Jeśli jednak potrzebujemy jakiegoś konkretnego zakresu (np. od 10 do 20 etc) i o konkretnym kroku (czyli elementy zwiększają się o konkretną wartość) można skorzystać z dodatkowych parametrów:

range(start, stop, krok)

Importowanie bibliotek

Jeśli problem, który staramy się rozwiązać nie jest trywialny dobrym pomysłem może być wykorzystanie kodu napisanego przez kogoś innego.

Python dysponuje szeroką gamą bibliotek dostępnych za darmo. Całkiem pokaźna ich ilość jest instalowana razem z Pythonem.

Jeśli potrzebujemy jakiejś zewnętrznej (np. znalezionej na www.pypi.org ) możemy ją zainstalować poprzez wykonanie w terminalu:

pip3 install nazwa_bibilioteki

Aby wykorzystać zainstalowaną bibliotekę w pliku z programem nad którym się pracuje należy na początku zaimportować ją poprzez:

import nazwa_biblioteki

np.

import turtle

Jeśli jest potrzeba zaktualizowania biblioteki do najnowszej wersji można:

pip3 install -U <NAZWA_BIBLIOTEKI>

Moduł Turtle

Służy do rysowania z pomocą kodu. Idealnie nadaje się do ćwiczeń pętli - od razu widać co się dzieje, tym samym łatwiej diagnozować błędy.

Importuje się go poprzez:

import turtle

Przygotowanie programu dla "żółwia"

Po zaimportowaniu biblioteki należy stworzyć "żółwia" i przestrzeń po której będzie się poruszał:

zolwik = turtle.Turtle

Z kolei pod koniec naszego kodu (po tym jak już skończymy rysować) powinniśmy dodać:

turtle.exitonclick()

Podstawowe metody

Wykonujemy je na obiekcie Turtle (ja zazwyczaj nazywam go zolwik).

.forward(odległość) - przesuwa żółwia do przodu

.left(kąt) - żółwik skręca w lewo o podany kąt

.right(kąt) - skręca w prawo o podany kąt

.circle(promień) - rysuje okrąg o podanym promieniu

.penup() - żółwik przemieszczając się przestanie zostawiać ślad

.pendown() - ponownie zacznie rysować

Turtle - struktura programu

Odnośniki

„3.12.5 Documentation”. Dostęp 18 sierpień 2024. https://docs.python.org/3/.

Bunn, Tristan. Learn Python visually: creative coding with processing.py. San Francisco, CA: No Starch Press Inc, 2021.

Gruppetta, Stephen. „Iterable: Python’s Stepping Stones”, 22 czerwiec 2024. https://www.thepythoncodingstack.com/p/python-iterable-data-structures.

„Python and Turtle – Python, Turtle, Projects, Learn”. Dostęp 11 sierpień 2024. https://pythonturtle.academy/.

Szyfrowanie

Szyfrowanie

Sposób na zakodowanie danych w taki sposób, aby stały się nieczytelne. Proces ten jest jednak odwracalny. Metoda ta służy m. in. do poufnego przekazywania informacji.

Algorytmy szyfrujące

Algorytm szyfrujący to sposób w jaki zamieniamy dane na nieczytelne. Jest wiele sposobów na szyfrowanie - czyli jest wiele algorytmów.

Jeśli chodzi o kategorie do których się je da zaliczyć w tej chwili najpopularniejszy podział to podział na szyfrowanie symetryczne i asymetryczne.

Szyfrowanie symetryczne.

Czyli obie strony (nadawca i odbiorca wiadomości) dzielą się tym samym sposobem ("hasłem" / "kluczem") umożliwiającym zakodowanie i odkodowanie wiadomości.

Pojawia się więc problem - jak przekazać bezpiecznie ten klucz? Zwłaszcza w sytuacji gdy nadawca i odbiorca nie mogą się jakoś "spotkać" w bezpiecznym miejscu, żeby podzielić się tym kluczem - np. jedna strona jest gdzieś w Polsce a druga np. w Brazylii.

Mimo tego problemu szyfrowanie tego rodzaju jest stosowane m.in. ze względu na szybkość (sprawnie działa nawet na urządzeniach o bardzo ograniczonych zasobach).

Szyfrowanie asymetryczne

Ja lubię myśleć o tym w ten sposób - mamy kłódkę i klucz. Kłódkę udostępniamy publicznie, każdy może sobie ją pobrać i zaszyfrować dane z jej pomocą. My jednak mamy klucz, którym się nie dzielimy - to nasz sekret (klucz prywatny). Tylko z jego pomocą można "otworzyć" (odszyfrować) dane zaszyfrowane z pomocą "kłódki".

Dzięki temu pozbywamy się problemu z przekazaniem sposobu odszyfrowania wiadomości. De facto nie robimy tego - przekazujemy jedynie sposób na zakodowanie.

Jeśli komunikacja ma zachodzić dwukierunkowo obie strony wysyłają sobie nawzajem "kłódki" (tzw. klucze publiczne).

Ta metoda ma jednak dość poważną wadę - dużo bardziej niż algorytmy symetryczne obciąża nasze komputery.

Zwróćcie jednak uwagę, że możemy wykorzystać wolne szyfrowanie asymetryczne, żeby podzielić się z kimś szybkim kluczem symetrycznym ;) .

Funkcja skrótu (hash)

Nie należy mylić szyfrowania z hashowaniem. Funkcja skrótu tworzy nam "bełkot" na podstawie jakiś danych wejściowych. Proces ten jest (a właściwie powinien być) NIEODWRACALNY.

Jednak zawsze jesteśmy w stanie stwierdzić, że dany hash powstał na podstawie konkretnych danych wejściowych. W najprostszej postaci - te same dane wejściowe zawsze dadzą nam ten sam hash.

Co może wydawać się dziwne - po co nam coś takiego?

To super przydatne narzędzie. Umożliwia np. bezpieczne przechowywanie haseł w bazach danych. Nie przechowujemy de facto haseł tylko ich hashe - przez co w przypadku wycieku strona atakująca nie dostaje haseł.

Wykorzystując hashe jesteśmy w stanie stwierdzić czy dany pliki / wiadomości zostały zmodyfikowane - bo zmodyfikowane dane dadzą inny hash.

Programy antywirusowe nie muszą przechowywać kodu całego wirusa, żeby go namierzyć - wystarczy, że mają jego hash i szukają jego wystąpień.

Słowniki i zbiory korzystają z funkcji skrótu do wskazania miejsca gdzie można znaleźć nasze dane (to dzięki temu słowniki są szybsze od list przy dużej ilości elementów).

Szyfr Cezara

To jedna z najprostszych technik szyfrowania - do tego idealnie nadająca się do przećwiczenia pętli.

Polega ona na podmianie każdej litery tekstu jawnego (niezaszyfrowanego) inną, oddaloną o stałą liczbę pozycji w alfabecie. Liczba ta staje się kluczem do wiadomości.

np.

Alfabet: AĄBCĆDEĘFGHIJKLŁMNŃOÓPRSŚTUWYZŹŻ

Chcemy zaszyfrować imię ADA. Jako klucz przyjmijmy 3 - co oznacza, że każdą literę podmieniamy na odpowiednik po prawej stronie oddalony o 3 pozycje.

Czyli dla litery A będzie to C. Stąd też całość będzie wyglądała następująco:

Wiadomość: ADA
Klucz: 3
Zaszyfrowana: CFC

Co robić w przypadku wyjścia poza naszą tablicę znaków? Jak znowu przeskoczyć na początek?

Z pomocą przyjdzie nam modulo. Aby z jego pomocą wskazać indeks zaszyfrowanego znaku w naszym alfabecie możemy skorzystać z formuły:

indekszaszyfrowany = (indeksznaku + klucz) % len(ZNAKI)

Odnośniki

„3.12.5 Documentation”. Dostęp 18 sierpień 2024. https://docs.python.org/3/.

Pengelly, James, i Gareth Marchant. The Official CompTIA Security+ Student Guide (Exam SY0-701). CompTIA, Inc, 2023.

„Szyfr Cezara”. W Wikipedia, wolna encyklopedia, 13 czerwiec 2024. https://pl.wikipedia.org/w/index.php?title=Szyfr_Cezara&oldid=74000247.

Pliki

Tworzenie nowego folderu pod projekt

  1. Uruchom menadżer plików

  2. Przejdź do miejsca w którym chcesz utworzyć nowy folder do pracy, kliknij prawym i wybierz Nowy -> Folder

    Na Windowsie 11 skorzystaj menadżer plików wygląda nieco inaczej - ma jednak identyczną opcję. Możesz kliknąć w lewym górnym rogu "Nowy" i wybrać tam folder.

    Nazywając foldery sugeruję unikać polskich liter, znaków specjalnych (w tym spacji).

  3. Uruchom nowe okno VSCode (Plik -> Nowe okno)

  4. Wybierz "Otwieranie folderu"

  5. Odnajdź stworzony wcześniej folder i kliknij "Wybierz folder"

  6. VSCode może zapytać czy ufasz autorom - wybierz "Tak"

  7. Można już w tym nowym oknie pracować - tworzyć nowe pliki etc.

Obsługa plików w Pythonie

Sugerowane jest też dodanie słowa with tak aby plik był automatycznie domykany na koniec pracy (korzystamy w ten sposób z tzw. context managera).

Odczytywanie

with open("mojplik.txt") as moj_plik:
	print(moj_plik.read())

Domyślnie Python otwiera pliki w trybie do odczytu ("r") - podczas pracy w nim można jedynie odczytać zawartość pliku, nie można nic dodać.

W powyższym przykładzie na wczytanym obiekcie jest użyta metoda .read() umożliwiająca odczytanie całej zawartości jako ciąg znaków (string).

Zapisywanie

Chcąc zapisać coś w pliku należy otworzyć go w trybie "w":

with open("mojplik.txt", "w") as moj_plik:
	moj_plik.write("Nowa zawartość")

Parametr "w" jest dodawany do funkcji open, w nawiasach okrągłych, obok nazwy pliku, który chcemy otworzyć.

W powyższym przykładzie tekst "Nowa zawartość" zostanie zapisana do pliku mojplik.txt.

WAŻNE!!! - w tym trybie istniejąca zawartość pliku zostanie nadpisana(!).

Jeśli chcemy dopisać coś do końca pliku trzeba użyć trybu "a" (od append):

with open("mojplik.txt", "a") as moj_plik:
	moj_plik.write("Nowa zawartość")

Tryb binarny i tryb tekstowy

Domyślnie Python traktuje otwarte pliki jako tekst. Czasami zachodzi jednak potrzeba otwarcia ich w trybie binarnym (np. przy większości formatów obrazków) - wystarczy dodać literkę "b" do trybu.

Pliki tekstowe to np. pliki .txt, .csv, .json, .xml, .py ... i wiele innych.

Binarne to większość obrazków (.jpg, .png...), pliki wykonywalne aplikacji (.exe)...

W tym kontekście mówimy o plikach tekstowych jako o plikach, które są w stanie odczytać ludzie (otwierając je np. w Notatniku czy innym edytorze tekstu).

Tabelka trybów

Poza trybem r służącym do odczytu,w do napisania i a do dopisania Python oferuje nam różne kombiancje trybów.

tryb opis
r odczyt
r+ odczyt i zapis, błąd jeśli plik nie istnieje
rb odczyt w trybie binarnym
rb+ odczyt i zapis w trybie binarnym, błąd gdy plik nie istnieje
w zapis, nadpisuje zawartość, tworzy plik gdy nie istnieje
w+ odczyt i zapis, nadpisuje zawartość, tworzy plik gdy nie istnieje
wb zapis w trybie binarnym, nadpisuje zawartość, tworzy plik gdy nie istnieje
wb+ odczyt zapis w trybie binarnym, nadpisuje zawartość, tworzy plik gdy nie istnieje
a dopisanie zawartości, tworzy plik gdy nie istnieje
a+ odczyt i dopisanie zawartości, tworzy plik gdy nie istnieje
ab dopisanie w trybie binarnym, tworzy plik gdy nie istnieje
ab+ odczyt i dopisanie w trybie binarnym, tworzy plik gdy nie istnieje

Na początku dobrze jest pozostać przy podstawowych trybach - bo początkowo np. a+ i tym podobne mogą zaskakiwać (np. w a+ jak domyślnie odczytamy zawartość to nic nie dostaniemy - co wynika z tego, że w tym trybie znacznik położenie w pliku ustawiany jest na końcu zawartości).

Sprawdzenie czy plik istnieje

os.path.exists("sciezka_do_pliku")

Pliki JSON

import json

Wczytanie JSON-a

import json

with open("plik_z_danymi.json") as plik_json:
    zawartosc = json.load(plik_json)

Zapis JSON-a

import json

dane = {"jabłka" : 4, "mango" : 3}

with open("plik_z_danymi.json", "w") as plik_json:
    json.dump(dane, plik_json)

Chcąc mieć ładniej sformatowany plik JSON można dodać jeszcze parametr indent do dump np.

import json

dane = {"jabłka" : 4, "mango" : 3}

with open("plik_z_danymi.json", "w") as plik_json:
    json.dump(dane, plik_json, indent=4)

Kodowanie znaków

Domyślnie biblioteka json przy zapisie podmieni nam wszystkie "dziwne" znaki (np. polskie litery) tak aby całość dało się zapisać jako ascii.

Chcąc wymusić kodowanie utf8 należy:

import json

with open("plik_z_danymi.json", encoding="utf8") as plik_json:
    zawartosc = json.load(plik_json)
import json

dane = {"jabłka" : 4, "mango" : 3}

with open("plik_z_danymi.json", "w", encoding="utf8") as plik_json:
    json.dump(
        dane,
        plik_json,
        indent=4,
        sort_keys=True,
        ensure_ascii=False,
    )

CSV

import csv

with open("currency.csv", "r") as plik:
    reader = csv.reader(plik)

    print(reader)

    for wiersz in reader:
        print(wiersz)

Pandas

https://pandas.pydata.org/

Np. otwarcie pliku csv z pomocą Pandas:

import pandas as pd

currency_dataframe = pd.read_csv("currency.csv")
print(currency_dataframe)

Przydatne funkcje do wczytywania plików z pomocą Pandas:

A na koniec krótkie przypomnienie:

Importowanie bibliotek

Aby wykorzystać jakąś bibliotekę, którą mamy zainstalowaną (pobraną na nasz komputer) należy użyć słowa import w kodzie pythonowym (najlepiej na początku):

import nazwa_biblioteki

Jeśli nazwa jest za długa możemy ją podmienić np.

import nazwa_biblioteki as nb

Instalacja bibliotek

Jeśli nie mamy biblioteki w systemie możemy ją pobrać np. wykorzystując narzędzie pip (w terminalu):

pip3 install nazwa_biblioteki

Własne funkcje

Funkcje

Funkcje to "czasowniki" w języku programowania. Czyli to nazwany blok kodu który wykonuje jakieś działania. Czasami w jego wyniku jest również zwracana jakaś wartość.

Do tej pory korzystaliśmy z funkcji dostarczonych nam przez Pythona (np . print), lub "wgranych" z biblioteki / modułu.

Pisząc własne programy już na tym etapie zetknęliśmy się z sytuacją gdy robiliśmy podobną rzecz wiele razy - zmieniały się np. tylko dane wejściowe np. modyfikowaliśmy jakiś adres w książce adresowej, gdzie zmieniało się tylko, który adres był modyfikowany.

Możemy taką funkcjonalność ;) zebrać w całość i udostępnić jako własną funkcję. Należy pamiętać, że Python odczytuje nasz kod linijka po linijce, z góry na dół - więc musimy stworzyć nasz "czasownik" przed jego użyciem (deklaracja funkcji musi być wcześniej w kodzie).

Własną funkcję deklarujemy używając słowa def.

np.

def kwadrat(x:int) ->  int:
	wynik = x * x
	return wynik

Zwróćcie uwagę na słowo return - wykorzystujemy je jeśli chcemy, żeby nasza funkcja poza wykonaniem jakiejś operacji zwróciła nam jakąś wartość (którą np. możemy przypisać do zmiennej). Bardzo dużo funkcji korzysta z tej opcji - np. funkcja sum zwraca nam sumę dwóch liczb, funkcja max zwraca nam największą wartość przechowywaną chociażby na liście itd.

Nie jest to jednak reguła - np. funkcja print wyświetla nam daną zawartość na ekranie, ale nie zwraca żadnej wartości.

Powyższy przykład zawiera nieco nieobowiązkowych rzeczy:

Dodawanie tych informacji jest dobrą praktyką - ułatwia nam (i innym użytkownikom tego kodu) korzystanie z programu.

Ponadto dzięki nim narzędzia jakich używamy do pisania będą w stanie podpowiadać nam w przypadku popełnienia błędu związanego z użyciem nieprawidłowego typu.

Tak skonstruowana funkcje wykorzystujemy identycznie jak te wbudowane np.

def kwadrat(x:int) ->  int:
	wynik = x * x
	return wynik

liczba = kwadrat(2)
print(liczba)

Powyższa kod wyświetli nam 4 gdyż jest to 2 podniesione do kwadratu (a właśnie to robi ta funkcja kwadrat).

Parametry pozycyjne

Paremetry do funkcji możemy przekazywać w kolejności ich zadeklarowania w sygnaturze funkcji np.

def wydrukuj_zmienne(a, b, c):
    print("a to:", a)
    print("b to:", b)
    print("c to:", c)

wydrukuj_zmienne(1,2,3)

zakończy się wynikiem:

a to: 1
b to: 2
c to: 3

Czyli do a została przypisana 1, do b 2, a do c 3.

Jeśli przekazujemy parametry w ten sposób musimy znać ich kolejność w sygnaturze funkcji.

Parametry po kluczu

Możemy też skorzystać z innego wariantu:

def wydrukuj_zmienne(a, b, c):
    print("a to:", a)
    print("b to:", b)
    print("c to:", c)

wydrukuj_zmienne(c=1,a=3,b=2)

co da:

a to: 3
b to: 2
c to: 1

W tym przypadku wywołując funkcję dokładnie podaliśmy do jakiej zmienne powinny przechowywać wskazane wartości.

Oznacza to trochę więcej pisania - ale jest mniejsze ryzyko popełnienia błędu. Ten wariant jest też bardzo przydatny przy bardziej rozbudowanych funkcjach, które mają masę możliwych parametrów - nie musimy zapamiętywać pozycji każdego z nich.

Zakres (scope)

"Wnętrze" funkcji jest odizolowane od reszty kodu. Ozancza to, że np. zmienna moja_zmienna zadaklarowana poza funkcją i identycznie nazwana zmienna wewnątrz funkcji to 2 RÓŻNE zmienne(!).

moja_zmienna = 55 # deklaruje zmienna 

def moja_funkcja():
    moja_zmienna = 1    # deklaruje zmienna wewnatrz funkcji
    print(moja_zmienna)

moja_funkcja()
print(moja_zmienna)

# wynikeim będzie
# 1
# 55
# bo mimo identycznej nazwy
# zmienna wewnątrz funkcji operuje
# w innym zakresie (ma swoje wlasne nazwy)

GUI - Flet

Flet to biblioteka bazująca na Flutterze. Umożliwia tworzenie aplikacji z graficznym interfejsem użytkownika na urządzenia stacjonarne i mobilne w Pythonie. Dostarcza gotowe komponenty umożliwiające budowanie graficznych interfejsów użytkownika.

Ponieważ Flutter został stworzony w Google to interfejs stworzony z jego pomocą ma dość charakterystyczny wygląd wpisujący się w tzw. Material Design.

Dużą zaletą Flet-a jest duża ilość dobrze opisanych domyślnych ustawień - stworzone są one w taki sposób, że edytory kodu "podpowiadają" nam jakie mamy możliwości.

Instalacja Flet-a

pip3 install flet[all]

Ściągnie to wszystkie zależności i dodatkowe narzędzia używane podczas korzystania z frameworku Flet.

Przez framework rozumiemy szablon z zestawem komponentów wykorzystywanych do budowy aplikacji.

Bazowy schemat aplikacji

Aplikacje w Flet-ie można budować m.in. stosując podejście obiektowe.

W przypadku początkowych aplikacji można jednak podejść do tego prościej.

Można np. stworzyć funkcję opisującą okno programu, w niej z kolei będziemy deklarować wszystkie komponenty.

Takim komponentem może być przycisk, pole tekstowe itd.

Następnie należy dodać te zadeklarowane elementy do okna aplikacji (poprzez page.add).

Na samym końcu z kolei wywołamy naszą funkcję.

import flet


def main(page: flet.Page):
    # Sekcja ogólnych ustawień okna aplikacji    

    page.title = "Tytul"  # np. tytulu okna

    # Sekcja deklaracji komponentów
    # - czyli opisujemy np. jakie pola tekstowe,
    # przyciski etc mają się znaleźć w oknie i jak 
    # mają one wyglądać.

    tekst = flet.Text("Jakiś tekst.") # np.tekst
    przycisk = flet.FilledButton("wciśnij mnie") # np. przycisk

    # Sekcja umieszczania komponentów w oknie
    # - czyli decydujemy jaki będzie ich układ,
    # w jakiej kolejności się będą wyświetlać,
    # gdzie zostaną umieszone
    page.add(
        tekst,
        przycisk,
    )


flet.app(main)

Czyli importujemy aplikację, tworzymy funkcję odpowiadającą za zbudowanie okna (czyli np. dodanie pól tekstowych, przycisków itd). Następnie funkcję tę wywołujemy za pomocą flet.app.

WAŻNE!!! Aplikację zbudowanej z framework-iem Flet lepiej uruchamiać przez flet run nazwapliku.py, a nie przez guzik "play" w VSCode czy python nazwapliku.py. Uruchomienie poprzez flet run powoduje, że nie musicie cały czas zamykać i otwierać okna po zaktualizowaniu aplikacji - zmiany wczytają się automatycznie.

Lista komponentów

Flet udostępnia całą masę flutterowych elementów - przycisków, pól do wpisywania danych, różnej maści menu itd.

Informacje na ich temat można znaleźć tutaj:

https://flet.dev/docs/controls

Najbardziej podstawowe komponenty to m.in.:

W praktyce

Przykładowa aplikacja wyświetlająca komunikat powitalny:

import flet


def strona_glowna(page: flet.Page):

    # funkcja wykorzystywana w oknie, podpieta do przycisku
    def przywitaj(_event):
        if imie_pole.value:
            imie = imie_pole.value
            komunikat_tekst.value = f"Witaj {imie}!"
            komunikat_tekst.update()

    # sekcja ustawien okna
    page.title = "Hello!"

    # sekcja komponentów
    imie_pole = flet.TextField(
        label="Wprowadź imię:",
    )
    witaj_przycisk = flet.FilledButton(
        text=" przywitaj ",
        on_click=przywitaj,
        style=flet.ButtonStyle(padding=10),
    )
    komunikat_tekst = flet.Text("")

    # sekcja umieszczenia komponentów w oknie
    page.add(
        imie_pole,
        witaj_przycisk,
        komunikat_tekst,
    )


flet.app(strona_glowna)

Warto zwrócić uwage na sposób podpięcia funkcji "przywitaj" do przycisku "witaj_przycisk" - czyli wciśnięcie przycisku spowoduje uruchomienie funkcji.

Wymaga to dodania "wyzwalacza" do przycisku - w tym wypadku "on_click" (gdyż chcemy aby funkcja była uruchamiana po wciśnięciu przycisku, są też inne wyzwalacze np. hover). Ponieważ chcemy żeby to Flet obsłużył uruchomienie funkcji to należy zwrócić uwagę, że po podaniu nazwy funkcji (w powyższym przykładzie przywitaj) NIE ma nawiasów okrągłych.

Drugą ciekawą rzeczą jest "tajemniczy" parametr _event dodawany do funkcji uruchaminej przez przycisk. Nazwę _event można zmienić, ale należy pamiętać, że musi się on pojawić jako pierwszy parametr funkcji wyzwalanej przez jakiś komponent graficzny aplikacji (przycisk etc).

GUI - TkInter

GUI

Graficzny interfejs użytkownika (GUI) umożliwia nam korzystanie z urządzeń przy pomocy ikon, przycisków, pasków i innych graficznych elementów.

Samodzielne przygotowanie tych komponentów i mechanizmów wymagałoby dużego nakładu pracy. Stąd też powstała cała gama "gotowców" z których można skorzystać w różnych języka programowania.

Do jednych z najpopularniejszych wariantów dostępnych w Pythonie należą:

  1. TkInter

    Ten zestaw narzędzi dostarczony jest razem z językiem Python. Umożliwia relatywnie łatwe rozpoczęcie tworzenia graficznych aplikacji.

    Do jego głównych wad zalicza się wygląd - domyślnie aplikacje stworzone z TkInter nie wyglądają zbyt nowocześnie. Spowodowało to powstanie licznych modyfikacji oryginalnej biblioteki mających na celu poprawienie tego aspektu.

  2. Qt (Pyside, PyQt)

    Qt należy do najpopularniejszych frameworków GUI. Dostępny jest w większości języków programowania. W Pythonie dostępny jest z pomocą bibliotek Pyside i PyQt (ten pierwszy to oficjalny produkt firmy odpowiedzialnej za rozwój Qt).

    Do bardziej wymagających projektów dostępny jest QtDesigner umożliwiający wizualne opracowywanie graficznego szablonu dla aplikacji.

  3. GTK (PyGObject)

    GTK wykorzystywany jest m.in. przez środowisko graficzne GNOME będący jednym z najpopularniejszych wyborów dla systemów opartych o Linux-a.

    Zapewnia przyzwoity wygląd aplikacji, ale nie zawsze dobrze integrujący się z środowiskami innymi niż GNOME.

    Istnieją nieoficjalne programy służące do projektowania okien dla GTK, takie jak Cambalanche.

  4. Kivy

    Framework wykorzystywany m.in. przy bardziej nietypowych interfejsach np. na urządzenia mobilne.

  5. DearPyGUI

    Zapewnia wsparcie wyświetlania z wykorzystaniem GPU, dzięki czemu w założeniu interakcja z aplikacją powinna być bardziej responsywna.

  6. Flet

    To stosunkowy nowy wariant, umożliwia wykorzystanie frameworku Flutter w Pythonie.

TkInter

Do zaimportowanie TkInter-a można użyć:

import tkinter as tk

Pierwszą czynnością zazwyczaj jest stworzenie podstawowego okna:

import tkinter as tk

okno = tk.Tk()
okno.title("Jakiś tytuł")

okno.mainloop()

Metoda mainloop odpowiada za uruchomienie głównej pętli odpowiedzialnej za wyświetlanie okna.

Podstawowe komponenty

Pełną listę komponentów można znaleźć tutaj:

https://docs.python.org/3/library/tkinter.html

Elementy interfejsu przypisujemy pod zmienne i przekazujemy odpowiednie parametry. Zazwyczaj pierwszy parametr wskazuje na to do którego elementu dany komponent należy (w najprostszym wariancie będzie to okno aplikacji).

Do podstawowych aplikacji można skoncentrować się na:

Label

etykieta = tk.Label(rodzic_komponentu, inne_parametry)

np.

text_label = tk.Label(okno, text="jakis napis")
pic_label = tk.Label(okno, image=obrazek)

Button

przycisk = tk.Button(text="jakis napis", command=nazwa_funkcji)

Input

wprowadz = tk.Entry(width=80)

Frame

Gdy chcemy pogrupować jakieś elementy (przyciski, etykiety etc.) z pomocą przychodzi nam frame. Taka ramka staje się rodzicem pozostałych komponentów.

To kontener na inne komponenty.

import tkinter as tk

okno = tk.Tk()
okno.title("Jakiś tytuł")

ramka = tk.Frame(okno, padding = 10)
text_label = tk.Label(ramka, text="jakis napis")
wprowadz = tk.Entry(ramka, width=80)

okno.mainloop()

Rozmieszczanie komponentów

Jeśli spróbujecie powyższego kodu zobaczycie, że nie działa to jeszcze poprawnie. Wynika to z tego, że nie zadeklarowaliśmy w jaki sposób mają być rozmieszczone komponenty.

Dwie podstawowe metody to:

Pack

Rozmieszcza komponenty półautomatycznie w ramach rodzica zgodnie z przekazanymi parametrami. Mogą one być umieszczone poziomo lub pionowo zgodnie z wartością parametru side.

Parametry:

np.

import tkinter as tk

okno = tk.Tk()
okno.title("Jakiś tytuł")

ramka = tk.Frame(okno)
ramka.pack()
text_label = tk.Label(ramka, text="jakis napis")
text_label.pack(side="left")
przycisk = tk.Button(ramka, text="jakis napis")
przycisk.pack(side="right")
wprowadz = tk.Entry(okno, width=80)
wprowadz.pack()

okno.mainloop()

Grid

Elementy rozmieszczane są na siatce wierszy i kolumn (można to sobie wyobrazić jako umieszczenie ich w komórkach arkusza kalkulacyjnego.)

https://www.pythontutorial.net/tkinter/tkinter-grid/

W przypadku "rozciągnięcia" elementu na kilka komórek trzeba:

CustomTkInter

Myślę, że dało się zauważyć, że domyślny TkInter jest dość brzydki. Z pomocą przychodzą nam różne warianty tego modułu, które mają na celu poprawę tego aspektu.

Jeśli chodzi o składnię w większości wypadków jest niemal identyczna z podstawowym TkInter - należy jedynie podmienić nazwę modułu.

import tkinter as tk
import customtkinter as ctk

okno = ctk.CTk()
okno.title("Jakiś tytyuł")
okno.configure(padx=50, pady=50)

etykieta = ctk.CTkLabel(master=okno, text="Tekst etykiety")
etykieta.pack()

wprowadz = ctk.CTkEntry(master=okno, width=400)
wprowadz.pack()

# Do przycisku zazwyczaj dodaje się parametr
# command = nazwa_funkcji
# uruchamiajacy jakas funkcje po przyciśnięciu
przycisk = ctk.CTkButton(master=okno, text="Tekst przycisku")
przycisk.pack()

okno.mainloop()

Odnośniki

https://docs.python.org/3/library/tkinter.html

https://www.pythontutorial.net/tkinter/tkinter-grid/

John Elder, TKinter Widget Quick Reference Guide

Programowanie obiektowe - klasy

Klasy

Zazwyczaj chcemy żeby w naszym kodzie było możliwie mało powtórzeń, całość była jak najbardziej czytelna, a osoby korzystające z naszego programu miały jasne wytyczne czego nasz kod oczekuje.

Jednym z narzędzi w drodze do tego celu są klasy, będące fundamentem podejścia nazywanego programowaniem obiektowym.

Korzystając z tej metody staramy się modelować nasz kod w sposób nieco przypominający to jak zbieramy informacje o otaczającym nas świecie. Np. grupujemy gatunki w podobne kategorie. Pies domowy należy do rodziny psowatych, które z kolei należą do rzędu ssaków drapieżnych.

Mimo tego, że psy różnią się od siebie (ciężko pomylić jamnika z dobermanem) to mają one pewne cechy wspólne, które umożliwiają nam przypisanie ich do tego typu (gatunku).

Podobnie modelując aplikację staramy się znaleźć jakieś elementy wspólne obiektów, które tworzymy i na tej podstawie stworzyć typ grupujący je - służy on jako swego rodzaju szablon do stworzenia nowych obiektów.

Oprócz cech charakterystycznych (atrybutów) klasa zawiera też metody - czyli rzeczy, które dany obiekt może robić.

Jako przykład posłużmy się postacią do gry komputerowej rpg. Każda z nich będzie miała jakieś punkty życia, punkty magii, profesję itd. Żeby nie wymyślać tej struktury za każdym razem możemy zdefiniować odpowiednią klasę:

class PostacGracza:
	def __init__(self, imie, profesja):
		self.imie = imie
		self.profesja = profesja
		self.hp = 100
		self.mana = 100

gracz1 = PostacGracza("Fizban", "mag")
gracz2 = PostacGracza("Taz", "łotrzyk")

Obie stworzone postaci (gracz1 i gracz2) zostały zrobione na podstawie tego samego szablonu (klasy PostacGracza), ale nie są identyczne - różnią się imieniem i profesją. Co więcej pozostałe ich atrybuty (hp i mana) mimo, że na start mają tę samą wartość są od siebie całkowicie niezależne (są to zupełnie osobne wartości). Czyli np. jeśli w trakcie gry Fizban otrzyma 30 obrażeń to będzie miał on 70 hp, podczas gdy Taz nadal będzie miał 100.

Klasa to nie obiekt

Ważne aby zdawać sobie sprawę, że ten przygotowany "szablon" nie jest równoznaczny z obiektem danego typu. W powyższym przykładzie PostacGracza to opis klasy, a gracz1 to juz obiekt klasy PostacGracza ("szablon wypełniony zawartością").

Na tej samej zasadzie np. definicja kota w encyklopedii nie jest naszym kotem Mruczkiem, który właśnie przeszkadza nam w pracy ;) .

Porównując klasy do np. szablonów ankiet, które dajemy komuś do wypełnienia to sam szablon byłby klasą, a uzupełniona, wypełniona przez kogoś ankieta byłaby obiektem.

Tworzenie klasy

Metoda __init__

Wartości domyślne

Jeśli do któregoś atrybutu klasy często przypisujemy tę samą wartość możemy oszczędzić użytkownikowi jej wpisywania poprzez zadeklarowanie tej wartości domyślnej. W powyższej klasie mogłoby to wyglądać tak:

class PostacGracza:
    def __init__(self, imie: str, profesja: str, hp: int = 100, mana: int = 100):
        self.imie = imie
        self.profesja = profesja
        self.hp = hp
        self.mana = mana

    def powitaj(self):
        print(f"Witaj {self.imie}.")

Tym samym chcąc stworzyć nową postać z domyślną wartością dla hp i many wystarczy wpisać:

gracz1 = PostacGracza("Taz", "łotrzyk")

Oczywiście chcąc dać np. magowi więcej punktów many możemy tę domyślną wartość napisać inną np.

gracz2 = PostacGracza("Fizban", "mag", mana=200)

Funkcje powiązane z klasą - metody

class PostacGracza:
	def __init__(self, imie, profesja, hp =100, mana = 100):
		self.imie = imie
		self.profesja = profesja
		self.hp = hp
		self.mana = mana

	def powitaj(self):
		print(f"Witaj {self.imie}. Twoja profesja to {self.profesja} .")

W powyższym przykładzie mamy jedną metodę - powitanie. Wykorzystujemy je identycznie jak metody dostarczone razem z klasami dostarczonymi z Pythonem (czyli po nazwie zmiennej stawiamy kropkę, następnie wpisujemy nazwę metody i korzystamy z nawiasów okrągłych do uruchomienia i ew. przekazania jakiś parametrów). Np. chcąc stworzyć obiekt klasy PostacGracza i użyć metody powitaj należy np.

gracz1 = (imie="Fizban", profesja="mag")

gracz1.powitaj()

Dziedziczenie

W poyższym przykładzie z klasą PostacGracza jest zauważalny problem - profesja to nic innego jak pole tekstowe. Poza informacją o profesji nie wnosi nic nowego, nie rozszerza możliwości obiektu tej klasy. Czyli np. mag nie dostanie jakiś metod odpowiadających za rzucanie zaklęć, nie dostanie automatycznie większej ilości punktów magii itd.

Jednym z rozwiązań może być zastosowanie mechanizmu tzw. dziedziczenia. Tworzymy nową klasę (np. Mag) i oznaczamy, że... odziedziczy ona wszystkie cechy / metody innej (np. PostacGracza - tym samym PostacGracza jest w tym przykładzie rodzicem :D ). Następnie dodajemy nowe atrybuty / metody do tej nowej klasy - tym samym zwiększając dostępne możlwości.

Mogłoby to wyglądać np. tak:

class PostacGracza:
	def __init__(self, imie, profesja, hp =100, mana = 100):
		self.imie = imie
		self.hp = hp
		self.mana = mana

	def powitaj(self):
		print(f"Witaj {self.imie}.")


class Mag(PostacGracza):
    def __init__(self, imie):
        super().__init__(imie, hp=80, mana=200)

    def rzuc_kule_ognia(self):
        print("Rzucono kulę ognia!")
        return 30

Klasa Mag rozszerza możliwości dostarczone przez PostacGracza o nową metodę - rzuc_kule_ognia oraz zmienia wartości przypisywane do atrybutów hp i mana.

super().__init__()

Funkcja ta służy do aktywowanie atrybutów klasy rodzica. Mówiąc inaczej - służy do uruchominie funkcji __init__ rodzica. Nie dodajemy w tym wypadku w niej słowa self - to jest przyjęte domyślnie.

Alternatywnie możemy zamiast funkcji super użyć nazwy klasy rodzica (np. PostacGracza)... ale w tym wypadku nie możemy pominąć słowa self.

class Mag(PostacGracza):
    def __init__(self, imie):
        PostacGracza().__init__(self, imie, hp=80, mana=200)

    def rzuc_kule_ognia(self):
        print("Rzucono kulę ognia!")
        return 30

Z tego względu sugerowane jest użycie funkcji super.

Odnośniki

„3.12.5 Documentation”. Dostęp 18 sierpień 2024. https://docs.python.org/3/.

PyGameZero

Tworzenie gier wymaga zapanowania nad m.in. wyśweitlaniem obiektów na ekranie, wykrywaniem kolizji, obsługą urządzeń wejścia (np. gamepadów) itd. Stąd też powszechne jest korzystanie z gotowych "silników" i bibliotek zapewniających te rzeczy - tak, żeby programiści mogli się skoncentrować na samej logice gry.

PyGame Zero buduje na innej bibliotece - PyGame. Upraszcza ją jednak trochę np. dba o uruchamianie właściwej pętli z programem.

https://pygame-zero.readthedocs.io/

Chcąc skorzystać z bazowej biblioteki PyGame, bez ułatwień oferowanych przez PGZero, warto sięgnąć po PyGame CE - to nowsza odsłona tego projektu.

Instalacja

pip3 install pgzero

Szablon aplikacji

import pgzrun
from pygame import display
from pgzero import screen as pgzero_screen
from pgzero.builtins import keyboard

# rozmiar okna - pozniej wrzucimy to do osobnego pliku
WIDTH = 800
HEIGHT = 600

screen = pgzero_screen.Screen(display.set_mode((WIDTH, HEIGHT), 0))

# pygame uruchamia pętlę gry automatycznie
# ... ale potrzebuje do tego odpowiednio nazwanych funkcji.
# Korzystamy z funkcji update i draw.


def update():
    # tu aktualizujemy stan obiektow
    pass


def draw():
    # tu je rysujemy
    pass


# wywołanie pętli gry
pgzrun.go()

W powyższym szablonie została nieco rozbudowana sekcja importów - tak, aby edytory kodu nie "marudziły" podczas tworzenia aplikacji (biblioteka niestety jest napisana w sposób, który niezbyt dobrze współgra z tego typu narzędziami).

Folder images i sounds

W folderze z projektem dobrze jest stworzyć podfoldery sounds i images. To miejsca w których domyślnie PGZero będzie szukać dźwięków i obrazków.

Niezbędne funkcje i klasy

Funkcja update()

Jeśli będziemy mieli jakieś obiekty, których stan się będzie zmieniał niezbędna będzie nam funkcja update(). W nią wrzucimy takie rzeczy jak np. sterowanie, wykrywanie kolizji etc. - czyli wszystko co zmienia stan obiektu (np. dodaje / odejmuje jakąś wartość do atrybutu odopowiadającego za jego położenie na ekranie).

Funkcja draw()

W kodzie na pewno będziemy potrzebowali stworzyć funkcję draw. Odpowiada ona rysowanie obiektów.

Klasa Actor

Wszędzie tam gdzie będą obiekty wchodzące z sobą w interakcje przyda nam się klasa Actor. Zapewni nam obsługę kolizji, zadba, że odpowiedni obrazek znajdzie się we właściwym miejscu itd.

Metoda .draw.text z screen

Służy do obsługi tekstu w grze.

Streamlit

Streamlit to framework (platforma, szkielet do budowy aplikacji) do tworzenie aplikacji web-owych z wykorzystaniem języka Python. Nie wymaga znajomości HTML-a, Javascript-u, czy CSS-a.

Powstał z myślą głównie o zastosowaniach w analizie danych (tworzenie różnej maści dashboardów etc). Jednak społeczność jego użytkowników często stosuje go do zastosowań mocno odbiegających od tej tematyki.

Instalacja i dodanie do programu

Framework ten możemy zainstalować wykorzystując menadżera pakietów pip:

pip3 install streamlit

Importujemy go do aplikacji w standardowy sposób, poprzez import. Przyjęło się wykorzystywać tę bibliotekę pod nazwą st (stąd pojawiają się po import dodatkowe słowa as st):

import streamlit as st

st.write("Hello World!")

Uruchamianie aplikacji

Aplikacje stworzone z Streamlit-em NIE SĄ uruchamiane w "normalny" sposób (czyli np. poprzez python nazwaprogramu.py czy guzik play w VSCode). Zamiast tego musimy:

streamlit run nazwaprogramu.py

Dzięki temu uruchamiany jest serwer www umożliwiający nam oglądanie naszej aplikacji w przeglądarce i pozostała funkcjonalność niezbędna Streamlit-owi.

Podstawowe komponenty Streamlit-a

Zachęcam do przejrzenia oficjalnej dokumentacji dostępnej pod:

https://docs.streamlit.io/

Zawiera ona bardzo dobre przykłady dla wszystkich dostępnych komponentów.

Tytuł okna


st.set_page_config(page_title="Jakiś tytuł")

Komponenty "tekstowe"

st.write("tekst")

st.title("tekst")

st.header("tekst")

Komponenty do wprowadzania / wyboru danych

st.text_input()

st.slider()

st.checkbox()

st.multiselect()

Wykresy

st.bar_chart()

st.line_chart()

st.scatter_chart

Formularze

Wciśnięcie / wybranie jakiegokolwiek elementu na stronie zbudowanej z pomocą Streamlit-a spowoduje jej całkowite przeładowanie.

Wyjątkiem jest formularz - jego zawartość jest przeładowywana jedynie po wciśnięciu odpowiedniego przycisku.

import streamlit as st

with st.form("Jakiś formularz"):
    st.write("Wewnątrz formularza")
    slider_val = st.slider("Suwak")
    checkbox_val = st.checkbox("Checkbox")

    # Formularz powinien mieć przycisk do wysłania.
    submitted = st.form_submit_button("Wyślij")
    if submitted:
        st.write("Suwak: ", slider_val, "Checkbox: ", checkbox_val)

st.write("Poza formularzem")

Przechowywanie stanu zmiennych

Co w przypadku gdy nie korzystamy z formularza i mamy potrzebę przechowania stanu jakiejś zmiennej, tak żeby nie została ona odświeżona po wciśnięciu przycisku lub wykonaniu innej akcji?

W tym celu udostępnia specjalną zmienną session state:

https://docs.streamlit.io/develop/api-reference/caching-and-state/st.session_state

Odnośniki

„Streamlit Documentation”. Dostęp 18 sierpień 2024. https://docs.streamlit.io/.

API

To rodzaj umowy - "jeśli zadasz pytanie w ten sposób, dostaniesz ode mnie odpowiedź w takim formacie". Tę spisaną umowę możemy potraktować jako rodzaj instrukcji umożliwiającej uzyskanie potrzebnych nam danych z zewnętrznej aplikacji.

Tworzą je programiści / architekci tworzący daną aplikację.

REST API

To zestaw wytycznych jak konstruować API w pewien określony sposób. Zawiera opis funkcji jakie API powinna realizować (GET, PUT, DELETE...) i sposób realizacji (np. brak przechowywania stanu pomiędzy zapytaniami, zapytania o ten sam zasób powinny wyglądać tak samo).

REST (Representational State Transfer) nie jest standardem / protokołem - to zestaw "luźnych" wytycznych np. nie ustala w jakim formacie będą dostarczane dane, może to być JSON, XML itd. (chociaż najczęściej spotyka się JSON-a).

Jeśli szukacie czegoś "sztywniejszego" należy sięgnąć po alternatywy np. SOAP (tam np. mamy sprecyzowane, że dane mają być wymieniane poprzez XML). Jednak ta elastyczność REST-a jest jednym z fundamentów sukcesu tego podejścia.

Aplikacje spełniające wymogi REST nazywane są RESTful.

Aby odczytać REST-owe API wysyłamy komunikat GET na odpowiedni adres pod którym jest przechowywany poszukiwany zasób (można to zrobić nawet w przeglądarce).

Jeśli chcemy uzyskać jakiś bardziej sprecyzowany zakres danych aplikacje je udostępniające zazwyczaj dają nam przynajmniej jedną z możliwości:

  1. Inny adres dający nam te sprecyzowane dane
  2. Parametry, które dodajemy do adresu lub w treści zapytania

Aby ułatwić testowanie / przeglądanie API powstała cała grupa aplikacji takich jak Postman , Insomnia, Hoppscotch itd.

Hoppscotch.

To narzędzie umożliwiające nam testowanie i konstruowanie zapytań do API - bardzo ułatwia pracę, umożliwia przygotowanie działających zapytań przed ich implementacją w aplikacji.

CRUD

W kontekście API często pojawia się termin CRUD jako aplikacji realizującej 4 podstawowe funkcje:

  1. Create (put/post - tworzenie)
  2. Read (get - odczyt)
  3. Update (post/put/patch - aktualizuj / modyfikuj)
  4. Delete (delete - usuń)

Python - requests

https://requests.readthedocs.io/en/latest/

Jak zwykle zaczynamy od zaimportowania, po czym od razu możemy przystąpić do wysyłania zapytań - np. typu GET (czyli pobierz).

import requests

odpowiedz = requests.get("https://api.frankfurter.app/latest")

Zwróci nam to obiekt typu Response. Jak go zdekodować?

Jeśli dostaliśmy JSON-a to np. tak:

odczytana = odpowiedz.json()

Jeśli chcemy po prostu odczytać odpowiedz jako tekst to Response ma atrybut .text.

Jeśli zachodzi potrzeba dodania jakiś parametrów wystarczy np. przekazać odpowiednio skonstruowany słownik.

parametry = {"key1" : "value1", "key2" : "value2"}

odpowiedz = requests.get("https://api.frankfurter.app/latest", params = parametry)

Podobnie jeśli chcemy dodać coś w nagłówku / body zapytania:

naglowek = {"user-agent" : "my-app/0.0.1"}

odpowiedz = requests.get("https://api.frankfurter.app/latest", headers = naglowek)

Hoppscotch - "gotowce"

Korzystając z narzędzi do testowania API możemy wspomóc się gotowym, wygenerowanym przez nie kodem:

Odnośniki

„3.12.5 Documentation”. Dostęp 18 sierpień 2024. https://docs.python.org/3/.

Amazon Web Services, Inc. „What Is an API? - Application Programming Interface Explained - AWS”. Dostęp 15 wrzesień 2024. https://aws.amazon.com/what-is/api/.

„API”. W Wikipedia, 13 wrzesień 2024. https://en.wikipedia.org/w/index.php?title=API&oldid=1245469139.

Dale, Kyran. Data Visualization with Python and Javascript: Scrape, Clean, Explore & Transform Your Data. Beijing Boston Farnham Sebastopol Tokyo, 2016.

„Hoppscotch • Open Source API Development Ecosystem”. Dostęp 15 wrzesień 2024. https://hoppscotch.io/.

„What Is a REST API?” Dostęp 15 wrzesień 2024. https://www.redhat.com/en/topics/api/what-is-a-rest-api.

„What Is a REST API? | IBM”, 19 październik 2021. https://www.ibm.com/topics/rest-apis.

Zagadnienie zaawansowane

Zagadnienie zaawansowane

Programowanie funkcyjne w Pythonie.

https://pl.wikipedia.org/wiki/Programowanie_funkcyjne

Podejście do programowanie (styl) który kładzie nacisk na tworzenie funkcji zamiast na zmianę stanu zmiennych. Koncetruje się bardziej na tym CO ma się wydarzyć a nie jak.

Python zazwyczaj nie jest pierwszym wyborem jeśli chodzi o ten styl pisania kodu - chociażby przez to, ze właściwie wszystko możemy w nim modyfikować, podczas gdy w programowaniu funkcyjnym raczej stawia się na niemodyfikowalność stanu (immutability).

Python jednak daje trochę możliwości, które umożlwiają pozanie tego podejścia np. to, że funkcje w nim możemy traktować jako wartości (first class functions - o czym często nieświadomie można przekonać się zaczynając przygodę z Pythonem i zapminając o dodaniu "()" po nazwie funkcji), to, że możemy zwracać funkcje jako wartości (higher order functions).

Lambda

x = lambda a : a + 10
print(x(5)) 
def myfunc(n):
  return lambda a : a * n

mydoubler = myfunc(2)

print(mydoubler(11))

mytripler = myfunc(3)

print(mytripler(11))

Map

def kwadrat(x):
    return x * x

nums = [1, 2, 3, 4, 5]
do_kwadratu = map(kwadrat, nums)

print(list(do_kwadratu))

Filter

Reduce

import functools

def silnia(n):
    return functools.reduce(lambda x, y: x * y, range(1, n + 1))

Odnośniki

https://www.w3schools.com/python/python_lambda.asp