Lekcja 4. Obiekty.¶
Nauka składni języka programowania nie powinna trwać w nieskończoność. W końcu nie uczymy się języka programowania dla samego języka. Celem, jest umiejętność rozwiązywania problemów, czy może raczej budowanie programów, które problemy rozwiązują. Obecnie znacie już zmienne, ich typy i literały na jakie wskazują, instrukcje sterujące tokiem programu oraz funkcje. Dzięki temu jesteście już w stanie zaatakować dowolny problem, zaprojektować program który go rozwiązuje i ostatecznie podać odpowiedź.
Ta lekcja będzie nieco inna. Omawiane tutaj zagadnienia nie są niezbędne do budowania skutecznych programów. Będzie to jedynie delikatne (na tyle na ile jesteśmy w stanie) wprowadzenie do programowania zorientowanego obiektowo (PZO). Jest to też ostatnia lekcja traktująca jedynie o składni języka Python.
Wszystko jest obiektem¶
Kluczem do zrozumienia PZO jest myślenie o obiektach jako o zbiorach zawierających
zarówno dane jak i metody (czy funkcje) które na tych danych operują. W języku Python
wszystko jest obiektem - każdy literał, operator, funkcja. Za każdym obiektem stoi
specyficzna klasa. Aby taką klasę zdefiniować używamy słowa kluczowego class
class NazwaKlasy:
ciało klasy
Tak naprawdę to w tym skrypcie posługujemy się klasami już od samego początku. Używaliśmy
ich przykładowo do rozszerzania list za pomocą metody append
, czy też zliczania
obiektów za pomocą count
. W obu przypadkach do danej metody odwoływaliśmy się poprzez
zapis z kropką
Jak widać wywołanie metody wygląda po kropce dokładnie tak samo jak wywołanie funkcji -
używamy jej nazwy append
oraz podajemy jej argument otoczony nawiasami okrągłymi.
Tak naprawdę owa metoda to nic innego jak funkcja sparowana z danym obiektem. OK,
wystarczy już teorii - napiszmy jakąś własną, prostą klasę.
Jak widać w środku klasy WitajSwiecie
mamy zdefiniowaną funkcję czesc(self)
.
Jej konstrukcja jest taka sama jak każdej innej funkcji - zawiera słowo def
, po nim
następuje nazwa funkcji oraz lista argumentów w nawiasach. Jedyną różnicą jest pojawienie
się słowa self
w definicji funkcji, które nie pojawia się jednak podczas jej wywołania,
w ostatniej linii powyższego kodu. Aby nie wnikać zbytnio w szczegóły, to wystarczy
wiedzieć, że mechanizmy klas przypisują do argumentu self
nazwę witaj
, czyli
nazwę instancji klasy WitajSwiecie
. Instancję czasami nazywa się konkretem,
bowiem konkretyzuje daną klasę budując obiekt. W powyższym przykładzie tworzymy
jedną instancję klasy WitajSwiecie
i nazywamy ją witaj
(linia 7). W kolejnej
linii wywołujemy funkcję czesc
właśnie dla instancji witaj
poprzez notację z
kropką. To ta funkcja (a raczej metoda) produkuje napis "Witaj Swiecie!"
widoczny
po uruchomieniu kodu.
Wskazówka
Metoda a funkcja
Aby nieco usystematyzować wiedzę zapamiętaj po prostu, że gdy programujesz daną
klasę KlasaABC
to w jej ciele implementujesz funkcje, np. xyz(self)
.
W chwili, gdy tworzysz instancję klasy poprzez operator przypisania (linia 8)
to odwołując się do xyz()
w linii 9 wywołujesz już metodę dla instancji.
Owa metoda to taka kopia funkcji xyz(self)
, gdzie self
jest już
ustalone i jest równe self = obiekt
.
1 2 3 4 5 6 7 8 9 | class KlasaABC:
...
def xyz(self): # to jest funkcja o nazwie 'xyz'
...
obiekt = KlasaABC()
obiekt.xyz() # to jest wywolanie metody 'xyz' dla instancji 'obiekt'
|
Skonstruujemy teraz, nieco bardziej skomplikowaną klasę, która stworzy nam nowy typ danych
- prostokąt. Samą klasę nazwiemy Prostokat
. Nazywanie klas z dużych liter, w konwencji
CamelCase, to dobra praktyka, zalecana przez programistów Pythona. Klasa będzie miała
minimalną funkcjonalność: będzie obliczać pole i obwód prostokątów.
Poeksperymentuj z powyższą klasą. Zbuduj inne prostokąty. Jak już skończysz, przeczytaj poniższy akapit. Powinien rozjaśnić co i jak działa w takiej klasie.
Funkcja __init__
¶
Przede wszystkim pojawiły się dziwne funkcje zaczynające się od znaku podłogi
_
lub też zaczynające i kończące się na dwóch takich znakach __nazwa__
.
Zaczniemy od tych drugich.
Funkcja specjalna __init__
to funkcja wywoływana podczas instancjonowania
(konkretyzacji) danej klasy, czyli po interpretacji linii:
p1 = Prostokat(1, 2)
W tym momencie tworzymy nowy obiekt klasy Prostokat
. Ma on nazwę p1
.
Posiada też atrybuty p1.x
i p1.y
. Jak widać powyżej, możemy się do nich
odwoływać, używając nazwy instancji p1
, kropki oraz nazwy atrybutu, np: x
p1.x
Do zmiennej p1.x
przypisana została liczba 1
, a do p1.y
liczba 2
.
W definicji klasy Prostokat
próżno takich nazw szukać. Jedyne co wyglądem
zbliżonego widzimy to zmienne self.x
czy self.y
w środku funkcji __init__
. To
są dokładnie te same zmienne, ale jak już pisaliśmy wcześniej, nazwa self
została
zmieniona na p1
, w czasie procesu instancjonowania. Oczywiście dla kolejnej instancji
p2 = Prostokat(2, 2)
ta nazwa to p2
. Cokolwiek nie wpiszemy do funkcji __init__
zostanie zinterpretowane w momencie instancjonowania.
Kolejną funkcją o podwójnych podogach jest funkcja __str__
. Ma ona na celu zaprogramowanie,
jak będzie wyglądał obiekt klasy Prostokat
w momencie wyświetlania go komendą print
.
Funkcja ta musi zwracać literał znakowy.
Więcej o tego typu funkcjach znajdziecie w Dodatku 2.
Funkcje (pseudo-)prywatne¶
Czasami chcemy ukryć pewnego rodzaju funkcjonalność przed użytkownikami klasy. Nie dlatego, że boimy się, że użytkownik coś zepsuje, ale raczej dlatego, że potrzebujemy czasem funkcji technicznych, tworzonych na potrzeby samej klasy. W wielu językach, będą do tego służyły funkcje prywatne, do których dostęp spoza klasy jest zabroniony. W języku Python dostęp do takich funkcji jest co najwyżej utrudniony, ale nie jest zabroniony. Funkcje prywatne definiujemy na dwa sposoby
- za pomocą pojedynczej podłogi przed nazwą funkcji, np:
_nazwa
- za pomocą podwójnego znaku podłogi przed nazwą funkcji, np:
__nazwa
W klasie Prostokat
mamy właśnie funkcję _spr
, która stworzona została tylko
na potrzeby sprawdzania czy podane boki prostokąta są dodatnie. Jeżeli jeden z nich nie
jest, funkcja zwróci fałsz. Używamy jej podczas instancjonowania w połączeniu z instrukcją
assert
. Ma ona zapewniać, że warunek będzie prawdziwy. Jeżeli nie - program się zatrzyma -
podnosząc wyjątek. Jako, że nie chcemy takiej funkcjonalności udostępniać
poza samą klasą, dlatego decydujemy się na stworzenie jej jako funkcji prywatnej.
Oczywiście, dostęp do niej dalej istnieje, ale nie będzie się ona pojawiać w podpowiedziach
metod (po naciśnięciu klawisza tabulacji w Jupyterze).
Przed wami zadanie: rozszerzcie możliwości klasy Prostokat
.
Zadanie 4.1
Zadanie polega na rozszerzeniu tej klasy o kilka nowych funkcji. Dodaj funkcję obliczającą długość przekątnej czy też funkcję zwracającą dłuższy (krótszy) bok.
Dziedziczenie¶
Wiele typów danych ma takie same własności. Przykładowo funkcja len
będzie działała
w identyczny sposób zarówno dla krotek jak i dla łańcuchów znaków. W obu przypadkach
poda nam długość sekwencji. Oznacza to, że funkcja len
nie jest specyficzna dla
list czy łańcuchów, ale jest pewną ogólną własnością sekwencji. Można też na ten problem
spojrzeć odwrotnie - listy są pewnymi specyficznymi sekwencjami, lub że sekwencje
są pewnym szerszym, abstrakcyjnym typem danych.
Przekazywanie pewnych specyficznych własności do innych klas nazywamy dziedziczeniem.
W języku Python klasa NowaKlasa
może dziedziczyć, przejmować własności klasy StaraKlasa
za pomocą prostego mechanizmu
class NowaKlasa(StaraKlasa):
...
Bez zbytnich dywagacji teoretycznych przejdźmy do przykładu. Napiszemy klasę Kwadrat
która będzie dziedziczyć z powyżej stworzonej klasy Prostokat
. Nie jest to trudne zadanie.
Kwadrat to specjalny typ prostokąta, który oba boki ma równe. Wystarczy więc podać jeden bok,
by taki kwadrat zdefiniować. Drugi musi być równy pierwszemu.
Jak widać, żeby zbudować klasę potomną (lub podrzędną) Kwadrat
, wystarczy odziedziczyć
wszystkie atrybuty klasy Prostokąt
i lekko przedefiniować funkcję inicjalizującą
__init__
, tak by obsługiwała tylko jeden parametr x
. Dodatkowo, zmieniliśmy
pole klasy _nazwa
, aby prawidłowo wskazywało na nazwę nowego obiektu.
Prawda, że proste?
Wszystkie inne funkcje, zostały przekopiowane do nowej klasy.
Zadanie 4.2
Możesz teraz poeksperymentować
z nową wiedzą i spróbować zbudować klasę Trapez
. Będzie to uogólnienie klasy
Prostokat
. Zastanów się która klasa będzie dziedziczyć z której. W ten sposób
zbudujesz hierarchię klas.
Na koniec kilka mniej lub bardziej użytecznych wiadomości o klasach, ich nazwach, przestrzeni nazw oraz informacji o klasach nadrzędnych.
Klasa.__bases__
zwraca nazwy klas nadrzędnych, z których dziedziczy klasaKlasa
Klasa.__dict__.keys()
zwraca atrybuty klasyinstancja.__class__
zwraca wskaźnik do klasy danej instancjiinstancja.__class__.__name__
zwraca nazwę klasyinstancja.__dict__.keys()
to lista atrybutów instancji (nie klasy)dir(instancja)
zwraca wszystkie artybuty instancji oraz artybuty odziedziczone z innych, nadrzędnych klas (jeżeli są)