_ _ _ _ | |_ ___ _ __ _ __ ___ (_)_ __ __ _| | | ___ __ _ | __/ _ \ '__| '_ ` _ \| | '_ \ / _` | | |/ _ \ / _` | | || __/ | | | | | | | | | | | (_| | | | (_) | (_| | \__\___|_| |_| |_| |_|_|_| |_|\__,_|_|_|\___/ \__, | |___/
☠ morketsmerke ☠
Część 1: Zapoznanie się z językiem Python
1.1. Wstęp.
Informacje zawarte tutaj odnoszą się do wersji Pythona powyżej 3.6. Python powinien być zainstalowany domyślnie we wiodących dystrybucjach systemu Linux. Jeśli nie ma w ogóle Pythona w wersji >= 3. To warto sprawdzić czy istnieją pakiety w repozytorium dystrybucji z wersją powyżej 3.6. Jeśli tak to instalujemy te pakiety. Jeśli nie, to wtedy należałoby pobrać kod źródłowy najnowszego Pythona i go skompilować. Zazwyczaj nie kompiluje, ale w przypadku kiedy użyłem dystrybucji BunsenLabs Helium, to pre-instalowanego Pythona miałem w wersji 3.5. Za mało na najnowszy Crash Course. Kompilacje wykonujemy w następujących krokach:
- Instalujemy odpowiednie dla naszej dystrybucji build-essential
oraz dwa pakiety, których nieobecność została zauważona przy
wykonywaniu polecenia
make altinstall
i zwrócona jako błąd, mianowicie chodzi o zlib1g oraz zlib1g-dev. - Pobieramy najnowszą wersję kodu z https://www.python.org/ftp/python/3.8.3/Python-3.8.3.tar.xz.
- Rozpakowujemy:
tar -xvf Python-3.8.3.tar.xz
- Przechodzimy do katalogu:
cd Python-3.8.3.tar.xz
- Ja w swoim systemie zachowałem obecną wersję Pythona, wskazując
skryptowi konfiguracyjnemu opcje
prefix
wskazującą na katalog utworzony w moim katalogu domowym:./configure --prefix="/home/xf0r3m/bin/python3.8"
. Użycie opcjiprefix
, przekazuje skryptowi, aby przygotował środowisko do kompilacji względem podanej ścieżki. Jeśli nie zostanie zwrócony żaden błąd podczas pracy skryptu, zostanie nam wygenerowany tzw. plik makefile. - Wydajemy polecenie kompilacji kodu:
make
. Po poprawnie zakończonej kompilacji, to znaczy po wyświetlonych przez poleceniemake
następnych kompilacjach, zostanie nam zwrócony prompt, możemy przejść do ostatniej części kompilacji czyli instalacji pakietu w systemie. Ze względu na to iż zostawiłem pre-instalowaną wersję 3.5, wydaje polecenie 'make altinstall
po wykonaniu tego polecenia nasz Python 3.8 znajduje się w podkatalogu bin na ścieżce, którą podaliśmy w opcjiprefix
. - Ostatni punkt jest dla wygodnych. W pliku .bashrc możemy
sobie zdefiniować alias python dla naszego nowo
zainstalowanego Pythona. Po uruchomieniu polecenia
python
powiśmy zobaczyć prompt powłoki wraz z informacjami odnośnie wersji.
Python 3.8.2 (default, Apr 27 2020, 15:53:34) [GCC 9.3.0] on linux Type "help", "copyright", "credits" or "license" for more information. >>>
Poniżej znajdują się zagadnienia z książki które warto mieć przy sobie przy pierwszych projektach tworzonych w tym języku. Kolejność jest identyczna z kolejnością występowania w książce.
1.2. Witaj w świecie Pythona.
print() - instrukcja wyświetlająca wszystko co umieścimy pomiędzy nawiasami.
Konwencja nadawania wartości zmiennym w Pythonie wygląda w ten sposób:
greetinsg_message = "Hello, World!"
Warto pamiętać o spacjach pomiędzy operatorem. Podobnie zresztą wygląda konwencja stosowania jakichkolwiek operatorów.
1.2.1. Ciągi tekstowe w Pythonie
Ciągi tekstowe f - formatowane ciągi tekstowe. Litera f pochodzi od słowa format, ich głównym zadaniem jest zamiana nawiasu klamrowego wraz z nazwą zmiennej na jej wartość w ciągu. Taki ciąg tworzy się w następujący sposób:
imie = 'xf0r3m' print(f"Witaj {imie}! Czy chcesz zanurzyć się w świat Pythona?")
Czy w innej zmiennej czy poleceniu print()
zapisanie litery f
przed znakami
cudzysłowu lub apostrofu, spowoduje że dany ciąg będzie ciągiem f.
Uwaga! Ciągi f są dostępne dopiero od wersji 3.6.
Formatowanie ciągu przed wersją Pythona 3.6. Takie formatowanie polega na użyciu wbudowanej metody format(). Przykład poniżej.
imie = 'xf0r3m' print("Witaj {}! Czy chcesz zanurzyć się w świat Pythona?".format(imie))
Dodatkowa zmienna do wyświetlenia, to dodatkowa para nawiasów i kolejna zmienna jako argument metody format.
Do obróbki ciągów tekstowych, a konkretnie ich normalizacji mogą służyć nam poniższe metody:
- title() - Zwraca ciąg tekstowy jak tytuł, czyli każdy wyraz w ciągu zaczyna się z wielkiej litery.
- lower() - Zwraca ciąg tekstowy po całości zapisany małymi literami.
- upper() - Zwraca ciąg tekstowy zapisany po całości z wielkich liter
Warto zaznaczyć że te metody nie zmieniają wartości ciągu, o ile nie przypiszemy nie ich wartości zwrotnych.
Białe znaki w ciągach tekstowych. Białe znaki w naszych ciągach uzyskuje się w bardzo prosty sposób, po prostu po przez umieszczenie w ciągu:
- \t - tabulator
- \n - znak nowej linii
imie = 'xf0r3m' print(f"Witaj \t{imie}!\nCzy chcesz zanurzyć się w świat Pythona?")
Usuwanie białych znaków. Może zdarzyć się taka sytuacja,
że weźmie się skądś dane, i nagle gdzieś nasz algorytm nie działa, bo
ma nieprawidłowe dane. Gdzieś na początku albo na końcu ciągu znajdują
się nadmiarowe białe znaki np. skopiowaliśmy jakiś ciąg ze spacją na
końcu. To zresztą zdarza się dość często. W tym przypadku korzystamy z
metody strip()
jeśli nie potrafimy jasno
określić czy z lewej lub prawej strony ciągu znajdują się nadmiarowe
białe znaki. Dla znaków po lewej stronie możemy użyć lstrip()
a dla znaków po prawej rstrip(). Warto mieć na uwadze,
że te metody nie dokonują w zmiennej żadnych zmian, aby zapisać
zmiany musimy nadać zmiennej wartość zwracaną przez tą metodę.
dirty_strings=' python ' washed_strings=dirty_strings.strip()
1.2.2. Więcej o zmiennych
Wielkie liczby możemy zapisywać za pomocą grup rozdzielonych przy pomocy znaku podkreślenia (_), na przykład:
one_bilion = 1_000_000_000
Z poziomu Pythona nie ma znaczenia, czy jest 1000000000 czy 1_000_000_000. Podobnie jest z mniejszymi liczbami, takimi jak 1000 (1_000). Uwaga! Ta funkcjonalność dostępna jest od Pythona 3.6 w górę.
Wiele przypisań wartość możemy skrócić sobie do praktycznego, aczkolwiek nie zawsze czytelnego zapisu. Na przykład:
x, y, z = 0, 0, 0 a, b, c = 3, 4, 5
W Pythonie nie ma stałych, jednak wśród programistów nie tylko Pythona przyjęło się, że każdą zmienną, której nazwa (identyfikator) będzie zapisany wielkimi literami traktuje się jako stałą.
UNIX_EPOCH = "01-01-1970"
1.2.3.Komentarze
Komentarze w Pythonie zaczynają się od krzyżyka (#). Wszystko co zostanie umieszczone po tym znaku zostanie zignorowane w przez interpreter Pythona.
NIGGA_AM = 300 #BANG!
1.2.4. Kilkanaście zasad.
Zen Pythona - to kilka zasad stworzonych przez społeczność Pythona. Cenne uwagi, które warto wziąć sobie do serca programując nie tylko w Pythonie. Warto sobie zrobić z tego swoisty kodeks programisty - 18 zasad.
>>> import this The Zen of Python, by Tim Peters Beautiful is better than ugly. Explicit is better than implicit. Simple is better than complex. Complex is better than complicated. Flat is better than nested. Sparse is better than dense. Readability counts. Special cases aren't special enough to break the rules. Although practicality beats purity. Errors should never pass silently. Unless explicitly silenced. In the face of ambiguity, refuse the temptation to guess. There should be one-- and preferably only one --obvious way to do it. Although that way may not be obvious at first unless you're Dutch. Now is better than never. Although never is often better than *right* now. If the implementation is hard to explain, it's a bad idea. If the implementation is easy to explain, it may be a good idea. Namespaces are one honking great idea -- let's do more of those!
1.3. Listy
Listy - bardzo podobne do tablic. Do poszczególnych elementów uzyskujemy dostęp poprzez podanie indeksu w nawiasie kwadratowym:
cars = ['audi', 'bmw', 'renault', 'honda'] print(cars[2])
Dostęp do końcowych elementów listy następuje poprzez podanie ujemnego indeksu, począwszy od -1 wskazujący ostatni element listy, kolejno -2 to przedostatni itd.
Nadając zmiennej parę nawiasów kwadaratowych możemy utworzyć pustą listę, do której wrazie potrzemy będziemy mogli dodać elementy.
cars=[]
1.3.1. Dodawanie elementów do listy
Dodawanie elementów na końcu listy możemy zrealizować za pomocą
wbudowanej metody append()
. Jak argument
metoda przyjmuje wartość dodawanego elementu.
cars = ['audi', 'bmw', 'renault', 'honda'] cars.append('toyota')
Za pomocą metody insert()
, możemy
wstawić element w dowolny indeks listy. Metoda jako argumenty przyjmuje
indeks dla nowego elementu oraz jego wartość.
cars = ['audi', 'bmw', 'renault', 'honda'] cars.insert(2, 'daewoo')
1.3.2. Usuwanie elementów z listy
Usuwanie elementów z listy możemy przeprowadzić na trzy
sposoby. Po prostu usuwając element poprzez podanie nazwy listy oraz
indeksu (tak jak byśmy uzyskiwali dostęp do wartości) instrukcji
del
.
del cars[1]
Kolejnym sposobem może być zabranie danej wartości z listy za pomocą
metody pop()
. Jako argument metoda
przyjmuje indeks, tak więc możemy pobrać dowolny element, jeśli nie
podamy żadnych argumentów metoda pobierze ostatni element.
cars = ['audi', 'bmw', 'renault', 'honda'] fura = cars.pop()
Ostatnim, chyba najciekawszym sposobem jest metoda
remove()
, która wyszukuje element na
podstawie wartości. Poszukiwaną wartość podajemy jak argument metody.
cars = ['audi', 'bmw', 'renault', 'honda'] swag_gablota = 'bmw' cars.remove(swag_gablota)
Uwaga! Metoda remove()
usuwa tylko pierwsze wystąpienie danej wartości na liście, więc jeśli
jest więcej niż jedna taka wartość to tylko pierwsza odnaleziona zostanie
usunięta.
1.3.3. Sortowanie list
Elementy na liście mogą być innej kolejności niż moglibyśmy sobie tego życzyć, w Pythonie istnieje kilka sposób na uporządkowanie listy (sortowanie).
Pierwszą z nich jest użycie metody
sort()
, metoda ta wyróżnia się tym, że
wprowadza zmiany samej liście, czyli jej użycie w kodzie zmieni kolejność
wartości na liście. Domyślnie metoda sort() sortuje rosnąco,
jednak możemy mieć na to wpływ, podając jej argument
reverse=True
, teraz metoda posortuje
listę malejąco.
Kolejnym sposobem na sortowanie jest sposób tymczasowy, na przykład gdy
kolejność danych ma znaczenie i nie możemy jej od tak sobie zmieniać, ale
jednak wypadałoby aby użytkownikowi wypisać posortowane dane.
Za sortowanie tymczasowe odpowiada funkcja
sorted()
. Funkcja od metody tym
kontekście różni się tym że musimy podać jako argument listę, a następnie
zrobić coś z danymi zwróconymi albo przechować je w zmiennej albo wypisać
za pomocą polecenia print()
. Oczywiście
funkcja sorted() również przyjmuje argument reverse=True.
cars = ['audi', 'bmw', 'renault', 'honda'] cars.sort() print(cars) cars.sort(reverse=True) print(cars) cars = ['audi', 'bmw', 'renault', 'honda'] print(sorted(cars)) print(sorted(cars, reverse=True))
W Pythonie sortowanie jest nieco bardziej skomplikowane, ponieważ ciągi tekstowe sortowane są za pomocą wartości tablicy ASCII. Mniejszą wartość (liczbę całkowitą przyporządkowaną do znaku w tablicy) mają wielkie litery, dlatego jeśli elementy listy mają wartość nie jednakowe (wszystkie z wielkiej/wszystkie z małej) to możemy spodziewać się, że wartość zapisane wielką literą będą miały pierwszeństwo na posortowanej liście.
1.3.4. Odwracanie listy.
Inną metodą na organizację listy jest jej odwrócenie. To znaczy, że
ostatni element będzie pierwszym, przedostatni drugim itd,
do odwracania listy służy metoda reverse()
.
cars = ['audi', 'bmw', 'renault', 'honda'] cars.reverse()
Warto wspomnieć, że reverse() trwale zmienia kolejność listy.
1.3.5. Długość listy
Wielkość listy możemy określić przy pomocy funkcji
len()
, funkcja jako argument przyjmuje
naszą listę.
cars = ['audi', 'bmw', 'renault', 'honda'] len(cars)
1.4. Pętla for
Pętla for w Pythonie jest podobna konstrukcyjnie do pętli
for z BASH-a, również używa się słowa kluczowego
in
. Jeśli chcielibyśmy przetłumaczyć na
język potoczny pętlę for to będzie to mniej więcej tak: "Dla
zmiennej value przypisz element z listy, następnie wykonaj blok
kodu, na koniec przesuń się na kolejny element listy". Ten rodzaj pętli
służy głównie iteracjom przez listy lub listy tworzone przez funkcję
range() (określone zbiory danych). Przy pętli for po
raz pierwszy spotkamy się z zagnieżdżaniem bloków kodu. Przy każdej
konstrukcji operującej na bloku kodu pojawia się dwukropek
(:), oznaczający początek blok kodu. Wszystkie linie,
które mają znaleźć się w owym bloku przesuwamy o jeden tab. Ponieważ
wcięcia Pythonie są wykorzystywane do oznaczania bloku kodu, trzeba
uważać przy ich stosowaniu.
cars = ['audi', 'bmw', 'renault', 'honda'] for car in cars: print(car)
1.5. Funkcja range
Za generowanie serii liczb ujętych w listę odpowiedzialna jest funkcja
range()
. Jako argumenty przyjmuje ona
zakres dla generowanej serii liczb. Cechą funkcji, o której warto
wiedzieć to to, że ostatnia liczba z zakresu jest traktowana w sposób
wyłączny, to znaczy, że jeśli użyjemy funkcji range() do
wygenerowania listy od 1 do 10, to jeśli podamy argumenty 1,10 to listę
otrzymamy od 1 do 9.
for number in range(1,10): print(number)
Funkcja range() może przyjmować jeden argument, zakres końcowy.
for number in range(5): print(number)
Dane wyjściowe takiej pętli będą od 0 od 4 - 5 elementów.
Funkcję range()możemy wykorzystać wraz inna funkcją -
list()
do tworzenia list liczbowych.
numbers = list(range(1,11)) for number in numbers: print(number)
Po za określeniem zakresu funkcja range() przyjmuje również krok, czyli o ile ma zwiększyć kolejne elementy.
even_numbers = list(range(2,11,2)) for evenn in even_numbers: print(evenn)
1.6. Proste funkcje statystyczne
W Pythonie mamy dostępnych kilka bardzo prostych wybudowanych funkcji statystycznych. Mianowicie:
- min() - zwraca najmniejszą wartość na liście
- max() - zwraca największa wartość na liście
- sum() - zwraca sumę wszystkich elementów listy
1.7. Lista składana
Aby ograniczyć do minimum proces tworzenie list. Python wprowadza coś takiego co nazywa się listą składaną. Taka lista składa się z modelu wartości elementu (np. value**2) oraz z pętli for wraz z funkcją range(). Liczba stworzonych elementów znajduje się w argumencie funkcji range(). Elementy będą miały wartość obliczoną w modelu.
squares = [value**2 for value in range(1,11)] print(squares)
1.8. Wycinek listy.
Powiedzmy, że do obliczeń potrzebujemy dwóch środkowych wartości z listy. Aby wydobyć je w najbardziej efektywny sposób możemy użyć wycinka listy. Wycinek tworzymy, podając w miejscu indeksu przy normalnym odwoływaniu się do listy, zakres składający się z wartości początkowej (uwaga, liczonej od 0), dwukropka (:) oraz wartości końcowej, z tym, że z wartością końcową jest identycznie co w przypadku funkcji range(). Chcąc stworzyć wycinek z drugiego oraz trzeciego elementu, należy policzyć od 0, czyli drugi element będzie mieć indeks 1, zakres końcowy trzeba podać o 1 większy więc licząc od 0 trzeci element to 2 oraz jeszcze 1 więc koniec, końców zakres końcowy to 3.
even_numbers = list(range(2,11,2)) evenn_slice = even_numbers[1:3]
Zakres wartości w wycinkach, jest nieco bardziej elastyczny. Na przykład chcemy wszystkie elementy do czwartego to zamiast [0:4] podajemy po prostu [:4] możemy pominąć wartość początkową, wtedy domyślnie będzie 0. Podobnie jest z wartością końcową [2:] ten zakres oznacza, że wycinek będzie zawierał wszystkie elementy od 3 do końca listy. Podobnie jak w przypadku indeksów, możemy podać ujemne wartości, przyczym podajemy tylko wartość początkową. Taki wycinek będzie zawierać elementy od tego wskazanego do końca listy. Przy podawaniu warto pamiętać że ostatni element to -1, im większa wartość ujemna tym bliżej początku listy jesteśmy.
even_numbers = list(range(2,11,2)) end_values = even_numbers[-2:]
1.8.1. Krok w wycinku listy
Zakres wycinku ma możliwość podania trzeciego argumentu, wartości kroku przy tworzeniu wycinka.
numbers = list(range(1,11)) non_even_numbers=numbers[0::2]
1.8.2. Iteracja przez wycinek listy
Do wskazania pętli for możemy użyć wycinka listy. Na poniższym przykładzie zaprezentowano jak należy to wykonać:
numbers = list(range(1,11)) for value in numbers[0::2]: print(value)
1.8.3. Kopia listy
Często może zdarzyć się taka sytuacja, że chcemy zachować pierwotną wersję listy, ale musimy użyć jej wartości do jakiś operacji, które mogą naruszyć wartości jakiś jej elementów, w tym wypadku możemy stworzyć kopię listy.
cars = ['audi', 'bmw', 'renault', 'honda'] cars_bkp = cars[:]
1.9. Krotki
Może zdarzyć się potrzeba posiadania listy stałych, których wartość nie może zmienić się przez cały cykl życia programu. Z taką listą jest już lepiej w niż ze zwykłymi stałymi. Taką listę nazywa się krotką. Definiowanie krotki, przypomina definiowanie listy, jednak zamiast nawiasów kwadratowych, mamy zwykłe okrągłe. Definicja wygląda w następujący sposób:
rgb = ('red', 'green', 'blue') print(rgb[0]) print(rgb[1]) print(rgb[2])
Dostęp do elementów krotki, jest identyczny jak do elementów listy, co zostało zobrazowane powyżej. Teraz dla eksperymentu możemy spróbować zmienić jeden z elementów krotki.
>>> rgb = ('red', 'green', 'blue') >>> rgb ('red', 'green', 'blue') >>> rgb[0]='yellow' >>> Traceback (most recent call last): File "", line 1, in TypeError: 'tuple' object does not support item assignment
Iteracja przebiega tak samo jak na listach. Jedyną rzeczą jaką możemy zrobić przy krotce aby zmodyfikować jej zawartość, jest jej nadpisanie.
rgb = ('red', 'green', 'blue') print(rgb[0]) print(rgb[1]) print(rgb[2]) rgb = ('cyan', 'yellow', 'magenta', 'black')
Krotka zawierająca jedną wartość musi zawierać przecinek na końcu, ponieważ dzięki niemu zmienna jest rozpoznawana jako krotka.
1.10. Wyrażenia warunkowe
Każdy bardziej rozbudowany program będzie potrzebował instrukcji decyzyjnej. Te instrukcje uruchamiają blok kodu na podstawie testów warunkowych. Najprostszym z nich jest sprawdzenie równości.
>>> car = 'Audi' >>> car == 'audi' False
W powyższym przykładzie wyszedł na jaw bardzo ważny szczegół, Python jest case-sensitive, rozróżnia wielkość liter. Dlatego warto przed jakimkolwiek testami warunkowymi przeprowadzić normalizacje.
>>> car = 'Audi' >>> car.lower() == 'audi' True
Kolejnym testem jaki możemy przeprowadzić na wszystkich typach danych jest sprawdzenie nierówności.
>>> car = 'Audi' >>> car.lower() != 'audi' False
Porównania mniejsze niż, mniejsze bądź równe, większe niż, większe bądź równe, są testami wykonywanymi na liczbach. Nawet jeśli zestawimy sobie w porównaniu dwa ciągi znaków, to Python porówna wartości z tablicy ASCII pierwszych znaków i na podstawie tego zostanie zwrócona wartość logiczna True lub False.
>>> age = 19 >>> age > 21 True >>> age >= 21 True >>> age > 21 False >>> age >= 21 False car1 = 'audi' >>> car2 = 'bmw' >>> car1 > car2 False >>> car2 > car1 True >>> car2='Kamaz' >>> car1 > car2 True
Czasami może być tak, że takie najzwyklejsze testy jak te przeprowadzone powyżej nie wystarczą, aby instrukcja warunkowa uruchomiła blok kodu potrzebne będzie sprawdzenie kilku połączonych ze sobą warunków do łączenia ze sobą testów służą operatory logiczne.
- and - operator służy do mnożenia wyników pomniejszych wyrażeń logicznych, używany gdy wszystkie warunki muszą zwrócić tę samą wartość logiczną.
- or - operator służy do dodawania wyników pomniejszych wyrażeń logicznych, używany gdy ocena końcowa testu wynika z sum poszczególnych wyrażeń.
- not - operator służy do negowania wyniku całego wyrażenia. Negowanie polega na odwróceniu wartość, kiedy mamy True to po zanegowaniu otrzymamy False i vice versa.
age = 19 (age >= 13) and (age > 20) True
Słowa kluczowego in
nie wykorzystujemy
tylko w pętli for, możemy również je wykorzystać do wyrażenia
warunkowego, w którym to możemy określić czy dana wartość znajduje się na
liście.
>>> pojazdy = ['audi', 'bmw', 'Kamaz'] >>> 'audi' in pojazdy True
Dodając słowo kluczowe not
przed słowem
in możemy sprawdzić czy danej wartości nie ma na liście.
pojazdy = ['audi', 'bmw', 'Kamaz'] 'audi' not in pojazdy False
1.11. Konstrukcja if
Oczywiście do sterowania wykonaniem programu za pomocą testów warunkowych służy instrukcja if, które jest częścią dużej konstrukcji warunkowej, składającej się z polecenia if, rozpoczynającego całą konstrukcje i definiującego pierwszy test warunkowy, jeśli test zwrócić wartość True zostanie wykonany blok kodu instrukcji if.
if test_warunkowy: #blok_kodu_instrukcji_if
A co jeśli test warunkowy zwróci wartość False, w tym wypadku możemy poddać wykonanie programu jeszcze jednemu warunkowi za pomocą instrukcji elif lub za pomocą ogólnego bloku przeznaczonego dla tego przypadku - else. Powiedzmy, że chcemy sprawdzić jeszcze jeden warunek.
if test_warunkowy: #blok_kodu_instrukcji_if elif test_warunkowy2: #blok_kodu_instrukcji_elif
Co jeśli oba warunki zawiodą to powinniśmy zdefiniować kod blok kodu dla takiej sytuacji. Z racji tego iż można pominąć w Pythonie blok else, ja zachęcam do jego stosowania. Autor książki podaje że:
Blok else jest wykorzystywany w sytuacji, gdy nie został spełniony żaden warunek z warunków wcześniej zdefiniowanych za pomocą poleceń if lub elif. Brak spełnienia warunku może wynikać z podania nieprawidłowych danych lub danych o złośliwym działaniu. Jeżeli istnieje konkretny, ostateczny warunek do sprawdzenia, rozważ użycie bloku elif i całkowite pominięcie bloku else. W ten sposób zyskasz absolutną pewność, że kod będzie wykonywany jedynie po spełnieniu oczekiwanych warunków.
Moim zdaniem blok domyślny przy nie spełnieniu żadnego warunku, daje nam szanse na reakcje na nieprawidłowe dane wejściowe. Najprościej jeśli nie spełniony został żaden warunek to kończymy działanie programu.
if test_warunkowy: #blok_kodu_instrukcji_if elif test_warunkowy2: #blok_kodu_instrukcji_elif else: #blok_kodu_else
Przydatną rzeczą jaką możemy zrobić z poleceniem
if
jest sprawdzenie przed rozpoczęciem
pracy z listą, czy lista aby nie jest pusta.
cars=[] if cars: for value in cars: print(value) else: print("Lista jest pusta")
Jak widać powyżej, aby sprawdzić czy lista jest pusta w Pythonie wystarczy podać jej nazwę w miejscu testu warunkowego.
1.12. Słowniki
Słowniki przypominają trochę format JSON zaimplementowany do języka programowania. Najprostszy słownik wygląda tak:
miasto1 = { 'kraj': 'Polska', 'powierzchnia': 517.14, 'populacja': 1_790_658, }
Nie, przecinek na końcu nie jest błędem. Wskazuje tylko, że do słownika mogą zostać wprowadzone nowe pary klucz-wartość. Kluczem określamy wartość występującą w słowniku, zawsze ujętą w apostrofy lub cudzysłowie, z dwukropkiem na końcu, dzięki kluczowi będziemy odwoływać się do wartości pary. Dostęp do dowolnej wartości słownika:
miasto1 = { 'kraj': 'Polska', 'powierzchnia': 517.14, 'populacja': 1_790_658, } print(miasto1['powierzchnia'])
Kiedy chcemy uzyskać dostęp do konkretnej wartości to przy słowniku, klucz zapisujemy w nawiasie kwadratowym. Analogicznie do tablic asocjacyjnych w innych językach programowania.
Pusty słownik definiujemy podobnie jak pustą listę tylko inne są znaki.
empty_dict={}
Jeśli chcemy zmodyfikować wartość z jakieś pary, to tak samo jak w przypadku list, tylko że klucz zapisujemy w miejscu indeksu.
miasto1['kraj'] = 'Polska'
Kiedy jakieś dane w słowniku staną nam się zbędne, możemy je usunąć przy
pomocy polecenia del
.
del miasto1['populacja']
1.12.1. Dodawanie nowych wartości do słownika
Dodawanie nowych par klucz-wartość jest analogiczne do modyfikacji. Jeśli będzie modyfikować wartość pod kluczem, który nie istnieje w słowniku to zostanie ona dodana jako nowa para.
Co jeśli podczas pracy z słownikiem odwołamy się do klucza, który nie istnieje? Python zwróci błąd.
>>> print(miasto1['gestosc']) Traceback (most recent call last): File "", line 1, in KeyError: 'gestosc'
Python daje możliwość użycia specjalnej metody
get()
, która jest wstanie zwrócić
wartość podanego jako argument klucza, lub też zdefiniowanego jako
kolejny argument komunikatu, w momencie kiedy dany klucz nie istnieje.
Komunikat nie jest wymagany w przypadku jego braku nie zostanie nam nic
zwrócone. Metoda get() nadaje się jako test czy dany klucz
istnieje.
if miasto1.get('gestosc'): #Gęstość istnieje, zrób to i to else: #Gęstość nie istnieje, zrób to i to
1.12.2. Iteracja przez słownik
Iteracji przez słownik w sumie są trzy rodzaje: przez pary, przez klucze i przez wartości. Przez pary, będzie prawdopodobnie najpopularniejszą z wszystkich rodzajów.
miasto1 = { 'kraj': 'Polska', 'powierzchnia': 517.14, 'populacja': 1_790_658, } for key,value in miasto1.items(): print(f"Klucz: {key}") print(f"Wartość: {value}\n")
Metoda items()
w dużym skrócie rozbije
na dwie listy wszystkie klucze i wszystkie wartości, i to na nich pętla
for w pamięci będzie przeprowadzać iteracje.
1.12.2.1. Iteracja przez klucze i wartości.
Iteracja przez klucze, zadziała na podobnej zasadzie co przez parę jednak do iteracji wykorzystuje się tylko jedną listę, listę z kluczami. Analogicznie z iteracją przez wartości tylko że listę z wartościami.
miasto1 = { 'kraj': 'Polska', 'powierzchnia': 517.14, 'populacja': 1_790_658, } for key in miasto1.keys(): print(f"Klucz: {key}\n") for value in miasto1.values(): print(f"Wartość: {value}\n")
Metody keys()
i
values()
tworzą listy, dlatego też
możemy przeprowadzić na nich iterację z wykorzystaniem pętli for
oraz in
.
W wydaniach Pythona począwszy od 3.7, słownik zachowuje kolejność, w której były dodawane pary. Podczas pracy z słownikiem jego domyślną uporządkowaniem jest kolejność dodawania par.
Listę kluczy możemy obudować funkcją sorted() aby je uporządkować.
1.13. Zbiory
Klucze muszą być unikatowe, aby wartości się na siebie nie nakładały. Ale wśród wartości już takich wymagań nie ma. Python udostępnia funkcję set(), która na podstawie przekazanej listy tworzy zbiór z unikatowych elementów. Zbiór jest składniowo bardzo podobny do słownika, jednak zawiera sam wartości, nie zawiera on żadnych danych porządkowych.
jezyki = {'python', 'ruby', 'c', 'python'} >>> jezyki {'python', 'ruby', 'c'}
Sposobem na to aby uzyskać dostęp do danych zapisanych na zbiorze jest iteracja lub konwersja na listę za pomocą funkcji sorted(). Należy pamiętać aby zapisać gdzieś listę zwróconą przez sorted()
1.14. Zagnieżdznie złożonych typów danych
Przedstawienie słowników umożliwia zaprezentowanie zagnieżdżania. Pozwala ono na umieszczenie jednego złożonego typu w drugim, możemy stworzyć między innymi listę słowników, listę w słowniku czy też słownik w słowniku. Listę słowników najlepiej rozpocząć od pustej listy, następnie czy to w skutek iteracji czy danych napływających do programu tworzyć nowy słownik i załadować go do listy za pomocą metody append(). Trochę eksperymentując zauważyłem ciekawą rzecz, mianowicie kiedy stworzyłem sobie model słownika powyżej iteracji, i chciałem odpowiednio według testów warunkowych modyfikować wartości modelu, to na listę trafiała tylko pierwsza aktualizacja przez całą iterację. Dostęp do danych zagnieżdżonych wygląda trochę jak tablice wielowymiarowe. W zależności od tego czy jest to lista słowników, to na początku podajemy indeks a później klucz, w przypadku zagnieżdżenia listy w słowniku, sytuacja jest odwrotna.
Ostatnim przypadkiem jest słownik w słowniku, tu podajemy klucz - klucz. Do operacji na zagnieżdżeniach warto wykorzystać metody stosowane do iteracji na słownikach. Ważną rzeczą stosowaną przy zagnieżdżeniach jest ich jak największe ograniczenie, jeśli algorytm zakłada większe zagnieżdżenie niż powyżej jednego poziomu (tj. każdy klucz zawiera pojedynczą listę lub każdy element listy zawiera słownik bez zagnieżdżeń), to oznacza że ma on poważne wady konstrukcyjne i na pewno jest jakiś lepszy sposób na rozwiązanie tego problemu.
1.15. Pobieranie danych od użytkownika.
Pobieranie danych od użytkowników jest realizowane dzięki funkcji
input()
. Funkcja ta jako argument
przyjmuje, komunikat zachęcający użytkownika do wpisania danych tzw.
znak zachęty. Dane wpisane przez użytkownika są zwracane w postaci ciągu
tekstowego.
Jeśli musimy pobrać od użytkownika dane do obliczeń, to wartością
zwracaną przez funkcje input() jest ciąg tekstowy. W tym wypadku
musimy dokonać konwersji na typ całkowity. Odpowiada za to funkcja
int()
, której wartością zwracaną jest
liczba całkowita, a argumentem dowolna wartość przypominająca liczbę,
która ma zostać skonwertowana ja postać liczby całkowitej. Do obliczeń
nie zawsze będziemy potrzebowali wartości całkowitej. Może zdarzyć się,
że będziemy potrzebować wartości zmiennoprzecinkowej, aby uzyskać wartość
w postaci liczby zmiennoprzecinkowej należy użyć funkcji
float()
. Te konwersje można odwrócić
uzyskując ciąg tekstowy z liczb za pomocą funkcji
str()
.
>>> age = input("Ile masz lat?") Ile masz lat? 12 >>> age '12' >>> age = int(age) >>> age 12 >>> age >= 18 False
Konwersje można skrócić do jednej linii, obudowując funkcję
input()
funkcją funkcją
int()
.
age = int(input("Ile masz lat? ")) Ile masz lat? 18 >>> age 18 >>> age >= 18 True
1.16. Operator modulo (reszty z dzielenia).
Przydatną rzeczą jeśli chodzi o obliczenia liczbowe wykorzystywane w
programowaniu napewno jest operator modulo (%
).
Przyjmuje dwa operandy (dzielną i dzielnik), zwraca resztę z dzielenia
dzielnej przez dzielnik. Najprostszym przykładem użycia operatora modulo
(%) jest sprawdzenie czy dana wartość jest parzysta czy
też nie.
if number % 2 == 0: print(f"{number} jest liczbą parzystą.") else: print(f"{number} nie jest liczbą parzystą.")
1.17. Pętla while.
Jeśli potrzebujemy pętli która działa do pewnego momentu, np. dopóki dana
zmienna ma wartość taką a taką, to wtedy musimy skorzystać z pętli
while. Pętla ta działa dopóty, dopóki warunek
umieszczony obok słowa kluczowego while
będzie zwracał wartość True. Pętla while nie posiada
wbudowanego licznika, dlatego takowy licznik musimy sobie zbudować sami.
number=1 while number >= 10: if number % 2 == 0: print(f"{number} jest parzysta.") else: print(f"{number} nie jest parzysta.") number += 1
Wynik działania powyższego przykładu:
1 nie jest parzysta. 2 jest parzysta. 3 nie jest parzysta. 4 jest parzysta. 5 nie jest parzysta. 6 jest parzysta. 7 nie jest parzysta. 8 jest parzysta. 9 nie jest parzysta. 10 jest parzysta.
Zmienna number przybrała w tym przykładzie rolę danych, na których operujemy oraz licznika.
Czasami może zajść potrzeba aby kontrolować wykonanie programu za pomocą pętli while. Powiedzmy że mamy aplikacje, która wyświetla nam listę opcji, następnie prosi nas jej wybór, wybieramy jedną z opcji, następnie po wykonaniu czynności dedykowanych dla tych opcji wracamy do menu, gdzie znów możemy wybrać jedną z opcji lub zakończyć program. Konstrukcja takie aplikacji może opierać się na pętli while, której warunek zwraca wartość True, kiedy wybieramy opcje "Exit/Wyjście", no właśnie co się dzieje? Możliwości są trzy.
- Możliwość 1: Wybór konkretnej opcji przypisuje do zmiennej określoną wartość, pętla wykonuje się dopóty, dopóki wyżej wymieniona zmienna jest nie równa wartości przypisywanej po wyborze opcji "Exit/Wyjście".
- Możliwość 2: Przed rozpoczęciem pętli deklarowana zmienna tzw. flaga, której przypisywana jest wartość True. Pętla wykonuje się do momentu gdy wartość wartość flagi jest równa True. Wybranie opcji "Exit/Wyjście", zmienia wartość flagi na False, co powoduje nie spełnienie warunku, czego następstwem jest przerwanie pracy pętli.
- Możliwość 3: Kod zdefiniowany pod opcją "Exit/Wyjście" zawiera tylko
jedną instrukcję słowo kluczowe
break
. Instrukcja ta powoduje zatrzymanie wykonania bloku kodu i przejście wykonania na pierwszą instrukcję po bloku kodu pętli.
Istotną rzeczą dla każdej iteracji jest możliwość jej zrestartowania,
kiedy gdzieś w kodzie pętli zostanie umieszczone słowo kluczowe
continue
, spowoduje powrót do
sprawdzenia warunku a następnie wykonania bloku pętli od początku. To
polecenie przydatne może być gdy chcemy pominąć jakieś elementy
Przy pracy z pętlami while warto pamiętać o prawidłowym warunku, zmianie licznika czy o zakończeniu pętli w odpowiednim momencie. Patrząc na pętle, o której mówiłem akapit wcześniej, odnośnie sterowania programem to są to pętle, które bez odpowiednich mechanizmów zatrzymujących będą wykonywać się w nieskończoność. Warunek nie zawiera licznika. Zmienna w teście warunkowym bez mechanizmów stopujących nie zmieni swojej wartości. Chociaż i w pętlach while opartych na licznikach też łatwo o nieskończoną pętle. Wystarczy zapomnieć zmienić licznik na końcu bloku.
Pętla while również nadaje się do pracy z listami czy słownikami, jedną taką ciekawą rzeczą jeśli chodzi o listy jest taki mechanizm, że jeśli umieścimy w warunku pętli nazwę listy, to pętla będzie wykonywać się dopóki na tej liście będą jakieś elementy, mechanizmem kontroli tutaj będzie pobieranie elementów listy za pomocą metody pop(). Jak w każdym innym teście warunkowym w warunku pętli while również istnieje możliwość użycia operatora in
1.18. Funkcje
Funkcje służą do przygotowania bloku kodu , który możemy później wykorzystać wielokrotnie w części głównej programu. Funkcje ułatwiają zarządzanie kodem, testowanie oraz ewentualne debugowanie z racji tego, iż jest to wydzielony fragment kodu, który możemy wykonywać niezależnie od pozostałej części programu.
1.18.1. Definiowanie funkcji
Definiowane funkcji rozpoczynamy od słowa kluczowego
def
następnie podajemy nazwę funkcji po
nazwie umieszczamy bez spacji
parę nawiasów okrągłych po nawiasach oczywiście występuje dwukropek.
W nowej linii od pojedynczego tabulatora rozpoczynamy blok kodu funkcji.
1.18.1.1 Komentarze w funkcji
Komentarze w funkcji umieszcza się jako tzw. docstring, którego zadaniem jest opis działania funkcji. Wszystko co zostało ujęte pomiędzy parą potrójnych cudzysłowów ("""...""") jest traktowane jako docstring.
def hello_user(): """To jest funkcja, która wita użytkownika"""
Docstring wykorzystywany jest przez Python do tworzenia opisów funkcji podczas generowania dokumentacji dla poszczególnych projektów.
1.18.1.2. Parametry funkcji
Bardziej zaawansowane funkcje, które mają nieco więcej czynności niż wypisane "Witaj świecie!", zazwyczaj potrzebują jakiś danych z zewnątrz. Dane, parametry funkcji deklarujemy w nawiasie zaraz obok nazwy funkcji, w ten sposób zaznaczamy że nasza funkcją będzie wymagać jakiś informacji do działania.
def hello_user(username): """To jest funkcja, która wita użytkownika""" print(f"Witaj, {username}!")
Prawdziwe dane przekazujemy w momencie wywołania funkcji w części głównej. Wywołanie funkcji jest realizowane poprzez podanie jej nazwy w raz parą nawiasów okrągłych, w których w zależności od definicji umieszczamy dane niezbędne do jej wykonania. Oczywiście jeśli funkcja nie potrzebuje żadnych danych, para nawiasów pozostaje pusta.
uname = 'xf0r3m' hello_user(uname)
1.18.1.3. Argumenty pozycjne
Zmienne lub też dane umieszczone w wywołaniu funkcji nazywamy argumentami. Argumenty może przekazać do funkcji na dwa różne sposoby. Pierwszy sposób to sposób klasyczny, sposób argumentów pozycyjnych, opera się on kolejności. W jakiej kolejności parametry zostały podane w definicji funkcji, to w takiej samej kolejności należy przekazać do funkcji argumenty.
def func1(para1, para2, para3): print(f"{para1} {para2} {para3}") func1('Ala', 'ma', 'kota') # Ala ma kota
Python nie zwróci błędu jeśli pomylimy kolejność. Ale wynik działania funkcji może być nieoczekiwany.
def func1(para1, para2, para3): print(f"{para1} {para2} {para3}") #arg1=param3, arg2=param2 arg3=param1 func1('kota', 'ma', 'Ala') # kota ma Ala
1.18.1.4. Argumenty w postaci słów kluczowych
Drugą metodą jest przekazywanie argumentów w postaci słów kluczowych. Opiera się ona zasadzie par "nazwa=wartość", w tym przypadku "parametr=argument". Tutaj kolejność nie ma znaczenia. Ponieważ już podczas wywołania przypisujemy parametrowi wartość argumentu.
def func1(para1, para2, para3): print(f"{para1} {para2} {para3}") func1(para3='kota', para1='Ala', para2='ma') # Ala ma kota
Może zdarzyć się że musimy przygotować funkcję dla typowych, ciągle powtarzających się danych, które mogli byśmy podać już podczas definicji funkcji. A jeśli zajdzie taka potrzeba będziemy mogli nadpisać wartość domyślną parametrów funkcji dla innych danych.
def func1(para3, para1='Ala', para2='ma'): print(f"{para1} {para2} {para3}" func1('psa') # Ala ma psa func1('kota') # Ala ma kota func1('nierówno pod sufitem') # Ala ma nierówno pod sufitem func1('w LoL-a', 'Tola', 'gra') # Tola gra w LoL-a
Jak można zauważyć parametry w definicji funkcji zmieniły kolejność, jest wymagane przez Python, aby parametry, które będą przekazywane jako argumenty pozycyjne zawsze były na pierwszym miejscu i tak też należy przekazywać argumenty.
1.18.1.5. Zwracanie wartości przez funkcje
Funkcje często są wykorzystywane do obliczeń. Niestety co nam po
obliczonych wartościach kiedy pozostają one w bloku kodu funkcji. Możemy
oczywiście zwrócić wartość uzyskaną przez funkcje. Służy do tego słowo
kluczowe return
.
def pitagoras_need_c_square(a,b): return (a**2 + b**2) c_square = pitagoras_need_c_square(3,4) print(c_square) # 25
Jak widać przy zwrocie wartości wywołanie funkcji musimy przypisać do zmiennej.
1.18.2. Przekazywanie do funkcji złożonych typów dancyh
Do funkcji możemy przekazać nie tylko pojedyncze dane oraz zmienne, ale również całe listy i składniowo nie rożni się to przekazania pojedynczych danych.
def iteration_via_list(l): for list_element in l: print(f"{list_element}")
Warto zaznaczyć, że tak jak przekazujemy zmienne, to przekazujemy ich wartość. W przypadku listy prawdopodobnie zamiast wartość przekazywane jest miejsce w pamięci rozpoczęcia listy, przez to lista zdefiniowana w głównej części programu może zostać przez tą funkcję zmodyfikowana. Istnieje technika dzięki, której możemy się przed tym uchronić, możemy przekazać kopię listy (wycinek od pierwszego do ostatniego elementu).
received_cars = ['audi', 'bmw', 'toyota', 'subaru']; repaired_cars = []; def repair_car(rcev_cars, rep_cars): while rcev_cars: repaired_car = rcev_cars.pop() print(f"Naprawiam: {repaired_car}") rep_cars.append(repaired_car) def print_raport(rcev_cars, rep_cars): print("Otrzymano następujące samochody: ") for rcev_car in rcev_cars: print(f"\t - {rcev_car}") print("\nNaprawiono następujące samochody: ") for rep_car in rep_cars: print(f"\t - {rep_car}") repair_car(received_cars[:], repaired_cars) repaired_cars.reverse() print_raport(received_cars, repaired_cars)
O ile nie istnieje żaden ważny powód należy przekazywać funkcjom prawdziwą listę, użycie kopii może być mniej efektywne oraz zajmować więcej pamięci.
1.18.3. Przekazywanie nieokreślonej liczy argumentów
Podczas prac z kodem możemy spotkać się z takim problemem że nie będziemy potrafili przewidzieć ile może być potrzebnych argumentów. W Pythonie możliwe jest przekazanie dla parametru dowolnej liczby argumentów. Taki parametr definiuje się z gwiazdka (*) przed nazwą. Przekazanie kilku argumentów pod ten parametr powoduje utworzenie krotki z tymi argumentami, przez które możemy normalnie iterować.
def make_pizza(*toppings): print("Tworze pizzę: ") for toping in topings: print(f"\tDodaję: {topping}") make_pizza('ser', 'szynka', 'pieczarki')
Nic nie stoi na przeszkodzie aby użyć np. argumenty pozycyjnego wraz z krotką argumentów. Należy oczywiście pamiętać o kolejności. Python zwróci błąd ponieważ za parametrem tego typu nie powinny znajdować się już żadne inne parametry.
Istnieje również metoda, która umożliwi przekazanie wielu argumentów jako słów kluczowych. Podobnie do powyższego przypadku wykorzystane są gwiazdki (**), dwie a nie jedna. Po przekazaniu par nazwa-wartość w funkcji zostanie utworzony słownik.
def make_car(make, model, **car): car2={} car2['make'] = make.title() car2['model'] = model.title() for k,v in car.items(): car2[k]=v return car2 car = make_car('subaru', 'outback', color='blue', towPackage=True) print(car)
Ten przykład jest nieco bardziej skomplikowany. Przy korzystaniu z
przekazywania wielu argumentów za pomocą słów kluczowych należy pamiętać
o tym że dane w słowniku są ułożone według kolejności dodawania
danych do słownika (Ta
zależność pojawiła się dopiero od Pythona >=3.7). W tym
przypadku dane do słownika przekazane przez '**car' trafiają
jeszcze przed przejściem do wykonywania kodu funkcji. Dlatego jeśli
chcemy mieszać argumenty jak na powyższym przykładzie, należy o tym
pamiętać. Poprzez utworzenie nowego słownika i na początku dodanie
argumentów pozycyjnych następnie przepisanie danych ze słownika
car
do słownika
car2
danych dodanych jako wielu
argumentów słów kluczowych. W ten sposób uzyskaliśmy słownik o kolejności
identycznej jak argumenty podane w funkcji.
1.19. Moduły
W Pythonie w prosty sposób możemy utworzyć tzw. moduły w innych językach możemy nazwać je np. plikami nagłówkowymi. Tworzymy je poprzez utworzenie oddzielnego pliku i wpisanie tam wszystkich funkcji. Kiedy mamy funkcje w oddzielnym pliku pozostaje je tylko zaimportować. Poniżej znajdują się metody importu, wraz z przykładem użycia funkcji z zaimportowanego modułu.
#import sandwich #sandwich.make_sandwich('pszenne', 'szynka', 'ser', 'ketchup') #from sandwich import make_sandwich #make_sandwich('pszenne', 'szynka', 'ser', 'ketchup') #from sandwich import make_sandwich as fn #fn('pszenne', 'szynka', 'ser', 'ketchup') #import sandwich as mn #mn.make_sandwich('pszenne', 'szynka', 'ser', 'ketchup') #from sandwich import * #make_sandwich('pszenne', 'szynka', 'ser', 'ketchup')
Możemy używać słowa kluczowego import
do zaimportowania całego modułu, lub zaimportować konkretną funkcję z
danego modułu poprzez kombinacje słów kluczowych
from
oraz import. Gdzie po
słowie from podajemy nazwę modułu, po słowie import
podajemy nazwę funkcji. Ciekawą techniką jest tworzenie
aliasów dla zaimportowanych modułów lub funkcji. Po
nazwie modułu lub funkcji oraz po słowie kluczomym
as
podajemy alias czyli taką nazwę
zastępczą lub pseudonim. Tworzenie aliasów przydaje się gdy np. mamy
podejrzenie, że funkcja w module ma taką samą nazwę jak funkcja w
głównej części programu. Linia przed ostatnia z powyższego przykładu
mogą wydawać się równoznaczne, ale ostatnii przykład pokazuje nam w
jaki sposób możemy zaimportować funkcje z modułu. Ta czynność pozwala
na pominięcie prefixu nazwy modułu
1.20. Programowanie zorientowane obiektowo
Programowanie zorientowane obiektowo jest metodą, która pozwala programistom przedstawiać przy użyciu komputera rzeczy znane nam z realnego świata. Weźmy takiego człowieka. Człowiek jest obiektem zarówno w znanym nam świecie, jak i również może zostać przedstawiony jako obiekt w świecie wirtualnym. Jeśli człowiek staje się obiektem w ujęciu programistycznym, to jego cechy szczególne stają się właściwościami/atrybutami obiektu. Czynności jakie wykonuje mogą być metodami.
Człowiek jest przedstawicielem gatunku. Gatunku ludzkiego. Obiekty są również przedstawicielami gatunku jaki sobie zdefiniujemy. Tym gatunkiem są klasy.
Definicje klasy rozpoczynamy od słowa kluczowego
class
następnie podajemy
nazwę tej klasy oraz zaraz obok nazwy bez spacji parę nawiasów
okrągłych oraz dwukropek. W następnej linii po jednym znaku tabulacji
możemy rozpoczynać definicję klasy. Klasa z poziomu kodu źródłowego
składa się głównie z funkcji, które tutaj nazywają się metodami.
Pierwszą definiowaną metodą jest metoda
__init__
. Uwaga, przy
zapisie tej nazwy należy uważać na to, że jest ona obudowana dwoma
znaki podkreślenia (__). Ta metoda jest metodą
specjalną, ma za zadanie zainicjować właściwości zapisane w jej bloku.
Te atrybuty będą atrybutami obiektu. Argumenty dla tej metody są
przekazywane podczas tworzenia egzemplarza klasy, czyli obiektu.
class User(): def __init__(self,firstname,lastname,age,sex): self.firstname = firstname self.lastname = lastname self.age = age self.sex = sex
Parametr self
jest obiektem, który
będzie tworzony na podstawie tej klasy. Słowo kluczowe self
będzie oznaczać po prostu obiekt tej klasy. To atrybutowi obiektu tej
klasy w metodzie __init__ nadajemy wartość parametru
firstname
. Z racji tego że obiekt jest
egzemplarzem klasy stąd słowo self (samemu sobie).
1.20.1. Objekt
Egzemplarz klasy (obiekt) tworzymy nadając zmiennej wartości w postaci wywołania zdefiniowanej wcześniej klasy.
user1 = User('Jan', 'Nowak', '47', 'M')
Argumentami przekazywanymi klasie są tak naprawę argumenty dla metody __init__, ponieważ to ona jest uruchamiana podczas tworzenia obiektu. Argument self jest przekazywany automatycznie.
Powiedzmy że mamy utworzy obiekt 'user1'. Dostęp do jego właściwości
uzyskujemy dzięki notacji kropki (.
).
print(f"Witaj {user1.firstname} {user1.lastname}") # >>> Witaj Jan Nowak
1.20.2. Metody
Mamy już cechy szczególne, to teraz pora na czynności jakie ten obiekt może wykonać. Do opisu czynności obiektu wykorzystuje się metody, czyli funkcje w kontekście obiektowym. Z pierwszą metodą mieliśmy już do czynienia przy okazji metody __init__, może jest ona nieco przezroczysta, jednak to nadal metoda. W poniższym przykładzie stworzymy sobie metodę, która opisze nam tego użytkownika, bo na razie właściwości to suche dane.
def describe_user(self): print(f"Imie i nazwisko: {self.firstname} {self.lastname}") print(f"\t-wiek: {self.age}\n\t-płeć: {self.sex}")
Metoda tym różni się od funkcji, że znajduje się wewnątrz klasy oraz tym, że zawsze ma parametr self, który pozwala jej na dostęp do właściwości obiektu. Metody wywołujemy prawie przez cały czas pod czas zabawy Pythonem, więc wywołanie metody naszej klasy nie powinno być problemem.
user1.describe_user()
1.20.3. Praca z atrybutami obiektów.
Właściwości/atrybuty obiektu nie muszą być zawsze przekazywane przez użytkownika podczas tworzenia obiektu mogą przyjmować wartości domyślne w zdefiniowane wewnątrz metody __init__.
class User(): def __init__(self, firstname, lastname, age, sex): self.firstname = firstname self.lastname = lastname self.age = age self.sex = sex self.rank = 'Junior user'
Wraz z biegiem programu opartego na obiektach ich właściwości mogą, czasami wręcz muszą się zmienić. W Pythonie możemy to zrealizować na trzy sposoby.
- Sposób 1: Zmiana bezpośrednia. Po prostu bierzemy obiekt następnie za pomocą notacji kropki uzyskujemy dostęp do właściwości i za pomocą operatora przypisania (=) nadajemy atrybutowi nową wartość. Ta technika generalnie nie jest polecana.
- Sposób 2: Nowa metoda przeznaczona do aktualizowania wartości poszczególnych właściwości. Tworzymy metodę w parametrem self oraz z parametrem, który będzie przechowywał nową wartość metody, w bloku w metody do atrybutów uzyskujemy dostęp poprzez parametr self oraz notację kropki i nadajemy mu nową wartość parametru, która ma zostać przekazana w postaci argumentu z głównej części programu.
- Sposób 3: Nowa metoda inkrementująca lub dekrementując wartość właściwość. Ta metoda od tej ze sposobu nr. 2 różni się w zasadzie tylko znakiem w tym wypadku jest to += a nie =. Wartość o jaką będziemy zmieniać naszą właściwość również podajemy w postaci parametru. Oczywiście może to zależeć od metody.
Sposób 1:
user1.rank = 'Standard user'
Sposób 2:
class User(): def __init__(self, firstname, lastname, age, sex): self.firstname = firstname self.lastname = lastname self.age = age self.sex = sex self.rank = 'Junior user' def promote_user(self, new_rank): self.rank = new_rank user1 = User('Jan', 'Nowak', '47', 'M') user1.promote_user('Standard user')
Sposób 3:
class User(): def __init__(self, firstname, lastname, age, sex): self.firstname = firstname self.lastname = lastname self.age = age self.sex = sex self.rank = 'Junior user' def promote_user(self, new_rank): self.rank = new_rank def happy_birthday(self): print(f"Happy Birthday {self.firstname.title()}") self.age += 1 user1 = User('Jan', 'Nowak', '47', 'M') user1.promote_user('Standard user') user1.happy_birthday()
1.20.4. Dziedziczenie
Mamy sobie naszych użytkowników. Jednak potrzebujemy specjalnego użytkownika, który będzie tym wszystkim zarządzał - Administratora. Problem napotykamy wtedy kiedy chcemy zdefiniować jakieś ekstra metody dla admina, ale nie chcemy umieszczać ich w tej samej klasie do definiowania użytkowników, musimy również pamiętać, że admin to też użytkownik, i wypadało by mieć w bazie jego dane. Przekopiowanie kodu jest nieefektywne, ale z pomocą przechodzi na dziedziczenie, czyli tworzenie klas potomnych na podstawie istniejącej już klasy.
Przy tworzeniu klas potomnych warto pamiętać o jednej rzeczy. Klasa potomna jest to oddzielna klasa zawierająca połączenie z klasą nadrzędną. Klasy potomne mogą inicjować dla swoich obiektów właściwość klasy nadrzędnej oraz obiekty klas potomnych mogą korzystać z metod klasy nadrzędnej. Definicja takiej klasy wygląda następująco.
class User(): def __init__(self, firstname, lastname, age, sex): self.firstname = firstname self.lastname = lastname self.age = age self.sex = sex self.rank = 'Junior user' self.quota = 100_000_000 def promote_user(self, new_rank): self.rank = new_rank def happy_birthday(self): print(f"Happy Birthday {self.firstname.title()}") self.age += 1 def check_quota_type(self): if self.quota < 0: print('Unlimited') else: print('100MB') class Admin(User): def __init__(self, firstname, lastname, age, sex): super().__init__(firstname, lastname, age, sex) self.quota = -1 self.rank = 'Admin' def ban_user(self, username): print(f'Użytkownik {username} został zbanowany')
Cechami po których można poznać że dana klasa jest klasą potomną, jest to,
że w parametrach klasy znajduje się nazwa klasy nadrzędnej. W metodzie
__init__
występuje metoda
super()
, której celem jest realizowanie
połączenia pomiędzy klasą potomną a klasą nadrzędną. Użycie metody
super() zależy od konstrukcji klasy nadrzędnej. Utworzenie
takiego obiektu różni się tylko nazwą klasy.
root = Admin('Jan' 'Kowalski', 25, 'M') root.happy_birthday() # Happy Birthday Jan root.check_quota_type() # Unlimited
Atrybuty mogą przechowywać wiele rzeczy, od pojedynczych wartość po obiekty innych klas. Istnieje możliwość wewnątrz klasy przekazać atrybutowi jako wartość egzemplarz obiektu. Rozważmy to że musimy każdemu z naszych użytkowników zdefiniować uprawnienia, co im wolno, a co nie. Możemy spojrzeć na zestaw uprawnień jak na obiekt.
class User(): def __init__(self, firstname, lastname, age, sex): self.firstname = firstname self.lastname = lastname self.age = age self.sex = sex self.rank = 'Junior user' self.quota = 100_000_000 self.permissions = Permissions('User') def promote_user(self, new_rank): self.rank = new_rank def happy_birthday(self): print(f"Happy Birthday {self.firstname.title()}") self.age += 1 def check_quota_type(self): if self.quota > 0: print('Unlimited') else: print('100MB') class Permissions(): def __init__(self, user_type): if user_type == 'Administrator': self.permissions = ['może dodać post', 'może usunac post', 'może zbanować użytkownika'] else: self.permissions = ['może dodać post', 'może usunać post'] def show_permissions(self): for permission in self.permissions: print(f"- {permission}") class Admin(User): def __init__(self, firstname, lastname, age, sex): super().__init__(firstname, lastname, age, sex) self.quota = -1 self.rank = 'Admin' self.permissions = Permissions('Administrator') def ban_user(self, username): print(f'Użytkownik {username} został zbanowany') root = Admin('Jacek', 'xf0r3m', 25, 'M') root.permissions.show_permissions() print('\n') user1 = User('Jan', 'Nowak', 47, 'M') user1.permissions.show_permissions()
W powyższym przykładzie pokazano jak można użyć obiektu jako wartości
atrybutu innego obiektu oraz jak uzyskać dostęp do metod takiego obiektu.
root
jest obiektem w klasie
Admin
, następnie po kropce podajemy
nazwę atrybutu w tym przypadku jest
permissions
, aby po następnej kropce
(z racji tego że wartość permissions
to
też obiekt) podajemy nazwę metody
show_permissions()
i w ten sposób
uzyskaliśmy dostęp do zdefiniowanych w innym obiekcie uprawnień
przypisanych do obiektów użytkowników jako atrybuty.
Klasy podobnie jak funkcje też można umieszczać w modułach, klasy umieszcza się w modułach w identyczny sposób jak funkcje. Identycznie też się je importuje. Kilka akapitów wyżej jest opis jak to zrobić.
1.21. Biblioteka standardowa
Wiele modułów zwierających pomocne klasy oraz funkcje jest dostarczanych
wraz z Python jako standardowe biblioteki. Wśród nich możemy znaleźć taki
moduł jak random
, który zawiera funkcje
służące do generowania liczb pseudolosowych -
randint()
oraz np.
choice()
, która zwraca nam losowo
wybraną wartość z listy lub krotki.
>>> from random import choice >>> cars = ['Shelby Cobra', 'Chevy', 'Pontiac', 'Dodge', 'Mustang'] >>> today_car = choice(cars) >>> today_car 'Dodge'
1.22. Obsługa plików
Python tak wiele języków programowania umożliwia pracę z plikami. Dostęp do pliku uzyskujemy w dość dziwny, ale bezpieczny (dla pliku) oraz efektywny sposób.
with open('filename.txt') as file_object
Samemu otwarciu pliku służy funkcja open()
,
jednak jeśli skorzystalibyśmy z niej bez tej specyficznej obudowy, po
zakończeniu wykonywania na nim jakiś operacji, musielibyśmy go zamknąć.
Tylko pytanie, kiedy? Po ostatniej czynności na nim wykonanej? Nie
wiadomo. Dlatego też w Pythonie pliki otwiera się za pomocą słowa
kluczowego with
, które to zamyka plik
kiedy przestaje być potrzebny, czy to naszemu programowi, czy to
Pythonowi podczas interpretacji kodu. Sekcja
as file_object
, będzie nam przedstawić
odniesie do pliku jako obiekt file_object. Wielu przypadkach
możemy spotkać się ze skróceniem file_object do
f. Czynności wykonywane na pliku są umieszczane w bloku
kodu pod linią otwarcia pliku z użyciem słowa kluczowego with.
W powyższym przykładzie, wewnątrz funkcji
open()
znajduje się argument o nazwie
filename.txt
, jest to ścieżka dostępowa
do pliku, która może być względna - odnosić się od danego miejsca, lub
też bezwzględna - pełna ścieżka od najwyższego katalogu na systemie
plików w systemach uniksopodobnych (root, katalog głowny -
/), w systemach MS Windows (Katalog dysku np.
c:\ itp.). Argument filename.txt
jest ścieżką względną, ponieważ odnosi się od CWD (obecnego katalogu
roboczego - katalogu, w którym się znajdujemy, mamy otwarte IDE lub w
katalogu lub w którym wykonuje się skrypt Pythona). W MS Windows
stosujemy ten sam ukośnik (/) co w systemach uniksopodobnych,
lub jeśli się już upieramy przy Windowsowych lewych ukośnikach
(\), to musimy podać je podwójnie, ponieważ lewy ukośnik
w Python oznacza rozpoczęcie białego znaku.
Dane z plików możemy odczytać na trzy różne sposoby.
- Sposób 1 - Ściągnięcie całej zawartości pliku jako ciągu tekstowego do zmiennej.
- Sposób 2 - Iteracja przez odniesienie do pliku.
- Sposób 3 - Przechowanie pliku jako lista wierszy występujących w pliku.
Sposób 1:
with open('filename.txt') as f: plik = f.read()
Warto zwrócić uwagę na dodatkowy ostatni pusty wiersz.
Metoda read()
po napotkaniu końca pliku
zwraca pusty wiesz. Z racji tego, że pusty wiersz (znak przejścia do
nowej linii '\n') to biały znak, cały plik ściągnęliśmy do
zmiennej w postaci pojedynczego ciągu tekstowego, to oznacza to, że znak
znajduje się po prawej stronie ciągu, więc za pomocą metody operującej na
ciągach tekstowych rstrip() możemy usunąć ten pusty wiersz.
Sposób 2:
with open('filename.txt') as f: for line in f: print(f"{line.replace('Python', 'C')}")
Metoda replace() zamienia wszystkie wystąpienia w ciągu pierwszego argumentu na drugi argument.
Sposób 3:
with open('filename.txt') as f: plik = f.readlines() for linia in plik: print(f"{linia.replace('Python', 'C')}")
Metoda readlines()
zwraca nam listę
wierszy znajdujących się w pliku.
1.22.1. Funkcja open
Funkcja open() pozwala na wybranie atrybutu dla otwieranego pliku.
- 'r' - Atrybut domyślny, otwiera tylko plik do odczytu.
- 'w' - Otwiera plik do zapisu. Jeśli nie może znaleźć pliku, nie generuje błędu. Tworzy nowy plik pod podaną funkcji ścieżką. Jeśli plik o takiej nazwie już istnieje to zostaje nadpisany.
- 'a' - Otwiera plik do dopisywania. Dopisuje dane do pliku. Nie nadpisuje danych w pliku. Podobnie jak atrybut w w przypadku braku pliku na ścieżce, to zostanie o utworzony.
- 'r+' - Rozszerza otwarcie pliku do odczytu o możliwości dopisania danych.
Zapis danych w pliku polega na otwarciu pliku do zapisu lub dopisywania używając wyżej wymienionych atrybutów jako drugiego argumenty funkcji open(). Plik nie musi istnieć na podanej ścieżce, zostanie utworzony w momencie zamknięcia odniesienia prze słowo kluczowe with
with open('filename.txt', 'w') as f: f.write('xf0r3m') with open('filename.txt') as f: plik = f.read() print(f"{plik}") # xf0r3m
1.23. Obsługa błędów
Python posiada konstrukcję do obsługi błędów, to znaczy, że jeżeli w
pewnym bloku umieścimy kod (technicznie, umieszcza się tam tylko jedną
linię do której nie możemy być pewni, że uruchomienie jej nie spowoduje
błędu), to jeśli spowoduje on błąd określony w drugiej części
konstrukcji, zostanie wykonany kod z bloku tej drugiej części. Tą
konstrukcją jest try-except
. Koronnym
przykładem jest chyba dzielenie przez zero.
try: print(8 / 0) except ZeroDivisionError: print("Nie wolno dzielić przez 0")
Jeśli kod w bloku try wykona się bez błędu, to zostanie
wyświetlony wynik z dzielenia. Jednak nigdy nie się to nie stanie.
Ponieważ wykonanie linii print(8/0)
spowoduje wygenerowanie błędu division by zero. Błędy w
programowaniu możemy nazwać wyjątkami od wykonania. Każdy błąd jest
częścią obiektu wyjątku w tym przypadku obiektem wyjątku
dla błędu jest ZeroDivisionError
. Skąd
to wiadomo ? Otóż z komunikatów zwróconych przez sam Python jako
stos wywołań. Ostatnia linia komunikatu o błędzie.
Mówi nam: 'obiekt wyjątku: błąd'. Aby obsłużyć błąd,
w linii except podajemy obiekt wyjątku, następnie w jej
bloku definiujemy kod, który ma się wykonać kiedy Python napotka błąd
tego typu. Jeśli potrzebujemy obiektu wyjątku, to spróbujmy zmusić tę
niepewną linię do wygenerowania błędu bez jego obsługi. Python poda nam
nazwę obiektu wyjątku.
W skład tej konstrukcji wchodzi jeszcze
else
, w tym bloku definiowany jest kod,
który jest wykonywany, kiedy kod w bloku try nie spowoduje
błędu.
a = input("Podaj pierwszą liczbę: ") b = input("Podaj drugą liczbę: ") try: result = int(a) + int(b) except ValueError: print("Jedna z podanych wartości jest tekstem nie liczbą!") else: print(f"{result}")
Często będzie tak, że nie będziemy chcieli wykonywać żadnego kodu, ani informować nikogo o tym, że wystąpił wyjątek. Wtedy możemy skorzystać ze słowa kluczowego pass, którego celem jest rezerwacja miejsca w bloku, na potrzeby późniejsze rozbudowy. Użycie instrukcji pass możemy tłumaczyć jako do nothing. Z racji tego, że instrukcja pass pozwala na ciche przyzwolenie na błędy. Musimy zdecydować kiedy użytkownik naszego programu powinien być informowany o błędzie. Na pewno wtedy gdy wyjątek powoduje jakaś informacja, która pochodzi od niego samego.
1.24. Wykorzystanie formatu JSON w Pythonie
Z racji tego iż mamy omówione pliki oraz wyjątki, czyli możemy definiować
kod, który obsłuży sytuacje, gdy próbujemy odczytać plik który nie
istnieje. Możemy zapisywać dane z naszych programów do pliku, a przy
następnym uruchomieniu je wczytywać. Do tego celu wykorzystamy moduł
json
, który umożliwia użycie formatu
JSON (JavaScript Object Notation) do przechowywania danych. A w
szczególności dwie funkcje, dump()
oraz
load()
.
Funkcja dump() jak argumenty pobiera, dane (jako pierwszy argument pozycyjny) np. zmienną, równie dobrze może być to lista. Drugim argumentem jest odniesie do pliku.
import json username = 'xf0r3m' with open('filename.json', 'w') as f: json.dump(username,f)
Dane zostaną za pisane w pliku. Nie koniecznie w formacie JSON jaki znamy z JavaScript. Idealnie funkcja dump() na JSON przerabia słowniki, którym chyba najbliżej do tego formatu. Odczyt danych z takiego pliku jest wykonywany jest za pomocą funkcji load().
import json with open('filename.json') as f: username = json.load(f) print(f"{username}) # xf0r3m
1.25. Testy jednostkowe
Python za pomocą modułów biblioteki standardowej umożliwia nam przeprowadzenie testów jednostkowych dla stworzonych przez nas funkcji oraz klas. Jeśli chcemy tworzyć naprawdę dobry kod, i to nie zależnie od języka powinniśmy go testować. Nie mówię o tworzeniu testów jednostkowych dla hello world, ale kiedy nasz projekt dość pokaźnie urósł. Zawiera klasy lub/i funkcję, to powinien on zawierać testy tych konstrukcji. Na czym polega test jednostkowy? Najprościej rzecz biorąc na sprawdzeniu czy wartość zwracana przez funkcje lub atrybut modyfikowany przez metodę jest równy z predefiniowaną przez nas wartością, którą uznajemy za poprawną. Takie porównanie nazywamy asercją. Zakładamy, że wartość użyta do asercji jest równa zdefiniowanej wartości. Jeśli tak jest to test zostaje zaliczony, w przeciwnym wypadku wykonanie zostaje przerwane (w przypadku asercji jako funkcji). Asercja w Pythonie nie organiczna się tylko do sprawdzenia równości. Poniżej znajduje się tabelka z możliwymi funkcjami asercji zaimplementowanymi w module unittest. To ten moduł odpowiada za testy jednostkowe.
Metoda | Opis |
assertEqual(a, b) | Sprawdza czy a == b |
assertNotEqual(a, b) | Sprawdza czy a != b |
assertTrue(x) | Sprawdza czy x przyjmuje wartość True |
assertFalse(x) | Sprawdza czy x przyjmuje wartość False |
assertIn(element, lista) | Sprawdza czy element jest na liście |
asserNotIn(element, lista) | Sprawdza czy element nie znajduje się na liście |
1.25.1. Testy jednostkowe funkcji
Na potrzeby przedstawienia testów zaczniemy od zdefiniowania sobie w module (city_functions.py) prostej funkcji 'city_countries', która pobiera od użytkownika nazwę miast oraz nazwę kraju, w którym to miasto leży następnie zwraca ciąg np. dla danych Warszawa Polska: Warszawa, Polska.
def city_countries(city, country): cc = f"{city.title()}, {country.title()}" return cc
Aby wywołać taką funkcje z racji tego, że jest plik modułu w nowym pliku musimy zaimportować moduł i wywołać funkcję:
from city_functions import city_countries city_countries('Warszawa', 'Polska') # Warszawa, Polska
Powiedzmy z racji tego, że jest to funkcja dużego projektu, musimy stworzyć dla niej test jednostkowy. Testy definiuje się w klasie potomnej klasy TestCase modułu unittest. Jedna metoda w wewnątrz klasy = jeden test. Taka klasa staje się zbiorem testów. Kiedy dla każdej funkcji w naszym projekcie zdefiniujemy chociaż jeden test, to możemy mówić wówczas o całkowitym pokryciu zbioru testów. Zatem dla testów jednostkowych utworzymy nowy plik, nazwiemy go test_cities.py
import unittest from city_functions import city_countries class TestCitiesFunctions(unittest.TestCase): def test_city_countries(self): cc = city_countries('Warszawa', 'Polska') self.assertEqual(cc, 'Warszawa, Polska') unittest.main()
importujemy moduł unittest
, następnie
importujemy naszą funkcję. Tworzymy klasę potomną klasy
unittest.TestCase
, w tym przypadku o
nazwie TestCitiesFunctions
. Wewnątrz
niej jako metodę definiujemy test. Funkcja
main()
modułu unittest
uruchomi automatycznie każdą metodę w zbiorze, których nazwy zaczynają
się od ciągu test_, zatem nie potrzeba jej wywoływać.
Wewnątrz metody wywołujemy naszą funkcję zachowując jej wartość zwracaną
w zmiennej cc
następnie w jednej z metod
asercji w tym przypadku assertEqual
podstawiamy otrzymaną wartość następnie wartość, którą naszym zdaniem
powinniśmy otrzymać od funkcji dla podanych w wywołaniu argumentów. Na
końcu uruchamiamy zbiór testów. Wynik testu znajduje się poniżej.
. ---------------------------------------------------------------------- Ran 1 test in 0.000s OK
W prawdzie nie wiele informacji, choć nas powinna zaciekawić pierwsza
linijka, zwracany jest w niej rezultat testu, jeśli widzimy kropkę
(.
), oznacza to że test zakończył się
sukcesem. Innymi wartościami zwrotnymi jest wielka litera
F, oznaczająca nie powodzenie asercji, otrzymaliśmy inny
wynik niż zakładaliśmy, oraz wielka litera E,
oznaczająca błąd gdzieś w kodzie. Np. jeśli rozbudowujemy funkcje o
kolejne parametry, ale już zapomnimy w teście tych parametrach.
Rozbudujemy naszą funkcję o kolejny parametr pozwalający przedstawić populacje.
def city_country(city, country, population): cc = f"{city.title()}, {country.title()} - populacja {population}" return cc
Próbując wykonać test dla tej funkcji dostaniemy o taką odpowiedź:
E ====================================================================== ERROR: test_city_country (__main__.TestCitiesFunctions) ---------------------------------------------------------------------- Traceback (most recent call last): File "/home/xf0r3m/python/test_cities.py", line 8, in test_city_country cc = city_country('Warszawa', 'Polska') TypeError: city_country() missing 1 required positional argument: 'population' ---------------------------------------------------------------------- Ran 1 test in 0.001s FAILED (errors=1)
Błąd polega na tym, że zapomnieliśmy jednym argumencie, jeśli zmienimy ten argument na opcjonalny, wtedy test zostanie pomyślnie wykonany. Poniżej zamieszczam również dane zwracane, kiedy wartość rzeczywista różni się od przez nas założonej, pojawia się wtedy błąd asercji.
F ====================================================================== FAIL: test_city_country (__main__.TestCitiesFunctions) ---------------------------------------------------------------------- Traceback (most recent call last): File "/home/xf0r3m/python/test_cities.py", line 9, in test_city_country self.assertEqual(cc, 'Warszawa, Polska') AssertionError: 'Warszawa, Polska - populacja ' != 'Warszawa, Polska' - Warszawa, Polska - populacja + Warszawa, Polska ---------------------------------------------------------------------- Ran 1 test in 0.001s FAILED (failures=1)
Oczywiście kiedy zdefiniujemy kolejne testy, to będą pojawiać się w pierwszej linijce kropki lub litery E czy F, wszystko w zależności od wyniku testu.
1.25.2. Testy jednostkowe metod
Testy jednostkowe możemy tworzyć nie tylko dla funkcji ale i dla klas (bardziej metod). Przygotowałem poniżej klasę modelującą pracownika w swoich atrybutach zawiera imię, nazwisko oraz roczne zarobki. W klasie znajduje się również metoda odpowiedzialna za przyznawanie podwyżki, domyślną wartością podwyżki jest 5000. Naszym zadaniem jest stworzenie dwóch testów sprawdzających ową metodę. Jak w przykładzie z funkcją, klasę umieszczamy w odrębnym pliku modułu.
Plik: pracownik.py
class Employee(): def __init__(self, imie, nazwisko, y_salary): self.imie = imie self.nazwisko = nazwisko self.y_salary = y_salary def give_raise(self, amount_raise=5000): self.y_salary += amount_raise
Poniżej znajduje się plik ze zbiorem testów. Dla klas tworzymy go w
identyczny sposób jak dla funkcji, z jedną różnicą. Dla poprawności testu,
każdy test powinien otrzymać oddzielny egzemplarz klasy, aby nie
powtarzać kodu tworzenia egzemplarza, z pomocą przychodzi nam metoda
unittest o nazwie setUp
.
Kiedy definiujemy taką
metodę w wewnątrz zbioru testów, powinna zawierać kod przeznaczony do
utworzenia egzemplarza danej klasy. Ta metoda będzie wywoływana przez
Python przed każdym wywołaniem testu.
Plik: test_employee.py
import unittest from pracownik import Employee class EmployeeTest(unittest.TestCase): def setUp(self): imie = 'Jan' nazwisko = 'Nowak' y_salary = 140_000 self.new_employee = Employee(imie, nazwisko, y_salary) def test_give_default_raise(self): self.new_employee.give_raise() self.assertEqual(self.new_employee.y_salary, 145_000) def test_give_custom_raise(self): self.new_employee.give_raise(25_000) self.assertEqual(self.new_employee.y_salary, 165_000) unittest.main()
Naszymi testami było sprawdzenie metody odpowiedzialnej za przyznawanie podwyżek. Pierwszy test to podwyżka o wartość domyślnej a drugi to podwyżka zdefiniowana przez użytkownika. Danymi zwracanymi przez Python dla uruchomienia tych testów są:
.. ---------------------------------------------------------------------- Ran 2 tests in 0.001s OK
1.26. Koncepcje stylu kodu źródłowego
Poniżej przedstawiam koncepcje stylu nadawanego zapisom kodu źródłowego, ponieważ Now is better than never oraz Beautiful is better than ugly.
Kilka reguł, których należy stosować pod czas używania zmiennych w Pythonie.
- W nazwach zmiennych mogą znajdować się jedynie, cyfry i znaki podkreślenia. Nazwa może rozpoczynać się od litery lub znaku podkreślenia ale nie od cyfry.
- Niedozwolone jest stosowanie spacji w nazwach zmiennych, choć znak podkreślenia można stosować w charakterze separatora.
- Należy unikać używania w nazwach zmiennych słów kluczowych Pythona oraz nazw funkcji.
- Nazwa zmiennej powinna być krótka, choć jednocześnie czytelna.
- Ostrożnie należy używać małej litery l oraz wielkiej litery O. Mogą zostać mylnie odczytane jako 1 lub 0.
Zmienne w Pythonie zapisujemy z małych liter.
Odnośnie komentarzy, to warto sobie opisywać jakieś bardziej złożone fragmenty naszego programu, żeby później, kiedy powrócimy do kodu po jakimś czasie, nie tracić czasu na próbę zrozumienia co autor miał na myśli.
Generalnie konwencje stylu, będą stosowane nie po to aby kod był łatwy do zapisu, ale do odczytu właśnie. Jest dokument w sieci nazywa się PEP 8 (Python Enhancement Proposal), który opisuje sposoby nakładania stylu na tworzony kod. Pierwszą rzeczą którą jest podczas zapisu kodu łatwego do odczytu, są wcięcia. W Pythonie sprawa jest nieco paradoksalna, ponieważ nie możemy sobie szastać wcięciami na prawo i lewo, ponieważ wcięcia często oznaczają blok kodu, ale jednocześnie przez to Python sam nakazuje nam stosowanie wcięć przy definiowaniu bloku kodu. Specyfikacja PEP 8 zaleca użycia czterech spacji jako jednego poziomu wcięcia. Najnowsze wersje edytorów ustawiają jeden znak tabulacji na wielkość czterech spacji. Kolejną rzeczą jest długość wiersza, która nie powinna przekraczać 80 znaków takie zalecenie później ułatwi nam pracę kiedy na jednym ekranie będziemy musieli mieć podzielone okno edytora na kilka plików naraz. W oknie edytora możemy zobaczyć taką pionową kreskę, właśnie ona stoi na miejscu 80-tego znaku. Taka linia powinna być już włączona, jeśli nie jest to zawsze można ją włączyć. Do organizacji kodu w schludny plik tekstowy mogą pomóc nam puste wiersze. Nie należy ich jednak nadużywać. Metodę jaką ja stosuje jest rozdzielanie kodu ze względu na jego kontekst. Np. mam sobie wywołanie funkcji, która zwraca mi wartość do zmiennej - jest jedna linia. Teraz chcę to wypisać - to jest druga linia operująca na tych samych danych, czyli umieszczam ją jedna pod drugą. Kiedy muszę wywołać podobnie inną funkcję muszę robię linię odstępu przed kolejnymi instrukcjami.
PEP 8 opisuje również styl dla testów warunkowych. W tego zalecenia powinniśmy rozdzielać operandy od operatora spacją, zarówno w test prostych jak i złożonych.
Podczas nadawania stylu funkcjom powinniśmy pamiętać o kilku kwestiach. Nazwa funkcji powinna sugerować jej przeznaczenie i składać się z małych liter i znaków podkreślenia. W kodzie każdej funkcji powinien znajdować się komentarz zaraz na początku bloku kodu funkcji, dla komentarza należy użyć formatu docstring. W przypadku definiowania parametrów domyślnych lub też opcjonalnych po obu stronach znaku równości nie należy umieszczać żadnych spacji. Jeżeli parametry w definicji nam się nie mieszczą 80 znakach, to możemy bez trudu złamać tą linię za przecinkiem, dać dwa znaki tabulatora aby nie zostało to potraktowane jako blok funkcji i kontynuować zapisywanie parametrów na końcu zamykając nawias, podając znak dwukropka. Jeśli przechowujemy naszą funkcję w module i ta funkcja nie jest jedyna w tym module, to możemy poszczególne funkcje w module rozdzielić dwoma pustymi wierszami.
Nazwa klasy powinna stosować styl CamelCase: każde słowo nazwy pisane wielką literą, brak znaków podkreślenia. Z kolei nazwy egzemplarzy klas powinny być zapisane małymi literami, a wykorzystanie w tych nazwach znaków podkreślenia między słowami jest dopuszczalne. Na początku każdej klasy należy umieścić komentarz typu docstring wyjaśniający przeznaczenie klas znajdujących się w danym module. W kodzie klasy umieszczamy pusty wiersz pomiędzy metodami. Klasy w module rozdzielamy dwoma pustymi wierszami.
Importując moduły z biblioteki standardowej oraz swoje, w pierwszej kolejności należy podać polecenia import modułów biblioteki a dopiero później swoje moduły.
Ostatnim pojęciem ze stylów będzie refaktoryzacja. Jest to podział dużego kodu na funkcje wykonujące zazwyczaj konkretne zadanie.
Tutaj kończy się część pierwsza, czyli poznawanie Pythona, jego składni, poleceń i konstrukcji. Teraz przejdziemy do konkretnego zagadnienia, gdzie raczej będziemy analizować studium przypadku. Przypadku tworzenia aplikacji internetowej, podobnej do tej jaką tworzy się za pomocą takich języków jak np. PHP.
Koniec części pierwszej.
Część 2: Aplikacje internetowe
Django to zestaw narzędzi (czyt. framework) pozwalający na tworzenie aplikacji internetowych/interaktywnych witryn za pomocą Pythona.
Tworząc witrynę czy też aplikacje w Django, tworzymy projekt. Każdy projekt składa się z wielu mniejszych części zwanych aplikacjami stworzonych za pomocą Pythona według zasad Django. Każdy porządny projekt powinien mieć jasno określone założenia, na których skupiają się programiści podczas pracy, taki określony zbiór cech i funkcjonalności nazywamy specyfikacją.
Środowisko wirtualne
Rozpoczynając pracę z Django musimy skonfigurować środowisko wirtualne, w którym będziemy mogli dowolnie instalować rzeczy, nie powodując bałaganu w modułach Pythona na naszym systemie. Każde środowisko można bez problemu usunąć wydając odpowiednie dla systemu polecenie powłoki służące do usuwania plików. W tej części będę przedstawiał polecenia z systemu Linux, dystrybucji opartych o Debian, takich jak np. Ubuntu. Przepraszam za pominięcie innych systemów. Konfiguracje środowiska wirtualnego zaczynamy od zainstalowania modułu w systemie.
$ sudo apt install python3-venv
Po zainstalowaniu pakietu, dla większego porządku tworzymy sobie katalog, w którym będziemy przechowywać nasz projekt. Wewnątrz katalogu wydajemy poniższe polecenie.
$ python3 -m venv project_env
Za project_env
możemy podstawić swoją
nazwę. Po wydaniu tego polecenia Python utworzy katalog
project_env, w którym będą przechowywane wszystkie pliki
utworzonego środowiska wirtualnego. Na tym konfiguracja się kończy.
Kiedy chcemy uruchomić nasze środowisko wydajemy poniższe polecenie.
$ source project_env/bin/activate
Polecenie powinno zakończyć działanie zaraz po uruchomieniu nie zwracając żadnych danych. Jedyną rzeczą, która się zmieni jest prompt (znak zachęty), przednim będzie znajdował się nawias, zawierający nazwę naszego katalogu ze środowiskiem wirtualnym.
(project_env) xf0r3m@macbook:/python/django$
Środowisko możemy wyłączyć wydając polecenie:
(project_env) xf0r3m@macbook:/python/django$ deactivate
lub po prostu zamykając powłokę, w której je aktywowaliśmy. Po
uruchomieniu środowiska będziemy używać polecenia
python
, to polecenie będzie odnosić się
do wersji Pythona jakiej użyliśmy to stworzenia środowiska, w tym
przypadku nie będziemy używać polecenia python3.
Teraz możemy przejść do instalacji Django. Wydajemy polecenie w
aktywowanym środowisku wirtualnym:
Podstawy pracy z frameworkiem Django
2.2.1. Instalacja django
(project_env) xf0r3m@macbook:python/django$ pip install django
Kiedy polecenie skończy działanie, przechodzimy do utworzenia naszego projektu.
2.2.2. Stworzenie nowego projektu w Django
Powiedzmy że stworzymy stronę pizzerii, gdzie będzie można zamówić pizzę i przejrzeć menu dla zamówień telefonicznych. Aby utworzyć projekt wydajemy poniższe polecenie.
(project_env) xf0r3m@macbook:python/django$ django-admin startproject pizzeria .
W tym poleceniu najważniejsza jest kropka na końcu powodująca, że projekt zostanie utworzony w katalogu, w którym się znajdujemy. Brak kropki spowoduje utworzenie projektu w jeszcze jednym wewnętrznym katalogu. Spoglądając na pliki umieszczone w naszym folderze powinniśmy dostrzec plik manage.py, folder środowiska wirtualnego zakończony sufiksem _env oraz folder o nazwie pizzeria zawierający pliki naszego projektu.
2.2.3. Tworzenie bazy danych dla projektu
Jak wszyscy wiemy, aplikacje internetowe często przetwarzają jakieś dane. Te dane należy gdzieś składować, najpopularniejszą metodą magazynowania danych przez aplikacje sieci WWW są bazy danych. Django również korzysta z baz danych, a my wykorzystamy najprostszą z nich, w postaci jednego pliku - bazy SQLite3. W przypadku innych systemów bazodanowych niezbędna będzie instalacja dodatkowych modułów Pythona. Na tym etapie po prostu stworzymy bazę danych dla naszego projektu aby wszystko funkcjonowało poprawnie. Wydajemy polecenie:
(project_env) xf0r3m@macbook:python/django$ python manage.py migrate
W Django, czynności na bazie wykonujemy za pomocą opcji
migrate
skryptu manage.py.
Teraz kiedy nasza baza nie istnieje, polecenie migrate spowoduje
jej utworzenie.
2.2.4. Uruchomienie serwera projektu
Teraz możemy uruchomić nasz serwer aplikacji. Wydajemy polecenie:
(project_env) xf0r3m@macbook:python/django$ python manage.py runserver
Po uruchomieniu tego polecenia, z danych przeznie zwracanych powinniśmy dowiedzieć się, że nasz serwer nasłuchuje na pętli zwrotnej (localhost/127.0.0.1) na porcie 8000. Możemy przejść pod ten adres za pomocą przeglądarki, a naszym oczom ukaże się strona domyślna projektu, predefiniowana przez Django.
2.2.5. Tworzenie aplikacji Django
Jak wiemy z początkowych akapitów, na projekt w Django składają się na mniejsze części nazywane aplikacjami, często odpowiadają one za konkretne rzeczy. Teraz stworzymy aplikacje odpowiedzialną za nasze pizze i ich dodatki.
(project_env) xf0r3m@macbook:python/django$ python manage.py startapp pizzas
Po wykonaniu tego polecenia na chwilę się zatrzymamy. Na początku wyświetlimy sobie zawartość utworzonego katalogu aplikacji.
(project_env) xf0r3m@macbook:python/django$ ls pizzas admin.py __init__.py models.py templates urls.py apps.py migrations __pycache__ tests.py views.py
2.2.6. Modele
Katalogiem templates zajmiemy się później, natomiast katalog
__pycache__ nie jest w ogóle istotny, jest on tworzony przez
Python podczas uruchamiania skryptu. Każda aplikacja w Django
reprezentuje wzorzec model-view-template, na
modele składają się reprezentacje danych w postaci klas.
Weźmy sobie zatem taką pizzę, co może ją opisać? Na przykład nazwa.
Każda pizza ma swoją nazwę, mamy zatem jakąś właściwość, która składa się
na ten model, modele posiadają także metodę, która pozwala na
reprezentowanie danego modelu. Kiedy wyciągniemy egzemplarz modelu z
bazy, to odwołując się do niego uzyskamy, jakąś wartość. Może być to po
prostu nazwa. Z technicznego punktu widzenia modele to klasy potomne,
klasy Model
z modułu
django.db.models. Wewnątrz tej klasy znajdują się atrybuty,
oraz metoda __str__
.
Modele przypominają tabele relacyjnych baz danych, nawet są do nich
konwertowane podczas migracji, każda właściwość takiego
modelu staje się kolumną w bazie. Modele definiujemy w pliku
models.py w folderze aplikacji, a tak przedstawia się model naszej pizzy.
class Pizza(models.Model): name = models.CharField(max_length=30) def __str__(self): return self.name
W powyższym przykładzie mamy jeden atrybut znakowy
(CharField
) o długość 30 znaków.
Odpowiada to SQL-owemu VARCHAR(30). Mamy również w naszej klasie
metodę odpowiadającą za reprezentacje danego modelu. W tym wypadku
zwracamy po prostu nazwę. Modele mogą być ze sobą połączone. Rozważmy
przykład, posiadamy model pizzy i mamy model dodatku na pizzę, teraz
chcieli byśmy dowiedzieć się jakie są dodatki dla konkretnej pizzy, aby
metaforycznie położyć dodatki na pizzy skorzystamy z tak zwanego klucz
obcego lub klucza zewnętrznego - odniesienia się do innego modelu.
class Toping(models.Model): pizza = models.ForeignKey(Pizza, on_delete=models.CASCADE) name = models.CharField(max_length=200) def __str__(self): return self.name
Za nasze połączenie opowiada metoda
ForeignKey
, która jako argumenty pobiera
nazwę modelu, do którego będzie odnosić się właściwość
pizza
, drugi argument jest zachowaniem,
w przypadku usunięcia egzemplarza pizzy, do którego odnoszą się
egzemplarze dodatków, kiedy jest ustawiony na
models.CASCADE
, to gdy usuniemy pizzę to
również powiązane z nią dodatki zostaną usunięte.
2.2.7. Instalacja aplikacji w projekcie
Kiedy mamy już zdefiniowane modele, to możemy je teraz wstawić do naszej
bazy. Przed przeprowadzeniem migracji, trzeba ją najpierw przygotować.
Jednak przed przygotowaniem migracji, należy na początku zainstalować
naszą aplikacje, aby Python wiedział, że
pizzas
należy do projektu
pizzeria i mógł w bazie danych projektu utworzyć tabele.
Instalacja przebiega w następujący sposób, otóż w katalogu projektu
odszukujemy plik settings.py. W nim szukamy zmiennej
INSTALLED_APPS
, a następnie umieszczamy
nazwę naszej aplikacje w pojedynczych apostrofach na samym początku listy.
Musimy pamiętać o przecinku po nazwie aplikacji.
INSTALLED_APPS = [ 'pizzas', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', ]
Dlaczego umieściliśmy naszą aplikacje, na samym początku listy? Lista zawiera aplikacje domyślnie instalowane przez Django w naszym projekcie aby zapewnić mu podstawową funkcjonalność, oraz zaoszczędzić czas na tworzenie aplikacji. Aplikacje te realizują identyczne zadania dla każdego projektu. Może jednak zdarzyć się tak że będzie trzeba nadpisać funkcje domyślnej aplikacji, dlatego też swoje aplikacje dopisujemy powyżej aplikacji domyślnych.
2.2.8. Zatwierdzanie modeli w bazie danych.
Aby przygotować migrację wydajemy poniższe polecenie:
(project_env) xf0r3m@macbook:python/django$ python manage.py makemigrations pizzas
Jeśli modele są poprawne pod względem składni, to przez skrypt zostaniemy poinformowani mniej więcej takim komunikatem.
Migrations for 'pizzas': pizzas/migrations/0001_initial.py - Create model Pizza - Create model Topping
Ten komunikat oznacza również wygenerowanie plik migracji. Wydajemy poniższe polecenie aby zatwierdzić nasze zmiany w bazie:
(project_env) xf0r3m@macbook:python/django$ python manage.py migrate
Wywołanie tego polecenia spowoduje utworzenie modeli. Warto zaznaczyć w tym momencie, że każda poważniejsza zmiana w modelu musi zostać zatwierdzona w bazie. Każdą zmianę na modelach możemy próbować migrować, najwyżej Python nie wygeneruje pliku migracji, ponieważ uzna, że zmiana, którą wprowadziliśmy nie była istotna dla modelu w bazie. Najlepiej kiedy jednak tworzymy model postarać się przemyśleć każdą właściwość, ponieważ zmiany wprowadzane w trakcie jego działania są nieco bardziej skomplikowane. Zostały one (dodawnie kolumny) opisane pod koniec strony podczas zabezpieczania naszego projektu. Przypuszczam że zmiany w bloku metody __str__ są takim zmianami, których nie trzeba zatwierdzać w bazie.
2.2.9. Zarządzanie modelami za pomocą wirtyny administracyjnej Django
Po dokonaniu migracji, możemy wprowadzić dane początkowe do naszych modeli, do wprowadzania danych początkowych, możemy posłużyć się witryną administracyjną dostarczaną nam przez Django. Jednak przed samym rozpoczęciem musimy wykonać dwie czynności. Pierwszą rzeczą jest utworzenie konta superużytkownika - użytkownika witryny administracyjnej. Wydajemy poniższe polecenie:
(project_env) xf0r3m@macbook:python/django$ python manage.py createsuperuser
Skrypt zapyta nas o kilka rzeczy. Mianowicie o nazwę użytkownika, która może zostać domyślna - nazwa użytkownika systemowego, który wydał polecenie utworzenia superużytkownika. Adres skrzynki mailowej, który jest opcjonalny. Ostatnią rzeczą jaką będziemy musieli podać to hasło. Dwukrotnie aby je potwierdzić. Po utworzeniu superużytkownika, możemy przejść do drugiej czynności.
Aby móc zarządzać danym z modeli za pomocą witryny administracyjnej, należy je najpierw zarejestrować. Modele rejestrujemy w pliku admin.py. Na początku importujemy nasz moduł z modelami za pomocą linii:
from .models import Pizza,Toping
Kropka przed models
oznacza, że Python ma wziąć moduł z tego samego katalogu, w którym
znajduje się interpretowany plik admin.py. Następnie przy pomocy
metody register()
obiektu
admin.site
rejestrujemy każdy z
zaimportowanych modeli.
admin.site.register(Pizza) admin.site.register(Toping)
Po zarejestrowaniu modeli możemy uruchomić nasz serwer deweloperski.
(project_env) xf0r3m@macbook:python/django$ python manage.py runserver
Otwieramy w przeglądarce następujący adres http://127.0.0.1:8000/admin . Następnie logujemy się na konto superużytkownika. Po zalogowaniu zobaczymy dwie sekcje w jednej są nasze modele, a w drugie modele użytkowników i grup. Na co chciałbym zwrócić uwagę, to fakt że nazwy modeli są zapisane w liczbie mnogiej. My tych zapisów nie generowaliśmy, Python sam sobie wywnioskował (dodał literę 's' i tyle). Jednak wiemy, że tworzenie odmian nazw języków ludzi w świecie komputerów od zawsze było problemem. Tak jest i w tym przypadku. Załóżmy że mamy (tylko tak dla przykładu), model o nazwie 'Entry'. Liczba mnoga od 'Entry' to 'Entries'. Python domyślnie stworzy 'Entrys'. Oczywiście zostało to przewidziane. W celu zdefiniowania prawidłowej nazwy dla liczby mnogiej możemy użyć podklasy (klasy w klasie, nie mylić z klasą potomną) Meta, która definiuje tak jakby atrybuty opisujące samą klasę, w niej definiujemy atrybut verbose_name_plural. Pamiętajmy o tym że klasa Meta musi być zawarta w innej klasie.
Wracając do dodawania danych do modeli za pomocą witryny administracyjnej. Dane dodajemy przy użyciu plusika - zostaniemy przeniesieni bezpośrednio do formularza wprowadzającego dane do bazy, albo klikając na nazwę modelu - zostaniemy przeniesieni do strony zawierającej egzemplarze tego modelu, gdzie klikamy przycisk "Add <nazwa_modelu> +". Kiedy dodamy jakiś egzemplarz (wypełniając formularz i klikając przycisk save), to podglądając listę egzemplarzy modelu to dane odnośnie egzemplarza jakie widzimy są właśnie wynikiem działania metody __str__. Jak widać panuje pełna dowolność, co zostanie nam przestawione jako reprezentacja danego modelu.
2.2.10. Powłoka Django
Po dodaniu danych, możemy przejść do powłoki Django. Tę powłokę możemy wykorzystać (a nawet powinniśmy) testowania zapytań do bazy. Powłokę uruchamiamy poleceniem.
(project_env) xf0r3m@macbook:python/django$ python manage.py shell
Przed przystąpieniem do wydawania zapytań, należy zaimportować do powłoki niezbędne nam modele. Najprostsze zapytanie wygląda następująco.
# Import modeli from pizzas.models import Pizza, Toping pizzas = Pizza.objects.all()
W poleceniu from
,
pizzas
jest nazwą aplikacji.
Zmienna pizzas
będzie przechowywać listę (tzw.
query set) wszystkich rekordów z tabeli (egzemplarzy modelu).
Aby pobrać konkretny rekord użyjemy innej metody.
Metoda get()
pobiera jako argument
warunek, na podstawie którego ma wybrać danych rekord, jeśli nie podamy
warunku, to zostanie zgłoszony błąd że funkcja zwróciła więcej niż jeden
egzemplarz.
pepperoni = Pizza.objects.get(name='Pepperoni')
Teraz mając jeden rekord w zmiennej, mamy możliwość sprawdzenia jakie są dodatki na naszej pizzy, odwołując się do innego modelu za pomocą naszego klucza zewnętrznego/obcego.
pepperoni_tops = pepperoni.toping_set.all()
Aby odwołać się do innego modelu za pomocą klucza obcego, używamy
atrybutu naszej wydobytej już pizzy (bo to jej dodatki chcemy obejrzeć),
który jest obiektem o nazwie składającej się z nazwy modelu do, którego
się odwołujemy, napisanego za pomocą małych liter (Uwaga,
zapis nazwy modelu małymi literami jest istotny!), zakończonego sufiksem
(przyrostkiem) _set
oraz metody,
która wydobędzie nam rekordy. Tak wydobyte dane możemy sobie posortować,
do sortowania służy metoda order_by()
jako argument przyjmuje nazwę pola według jakiego ma sortować zwracane
dane. Nazwę pola umieszczamy jako ciąg tekstowy, czy to w apostrofach,
czy w cudzysłowie. Wynik sortowania możemy odwrócić, stawiając myślnik
(-) przed nazwą pola, nadal w ciągu tekstowym.
pepperoni_tops = pepperoni.toping_set.order_by('-name')
Powłoka w naszej pracy z Django będzie odgrywać ważną rolę, dzięki niej możemy testować nasze zapytania zanim umieścimy je w widokach, co jest znacznie lepszym rozwiązaniem a niżeli używanie niepewnych zapytań.
2.3. Tworzenie aplikacji
Kiedy mamy omówione modele oraz zapytania to teraz możemy przejść do tworzenia naszych stron aplikacji. Tworzenie stron aplikacji możemy podzielić na trzy czynności: mapowanie adresów URL, utworzenie widoku oraz utworzenie szablonu strony. Skupimy się w tym momencie na stronie głównej. Strona główna powinna zostać wyświetlona, kiedy użytkownika poda adres strony np. https://terminallog.morketsmerke.net. Kiedy my podamy teraz adres naszego serwera dostaniem stronę z informacją o tym, że nasz projekt Django jest gotowy do pracy.
2.3.1. Mapowanie stron aplikacji
Aby moc mapować nasze adresy na konkretne strony, musimy przekierować adres serwera w pliku urls.py w katalogu naszego projektu na plik mapowania URL (urls.py) w katalogu naszej aplikacji. Jakby jeszcze tego było mało, Django mapując URL-e, całkowicie ignoruje adres serwera.
Na początku w katalogu naszej aplikacji pizzas tworzymy plik o
nazwie urls.py. Wewnątrz pliku importujemy funkcje
path
z modułu
django.urls
oraz importujemy widoki
(o widokach za chwilę). Następnie definiujemy zmienną
app_name
z nazwą aplikacji, pozwoli nam
to na łatwe tworzenie odnośników, do innych podstron tej aplikacji,
następnie wewnątrz listy urlpatterns
deklarujemy nasze mapowania, zawartość pliku powinna wyglądać mniej
więcej tak.
"""Definiuje wzorce adresów URL dla pizzas.""" from django.urls import path from . import views app_name='pizzas' urlpatterns = [ path('', views.index, name='index'), ]
Funkcja path
przyjmuje
jak widzimy powyżej trzy argumenty. Pierwszy z nich, jest ciąg, który
zostanie przekazany przez przeglądarkę w momencie żądania danej strony.
Teraz ten ciąg jest pusty, ponieważ po przez adres serwera będziemy żądać
strony głównej naszej aplikacji. Drugim argumentem jest funkcja widoku,
której zadaniem jest przetworzenie i przygotowanie danych do użycia w
szablonie. Jeśli mówimy o czynnościach jakie wykonuje dana
aplikacja internetowa w Django, to mówimy o czynnościach wykonywanych
właśnie przez kod zdefiniowany ciele funkcji widoku. Przeważnie jedna
podstrona to jedna funkcja widoku. Funkcja widoku jest uruchamiana w
momencie gdy żądanie zostanie dopasowane do pierwszego argumentu funkcji
path. Ostatnim argumentem jest nazwa zmiennej używana przy
tworzeniu odnośników w szablonach, Python podstawi wartość pierwszego
parametru jeśli tworząc odnośnik odwołamy się do tej zmiennej.
2.3.2. Tworzenie widoku
Po zdefiniowaniu mapowania, czas na drugą czynność. Na zdefiniowanie kodu
funkcji widoku. Przechodzimy do pliku views.py następnie
tworzymy nową funkcję o nazwie index przyjmującej parametr o
nazwie request. Z racji tego że jest to strona główna, to będzie
się ona opierać głównie na szablonie. Więc tutaj nasza funkcja widoku,
będzie mieć tylko jedną linę, kiedy otworzymy plik views.py
pierwsza linia zawiera import funkcji
render
,
odpowiada ona za wygenerowanie gotowego pliku HTML z szablonu, który
zrozumie każda przeglądarka. Funkcja render przyjmuje w tym
przypadku dwa argumenty. Żądanie przechowywane w parametrze
request
oraz ścieżkę dostępu do
szablonu. Plik views.py powinien wyglądać następująco.
from django.shortcuts import render # Create your views here. def index(request): """Strona główna aplikacji Pizzas""" return render(request, 'pizzas/index.html')
2.3.3. Tworzenie szablonu
Ze ścieżką dostępu do szablonu jest o tyle zabawnie że, aby szablon był widziany przez Django musi być umieszczony w katalogu aplikacji następnie w podkatalogu templates, w którym znajduje się kolejny podkatalog o nazwie aplikacji, a w nim dopiero szablon. Pliki szablonów to tak naprawdę pliki HTML, które zawierają znaczniki szablonu, dzięki którym możemy wprowadzać nasze dane na strony witryny, ale również w prosty sposób nią zarządzać. Szablon strony głównej nie zawiera wiele informacji, nie zawiera nawet żadnego znacznika szablonu.
<p>Pizzeria</p> <p>Aplikacja ta pozwala na przymowanie zamówień oraz wyświetlanie menu pizzerii</p>
2.3.4. Mapowanie aplikacji
Chcąc uruchomić do co zrobiliśmy do tej pory, musimy przed uruchomieniem
serwera wskazać w pliku mapowania URL-i projektu konkretną aplikację. Tę
czynność należy wykonać w pliku <nazwa_projektu>/urls.py w
tym konkrentym przypadku jest to pizzeria/urls.py. Wykonanie
czynności rozpoczynamy od zaimportowanie funkcji include.
Funkcją ta jest dostępna w module django.urls więc po
path na górze pliku do pisujemy
, include
.
from django.urls import path, include
Następnie podobnie do mapowań stron aplikacji, jako kolejny element list
podajemy wywłołanie funkcji path
wraz
z pustym żądaniem oraz zamiast podawać funkcję widoku oraz nazwę zmiennej
wywołujemy funkcję include
wraz ze
wskazaniem pliku mapowania stron aplikacji w postaci
'nazwa_aplikacji.urls'
from django.contrib import admin from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), path('', include('pizzas.urls')), ]
2.3.5. Rozszerzanie przykładu
Z racji tego że mamy trochę za mało informacji, aby zaprezentować duże możliwości szablonów w Django, rozbudujemy trochę naszą aplikacje o jedną podstronę zawierającą menu naszej pizzerii.
Naszą rozbudowę zaczniemy od stworzenia szablonu, w którym to będą znajdować się same odnośniki do naszych podstron, Inne szablony będą dziedziczyć te elementy od niego. W naszym katalogu z szablonami tworzymy plik o nazwie base.html. Plik zawiera w sumie dwie linijki
<a href="{% url 'pizzas:index' %}">Pizzeria>/a< {% block content %}{% endblock content %}
Znacznik szablonu url
w atrybucie
href
wstawi bezwzględny adres URL
zawierający wzorzec przypisany do nazwy
index
w funkcji path w pliku
urls.py. Po przeniesieniu tytułu jako odnośnika do
szablonu nadrzędnego. Możemy teraz wpisać linię ustawiającą
dziedziczenie w szablonie
index.html. Linię wstawiamy na samej górze.
{% extends 'pizzas/base.html' %}
Kiedy mamy już odpowiednie miejsce gdzie będziemy umieszczać odnośniki do
naszych podstron. Możemy przejść teraz do utworzenia widoku naszej
podstrony, naszą stronę nazwiemy klasycznie
menu
. W pliku views.py naszej
aplikacji pizzas, umieszczamy poniższy kod.
def menu(request): from pizzas.models import Pizza,Toping pizzas = Pizza.objects.all() pizza_dict={} pizzas_list=[] class PizzaClass(): def __init__(self, name, topings): self.name = name self.topings = topings for pizza in pizzas: pizza_name = f"{pizza}" topings = pizza.toping_set.all() t="" for toping in topings: if len(t) == 0: t=f"{toping}, " else: t=f"{t}{toping}, " t=t.rstrip(', ') some_pizza = PizzaClass(pizza_name, t) pizzas_list.append(some_pizza) pizza_dict['pizzas']=pizzas_list return render(request, 'pizzas/menu.html', pizza_dict)
Przed przystąpieniem do objaśniania chciałbym przestawić model wyświetlania danych, otóż chciałem uzyskać taki sam lub zbliżony ulotkom z pizzerii wydruk menu, czyli:
nazwa_pizzy - składniki/dodatki
A więc problem nie był kod widoku, ponieważ tu mogliśmy zbudować kod
realizujący to samo zadanie w znacznie prostszy sposób, ale kod szablonu.
Znaczniki szablonu mają duże możliwości, jednak nie są pełnym
odwzorowaniem tego co można zrobić w Pythonie. Rozwiązanie było banalne.
Mamy listę z nazwami pizz, uruchamiamy pętle, wewnątrz tej iteracji,
odwołujemy się nazwą pizzy do listy składników i problem rozwiązany.
Jednak w szablonach Django, jedyne co możemy zrobić z zmiennymi to nadać
im wartość i wypisać. Nie możemy użyć zmiennej szablonu jako klucza aby
odwołać się do kolejnej listy w zagnieżdżonej pętli
for
. Używanie zmiennej context
w szablonie zakończyło się fiaskiem, mogliśmy pisać kod szablonu, który
niczego nie generował. Zajrzałem więc do oficjalnej dokumentacji Django,
rozwiązanie od razu nie przypadło mi do gustu, słownik + lista + obiekty,
jednak jak możemy się o tym przekonać rozwiązanie działa.
Słownik w tym przypadku jest strukturą danych przekazywaną do szablonu za
sprawą funkcji render
, lista jest
strukturą dzięki, której może odbyć się iteracja przez nasze pizze,
obiekty pizzy został użyty w celu spięcia nazwy pizzy oraz jej dodatków w
jednej strukturze, do której będziemy mogli się odwołać w szablonie.
Zapewne istnieje inne rozwiązanie, jednak jeśli drogi czytelniku w życiu
stworzyłeś może jeden program w Pythonie, przeczytałeś całą pierwszą
część Crash Course (uwaga wykonując większość ćwiczeń,
wszystkich nie trzeba ponieważ się powtarzają, ewentualnie trzeba zmienić
jedną rzecz jednak logika pozostaje ta sama) i dotarłeś aż tutaj to moja
umiejętność programowania w Pythonie jest mniej więcej na Twoim poziomie.
Tak więc co się dzieje w widoku, już tłumaczę. Na początku importujemy
nasze modele, żeby było skąd dane pobrać. Wydane zostaje zapytanie do
modelu Pizza
o wszystkie (egzemplarze
modelu) rekordy, tworząc listę naszych pizz.
Tworzymy klasę naszej pizzy, klasa będzie zawierać tylko metodę
__init__
a oraz dwie właściwości nazwę
oraz dodatki jako ciągi tekstowe. Przeprowadzając iteracje z elementu
naszej listy pizz tworzymy nazwę pizzy po prostu przechowując ją w
zmiennej jako ciąg tekstowy, pobieramy listę naszych dodatków odwołując
się kluczem obcym pizzy do modelu Toping
.
Przerabiamy listę dodatków na ciąg tekstowy. Po utworzeniu listy dodatków
jako ciągu możemy, zacząć tworzyć obiekty. Nowo utworzone obiekty
trafiają do listy i tak kończy się iteracja. Na samym końcu dodajmy naszą
listę obiektów do słownika, który w ostatniej linii naszego widoku jest
przekazywany do szablonu za pomocą funkcji
render
. Poniżej znajduje się kod naszego
szablonu.
{% extends 'pizzas/index.html' %} {% block content %} <p>Menu</p> <ul> {% for pizza in pizzas %} <li> {{ pizza.name }} <small>- {{ pizza.topings }}</small> </li> {% endfor %} </ul> {% endblock content %}
W tym momencie powinniśmy mieć gotowe menu. Teraz przygotujemy stronę, która wyświetli nam listę dodatków każdej z pizzy, abyś mogli zobaczyć jak działa przekazywanie danych do aplikacji za pomocą adresu URL.
Naszą rozbudowę rozpoczęliśmy od utworzenia widoku dla nowej strony. Widok jak widać poniżej jest bardzo prosty.
def pizza_site(request, pizza_id): from pizzas.models import Pizza,Toping context = {} name = Pizza.objects.get(id=pizza_id) topings = name.toping_set.all() context['name'] = name context['topings'] = topings return render(request, 'pizzas/pizza_site.html', context)
Zwrócić uwagę należy na dodatkowy parametr przekazywany do widoku.
Parametr o nazwie pizza_id
będzie
przekazywali za pomocą adresu URL, taki parametr definiuje się we wzorcu
adresu URL. Wzorcem zajmiemy się pod koniec naszych prac.
Szablon dla tej strony został przedstawiony poniżej. Za pomocą słownika
context
została przekazana nazwa pizzy
oraz lista z jej składnikami, nową rzeczą wprowadzoną tutaj jest
znacznik szablonu {% empty %}
oznacza on
mniej więcej to że jeśli interpreter natrafi na pustą listę to możemy
zdefiniować w bardzo prosty sposób komunikat o tym że składniki na tej
pizzy są na życzenie.
{% extends 'pizzas/base.html' %} {% block content %} <p>{{ name }}</p> <ul> {% for toping in topings %} <li>{{ toping }}</li> {% empty %} <li>Dodatki na życzenie</li> {% endfor %} </ul> {% endblock content %}
Mając już szablon możemy przejść do mapowania URL-i. W aplikacji
pizzas w pliku urls.py za pomocą funkcji
path
dopisujemy do listy
urlspattern kolejny wzorzec adresu.
path('pizza/<int:pizza_id>', views.pizza_site, name='pizza_site')
Stąd właśnie bierze się parametr widoku
pizza_site
, jeśli po pizza/
podamy liczbę całkowitą to wywołamy widok pizza_site przekazując
tę liczbę jako identyfikator rekordu pizzy. Wszystko świetnie,
jednak nie znamy id pizz. Należało by stworzyć odnośnik do
konkretnej pizzy w menu. W obecnej formie naszego menu próżno szukać
id. Dlatego też musimy zmodyfikować widok menu dodając do
naszej klasy atrybut id, który można pobrać bezpośrednio z egzemplarza
modelu za pomocą notacji kropki i właściwości o nazwie id. Poniżej
wstawiam zmieniony kod widoku menu
.
def menu(request): from pizzas.models import Pizza,Toping pizzas = Pizza.objects.all() pizza_dict={} pizzas_list=[] class PizzaClass(): def __init__(self, id, name, topings): self.id = id self.name = name self.topings = topings for pizza in pizzas: pizza_name = f"{pizza}" topings = pizza.toping_set.all() t="" for toping in topings: if len(t) == 0: t=f"{toping}, " else: t=f"{t}{toping}, " t=t.rstrip(', ') some_pizza = PizzaClass(pizza.id, pizza_name, t) pizzas_list.append(some_pizza) pizza_dict['pizzas']=pizza_list return render(request, 'pizzas/menu.html', pizza_dict)
Teraz musimy utworzyć nasz odnośnik w pętli w pliku szablonu menu, aby pojawił się dla każdej pizzy.
{% extends 'pizzas/index.html' %} {% block content %} <p>Menu</p> <ul> {% for pizza in pizzas %} <li> {{ pizza.name }} <small>- {{ pizza.topings }}</small> <a href="{% url 'pizzas:pizza_site' pizza.id %}">Pokaż</a> </li> {% endfor %} </ul> {% endblock content %}
Podczas tworzenia adresu odnośnika za pomocą znacznika szablonu
url
odwołujemy się do wzorca
zdefiniowanego za pomocą wartości nadanej parametrowi name
funkcji pathw pliku urls.py, tym razem wartość
pizza_id podajemy obok odwołania się do wzorca. Teraz przy
wywołaniu strony menu przy każdej z pizz powinien pokazać się odnośnik
"Pokaż" kierujący nas do konkretnej strony pizzy.
2.4. Aplikacja users
Skoro mamy już stronę główną, mamy menu i jest z czego wybierać możemy teraz przejść do składania zamówień. Złożenie zamówienia w naszej pizzerii będzie wymagało utworzenia konta. Obsługą kont zajmie się odrębna aplikacja users, którą teraz utworzymy. Przechodzimy do katalogu naszego projektu i wydajemy następujące polecenie:
(project_env) xf0r3m@macbook:python/django$ python manage.py startapp users
Po wydaniu tego polecenia zostanie utworzona struktura plików identyczna jak dla aplikacji pizzas. Teraz instalujemy aplikacje users w identyczny sposób jak instalowaliśmy aplikacje pizzas.
INSTALLED_APPS = [ 'pizzas', 'users', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', ]
2.4.1. Logowanie użytkowników
Naszą przygodę z kontami użytkowników zaczniemy od dania możliwości
logowania potencjalnym klientom naszej pizzerii. Samo logowanie w Django
jest banalne ponieważ korzystać będziemy z gotowego, przygotowanego przez
Django widoku dla logowania. Jedynego czego będziemy potrzebować to
mapowania URL oraz szablonu. Zaczniemy od mapowania.
W pliku urls.py projektu pizzeria na liście
urlpatterns
dokonujemy takiego samego
przekierowania jak w przypadku przekierowania w aplikacji pizzas
jednak naszym wzorcem będzie users/
a
pliku urls.py będzie znajdował się w katalogu aplikacji
users (jako argument funkcji
include
podajemy
users.urls
).
urlpatterns = [ path('admin/', admin.site.urls), path('users/', include('users.urls')), path('', include('pizzas.urls')) ]
W aplikacji users tworzymy plik urls.py. W nim importujemy
funkcje path
oraz
include
z modułu
django.urls
, definiujemy
app_name
następnie na liście
urlpatterns
dodajemy jedno wywołanie
funkcji path
kierujące puste domyślne
zapytanie do pliku zdefiniowanego przez Django
django.contrib.auth.urls
.
from django.urls import path,include app_name="users" urlpatterns = [ path('', include('django.contrib.auth.urls')) ]
Teraz kiedy mapowanie jest gotowe, możemy przejść do utworzenia szablonu. Domyślna funkcja widoku stworzona do logowania generuje stronę na podstawie szablonu login.html umieszczonego w katalogu templates/registration. Te nazwy muszą zostać zachowane. Utwórzmy te katalogi wewnątrz katalogu aplikacji users. Następnie utworzymy plik z szablonem.
{% extends 'pizzas/base.html' %} {% block content %} <form action="{% url 'users:login' %}" method="post"> {% csrf_token %} {{ form.as_p }} <button name="submit">Zaloguj się</button> <input type="hidden" name="next" value="{% url 'pizzas:index' %}" /> </form> {% endblock content %}
Tak prezentuje się nasz szablon logowania. Warto zwrócić uwagę na to jak
łatwy jest dostęp do zasobów innej aplikacji wystarczy odpowiednia
ścieżka (patrz znacznik extends), w atrybucie
action zdefiniowana została strona, która ma obsłużyć dany
formularz. Znacznik szablonu csrf_token
służy zabezpieczeniu formularza przez atakiem CSRF. Następnie wyświetlany
jest sam formularz, który przechowywany jest w zmiennej
form
. Wyświetlenie jej wraz z
as.p
spowoduje, że etykiety oraz pola
wprowadzania danych zostaną wyświetlone jako jeden akapit i tak dla
każdego z elementów formularza. Formularze w Django nie zawierają
przycisków, trzeba je zdefiniować samemu. Ostatnią rzeczą jest
przekazanie wraz z formularzem do widoku adres strony, do której ma
zostać przekierowany użytkownik po poprawnym zalogowaniu. Jako
ciekawostkę możemy sobie dodać warunek w formularzu, który wyświetli nam
komunikat z informacją o tym że formularz nie został poprawnie
wypełniony. W ten czas nasz formularz wyglądać będzie tak:
{% extends 'pizzas/base.html' %} {% block content %} {% if form.errors %} <p> Nazwa użytkownika i hasło są niepoprawne. Spróbuj ponownie. </p> {% endif %} <form action="{% url 'users:login' %}" method="post"> {% csrf_token %} {{ form.as_p }} <button name="submit">Zaloguj się</button> <input type="hidden" name="next" value="{% url 'pizzas:index' %}" /> </form> {% endblock content %}
Dlaczego jednak jako ciekawostkę? Otóż Django sam dodaje takową informacje jeśli formularz nie przejdzie walidacji. Teraz dodamy warunek w pliku w szablonie bazowym aplikacji pizzas, po to abyśmy wiedzieli, że zostaliśmy zalogowani oraz w przeciwnym wypadku wyświetli odnośnik prowadzący do strony logowania. Poniżej znajduje się już uzupełniony kod szablonu bazowego aplikacji pizzas.
<p> <a href="{% url 'pizzas:index' %}">Pizzeria</a>- <a href="{% url 'pizzas:menu' %}">Menu</a> </p> {% if user.is_authenticated %} <p> Witaj, {{ user.username }} </p> {% else %} <p> <a href="{% url 'users:login' %}">Zaloguj się</a> </p> {% endif %} {% block content %} {% endblock content%}
Jednego użytkownika już mamy. Możemy się zalogować się jako superużytkownik. Po poprawnym zalogowaniu zostaniemy przeniesieni z powrotem na stronę główną, jednak teraz zamiast Zaloguj się zobaczymy Witaj, pizza_admin. Teraz pod powitaniem umieścimy przycisk Wyloguj, który będzie kończył sesję użytkownika w aplikacji. Wylogowanie również jest funkcją, która jest dostarczana wraz z Django. Mapowanie URL-u wylogowania jest również dostarczane przez Django, tak jak mapowanie URL-u logowania. Szablon bazowy wygląda następująco po dodaniu nowego odnośnika.
<p> <a href="{% url 'pizzas:index' %}">Pizzeria</a>- <a href="{% url 'pizzas:menu' %}">Menu</a> </p> {% if user.is_authenticated %} <p> Witaj, {{ user.username }} </p> <p> <a href="{% url 'users:logout' %}">Wyloguj</a> </p> {% else %} <p> <a href="{% url 'users:login' %}">Zaloguj się</a> </p> <p> <a href="{% url 'users:register' %}">Zarejestruj się</a> </p> {% endif %} {% block content %} {% endblock content%}
W ramach eksperymentu, kliknijmy w ten odnośnik. Zostaniemy poinformowani o wylogowaniu z naszej aplikacji przez witrynę administracyjną Django. Nie zbyt fajnie, co? Oczywiście to zachowanie możemy zmienić, tworząc nowy szablon w katalogu templates/registration. Uwaga, Plik musi nazywać się: logged_out.html. Po rozszerzeniu pliku o szablon bazowy, między znacznikami szablonu oznaczającymi zawartość strony w tym szablonie możemy umieścić wszystko co nam się podoba.
{% extends 'pizzas/base.html' %} {% block content %} <p> Dziękujemy za skorzystanie z naszej aplikacji. </p> {% endblock content %}
2.4.2. Rejestracja użytkowników
Teraz możemy dać możliwość samodzielnego utworzenia konta na stronie naszej pizzerii. Tutaj nie jest już tak kolorowo. Jedyne co nam Django oferuje w tym przypadku jest formularz. Widok, szablon oraz mapowanie URL-u musimy utworzyć samodzielnie. Zaczynamy od widoku dla formularza rejestracji.
from django.shortcuts import render, redirect from django.contrib.auth.forms import UserCreationForm from django.contrib.auth import login def register(request): if request.method != 'POST': form = UserCreationForm() else: form = UserCreationForm(data=request.POST) if form.is_valid(): new_user = form.save() login(request, new_user) return redirect('pizzas:index') content = {'form': form} return render(request, 'registration/register.html", context)
W tym przypadku aby funkcja widoku mogła poprawnie działać oraz
wyświetlać nam formularz, potrzebujemy modułów. Pierwszą funkcją jaką
importujemy jest redirect
, powoduje ona
przekierowanie HTTP, na stronę podaną jako argument. Adresy stron podane
jako argumenty funkcji redirect czy też w znacznikach szablonu
url podajem w formacie
nazwa_aplikacji:nazwa_mapowania
-
wartość argumentu name funkcji path w pliku
urls.py. Następnie importujemy nasz formularz oraz
funkcję login. Za pomocą tej funkcji będzie możliwe automatyczne
zalogowanie nowo utworzonego użytkownika. Funkcje widoku obsługujące
formularz posiadają pewien schemat. Taka funkcja rozpoczyna się od
sprawdzenia użytej metody przesłania danych do serwera, protokół HTTP
wyróżnia dwie GET oraz POST.
Metoda GET służy głównie do obsługi żądań osób odwiedzających
witrynę. Poniżej żądanie wyświetlenia strony konkretnej pizzy.
http://localhost:8000/pizza/1
Żądanie wyświetlenia konkretnej pizzy zostało przesłane do serwera za pomocą żądania GET. Najprościej można to ująć w ten sposób, że żądania GET są przesyłanie w adresie strony. Natomiast żądania POST są przesyłane oddzielnym pakietem transmisji. W żądaniach POST najczęsciej są przesyłane dane formularzy.
Jeśli nasz widok, ktoś wywołał za pomocą żądania GET to wtedy
zmiennej form
zostanie nadany pusty
formularz. Następnie definiowana jest zmienna
context
zawierająca wszelkie dane dla
szablonu w tym nasz formularz. Na sam koniec funkcja widoku zwraca
wygenerowaną przed funkcję render()
stronę, na której znajduje się nasz formularz rejestracji. W przeciwnym
wypadku jeśli nasza funkcja została wywołana za pomocą żądania POST
(np. kiedy użytkownik przesłał formularz), to tworzona jest zmienna z
formularzem gdzie jego dane są pobierane z właściwości
POST
obiektu
request
.
Za pomocą instrukcji warunkowej if
oraz
metody formularza is_valid()
sprawdza
jest poprawność wypełnienia pól formularza, jeśli któraś z wartości pół
nie przechodzi walidacji (sprawdzenia poprawności), to następną czynnością
jest wpisanie formularza do zmiennej
context
i wygenerowanej strony z
formularzem oraz komunikatem błędu.
Zaś kiedy formularz został wypełniony bezbłędnie to wykonywana jest
metoda formularza save()
, która powoduje
zapisanie danych z formularza w bazie danych na podstawie przypisanego do
niego modelu. Zwróćmy uwagę na to, że wartość zwraca przez metodę
save() została zapisana do zmiennej
new_user
. Dzięki tej zmiennej będzie
możliwe automatyczne zalogowanie zarejestrowanego użytkownika, kiedy mamy
tą zmienną możemy zalogować użytkownika za pomocą funkcji
login()
ta funkcja pobiera dwa argumenty
obiekt żądania (request) oraz odniesienie do użytkownika (
new_user).
Kiedy użytkownik został zalogowany, możemy teraz przekierować go na
stronę główną, która zostanie wyświetlona ze spersonalizowanym powitaniem
oraz odnośnikiem do wylogowania się z konta na naszej stronie. Teraz
potrzebujemy jeszcze szablonu, który poza odnośnikiem nie różni się
niczym do szablonu logowania.
Plik szablonu formularza rejestracji znajduje się w folderze templates/registration pod nazwą register.html:
{% extends 'pizzas/base.html' %} {% block content %} {% if form.errors %} <p>Rejestracja nie powiodła się. Spróbuj ponownie</p> {% endif %} <form action="{% url 'users:register' %}" method="post"< {% csrf_token %} {{ form.as_p }} <button name="submit">Zarejestruj się</button> <input type="hidden" name="next" value="{% url 'pizzas:index' %}" /> </form> {% endblock content %}
Funkcja mapowania w pliku users/urls.py wygląda następująco:
path('register/', views.register, name="register"),
Teraz możemy do szablonu bazowego aplikacji pizzas dodać odnośnik dla niezalogowanych, prowadzący do naszej strony z rejestracją.
<p> <a href="{% url 'pizzas:index' %}">Pizzeria</a>- <a href="{% url 'pizzas:menu' %}">Menu</a> </p> {% if user.is_authenticated %} <p> Witaj, {{ user.username }} </p> <p> <a href="{% url 'users:logout' %}">Wyloguj</a> </p> {% else %} <p> <a href="{% url 'users:login' %}">Zaloguj się</a> </p> <p> <a href="{% url 'users:register' %}">Zarejestruj się</a> </p< {% endif %} {% block content %} {% endblock content%}
2.5. Rozbudowa aplikacji 'pizzas'.
Teraz kiedy mamy logowanie oraz rejestracje. To powinniśmy mieć możliwość złożenia zamówienia w naszej pizzerii (aplikacja pizzas). Sama operacja jest dość złożona bo do realizacji tego zadania potrzebujemy aż 5 elementów: modelu zamówienia, widoku wyświetlającego zamówienia, widoku dodającego zamówienia, formularza, szablonu i mapowania URL. Zaczniemy od modelu zamówienia. Na nasze zamówienie będzie składać się pizza, wielkość, sos pierwszy oraz sos drugi, dowóz czy odbiór własny oraz datę zamówienia. Model prezentuje się poniżej. Stosować będziemy skróty, które nasz kucharz będzie stanie odszyfrować dlatego pola są takie małe.
class Order(models.Model): pizza = models.ForeignKey(Pizza, on_delete=models.CASCADE) size = models.CharField(max_length=10) sauce1 = models.CharField(max_length=10) sauce2 = models.CharField(max_length=10) deliver = models.CharField(max_length=10) date_order = models.DateTimeField(auto_now_add=True) def __str__(self): return f"{self.pizza.name} - {self.size},{self.sauce1},\ {self.sauce2},{self.deliver} - {self.date_order}"
Po dopisaniu naszego modelu zamówienia musimy wykonać migrację.
(project_env) xf0r3m@macbook:python/django$ python manage.py makemigrations pizzas (project_env) xf0r3m@macbook:python/django$ python manage.py migrate
Po pomyślnej migracji zarejestrujemy nowy model na witrynie administracyjnej, po to aby dodać kilka testowych wartość do sprawdzenia widoku wyświetlania złożonych zamówień. Widok złożonych zamówień prezentuje się następująco:
def orders_view(request): from pizzas.models import Order orders = Order.objects.order_by('-date_order') context = {'orders': orders} return render(request, 'pizzas/orders_view.html', context)
W szablonie zamiast używać listy użyjemy tabeli. Poniżej znajduje się kod szablonu.
{% extends 'pizzas/base.html' %} {% block content %} <table> <thead> <tr> <td>Numer zamówienia>/td> <td>Pizza<td> <td>Wielkość</td> <td>Sos_1>/td> <td>Sos_2<td> <td>Dostawa</td> </tr> </thead> <body> {% for order in orders %} <tr> <td>{{ order.id }}</td> <d>{{ order.pizza.name }}</td> <td>{{ order.size }}</td> <td>{{ order.sauce1 }}</td> <td>{{ order.sauce2 }}</td> <td>{{ order.deliver }}</td> </tr> {% empty %} <tr><td colspan="6">Brak zamówień</td></tr> {% endfor %} </tbody> </table> {% endblock content %}
Teraz możemy przejść do składania zamówień na naszą pizzę, zaczniemy od formularza. Nie jest on może trudny w interpretacji ale jest nieco bardziej złożony niż te przedstawione w książce.
from django import forms from .models import Order class NewOrder(forms.ModelForm): class Meta: model = Order fields = ['pizza', 'size', 'sauce1', 'sauce2', 'deliver'] labels = { 'pizza': 'Pizza', 'size': 'Wielkość', 'sauce1': 'Pierwszy sos', 'sauce2': 'Drugi sos', 'deliver': 'Dostawa' } size_choice = [ (None, '-----------'), ('S', 'Mała'), ('M', 'Duża'), ('L', 'Familijna'), ('XL', 'XXL 55cm') ] sauce_choice = [ ('br', '-----------'), ('k', 'Ketchup'), ('m', 'Majonez'), ('cz', 'Czosnkowy'), ('pik', 'Pikantny') ] deliver_choice = [ (None, '-----------'), ('odb', 'Odbiór osobisty'), ('dwz', 'Dowóz') ] widgets = { 'size': forms.Select(choices=size_choice), 'sauce1': forms.Select(choices=sauce_choice), 'sauce2': forms.Select(choices=sauce_choice), 'deliver': forms.Select(choices=deliver_choice) }
Formularze w Django to nic innego jak klasy potomne, klasy
ModelForms
modułu
django.forms
, jednak kodu określającego
formularz nie tworzymy w samej klasie potomnej tylko w pod klasie
Meta
. Z racji tego, że klasa
ModelForm bazuje na modelach (zresztą jak sama nazwa wskazuje)
musimy pamiętać o ich imporcie jeszcze przed zdefiniowaniem samej klasy.
W klasie Meta podajemy takie informacje jak
model na jakim bazuje formularz
(zmienna model
), pola formularza
(lista fields
) oraz etykiety
(słownik labels
), z
racji tego że informacje przekazywane przez zamawiającego mogą być
predefiniowane, to cały formularz oprzemy na jednym elemencie, na liście
wyboru select.
Jeśli nasz model zawiera klucz obcy, to pole z automatu jest
ustawiane przez Django jako element select. Resztę
naszych pól musimy ustawić samodzielnie.
Zmianie elementów przydzielanych przez Django, służy słownik
widgets
, w którym każdemu polu
przypisujemy wyświetlany element, w tym przypadku jest to
forms.Select
czyli element HTML
select jako argument choices
podajemy listę z dwuelementowych krotek, w naszym przypadku
zdefiniowaną powyżej słownika widgets.
Jako ciekawostkę podam fakt, że jeśli w jakieś krotce w pierwszym elemencie podamy jako pierwszy argument, czyli wartość elementu option, wartość None to pole automatycznie stanie się polem required, czyli jego wartość musi być ustawiona inaczej przeglądarka nie pozwoli wysłać formularza. Poniżej znajduje się kod widoku obsługującego ten formularz.
def new_order(request): if request.method != 'POST': form = NewOrder() else: form = NewOrder(data=request.POST) if form.is_valid(): form.save() return redirect('pizzas:thank_you') context = {'form': form} return render(request, 'pizzas/new_order.html', context)
Ten przypadek obsługi jest chyba najprostszy. Nie ma tu nic złożonego, po przejściu walidacji zamówienie jest zapisywane w bazie. Pod mapowaniem thank_you kryje się jeden akapit z podziękowanie za złożenie zamówienia. Poniżej znajduje kod szablonu.
{% extends 'pizzas/base.html' %} {% block content %} {% if form.errors %} <p> Złożenie zamówienia nie powiodło się. Spróbuj ponownie </p> {% endif %} <form action="{% url 'pizzas:new_order' %}" method="post"> {% csrf_token %} {{ form.as_p }} <button name="submit">Zamów</button> </form> {% endblock content %}
Powiedzmy że zamówiliśmy pizzę, ale w pizzerii skończył się jeden jej składnik. Kelnerka po kontakcie telefonicznym z klientem doszła do porozumienia i klient wybrał inną pizzę. Jednak w systemie nie ma możliwości zmiany zamówienia. Utworzymy ją. Do tego celu potrzebujemy dodatkowego widoku, mapowania, szablonu oraz odnośnika do edycji zamówienia w tabeli z zamówieniami. Edycje danych wpisywanych za pomocą formularza tworzy się bardzo szybko ponieważ wystarczy skopiować widok oraz szablon. W widoku dodać kilka rzeczy, w szablonie zmieć dwie rzecz oraz dodać jedną. Poniżej znajduje się kod widoku, najważniejszy w tym przedsięwzięciu.
def edit_order(request, order_id): from .models import Order order = Order.objects.get(id=order_id) if request.method != 'POST': form = NewOrder(instance=order) else: form = NewOrder(instance=order, data=request.POST) if form.is_valid(): form.save() return redirect('pizzas:edit_ok') context = {'form': form, 'order': order} return render(request, 'pizzas/edit_order.html', context)
Zatem po kolei żeby było wiadomo jakie zamówienie mamy zmienić
potrzebujemy jego identyfikatora, id z tabeli w bazie danych.
Potrzebne nam id będzie przekazywane w żądaniu GET,
przez kliknięcie w łącze na stronie z zamówieniami. Na początku naszego
widoku importujemy model Order
, aby
uzyskać dostęp do egzemplarzy tego modelu w bazie. Za pomocą zapytania
Order.objects.get(id=order_id)
,
pobraliśmy rekord zamówienia do zmiennej
order
. Następnie wykonuje się klasyczna
obsługa formularza z tą jednak różnicą, że kiedy tworzone są egzemplarze
formularza to przekazywany jest argument
instance
, któremu nadawana jest wartość
naszego zapytania, ustawienie argumentu instance powoduje
wypełnienie pół formularza danymi z zapytania,
strona edit_ok zawiera jeden akapit z informacją że zapisano
zmiany w zamówieniu.
W zmiennej context
została dodana
wartość obiektu order
pod kluczem
order
. Jest to niezbędne kiedy zmieniamy
rekordy, ponieważ w atrybucie action
formularza musimy podać id zmienianego rekordu, ponieważ ten
widok będzie wykonywany po raz drugi gdy użytkownik prześle dane. Dlatego
też po słowie kluczowym else
gdzie
deklarowany jest formularz, podano argument
instance
, aby było wiadomo jaki rekord
należy zmienić. Poniżej znajduje się kod szablonu.
{% extends 'pizzas/base.html' %} {% block content %} {% if form.errors %} <p> Zmiana zamówienia nie powiodła się. Spróbuj ponownie </p> {% endif %} <form action="{% url 'pizzas:edit_order' order.id %}" method="post"> {% csrf_token %} {{ form.as_p }} <button name="submit"&t;Zapisz zmiany</button> </form> {% endblock content %}
Tutaj zmieniono atrybut action
na
edit_order
, dodano
order.id
oraz zmieniono treść
przycisku. Poniżej znajduje się zawartość pliku pizzas/urls.py
z uwzględnieniem nowego mapowania oraz zmieniony kod szablonu
pizzas/orders_view.html.
Plik pizzas/urls.py
from django.urls import path from . import views app_name='pizzas' urlpatterns = [ path('', views.index, name='index'), path('menu', views.menu, name='menu'), path('pizza/<int:pizza_id>', views.pizza_site, name='pizza_site'), path('orders/', views.orders_view, name="orders"), path('new_order/', views.new_order, name="new_order"), path('edit_order/<int:order_id>', views.edit_order, name='edit_order') ]
Plik pizzas/templates/pizzas/orders_view.html
{% extends 'pizzas/base.html' %} {% block content %} <table border="1"> <thead> <tr> <th>Numer zamówienia</th> <th>Pizza</th> <th>Wielkość</th> <th>Sos_1</th> <th>Sos_2</th> <th>Dostawa</th> <th>>/th> </tr> </thead> <tbody> {% for order in orders %} <tr> <td>{{ order.id }}</td> <td>{{ order.pizza.name }}>/td> <td>{{ order.size }}</td> <td>{{ order.sauce1 }}</td> <td>{{ order.sauce2 }}</td> <td>{{ order.deliver }}</td> <td><a href="{% url 'pizzas:edit_order' order.id %}">Zmień</a></td> </tr> {% empty %} <tr><td colspan="7">Brak zamówień</td></tr> {% endfor %} <tbody> </table> {% endblock content %}
2.6. Zabezpieczenie aplikacji
Kolejną rzeczą jak należy zrobić to zabezpieczyć pewne strony przed
niepowołanym dostępem. W zależności od tego jakie restrykcje chcemy
wprowadzić możemy użyć konkretnych metod. Pierwszą z nich jest wymaganie
logowania, jeśli nie zalogowani przejedziemy pod adres
http://localhost:8000/new_order. Zostanie nam przedstawiona
strona do składania zamówień. W ten sposób osoba złośliwa może po
nabijać zamówień, zostawiając jedynie ślad w logach o ile uruchomiliśmy
serwer wraz z opcją zapisywania dziennika. Django daje nam banalny sposób
na zabezpieczenie się przed tego typu sytuacjami.
W pliku views.py w aplikacji pizzas importujemy
dekorator (dodatek/modyfikator do funkcji) o nazwie
login_required
.
from django.contrib.auth.decorators import login_required
Dekorator login_required
sprawdzi czy
jesteśmy zalogowani, kiedy będziemy wywoływać funkcje widoku. Dekoratora
używamy w pomocą znaku @
oraz nazwy
dekoratora tuż nad definicją widoku, tak jak przedstawiono to
poniżej:
@login_required def orders_view(request): ... @login_required def new_order(request): ... @login_required def edit_order(request, order_id):
Aby działało to poprawnie w ustawieniach projektu pizzeria/settings.py
musimy zdefiniować zmienną LOGIN_URL na samym dole pliku, jeśli tego nie
zrobimy niezalogowani użytkownicy, przechodzący pod zabezpieczoną przez
dekorator stronę otrzymają stronę błędu, że nie znaleziono strony.
Wartość zmiennej LOGIN_URL
zapisujemy w
klasycznym odniesieniu do strony, tak jak w przypadku znaczników szablonu
URL, czy funkcji redirect
(nazwa_aplikacji:nazwa_mapowania_URL).
LOGIN_URL = 'users:login'
Teraz kiedy nie zalogowani, spróbujemy na przykład podejrzeć zamówienia. Zostaniemy przekierowani na stronę logowania, identycznie będzie z wywołaniem widoku new_order, czyli złożeniem nowego zamówienia oraz z jego edycją.
Drugi sposób w jaki możemy zabezpieczyć naszą aplikacje, jest wyświetlenie użytkownikom rzeczy stricte tyczących się ich. Powiedzmy że użytkownik powinien mieć wgląd w swoje zamówienia. Ta zmiana będzie wymagać trochę pracy, ale myślę że jest ciekawy do opisania przypadek, ponieważ musimy zmienić już wdrożony model danych. Dodać jedną kolumnę do tabeli, to nie jest problem jeśli tabela była by pusta, ale co jeśli dane do tej tabeli już zostały wprowadzone dane. Zaczniemy od zaimportowania modelu User w pliku pizzas/models.py.
from django.contrib.auth.models import User
Następnie dodamy do naszego modelu Order
nowe kolumnę o nazwie owner
, która
będzie kluczem obcym w modelu User dostarczanym przez Django.
class Order(models.Model): pizza = models.ForeignKey(Pizza, on_delete=models.CASCADE) size = models.CharField(max_length=10) sauce1 = models.CharField(max_length=10) sauce2 = models.CharField(max_length=10) deliver = models.CharField(max_length=10) date_order = models.DateTimeField(auto_now_add=True) owner = models.ForeignKey(User, on_delete=models.CASCADE) def __str__(self): return f"{self.pizza.name} - {self.size},{self.sauce1},\ {self.sauce2},{self.deliver} - {self.date_order}"
Aby wdrożyć nasze zmiany musimy wykonać migrację.
(project_env) xf0r3m@macbook:python/django$ python manage.py makemigrations pizzas
Podczas wykonywania tego polecenia Python wyświetli informacje o tym, że próbujemy dodać nie-zerowe pole do modelu Order bez wartości domyślnej i nie jest to możliwe, ponieważ zmiany muszą być również uwzględnione na istniejących już rekordach. Python daje nam do wyboru dwie opcje, albo podanie teraz wartość jaka zostanie przypisana w polu owner dla istniejących już rekordów, lub porzucenie migracji i dodanie wartości domyślnej w deklaracji pola w klasie modelu.
You are trying to add a non-nullable field 'owner' to order without a default; we can't do that (the database needs something to populate existing rows). Please select a fix: 1) Provide a one-off default now (will be set on all existing rows with a null value for this column) 2) Quit, and let me add a default in models.py Select an option: 2
Teraz wybierzemy opcje nr. 2. Jeśli podczas testowania rejestracji stworzyliśmy użytkownika to możemy mu nadać wszystkie zamówienia, models.ForeignKey() zawiera identyfikatory (id) z zewnętrznej tabeli. Aby dowiedzieć się jakie id mają nasi użytkownicy skorzystamy z pomocy powłoki Django.
(project_env) xf0r3m@macbook:python/django$ python manage.py shell
>>> from django.contrib.auth.models import User >>> users = User.objects.all() >>> for user in users: ... print(f"{user.id} {user.username}") ... 1 pizza_admin 2 xf0r3m
Na początku importujemy nasz model, następnie pobieramy wszystkie egzemplarze tego modelu aby poprzez iteracje wpisać id oraz nazwę użytkownika. Teraz wiemy, że nasz stworzony w celach testowych użytkownik ma id = 2. Oczywiście moglibyśmy to pominąć i domyślić się że musi mieć id = 2. Jednak chciałem przedstawić tutaj sposób w jaki możemy to sprawdzić aby mieć pewność. Również możemy wyszukać użytkownika za pomocą lekko zmodyfikowanego zapytania.
>>> user = User.objects.get(username='xf0r3m') >>> user.id 2
Teraz kiedy znamy już id możemy ponownie wydać polecenie migracji. Python znów nas zapyta o to samo, jednak teraz wybieramy opcje nr. 1 a następnie po znaku zachęty >>> wpiszemy id naszego użytkownika.
You are trying to add a non-nullable field 'owner' to order without a default; we can't do that (the database needs something to populate existing rows). Please select a fix: 1) Provide a one-off default now (will be set on all existing rows with a null value for this column) 2) Quit, and let me add a default in models.py Select an option: 1 Please enter the default value now, as valid Python The datetime and django.utils.timezone modules are available, so you can do e.g. timezone.now Type 'exit' to exit this prompt >>> 2
Teraz zmodyfikujemy widok order_views
,
tak aby wyświetlał zamówienia wszystkich użytkowników, jeśli zalogowanym
użytkownikiem jest pizza_admin
, czyli
nasz superużytkownik. A jeśli zalogowany jest kto inny, to widzi on
tylko swoje zamówienia.
@login_required def orders_view(request): from pizzas.models import Order if request.user.username != 'pizza_admin': orders = Order.objects.filter(owner=request.user).order_by('-date_order') else: orders = Order.objects.order_by('-date_order') context = {'orders': orders} return render(request, 'pizzas/orders_view.html', context)
Ważną rzeczą użytą w tym przypadku jest to, iż egzemplarz klasy
User zalogowanego użytkownika jest przechowywany w właściwości
user
obiektu
request
. Jednak chcąc
przyrównać nazwy użytkowników musimy użyć właściwości username.
Ze względu na iż zmodyfikowaliśmy model Order teraz nie możliwe
jest dodanie nowego zamówienia bez zmian uwzględniających właściciela.
Poniżej znajduje się kod widoku new_order uwzględniający właściciela.
@login_required def new_order(request): if request.method != 'POST': form = NewOrder() else: form = NewOrder(data=request.POST) if form.is_valid(): new_order = form.save(commit=False) new_order.owner = request.user new_order.save() return redirect('pizzas:index') context = {'form': form} return render(request, 'pizzas/new_order.html', context)
Z założeń wynika iż użytkownik raz przypisany do zamówienia jest nietykalny, nie musimy w ogóle zmieniać kodu widoku edit_order. Wprowadziłem lekką zmianę do szablonu orders_view.html aby wyświetlał datę, według narzuconego formatu.
{% extends 'pizzas/base.html' %} {% block content %} <table border="1"> <thead> <tr> <th>Numer zamówienia</th> <th>Pizza</th> <th>Wielkość</th> <th>Sos_1</th> <th>Sos_2</th&t; <th>Dostawa</th> <th>Data zamówienia</th> <th></th> </tr> </thead> <tbody> {% for order in orders %} <tr> <td>{{ order.id }}</td> <td>{{ order.pizza.name }}</td> <td>{{ order.size }}</td> <td>{{ order.sauce1 }}</td> <td>{{ order.sauce2 }}</td> <td>{{ order.deliver }}</td> <td>{{ order.date_order|date:'d M Y H:i' }}</td> <td><a href="{% url 'pizzas:edit_order' order.id %}">Zmień</a></td> </tr> {% empty %} <tr><td colspan="7">Brak zamówień</td></tr> {% endfor %} </tbody> </table> {% endblock content %}
Pionowa kreska |
oraz
date:'d M Y H:i'
są to filtry
znaczników szablonu, w tym przypadku jest filtr daty, który wyświetla
nam date w formacje 01 Jan 1970 00:00.
Aby upewnić się ze wszystko działa utwórzmy trzeciego użytkownika i złóżmy zamówienie. Użytkownicy powinni widzieć tylko swoje zamówienia, z kolei superużytkownik powinien widzieć je wszystkie. Ostatnią rzeczą jaka pozostała do zabezpieczenia, jest wyłączność na edycje zamówień przez superużytkownika. Zwykli użytkownicy nie powinni mieć dostępu do zmiany zamówienia. Zmian musimy dokonać w dwóch miejscach: w szablonie oraz w widoku edit_order. Najpierw rozpoczniemy od szablonu. Poniżej zmieniony kod szablonu
{% extends 'pizzas/base.html' %} {% block content %} <table border="1"> <thead> <tr> <th>Numer zamówienia</th> <th>Pizza</th> <th>Wielkość</th> <th>Sos_1</th> <th>Sos_2</th> <th>Dostawa</th> <th>Data zamówienia</th> {% if user.username == 'pizza_admin' %} <th></th> {% endif %} </tr> </thead> <tbody> {% for order in orders %} <tr> <td>{{ order.id }}</td< <td>{{ order.pizza.name }}</td> <td>{{ order.size }}</td> <td>{{ order.sauce1 }}</td< <td>{{ order.sauce2 }}</td> <td>{{ order.deliver }}</td> <td>{{ order.date_order|date:'d M Y H:i' }}</td> {% if user.username == 'pizza_admin' %} <td><a href="{% url 'pizzas:edit_order' order.id %}">Zmień</a></td> {% endif %} </tr> {% empty %} <tr><td colspan="7">Brak zamówień</td></tr> {% endfor %} </tbody> </table> {% endblock content %}
Dodaliśmy dwa znaczniki warunkowe szablonu, sprawdzające czy
uwierzytelniony użytkownik to
pizza_admin
. Teraz zajmiemy się kodem
widoku, na początek importujemy dwie rzeczy z modułu
django.http
.
from django.http import HttpResponseRedirect, Http404
Następnie w kodzie widoku dopisujemy warunek, który przyrówna
request.user.username
do ciągu
pizza_admin
jeśli się te wartości nie
będą się zgadzać wtedy zostanie zgłoszony wyjątek
Http404
. Poniżej znajduje się
kod zmodyfikowanej funkcji widoku.
@login_required def edit_order(request, order_id): if request.user.username != 'pizza_admin': raise Http404 from .models import Order order = Order.objects.get(id=order_id) if request.method != 'POST': form = NewOrder(instance=order) else: form = NewOrder(instance=order, data=request.POST) if form.is_valid(): form.save() return redirect('pizzas:index') context = {'form': form, 'order': order} return render(request, 'pizzas/edit_order.html', context)
2.7. Nadawanie stylu aplikacji - Bootstrap 4
Przed nami ostatni rozdział podróży przez Pythona oraz przez Django. Zostały jeszcze dwa zagadnienia, które należało by omówić w kontekście Django. Nadawanie stylu oraz oraz wdrożenie naszej aplikacji na platformie sieciowej Heroku. Zaczniemy od nadawania stylu, aby nasza aplikacja zaczęła wyglądać bardziej profesjonalnie za pomocą znanego zestawu narzędzi jakim jest Bootstrap 4. Dla naszej aplikacji skorzystamy z prostego motywu. Na początku instalujemy dodatek zawierający Bootstrap przygotowany do współpracy z Django. W aktywnym środowisku wirtualnym wydajemy polecenie:
(project_env) xf0r3m@macbook:python/django$ pip install django-bootstrap4
Po zainstalowaniu Bootstrap, dopisujemy go zainstalowanych aplikacji w
naszym projekcie. Plik pizzeria/settings.py, zmienna
INSTALLED_APPS
.
INSTALLED_APPS = [ 'users', 'pizzas', 'bootstrap4', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', ]
Teraz kiedy nasz Bootstrap jest już zainstalowany. Możemy podmienić kod z szablonu base.html na ten poniżej.
Najprościej rzecz ujmując, ten szablony stworzy bazową stronę naszej aplikacji. Na górze strony zostanie umieszczony responsywny pasek nawigacyjny, zawierający wszystkie nasze dotychczasowe odnośniki oraz nazwę aplikacji. Responsywność wygląda w ten sposób że jeśli strona zostanie wyświetlona na wąskim ekranie, odnośniki zostaną schowane w wysuwanym na dół menu. A na pasku zostanie wyświetlona nazwa aplikacji czy już w tym przypadku projektu oraz przycisk aktywujący wysunięcie menu. Poniżej znajduje się kod szablonu bazowego.
{% load bootstrap4 %} <!doctype html> <html lang="pl"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"> <title>Pizzas>/title< {% bootstrap_css %} {% bootstrap_javascript jquery='full' %} </head> <body< <nav class="navbar navbar-expand-md navbar-light bg-light mb-4 border"> <a class="navbar-brand" href="{% url 'pizzas:index' %}"< Dolce&Gusto </a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarCollapse" aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarCollapse"> <ul class="navbar-nav ml-auto"> {% if user.is_authenticated %} <li class="nav-item"< <span class="navbar-text"> Witaj, {{ user.username }} </span> </li> {% if user.username == 'pizza_admin' %} <li class="nav-item"> <a class="nav-link" href="{% url 'pizzas:orders' %}"> Zamówienia </a> </li> {% else %} <li class="nav-item"> <a class="nav-link" href="{% url 'pizzas:new_order' %}"> Złóż zamówienie </a> </li> <li class="nav-item"> <a class="nav-link" href="{% url 'pizzas:orders' %}"> Moje zamówienia </a> </li> {% endif %} <li class="nav-item"> <a class="nav-link" href="{% url 'users:logout' %}"> Wyloguj </a> <li> {% else %} <li class="nav-item"> <a class="nav-link" href="{% url 'users:register' %}"> Rejestruj </a> </li> <li class="nav-item"> <a class="nav-link" href="{% url 'users:login' %}"> Zaloguj </a< </li> {% endif %} </ul> </div> </nav> <main role="main" class="container"> <div class="pb-2 mb-2 border-bottom"< {% block page_header %} {% endblock page_header %} </div> <div> {% block content %}{% endblock content %} </div> </main> </body> </html>
W tym szablonie na samy początku ładujemy Bootstrap 4 aby
mieć możliwość skorzystania ze znaczników szablonu przygotowanych przez
tę aplikację. Następnie tworzona jest podstawowa struktura strony HTML,
z zaznaczeniem użycia języka polskiego w atrybucie
lang
w znaczniku otwierającym HTML, w
nagłówku strony - pomiędzy znacznikami
head
, przekazywane są informacje
odpowiedzialne za kodowanie strony <meta
charset="utf-8">
, responsywność
<meta name="viewport" ...>
,
ustawienie tekstu wyświetlanego w tytule karty lub/i okna przeglądarki
<title></title>
.
Następnie mamy dwa znaczniku szablonu Django, w których to ładowane są
pliki arkusza stylów CSS oraz biblioteki JavaScript ze wskazaniem o
użyciu pełnej biblioteki, a nie wersji odchudzonej (nie zawierającej
obsługi AJAX-a oraz efektów - takich rzeczy jak np. slideToggle).
W ciele strony - między znacznikami
<body>
umieszczamy nasz pasek
nawigacyjny <nav ...>
. Na pasku
nawigacyjnym umieszczamy odnośnik do strony głównej zatytułowany nazwą
projektu w Django, przycisk widoczny jedynie na małych ekranach,
zwężonych oknach przeglądarki lub dużych zoomach, powodujący wyjechanie
menu ukrytego w wyżej wymienionych sytuacjach zaraz pod przyciskiem
znajduje się znajduje się sekcja
<div>
z menu przedstawionym tutaj
za pomocą listy nieuporządkowanej. Wewnątrz listy znajdują się znaczniki
szablonu oraz odnośniki znane nam z poprzedniej wersji pliku
base.html. Pod paskiem nawigacji znajduje się element główny
naszej strony, zwierający dwa znaczniki sekcji.
Pierwszy z nich zawiera, parę znaczników szablonu
block page_header
, dzięki temu
zdefiniowany na innej stronie znaczniki block page_header
zostanie wstawiony w to miejesce, nieco enigmatyczne wydają się użyte w
tej sekcji klasy, pierwszy z nich pb-2
oznacza wewnętrzny odstęp dolny treści elementu krawędzi elementu
(padding-bottom) o wielkości
$spacer
* 0,5, gdzie $spacer
(zmienna SASS - język pre procesora kompilowany do CSS) przyjmuje wartość
16px. klasa pb istnieje w 5 wariantach. od * 0,25 od * 3.
Identycznie jest z drugą klasą, tylko że zamiast dopełnienia (odstęp
pomiędzy treścią elementu a jego krawędzią), mamy margines czyli odstęp
między krawędzią elementu a krawędzią następnego elementu, w naszym
przypadku mamy margines dolny, ponieważ po literce
m
występuje literka
b
następnie po myślniku mamy warjant,
klasa border-bottom powoduje nałożenie stylów zdefiniowanych w tej
klasie na dolną krawędź elementów.
Nie ma co rozwodzić się na wszystkimi użytymi tym przykładzie klasami.
Ponieważ ten kod w 90% został zaczerpnięty z przykładów znajdujących
się po tym adresem
https://getbootstrap.com/docs/4.5/examples/
jedne co zostało zmienione to podział elementu
main
na dwie sekcje div i nadanie
większego odstępu pierwszej sekcji od drugie wyżej wymienionymi stylami.
Teraz nadamy styl stronie głównej.
Poniżej prezentuje się kod szablonu strony głównej, w przeciwieństwie do poprzedniego nie jest tak obszerny.
{% extends 'pizzas/base.html' %} {% block page_header %} <div class="jumbotron"> <div class="container"> <h1 class="display-3">Dolce&Gusto</h1> <p>Pyszna pizza tylko na dowóz. Zamów już dziś.</p> <p>>a class="btn btn-primary btn-lg" href="{% url 'users:register' %}" role="button"<Zarejestruj się »</a></p> </div> </div> {% endblock page_header %}
Tutaj zostaje wykorzystany blok page_header
aby umieścić element nazwany przez Bootstap Jumbotronem. Jest to
wyróżniający się duży prostokąt, przeważnie z dużym tytułem, drobnym
opisem oraz łączem w postaci przycisku. "»" to encja HTML
(encje zawierają znaki specjalnej, cięzko jest wpisać za pomocą
klawiatury) przedstawiąca dwa połączone znaki większości lub jak kto woli
dwa prawe ostre nawiasty "»" . Przycisk kieruje na formularz rejestracji,
teraz sobie obstylujemy formularze. Styl dla formularzy rejestracji
będzie taki podstawowy, a styl dla formlarza logowania weźmiemy z
przykładów na stronie Bootstrap.
Poniżej znajduje się kod formularza rejestracji, jest to klasyczny książkowy przykład nadawania formularzom w Django stylów Bootstrap 4.
{% extends 'pizzas/base.html' %} {% load bootstrap4 %} {% block page_header %} <h2>Rejestracja</h2> {% endblock page_header %} {% block content %} <form action="{% url 'users:register' %}" method="post"> {% csrf_token %} {% bootstrap_form form %} {% buttons %} <button name="submit" class="btn btn-primary">Zarejestruj się</button> {% endbuttons %} <input type="hidden" name="next" value="{% url 'pizzas:index' %}" /> </form< {% endblock content %}
Ten formularz będzie nieco inny, dodamy obraz w bloku
page_header
. Musimy dodać również
dostosowane dlatego tego formularza style. W tym przykładzie warto
zwrócić uwagę na to że nie wykorzystaliśmy formularza przygotowanego
przez Django, skorzystaliśmy jedynie z nazw pól odpowiednich dla
formularza i przesłaliśmy nasz spersonalizowany formularz. Kod jest nieco
obszerny.
{% extends 'pizzas/base.html' %} {% block page_header %} <img src="https://i.imgur.com/olxvhk5.jpg" style="width: 100%;" /> {% endblock page_header %} {% block content %} <style> .form-signin { width: 100%; max-width: 330px; padding: 15px; margin: auto; } .form-signin .checkbox { font-weight: 400; } .form-signin .form-control { position: relative; box-sizing: border-box; height: auto; padding: 10px; font-size: 16px; } .form-signin .form-control:focus { z-index: 2; } .form-signin input[type="email"] { margin-bottom: -1px; border-bottom-right-radius: 0; border-bottom-left-radius: 0; } .form-signin input[type="password"] { margin-bottom: 10px; border-top-left-radius: 0; border-top-right-radius: 0; } </style> {% if form.errors %} <div class="alert alert-danger alert-dismissible fade show" role="alert"> Niepoprawny login lub hasło. Spróbuj ponownie. <button type="button" class="close" data-dismiss="alert" aria-label="Close"> <span aria-hidden="true">×</span> </button< </div> {% endif %} <form class="form-signin" action="{% url 'users:login' %}" method="post"> {% csrf_token %} <h1 class="h3 mb-3 font-weight-normal text-center">Zaloguj się</h1> <label for="username" class="sr-only">Nazwa użytkownika</label> <input type="text" id="username" name="{{form.username.html_name}}" class="form-control" placeholder="Nazwa użytkownika" required autofocus> <label for="form.password" class="sr-only">Hasło</label> <input type="password" id="password" name="{{form.password.html_name}}" class="form-control" placeholder="Hasło" required> <button class="btn btn-lg btn-primary btn-block" type="submit">Zaloguj</button> <p class="mt-5 mb-3 text-muted text-center">© 2020</p> <input type="hidden" name="next" value="{% url 'pizzas:index' %}" /> </form> {% endblock content %}
Z racji tego iż nasz formularz jest dostosowywany. Musieliśmy też dodać również własny komunikat o tym że logowanie się nie powiodło. Alert z możliwością zamknięcia znajduje się w pomiędzy warunkowymi znacznikami szablonu. Do obstylowania pozostała jeszcze tabelka z zamówieniami, formularz zamówienia, edycja zamówienia, menu, strona pizzy oraz strona wylogowania. Poniżej znajdują sią szablony obstylowanych już stron.
Strona podglądu zamówień:
{% extends 'pizzas/base.html' %} {% block page_header %} <img src="https://i.imgur.com/olxvhk5.jpg" style="width: 100%;" /< {% endblock page_header %} {% block content %} <table class="table"> <thead> <tr> <th scope="col">Nr zamówienia</th> <th scope="col">Pizza</th> <th scope="col">Wielkość</th;> <th scope="col">Sos_1</th> <th scope="col">Sos_2</th> <th scope="col">Dostawa</th> <th scope="col">Data zamówienia</th> {% if user.username == 'pizza_admin' %} <th></th> {% endif %} </tr> </thead> <tbody> {% for order in orders %} <tr> <th scope="row">{{ order.id }}</th> <td>{{ order.pizza.name }}>/td< <td>{{ order.size }}</td> <td>{{ order.sauce1 }}</td> <td>{{ order.sauce2 }}</td> <td>{{ order.deliver }}</td> <td<{{ order.date_order|date:'d M Y H:i' }}</td> {% if user.username == 'pizza_admin' %} <td><a href="{% url 'pizzas:edit_order' order.id %}">Zmień</a></td> {% endif %} </tr> {% empty %} <tr><td colspan="7">Brak zamówień</td></tr> {% endfor %} </tbody> </table> {% endblock content %}
Strona formularza zamówień:
{% extends 'pizzas/base.html' %} {% load bootstrap4 %} {% block page_header %} <img src="https://i.imgur.com/olxvhk5.jpg" style="width: 100%;" /> {% endblock page_header %} {% block content %} <form action="{% url 'pizzas:new_order' %}" method="post"> {% csrf_token %} {% bootstrap_form form %} {% buttons %} <button name="submit" class="btn btn-primary">Zamów</button> {% endbuttons %} </form> {% endblock content %}
Warto zaznaczyć że usunięto znacznik warunkowy, który wyświetlał informacje, o ile zapisanie zmian się nie powiodło, otóż jeśli wyświetlenie formularza pozostawiamy Bootstrapowi, to on sam wyświetli komunikat jeśli w formularzu pojawią się błędy.
Strona edycji zamówień:
{% extends 'pizzas/base.html' %} {% load bootstrap4 %} {% block page_header %} >img src="https://i.imgur.com/olxvhk5.jpg" style="width: 100%;" /> {% endblock page_header %} {% block content %} <form action="{% url 'pizzas:edit_order' order.id %}" method="post"> {% csrf_token %} {% bootstrap_form form %} {% buttons %} <button name="submit" class="btn btn-primary">Zapisz zmiany</button> {% endbuttons %} </form> {% endblock content %}
Strona menu:
{% extends 'pizzas/index.html' %} {% block page_header %} <img src="https://i.imgur.com/olxvhk5.jpg" style="width: 100%;" /> {% endblock page_header %} {% block content %} <table class="table"> <thead< <th scope="col">Pizza</th> <th scope="col">Składniki</th> <th scope="col"></th> </thead> <tbody> {% for pizza in pizzas %} <tr< <th scope="row">{{ pizza.name }}</td> <td>{{ pizza.topings }}</td> <td><a href="{% url 'pizzas:pizza_site' pizza.id %}">Wybierz</a></td> </tr> {% empty %} <tr>Nie ustalono jeszcze menu</tr> {% endfor %} </tbody> </table> {% endblock content %}
Strona pizzy:
{% extends 'pizzas/base.html' %} {% block page_header %} <img src="https://i.imgur.com/olxvhk5.jpg" style="width: 100%;" /> {% endblock page_header %} {% block content %} <h2>{{ name }}</h2> <ul class="list-group list-group-flush"> {% for toping in topings %} <li class="list-group-item">{{ toping }}<li> {% empty %} <li class="list-group-item">Dodatki na życzenie>/li> {% endfor %} </ul> {% endblock content %}
Strona wylogowania:
{% extends 'pizzas/base.html' %} {% block page_header %} <img src="https://i.imgur.com/olxvhk5.jpg" style="width: 100%;" /> {% endblock page_header %} {% block content %} <div class="alert alert-primary" role="alert"> Dziękujemy za skorzystanie z naszej aplikacji. </div> {% endblock content %}
2.8. Platforma Heroku
Teraz kiedy mamy obstylowaną naszą aplikacje, możemy wrzucić ją na platformę sieciową Heroku. Wdrożenie danych na platformę Heroku, będzie wymagało trochę zachodu. Na początku sprawdzimy czy w naszym systemie są dostępne pakiety, które umożliwią kompilacje instalowanego przez pip oprogramowania. Poniższa procedurę testowano na Ubuntu LTS Desktop 20.04, w terminalu wydajemy następujące polecenie.
$ sudo apt install python3-psycopg2 python3-dev libpq-dev postgresql-server-dev-all build-essential
Powyższe polecenie zainstaluje wybrane pakiety jeśli, któregoś brakuje w systemie. Aby móc w ogóle pracować z Heroku potrzebujemy założyć sobie na platformie konto, aby to zrobić przechodzimy pod ten adres: https://signup.heroku.com/.
Po założeniu konta możemy zainstalować heroku w naszym systemie.
$ sudo snap install --classic heroku
Kiedy nasze oprogramowanie zostanie zainstalowane, możemy przejść do katalogu naszą aplikacją i uruchomić środowisko wirtualne.
$ cd python/django3 python/django3 $ source blog_env/bin/activate
W środowisku wirtualnym instalujemy wymagane poniższe pakiety.
(blog_env) python/django3 $ pip install wheel (blog_env) python/django3 $ pip install psycopg2 (blog_env) python/django3 $ pip install django-heroku (blog_env) python/django3 $ pip install gunicorn
Aby nasza aplikacja mogła działać na platformie Heroku w sposób
identyczny jak na komputerze lokalnym, musi mieć zainstalowane takie
samo oprogramowanie jakie instalowaliśmy przez
pip
do tej pory. Nie było tego dużo,
django oraz django-bootstrap4. Jednak Heroku nie
rozwiąże sobie zależności samodzielnie, dlatego na liście trzeba
uwzględnić wszystko razem z zależnościami, to zadanie możemy wykonać
jednym poleceniem.
(blog_env) python/django3 $ pip freeze > requirements.txt
Plik musi nazywać requirements.txt. Tak wygląda jego zawrtość:
asgiref==3.2.10 beautifulsoup4==4.9.1 dj-database-url==0.5.0 Django==3.1 django-bootstrap4==2.2.0 django-heroku==0.3.1 gunicorn==20.0.4 psycopg2==2.8.5 pytz==2020.1 soupsieve==2.0.1 sqlparse==0.3.1 whitenoise==5.2.0
Uwaga, może się zdarzyć że w tym pliku znajdzie się taka
linia: pkg-resources==0.0.0
. Należy ją
usunąć, gdyż nasz projekt nie zostanie zbudowany na Heroku, bo nie będzie
mógł odnaleźć takiej zależności. Ta linia nie jest istotna, została
wpisana do pliku przez błąd pip
.
Po utworzeniu listy zależności należy wskazać Heroku, jakiej wersji Pythona ma używać dla naszego projektu, wersje sprawdzimy poniższym poleceniem.
(blog_env) python/django3 $ python --version Python 3.8.2
Natomiast zawartość pliku runtime.txt (nazwa pliku musi być identyczna) musi wyglądać następująco:
python-3.8.2
Python z małej litery i myślnik zamiast spacji. Nastepnym a zarazem ostatnim potrzebnym nam plikiem, będzie plik stricte Heroku Procfile, w nim zostanie zdefiniowany proces maszyny wirtualnej odpowiedzialny za hostowanie naszego projektu.
Zawrtość pliku Procfile
web: gunicorn blog.wsgi --log-file -
Teraz kiedy mamy wszystkie pliki możemy dopisać ustawienia dla Heroku w naszym projekcie. W głównym katalogu projektu w pliku settings.py na samym końcu pliku dopisujemy poniższe linie.
import django_heroku django_heroku.settings(locals())
Aby móc synchronizować nasz kod źródłowy z platformą Heroku skorzystamy z systemu kontroli wersji Git. Git w dużym skrócie tworzy zatwierdzenie (commit) zmian w kodzie, które są tak jakby oddzielną jego wersją. Projekty tworzą repozytoria, w których przechowywane są zatwierdzenia, każda nowa funkcjonalność może być (nawet dobrze, gdyby była) kolejnym zatwierdzeniem. Daje nam to możliwość powrotu do poprzedniej wersji w razie problemów. Git jest obecnie standardem zarządania kodem źródłowym, w nie długim czasie od publikacji tego materiału pojawi się kolejny tym razem dużo mniejszy posty odnośnie podstaw Git-a.
Git powinien być już zainstalowany w systemie. Jednak warto się upewnić wydając polecenie:
$ git --version
git version 2.25.1
Jeśli dostaniemy taki wynik polecenie, oznacza to że Git jest już zainstalowany w systemie. Instalacja na Ubuntu wymaga jednego polecenia:
$ sudo apt install git
Teraz możemy przejść do konfiguracji, musimy podać nazwę użytkownika oraz adres e-mail, który dokonuje zatwierdzeń, ponieważ Git to monitoruje, jeśli zapomnimy o tym kroku, Git będzie wymagał podania tych danych przy pierwszym przesłaniu kodu do zdalnego repozytorium na platformie Heroku.
$ git config --global user.name "xf0r3m" $ git config --global user.email "morketsmerke@gmail.com"
Po skonfigurowaniu gita możemy utworzyć puste repozytorium w katalogu z danymi projektu wydając bardzo proste polecenie
python/django3 $ git init
Powinniśmy dostać taki wyniki działania dla tego polecenia:
Initialized empty Git repository in /home/xf0r3m/python/django3/.git/
Teraz dodamy plik, w którym zdefiniujemy wszystkie pliki/katalogi, które mają być ignorowane przez Git podczas dodawania zmian do repozytorium. Tworzymy plik .gitignore, plik ten będzie ukryty z powodu kropki poprzedzającej nazwę pliku. W tym pliku wpisujemy katalog środowiska wirtualnego blog_env/, katalog __pycache__/ oraz wszystkie pliki baz danych *.sqlite3. Dlaczego? Otóż monitowanie plików lokalnej bazy danych jest generalnie złą praktyką i może doprowadzić do niezłych tarapatów, kiedy np. nadpiszemy bazę produkcyjną tą na platformie Heroku, bazą testową z Git-a. Dlatego pomijamy ten plik. Poniżej znajduje się zawartość pliku .gitignore
blog_env/ __pycache__/ *.sqlite3
Teraz kiedy mamy już zdefiniowany plik .gitignore, możemy przejść do pierwszego zatwierdzenia. W katalogu projektu wydajemy polecenie:
$ git add .
To polecenie doda do repozytorium wszystkie pliki projektu poza plikami i katalogami z pliku .gitignore.
$ git commit -am "Projekt gotowy do wdrożenia na platformie Heroku"
Powyższe polecenie powoduje właśnie zatwierdzenie. Opcja
-a
powoduje uzwględnienie wszystkich
zmodyfikowanych plików do zatwierdzenia, natomiast opcja
-m
powoduje dodanie komunikatu do
zatwierdzenia.
Po wydaniu powyższego polecenia warto wydać polecenie:
$ git status
Aby sprawdzić stan wykonanego zatwierdzania, wynik działania polecenia powinien wyglądać podobnie jak poniżej.
$ git status On branch master nothing to commit, working tree clean
Po wykonaniu pierwszego zatwierdzenia przyszedł czas na przekazanie projektu do Heroku. Teraz musimy przypomnieć sobie adres e-mail, na jaki zakładaliśmy konto Heroku oraz jakie było do niego hasło. Jeśli mamy już te dane w pobliżu wydajemy polecenie
python/django3 $ heroku login heroku: Press any key to open up the browser to login or q to exit: Logging in... done Logged in as user@example.com
Po wydaniu powyższego polecenia, zgodnie z instrukcją naciskamy dowolny
klawisz poza literą q
, aby otworzyła
nam się przeglądarka. Tam podajemy dane dostępowe do Heroku, po poprawny
zalogowaniu, zostanie wyświetlony komunikat o poprawyn zalogowaniu oraz
o tym że możemy już zamknąć okno przeglądarki. W terminalu pod instrukcją
wyświetloną przez polecenie, zostaną dopisane pozostałe dwie linie takie
jak przedstawione powyżej.
Po zalogowaniu możemy już utworzyć nową aplikację wydając w terminalu poniższe polecenie.
python/django3 $ heroku create Creating app... done, ⬢ dry-atoll-67152 https://dry-atoll-67152.herokuapp.com/ | https://git.heroku.com/dry-atoll-67152.git
W odpowiedzi na to polecenie otrzymamy nazwę nowo utworzonej aplikacji na
Heroku, potem zmienimy ją na bardziej przystępną, oraz dwa adresy. Jeden
pod którym aplikacja jest dostępna w internecie, natomiast drugi to adres
zdalnego repozytorium Git, na który będziemy wpychać
zatwierdzenia kodu naszej aplikacji. Adres Git dla naszej aplikacji
Heroku został już automatycznie zdefiniowany jako adres zdalnego
repozytorium pod nazwą heroku
.
Dokonajmy teraz pierwszego wepchnięcia oraz zbudujmy naszą aplikacje na
platformie Heroku. Budowa następuje automatycznie po wepchnięciu.
python/django3 $ git push heroku master
Po wydaniu tego polecenia na ekranie zostanie wyświetlony cały proces budowania aplikacji w Heroku.
Enumerating objects: 46, done. Counting objects: 100% (46/46), done. Delta compression using up to 4 threads Compressing objects: 100% (39/39), done. Writing objects: 100% (46/46), 9.12 KiB | 1.14 MiB/s, done. Total 46 (delta 6), reused 0 (delta 0) remote: Compressing source files... done. remote: Building source: remote: remote: -----> Python app detected remote: ! Python has released a security update! Please consider upgrading to python-3.8.5 remote: Learn More: https://devcenter.heroku.com/articles/python-runtimes remote: -----> Installing python-3.8.2 remote: -----> Installing pip 20.1.1, setuptools 47.1.1 and wheel 0.34.2 remote: -----> Installing SQLite3 remote: -----> Installing requirements with pip remote: Collecting asgiref==3.2.10 remote: Downloading asgiref-3.2.10-py3-none-any.whl (19 kB) remote: Collecting beautifulsoup4==4.9.1 remote: Downloading beautifulsoup4-4.9.1-py3-none-any.whl (115 kB) remote: Collecting dj-database-url==0.5.0 remote: Downloading dj_database_url-0.5.0-py2.py3-none-any.whl (5.5 kB) remote: Collecting Django==3.1 remote: Downloading Django-3.1-py3-none-any.whl (7.8 MB) remote: Collecting django-bootstrap4==2.2.0 remote: Downloading django_bootstrap4-2.2.0-py3-none-any.whl (24 kB) remote: Collecting django-heroku==0.3.1 remote: Downloading django_heroku-0.3.1-py2.py3-none-any.whl (6.2 kB) remote: Collecting gunicorn==20.0.4 remote: Downloading gunicorn-20.0.4-py2.py3-none-any.whl (77 kB) remote: Collecting psycopg2==2.8.5 remote: Downloading psycopg2-2.8.5.tar.gz (380 kB) remote: Collecting pytz==2020.1 remote: Downloading pytz-2020.1-py2.py3-none-any.whl (510 kB) remote: Collecting soupsieve==2.0.1 remote: Downloading soupsieve-2.0.1-py3-none-any.whl (32 kB) remote: Collecting sqlparse==0.3.1 remote: Downloading sqlparse-0.3.1-py2.py3-none-any.whl (40 kB) remote: Collecting whitenoise==5.2.0 remote: Downloading whitenoise-5.2.0-py2.py3-none-any.whl (19 kB) remote: Building wheels for collected packages: psycopg2 remote: Building wheel for psycopg2 (setup.py): started remote: Building wheel for psycopg2 (setup.py): finished with status 'done' remote: Created wheel for psycopg2: filename=psycopg2-2.8.5-cp38-cp38-linux_x86_64.whl size=483298 sha256=7daad67c78674aaee0b224f104bccb4ab6a0dbc40682c37b80f6f6e5572f5f7e remote: Stored in directory: /tmp/pip-ephem-wheel-cache-105idbzs/ wheels/35/64/21/9c9e2c1bb9cd6bca3c1b97b955615e37fd309f8e8b0b9fdf1a remote: Successfully built psycopg2 remote: Installing collected packages: asgiref, soupsieve, beautifulsoup4, dj-database-url, pytz, sqlparse, Django, django-bootstrap4, whitenoise, psycopg2, django-heroku, gunicorn remote: Successfully installed Django-3.1 asgiref-3.2.10 beautifulsoup4-4.9.1 dj-database-url-0.5.0 django-bootstrap4-2.2.0 django-heroku-0.3.1 gunicorn-20.0.4 psycopg2-2.8.5 pytz-2020.1 soupsieve-2.0.1 sqlparse-0.3.1 whitenoise-5.2.0 remote: -----> $ python manage.py collectstatic --noinput remote: 132 static files copied to '/tmp/build_f49f7b9a/staticfiles', 418 post-processed. remote: remote: -----> Discovering process types remote: Procfile declares types -> web remote: remote: -----> Compressing... remote: Done: 56.1M remote: -----> Launching... remote: Released v5 remote: https://dry-atoll-67152.herokuapp.com/ deployed to Heroku remote: remote: Verifying deploy... done. To https://git.heroku.com/dry-atoll-67152.git *[new branch] master -> master
Jeśli otrzymaliśmy podobny input, to znaczy że cały proces przebiegł prawidłowo. Teraz jeśli wydamy polecenie:
python/django3 $ heroku open
Zostanie otworzona przeglądarka z nasza aplikacją w Heroku. Jednak,
jeśli klikniemy w odnośnik 'Menu', zostanie nam zrócona strona błedu
ProgrammingError, oznacza to mniej więcej tyle i aż tyle że nie
ma bazy danych. Utworzenie bazy danych oraz dodanie superużytkownika, to
dwie kolejne czynności, które możemy wykonać dwa różne sposoby.
Przedstawie je poniżej, aby byśmy zwrócili uwagę na to że z aplikacjami
na Heroku można wchodzić w interakcje w dwojaki sposób. Pierwszy z nich
to wydawanie konkretnych poleceń poprzedzając nasze polecenie słowami
heroku run
. Uruchamiamy daną czynność
tak samo jak w środowisku wirtualnym na naszym lokalnym komputerze. Tak
też stworzymy bazę danych.
python/django3 $ heroku run python manage.py migrate Running python manage.py migrate on ⬢ dry-atoll-67152... up, run.7671 (Free) Operations to perform: Apply all migrations: admin, auth, blogs, contenttypes, sessions Running migrations: Applying contenttypes.0001_initial... OK Applying auth.0001_initial... OK Applying admin.0001_initial... OK Applying admin.0002_logentry_remove_auto_add... OK Applying admin.0003_logentry_add_action_flag_choices... OK Applying contenttypes.0002_remove_content_type_name... OK Applying auth.0002_alter_permission_name_max_length... OK Applying auth.0003_alter_user_email_max_length... OK Applying auth.0004_alter_user_username_opts... OK Applying auth.0005_alter_user_last_login_null... OK Applying auth.0006_require_contenttypes_0002... OK Applying auth.0007_alter_validators_add_error_messages... OK Applying auth.0008_alter_user_username_max_length... OK Applying auth.0009_alter_user_last_name_max_length... OK Applying auth.0010_alter_group_name_max_length... OK Applying auth.0011_update_proxy_permissions... OK Applying auth.0012_alter_user_first_name_max_length... OK Applying blogs.0001_initial... OK Applying blogs.0002_blogpost_owner... OK Applying sessions.0001_initial... OK
Jak można zauważyć na początku trwa nawiązywanie połączenia z naszym kontenerem (naszą aplikacją), a następnie wykonywane jest polecenie. Drugim sposobem jest uruchomienie powłoki w środowisku kontenera Heroku, za pomocą tej metody utworzymy superużytkownika. Aby uruchomić powłokę należy wydać następujące polecenie.
python/django3 $ heroku run bash Running bash on ⬢ dry-atoll-67152... up, run.8503 (Free) ~ $
Po połączeniu zostanie na zrócony taki charakterystyczny, krótki znak zachęty. Zaraz zanim wydajemy polecenie, takie jakie wydawaliśmy podczas tworzenie superużytkownika na naszym komputerze.
~ $ python manage.py createsuperuser Użytkownik (leave blank to use 'u17122'): blog_admin Adres email: Password: Password (again): Superuser created successfully.
Teraz możemy przjeść do naszej aplikacji oraz móc sprawdzić moduł logowania oraz zalogować się do witryny administracyjnej aby zdefiniować trochę danych.
Teraz kiedy nasza aplikacja jest gotowa, możemy przjeść do nadania jej nieco przyjaźniejszej nazwy. Zmianę nazwy realizujemy za pomocą polecenia:
python/django3 $ heroku apps:rename blogs-morkme-v01
Pamiętać jednak należy o tym że podczas nadawania nazw w Heroku możemy korzystać jedynie z liter, cyfr oraz myślników.
Teraz przejdziemy do zabepieczenia naszej aplikacji. Zwróćmy uwagę na to, że gdy w plikach naszego projektu na Heroku zabrakło bazy danych, została na zwrócona strona z opisem błędu ProgrammingError, ta strona została przygotowana przez Debuger. W środowisku testowym jest powszechne i często spotykane zjawisko, że w razie błędów zostanie wyświetlona strona z informacjami o tym gdzie szukać danego błędu, jednak w środowisku produkcyjnym takim jak Heroku jest to niedopuszczalne, takie strony zazwyczaj zwracają za dużo newralgicznych danych i mogą być wykorzystane do ataków przeciwko nam. Na podstawie zmiennej środowiskowej będziemy regulować wyświetlanie stron debugera. Z racji tego iż w Django zmienna Debug przyjmuje wartość boolowską, a z kolei zmienne środowiskowe przechowują ciągi tekstowe, będzie nam potrzebna konstrukcja if, którą zaimplementujemy w pliku settings.py w głównym katalogu projektu.
Naszą implementacje zaczynamy od zaimportowania obiektu
environ
modułu
os
na górze pliku. Następnie na samym
dole pliku wprowadzamy poniższy kod.
if environ.get('DEBUG') == 'TRUE': DEBUG = True elif environ.get('DEBUG') == 'FALSE': DEBUG = False
Za pomocą metody get
obiektu
environ, pobieramy wartość zmiennej środowiskowej, której nazwa
jest pobierana jako ciąg tekstowy w pierwszym argumencie. W zależności od
wyniku przyrównania wartości w zmiennej
DEBUG
nadawana jest wartość boolowska
True
or
False
. Z racji tego iż przesłaliśmy plik
settings.py, wraz ustawioną opcją DEBUG na
True. To teraz musimy ją zmienić w naszej aplikacji na
platformie Heroku, możemy tego dokonać za pomocą jednego polecenia.
python/django3 $ heroku config:set DEBUG=FALSE
To polecenie spowoduje ustawienie podanej zmiennej środowiskowej na
podaną wartość oraz restart aplikacji. Teraz otworzymy naszą aplikację za
pomocą polecenia heroku open
i
przejdziemy pod stronę, której nie zaimplementowano w naszej aplikacji.
Powinniśmy otrzymać typowy komunikat Not Found. Ten komunikat
nie pasuje do szaty graficznej naszej aplikacji, my jednak możemy dodać
własne szablony dla typowych błędów. Szablony będą banalne, ale będą
miały jedną ważną cechę. Będą ładować podstawowy wygląd naszej aplikacji.
Zaczynamy od utworzenia katalogu templates w głównym katalogu
projektu. W tym katalogu tworzymy dwa pliki: 404.html oraz
500.html. Poniżej zamieszczam zawartość każdego szablonu.
Plik 404.html
{% extends 'blogs/base.html' %} {% block page_header %} <h2<Żądana strona nie zostałą odnaleziona (404).</h2> {% endblock page_header %}
Plik 500.html
{% extends 'blogs/base.html' %} {% block page_header %} <h2>Wystąpił wewnętrzny błąd serwera (500).</h2> {% endblock page_header %}
Teraz wracamy do naszego pliku settings.py w nim ustawiamy jedną rzecz. Na początku importujemy obiekt path z modułu os. Następnie odszukujemy klucz DIRS w słowniku wenątrz listy TEMPLATES. Następnie wewnątrz listy DIRS wpisujemy jedną wartość zwracaną przez metodę join obiektu path, która jako argumenty przyjmuje ścieżkę bazową BASE_DIR oraz ścieżkę dostępu do szablonów względem ścieżki bazowej. Trochę to zagmatwane, ale jak spojrzymy w górę kodu dostrzerzemy fakt iż BASE_DIR to zmienna, którą możemy sobie odtworzyć w powłoce. Jako__file__ podstawimy ścieżke do dostępową do pliku settings.py względem naszego katalogu głównego z plikami, gdzie znajdują katalog projektu, katalogi aplikacji oraz katalog ze środowiskiem wirtualnym. Poniżej zamieściłem listing z konsoli jak możemy sprawdzić naszą ścieżkę.
>>>from pathlib import Path >>> base_dir = Path('blog/settings.py').resolve(strict=True).parent.parent >>> base_dir PosixPath('/home/xf0r3m/python/django3')
Teraz znając już wartość naszej zmiennej BASE_DIR możemy perfekcyjnie ustalić ścieżke dostępową do naszych szablonów.
Warto pamiętać o tym, że te szablony będą wyświetlane tylko wtedy, gdy
opcja DEBUG jest ustawiona na False. Możemy je przetestować
żądając strony, której nie ma, otrzymamy stronę błędu 404. Natomiast
jeśli spróbujemy edytować zamówienie, którego niema to w ten czas
otrzymamy stronę błędu 500. Skupmy się przez chwilę na tym drugim
przypadku, kiedy chcemy edytować zamowienie, którego nie ma.
Przezkazujemy za pomocą metody GET identyfikator zamówienia,
kiedy widok próbuje wydobyć informacje odnośnie rekordu o takim
identyfikatorze otzymuje wartość None oznacza to tyle, że nie ma
takiego rekordu, jeśli więc nie ma takiego rekordu to żądana strona
edycji zamówienia nie może istnieć, więc lepiej użyć strony 404 niż 500.
Można to zrealizować wykorzystując funkcję
get_object_or_404
z tego samego modułu
django.shortcuts
, z którego
importowaliśmy funkcje render oraz redirect, następnie
możemy jej użyć w każdym widoku wymagającym identyfikatora np. w
zapytaniach do bazy.
# order = Order.objects.get(id=order_id) order = get_object_or_404(Order, id=order_id)
Zbliżamy się do końca tego materiału, jednak zanim się rozejdziemy dalej poznawać świat Pythona, posprzątamy po sobie. Jednak jeśli chcesz możesz w ramach ćwiczenia rozwinąć projekt Pizzeria do stanu użytczeności.
Jednak jeśli ta aplikacja, która była z nami przez dłuższą część nauki frameworka Django przestała być potrzebna, to możemy ją usunąć z platformy wydajac polecenie:
python/django2 $ heroku apps:destroy pizzeria-morkme-1a ▸ WARNING: This will delete ⬢ pizzeria-morkme-1a including ▸ all add-ons. ▸ To proceed, type pizzeria-morkme-1a or re-run this ▸ command with --confirm pizzeria-morkme-1a >
Heroku zarząda od nas potwierdzenia, w postaci ponownego wprowadzenia nazwy aplikacji. Po jej podaniu, otrzymamy informacje poprawnym zniszczeniu (usunięciu) aplikacji na platformie Heroku.
> pizzeria-morkme-1a Destroying ⬢ pizzeria-morkme-1a (including all add-ons)... done
Jeśli chodzi o podstawy Pythona oraz Django to wydaje mi się, że mamy je już za sobą.