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ć.