Object Oriented Programming (OOP) jest podstawą obecnego modelu programowania – tworzymy programy gdzie kod umieszczamy w wygodnych kontenerach, które możemy użyć tworząc dynamicznie struktury do obróbki naszych danych jakie chcemy przetwarzać. Wpierw jednak definiujemy swoisty przepis na nasz obiekt w którym zamierzamy przetwarzać porcję danych. Ten przepis to opis co dany obiekt musi posiadać, jakie zmienne i jakie funkcje, procedury, a także w jaki sposób ma przyjmować, zmieniać i przekazywać nam przetworzone dane. Tą strukturą jest definicja klasy.

Najprostszą definicję klasy, nie zawierającą żadnych funkcjonalności można zobaczyć poniżej:

class Samochod:
    None

s=Samochod()
print(s)

Jak widać został stworzony obiekt s, na podstawie zdefiniowanej klasy Samochod. Nie zawiera ten obiekt żadnych potrzebnych nam struktur danych, ani też żadnych metod.

Zastanówmy się więc jakie struktury winien zawierać obiekt klasy Samochód, aby był mniej/więcej odwzorowaniem cech prawdziwego samochodu. Propozycja poniżej:

class Samochod:
    VIN = None
    kolor=None
    marka=None
    typ=None
    nrRej=None



s=Samochod()
s.VIN="TTVW45RTR44488585"

print(s.VIN)

Kilka słów o nomenklaturze używanej w opisie klas. Klasa jest “przepisem” na obiekt, czyli instrukcją jak stworzyć taki obiekt. Zatem obiekt jest stworzony na podstawie definicji, klasa nie jest obiektem – jest jedynie opisem jak obiekt powinien wyglądać jeśli/gdy zostanie stworzony. Klasa nie jest bytem – nie istnieje, tak jak po przeczytaniu przepisu na ciasto nie jesteśmy ukontentowani smakiem i aromatem tego ciasta… dalej chcemy dostać kawałek do zjedzenia. Obiekt jest manifestacją tego swoistego przepisu według którego został stworzony, jest tzw. instancją klasy, jest bytem fizycznym do którego możemy się w programie odwołać poprzez jego nazwę (bądź instancję w przypadku dekoratorów1nie nazwanych obiektów powstałych do jednorazowego użycia w bezpośrednim wywołaniu/kreacji/inicjacji i zniszczeniu.

Powyższy przykład pokazuje jak przypisać obiektowi poza nazwy także cechy (pola wartości) takie jak VIN, kolor, marka itp… Możemy jednak uczynić znacznie więcej – możemy stworzyć podprogramy manipulacji danymi, przede wszystkim2intencjonalnie tymi danymi, choć nie istnieją żadne przeciwwskazania, by zajmować się innymi danymi tymi danymi, które są umieszczone w ramach obiektu.

Settery/Gettery

Settery to ogólna nazwa metody, która służy do ustawiania wartości pól instancji danej klasy, gettery to ogólna nazwa metody służącej pobieraniu wartości pól instancji danej klasy. Poniżej przykład implementacyjny.

class Samochod:
    kolor=None
    VIN = None
    typ=None
    nrRej=None

    def setKolor(self,kolor):
        self.kolor=kolor
        return self

    def setVIN(self,VIN):
        self.VIN=VIN
        return self

    def setTyp(self,typ):
        self.typ=typ
        return self

    def setNrRej(self,nrRej):
        self.nrRej=nrRej
        return self

    def getKolor(self):
        return self.kolor

    def getVIN(self):
        return self.VIN

    def getTyp(self):
        return self.typ

    def getNrRej(self):
        return self.nrRej

    def values(self):
        return dict({"kolor":self.kolor,"VIN":self.VIN,"typ":self.typ,"nrRej":self.nrRej})

s=Samochod()

s.VIN="TTVW45RTR44488585"
print(s.VIN)

s.setKolor("czerwony")
s.setTyp("osobowy")

print(s.values())

Powyżej mamy przykład zdefiniowania klasy wraz z metodami manipulacji danymi. Zaletą tego podejścia jest umożliwienie przeprowadzania walidacji poprawności danych przed wprowadzeniem ich do cech (wartości) instancji klasy.

Inicjacja domyślna(niejawna) i jawna (intencjonalna)

Często zachodzi potrzeba, aby nowo-powstający obiekt przyjął od momentu stworzenia pewne określone wartości, przypisane, bądź obliczane w momencie tworzenia obiektu. Taką funkcjonalność przyjmuje konstruktor obiektu, czyli mechanizm, który na podstawie opisu klasy tworzy jej instancję – obiekt.

Do tej pory powoływaliśmy instancję klasy nie myśląc o tej funkcjonalności. Zatem uruchamiał nam się konstruktor w niejawny sposób tworząc instancję i przypisując ją do obiektu (referencji do fizycznego miejsca, gdzie znajduje się umieszczona instancja klasy) o podanej przez nas nazwie. Najwyższy czas, aby pokazać inicjację w sposób jawny:

class Samochod:
    kolor=None
    VIN = None
    typ=None
    nrRej=None

def __init__(self): None

    def setKolor(self,kolor):
        self.kolor=kolor
        return self

    def setVIN(self,VIN):
        self.VIN=VIN
        return self

    def setTyp(self,typ):
        self.typ=typ
        return self

    def setNrRej(self,nrRej):
        self.nrRej=nrRej
        return self

    def getKolor(self):
        return self.kolor

    def getVIN(self):
        return self.VIN

    def getTyp(self):
        return self.typ

    def getNrRej(self):
        return self.nrRej

    def values(self):
        return dict({"kolor":self.kolor,"VIN":self.VIN,"typ":self.typ,"nrRej":self.nrRej})

s=Samochod()

s.VIN="TTVW45RTR44488585"
print(s.VIN)

s.setKolor("czerwony")
s.setTyp("osobowy")

print(s.values())

Jedyną zmianą jest umieszczenie w deklaratywnej części opisu struktury klasy metody __init__. Póki co ta metoda nic nie wnosi nam do działania obiektu, ale rozważmy sytuację, kiedy chcemy nadać pewne wyjściowe (startowe) wartości powstającego obiektu.

Zadanie A1:

Przygotuj definicję klasy Samochod, zastanów się nad zmianą ogólnie przypisywanych wartości typu string (lub liczbowych) na rzecz zbioru wartości zawierających jedyne możliwe i poprawne zestawy tych wartości (dla uniknięcia pomyłek typu semantyczno/ortograficznego: “czerwony, czerwny, czarwony”, czy opartego o specyfikę ciągów np: czerwony, a Czerwony.

Zadanie A2:

Przygotuj definicję klasy Czlowiek, zastanów się nad niezbędnymi cechami przechowującymi kluczowe wartości i metodami, które mogą być potrzebne dla obsługi obiektu.

Hermetyzacja danych

Język Python traktuje bardzo naiwnie temat hermetyzacji danych. Możliwe, że ta kwestia zostanie rozwiązana w kolejnych generacjach języka, ale na czas wersji 3.14 jest to dalej nie rozwiązane.

Hermetyzacja danych3danych lub też i metod – nie ma to znaczenia jest bardzo istotna dla bezpieczeństwa przetwarzania tych danych. Nie blokowanie dostępu do danych w klasach, gdzie setterami ustawiamy ich wartość DOPIERO po walidacji poprawności struktury tych danych jest wręcz zaproszeniem do wprowadzania błędów. Twórcy języka Python na tę chwilę ustawili jedynie “zalecenia” traktowania danych jako chronione lub prywatne wprowadzając konwencję ich oznaczania. Tak więc znane z innych języków programowania selektory dostępu: protected, czy private w języku Python póki co nie istnieją! Przyjmuje się, że aby oznaczyć jakąś cechę (wartość)4lub metodę! jako protected należy przed jej nazwą użyć pojedynczego znaku _, a w przypadku chęci oznaczenia jako private należy oznaczyć podwójnym znakiem __. W dalszej części jednak udowodnię, że zabezpieczenie to jest jedynie iluzoryczne. Spróbujmy zatem stworzyć w naszej testowej klasie Samochód zmienną VIN, którą można będzie przetwarzać jedynie z poziomu setterów… Dla uproszczenia odchudzimy definicję naszej klasy do jedynie niezbędnych elementów.

class Samochod:
    _VIN = None

    def setVIN(self,VIN):
        self._VIN=VIN
        return self

    def getVIN(self):
        return self._VIN

    def __init__(self, VIN):
        self._VIN=VIN


s=Samochod('FR86ADSSATR488585')
print(s.getVIN())
s.setVIN('TL76YYHS9OOO23VDD')
print(s._VIN)
s._VIN='TTVW45RTR44488585'
print(s._VIN)

'''
FR86ADSSATR488585
TL76YYHS9OOO23VDD
TTVW45RTR44488585

Process finished with exit code 0
'''

Działanie powyższego programu pokazuje, że oznakowanie jakiejkolwiek zmiennej “selektorem” chronienia (sel: protected) jest… bzdurą.

Spróbujmy to samo zrobić dla uprywatnienia naszych danych (sel: private).

class Samochod:
    __VIN = None

    def setVIN(self,VIN):
        self.__VIN=VIN
        return self

    def getVIN(self):
        return self.__VIN

    def __init__(self, VIN):
        self.__VIN=VIN


s=Samochod('FR86ADSSATR488585')
print(s.getVIN())
s.setVIN('TL76YYHS9OOO23VDD')
print(s.getVIN())
s.__VIN='TTVW45RTR44488585'
print(s.__VIN)

'''
FR86ADSSATR488585
TL76YYHS9OOO23VDD
TTVW45RTR44488585

Process finished with exit code 0
'''

I… to samo… próba dostania się do zmiennej __VIN w obiekcie s zakończyła się sukcesem… wyświetliliśmy na wszystkie sposoby, nawet z bezpośrednim dostępem…

…tak by się wydawało, ale… zróbmy mały test: wyświetlmy raz jeszcze __VIN za pomocą gettera…

class Samochod:
    __VIN = None

    def setVIN(self,VIN):
        self.__VIN=VIN
        return self

    def getVIN(self):
        return self.__VIN

    def __init__(self, VIN):
        self.__VIN=VIN


s=Samochod('FR86ADSSATR488585')
print(s.getVIN())
s.setVIN('TL76YYHS9OOO23VDD')
print(s.getVIN())
s.__VIN='TTVW45RTR44488585'
print(s.__VIN)
print(s.getVIN())    ### << jeszcze raz wyświetlamy zmienną za pomocą gettera.

'''
FR86ADSSATR488585
TL76YYHS9OOO23VDD
TTVW45RTR44488585
TL76YYHS9OOO23VDD

Process finished with exit code 0
'''

Ale, ale… jak to?! Przecież… przed chwilą zmieniliśmy wartość __VIN w dostępie bezpośrednim, a tutaj wyświetlana jest poprzednia wartość?! O co chodzi?!

Jest to potencjalny punkt popełnienia DUŻEGO błędu…

Zobaczmy raz jeszcze kod programu…

class Samochod:
    __VIN = None

    def setVIN(self,VIN):
        self.__VIN=VIN
        return self

    def getVIN(self):
        return self.__VIN

    def __init__(self, VIN):
        self.__VIN=VIN


s=Samochod('FR86ADSSATR488585')
print(s.getVIN())
s.setVIN('TL76YYHS9OOO23VDD')
print(s.getVIN())
####################s.__VIN='TTVW45RTR44488585' ##<< USUWAMY to "bezpośrednie" przypisanie!
print(s.__VIN)
print(s.getVIN())    ### << jeszcze raz wyświetlamy zmienną za pomocą gettera.

'''
FR86ADSSATR488585
TL76YYHS9OOO23VDD
TTVW45RTR44488585
TL76YYHS9OOO23VDD

Process finished with exit code 0
'''

I co teraz?? Program się nam nie wykonuje poprawnie zgłaszając błąd:

Traceback (most recent call last):
  File "C:\JetBrains\PyCharm2024.2\scratches\Klasy2.py", line 26, in <module>
    print(s.__VIN)
          ^^^^^^^
AttributeError: 'Samochod' object has no attribute '__VIN'
FR86ADSSATR488585
TL76YYHS9OOO23VDD

Process finished with exit code 1

Tak, więc co tutaj się podziało!?

Wpierw trzeba powiedzieć, że linijka kodu: s.__VIN='TTVW45RTR44488585' utworzyła nam w obiekcie s (a dokładniej w przestrzeni nazw obiektu s) zmienną __VIN inną niż ta z definicji klasy Samochod. Zbieżność nazw nie ma znaczenia – są to INNE obiekty przenoszące podobne cechy, więc łatwo o pomyłkę – i jak widać – jej ulegliśmy. Stąd późniejsze odwołanie do zmiennej __VIN poprzez getter (getVIN()) i poprzez odwołanie bezpośrednie (s.__VIN) to de facto odwołania do dwu różnych obiektów! Od tego momentu w ramach przestrzeni nazw obiektu s funkcjonowałyby oba i w zależności jak byśmy się do nich odwoływali to modyfikowalibyśmy raz jeden, a raz drugi obiekt. Program nasz działałby niepoprawnie, a błąd ten jest bardzo trudny do wychwycenia!

Name Mangling

Podsumowując wiedzę pozyskaną z poprzedniego rozdziału możemy mieć wrażenie, że wiemy już jak tworzyć zmienne prywatne, do których nie będzie dostępu spoza obiektu. Możemy teraz ze spokojem zabezpieczyć zmienne typu VIN, czy Pesel i spokojnie walidować poprawność ich formatu i wartości zanim zapiszemy ją do tych zmiennych… Niestety nie! Python znów nas… nazwijmy to: oszukał. Zobaczmy następujący przykład:

s=Samochod('FR86ADSSATR488585')
print(s.getVIN())
s.setVIN('TL76YYHS9OOO23VDD')
print(s.getVIN())

print(s._Samochod__VIN)
s._Samochod__VIN="Bzdura!!!"
print(s.getVIN())

'''
FR86ADSSATR488585
TL76YYHS9OOO23VDD
TL76YYHS9OOO23VDD
Bzdura!!!

Process finished with exit code 0
'''

Posługując się techniką zwaną “name mangling” uzyskaliśmy dostęp do zmiennej “prywatnej”, co pozwoliło nam odczytać jej “prywatną” wartość, jak i ustawić nową wartość z ominięciem całego misternie przygotowanego procesu walidacji poprawności w metodzie settera setVIN().

Inspect

Inną metodą obejścia tak solidnego zabezpieczenia bezpieczeństwa naszych danych jest wykorzystanie modułu inspect, co przedstawia następujący kod (reszta kodu taka sama jak w powyższych przykładach):

#moduł inspect
setattr(s,'_Samochod__VIN','Bzdura roku!!!')
print(getattr(s,'_Samochod__VIN'))
print(s.getVIN())

'''
FR86ADSSATR488585
TL76YYHS9OOO23VDD
TL76YYHS9OOO23VDD
Bzdura!!!
Bzdura roku!!!
Bzdura roku!!!

Process finished with exit code 0
'''

Jak widać, język Python absolutnie, wcale nie zabezpiecza, nie hermetyzuje danych umożliwiając ich swobodną modyfikację, bądź też ryzykujemy popełnienie karygodnych błędów z “podwójnymi zmiennymi o tych samych nazwach” z powodu braku zrozumienia zakresów przestrzeni nazw. Na tym powyższe rozważania o bezpieczeństwie danych w klasach języka Python uważam za zakończone na ten moment.

Tworzenie bibliotek

Na powyższe problemy jest metoda ich zaradzenia, ale jest to raczej obejście problemu i łatanie sera szwajcarskiego niż racjonalne rozwiązanie. Należy mocno mieć nadzieję i liczyć, że twórcy języka zdecydują się na wprowadzenie odpowiednich technik w kolejnych generacjach po wersji 3.14 na której obecnie się opieramy.

Jedną z możliwości utrudniających próby jest podzielenie naszego programu na części i umieszczenie ich w odrębnych plikach. Co prawda, nie zmienia to w żaden sposób kwestii dostępu do “chronionych” zasobów, ale utrudnia to osobom korzystającym z naszej biblioteki “odkrycia” szczegółów, gdy podajemy w dokumentacji sposoby z jej korzystania.

Tak jak poniżej, “znamy”5oczywiście wystarczy otworzyć plik ten i przeanalizować zawarte tak linie kodu, by “poznać” go na wskroś jedynie nazwę biblioteki (plik) oraz umieszczony tam zasób wraz z wylistowanymi metodami.

##program główny...

from TestLib import Test

c=Test(1)
print(c.test())
print(c.punkt(10,30))

Poniżej wspomniana biblioteka…

#TestLib.py

class Test:
    __x=100
    __y=200

    def __init__(self,par):
        if __name__=='__main__':
            print('Nieprawidłowe wywołanie biblioteki')
            exit(1000)

    def punkt(self,x,y):
        if x in range(0,100) and y in range(0,200):
            self.__x=x
            self.__y=y
            return True
        return False


    def test(self):
        print('Oto jestem w module:',__name__,end='')
        return ''

Próba kreacji instancji klasy w pliku biblioteki skończy się niepowodzeniem i zakończeniem działania programu z kodem wyjścia 1000. Można jedynie tworzyć instancję na “zewnątrz” pliku biblioteki. Działanie tego wynika ze sprawdzenia nazwy modułu z którego wywoływany jest podprogram w bibliotece i porównanie tego do nazwy modułu w którym znajduje się podprogram biblioteki. Jeśli __name__ zawiera string ‘__main__’ znaczy to, że moduł jest plikiem głównym programu, w przeciwnym razie zawiera nazwę pliku w którym znajduje się podprogram wywołujący ten test.

Jak widać, powyższy sposób umożliwia dekompozycję programu na pliki, łączące się w jedną całość w trakcie wykonywania. Nie działa to jednak na wspomnianą hermetyzację danych w bezpośredni sposób, raczej… poszlakowy.

Pewnym rozwiązaniem jest… wykorzystanie schedulera celem “naprawy” wartości zmiennych “chronionych”, czy prywatnych” do których ktoś nieuprawniony zdołał się dostać i zmienić ich zawartość. Taki scheduler kontrolowałby w określonym interwale zespół zmiennych i w przypadku wykrycia zmiany wartości w jednej z nich – odtwarzałby tę wartość z innych “ukrytych” kopii. Powodowałoby to cykliczne przywracanie wartości zmienionych bezpośrednio, a nie za pośrednictwem uprawnionego settera, który uprzednio wyłączałby mechanizm zabezpieczający, ustawiał zwalidowaną wartość zmiennej oraz także jej kopie i następnie na powrót włączałby cyklicznie działający mechanizm chroniący. Takim schedulerem może być pakiet APScheduler doinstalowany do środowiska z Python. Powyższe rozwiązanie nie jest ani efektywne, ani “ładne”, ale możliwe do zrealizowania pewnej namiastki hermetyzacji danych w Python.

Dziedziczenie

Mechanizm dziedziczenia jest bardzo przydatny przy programowaniu obiektowym. Co więcej jest wręcz wymarzony przy rozwijaniu oprogramowania, a idea programowania obiektowego niejako czyni go leżącym u jej podstaw. Pozwala na tworzenie nowych klas i wynikających z nich instancji: obiektów bez powielania treści kodu. Programista nie musi nowej klasy definiować i kodować od początku, może oprzeć się na klasie już istniejącej, o ile nowa, potrzebna klasa jest do niej podobna na tyle, że część kodu byłaby identyczna. Tym zajmie się właśnie mechanizm dziedziczenia. Przykładem samego dziedziczenia może być poniższy schemat:

Wynika z tego, że szczycie schematu (a logicznie w korzeniu drzewa dziedziczenia) znajduje się klasa, która opisuje największy zbiór zawierając najmniej cech. Poruszając się “w dół”, definiując kolejne klasy doprecyzowujemy ich opis, który zaczyna obejmować coraz mniejszy zbiór, za to posiadając coraz liczniejsze i dokładniejsze cechy. Dzieje się to poprzez dziedziczenie listy cech (metody i wartości) z klasy nadrzędnej (tego “wyżej” inaczej zwanego rodzicem, z ang.: parent) i tworzenie klasy potomnej (zwanej dzieckiem, ang.: child), która wynikowo posiada w sobie metody odziedziczone oraz własne, stworzone przez programistę. Jak widać taki zabieg pozwala uniknąć “ręcznego” kopiowania ze źródła klasy nadrzędnej. Wpływa to na czytelność kodu, zmniejszenie jego skomplikowania i uniknięcie błędów przy kopiowaniu kodu i ustalaniu jego współzależności. Ewidentnie wpływa też na zmniejszenie liczby pomyłek, przy aktualizacji kodu – nie trzeba zmieniać każdego fragmentu, gdzie występuje identyczny kod.

Poniżej przedstawiony jest kod pokazujący dziedziczenie jednobazowe i wykorzystanie tego w programie:

class Czlowiek(object):
    imie = None
    nazwisko = None
    def __init__(self, imie, nazwisko):
        self.imie = imie
        self.nazwisko = nazwisko

    def __str__(self):
        return f'{self.imie} {self.nazwisko}'



class Obywatel(Czlowiek):
    pesel = None
    def __init__(self, imie, nazwisko, pesel):
        self.pesel = pesel
        super().__init__(imie, nazwisko)

class Pracownik(Obywatel):
    nrUmowy = None
    def __init__(self, nrUmowy, imie, nazwisko, pesel):
        self.nrUmowy = nrUmowy
        super().__init__(imie, nazwisko, pesel)



pracownik = Pracownik("kadry/2025/um0034","Janina","Prochaska","97091287693")

print(pracownik.__str__())
print(pracownik.pesel)
print(pracownik.nrUmowy)

W programie zdefiniowaliśmy klasę Czlowiek, która zawiera takie pola jak: imie, nazwisko, dataUrodzenia (struktura złożona z trzech pól typu int: rok, miesiac, dzien) oraz wzrost. W programie powołaliśmy do życia obiekt czlowiek klasy Czlowiek.

Możemy “dostać się” do zdefiniowanych elementów poprzez instancję tegoż obiektu.

Druga klasa to Obywatel, zawiera identyczne dane jak w przypadku klasy Człowiek, ale posiada też cechę (pole) Pesel. Dlatego też sensownym jest odziedziczenie cech klasy Człowiek, definiując klasę Obywatel.

Także i mamy drugą klasę o nazwie Pracownik, w której zdefiniowaliśmy zmienną numerUmowy, i która to klasa dziedziczy (jednobazowo) z klasy Obywatel. W programie powołujemy do życia instancję klasy Pracownik w postaci obiektu pracownik.

Janina Prochaska
97091287693
kadry/2025/um0034

Process finished with exit code 0

Jak widać na powyższym przykładzie udało nam się stworzyć dość zaawansowaną definicję Pracownik, korzystając z już uprzednio zdefiniowanych, szerszych zakresowo definicji Obywatel, czy Człowiek. Teraz już chyba jest zrozumiałe, że aktualizacja kodu w klasie np. Człowiek spowoduje automatyczne wdrożenie tych zmian we wszystkich klasach potomnych.

Ten przykład nie wyczerpuje jednak zagadnienia dziedziczenia i ono będzie przedmiotem kolejnych rozważań.

Tematem obecnym będzie spostrzeżenie, że metoda __str__() odziedziczona z klasy Człowiek jest już nie przystająca do potrzeb wynikających w klasie Pracownik, gdzie chcielibyśmy jeszcze wyświetlić dane m.in. umowy. Dla tego celu zmodyfikujemy więc definicję klasy Pracownik, dodając do niej:

class Pracownik(Obywatel):
    nrUmowy = None
    def __init__(self, nrUmowy, imie, nazwisko, pesel):
        self.nrUmowy = nrUmowy
        super().__init__(imie, nazwisko, pesel)

    def __str__(self):
        return f'{self.imie} {self.nazwisko} {self.nrUmowy} {self.pesel}'


pracownik = Pracownik("kadry/2025/um0034","Janina","Prochaska","97091287693")

print(pracownik.__str__())

Na powrót zdefiniowana metoda __str__() przykryła nam poprzednią z klasy Człowiek.

Janina Prochaska kadry/2025/um0034 97091287693

Ale, ale… czy musimy redefinować całą metodę? Nie6a nawet nie powinniśmy, bowiem gdy zredefiniujemy w klasie Czlowiek metodę __str__() zmieniając np. format wyświetlanych danych, analogicznie dla zachowania porządku musimy to samo zrobić w innych, dziedziczących klasach i redefiniowanych metodach dla zachowania spójnego wyglądu… wystarczy wykorzystać poprzednią i uzupełnić ją o brakujące elementy np. tak:

class Pracownik(Obywatel):
    nrUmowy = None
    def __init__(self, nrUmowy, imie, nazwisko, pesel):
        self.nrUmowy = nrUmowy
        super().__init__(imie, nazwisko, pesel)

    def __str__(self):
        return f'{super().__str__()} {self.nrUmowy} {self.pesel}'

pracownik = Pracownik("kadry/2025/um0034","Janina","Prochaska","97091287693")

print(pracownik.__str__())

Jak widać, wygodniejsze, działa i… pokazuje, że pomimo przykrycia poprzedniej metody nową, do poprzedniej struktury mamy nadal dostęp.

Janina Prochaska kadry/2025/um0034 97091287693

Wynik działania jest identyczny, ale zabezpieczamy spójność programu niejako na przyszłość.

Zadanie B1: Napisz przykładowy kod programu zawierające definicje wynikające z przykładowej ilustracji dziedziczenia umieszczonej powyżej.

Zadanie B2: Jest firma transportowa posiadająca flotę samochodów i innych pojazdów. Zaproponuj strukturę w której mogliby zapisać niezbędne dane o ich pojazdach. Flota ta składa się z: 120 samochodów osobowych dla przedstawicieli handlowych, 56 samochodów ciężarowych do przewożenia ładunku, 3 statków oceanicznych dla transportu międzykontynentalnego, 5 samolotów dla transportu międzynarodowego i roweru dla woźnego w siedzibie firmy. Każdy z pojazdów ma swój tryb wykonywania przeglądu – zdefiniuj metodę, która poda jakie pojazdy winny mieć przegląd w najbliższym czasie.

Zadanie B3: Napisz program w którym zdefiniujesz graczy drużyn piłkarskich. Powołaj “do życia” dwa zespoły i dokonaj rozgrywki na zasadzie losowania 50:50 wygranej każdego z zawodników zespołu z każdym zawodnikiem przeciwnego zespołu (w ramach zespołu – każdy z każdym). Zespół, który ma więcej “wygrywających” zawodników, wygrywa rozgrywkę. Podaj wynik jako współczynnik wygrania wyliczony na podstawie wygranych indywidualnych 22 zawodników.

Przeciążanie metod

Znane w innych językach przeciążanie metod poprzez użycie kilku definicji metody o tej samej nazwie ale różnej ilości czy typie parametrów w języku Python nie jest obsłużone. Wynika to z powodu, że operatory w języku Python nie mają zdefiniowanego i przypisanego typu. Jest to kolejna różnica pomiędzy tym językiem i podejściem obiektowym, a innymi językami obiektowymi typu Java, C++ czy C#.

class S...
    def pokaz(self,a):
        return 255-a

    def pokaz(self,a,b):
        return a+b

s=S()
print(s.pokaz(10))

Powyższy przykład zgłosi błąd, że metoda pokaz() wymaga dwu argumentów (druga definicja nadpisze pierwszą).

Jednym ze sposobów obejścia tego problemu jest… wykorzystanie braku przypisania typu do operatora.

    def pokaz(self,a):
        if type(a) in [int, float]:
            if (a in range(0,255)):
                return "negacja bajtowa: " + (255-a).__str__()
            else: return "Wartość nie mieści się w bajcie"

        elif type(a) in [set, list, tuple]:
            wynik = 0
            for i in a: wynik = wynik + i
            return "sumowanie argumentów:"+wynik.__str__()

print(s.pokaz(10))

z={10,25}
print(s.pokaz(z))
negacja bajtowa: 245
sumowanie argumentów:35

Jak widać udało nam się zmodyfikować działanie metody w zależności od rodzaju (typu) argumentów użytych, ale nadal nie jest to typowe przeciążanie metod.

Innym sposobem jest rozszerzenie zakresu języka Python poprzez doinstalowanie pakietu dekoratora Multiple Dispatch, który pozwala użyć wprowadzonej funkcjonalności jak na poniższym przykładzie:

from multipledispatch import dispatch

# passing one parameter


@dispatch(int, int)
def product(first, second):
    result = first*second
    print(result)

# passing two parameters


@dispatch(int, int, int)
def product(first, second, third):
    result = first * second * third
    print(result)

# you can also pass data type of any value as per requirement


@dispatch(float, float, float)
def product(first, second, third):
    result = first * second * third
    print(result)


# calling product method with 2 arguments
product(2, 3)  # this will give output of 6

# calling product method with 3 arguments but all int
product(2, 3, 2)  # this will give output of 12

# calling product method with 3 arguments but all float
product(2.2, 3.4, 2.3)  # this will give output of 17.985999999999997

Dziedziczenie wielobazowe

Dziedziczenie wielobazowe zdaje się mieć sens, gdy rozważymy poniżej przedstawione zależności.

Niektórzy “puryści językowi” podnoszą kwestię, że dziedziczenie wielobazowe wpływa negatywnie na szereg cech programu – powstają niejednoznaczności lub tworzone są luki bezpieczeństwa. Z tego powodu takie dziedziczenie jest niedozwolone np. w Javie. Powstaje jedynie pytanie, czy w języku Python jest ono możliwe. Okazuje się, że tak. Przykład takiego kodu zaprezentowany jest poniżej.

class Samolot(object):
    nrRej=None
    silniki=None
    def __init__(self,nrRej):
        self.nrRej=nrRej
        print("latam nad ziemią...")

class Samochod(object):
    nrRej=None
    felgi=None
    VIN=None
    def __init__(self,nrRej):
        self.nrRej=nrRej
        self.VIN=VIN
        print("jeżdżę po drogach...")

class Statek(object):
    nrRej=None
    wypornosc=None
    def __init__(self,nrRej):
        self.nrRej=nrRej
        print("pływam po morzach i oceanach...")

class Amfibia(Statek,Samochod):
    def __init__(self,nrRej,wypornosc,VIN):
        super().__init__(VIN)
        super().__init__(wypornosc)
        self.nrRej=nrRej
        self.wypornosc=wypornosc
        self.VIN=VIN



a  = Amfibia("SO5564T","785kg","JP1OPR65TY667655673")

print(f'Numer rejestracji: {a.nrRej} wyporność: {a.wypornosc} z VIN: {a.VIN}')
pływam po morzach i oceanach...
Numer rejestracji: SO5564T wyporność: 785kg z VIN: JP1OPR65TY667655673

Jak widać z powyższego przykładu… coś zadziałało, błędów nie zgłoszono, raportuje się wszystko “poprawnie”… ale jednak nie. W przykładzie powołaliśmy dziedziczenie wielobazowo oparte na dwu klasach, podczas, gdy przy inicjacji… zaprezentowała się jedynie jedna klasa (Statek)… Co więcej… przypisania self.nrRej, self.wypornosc, self.VIN wcale nie muszą dotyczyć odziedziczonych pól danych z klas nadrzędnych! Zatem o co chodzi?

class Samolot(object):
    nrRej=None
    silniki=None
    def __init__(self,nrRej):
        self.nrRej=nrRej
        print("latam nad ziemią...")

class Samochod(object):
    nrRej=None
    felgi=None
    VIN=None
    def __init__(self,nrRej,felgi,VIN):
        self.nrRej=nrRej
        self.VIN=VIN
        self.felgi=felgi
        print("jeżdżę po drogach...")

class Statek(object):
    nrRej=None
    wypornosc=None
    def __init__(self,nrRej,wypornosc):
        self.nrRej=nrRej
        self.wypornosc=wypornosc
        print("pływam po morzach i oceanach...")

class Amfibia(Statek,Samochod):
    def __init__(self,nrRej,wypornosc,VIN,felgi):
        Samochod.__init__(self,nrRej,felgi,VIN)
        Statek.__init__(self,nrRej,wypornosc)


a  = Amfibia("SO5564T","785kg","JP1OPR65TY667655673","R18")

print(f'Numer rejestracji: {a.nrRej} wyporność: {a.wypornosc} z VIN: {a.VIN}')

jeżdżę po drogach...
pływam po morzach i oceanach...
Numer rejestracji: SO5564T wyporność: 785kg z VIN: JP1OPR65TY667655673

Process finished with exit code 0

Czyli TAK MA BYĆ… Powstaje pytanie, czy aby na pewno jest wszystko w porządku!!?? To zmodyfikujemy dwukrotnie kod, zmieniając kolejność dwu linijek7… i zmieniając wartość przypisywaną jako nrRej statkowi, byśmy byli w stanie rozróżnić do którego z pól o nazwie nrRej zapisaliśmy ustawianą wartość SO5564T:

class Amfibia(Statek,Samochod):
    def __init__(self,nrRej,wypornosc,VIN,felgi):
        Statek.__init__(self,"Amfibie mają numer rejestracji zgodne z samochodami",wypornosc) 
        Samochod.__init__(self, nrRej, felgi, VIN)


a  = Amfibia("SO5564T","785kg","JP1OPR65TY667655673","R18")

print(f'Numer rejestracji: {a.nrRej} wyporność: {a.wypornosc} z VIN: {a.VIN}')
pływam po morzach i oceanach...
jeżdżę po drogach...
Numer rejestracji: SO5564T wyporność: 785kg z VIN: JP1OPR65TY667655673

Process finished with exit code 0
class Amfibia(Statek,Samochod):
    def __init__(self,nrRej,wypornosc,VIN,felgi):
        Samochod.__init__(self, nrRej, felgi, VIN)
        Statek.__init__(self,"Amfibie mają numer rejestracji zgodne z samochodami",wypornosc)


a  = Amfibia("SO5564T","785kg","JP1OPR65TY667655673","R18")

print(f'Numer rejestracji: {a.nrRej} wyporność: {a.wypornosc} z VIN: {a.VIN}')
pływam po morzach i oceanach...
jeżdżę po drogach...
Numer rejestracji: Amfibie mają numer rejestracji zgodne z samochodami wyporność: 785kg z VIN: JP1OPR65TY667655673

Process finished with exit code 0

Jak widać z dwu przykładów – wyniki są dwa odrębne w zależności od kolejności inicjacji obiektów z klas nadrzędnych…

Niebezpieczeństwo wynika z braku części deklaratywnej i umożliwienia deklarowania pól danych w dowolnym momencie. W przypadku tworzenia dziedziczenia sekwencyjnego nie ma to znaczenia, bowiem sekwencyjny dostęp do pól nie jest wieloznaczny. Gorzej jest w przypadku, gdy mamy do czynienia z różnymi klasami zawierającymi te same nazwy pól danych we współdziedziczeniu wielobazowym. Wtedy niejednoznaczność użycia pola powoduje użycie pól w zależności od kolejności odwołań się do nich. Dlatego też w takim programowaniu należy być bardzo precyzyjnym w określaniu dostępów.

class Samolot(object):
    nrRej=None
    silniki=None
    def __init__(self,nrRej):
        Samolot.nrRej=nrRej
        print("latam nad ziemią...")

class Samochod(object):
    nrRej=None
    felgi=None
    VIN=None
    def __init__(self,nrRej,felgi,VIN):
        Samochod.nrRej=nrRej
        Samochod.VIN=VIN
        Samochod.felgi=felgi
        print("jeżdżę po drogach...")

class Statek(object):
    nrRej=None
    wypornosc=None
    def __init__(self,nrRej,wypornosc):
        Statek.nrRej=nrRej
        Statek.wypornosc=wypornosc
    print("pływam po morzach i oceanach...")

class Amfibia(Statek,Samochod):
    def __init__(self,nrRej,wypornosc,VIN,felgi):
        Statek.__init__(self,"Amfibie mają numer rejestracji zgodne z samochodami",wypornosc)
        Samochod.__init__(self, nrRej, felgi, VIN)

    def getRej(self):
        return Samochod.nrRej

a  = Amfibia("SO5564T","785kg","JP1OPR65TY667655673","R18")

print(f'Numer rejestracji: {a.getRej()} wyporność: {a.wypornosc} z VIN: {a.VIN}')
pływam po morzach i oceanach...
jeżdżę po drogach...
Numer rejestracji: SO5564T wyporność: 785kg z VIN: JP1OPR65TY667655673

Process finished with exit code 0

Teraz zamieńmy w klasie Amfibia kolejność dwu linijek celem sprawdzenia, czy nadal odwołujemy się do tego samego pola…

class Amfibia(Statek,Samochod):
    def __init__(self,nrRej,wypornosc,VIN,felgi):
        Samochod.__init__(self, nrRej, felgi, VIN)
        Statek.__init__(self,"Amfibie mają numer rejestracji zgodne z samochodami",wypornosc)
        
    def getRej(self):
        return Samochod.nrRej

a  = Amfibia("SO5564T","785kg","JP1OPR65TY667655673","R18")

print(f'Numer rejestracji: {a.getRej()} wyporność: {a.wypornosc} z VIN: {a.VIN}')
jeżdżę po drogach...
pływam po morzach i oceanach...
Numer rejestracji: SO5564T wyporność: 785kg z VIN: JP1OPR65TY667655673

Process finished with exit code 0

Tak więc… działa. Korzystając z precyzyjniej nazwanych struktur danych w klasach z których dziedziczymy; wiedząc, że Python dla tego celu wykorzystuje poznaną już poprzednio Name Mangling; potrafimy rozróżnić pola wartości (cechy) o tych samych nazwach z poszczególnych klas i odwołać się dokładnie do tego pola, które chcemy przetwarzać.

Zadanie C1: Stwórz klasę opisującą człowieka, zdefiniuj przynajmniej 2 niezależne uczelnie (określ cechy wspólne i charakterystyczne dla tych uczelni), a następnie zdefiniuj Studenta, uczęszczającego na obie uczelnie. Powołaj do życia 100 studentów – losowo przypisując ich do jednej, lub drugiej uczelni (mogą być studentami tylko pierwszej, tyko drugiej, lub obu uczelni). Wyświetl wyniki tych studentów z ostatniego semestru…

Spytaj prowadzącego o hasło
To view this protected content, enter the password below: