TI/Programowanie dla Fizyków Medycznych:Klasy: Różnice pomiędzy wersjami

Z Brain-wiki
Linia 1: Linia 1:
 
==Klasy==
 
==Klasy==
 +
DDD
 
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.  
 
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:
 
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:

Wersja z 17:13, 8 cze 2015

Klasy

DDD 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.))

Zadanie 1

Napisz klasę reprezentującą drzewa binarne, niech ma następujące metody:

  • konstruktor jednoargumentowy tworzący liść o zadanej wartości
  • konstruktor umożliwiający stworzenie węzła o zadanej wartości i prawym lub lewym potomku
  • wysokosc() - zwracającą wysokość drzewa
  • szerokosc() - zwracającą szerokość drzewa
  • liczbaWezlow() - zwracającą liczbę węzłów
  • liczbaLisci() - zwracająca liczbę liści w drzewie
  • wypisz(porzadek) - wypisuje drzewo w kolejności zadanej przez napis początek, możliwe wartości: 'preLP', 'postLP', 'infLP', 'prePL', 'postPL' i 'infPL', gdy nie podano parametru porzadek niech będzie przyjmowany 'preLP', w przypadku podania innego napisu lub wartości nie będącej napisem niech będzie zgłaszany wyjątek
  • czyIzomorficzne(drzewo) - zwracająca True gdy drzewo jest izomorficzne z self
  • doListy() - zwracająca listę zawierającą wartości poszczególnych węzłów - indeksowanie na drzewie: lewy syn i-tego węzła ma indeks 2 * i, a prawy 2 * i + 1, korzeń ma indeks 1, przy takim indeksowaniu wartości drzewa można wygodnie trzymać na liście (na pozycji indeks - 1), niech pozycje którym nie odpowiadają żadne węzły mają wartość None, a zwracana lista niech nie ma zbędnych None na końcu
  • zListy(lista) - klasowa metoda generująca drzewo na podstawie listy takiej jak opisana w poprzednim punkcie
  • zdefiniować operatory logiczne or i and zwracające drzewo o wszystkich wartościach równych 0 i mające kształt będący odpowiednio sumą i częścią wspólną drzew będących argumentami
  • zdefiniować operator + zwracający drzewo mające kształt będący sumą drzew będących argumentami, a w każdym węźle niech będzie suma wartości z odpowiednich węzłów z danych drzew (jeśli w jednym drzewie nie ma odpowiedniego węzła to kopiujemy wartość z odpowiedniego węzła drugiego drzewa)

Zadanie 2

Napisać klasę reprezentującą drzewo wyszukiwań binarnych, niech ma następujące metody:

  • czyJestWartosc(wartosc) - zwraca True gdy wartosc jest w drzewie False w przeciwnym wypadku
  • wstaw(wartosc) - wstawia wartosc do drzewa, jeśli wartosc jest już w drzewie to nie robi nic
  • usun(wartosc) - usuwa wartość z drzewa, jeśli jej nie było to zwraca wyjatek
  • czyPuse() - zwraca True gdy drzewo jest puste, False w przeciwnym wypadku

Zadanie 3

Napisz klasę reprezentującą wielomian, niech ma następujące metody:

  • pochodna() - zwracająca wielomian reprezentujący pochodną danego
  • calka() - zwracającą wielomian reprezentujący całkę z danego
  • wartosc(x) - zwraca wartość wielomianu w punkcie x
  • calka_oznaczona(x1, x2) - zwraca wartość całki oznaczonej od x1 do x2
  • zdefiniować operatory +, -, * i wypisywanie

Napisać klasę reprezentującą wyrażenia wymierne, niech ma następujące metody:

  • wartosc(x) - zwraca wartość w punkcie x

Zadanie 4

Napisać klasę reprezentującą ciąg arytmetyczny, niech ma następujące metody:

  • konstruktor bezparametrowy przyjmujący a_1 = 1 i r = 1
  • konstruktory jedno i dwuargumentowe
  • pobieranie a_n przez ciag[n], niech zwraca wyjątek gdy n <= 0
  • ustawianie a_n przez ciag[n] = wartosc, jeśli ustawimy ręcznie wartość to przy kolejnym pobraniu a_n ta wartość ma być zwrócona
  • usuwanie ustawionej ręcznie wartości przez del ciag[n]
  • przedefiniować operatory + i - tak aby zwracały ciąg którego elementy są sumą i różnicą elementów danych ciagów

Zadanie 5

Napisać klasę po której będzie można się iterować i będzie ona sekwencją po ciągu liczb Fibonacciego, niech sekwencja przebiega po tylu liczbach jaką wartość podano w konstruktorze

Zadanie 6

Napisz program, który wczyta z plików listę miast, połączeń między nimi i odległości tych połączeń a następnie pozwoli wyszukiwać połączeń między zadanymi punktami o długości mniejszej niż zadana.


"Programowanie dla Fizyków Medycznych"