Tworzenie klas

https://docs.python.org/3/tutorial/classes.html

WPROWADZENIE

W matematyce punkt na płaszczyźnie można zapisać za pomocą pary dwóch współrzędnych (x, y). W Pythonie naturalnym sposobem reprezentacji będzie para liczb typu float. Chcemy taką parę traktować jako jeden złożony obiekt. Przykładowym rozwiązaniem jest lista lub krotka, ale zwykle lepsze jest zdefiniowanie nowego typu, inaczej klasy. Składnia (dwukropek i wcięcia) jest taka jak dla innych wyrażeń złożonych w Pythonie. Nazewnictwo klas powinno być typu StudlyCups.

Instrukcja wykonywalna class tworzy obiekt klasy i przypisuje go do nazwy. Zakres instrukcji class staje się przestrzenią nazw atrybutów obiektu klasy. Atrybuty klasy udostępniają stan obiektu i jego zachowanie.

Wywołanie obiektu klasy tworzy za każdym razem nowy obiekt instancji klasy. Każdy obiekt instancji dziedziczy atrybuty klasy oraz otrzymuje własną przestrzeń nazw. Przypisania do atrybutów self w metodach tworzą atrybuty instancji (a nie klasy).


# Składnia.
class NazwaKlasy:
    class_docstring   # opcjonalnie
    instrukcje

class NazwaKlasy(lista_klas_nadrzędnych):   # dziedziczenie
    class_docstring   # opcjonalnie
    instrukcje

class Point:   # instrukcja tworząca obiekt klasy
    """Klasa odpowiadająca punktom na płaszczyźnie."""
    pass   # wymagana jakaś instrukcja

point = Point()   # tworzenie instancji klasy
# Uwaga: nawiasy pokazują, że jest to wywołanie klasy.

# Do punktu (instancji) przypisujemy atrybuty korzystając z notacji z kropką.
point.x = 3.4
point.y = 5.6
x = 7.8

# Zmienne x i point.x to dwie różne wartości.
# Instancja point jest osobną przestrzenią nazw.
print("{} {}".format(x, point.x))   # 7.8 3.4
print(point)   # heksadecymalne info

# Obiekt może być parametrem funkcji w zwykły sposób.
# Nazwa 'point' wewnątrz funkcji jest lokalna i nie koliduje
# z globalną zmienną point.

def print_point(point):
    """Wypisz punkt."""
    print("({}, {})".format(point.x, point.y))

# Wywołujemy funkcję dla punktu. Do funkcji przekazujemy wartość
# zmiennej 'point', czyli referencję do obiektu.

print_point(point)   # (3.4, 5.6)

PORÓWNYWANIE OBIEKTÓW

Są sytuacje, w których trzeba dobrze rozumieć co to znaczy, że dwa obiekty są "te same" czy "takie same".


point1 = Point()   # pierwszy punkt
point1.x = 3
point1.y = 4

point2 = Point()   # drugi punkt
point2.x = 3
point2.y = 4

point1 == point2   # False, to nie są te same obiekty, inne id
point3 = point1    # kopiujemy referencję
point1 == point3   # True,  to jest porównywanie referencji

# Taki typ równości nazywa się shallow equality (płytka, powierzchowna równość).
# Aby porównać zawartość obiektów (deep equality, głęboka równość),
# możemy napisać funkcję.

def same_point(point1, point2):
    """Porównaj punkty."""
    return (point1.x == point2.x) and (point1.y == point2.y)

same_point(point1, point2)   # True, te same wartości x i y
same_point(point1, point3)   # True, oczywiste

KOMPOZYCJA

Chcemy stworzyć klasę do reprezentowania prostokąta. Zakładamy dla prostoty, że boki prostokąta są równoległe do osi X i Y. Będziemy określać lewy dolny róg prostokąta, szerokość (width) i wysokość (height).


class Point:
    """Klasa dla punktów."""
    pass

class Rectangle:
    """Klasa dla prostokątów."""
    pass

box = Rectangle()
box.width = 100.0
box.height = 200.0
box.corner = Point()   # obiekt Point jest zagnieżdżony w obiekcie Rectangle
box.corner.x = 0.0     # określamy atrybuty punktu
box.corner.y = 0.0

# Nowe instancje klas mogą powstawać wewnątrz funkcji.

def find_center(rect):   # zwraca instancję punktu
    """Znajdź środek prostokąta."""
    point = Point()
    point.x = rect.corner.x + rect.width / 2.0
    point.y = rect.corner.y + rect.height / 2.0
    return point   # referencja do nowego obiektu

center = find_center(box)
print_point(center)   # (50.0, 100.0)

# Możemy zmieniać stan obiektu przez podstawienia jego atrybutów.
# Zapiszemy to wewnątrz funkcji, która pozwala na powiększanie prostokąta.

def grow_rect(rect, dw, dh):   # funkcja zmienia oryginał
    """Powiększ prostokąt."""
    rect.width = rect.width + dw
    rect.height = rect.height + dh

KOPIOWANIE OBIEKTÓW

Często potrzebna jest nam operacja kopiowania obiektu, która czasem powiązana jest z tworzeniem aliasu. Istnieje moduł copy, który potrafi kopiować każdy obiekt.


import copy

point1 = Point()
point1.x = 3
point1.y = 4
point2 = copy.copy(point1)
point1 == point2             # False, inne referencje
same_point(point1, point2)   # True, te same wartości x i y

Dla obiektów prostych jak Point, które nie mają zagnieżdzonych innych obiektów, zwykłe copy.copy() jest wystarczające (shallow copy). Ale dla obiektu Rectangle, który zawiera referencję do Point, copy.copy() nie robi tego, czego oczekujemy. Jeżeli stworzymy prostokąt rect1, a następnie za pomocą copy.copy() zrobimy kopię rect2, to okaże się, że rect1 i rect2 zawierają referencję do tego samego punktu (rect1.corner jest identyczny z rect2.corner). Rozwiązaniem jest głęboka kopia (deep copy). Wykorzystamy ją do przepisania funkcji grow_rect() tak, aby zwracała nowy obiekt klasy Rectangle, który jest w tym samym położeniu, ale ma nowe wymiary.


def grow_rect2(rect, dw, dh):   # zwraca nowy obiekt
    """Zwróć powiększony prostokąt."""
    import copy
    rect2 = copy.deepcopy(rect)
    rect2.width = rect2.width + dw
    rect2.height = rect2.height + dh
    return rect2