Klasy i ich obiekty¶
Przykładowa klasa¶
Klasa to przepis na kawałek kodu zrobiony z funkcji i zmiennych. Oto przykład w języku C++:
1class Vec3 {
2public:
3 float x = 0, y = 0, z = 0;
4
5 Vec3(const Vec3 & v) { x = v.x; y = v.y; z = v.z; }
6
7 double length() { return sqrt(x*x + y*y + z*z); }
8
9 double distance_to(const Vec3 & v) {
10 return sqrt((x - v.x)*(x - v.x) + (y - v.y)*(y - v.y) + (z - v.z)*(z - v.z));
11 }
12}
… a to w języku Python:
1class Vec3:
2
3 def __init__(self,x = 0, y = 0, z = 0) :
4 self.__x = x
5 self.__y = y
6 self.__z = z
7
8 def length(self) :
9 return math.sqrt(self.__x*self.__x + self.__y*self.__y + self.__z*self.__z)
10
11 def distance_to(self, v) :
12 return math.sqrt( (self.__x - v.x)*(self.__x - v.x) +
13 (self.__y - v.y)*(self.__y - v.y) + (self.__z - v.z)*(self.__z - v.z) )
Zmienne, będące składowymi klasy nazywamy polami lub własnościami (ang. properties); funkcje zaś metodami. W powyższym przykładzie mamy pola
x, y i z oraz np metodę distance_to().
Przeciążenie¶
Z metodami jest jak z konstruktorem: w C++ i w JAVA jedna klasa może mieć kilka o tej samej nazwie, nazywane je przeciążonymi (overloaded). Metody te muszą różnić
się listą argumentów. W Pythonie przeciążenie udajemy, stosując *args. Pól nie można przeciążać.
Konstruktor klasy¶
Klasa może mieć wiele różnych pól i metod; szczególną rolę ma metoda zwana konstruktorem. Konstruktor to funkcja wywoływana dokładnie raz: w momencie tworzenia obiektu. Może robić przeróżne rzeczy, ale najczęściej służy do inicjowania zmiennych. W Pythonie (linie 3-6 powyżej) ma szczególne znaczenie: nie tylko inicjuje zmienne, ale je tworzy. Konstruktor nie może zwracać wartości.
Obiekty danej klasy tworzymy, używając nazwy klasy:
1v1 = Vec3() # Argumenty domyślne!
2v2 = Vec3(1.2, 0.9, 67.8)
W C++, podobnie jak w JAVA, konstruktor musi nazywać się tak, jak nazwa klasy i może być ich kilka.
Zauważ, że nie ma zadeklarowanego typu wartości zwracanej. W Pythonie można stworzyć tylko jedną funkcję o danej nazwie, w szczególności tylko jedna może nazywać się __init__(), dlatego konstruktor w klasie może być tylko jeden. Aby udostępnić tworzenie obiektów z różnych danych wejściowych, trzeba posłużyć się zmienną listą argumentów
1class Vec3 :
2
3 def __init__(self,*args) :
4
5 if len(args) == 3 : # support for Vec3(1.2,3.8,0.1)
6 self.x, self.y, self.z = args[0], args[1], args[2]
7 elif len(args) == 1 :
8 if isinstance(args[0], Vec3) : # support for Vec3(v)
9 self.x, self.y, self.z = args[0].x, args[0].y, args[0].z
10 elif isinstance(args[0], list) : # support for Vec3( [1.2,3.8,0.1] )
11 self.x, self.y, self.z = args[0][0], args[0][1], args[0][2]
12 else : # support for Vec3(0)
13 self.x, self.y, self.z = args[0], args[0], args[0]
Konstruktory poniżej nazywamy konstruktorem domyślnym oraz konstruktorem kopiującym. Ten ostatni służy do tworzenia głębokiej kopii istniejącego obiektu:
1v1 = Vec3() # Argumenty domyślne - konstruktor domyślny
2v2 = Vec3(v1) # konstruktor kopiujący
Pola i metody¶
Do elementów składowych klasy, tj. pól i metod, odwołujemy się “po kropce”. Dla przykładu, poszukajmy wersora wskazującego na \(v_{1} + v_{2}\), gdzie \(v_{1} = (1.2, 0.9, 6.8)\) oraz \(v_{1} = (-4.2, 1.2, -2.8)\):
v1 = Vec3(1.2, 0.9, 6.8) v2 = Vec3(-4.2, 1.2, -2.8) v = Vec3() v.x = v1.x + v2.x v.y = v1.y + v2.y v.z = v1.z + v2.z d = v.length() v.x /= d v.y /= d v.z /= d
Note
Pisząc klasę warto z góry przewidzieć, jakie metody będą potrzebne jej użytkownikowi. W przypadku klady Vec3 przydały by się add(), sub(),
mul(), norm(), dot_product() i vec_product(). Mając je, można by np. na podstawie \(v_{1}\) i \(v_{2}\) łatwo zbudować ortonormalny układ wektorów:
v1n = Vec3(v1)
v1n.norm()
v2n = Vec3(v2)
v2n.norm()
vx = Vec3(v1n)
vx.add(v2n)
vy = Vec3(v1n)
vy.sub(v2n)
vx.norm()
vy.norm()
vz = vx.vec_product(vy)
Todo
Napisz klasę Vec3, tak aby powyższe przykłady działały poprawnie
Metody set() i get()¶
Bezpośredni zapis i odczyt pól klasy jest bardzo wygodny; korzystamy z nich jak ze zwykłej zmiennej. Rozwiązanie takie jest mało elastyczne. Zazwyczaj stosuje się do tego celu odpowiednie funkcje, zwane odpowiednio setter i getter
def set_x(self,x) : self.x = x
def get_x(self) : return self.x
Stosowanie funkcji set() i get() nie jest obowiązkowe w programowaniu obiektowym. To jedynie pewna często przyjmowana konwencja.
Kontrola dostępu do składowych klasy¶
Niezmiernie ważnym elementem programów obiektowych jest kontrola dostępu do pól i metod. Generalnie moglibyśmy wyobrazić sobie następujące możliwości kontroli:
dla obiektu samego sobie
dla innych obiektów tej samej klasy
dla obiektów potomnych
dla wszystkich innych
- A jak jest naprawdę?
dany obiekt ma zawsze pełen dostęp do wszystkich swoich składowych.
Dostęp ustalany jest dla klasy a nie dla obiektu, co oznacza, że dwa obiekty tej samej klasy mogą nawzajem odczytywać wszystkie swoje dane!
obiekty potomne widzą publiczne składowe, składowe chronione, składowe prywatne nie są dostępne
wszystkie pozostałe elementy programu widzą jedynie składowe prywatne
Język Python nie ma niestety ścisłej kontroli dostępu. Przyjmuje się jednak, że metoda lub pole, którego nazwa zaczyna się od podwójnego podkreślenia (‘__’), jest prywatne (prywatną składową klasy). Składowe (czyli pola i metody) chronione zaczynają się od pojedynczego podkreślenia (‘__’) Różnicę w działaniu pól prywatnych i publicznych w Pythonie ilustruje poniższy przykład. Pola chronione zaprezentowane zostaną po wyjaśnieniu dziedziczenia klas w kolejnym rozdziale.
1class TajnePoleW :
2
3 def __init__(self) :
4 self.v = 5 # składowa publiczna
5 self.__w = 7 # składowa prywatna
6
7 def print_w(self, other_c) :
8 print(other_c.__w)
9
10# Metoda nadpisuje składową v
11 def set_v(self, v) : self.v = v
12
13# Metoda nadpisuje składową w
14 def set_w(self, w) : self.__w = w
15
16if __name__ == "__main__" :
17 c = TajnePoleW()
18 print(c.v) # To działa, gdyż pole v jest publiczne
19# print(c.__w) # TO NIE ZADZIAŁA, pole __w jest prywatne
20
21# obiekt d jest tej samej klasy co c, może odczytać jego wartość __w
22 d = TajnePoleW()
23 d.print_w(c)
W powyższym przykładzie tylko obiekt klasy TajnePoleW (na przykład d) może odczytać pole c.__w, nikt inny.
Faktycznie, próba “nieuprawnionego” odczytania składowej prywatnej zakończy się wyrzuceniem wyjątku.
Ograniczenie to nie jest jednak sztywne i można je obejść:
1class Vec2:
2
3 def __init__(self, x=0,y=0):
4 self.__x = x
5 self.__y = y
6
7if __name__ == "__main__" :
8 v = Vec2(1,2)
9 print(v._Vec2__y)
Teraz zatem możemy dopisać kolejny kawałek klasy Vec3:
1class Vec3 :
2
3 def __init__(self,*args) :
4
5 if len(args) == 3 : # support for Vec3(1.2,3.8,0.1)
6 self.__x, self.__y, self.__z = args[0], args[1], args[2]
7 elif len(args) == 1 :
8 if isinstance(args[0], Vec3) : # support for Vec3(v)
9 self.__x, self.__y, self.__z = args[0].__x, args[0].__y, args[0].__z
10 elif isinstance(args[0], list) : # support for Vec3( [1.2,3.8,0.1] )
11 self.__x, self.__y, self.__z = args[0][0], args[0][1], args[0][2]
12 else : # support for Vec3(0)
13 self.__x, self.__y, self.__z = args[0], args[0], args[0]
14
15 def get_x(self) : return self.__x
16
17 def set_x(self,x) : self.__x = x
18
19 def get_y(self) : return self.__y
20
21 def set_y(self,y) : self.__y = y
22
23 def get_z(self) : return self.__z
24
25 def set_z(self,z) : self.__z = z