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

Definiowanie klasy w języku Python.
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 klasa Klasa
  • Klasa.__dict__.keys() zwraca atrybuty klasy
  • instancja.__class__ zwraca wskaźnik do klasy danej instancji
  • instancja.__class__.__name__ zwraca nazwę klasy
  • instancja.__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ą)
Next Section - Zadania do lekcji 4