Klasy

Autor podstrony: Krzysztof Zajączkowski

Stronę tą wyświetlono już: 8609 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:

class Point2D: def __init__(self, x = None, y = None): self.x = float(input("Podaj współrzędną x: ")) if (x is None) else x self.y = float(input("Podaj współrzędną y: ")) if (y is None) else y def __del__(self): 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:

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 p2 = Point2D(20, 30) # a tutaj tworzę obiekt, o podanych współrzędnych del p # tutaj usuwam obiekt, wywołując destruktor 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__:

def __int__(self): 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:

p = Point2D(10,0) print(int(p))

Wynik działania powyższego kodu:

10

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

def __float__(self): 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:

p = Point2D(10,0) print(float(p))

Wynik działania powyższego kodu:

10.0

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

def __str__(self): 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:

p = Point2D(10,0) 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:

def __len__(self): return int(self)

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

p = Point2D(10,0) 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:

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

def __eq__(self, other): if isinstance(other, Point2D): return True if other.x == self.x and other.y == self.y else False else: return True if (float(self) == other) else False def __ne__(self, other): return not (self == other) def __lt__(self, other): return float(self) < float(other) def __gt__(self, other): return float(self) > float(other) def __le__(self, other): return not (self > other) def __gt__(self, other): return not (self < other)

Operatory arytmetyczne

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

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

def __add__(self, other): if isinstance(other, Point2D): return Point2D(self.x + other.x, self.y + other.y) else: return Point2D(self.x + float(other), self.y + float(other)) def __sub__(self, other): if isinstance(other, Point2D): return Point2D(self.x - other.x, self.y - other.y) else: return Point2D(self.x - float(other), self.y - float(other)) def __mul__(self, other): if isinstance(other, Point2D): return self.x * other.x + self.y * other.y else: 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:

p = Point2D(10,20) p = p * 10

Nie obsługują natomiast takiego przypadku:

p = Point2D(10,20) 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:

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

def __radd__(self, other): if isinstance(other, Point2D): return Point2D(self.x + other.x, self.y + other.y) else: return Point2D(self.x + float(other), self.y + float(other)) def __rsub__(self, other): if isinstance(other, Point2D): return Point2D(other.x - self.x, other.y - self.y) else: return Point2D(float(other) - self.x, float(other) - self.y) def __rmul__(self, other): if isinstance(other, Point2D): return self.x * other.x + self.y * other.y else: 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:

p = Point2D(10,20) p += p

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

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

def __iadd__(self, other): if isinstance(other, Point2D): self.x += other.x self.y += other.y else: self.x += float(other) self.y += float(other) return self def __isub__(self, other): if isinstance(other, Point2D): self.x -= other.x self.y -= other.y else: self.x -= float(other) self.y -= float(other) return self def __imul__(self, other): self.x *= float(other) self.y *= float(other) return self

Operatory jednoargumentowe

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

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

def __pos__(self): return Point2D(self.x, self.y) def __neg__(self): return Point2D(- self.x, - self.y) def __abs__(self): 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:

class Matrix_tr: def __init__(self, m11 = 1, m12 = 0, m21 = 0, m22 = 1, dx = 0, dy = 0, alpha = None): if alpha is None: # if alpha angle is set on None value self.m11 = m11 self.m12 = m12 self.m21 = m21 self.m22 = m22 else: # in other case calculate m11 - m22 using alpha angle self.m11 = mt.cos(alpha) self.m12 = -mt.sin(alpha) self.m21 = - self.m12 # this should be sin of angle alpha self.m22 = self.m11 # this should be cos of angle alpha self.dx = dx # this is an offset for x self.dy = dy # this is an offset for y def __mul__(self, other): # multiplication operator (*) if isinstance(other, Point2D): # for object of Point2D class return Point2D(other.x * self.m11 + other.y * self.m12 + self.dx, other.x * self.m21 + other.y * self.m22 + self.dy) if isinstance(other, Matrix_tr): # for object of Matrix_tr class 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) def __str__(self): 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:

class Point2D: tr = Matrix_tr() # atrybut klasy Point2D

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

print(Point2D.tr) p = Point2D(100,200) 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:

import math as mt p = Point2D(100,200) p.tr = Matrix_tr(alpha = mt.pi / 3) print("Z poziomu definicji klasy:\n",Point2D.tr) 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:

def draw(self): 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:

import math as mt p = Point2D(100,200) p.draw() Point2D.tr = Matrix_tr(dx = 10, dy = 10) print("Po przesunięciu:") p.draw() Point2D.tr = Matrix_tr(alpha = mt.pi / 3) * Point2D.tr print("Po przesunięciu i obróceniu o 60 stopni:") 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:

#!/usr/bin/env python # coding: utf-8 import math as mt import turtle as tr class matrix_tr: def __init__(self, m11 = 1, m12 = 0, m21 = 0, m22 = 1, dx = 0, dy = 0, alpha = None): if alpha is None: # if alpha angle is set on None value self.m11 = m11 self.m12 = m12 self.m21 = m21 self.m22 = m22 else: # in other case calculate m11 - m22 using alpha angle self.m11 = mt.cos(alpha) self.m12 = -mt.sin(alpha) self.m21 = - self.m12 # this should be sin of angle alpha self.m22 = self.m11 # this should be cos of angle alpha self.dx = dx # this is an offset for x self.dy = dy # this is an offset for y def __mul__(self, other): # multiplication operator (*) if isinstance(other, Point2D): # for object of Point2D class return Point2D(other.x * self.m11 + other.y * self.m12 + self.dx, other.x * self.m21 + other.y * self.m22 + self.dy) if isinstance(other, matrix_tr): # for object of matrix_tr class 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) def __str__(self): 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) class Point2D: tr = matrix_tr(alpha = mt.pi / 2) def __init__( self, x = None, y = None): # konstruktor if x is None: self.x = float(input("Podaj wartość współrzędnej x: ")) else: self.x = x if y is None: self.y = float(input("Podaj wartość współrzędnej y: ")) else: self.y = y self.wtr = matrix_tr() def __float__(self): # rzutowanie na float return (self.x * self.x + self.y * self.y) ** 0.5 def __int__(self): return int((self.x * self.x + self.y * self.y) ** 0.5) def __len__(self): return int(self) def __add__(self, other): # dodawanie return Point2D( self.x + other.x, self.y + other.y) def __sub__(self, other): # odejmowanie return Point2D( self.x - other.x, self.y - other.y) def __mul__(self, other): # mnożenie if isinstance(other, Point2D): return self.x * other.x + self.y * other.y if isinstance(other, int) or isinstance(other, float): return Point2D(self.x * other, self.y * other) return None def __radd__(self, other): # dodawanie return Point2D( self.x + other.x, self.y + other.y) def __rsub__(self, other): # odejmowanie return Point2D( other.x - self.x, other.y - self.y) def __rmul__(self, other): # mnożenie if isinstance(other, Point2D): return self.x * other.x + self.y * other.y if isinstance(other, int) or isinstance(other, float): return Point2D(self.x * other, self.y * other) return None def __iadd__(self, other): # dodawanie return Point2D( self.x + other.x, self.y + other.y) def __isub__(self, other): # odejmowanie return Point2D( other.x - self.x, other.y - self.y) def __imul__(self, other): # mnożenie if isinstance(other, Point2D): return self.x * other.x + self.y * other.y if isinstance(other, int) or isinstance(other, float): return Point2D(self.x * other, self.y * other) return None def __div__(self, other): # dzielenie if isinstance(other, int) or isinstance(other, float): return Point2D(self.x / other, self.y / other) def __str__(self): # rzutowanie na stringa return "Point2D: x={x}; y={y}".format(x = self.x, y = self.y) def draw(self): p = self.wtr * Point2D.tr * self tr.goto(p.x, p.y) def drawfu(function, xmin, xmax): x = xmin for x in range(xmin, xmax): y = eval(function) tr.pencolor(((x - xmin) / (xmax - xmin), 1 - (x - xmin) / (xmax - xmin), 0)) p = Point2D(x, y) p.draw() if __name__ == "__main__": while True: tr.reset() tr.pensize(5) sc = tr.getscreen() sc.bgcolor(0,0,0) for j in range(3): for i in range(6): Point2D.tr = matrix_tr(dx = tr.pos()[0], dy = tr.pos()[1], alpha = i * mt.radians(60) + j * mt.radians(120)) 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.