In [1]:
# setup
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"

Programowanie obiektowe

W sytuacji, w których używamy bibliotek utworzonych przez innych autorów znajdujące się tam funkcje działają na dwa różne sposoby:

  1. jako funkcja pobierająca obiekt jako jeden z argumentów: func(object)
  2. jako metody obiektów object.func()

W przypadku pierwszym, nazwyanym programowaniem proceduralnym, argumentem funkcji może być dowolny obiekt, niekoniecznie właściwy, w drugim przypadku obiekt posiada ograniczoną liczbę metod, jaką można dla niego zastosować. Listę metod obiektów możemy wywołać poleceniem dir() zastosowanym dla danego obiektu. Jeżeli obiekt jest typu prostego, zrozumiałego dla języka Python, zastosowanie roziązania #1 jest o tyle wygodne, że pozwala napisać funkcję w sposób generyczny (ogólny) z zastosowaniem do dowolnych obiektów, utworzonych w dowolny sposób. Jeżeli jednak obiekt ma złożoną strukturę wewnętrzną, która jest wymagana do prawidłowego działania funkcji, taka sytuacja sprawia że funkcja staje się potencjalnym źródłem błędów i frustracji w przyszłości. Porównajmy:

In [2]:
def add_posfix(strg):
    return str(strg) + '_postfix'

def mod_lista(lista):
    return str(lista[0])+"_"+str(lista[1][1])

add_posfix(11)
add_posfix("napis")
add_posfix([1,2,3])
"###"
lista = ["napis",["jeden","dwa"]]
mod_lista(lista)
Out[2]:
'11_postfix'
Out[2]:
'napis_postfix'
Out[2]:
'[1, 2, 3]_postfix'
Out[2]:
'###'
Out[2]:
'napis_dwa'

Powyższy przykład pokazuje, że funkcja add_posfix jest w stanie w sposób bezpieczny przetworzyć dowolny argument, który jest w stanie przekształcić na łańcuch (w praktyce każdy prosty obiekt Pythona) natomiast funkcja druga wymaga prawidłowo zbudowanej zagnieżdżonej listy (min. dwa elementy, element drugi musi mieć również min. dwa elementy) co w praktyce czyni ją bezużyteczną. Oznacza to że w przypadku złożonych obiektów zdecydowanie lepiej jest wyposażyć obiekt w komplet metod, które będą w stanie pracować na danym obiekcie, jak pokazano w #2, bez konieczności zmuszania użytkownika do dbania o jego prawidłową strukturę. Do dego celu służy programowanie obiektowe dokładnie, programowanie zorientowane obiektowo.

Klasy obiektów

Klasy i obiekty to podstawa programowania obiektoweg. Klasa definiuje typ danych (i jego metody) natomiast obiekt jest instancją (przypadkiem) klasy. Tak na prawdę Python jest w całości językiem obiektowym. Na przykład istnieje klasa _int_ a zmienna liczba =11 jest instancją tej klasy. Obiekty przechowują zmienne -- o różnym stopniu złożoności -- te zmienne określa się jako pola (fields) obiektu. Funkcje, które są przypisane do klasy obiektu i operują na jego zawartości nazwywamy metodami obiektu (methods).

Tworzenie klasy

Klasę definiuje się podobnie jak funkcję, z tym że stosuje się słowo kluczowe class. Upraszczając zagadnienie, które w szczegółach można przeczytać w [https://pl.wikibooks.org/wiki/Zanurkuj_w_Pythonie/], aby zdefiniować klasę będziemy potrzebować mechanizmu definiowania zmiennych oraz metod pracujących na tych zmiennych. Zmienne dzieją się na dwie grupy: zmienne klasy (o których poźniej) oraz ważniejsze: zmienne obiektu. Te ostatnie muszą być poprzedzone słowem self, które wskazuje że zmienna -- oraz ew. metody, należą do klasy a nie są zmiennymi czy funkcjami globalnymi zadeklarowanymi poza klasami. Zmienne klasy mogą być definiowane w obrębie metod klasy lub w specjalnej metodzie init, która wykonywana jest w momencie tworzenia klasy.

Metoda __init__

Zwana jest popularnie (i nieprawidłowo) konstruktorem klasy, służy do zainicjowania wartości wybranych zmiennych tworzonego obiektu (nie samego obiektu!). Python nie wymaga aby inicjowane były wszystkie zmienne, w dowolnym momencie, dowolną funkcją można zainicjować kolejne zmienne. Metoda __init__ nie jest obligatoryjna, pozwala jednak przyjąć argumenty w czasie tworzenia klasy. Metoda __init__, podobie jak inne metody musi jako pierwszy argument przyjąć self

In [3]:
class napis:
    def __init__(self, np1, wartosc):
        self.napis1 = np1
        self.liczba = wartosc # pomijamy sprawdzanie typów
        self.lista = [1,0,2]

obj = napis("napis",10) # pomijamy argument self
print(obj.napis1)
print(obj.liczba) 
print(obj.lista)
napis
10
[1, 0, 2]

Metody

Metody do funkcje definiowane w obrębie klasy i działające na polach danego obiektu. Podobnie jak metoda __init__ ich argumenty muszą się rozpoczynać od od słowa self, aby pokazać, że jest metodą danej klasy. W ramach przykładu pokażemy jak w "bezpieczny" sposób pracować ze strukturą pokazaną w przykładzie mod_lista powyżej

In [4]:
class moja_lista:
    def __init__(self, np1, np20, np21):
        self.lista = [np1,[np20,np21]] # zbudowanie listy
        self.np1 = np1
        self.np2 = 2
    def mod_lista(self): # żadnych dodatkowych argumentów!
        return str(self.lista[0])+"_"+str(self.lista[1][1])
    
lst = moja_lista("slowo1","slowo2","slowo3")
lst.mod_lista()
Out[4]:
'slowo1_slowo3'

Rodzaje zmiennych i metod

Python umożliwia modyfikowanie zmiennych w obrębie obiektów poprzez ich zwykłe nadpisanie. Powoduje to że działanie obiektu może być zmodyfikowane w sposób niezgodny z jego przeznaczeniem. Jeżeli nadpiszemy pole lista lub funkcję lista inną zmienną lub wartością obiekt będzie zachowywał się niezgodnie z przeznaczeniem lub będzie zwracał błąd:

In [5]:
lst.lista=10
lst.mod_lista=20
lst.mod_lista
Out[5]:
20

Aby zapobiec takim sytuacjom w programowaniu obiektowym w Pythonie, podobnie jak w innych językach programowania pola i metody klasy dzielą się na trzy grupy:

  • publiczne (public): wszystkie zmienne i metody domyślnie są publiczne. Oznacza to że są dostępne dla użytkownika klasy i mogą być w dowolny sposób zmieniane, modyfikowane itp.
  • prywatne (private): Jeżeli zmienne z jakiegoś powodu są istotne dla dla działania klasy powinny być definiowane. W takiej sytuacji użytkownik nie może ich zmodyfikować, chyba że autor klasy da taką możliwość poprzez dostarczenie odpowiedniej metody. Zmienne i metody prywatne rozpoczynają się od podwójnego podkreślnika __zmienna.
  • chronione (protected): Pośrednim typem zmiennych są zmienne chronione, rozpoczynające się od pojedynczego podkreślnika: _zmienna. Nie powinny być modyfikowane przez użytkownika, ale mogą być modyfikowane przez obiekty będące instancjami tzw. klas pochodnych. W przypadku Pythona kluczowe jest pojęcie "powinny". Język w żaden sposób nie chroni tych zmiennych, odbywa się to "by convention"

Dobrze napisane funkcje modyfikujące zmienne prywatne z reguły poza modyfikowaniem zmiennej prywatnej dokonują również modyfikacji w obrębie klasy tak aby zachować jej spójność. W poniższym przykładzie założymy że istnieje zależność pomiędzy dwoma zmiennymi, jedna jest dwukrotnością drugiej. Jeżeli zmieniamy jedną modyfikacji powinna też ulec druga:

In [6]:
class zmienne:
    def __init__(self):
        self.publiczna = 10
        self._chroniona = 20
        self.__prywatna1 = 30
        self.__prywatna_podwojna = self.__prywatna1*2 # jest zależność między zmiennymi
    
    def pobierz_prywatna2(self):
        return self.__prywatna_podwojna
    
    def pobierz_prywatna1(self):
        return self.__prywatna1
    
    def zmien_prywatna1(self,nowa_wart): # zmieniamy dwie waratości
        self.__prywatna1 = nowa_wart
        self.__prywatna_podwojna = self.__prywatna1*2

zm = zmienne()
dir(zm)
Out[6]:
['__class__',
 '__delattr__',
 '__dict__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__gt__',
 '__hash__',
 '__init__',
 '__le__',
 '__lt__',
 '__module__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 '__weakref__',
 '_chroniona',
 '_zmienne__prywatna1',
 '_zmienne__prywatna_podwojna',
 'pobierz_prywatna1',
 'pobierz_prywatna2',
 'publiczna',
 'zmien_prywatna1']
In [7]:
zm.publiczna
zm.publiczna = 30
zm.publiczna
Out[7]:
10
Out[7]:
30
In [8]:
zm._chroniona
zm._chroniona = 40 # nie powinniśmy tego robić!!!
zm._chroniona
Out[8]:
20
Out[8]:
40
In [9]:
zm.pobierz_prywatna1()
zm.pobierz_prywatna2()
"###"
zm.zmien_prywatna1(100)
zm.pobierz_prywatna1()
zm.pobierz_prywatna2()
Out[9]:
30
Out[9]:
60
Out[9]:
'###'
Out[9]:
100
Out[9]:
200

Dekoratory klas: property i setter

Jeżeli twórca klasy zdecydował się udostępnić możliwość modyfikacji wybranych zmiennych prywatnych, to oznacza że można ją zmienić. W takiej sytuacji wygodnie jest obsługiwać zmienne poprzez zwykłe przypisanie aniżeli funkcje. Nowoczesne języki obiektowe posiadają specjalne procedury do obsługi zmiennych prywatnych określane jako getters i setters. Obsługa klasy poprzez zmienne prywante uniemożliwia wygodę modyfikacji zmiennych klasy poprzez proste przypisanie, bez konieczności martwienia się o ewentulne zależności lub znajomosć składni prywantych fukcji klasy. Można to ominąć poprzez zastosowanie dekoratorów @proterty oraz @setter.

Dekoratory to są to nazwy, które w sposób niejawny zmieniają działanie wybranych fragmentów kodu. Ich stosowanie pozwala na napisać kod w sposób prosty i zrozumiały a następnie zmodyfikować działanie funkcji w sposób, którego wykonianie w sposób prosty nie było by możliwe.

Dekorator @property pozwala zmodyfikować funkcję zwracającą wartość zmiennej prywatnej, tak aby naśladowała zachowanie zmiennej publicznej. Podobnie dekorator @setter pozwala na zmodyfikowanie działania funkcji modyfikującej zmienną prywatną (i jej zależności) poprzez proste przypisanie. Jako przykład zmodyfikujemy poprzednią klasę, w taki sposób, aby umożliwić modyfikację zmiennej prywatnej poprzez zwykłe przypisanie.

In [10]:
class zmienne:
    def __init__(self):
        self.publiczna = 10
        self._chroniona = 20
        self.__prywatna1 = 30
        self.__prywatna_podwojna = self.__prywatna1*2 # jest zależność między zmiennymi
    
    def pobierz_prywatna2(self):
        return self.__prywatna_podwojna
    
    @property
    def zmienna_prywatna(self):
        return self.__prywatna1
    
    @zmienna_prywatna.setter
    def zmienna_prywatna(self,nowa_wart): # zmieniamy dwie waratości
        self.__prywatna1 = nowa_wart
        self.__prywatna_podwojna = self.__prywatna1*2

zm = zmienne()

Aby zasymulować takie działanie należy utworzyć dwie funkcje o tej samej nazwie, która będzie symulowała zmienną. Funkcję zwaracającą wartość poprzedzamy dekoratotem @property natomiast funkcję modyfikującą zmienną prywatną1 poprzedzamy dekoratorem @zmienna_prywatna.setter, tak aby jednoznacznie przypisać do jakiej zmiennej dany setter się odnosi. Należy zaznaczyć że zmienna_prywatna2 nie jest udostępniona dla użytkownika klasy w związku z tym nie jest udostępniana.

In [11]:
zm.zmienna_prywatna
zm.pobierz_prywatna2()
"###"
zm.zmienna_prywatna = 100
zm.zmienna_prywatna
zm.pobierz_prywatna2()
Out[11]:
30
Out[11]:
60
Out[11]:
'###'
Out[11]:
100
Out[11]:
200

Dziedziczenie

Dziedziczenie jest mechanizmem, który ma na celu ułatwienie zarządzania strukturami danych które współdzielą ze sobą część danych i metod. Ułatwia to proces budowania oprogramowania, w przypadku programowania grupowego, pozwala również modyfikować istniejące klasy w taki sposób aby były one dostosowane do potrzeb użytkownika.

W przykładzie zbudujemy klasę nadrzędną miejscowosc, która będzie defniowała imię i nazwisko jako zmienne prywatne, oraz dwie funkcje: publiczną _przedstawsie i chronioną _ dane a więc niedostępne do zmiany po zainicjowaniu, oraz dwie klasy potomne. Każda z nich będzie redefniować funkcję _przedstawsie ale jednoczesnie wykorzystywać funcję _ dane w taki sposób aby modyfikując klasę osoba i zasady jej działania nie wpływać na działanie klas potomnych

In [12]:
# nie zakładamy sprawdzania zgodności typów
class miejscowosc:
    def __init__(self,nazwa,powiat):
        self.__nazwa=nazwa # zmienne prywatne, niedostępne do modyfikacji po zainicjowaniu
        self.__powiat=powiat
        
    def lokalizacja(self):
        print(self.__nazwa + ", powiat " + self.__powiat)
        
    def _powiat(self):
        '''metoda z założenia chroniona jedynie do wykorzystania w klasach potomnych'''
        return self.__powiat
    
    def _nazwa(self):
        '''metoda z założenia chroniona jedynie do wykorzystania w klasach potomnych'''
        return self.__nazwa
      

miejscowosc1 = miejscowosc("Luboń","poznański")
miejscowosc1.lokalizacja()
       
Luboń, powiat poznański

W następnym kroku zdefiniujemy dwie klasy potomne: miasto i wies. Dla uproszczenia zakładamy że miasto jest jednocześnie gminą. Pole miejscowosc i powiat nie zmienią się, dodamy pole gmina dla wsi oraz w obu klasach zmodyfikujemy funkcję lokalizacja()

Ponieważ zmienne nazwa i powiat są zmiennymi prywatnymi klasy miejscowosc nie są dostępne nigdzie indziej. Z tego powodu udostępnimy je w formie funkcji chronionych, czyli dostępnych w klasach potomnych, ale nie poza nimi.

In [13]:
class miasto(miejscowosc):
    def __init__(self,nazwa,powiat):
        miejscowosc.__init__(self,nazwa,powiat)
    
    def lokalizacja(self):
        print('Miasto i gmina {0:%>s}, powiat {1:%>s}'.format(self._nazwa(), self._powiat()) )
        
        
miasto1 = miasto("Nowe","świecki")
miasto1.lokalizacja()
Miasto i gmina Nowe, powiat świecki

W klasie wies, dodamy dodatkową zmienną gmina i również zmodyfikujemy funkcję lokalizacja. Należy zauważyć, że zmienna __gmina jest prywatną zmienną klasy wies, i można w obrębie klasy odwoływać się do niej bezpośrednio.

In [14]:
class wies(miejscowosc):
    def __init__(self,nazwa,gmina,powiat):
        miejscowosc.__init__(self,nazwa,powiat)
        self.__gmina = gmina
    
    def lokalizacja(self):
        print('{0:%>s}, gmina {1:%>s}, powiat {2:%>s}'.format(self._nazwa(), self.__gmina, self._powiat()) )
        
        
wies1 = wies("Warlubie","Nowe","świecki")
wies1.lokalizacja()
Warlubie, gmina Nowe, powiat świecki

Ćwiczenie

Przygotować obiekt przechowujący wybrane dane na temat powiatu i wyświetlający podsumowanie