TI/Programowanie dla Fizyków Medycznych:Klasy

Z Brain-wiki
Wersja z dnia 14:22, 23 maj 2015 autorstwa Jarekz (dyskusja | edycje) (Utworzono nową stronę "Klasy są związane z programowanie zorientowanym obiektowo (Object Oriented Programming). Dotąd pisane przez nas programy składały się z ciągu instrukcji, które b...")
(różn.) ← poprzednia wersja | przejdź do aktualnej wersji (różn.) | następna wersja → (różn.)

Klasy są związane z programowanie zorientowanym obiektowo (Object Oriented Programming). Dotąd pisane przez nas programy składały się z ciągu instrukcji, które były wykonywane jedna za drugą, niektóre wiele razy lub tylko przy spełnieniu pewnych warunków. OOP wprowadza nowe podejście do programowania - o programie staramy się myśleć jak o modelu rzeczywistości. Jak ten model będzie wyglądał zależy od rozważanego problemu, jeśli na przykład będziemy pisać program dotyczący pracy banku pojawią się w nim takie obiekty jak: klient, kasjer, konto, kredyt czy lokata - każdy z tych obiektów reprezentuje inną klasę z którą wiążą się pewne możliwe działania (metody klasy) - na przykład kasjer może obsłużyć danego klienta, lokata może zostać otwarta, a konto zapytane o stan. Istnieje prosta reguła mówiąca jak wyszczególnić klasy występujące w danym problemie: opiszmy słownie rozważane zagadnienie, podkreślmy wszystkie rzeczowniki - to będą klasy i wszystkie czasowniki - to będą metody danych klas. Szybko można zauważyć, że niektóre klasy mają ze sobą coś wspólnego na przykład zarówno kasjer jak i klient są osobami, zatem mają imię i nazwisko - w programowaniu obiektowym o takiej zależności mówi się, że kasjer JEST osobą i klient JEST osobą, a realizuje się ją przez dziedziczenie - klasa kasjer i klasa klient dziedziczą po klasie osoba. Klasa osoba może dostarczać pewnych metod - na przykład podajImię lub podajNazwisko - przy zastosowaniu dziedziczenia klasy pochodne (podklasy) także będą miały te metody. Dodatkowo podklasy mogą przesłaniać metody zdefiniowane w nadklasie definiując metody o tej samej nazwie co metody nadklasy, wtedy te same metody wywołane na rzecz klienta i kasjera będą miały różny skutek - ten mechanizm nazywany jest polimorfizmem. Kolejną zaletą programowania obiektowego jest łatwość wielokrotnego używania kodu. Gdy napiszemy i przetestujemy jakiś fragment naszego projektu to chcielibyśmy aby już nigdy nie trzeba było go zmieniać, aby przypadkiem czegoś nie uszkodzić, ale z drugiej strony zależy nam na łatwej modyfikacji na wypadek pojawienia się nowych wymagań projektu - te dwa dążenia są w oczywisty sposób sprzeczne. Ale mechanizm dziedziczenia pozwala zaspokoić oba wymogi - nowe funkcjonalności realizujemy w klasach dziedziczących po już istniejących, w ten sposób nie modyfikujemy napisanego raz kodu, a z drugiej strony łatwo rozbudowujemy nasz projekt. Po tym krótkim wstępie ideowym przejdziemy do opisu tworzenia klas w Pythonie. Klasy definiuje się za pomocą słowa kluczowego class, po nim następuje nazwa klasy, w nawiasie lista klas po których dziedziczy tworzona klasa i dwukropek:

class A(object):
    pass

Klasa w przykładzie dziedziczy po object, nie jest to wymagane, ale klasy, które nie dziedziczą po object są klasami Pythona w starym stylu i nie posiadają części funkcjonalności, która będzie tu opisywana. W ciele klasy możemy definiować różne metody. Określone metody rozpoczynające się od __ są nazywane metodami magicznymi, gdyż Python będzie je wywoływał niejawnie przy różnych okazjach. Najczęściej stosowaną metodą magiczną jest konstruktor (__init__) - metoda wywoływana przy tworzeniu obiektu - jeśli nie podamy konstruktora to jest generowany domyślny bezargumentowy konstruktor, który wywołuje bezargumentowe konstruktory nadklas. Należy pamiętać, że gdy sami definiujemy konstruktor musi on wywołać konstruktory nadklas. W innych językach programowania występuje mechanizm przeładowywania nazw funkcji - definiuje się wiele funkcji o tych samych nazwach ale różnych parametrach i w czasie wywołania na podstawie listy argumentów wywoływana jest odpowiednia funkcja, takie działanie jest niemożliwe w Pythonie - w szczególności jeśli chcemy mieć "różne konstruktory" klasy (np. przyjmujący liczbę całkowitą, dwie liczby zmiennoprzecinkowe i bezparametrowy to musimy wykorzystać mechanizm domyślnych argumentów lub sprawdzania typów przekazanych argumentów, napisanie dwóch konstruktorów spowoduje, że tylko ostatni będzie widoczny). Pierwszym argumentem wszystkich metod klasy musi być zmienna na którą zostanie przypisana referencja do obiektu na rzecz którego została wywołana dana metoda (zwyczajowo nazywa się ją self):

class A(object):
    y = 5
    def __init__(self, x = 0):
        super(A, self).__init__()
        self.x = x
    def f(self, z):
        print self.x, self.y, z
        
print A.y #sięganie do zmiennej klasowej y przez klasę, a nie obiekt klasy
a = A() #tworzenie obiekty klasy konstruktorem "bezargumentowym"
b = A(5) #tworzenie obiekty klasy konstruktorem "jednoargumentowym"
a.f(2) #wywołanie metody f na rzecz obiektu a
A.f(a, 2) #alternatywna forma powyższego wywołania
b.f(2) #wywołanie metody f na rzecz obiektu b
A.f(b, 2) #alternatywna forma powyższego wywołania

W powyższym przykładzie tworzymy klasę A, która dziedziczy po object, wewnątrz niej zmienną y, która jest klasowa (wspólna dla wszystkich obiektów danej klasy, można się do niej dostać bezpośrednio przez klasę - nie potrzeba obiektu tej klasy). Z kolei zmienna x jest tworzona na poziomie instancji (obiektu) klasy, oznacza to, że każdy obiekt, będzie miał swoją zmienną x - takie zmienne mogą być tworzone w metodach klasy i ich nazwy muszą być poprzedzone self. W komentarzach opisy konstruktorów są wzięte w cudzysłowy, gdyż tak na prawdę w obu wywołaniach jest to ten sam konstruktor (z przyczyn opisanych wcześniej). Tworzenie obiektów klas odbywa się przez podanie nazwy klasy i w nawiasie argumentów konstruktora - taka konstrukcja zwraca obiekt danej klasy, można go przypisać na zmienną i następnie na nim wywoływać metody klasy przy pomocy konstrukcji z kropką: obiekt.metoda(argumenty) w tym zapisie niejawnie na zmienną self przekazywany jest obiekt na którym została wywołana metoda, alternatywna konstrukcja jawnie przekazuje obiekt do self. Zobaczmy teraz prosty przykład dziedziczenia:

class X(object):
    def d(self):
        print 'metoda d z klasy X'

class B(object):
    def c(self):
        print 'metoda c z klasy B'
        
class A(X):
    def a(self):
        print 'metoda a z klasy A'
    def b(self):
        print 'metoda b z klasy A'

class C(B):
    def a(self):
        print 'metoda a z klasy C'
    def b(self):
        print 'metoda b z klasy C'
    def d(self):
        print 'metoda d z klasy C'
    def e(self):
        print 'metoda e z klasy C'

class D(A, C):
    def a(self):
        print 'metoda a z klasy D'

d = D()
d.a()
d.b()
d.c()
d.d()
d.e()

wynikiem wykonania tego skryptu jest:

>>>
metoda a z klasy D
metoda b z klasy A
metoda c z klasy B
metoda d z klasy X
metoda e z klasy C

Teraz parę słów wyjaśnienia: gdy wywołujemy metodę na obiekcie pewnej klasy to jej definicja jest poszukiwana najpierw w danej klasie jeśli zostanie tu znaleziona to jest wywoływana i poszukiwania się kończą (tak jest w przypadku d.a() - mimo, że w klasach A i C były metody a() to kasa D nadpisała tą metodę i metoda nadpisana jest wywoływana), jeśli poszukiwana metoda nie zostanie znaleziona w danej klasie to przeszukiwane są nadklasy rozpoczynając od tej najbardziej na lewo w definicji naszej klasy, jeśli w niej zostanie znaleziona pożądana metoda to zostanie ona wywołana (tak jest w przypadku d.b()), jeśli nie to przeszukiwane są rekurencyjnie klasy bazowe pierwszej nadklasy naszej klasy (znów od lewej) (w wyniku tych poszukiwań zostanie znaleziona metoda d.d() z klasy X), jeśli w pierwszej nadklasie i jej klasach bazowych nie znaleziono danej metody to przechodzimy do poszukiwania w drugiej nadklasie (w tym przypadku jest to klasa C) i tu zostaną znalezione metody d.c() i d.e(). Warto zwrócić uwagę na fakt, że kolejność podawania klas bazowych jest znacząca, dziedziczenie po wielu klasach często sprawia problemu i dlatego w niektórych jeżykach programowania (np. Java) występuje tylko pojedyncze dziedziczenie i dodatkowo mechanizm interfejsów zapewniający, że definiowana klasa implementuje pewne określone metody. Zmieniając w przykładzie kolejność klas po których dziedziczy klasa D dostajemy wynik:

>>> 
metoda a z klasy D
metoda b z klasy C
metoda c z klasy B
metoda d z klasy C
metoda e z klasy C

Tak na prawdę to przed przeszukiwaniem klasy, której instancją jest dany obiekt przeszukiwana jest jeszcze sama instancja - okazuje się, że w Pythonie nawet po stworzeniu obiektu można dodać do niego metody lub atrybuty:

class D(A, C):
    def a(self):
        print 'metoda a z klasy D'

def a():
    print 'metoda a dodana do instancji'

d = D()
d.a = a
d.a()
d.b()
d.c()
d.d()
d.e()

Teraz metoda d.a() jest znaleziona w instancji, a nie w klasie D i wynik tego skryptu jest następujący:

>>> 
metoda a dodana do instancji
metoda b z klasy A
metoda c z klasy B
metoda d z klasy X
metoda e z klasy C

Atrybuty klas można też definiować w bardzo elegancki sposób za pomocą funkcji wbudowanej property. Pozwala ona definiować atrybuty tylko do odczytu, a także definiować funkcje, które mają być wywołane w celu obliczenia wartości żądanego atrybutu. Dla przykładu jeśli chcemy mieć atrybut x, który będzie tylko do odczytu możemy napisać:

class A(object):
    def __init__(self, x):
        super(A, self).__init__()
        self._x = x
    def getX(self):
        return self._x
    x = property(getX)

a = A(5)
print a.x
a.x = 6

W Pythonie obowiązuje konwencja, że atrybuty rozpoczynające się od _ są prywatne i nie należy próbować ich odczytywać ani modyfikować poza klasą, jest to jednak tylko konwencja. Funkcja property przyjmuje 4 argumenty: pierwszy - obowiązkowy, podający funkcję, która służy do pobrania wartości danego atrybutu, pozostałe opcjonalne - fset - metoda do ustawiania wartości atrybutu (wykorzystywana w przypisaniach), fdel - metoda do usuwania atrybutu (wykorzystywana w poleceniu del) i doc - przyjmujący napis będący opisem danego atrybutu (będzie widoczny w helpie do klasy).

class A(object):
    def __init__(self, x):
        super(A, self).__init__()
        self._x = x
    def getX(self):
        return self._x
    def setX(self, x):
        self._x = x
    def delX(self):
        del self._x
    x = property(getX, setX, delX, "Zmienna x")

a = A(5)
print a.x
a.x = 2
del a.x

Opiszę teraz szereg metod magicznych powalających naszej klasie na upodobnianie się do obiektów wbudowanych w Pythona.

Aby umożliwić operacje algebraiczne na obiektach naszej klasy należy definiować metody __add__(), __sub__(), __mul__(), __div__(), __floodiv()__ czy __pow__() dla odpowiednio operacji +, -, *, /, // i **, podobnie dla operatorów logicznych and, or i xor możemy zdefiniować metody __and__(), __or__() i __xor__(), wszystkie te metody przyjmują (poza self), jeden parametr będący drugim argumentem operatora, na przykład (na self zostanie przekazany w1, a na w w2):

class Wektor(object):
    def __init__(self, x, y):
        super(Wektor, self).__init__()
        self.x = x
        self.y = y
    def __add__(self, w):
        return Wektor(self.x + w.x, self.y + w.y)

w1 = Wektor(1, 3)
w2 = Wektor(2, 5)
w3 = w1 + w2
print w3.x, w3.y

Można też przedefiniować operatory typy += - odpowiednie metody mają nazwy rozpoczynające się od __i (te metody powinny zwracać self):

class Wektor(object):
    def __init__(self, x, y):
        super(Wektor, self).__init__()
        self.x = x
        self.y = y
    def __iadd__(self, w):
        self.x += w.x
        self.y += w.y
        return self

w1 = Wektor(1, 3)
w2 = Wektor(2, 5)
w1 += w2
print w1.x, w1.y

Z kolei operatory jednoargumentowe (-, +, ~ i abs()) można przedefiniować za pomocą metod __neg__(), __pos__(), __invert__() i __abs__() - nie przyjmują żadnych argumentów poza self:

class Wektor(object):
    def __init__(self, x, y):
        super(Wektro, self).__init__()
        self.x = x
        self.y = y
    def __abs__(self):
        return (self.x ** 2 + self.y ** 2) ** 0.5

w = Wektor(1, 3)
print abs(w)

Aby nasza klasa obsługiwała indeksowanie należy zdefiniować metody __getitem__(self, key), __setitem__(self, key, value) i __del__(self, key). W założeniu key jest kluczem pod którym przechowywana jest wartość, którą chcemy pobrać, ustawić czy usunąć. Jeśli key jest niepoprawnego typu to powinien zostać zgłoszony wyjątek TypeError, jeśli kluczowi key nie odpowiada, żadna wartość w naszej klasie, to powinien zostać zgłoszony wyjątek IndexError (jeśli nasza klasa jest sekwencją) lub KeyError (jeśli jest odwzorowaniem). Niech przykładem będzie klasa generująca elementy ciągu arytmetycznego. W przykładzie zaimplementowano klasę reprezentującą ciąg arytmetyczny, jeśli użytkownik ustawi jakąś wartość to dopóki jej nie usunie będzie zwracana ustawiona przez niego wartość zamiast wynikającej z definicji ciągu arytmetycznego:

class CiagArytmetyczny(object):
    def __init__(self, a0, r):
        super(CiagArytmetyczny, self).__init__()
        self.a0 = a0
        self.r = r
        self.zmienione = {}
    def sprawdzKlucz(self, key):
        if type(key) != type(1):
            raise TypeError
        if key < 0:
            raise IndexError
    def __getitem__(self, key):
        self.sprawdzKlucz(key)
        if key in self.zmienione:
            return self.zmienione[key]
        return self.a0 + (key - 1) * self.r
    def __setitem__(self, key, value):
        self.sprawdzKlucz(key)
        self.zmienione[key] = value
    def __delitem__(self, key):
        self.sprawdzKlucz(key)
        if key in self.zmienione:
            del self.zmienione[key]

c = CiagArytmetyczny(0, 5)
print c[2]
c[2] = -11
print c[2]
del c[2]
print c[2]

Metoda __str__(self) jest wywoływana przez funkcję wbudowaną str, a __repr__(self) przez funkcję wbudowaną repr, pozwala to na kontrolowanie sposobu wypisywania obiektów naszej klasy:

class CiagArytmetyczny(object):
    def __init__(self, a0, r):
        super(CiagArytmetyczny, self).__init__()
        self.a0 = a0
        self.r = r
    def __str__(self):
        return "Ciąg arytmetyczny o wyrazie począrkowym " + str(self.a0) + " i różnicy " + str(self.r)

c = CiagArytmetyczny(0, 5)
print str(c)

Można też zdefiniować operatory porządków: <, <=, ==, !=, > i >= przy pomocy metod __lt__(self, other), __le__(self, other), __eq__(self, other), __ne__(self, other), __gt__(self, other), __ge__(self, other), przykład:

class Ulamek(object):
    def __init__(self, licznik, mianownik):
        super(Ulamek, self).__init__()
        self.licznik = licznik
        self.mianownik = mianownik
    def __eq__(self, inny):
        return inny.mianownik * self.licznik == inny.licznik * self.mianownik

u1 = Ulamek(1, 2)
u2 = Ulamek(5, 10)
if u1 == u2:
    print 'równe'
else:
    print 'różne'

Z kolei definiując metodę __call__(self, *args) sprawiamy, że obiekty naszej klasy można wywoływać tak jak funkcje:

class CiagArytmetyczny(object):
    def __init__(self, a0, r):
        super(CiagArytmetyczny, self).__init__()
        self.a0 = a0
        self.r = r
    def __call__(self, n):
        return self.a0 + self.r * (n - 1)

c = CiagArytmetyczny(0, 5)
print c(5)

Definiując metodę __iter__(self) zwracającą obiekt, który będzie miał metodę next(self) zwracającą kolejne elementy tworzonej przez nas sekwencji i zgłaszającą wyjątek StopIteration gdy dojdziemy do końca sekwencji. W przykładzie obiekt sam jest swoim iteratorem - klasa CiagFibonacciego jest sekwencją po n pierwszych wyrazach ciągu Fibonacciego gdzie n jest zadawane w konstruktorze klasy:

class CiagFibonacciego(object):
    def __init__(self, n):
        super(CiagFibonacciego, self).__init__()
        self.n = n
    def __iter__(self):
        self.a = 0
        self.b = 1
        self.nn = self.n
        return self
    def next(self):
        if self.nn == 0:
            raise StopIteration
        self.nn -= 1
        tmp = self.a
        self.a, self.b = self.b, self.a + self.b
        return tmp

fib = CiagFibonacciego(10)
for f in fib:
    print f

Przydatne może być też zdefiniowanie metody __nonzero__(self), będzie ona wywoływana w przypadku wywołania funkcji bool lub przy testach logiczncyh:

class Wektor(object):
    def __init__(self, x, y):
        super(Wektor, self).__init__()
        self.x = x
        self.y = y
    def __nonzero__(self):
        return self.x != 0 or self.y != 0

w = Wektor(1, 1)
if w:
    print 'niezerowy'
else:
    print 'zerowy'

Gdy nie ma zdefiniowanej funkcji __nonzero__(self) do badania wartości logicznej może być wykorzystana funkcja __len__(str) zwracająca długość sekwencji, przykład:

class Wektor(object):
    def __init__(self, x, y):
        super(Wektor, self).__init__()
        self.x = x
        self.y = y
    def __len__(self):
        return (self.x ** 2 + self.y ** 2) ** 0.5

w = Wektor(1, 1)
if w:
    print 'niezerowy'
else:
    print 'zerowy'

Przydatne mogą okazać się też metody pozwalające na implementację rzutowania wywoływane przez funkcje wbudowane float, hex, int, long i oct: __float__(self), __hex__(self), __int__(self) i __oct__(self)

Zdefiniowanie operatorów dla tworzonych przez nas klas pozwala wykorzystywać w pracy z nimi napisane wcześniej programy działające na przykład na liczbach (a także funkcje wbudowane Pythona takie jak sum):

class Wektor(object):
    def __init__(self, x, y):
        super(Wektor, self).__init__()
        self.x = x
        self.y = y
    def __add__(self, w):
        return Wektor(self.x + w.x, self.y + w.y)
    def __str__(self):
        return 'Wektor [' + str(self.x) + ', ' + str(self.y) + ']' 

A = [Wektor(1., 1.), Wektor(0., 7.), Wektor(-11., 12)]
print sum(A, Wektor(0., 0.))