Dziedziczenie klas¶
Pojęcie dziedziczenia klas jest fundamentalne dla programowania obiektowego. Bez dziedziczenia nie wyszlibyśmy poza etap zaawansowanych struktur. Dziedziczenie umożliwia dodawanie nowej funkcjonalności do klas już napisanych. Nowe klasy mają to, co miała klasa bazowa plus coś jeszcze. Na przykład atom jest nie tylko wektorem w 3D, ma też swoją nazwę. Indeks w zasadzie nie jest konieczny dla atomu, ale programy do modelowania w zasadzie nie mogą się bez tego obyć:
1class Atom(Vec3):
2
3 def __init__(self, id, name, x = 0, y = 0, z = 0) :
4 super.__init__(x,y,z)
5 self.__id = id
6 self.__name = name
Dla porównania analogiczny kod w C++:
1class Atom : public Vec3 {
2public:
3
4 Atom(const int id, const std::string & name, float x, float y, float z ) :
5 Vec3(x,y,z) { id_=id; name_=name; }
6
7 Atom(const int id, const std::string & name) :
8 Vec3(0,0,0) , name_(name), id(id) {}
9}
Klasę Vec3 nazwiemy klasą bazową albo nadklasą; klasę Atom zaś klasą potomną albo podklasą. Klasa Atom dziedziczy po klasie Vec3
wszystkie jej publiczne metody i pola. Dzięki temu Atom ma współrzędne oraz umie policzyć długość swojego wektora (odziedziczył length()).
W linii 4 kodu w Pythonie następuje wywołanie konstruktora klasy bazowej. W tym momencie klasa Vec3, a zatem i klasa Atom, uzyskuje pola __x,``__y`` oraz __z.
Instrukcja ta jest poprawna dla Pythona w wersji 3.x. Skrypty napisane dla Pythona 2.x powinny wywoływać
konstruktor klasy bazowej jak poniżej:
1class Atom(Vec3):
2
3 def __init__(self, id, name, x = 0, y = 0, z = 0) :
4 super(Vec3, self).__init__()
5 self.__id = id
6 self.__name = name
- Zwiększanie funkcjonalności klasy bazowej
W powyższym przypadku klasa bazowa
Vec3dostała nowe ficzery, co zrobiło z niejAtom. Każdy atom jest wektorem, ale nie każdy wektor to atom.
Pełna wersja klasy Atom znajduje się poniżej:
(rozwiń kod klasy Atom)
1from Vec3 import Vec3
2
3
4class Atom(Vec3):
5
6 def __init__(self, atom_no, name, *args):
7 """Represents a chemical atom"""
8 super(Atom, self).__init__(*args)
9 self.__id = atom_no
10 self.__name = name
11
12 def to_pdb(self):
13 return "ATOM %4d %4s ARG A 3 %8.3f%8.3f%8.3f 0.50 35.88 N" % \
14 (self.__id, self.__name, self.x, self.y, self.z)
15
16 @property
17 def name(self):
18 return self.__name
19
20 @property
21 def element(self):
22 return self.__element
23
24
25def atoms_from_pdb(pdb_fname):
26 atoms = []
27 for atom_line in open(pdb_fname):
28 atom_number = int(atom_line[6:11].strip())
29 x_position = float(atom_line[30:38].strip())
30 y_position = float(atom_line[38:47].strip())
31 z_position = float(atom_line[47:54].strip())
32 atom_name = atom_line[12:16].strip()
33 atoms.append(Atom(atom_number, atom_name, x_position, y_position, z_position))
34 return atoms
35
36
37class ChargedAtom(Atom):
38 """represents an atom with a charge (might be fractional)"""
39 def __init__(self, atom_no, name, q):
40 super(ChargedAtom, self).__init__(atom_no, name)
41 self.__q = q
42
43
44if __name__ == "__main__" :
45 v = Vec3(1.3, 2.4, 3.4)
46 a = Atom(1, " CA ")
47 qa = ChargedAtom(1, " NZ ", 1.0)
48
49 # ---------- Atoms behave like vectors: you can add them, or add a vector to an atom
50 print(a.to_pdb())
51 a += v
52 print(a.to_pdb())
53
54 v_cm = Atom(1, "C")
55 atoms = atoms_from_pdb("surpass.pdb")
56 for atom in atoms: v_cm += atom
57 v_cm *= 1.0 / len(atoms)
58 print("center of mass:", v_cm)
Prosta symulacja - oscylator harmoniczny¶
Dla przykładu napiszmy prosty skrypt, który modeluje oscylator harmoniczny w jednym wymiarze: Na ciało o masie m działa siła \(-k x\), na wskutek której oscyluje ono wokół położenia x0. Proces ten opisany jest równaniem ruchu:
Równanie to rozwiązuje numerycznie (całkuje) poniższy skrypt:
1mass = 1.0
2x0 = 10.0
3k = 5.0
4dt = 0.001
5x = 3.0
6v = 0.0
7t = 0
8for i in range(100) :
9 for i in range(100):
10 a = -k * (x-x0) / (mass*2.0)
11 v = v + a*dt
12 x = x + v*dt
13 t += dt
14 print(t,x)
Wersja obiektowa będzie dużo dłuższa, ale za to łatwiejsza w rozbudowie. Zastanówmy się najpierw, jakie elementy powinniśmy reprezentować w programie?
- system¶
reprezentuje tu modelowany obiekt - oscylator harmoniczny (klasa
Harmonic1DSystem)- solver¶
rozwiązuje równanie ruchu; tu akurat jest to klasa
EulerIntegratorcałkująca wg schematu Eulera- observer¶
odpowiada za wydruk wyników na ekran
1class Harmonic1DSystem:
2
3 def __init__(self):
4 self.x = 0
5 self.v = 0
6 self.k = 5.5
7 self.x0 = 10
8 self.mass = 1
9
10 @property
11 def a(self):
12 return -self.k * (self.x - self.x0) / (self.mass * 2.0)
13
14
15class EulerIntegrator:
16
17 def __init__(self, my_system):
18 self.system = my_system
19 self.observers = []
20 self.dt = 0.001
21
22 def add_observer(self, o):
23 self.observers.append(o)
24
25 def run(self, n_inner_steps, n_outer_steps):
26
27 for j in range(n_outer_steps):
28 for i in range(n_inner_steps):
29 self.system.v += self.system.a * self.dt
30 self.system.x += self.system.v * self.dt
31 for o in self.observers:
32 o.observe()
33
34class ObserveToScreen (Observer):
35
36 def __init__(self, object):
37 self.o = object
38
39 def observe(self):
40 print(self.o.x)
41
42
43if __name__ == "__main__":
44
45 system = Harmonic1DSystem()
46 ene = ObserveToScreen(system)
47 s = EulerIntegrator(system)
48 s.add_observer(ene)
49 s.run(10, 10)
50 print(mmx.min_x, mmx.max_x)
51
52 for obs in s.observers: obs.finalize()
53
Zaletą obiektowości jest to, że można łatwo wymieniać poszczególne komponenty programu na inne, zmieniając w ten sposób
jego zachowanie. Na przykład rolą ObserveToScreen jest drukowanie aktualnego położenia oscylatora
na ekranie. Łatwo można zmienić wyniki produkowane przez symulację, zmieniając observer na inny, np taki,
który obserwuje tylko zakres położenia (jego minimalną i maksymalną wartość) albo obserwer, który nagrywa wyniki do pliku.
1class Observer:
2
3 def observe(self):
4 pass
5
6 def finalize(self):
7 pass
8
9
10class ObserveToScreen (Observer):
11
12 def __init__(self, object):
13 self.o = object
14
15 def observe(self):
16 print(self.o.x)
17
18
19class ObserveToFile (Observer):
20
21 def __init__(self,object, fname):
22 self.o = object
23 self.__file = open(fname,"w")
24
25 def observe(self):
26 print(self.o.x, file=self.__file)
27
28 def finalize(self): self.__file.close()
29
30
31class MinMaxObserver (Observer):
32
33 def __init__(self, object):
34
35 self.o = object
36 self.min_x = self.o.x
37 self.max_x = self.o.x
38
39 def observe(self):
40 if self.o.x > self.max_x : self.max_x = self.o.x
41 if self.o.x < self.min_x : self.min_x = self.o.x
42
Wszystkie obserwery dziedziczą po klasie bazowej Observer, która definiuje ich interfejs,
czyli publiczne metody które one udostępniają. Są to metody observe(), która dokonuje obserwacji, oraz finalize(),
która kończy obserwacje. Ta ostatnia może zamykać plik, rysować wykres, itp. Wszystkie klasy muszą mieć ten sam interfejs,
a więc obie w/w metody. Nie każdy obserwer potrzebuje metody finalize(), dlatego klasa bazowa dostarcza domyślną implementację,
która po prostu nic nie robi (instrukcja pass). Tylko dzięki temu poniższa linia programu działa poprawnie:
1 for obs in s.observers: obs.finalize()
Gdyby choć jeden z obiektów na liście nie miał metody finalize(), program zrzuciłby wyjątek.