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:

\[\begin{split}\begin{align} v_x & = & | |v_1| + |v_2| |\\ v_y & = & | |v_1| - |v_2| |\\ v_z & = & v_x \times v_y \end{align}\end{split}\]
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