W tym odcinku postaramy się poznać i zrozumieć podstawy programowania obiektowego w Pythonie, czyli podstawy pisania klas i tworzenia obiektów. Myślę, że nadszedł na to czas, w poprzednich częściach tutoriala zdobyliśmy wszystkie potrzebne podstawy.

Klasa to tak jakby przepis, zaś obiekt to konkretna jego implementacja. Jeżeli mamy w domu psa o imieniu Azor, to „pies” jest klasą, „Azor” obiektem klasy pies. Tyle teorii.

Pierwsza klasa, konstruktor i korzystanie z obiektu

Oto nasza najłatwiejsza klasa. Klasa o nazwie osoba, która w konstruktorze (funkcja __init__) przyjmuje imię i wiek:

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age


person1 = Person("John", 30)
print(person1.name)
print(person1.age)

# John
# 30

self jest pierwszym argumentem każdej metody klasy i odnosi się do obiektu. W taki sposób jak powyżej, używając konstruktora, możemy przypisywać do obiektu różne właściwości (tutaj name oraz age).

Możemy potem stworzyć obiekt i „po kropce” odnosić się do jego właściwości.

Tutaj nie ma co specjalnie tłumaczyć dlaczego coś nazywa się jak się nazywa albo wygląda jak wygląda, tutaj po prostu trzeba nabrać wprawy.

Metoda str – wypisz obiekt

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __str__(self):
        return f"Name: {self.name}, Age: {self.age}"


person1 = Person("John", 30)
print(person1.name)
print(person1.age)
# John
# 30
print(person1)
# Name: John, Age: 30

Napisaliśmy sobie metodę str. Tam określamy, co chcemy zwrócić, jeżeli obiekt będzie wrzucony do funkcji print. Zwracamy taki oto f-string z imieniem i wiekiem danego obiektu.

Jak widać, po wrzuceniu do print – nasza funkcja działa.

Funkcje z dwoma podkreślnikami to tzw. magiczne funkcje. Są one zdefiniowane, jest ich ograniczona ilość, określają one jak obiekt działa w konkretnych sytuacjach.

Nie musimy tylko z nich korzystać. Możemy pisać własne metody:

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __str__(self):
        return f"Name: {self.name}, Age: {self.age}"

    def get_older(self):
        self.age += 1


person1 = Person("John", 30)
print(person1.name)
print(person1.age)
# John
# 30
print(person1)
# Name: John, Age: 30
person1.get_older()
print(person1)
# Name: John, Age: 31

Tutaj mamy własną metodę get older, która zwiększa nam wiek o 1, a także przykład jej zastosowania.

Funkcja len – długość obiektu

Funkcja __len__ odpowiada za to w jaki sposób zachowywać się będzie obiekt, wrzucony w funkcję len(). Jeżeli jej nie będzie – taki obiekt nie będzie mógł być wrzucony w tę funkcję.

W naszym przykładzie sensownie byłoby, aby funkcja len zwracała wiek danej osoby, choć możemy dać cokolwiek (ilość liter w imieniu albo dosłownie cokolwiek, co nam do głowy przyjdzie).

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __str__(self):
        return f"Name: {self.name}, Age: {self.age}"

    def get_older(self):
        self.age += 1

    def __len__(self):
        return self.age

person1 = Person("John", 30)
person1.get_older()
print(person1)
# Name: John, Age: 31
print(len(person1))
# 31

Tutaj zdecydowaliśmy się zwracać wiek, ilekroć ktoś będzie chciał użyć funkcji len() na naszym obiekcie.

Funkcja eq – porównanie

Funkcja __eq__ służy porównaniu dwóch obiektów ze sobą przy pomocy operatora „==”. Poza self musimy użyć drugiego argumentu, zwyczajowo nazywanego other, jako że będziemy porównywać dwa obiekty ze sobą.

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __str__(self):
        return f"Name: {self.name}, Age: {self.age}"

    def get_older(self):
        self.age += 1

    def __len__(self):
        return self.age
    
    def __eq__(self, other):
        return self.name == other.name and self.age == other.age

person1 = Person("John", 30)
person2 = Person("John", 30)
print(person1 == person2)
person1.get_older()
print(person1 == person2)
# True
# False

Tutaj nasza funkcja __eq__ porównuje, czy oba obiekty mają takie same imię i taki sam wiek.

Następnie porównujemy obiekty person1 i person2. Spełniają warunek, True zwracają.

Ale gdy jeden z naszych obiektów się „postarzał” funkcją get_older(), ten warunek już przestał być prawdziwy.

Funkcja add – obsługiwanie dodawania

Funkcja add pozwala nam obsługiwać sytuację, w której ktoś próbuje dodać nam coś do obiektu operatorem „+”.

Spróbujmy sobie napisać taką funkcję, która pozwoli nam dodać do obiektu liczbę typu int, co ma zaowocować „postarzeniem” obiektu o daną liczbę.

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __str__(self):
        return f"Name: {self.name}, Age: {self.age}"

    def get_older(self):
        self.age += 1

    def __len__(self):
        return self.age

    def __eq__(self, other):
        return self.name == other.name and self.age == other.age

    def __add__(self, other):
        if type(other) == int:
            self.age += other
            return self
        raise ValueError("Cannot add this type to person")

person1 = Person("John", 30)
print(person1)
person1 = person1 + 10
print(person1)

Tutaj dokonujemy sprawdzenia, jakiego typu jest argument other. Jeżeli to int, zwiększamy wiek o tę liczbę i zwracamy self, czyli zwracamy obiekt.

W innym przypadku – Value Error, nie możemy dodawać innych elementów, niż liczby typu int.

Return self – chainowanie

„return self” to ciekawa konstrukcja, która pozwala nam na „chainowanie” czyli łańcuchowe wywoływanie metody, która zwraca zawsze ten obiekt na końcu.

Dodajmy sobie return self do naszej funkcji get_older i zobaczmy, co możemy osiągnąć:

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __str__(self):
        return f"Name: {self.name}, Age: {self.age}"

    def get_older(self):
        self.age += 1
        return self

    def __len__(self):
        return self.age

    def __eq__(self, other):
        return self.name == other.name and self.age == other.age

    def __add__(self, other):
        if type(other) == int:
            self.age += other
            return self
        raise ValueError("Cannot add this type to person")


person1 = Person("John", 30)
person1.get_older().get_older().get_older()
print(person1)
#Name: John, Age: 33

Dzięki „return self” możemy wywołać kilka metod na raz „po kropce”. Możemy też wywoływać różne metody, jeżeli zwracają one self, czyli obiekt.

Moglibyśmy np. wywołać metodę „postarz o jeden rok”, „dodaj 500$ do ilości pieniędzy” i „zwiększ ilość punktów życia” za jednym razem na naszej klasie (gdyby miała takie właściwości i metody) i nie byłoby z tym żadnego problemu – wystarczy, że funkcja zwraca self, czyli obiekt, a można sobie metody „chainować”.

Zmienna klasowa

Mieliśmy do czynienia ze zmiennymi obiektu (name, age), ale możemy też mieć zmienną klasową, zmienną dostępną dla całej klasy.

Zróbmy sobie taką zmienną. Będzie nią „ilość osób”, równa 0 na początek i zwiększana o 1 za każdym razem, gdy tworzona jest nowa osoba.

class Person:
    number_of_people = 0
    def __init__(self, name, age):
        self.name = name
        self.age = age
        Person.number_of_people += 1

    def __str__(self):
        return f"Name: {self.name}, Age: {self.age}"

    def get_older(self):
        self.age += 1
        return self

    def __len__(self):
        return self.age

    def __eq__(self, other):
        return self.name == other.name and self.age == other.age

    def __add__(self, other):
        if type(other) == int:
            self.age += other
            return self
        raise ValueError("Cannot add this type to person")

print(Person.number_of_people)
person1 = Person("John", 30)
print(Person.number_of_people)
person2 = Person("Jane", 20)
print(Person.number_of_people)
# 0
# 1
# 2

Jak widać, mamy zmienną klasową, wewnątrz klasy, ale nie obiektu. Wynosi ona 0. Za każdym razem, gdy tworzymy obiekt (funkcja __init__, funkcja konstruktor) zwiększamy tę zmienną o 1.

Możemy tworzyć nowe obiekty i patrzeć, jak ta zmienna rośnie.

Funkcja del

Nie wiem, czy znamy słówko kluczowe „del”. Jest to słówko służące do ręcznego usuwania zmiennych. Możemy w ten sposób usunąć jakiś obiekt.

Mamy też funkcję __del__, która obsługuje to, co dzieje się, gdy tak obiekt sobie tym słówkiem kluczowym usuniemy.

Dodajmy funkcjonalność, aby zmienna klasowa number_of_people zmniejszała się o 1, gdy obiekt jest usuwany

class Person:
    number_of_people = 0
    
    def __init__(self, name, age):
        self.name = name
        self.age = age
        Person.number_of_people += 1

    def __del__(self):
        Person.number_of_people -= 1

    def __str__(self):
        return f"Name: {self.name}, Age: {self.age}"

    def get_older(self):
        self.age += 1
        return self

    def __len__(self):
        return self.age

    def __eq__(self, other):
        return self.name == other.name and self.age == other.age

    def __add__(self, other):
        if type(other) == int:
            self.age += other
            return self
        raise ValueError("Cannot add this type to person")

print(Person.number_of_people)
person1 = Person("John", 30)
print(Person.number_of_people)
person2 = Person("Jane", 20)
print(Person.number_of_people)
# 0
# 1
# 2
del person2
print(Person.number_of_people)
# 1

Jak widać, nasza zmienna działa poprawnie, po usunięciu obiektu przy użyciu del, liczba osób zmalała.

Metody statyczne

Metody statyczne to takie, do których nie jest w ogóle potrzebny obiekt klasy. Nie przyjmują self, tylko jakiś argument (albo żaden) i zwracają jakąś wartość (jak normalne funkcje).

Napiszmy sobie metodę statyczną, która przyjmuje jakąś liczbę (ilość lat) i zwraca wiek w miesiącach. Nie wiek danego obiektu, ale wiek podany w tej funkcji.

class Person:
    number_of_people = 0

    def __init__(self, name, age):
        self.name = name
        self.age = age
        Person.number_of_people += 1

    def __del__(self):
        Person.number_of_people -= 1

    def __str__(self):
        return f"Name: {self.name}, Age: {self.age}"

    def get_older(self):
        self.age += 1
        return self

    def __len__(self):
        return self.age

    def __eq__(self, other):
        return self.name == other.name and self.age == other.age

    def __add__(self, other):
        if type(other) == int:
            self.age += other
            return self
        raise ValueError("Cannot add this type to person")

    @staticmethod
    def age_in_moths(years):
        return years * 12

print(Person.age_in_moths(30))
# 360

Jak widać potrzebowaliśmy dekoratora @staticmethod, nie potrzebowaliśmy self, nie potrzebowaliśmy też żadnego obiektu, aby tej metody użyć.

A jak wyglądałaby metoda niestatyczna? Niech robi to samo, ale na konkretnym obiekcie.

class Person:
    number_of_people = 0

    def __init__(self, name, age):
        self.name = name
        self.age = age
        Person.number_of_people += 1

    def __del__(self):
        Person.number_of_people -= 1

    def __str__(self):
        return f"Name: {self.name}, Age: {self.age}"

    def get_older(self):
        self.age += 1
        return self

    def __len__(self):
        return self.age

    def __eq__(self, other):
        return self.name == other.name and self.age == other.age

    def __add__(self, other):
        if type(other) == int:
            self.age += other
            return self
        raise ValueError("Cannot add this type to person")

    def age_in_months(self):
        return self.age * 12


person1 = Person("John", 30)
print(person1.age_in_months())
# 360

Tutaj musimy wywołać ją na obiekcie. Zwraca wiek w miesiącach danego obiektu.

Dodam, że można stworzyć klasę, która nie posiada dosłownie niczego, poza metodami statycznymi i nazwą, która w jakiś sposób łączy się z nimi.

Pomyślmy o czymś takim jak kalkulator. Nie będziemy mu nadawać imienia, numeru seryjnego, ani innych takich (chyba że piszemy program dla sklepu internetowego, wszystko zależy od kontekstu).

Oto przykład kalkulatora, wykonującego tylko obliczenia.

class Calculator:

    @staticmethod
    def add(a, b):
        return a + b

    @staticmethod
    def sub (a, b):
        return a - b

    @staticmethod
    def mul(a,b):
        return a * b

    @staticmethod
    def div(a,b):
        return a / b

print(Calculator.add(2,2))
#4

W ten sposób możemy tworzyć klasy, wykonujące różnego rodzaju „utilities” czyli klasy zawierające statyczne metody. Używamy tych klas do wykonywania jakichś czynności, nie tworząc żadnych obiektów tych klas, tylko wykorzystując statyczne metody.

Zadanie – klasa couple

Postaramy się utrwalić tę nową poznaną i nieco jeszcze pobieżną wiedzę o klasach i obiektach.

Mamy taką oto klasę Person:

class Person:

    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __str__(self):
        return f"Name: {self.name}, Age: {self.age}"

    def __add__(self, other):
        if type(other) == int:
            self.age += other
            return self
        raise ValueError("Cannot add this type to person")



person1 = Person("John", 30)
person2 = Person("Jane", 28)

Chcemy do tej klasy dodać możliwość dodawania do siebie dwóch obiektów typu person oraz by efektem tego dodawania była klasa Couple (para), która składa się z dwóch elementów (person1 i person2).

Napiszmy sobie na razie klasę Couple i sprawdźmy, czy działa:

class Person:

    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __str__(self):
        return f"Name: {self.name}, Age: {self.age}"

    def __add__(self, other):
        if type(other) == int:
            self.age += other
            return self
        raise ValueError("Cannot add this type to person")

class Couple:
    def __init__(self, p1, p2):
        self.person1 = p1
        self.person2 = p2

person1 = Person("John", 30)
person2 = Person("Jane", 28)
couple = Couple(person1, person2)
print(couple.person1.name)
print(couple.person1.age)
print(couple.person2.name)
print(couple.person2.age)
# John
# 30
# Jane
# 28

Do couple przekazaliśmy dwa obiekty typu person i rzeczywiście, działa. Dodajmy sobie __str__:

class Person:

    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __str__(self):
        return f"Name: {self.name}, Age: {self.age}"

    def __add__(self, other):
        if type(other) == int:
            self.age += other
            return self
        raise ValueError("Cannot add this type to person")

class Couple:
    def __init__(self, p1, p2):
        self.person1 = p1
        self.person2 = p2

    def __str__(self):
        return f"Couple: {self.person1.name} ({self.person1.age}) and {self.person2.name} ({self.person2.age}) "

person1 = Person("John", 30)
person2 = Person("Jane", 28)
couple = Couple(person1, person2)
print(couple)
# Couple: John (30) and Jane (28) 

Zobaczmy sobie, czy elementy person1 i person2 dla klasy Couple są instancją (obiektem) klasy person:

class Person:

    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __str__(self):
        return f"Name: {self.name}, Age: {self.age}"

    def __add__(self, other):
        if type(other) == int:
            self.age += other
            return self
        raise ValueError("Cannot add this type to person")

class Couple:
    def __init__(self, p1, p2):
        self.person1 = p1
        self.person2 = p2

    def __str__(self):
        return f"Couple: {self.person1.name} ({self.person1.age}) and {self.person2.name} ({self.person2.age}) "

person1 = Person("John", 30)
person2 = Person("Jane", 28)
couple = Couple(person1, person2)
print(isinstance(couple.person1, Person))
print(isinstance(couple.person2, Person))
# True
# True

Są.

Teraz możemy zmodyfikować add, aby zwracał nam odpowiedni Couple:

class Person:

    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __str__(self):
        return f"Name: {self.name}, Age: {self.age}"

    def __add__(self, other):
        if type(other) == int:
            self.age += other
            return self
        if isinstance(other, Person):
            return Couple(self, other)
        raise ValueError("Cannot add this type to person")

class Couple:
    def __init__(self, p1, p2):
        self.person1 = p1
        self.person2 = p2

    def __str__(self):
        return f"Couple: {self.person1.name} ({self.person1.age}) and {self.person2.name} ({self.person2.age}) "

person1 = Person("John", 30)
person2 = Person("Jane", 28)
couple = person1 + person2
print(couple)
# Couple: John (30) and Jane (28) 

I tak nam to działa. Możemy dodawać obiekty klasy person do siebie i otrzymywać obiekt typu Couple.

Obiekt typu Couple zawiera 2 obiekty klasy Person (person1, person2). Dzięki temu może odnosić się do ich imienia i wieku, które z kolei są zdefiniowane w klasie Person.

Dokonaliśmy nie tylko stworzenia nowej klasy, ale także nawiązania pewnej relacji między klasami. Relacji „HAS-A”, czyli „ma”. Para ma osobę (a w zasadzie dwie).

Istnieje jeszcze inny typ relacji, relacja „IS-A”. Związane jest to z dziedziczeniem. Wiemy już, że w przypadku psa o imieniu Azor, Azor jest obiektem, zaś pies klasą.

Jest jeszcze takie zagadnienie jak dziedziczenie, i możemy mieć klasę zwierzę i podklasę pies. Relacja między nimi jest taka, że obiekt klasy Pies jest Zwierzęciem. To oznacza relacja „IS-A” (jest). Tego jeszcze nie poznaliśmy, ale wszystko w swoim czasie.

Na razie w dość ekspresowym tempie poznaliśmy podstawy klas i obiektów oraz możliwości, jakie programowanie obiektowe daje.

Wraz z kolejnymi odcinkami tutoriala powinniśmy poradzić sobie z oswojeniem się z programowaniem obiektowym. Na razie myślę, że to, co już poznaliśmy, wystarczy.

Przeanalizujmy to sobie raz jeszcze i ewentualnie postarajmy się poeksperymentować.