Blog JSystems - z miłości do programowania

Obiektowość w Pythonie



Klasy pozwalają nam deklarować własne obiekty złożone posiadające wbudowane pola a także funkcje.


 


Deklaracja klasy i pola


Klasy mogą posiadać pola, funkcje, konstruktory. Aby powołać do życia obiekt i korzystać z tych wszystkich dobrodziejstw, trzeba najpierw zadeklarować klasę. Klasę dodajemy w zasadzie w dowolnym miejscu pliku, jednak korzystać z niej będziemy mogli tylko w liniach znajdujących się pod jej deklaracją. Z tego też powodu najlepiej deklarować klasy na początku pliku (lub w ogóle w osobnym pliku który następnie będziesz importować). Elementy związane z klasą rozpoznać można po ich odsunięciu od lewej krawędzi, podobnie jak elementy związane z pętlą czy instrukcjami warunkowymi. W pierwszej kolejności zadeklarujemy prostą klasę posiadającą jedynie pola:


class Osoba:
    imie='Andrzej'
    nazwisko='Klusiewicz'
    wiek=33
    pustePole=None


Obiekty powyższej klasy będą posiadały 4 pola. Każdy nowo powołany do życia obiekt tej klasy bedzie posiadał już przypisane wartości do 3 z 4 pól. Jeśli chesz by do jakiegoś pola nie zostało przypisane nic, wystarczy przypisać "None". Aby stworzyć obiekt takiej klasy używamy tak zwanego konstruktora:


o=Osoba()
print(o.imie,o.nazwisko,o.wiek, o.pustePole)


Po stworzeniu obiektu "o" klasy Osoba, wypisuje zawartość jego pól.  Domyślnie możemy sięgać do wszystkich pól, czyli jest to odpowiednik "public" znanego Ci drogi czytelniku być może z języków Java czy C#. Efektem działania powyższego kodu jest następujący tekst na konsoli:


Andrzej Klusiewicz 33 None


Klasę możesz umieścić również w innym module i zwyczajnie zaimportować:


from inna import Osoba
o=Osoba()
print(o.imie,o.nazwisko,o.wiek, o.pustePole)


Zawartość pól w obiektach można oczywiście również podmieniać:


class Osoba:
    imie='Andrzej'
    nazwisko='Klusiewicz'
    wiek=33
    pustePole=None

o=Osoba()
o.imie='Krzysztof'
o.nazwisko='Jarzyna'
print(o.imie,o.nazwisko)


W polach imie i nazwisko obiektu o znajdowały się dotychczas dane autora niniejszego kursu, aktualnie zostały one zastąpione przez Krzysztofa Jarzynę (ze Szczecina ;) ).  Po uruchomieniu powyższego kodu zostaje wyświetlone już nowe imię i nazwisko:


Krzysztof Jarzyna


Pewnym zaskoczeniem dla programistów innych języków może być wynik działania poniższego kodu:


class Osoba:
    imie=None
    nazwisko=None
    ksywka=None
o1=Osoba()
o1.imie='Jerzy'
o1.nazwisko='Kiler'
o1.ksywka='Killer'
o2=Osoba()
o2.imie='Stefan'
o2.nazwisko='Siarzewski'
o2.ksywka='Siara' #i wszystko jasne
Osoba.imie='zamienione...'
print(o1.imie,o2.imie)
o3=Osoba()
print(o3.imie)


I tu pytanie do programistów - jakiego wyniku się spodziewacie? Można by przypuszczać że pole "imie" jest z jakiegoś powodu statyczne skoro można przypisać wartość na zasadzie "Klasa.pole=xxx", ale nie bo przecież przypisujemy wartość do pola obiektu również tak "obiekt.pole=xxx" w odniesieniu do tego samego pola... Taki unikatowy przypadek w Pythonie :) Najpierw zobaczmy co pojawia się na konsoli:


Jerzy Stefan


zamienione...


Teraz o co chodzi - od momentu przypisania wartości niejako do pola klasy, wszystkie obiekty tej klasy stworzonego od tego momentu (do końca pliku OFC), będą miały nową wartość domyślną w tym polu.


Sprawa powinna się troszkę przejaśnić gdy uruchomisz to:


class Osoba:
    imie='Andrzej'
print(Osoba.imie)


W wyniku działania tego kodu na konsoli zobaczysz:


Andrzej


Krótko mówiąc, takie odniesienie "Klasa.pole" oznacza odwołanie do wartości domyślnej tego pola. Za mało namieszane? To przyjrzyjmy się temu:


class Osoba:
    imie=None
    nazwisko=None
    ksywka=None
o1=Osoba()
o1.imie='Jerzy'
o1.nazwisko='Kiler'
o1.ksywka='Killer'
o1.wtf='omg!'

print(o1.imie,o1.nazwisko,o1.ksywka,o1.wtf)


Czy mogę przypisywać coś do pola którego nie deklarowałem? Czy program zadziała jeśli odwołam się do pola którego nie ma (nie ma ??)? Robię to nawet dwukrotnie - raz przypisuję wartość, a raz się do niej odwołuję. Wynik działania:


Jerzy Kiler Killer omg!


Sponsorem Twojego zdziwienia jest elastyczna składnia języka Python :) A teraz o co chodzi - w chwili przypisania wartości do "nieistniejącego" pola w obiekcie, takie pole po prostu w nim powstało. W tym jednak przypadku będzie to dotyczyło wyłącznie tego obiektu, a nie innych - niezależnie od tego czy zadeklarowanych przed czy po.


 


Funkcje w klasie


Dotychczas wielokrotnie wyświetlaliśmy wszystkie pola obiektu, za każdym razem odwołując się do każdego z pól osobno. Czy nie byłoby wygodniej gdybyśmy mieli jakąś wbudowaną w obiekt funkcję  która by wyświetlała wszystkie jego pola?


class Osoba:
    imie='Andrzej'
    nazwisko='Klusiewicz'
    wiek=33
    def wypiszMnie(self):
        print(self.imie,self.nazwisko,self.wiek)

o=Osoba()
o.wypiszMnie()


Wynik działania:


Andrzej Klusiewicz 33


Funkcje mogą również przyjmować argumenty:


class Witacz:
    def przywitaj(self,imie,nazwisko):
        print("Witaj "+imie+" "+nazwisko+"! I kto jest teraz debeściak?")

w=Witacz()
w.przywitaj("Jerzy","Ryba")


Tradycyjnie wynik działania kodu:


Witaj Jerzy Ryba! I kto jest teraz debeściak?


Struktura funkcji nie różni się tu jakoś specjalnie od struktury funkcji jakie znaliśmy do tej pory. Jedyną różnicą jest pojawienie się argumentu "self". To jest wskaźnik do obiektu w którym się znajdujemy i pozwala np odwoływać się do jego pól. Jeśli zechcemy odwołać się do takich pól to robimy to tak:


class Programista:
    jezyk="Python"
    def funkcja(self):
        print('programuję w języku '+self.jezyk)

p=Programista()
p.funkcja()


A teraz dla odmiany nie będzie wyniku działania :) Powinieneś już wiedzieć co się stanie gdy uruchomisz ten kod.


Funkcja może również coś zwracać :


class Polska:
    def roszczenie(self):
        return "Mienie bezspadkowe"

p=Polska()
print(p.roszczenie())


 


Porównywanie obiektów tej samej klasy


Sprawdźmy działanie poniższego kodu:


class Owoc:
    pass

jablko=Owoc()
pomarancza=Owoc()
print(jablko)
print(pomarancza)

if(jablko==pomarancza):
    print('to samo')
else:
    print('inne')


Zadeklarowałem pustą klasę Owoc. Klasa ta nie posiada żadnych pól ani metod. Stworzyłem dwa obiekty tej klasy. Wypisuję je na konsoli i sprawdzam czy są sobie równe. Dla programistów innych języków obiektowych zaskoczenia pewnie nie będzie:


<__main__.Owoc object at 0x000001CC289333C8>


<__main__.Owoc object at 0x000001CC28933278>


inne


Ponieważ obiekty te nie posiadają żadnych pół, toteż w porównaniu nie może być brana zawartość obiektów. Jeśli porównujesz dwa obiekty w taki sposób, w rzeczywistości sprawdzasz czy chodzi o ten sam obiekt - w skrócie czy oba obiekty odnoszą się do  tego samego adresu w pamięci operacyjnej. Oczywiście tak nie jest, dlatego kod zwraca nam informację że obiekty są różne.


 


Przeciążanie funkcji


Niet! W Pythonie przeciążanie funkcji nie bangla. Dla osób które nie wiedzą czym jest przeciążanie - chodzi o możliwość posiadania kilku funkcji o takiej samej nazwie, a różniących się tylko ilością (ewentualnie typem) parametrów. W innych językach programowania w takiej sytuacji to która metoda zostałaby wywołana zależałoby od podanych do niej parametrów. W Pythonie jest to zrobione zupełnie inaczej. Przeciążanie nie działa, każda kolejna funkcja o tej samej nazwie przesłania jej poprzednie wystąpienie. Sprawdźmy to w praktyce:


class Witacz:
    def powitaj(self):
        print("No siemka!")
    def powitaj(self,kogo):
        print("No siemka {}!".format(kogo))
w=Witacz()
w.powitaj()


Wywołajmy kod:


Traceback (most recent call last):


  File "D:/data/workspace-python/nauka/materialy/02.py", line 7, in <module>


    w.powitaj()


TypeError: powitaj() missing 1 required positional argument: 'kogo'


Python krzyczy na mnie, ponieważ spodziewa się otrzymania parametru "kogo" - a tego nie podałem. Ta druga funkcja "powitaj" z dodatkowym parametrem zasłoniła jej poprzednią bezparametrową wersję. Kod zadziała poprawnie jeśli odwrócimy kolejność deklaracji funkcji:


class Witacz:
    def powitaj(self,kogo):
        print("No siemka {}!".format(kogo))
    def powitaj(self):
            print("No siemka!")
w=Witacz()
w.powitaj()


 


Metody specjalne


W klasach Pythona możemy deklarować metody specjalne - czyli taki które spełniają specjalną funkcję. Umożliwiają one na przykład określenie na przykład co ma się wydarzyć gdy dodasz do siebie dwa obiekty (przeciążanie operatorów), gdy zechcesz je zamienić na ciąg tekstowy, czy co ma się stać gdy obiekt jest tworzony. Wszystkie funkcje specjalne zaczynają się i kończą podwójnym znakiem "_".


 


__init__


Funkcja wywoływana automatycznie przy tworzeniu obiektu. Pozwala ona na inicjowanie np atrybutów tworzonego obiektu. Programistom innych języków może się to skojarzyć z konstruktorami, słusznie - choć funkcja "__init__" konstruktorem nie jest.  Może być wykorzystywany jako konstruktor, ale warto pamiętać że funkcja "__init__" wywoływana jest już PO stworzeniu obiektu. Taka drobna różnica, ale powodująca że ta funkcja nie jest konstruktorem formalnie.


class Samochod:
    def __init__(self):
        print('to jest funkcja init!')

s = Samochod()


Po uruchomieniu powyższego kodu, na konsoli zostaje oczywiście wypisane "to jest funkcja init!".


class Samochod:
    marka=None
    model=None
    def __init__(self):
        self.marka="nie podano"
        self.model="nie podano"

s = Samochod()
print(s.marka,s.model)


Tym razem moja klasa ma dwa pola: "marka" i "model". Wewnątrz funkcji "__init__" inicjalizuję wartości tych pól. Po stworzeniu obiektu i wypisaniu zawartości pól na konsoli widzimy że pola przybrały wartości określone w funkcji "__init__".


"__init__" musi otrzymywać przez parametr obiekt reprezentujący "siebie". Nie musi się on nazywać akurat "self", możesz go nazwać choćby "ja". Jest on potrzebny by móc się odnosić do atrybutów i funkcji obiektu. Podajemy go tylko na etapie deklaracji funkcji "__init__", nie przekazujemy przy wywołaniu konstruktora ( s=Samochod() ), jest on podstawiany automagicznie.


Funkcja "__init__" może też opcjonalnie przyjmować parametry:


class Samochod:
    marka=None
    model=None
    def __init__(self,marka,model):
        self.marka=marka
        self.model=model

s = Samochod("Renault","Kadjar")
print(s.marka,s.model)


na konsoli zostaje wypisane:


Renault Kadjar


Do funkcji "__init__" dodałem teraz dwa dodatkowe parametry: "marka" i "model". Zwróć uwagę na zawartość tej funkcji. Ilekroć odnoszę się na zasadzie "marka" we wnętrzu funkcji - mam na myśli parametr funkcji. Gdy odnoszę się do pola w klasie - "self.marka".  W związku z tym, że nowa wersja "__init__" oczekuje teraz podania 2 parametrów, to tworząc obiekt klasy Samochód muszę te wartości podać:


s = Samochod("Renault","Kadjar")


Programiści innych języków zapewne zadadzą sobie teraz pytanie - "a, czyli mogę zadeklarować jedną funkcję bez parametów, a drugą z, by później tworząc obiekt podawać dane albo nie?".  Otóż nie...


class Samochod:
    marka=None
    model=None
    def __init__(self):
        print("to się nie zadzieje")
    def __init__(self,marka,model):
        self.marka=marka
        self.model=model

s = Samochod("Renault","Kadjar")
print(s.marka,s.model)
s2=Samochod()
print(s2.marka,s2.model)


Zrobiłem dwie funkcje "__init__", jedną z parametrami a drugą bez. W Javie czy C# oczekiwalibyśmy przeciążania konstruktora - czyli możliwości stworzenia obiektu podając albo nie podając wartości parametrów, stosownie wywołując jeden albo drugi konstruktur. Nie w Pythonie, tutaj nie funkcjonuje przeciążanie funkcji czy konstruktorów. Po prostu każda kolejna definicja funkcji o nazwie istniejącej już nad nią funkcji skończy się przesłonięciem (a nie przeciążeniem!) tej pierwszej. Sprawdźmy więc:


Renault Kadjar


Traceback (most recent call last):


  File "D:/data/workspace-python/nauka/materialy/02.py", line 12, in <module>


    s2=Samochod()


TypeError: __init__() missing 2 required positional arguments: 'marka' and 'model'


Python pokrzyczał na mnie że nie podałem mu parametrów dla "__init__" w linii 12 czyli w :


s2=Samochod()


Dzieje się tak za sprawą opisanego przed momentem mechanizmu.         


 


__str__


To jest fukcja będąca odpowiednikiem metody "toString" występującej w Javie czy C#. Jej zadaniem jest zwracanie obiektu w postaci tekstowej.


Domyślnie próba wypisania obiektu zakończy się w ten sposób:


class Samochod:
    marka=None
    model=None

s=Samochod()
print(s)


Dostajemy:


<__main__.Samochod object at 0x000001E44AF724E0>


W zasadzie wolałbym by wyświetlane były zawartości pół - na potrzeby na przykład sprawdzenia jakie dane znajdują się na którym etapie przetwarzania skryptu. To jest właśnie miejsce na pojawienie się przesłonięcia funkcji "__str__".


class Samochod:
    marka=None
    model=None
    def __str__(self):
        return "marka={} model={}".format(self.marka,self.model)

s=Samochod()
print(s)


Tym razem dostajemy:


marka=None model=None


 


__add__ i przeciążanie operatorów


Tym razem od praktyki do teorii. Przeanalizujmy poniższy kod:


class Samochod:
    marka=None
    model=None
    def __init__(self,marka,model):
        self.marka=marka
        self.model=model
    def __str__(self):
        return "marka={} model={}".format(self.marka,self.model)

s=Samochod("Audi","A4")
print(s)
s2=Samochod("Bmw","e46")
s3=s+s2
print(s3)


Nic nowego merytorycznie tutaj nie ma. Sparametryzowana funkcja "__init__" pozwalająca mi tworzyć obiekty podając jednocześnie parametry których wartości trafiają do pól, do tego funkcja "__str__" pozwalająca mi odebrać obiekt w postaci tekstowej. Tworzę dwa obiekty klasy "Samochód". W przedostatniej linii próbuję je do siebie dodać i wypisać. Uruchomienie tego kodu kończy się błędem:


marka=Audi model=A4


Traceback (most recent call last):


  File "D:/data/workspace-python/nauka/materialy/02.py", line 13, in <module>


    s3=s+s2


TypeError: unsupported operand type(s) for +: 'Samochod' and 'Samochod'


Zaskoczenia pewnie nie ma, trudno sobie wyobrazić dodanie do siebie dwóch samochodów. Co jednak jeśli stworzylibyśmy hipotetyczną klasę "Zamówienie" i chcielibyśmy umożliwić dodawanie do siebie i łączenie 2 zamówień? Wystarczy zaimplementować funkcję specjalną "__add__".  Przerobię powyższy przykładowy kod klasy Samochód w celu pokazania tego.


class Samochod:
    marka=None
    model=None
    def __add__(self, other):
        return "Kraksa 2 Sebastianów w dresie. Jeden jechał {}, drugi {}".format(self,other)
    def __init__(self,marka,model):
        self.marka=marka
        self.model=model
    def __str__(self):
        return "marka={} model={}".format(self.marka,self.model)

s=Samochod("Audi","A4")
print(s)
s2=Samochod("Bmw","e46")
s3=s+s2
print(s3)


Jedyne co uległo tu zmianie, to pojawiła się funkcja "__add__" która instruuje Pythona co ma się dziać gdy ktoś spróbuje dodać do siebie dwa obiekty klasy Samochod. Ja zwracam akurat ciąg tekstowy, ale równie dobrze mógłby to być jakiś obiekt innej niż "str" klasy. Skutek działania tak przerobionego kodu:


marka=Audi model=A4


Kraksa 2 Sebastianów w dresie. Jeden jechał marka=Audi model=A4, drugi marka=Bmw model=e46


 


__getitem__ i __setitem__


Funkcje __getitem__ i __setitem__ umożliwiają stworzenie własnego tworu zbliżonego do list czy słownika. Przeanalizujmy poniższy wstępny przykład:


class MyDictionary:
    dict=dict()
    def add_element(self,key,value):
        self.dict[key]=value
    def get_element(self,key):
        return self.dict[key]

md = MyDictionary()
md.add_element('A',1000)
print(md.get_element('A'))


Stworzyłem w zasadzie klasę opakowującą słownik z możliwością uzupełniania i pobierania wartości z tego słownika. Chciałbym jednak móc stosować zapis taki jak przy słownikach i móc przypisywać wartości i odczytywać je tak jakby to był słownik tj. np:


ml['A']='Wartosc'


print(ml['A'])


Do tego celu przesłonimy funkcje __getitem__ i __setitem__ których zadanie będzie takie jak wcześniejszych metod add_element i get_element. Wykorzystanie metod "magicznych" pozwala nam jednak na przypisywanie i odczytywanie wartości w sposób zaprezentowany powyżej:


class MyDictionary:
    dict=dict()
    def __setitem__(self, key, value):
        self.dict[key]=value
    def __getitem__(self, key):
        return self.dict[key]

md = MyDictionary()
md['klucz']='wartość'
print(md['klucz'])


Teraz możemy naszą klasę wzbogacić o elementy których w tradycyjnych słownikach nie ma


 


Metody statyczne


W większości obiektowych języków programowania występuje pojęcie metod statycznych. Są to takie metody które możemy wywołać nie tworząc obiektu klasy.  W poniższym przykładzie zadeklarowałem dwie metody - jedną zwykłą i jedną statyczną, nazwy chyba całkiem trafne:


class Ms:
    def zwykla(self):
        print('hej, jestem zwykłą metodą')
        pass
    @staticmethod
    def statyczna():
        print('hej, jestem metodą statyczną')
        pass

ms=Ms()
ms.zwykla()
Ms.statyczna()


Jeszcze tylko skutek działania powyższego kodu i przechodzimy do tłumaczenia:


hej, jestem zwykłą metodą


hej, jestem metodą statyczną


Aby wywołać metodę "zwykla", musimy powołać do życia obiekt klasy Ms i z niego dopiero wywołać tę metodę. Klasa Ms może jednak mieć bardzo wiele pól, lub pola które są od razu inicjalizowane dużą ilością danych, a to nie koniecznie musi być pożądany przez nas skutek. My chcemy tylko wywołać metodę która akurat znajduje się w tej klasie. W takiej sytuacji możemy wykorzystać właśnie metodę statyczną. Przyjrzyj się wywołaniu "Ms.statyczna()" - wywołuję tę metodę z klasy a nie z obiektu (!!) "ms", który odróżnić można po wielkości liter. Zwykłej metody w ten sam sposób wywołać nie mogę. Metoda statyczna staje się taką za sprawą 2 rzeczy - adnotacji "staticmethod", oraz faktu nie podania parametru "self". 


W zasadzie to jest jedna sztuczka na wywołanie metody niestatycznej tak jak statycznej, może nawet warto ją znać, ale raczej nie będziesz jej często stosować. By tak się dało zrobić, przy wywołaniu metody zwykłej z klasy (a nie z obiektu!) jako jej parametr podajemy "None":


class Ms:
    def zwykla(self):
        print('hej, jestem zwykłą metodą')
        pass
    @staticmethod
    def statyczna():
        print('hej, jestem metodą statyczną')
        pass

Ms.zwykla(None)


Na konsoli pojawia się:


hej, jestem zwykłą metodą


i wszystko działa poprawnie, ale tylko dlatego że w metodzie "zwykla" nie odwołuję się do żadnych atrybutów ani metod obiektu.


 


Dziedziczenie


Dziedziczenie występuje w Pythonie jak chyba w każdym języku obiektowym, choć i tu nie obędzie się bez niespodzianek. Dziedziczenie to rozszerzanie klas, dla przykładu możemy przyjąć że mamy klasę "Samochod"  posiadającą funkcje "jedz", "skrec", "hamuj". Nastepnie tworzymy klasę "SuperSamochod". Chcemy by klasa "SuperSamochod" zawsze posiadała to co klasa "Samochod", plus  jakieś dodatkowe funkcje np "turbo". To jest właśnie miejsce na zastosowanie dziedziczenia.  Przeanalizujmy taki właśnie przykład:


class Samochod:
    def jedz(self):
        print("no to jedziemy...")
    def hamuj(self):
        print("khhhhh.....")
    def skrec(self):
        print("na ręcznym!")

class SuperSamochod(Samochod):
    def turbo(self):
        print("włączamy podtlenek gazotu w 1.6 i mamy turbo na miarę 'Szerszenia'")

s=Samochod()
s.jedz()
s.hamuj()
s.skrec()
ss=SuperSamochod()
ss.jedz()
ss.hamuj()
ss.skrec()
ss.turbo()


Zbieżność osób i nazwisk, a także odniesienia do "Szerszenia" z "Blok Ekipy" zupełnie przypadkowe ;)


A więc... nie zaczyna się zdania od "a więc", ale ja mogę bo to moja książka ;). A więc... Mam klasę "Samochod", oraz klasę "SuperSamochod" dziedziczącą po klasie Samochod. Skąd wiemy że klasa "SuperSamochod" dziedziczy po klasie "Samochod" - z zapisu:


class SuperSamochod(Samochod):


Taki zapis oznacza że klasa deklarowana dziedziczy po klasie której nazwę obejmujemy w nawiasy. Co tu się dzieje dalej? Stworzyłem obiekt klasy "Samochod" i wywołałem na nim trzy obecne metody. Następnie stworzyłem obiekt klasy "SuperSamochod". Z niego też wywołałem te same metody, oraz dodatkową metodę "turbo". Posiadam w tej klasie również funkcje z klasy "Samochód" co wprost wynika z dziedziczenia właśnie.


 


Dziedziczenie po wielu klasach


Programiści Javy mogliby odnieść mylne wrażenie że to jakaś pomyłka. Otóż nie jest to pomyłka, taka konstrukcja jest możliwa w Pythonie. Klasa dziedzicząca będzie posiadać pola i metody wszystkich klas po których dziedziczy. Są tu oczywiście pewne ograniczenia, ale o tym za chwilę. Przyjrzyjmy się poniższym klasom:


class Pojazd:
    def jedz(self):
        print("wroom!")

class Armata:
    def strzelaj(self):
        print("jeb, jeb, jeb!")

class Czolg(Pojazd,Armata):
    pass


c=Czolg()
c.jedz()
c.strzelaj()


Mamy klasę "Pojazd" która posiada funkcję "jedz" i klasę "Armata" która posiada funkcję "strzelaj". Na końcu stworzyłem klasę Czołg dziedziczącą po obu wymienionych. Obiekty klasy "Czolg" będą więc posiadały obie metody. Uruchomienie spowoduje wyświetlenie obu komunikatów z obu funkcji. Co jednak gdyby okazało się że obie klasy posiadają funkcję o takiej samej nazwie? Którą z nich będziemy w takiej sytuacji wywoływać? Czy to w ogóle możliwe? Tak, jest to możliwe a wszystko sprowadza się do kolejności wymieniania klas po których dziedziczymy. Poniżej deklaracja trzech klas. Klasa C dziedziczy zarówno po klasie B jak i A. Klasy B i A posiadają taką samą funkcję "hello".


class A:
    def hello(self):
        print('siema, tu A')

class B:
    def hello(self):
        print('siema, tu B')

class C(B,A):
    pass


Po stworzeniu obiektu klasy C i jej wywołaniu :


c=C()
c.hello()


otrzymujemy na konsoli komunikat:


siema, tu B


świadczący o tym że wywołana została metoda z klasy B.  Teraz odwróćmy kolejność klas w dziedziczeniu:


class C(A,B):
    pass

c=C()
c.hello()


Tym razem na konsoli pojawia się:


siema, tu A


co prowadzi nas do wniosku, że w takiej sytuacji obowiązują metody z klasy wymienionej jako pierwszej w dziedziczeniu.


 


Polimorfizm


Polimorfizm to po prostu wielopostaciowość. Bazując na naszym przykładzie z klasami "Samochod" i "SuperSamochod" - Każdy super samochód jest też samochodem i może t ak być traktowany.  Tu pojawia się jednak kilka zagadnień które mogą nieco skomplikować sprawę. Przyjmijmy że klasa "SuperSamochod" dziedzicząca po klasie "Samochod" posiada również fukcję "jedz" obecną w klasie po której dziedziczy. Co w takim przypadku? Co jeśli ta metoda jedz w klasie "SuperSamochod" będzie chciała wywołać tę metodę "jedz" z klasy dziedziczonej? Sprawdźmy to!


class Samochod:
    def jedz(self):
        print("pyr pyr")

class SuperSamochod(Samochod):
    def jedz(self):
        print("WROOOOOOOOM!!!!")

s=Samochod()
ss=SuperSamochod()
ss.jedz()


Obie klasy posiadają metodę "jedz", przy czym klasa "SuperSamochod" dziedziczy po klasie "Samochod". Jeśli wywołuję "ss.jedz()" to która z metod zostaje wywołana? Na konsoli pojawia się :


WROOOOOOOOM!!!!


Wywołana została metoda z klasy "SuperSamochod" - czego prawdę mówiąc można się było spodziewać. Dzieje się  tak za sprawą przesłonięcia metody dziedziczonej przez metodę w klasie dziedziczącej. Gdybyś zechciał odwołać się do metody z klasy po której dziedziczysz, musisz posłużyć się takim zapisem:


class SuperSamochod(Samochod):
    def jedz(self):
        print("WROOOOOOOOM!!!!")
        super().jedz()


To "super()" odnosi się do klasy po której dziedziczymy. Zapis "super().jedz()" oznacza więc że wywołana zostaje metoda jedz() z klasy po której dziedziczymy.


 


Funkcje prywatne


Funkcje prywatne to takie funkcje, które można wywołać tylko z wnętrzna klasy. Aby funkcja stała się prywatną należy przed jej nazwą dodać dwa znaki "_":


class FunkcjePrywatne:
    def funkcjaPubliczna(self):
        print("Cześć, jestem funkcją publiczną!")
    def __funkcjaPrywatna(self):
        print("Cześć, jestem funkcją PRYWATNĄ!")

fp = FunkcjePrywatne()
fp.funkcjaPubliczna()
fp.__funkcjaPrywatna()


Po uruchomieniu tego kodu na konsoli dostaniemy:


Cześć, jestem funkcją publiczną!


Traceback (most recent call last):


  File "D:/data/workspace-python/nauka/materialy/02.py", line 9, in <module>


    fp.__funkcjaPrywatna()


AttributeError: 'FunkcjePrywatne' object has no attribute '__funkcjaPrywatna'


Process finished with exit code 1


Dzieje się tak dlatego, że spoza obiektu nie możemy wywołać funkcji prywatnej. Teraz jednak zmienimy nieco kod:


class FunkcjePrywatne:
    def funkcjaPubliczna(self):
        print("Cześć, jestem funkcją publiczną!")
        self.__funkcjaPrywatna()
    def __funkcjaPrywatna(self):
        print("Cześć, jestem funkcją PRYWATNĄ!")

fp = FunkcjePrywatne()
fp.funkcjaPubliczna()


Tym razem na konsoli pojawia się:


Cześć, jestem funkcją publiczną!


Cześć, jestem funkcją PRYWATNĄ!


Tym razem udało się wywołać funkcję prywatną, ale tylko dlatego że wywoływaliśmy ją z wnętrza obiektu. Przy okazji chciałem zwrócić uwagę na pewną rzecz która mogła Ci umknąć - odwołałem się z jednej funkcji do drugiej znajdującej się NAD funkcją wywołującą! W klasach to działa, w przeciwieńskie to funkcji poza klasami.


Prywatne mogą być również pola, sprawiamy że stają się prywatne dokładnie tak samo jak w przypadku funkcji - poprzedzając ich nazwę "__":


class PracownikJanuszexu:
    imie='Wania'
    nazwisko='Typowy'
    __wyplata='Czy pindzisiont za godzine'
    def info(self):
        print(self.imie,self.nazwisko,self.__wyplata)

pj = PracownikJanuszexu()
pj.info()
print(pj.nazwisko,pj.imie,pj.__wyplata)


Zaskoczenia pewnie nie będzie:


Traceback (most recent call last):


Wania Typowy Czy pindzisiont za godzine


  File "D:/data/workspace-python/nauka/materialy/02.py", line 10, in <module>


    print(pj.nazwisko,pj.imie,pj.__wyplata)


AttributeError: 'PracownikJanuszexu' object has no attribute '__wyplata'


 


Abstrakcja


Załóżmy że chcemy stworzyć klasę bazową reprezentującą bliżej nieokreślona figurę i kilka klas dziedziczących po klasie bazowej, takich jak "Kwadrat" i "Koło". Wymagamy by wszystkie klasy dziedziczące posiadały zdefiniowaną i dziedziczoną metodę "pokaz_nazwe" oraz pole "nazwa". 


Dla każdej figury pole obliczać będziemy w inny sposób, dlatego też każda klasa dziedzicząca powinna posiadać swoją adaptację metody "oblicz_pole". Nie możemy zdefiniować sposobu działania tej metody w klasie "Figura" ponieważ pole każdej z figur będzie obliczane inaczej.


 W klasie "Figura" określimy tylko rzeczy wspólne dla wszystkich figur. Aby osiągnąć opisany efekt będziemy musieli stworzyć metodę abstrakcyjną w klasie "Figura" i zaimplementować ją w klasach dziedziczących. Będzie to oznaczało że wszystkie klasy dziedziczące po klasie "Figura" bedą musiały zaimplementować metodę obliczającą pole. W związku z tym że klasa "Figura" będzie posiadała metodę abstrakcyjną sama będzie klasą abstrakcyjną i nie będzie można stworzyć instancji tej klasy. Przyjrzyjmy się więc klasie "Figura":


from abc import ABC, abstractmethod

class Figura(ABC):

    nazwa=None

  def pokaz_nazwe(self):
        print(self.nazwa)

    @abstractmethod
    def oblicz_pole(self):
        pass


Zwróć uwagę że nasza klasa Figura dziedziny po klasie ABC - jest to niezbedne by tworzyć metody abstrakcyjne, a co za tym idzie klasy abstrakcyjne.


Klasa ta definiuje pole "nazwa" które będzie posiadała każda klasa dziedzicząca po klasie "Figura". Podobnie sprawy się mają z metodą "pokaz_nazwe". Wszystkie klasy dziedziczącę klasę "Figura" będą już posiadały tą metodę odziedziczoną i zaimplementowaną. Nowością tutaj jest metoda "oblicz_pole" oznaczona adnotacją "@abstractmethod". Jak widzisz nie posiada ona implementacji. Każda klasa dziedzicząca po klasie "Figura" będzie więc musiała mieć własną implementację tej metody.  Próba stworzenia obiektu tej klasy:


f=Figura()


spotka się z błędem:


TypeError: Can't instantiate abstract class Figura with abstract methods oblicz_pole


Dodajemy klasę "Kwadrat" dziedziczącą po klasie "Figura". Tworzymy konstruktor który umożliwia nam wstrzyknięcie do obiektu długość boku i dodatkowo uzupełnia pole "nazwa":


class Kwadrat(Figura):
    def __init__(self,dlugosc_boku):
        self.nazwa='Kwadrat'
   self.dlugosc_boku=dlugosc_boku

    def oblicz_pole(self):
        return pow(self.dlugosc_boku,2)


Tym razem możemy już stworzyć obiekt klasy, ponieważ mamy zaimplementowaną dziedziczoną metodę abstrakcyjną "oblicz_pole". Wywołajmy zatem:


kw=Kwadrat(5)
print(kw.nazwa)
print(kw.oblicz_pole())


Wynik na konsoli:


Kwadrat


25


Dodajmy kolejną klasę, stwórzmy jej obiekt i wywołajmy wyświetlenie pola nazwa i wynik działania metody "oblicz_pole" która tym razem zaimplementowana jest inaczej:


class Prostokat(Figura):
    def __init__(self,bok_a,bok_b):
        self.nazwa='Prostokąt'
        self.bok_a=bok_a
        self.bok_b=bok_b

    def oblicz_pole(self):
        return self.bok_a*self.bok_b

pr=Prostokat(4,5)
print(pr.nazwa)
print(pr.oblicz_pole())


Wynik na konsoli:


Prostokąt


20


 

Przyjdź do nas na szkolenie z języka Python! Mamy szereg szkoleń w ofercie, od podstawowych po aplikacje webowe z użyciem Django, analizę danych, tesowanie, machine learning i wiele innych.
Sprawdź dostępne szkolenia Python
Zapisz się do newslettera aby otrzymywać najnowsze świeżynki pojawiające się na blogu!