Klasy

Stronę tą wyświetlono już: 340 razy

Najwyższa pora porozmawiać o tym, jak to w Pythonie deklaruje się klasy. Będzie to bardzo dziwaczna opowieść, albowiem sam Python jest dziwacznym językiem programowania. Mimo wszystko słowo kluczowe class na szczęście nawet w Pythonie jest stosowane do deklarowania własnych szablonów klas.

Metody magiczne klas

Magiczne metody są ściśle powiązane z funkcjami lub operatorami. Wszystkie nazwy magicznych metod klas zaczynają się i kończą podwójną dolną spacją (__).

Konstruktor i destruktor klas w Pythonie

Omówię pierwsze Pythonowe dziwactwa, czyli konstruktor i destruktor klasy:

Listing 1
  1. class Point2D:
  2. def __init__(self, x = None, y = None):
  3. self.x = float(input("Podaj współrzędną x: ")) if (x is None) else x
  4. self.y = float(input("Podaj współrzędną y: ")) if (y is None) else y
  5. def __del__(self):
  6. print("destruktor uruchomiony")

__init__ - konstruktor klasy. Konstruktora, jak i innych metod nie można przeciążać. Każda metoda klasy (w tym również konstruktor jak i destruktor muszą w swej deklaracji przyjmować jako pierwszy argument o nazwie self, który jest etykietą obiektu tej klasy. Co jeszcze bardziej dziwaczne jest, to to, że w konstruktorze deklaruje się pola klasy w sposób następujący: self.nazwa_pola_klasy.

__del__ - destruktor, uruchamiany gdy zostanie wykonane polecenie del na obiekcie klasy.

Oto przykład działania konstruktora i destruktora:

Listing 2
  1. p = Point2D() # tutaj tworzę pierwszy obiekt, a ponieważ nie podałem żadnych danych na wejście to program zapyta się o współrzędne x i y punktu
  2. p2 = Point2D(20, 30) # a tutaj tworzę obiekt, o podanych współrzędnych
  3. del p # tutaj usuwam obiekt, wywołując destruktor
  4. del p2 # to samo co powyżej dla drugiego obiektu

Wynik działania powinien być następujący:

Podaj współrzędną x: 10
Podaj współrzędną y: 20
destruktor uruchomiony
destruktor uruchomiony

Rzutowanie

Oto przykład magicznej metody związanej z rzutowaniem obiektu na typ int za pomocą metody magicznej __int__:

Listing 3
  1. def __int__(self):
  2. return int((self.x * self.x + self.y * self.y) ** 0.5)

Powyższa metoda klasy Point2D oblicza i zwraca całkowitą wartość długości wektora. Oto przykład użycia tej metody:

Listing 4
  1. p = Point2D(10,0)
  2. print(int(p))

Wynik działania powyższego kodu:

10

Istnieje też możliwość rzutowania na typ float za pomocą metody magicznej __float__:

Listing 5
  1. def __float__(self):
  2. return (self.x * self.x + self.y * self.y) ** 0.5

Powyższa metoda klasy Point2D oblicza i zwraca zmiennoprzecinkową wartość długości wektora. Oto przykład użycia tej metody:

Listing 6
  1. p = Point2D(10,0)
  2. print(float(p))

Wynik działania powyższego kodu:

10.0

Teraz rzutowanie na str poprzez obsługę magicznej metody __str__:

Listing 7
  1. def __str__(self):
  2. return "Point2D(x={s.x}, y={s.y})".format(s = self)

Powyższa metoda klasy Point2D zwraca zmienną tekstową zawierającą współrzędne tego punktu. Oto przykład użycia tej metody:

Listing 8
  1. p = Point2D(10,0)
  2. print(p)

Wynik działania powyższego kodu:

Point2D(x=10, y=0)

Metoda magiczna __len__

Tę metodę już znamy z list, krotek, słowników i zbiorów. Teraz poznamy jak się ją deklaruje w własnej klasie:

Listing 9
  1. def __len__(self):
  2. return int(self)

Oto wywołanie tej metody za pomocą funkcji len:

Listing 10
  1. p = Point2D(10,0)
  2. print(len(p))

Wynik działania powyższego kodu:

10

Operatory porównania wartości

Obsługa operatorów klas w Pythonie również została zrzucona na barki metod magicznych. W tej części metody związane z obsługą operatorów porównania wartości, czyli:

  • __cmp__ - najbardziej kompleksowy operator, obsługuje on wszystkie operatory porównania wartości czyli: <; <=; ==; !=;; >=; >;
  • __eq__ - obsługuje operator równości ==;
  • __ne__ - obsługuje operator nierówności !=;
  • __lt__ - obsługuje operator mniejsze od <;
  • __gt__ - obsługuje operator większe od >;
  • __le__ - obsługuje operator mniejsze równe <=;
  • __ge__ - obsługuje operator większe równe >=.

Oto przykład implementacji metod magicznych dla operatorów porównania wartości związanych z klasą Point2D:

Listing 11
  1. def __eq__(self, other):
  2. if isinstance(other, Point2D):
  3. return True if other.x == self.x and other.y == self.y else False
  4. else:
  5. return True if (float(self) == other) else False
  6. def __ne__(self, other):
  7. return not (self == other)
  8. def __lt__(self, other):
  9. return float(self) < float(other)
  10. def __gt__(self, other):
  11. return float(self) > float(other)
  12. def __le__(self, other):
  13. return not (self > other)
  14. def __gt__(self, other):
  15. return not (self < other)

Operatory arytmetyczne

Oto lista operatorów magicznych metod, które odpowiedzialne są za obsługę operatorów arytmetycznych:

  • __add__ - dodawanie +;
  • __sub__ - odejmowanie -;
  • __mul__ - mnożenie *;
  • __floordiv__ - dzielenie całkowite //;
  • __div__ - dzielenie zmiennoprzecinkowe /;
  • __mod__ - reszta z dzielenia %;
  • __divmod__ - zwraca część całkowitą i resztę z dzielenia jako krotkę, pod tę metodą podpina się funkcja divmod;
  • __pow__ - operator potęgowania **;
  • __lshift__ - operator przesunięcia bitowego w lewo <<;
  • __rshift__ - operator przesunięcia bitowego w prawo >>;
  • __and__ - operator and;
  • __or__ - operator or;
  • __xor__ - operator xor

W klasie Point2D zastosowanie znajdą tylko niektóre z powyższych operatorów, których przykład implementacji poniżej zamieszczam:

Listing 12
  1. def __add__(self, other):
  2. if isinstance(other, Point2D):
  3. return Point2D(self.x + other.x, self.y + other.y)
  4. else:
  5. return Point2D(self.x + float(other), self.y + float(other))
  6. def __sub__(self, other):
  7. if isinstance(other, Point2D):
  8. return Point2D(self.x - other.x, self.y - other.y)
  9. else:
  10. return Point2D(self.x - float(other), self.y - float(other))
  11. def __mul__(self, other):
  12. if isinstance(other, Point2D):
  13. return self.x * other.x + self.y * other.y
  14. else:
  15. return Point2D(self.x * float(other), self.y * float(other))

Odwrócone operatory arytmetyczne

Operatory arytmetyczne wcześniej opisane obsługują tylko przypadek następującej postaci:

Listing 13
  1. p = Point2D(10,20)
  2. p = p * 10

Nie obsługują natomiast takiego przypadku:

Listing 14
  1. p = Point2D(10,20)
  2. p = 10 * p

ale, nie ma co płakać, albowiem Pythoniści znaleźli rozwiązanie tego problemu, którym są następujące metody magiczne:

  • __radd__ - dodawanie +;
  • __rsub__ - odejmowanie -;
  • __rmul__ - mnożenie *;
  • __rfloordiv__ - dzielenie całkowite //;
  • __rdiv__ - dzielenie zmiennoprzecinkowe /;
  • __rmod__ - reszta z dzielenia %;
  • __rpow__ - operator potęgowania **;
  • __rlshift__ - operator przesunięcia bitowego w lewo <<;
  • __rrshift__ - operator przesunięcia bitowego w prawo >>;
  • __rand__ - operator and;
  • __ror__ - operator or;
  • __rxor__ - operator xor

W klasie Point2D zastosowanie znajdą tylko niektóre z powyższych operatorów, których przykład implementacji poniżej zamieszczam:

Listing 15
  1. def __radd__(self, other):
  2. if isinstance(other, Point2D):
  3. return Point2D(self.x + other.x, self.y + other.y)
  4. else:
  5. return Point2D(self.x + float(other), self.y + float(other))
  6. def __rsub__(self, other):
  7. if isinstance(other, Point2D):
  8. return Point2D(other.x - self.x, other.y - self.y)
  9. else:
  10. return Point2D(float(other) - self.x, float(other) - self.y)
  11. def __rmul__(self, other):
  12. if isinstance(other, Point2D):
  13. return self.x * other.x + self.y * other.y
  14. else:
  15. return Point2D(self.x * float(other), self.y * float(other))

Operatory arytmetyczne z podstawieniem

Przydałoby się jeszcze obsłużyć operatory arytmetyczne z podstawieniem, czyli dla przykładu takie:

Listing 16
  1. p = Point2D(10,20)
  2. p += p

I w tym przypadku Pythoniści znaleźli rozwiązanie tego problemu, którym są następujące metody magiczne:

  • __iadd__ - dodawanie z podstawieniem +=;
  • __isub__ - odejmowanie z podstawieniem -=;
  • __imul__ - mnożenie z podstawieniem *=;
  • __ifloordiv__ - dzielenie całkowite z podstawieniem //=;
  • __idiv__ - dzielenie zmiennoprzecinkowe z podstawieniem /=;
  • __imod__ - reszta z dzielenia z podstawieniem %=;
  • __ipow__ - operator potęgowania z podstawieniem **=;
  • __ilshift__ - operator przesunięcia bitowego w lewo z podstawieniem <<=;
  • __irshift__ - operator przesunięcia bitowego w prawo z podstawieniem >>=;
  • __iand__ - operator and z podstawieniem &=;
  • __ior__ - operator or z podstawieniem |=;
  • __ixor__ - operator xor z podstawieniem ^=

W klasie Point2D zastosowanie znajdą tylko niektóre z powyższych operatorów, których przykład implementacji poniżej zamieszczam:

Listing 17
  1. def __iadd__(self, other):
  2. if isinstance(other, Point2D):
  3. self.x += other.x
  4. self.y += other.y
  5. else:
  6. self.x += float(other)
  7. self.y += float(other)
  8. return self
  9. def __isub__(self, other):
  10. if isinstance(other, Point2D):
  11. self.x -= other.x
  12. self.y -= other.y
  13. else:
  14. self.x -= float(other)
  15. self.y -= float(other)
  16. return self
  17. def __imul__(self, other):
  18. self.x *= float(other)
  19. self.y *= float(other)
  20. return self

Operatory jednoargumentowe

Oto lista jednoargumentowych metod obsługujących operatory i niektóre funkcje matematyczne:

  • __pos__ - jednoargumentowy operator znaku +;
  • __neg__ - jednoargumentowy operator znaku -;
  • __abs__ - implementacja zachowania funkcji abs;
  • __invert__ - implementacja operatora inwersji ~;
  • __floor__ - implementacja zachowania funkcji math.floor;
  • __ceil__ - implementacja zachowania funkcji math.ceil;
  • __trunc__ - implementacja zachowania funkcji math.trunc;

Przykład implementacji co niektórych magicznych metod wyżej wymienionych dla klasy Point2D:

Listing 18
  1. def __pos__(self):
  2. return Point2D(self.x, self.y)
  3. def __neg__(self):
  4. return Point2D(- self.x, - self.y)
  5. def __abs__(self):
  6. return Point2D(abs(self.x), abs(self.y))

Atrybuty klasy

Kolejnym Pythonowym dziwactwem są atrybuty klasy, które są związane z nazwą klasy a nie z konkretnym obiektem tejże klasy. Zanim jednak pokażę jak tworzy się atrybuty klasy, to najpierw utworze klasę Matrix_tr, która będzie opisywała macierz transformacji:

Listing 19
  1. class Matrix_tr:
  2. def __init__(self, m11 = 1, m12 = 0, m21 = 0, m22 = 1, dx = 0, dy = 0, alpha = None):
  3. if alpha is None: # if alpha angle is set on None value
  4. self.m11 = m11
  5. self.m12 = m12
  6. self.m21 = m21
  7. self.m22 = m22
  8. else: # in other case calculate m11 - m22 using alpha angle
  9. self.m11 = mt.cos(alpha)
  10. self.m12 = -mt.sin(alpha)
  11. self.m21 = - self.m12 # this should be sin of angle alpha
  12. self.m22 = self.m11 # this should be cos of angle alpha
  13. self.dx = dx # this is an offset for x
  14. self.dy = dy # this is an offset for y
  15. def __mul__(self, other): # multiplication operator (*)
  16. if isinstance(other, Point2D): # for object of Point2D class
  17. return Point2D(other.x * self.m11 + other.y * self.m12 + self.dx, other.x * self.m21 + other.y * self.m22 + self.dy)
  18. if isinstance(other, Matrix_tr): # for object of Matrix_tr class
  19. return Matrix_tr(self.m11 * other.m11 + self.m12 * other.m21, self.m11 * other.m12 + self.m12 * other.m22, self.m21 * other.m11 + self.m22 * other.m21, self.m21 * other.m12 + self.m22 * other.m22, self.m11 * other.dx + self.m12 * other.dy +
  20. self.dx, self.m21 * other.dx + self.m22 * other.dy + self.dy)
  21. def __str__(self):
  22. return "| M11 = {m.m11:+{l}.5f} M12 = {m.m12:{l}.5f} dx = {m.dx:+{l}.5f} |\n| M21 = {m.m21:+{l}.5f} M22 = {m.m22:+{l}.5f} dy = {m.dy:+{l}.5f} |\n| M31 = {m31:+{l}.5f} M32 = {m32:+{l}.5f} {m33:+{l}.5f} |".format(m = self, l = 10, m31 = 0, m32 = 0, m33 = 1)

Teraz wykorzystam klasę Matrix_tr jako atrybut klasy Point2D:

Listing 20
  1. class Point2D:
  2. tr = Matrix_tr() # atrybut klasy Point2D

Dostęp atrybutów klasy można osiągnąć na dwa następujące sposoby:

Listing 21
  1. print(Point2D.tr)
  2. p = Point2D(100,200)
  3. print("Z poziomu obiektu klasy:\n",p.tr)

Wynik działania:

| M11 =   +1.00000 M12 =    0.00000 dx =   +0.00000 |
| M21 =   +0.00000 M22 =   +1.00000 dy =   +0.00000 |
| M31 =   +0.00000 M32 =   +0.00000        +1.00000 |
Z poziomu obiektu klasy
| M11 =   +1.00000 M12 =    0.00000 dx =   +0.00000 |
| M21 =   +0.00000 M22 =   +1.00000 dy =   +0.00000 |
| M31 =   +0.00000 M32 =   +0.00000        +1.00000 |

Co ciekawe, jeżeli zrobię coś takiego:

Listing 22
  1. import math as mt
  2. p = Point2D(100,200)
  3. p.tr = Matrix_tr(alpha = mt.pi / 3)
  4. print("Z poziomu definicji klasy:\n",Point2D.tr)
  5. print("Z poziomu obiektu klasy:\n", p.tr)

Wynik działania:

Z poziomu definicji klasy:
| M11 =   +1.00000 M12 =    0.00000 dx =   +0.00000 |
| M21 =   +0.00000 M22 =   +1.00000 dy =   +0.00000 |
| M31 =   +0.00000 M32 =   +0.00000        +1.00000 |
Z poziomu obiektu klasy:
| M11 =   +0.50000 M12 =   -0.86603 dx =   +0.00000 |
| M21 =   +0.86603 M22 =   +0.50000 dy =   +0.00000 |
| M31 =   +0.00000 M32 =   +0.00000        +1.00000 |

Jak widać, na powyższym przykładzie, zmiana wartości atrybutu zawartego w danym obiekcie klasy nie zmienia wartości atrybutu definicji klasy.

Własne metody

Tworzenie własnych metod klasy nie różni się znacząco deklaracji metod magicznych. Utwórzmy zatem metodą klasy Point2D w następujący sposób:

Listing 23
  1. def draw(self):
  2. print(Point2D.tr * self)

Powyższa metoda wykorzystuje atrybut klasy, którym jest macierz transformacji. Macierz ta przemnożona lewostronni przez punkt zwraca punkt, który może zostać obrócony i przesunięty (w zależności od ustawień macierzy transformacji Point2D.tr. Oto przykład użycia tej metody:

Listing 24
  1. import math as mt
  2. p = Point2D(100,200)
  3. p.draw()
  4. Point2D.tr = Matrix_tr(dx = 10, dy = 10)
  5. print("Po przesunięciu:")
  6. p.draw()
  7. Point2D.tr = Matrix_tr(alpha = mt.pi / 3) * Point2D.tr
  8. print("Po przesunięciu i obróceniu o 60 stopni:")
  9. p.draw()

Wynik działania:

Point2D(x=100, y=200)
Po przesunięciu:
Point2D(x=110, y=210)
Po przesunięciu i obrócenie o 60 stopni:
Point2D(x=-126.86533479473209, y=200.2627944162883)

Jak widać macierz transformacji zapisana jako atrybut klasy Point2D umożliwia przemieszczanie oraz obracanie wszystkich punktów, które zostaną utworzone tak przed, jak i po zmianie macierzy transformacji.

Wykorzystanie klasy Point2D w grafice żółwia

Po lekkiej przeróbce klasy Point2D polegającej na zmianie metody draw można wykorzystać tę klasę do rysowania w omawianej już wcześniej grafice żółwia. Oto kod programu:

Listing 25
  1. #!/usr/bin/env python
  2. # coding: utf-8
  3. import math as mt
  4. import turtle as tr
  5. class matrix_tr:
  6. def __init__(self, m11 = 1, m12 = 0, m21 = 0, m22 = 1, dx = 0, dy = 0, alpha = None):
  7. if alpha is None: # if alpha angle is set on None value
  8. self.m11 = m11
  9. self.m12 = m12
  10. self.m21 = m21
  11. self.m22 = m22
  12. else: # in other case calculate m11 - m22 using alpha angle
  13. self.m11 = mt.cos(alpha)
  14. self.m12 = -mt.sin(alpha)
  15. self.m21 = - self.m12 # this should be sin of angle alpha
  16. self.m22 = self.m11 # this should be cos of angle alpha
  17. self.dx = dx # this is an offset for x
  18. self.dy = dy # this is an offset for y
  19. def __mul__(self, other): # multiplication operator (*)
  20. if isinstance(other, Point2D): # for object of Point2D class
  21. return Point2D(other.x * self.m11 + other.y * self.m12 + self.dx, other.x * self.m21 + other.y * self.m22 + self.dy)
  22. if isinstance(other, matrix_tr): # for object of matrix_tr class
  23. return matrix_tr(self.m11 * other.m11 + self.m12 * other.m21, self.m11 * other.m12 + self.m12 * other.m22, self.m21 * other.m11 + self.m22 * other.m21, self.m21 * other.m12 + self.m22 * other.m22, self.m11 * other.dx + self.m12 * other.dy + self.dx, self.m21 * other.dx + self.m22 * other.dy + self.dy)
  24. def __str__(self):
  25. return "| M11 = {m.m11:+{l}.5f} M12 = {m.m12:{l}.5f} dx = {m.dx:+{l}.5f} |\n| M21 = {m.m21:+{l}.5f} M22 = {m.m22:+{l}.5f} dy = {m.dy:+{l}.5f} |\n| M31 = {m31:+{l}.5f} M32 = {m32:+{l}.5f} {m33:+{l}.5f} |".format(m = self, l = 10, m31 = 0, m32 = 0, m33 = 1)
  26. class Point2D:
  27. tr = matrix_tr(alpha = mt.pi / 2)
  28. def __init__( self, x = None, y = None): # konstruktor
  29. if x is None:
  30. self.x = float(input("Podaj wartość współrzędnej x: "))
  31. else:
  32. self.x = x
  33. if y is None:
  34. self.y = float(input("Podaj wartość współrzędnej y: "))
  35. else:
  36. self.y = y
  37. self.wtr = matrix_tr()
  38. def __float__(self): # rzutowanie na float
  39. return (self.x * self.x + self.y * self.y) ** 0.5
  40. def __int__(self):
  41. return int((self.x * self.x + self.y * self.y) ** 0.5)
  42. def __len__(self):
  43. return int(self)
  44. def __add__(self, other): # dodawanie
  45. return Point2D( self.x + other.x, self.y + other.y)
  46. def __sub__(self, other): # odejmowanie
  47. return Point2D( self.x - other.x, self.y - other.y)
  48. def __mul__(self, other): # mnożenie
  49. if isinstance(other, Point2D):
  50. return self.x * other.x + self.y * other.y
  51. if isinstance(other, int) or isinstance(other, float):
  52. return Point2D(self.x * other, self.y * other)
  53. return None
  54. def __radd__(self, other): # dodawanie
  55. return Point2D( self.x + other.x, self.y + other.y)
  56. def __rsub__(self, other): # odejmowanie
  57. return Point2D( other.x - self.x, other.y - self.y)
  58. def __rmul__(self, other): # mnożenie
  59. if isinstance(other, Point2D):
  60. return self.x * other.x + self.y * other.y
  61. if isinstance(other, int) or isinstance(other, float):
  62. return Point2D(self.x * other, self.y * other)
  63. return None
  64. def __iadd__(self, other): # dodawanie
  65. return Point2D( self.x + other.x, self.y + other.y)
  66. def __isub__(self, other): # odejmowanie
  67. return Point2D( other.x - self.x, other.y - self.y)
  68. def __imul__(self, other): # mnożenie
  69. if isinstance(other, Point2D):
  70. return self.x * other.x + self.y * other.y
  71. if isinstance(other, int) or isinstance(other, float):
  72. return Point2D(self.x * other, self.y * other)
  73. return None
  74. def __div__(self, other): # dzielenie
  75. if isinstance(other, int) or isinstance(other, float):
  76. return Point2D(self.x / other, self.y / other)
  77. def __str__(self): # rzutowanie na stringa
  78. return "Point2D: x={x}; y={y}".format(x = self.x, y = self.y)
  79. def draw(self):
  80. p = self.wtr * Point2D.tr * self
  81. tr.goto(p.x, p.y)
  82. def drawfu(function, xmin, xmax):
  83. x = xmin
  84. for x in range(xmin, xmax):
  85. y = eval(function)
  86. tr.pencolor(((x - xmin) / (xmax - xmin), 1 - (x - xmin) / (xmax - xmin), 0))
  87. p = Point2D(x, y)
  88. p.draw()
  89. if __name__ == "__main__":
  90. while True:
  91. tr.reset()
  92. tr.pensize(5)
  93. sc = tr.getscreen()
  94. sc.bgcolor(0,0,0)
  95. for j in range(3):
  96. for i in range(6):
  97. Point2D.tr = matrix_tr(dx = tr.pos()[0], dy = tr.pos()[1], alpha = i * mt.radians(60) + j * mt.radians(120))
  98. drawfu("20 * mt.sin(mt.radians(x * 3.6 * 5)) * mt.sin(mt.radians(x * 1.8))", 0, 100)

Wynikiem działania programu będzie powtarzanie animacji pokazanej na poniższym nagraniu.

Komentarze