Klasy i obiekty w Pythonie
Jak już wchodzimy w tematy bardziej zaawansowane, zacznijmy od jednego z kluczowych pojęć w języku Python: od klas i obiektów.
Jak spojrzysz przez chwilę poza strony tej książki, zapewne zobaczysz wiele obiektów. Ja widzę teraz laptopa, kubek z kawą, notatki, tablet do rysowania... Nasz świat wypełniony jest obiektami. Podobnie w programowaniu – operujemy na obiektach. Pewne z nich są bardzo podstawowe, na przykład stringi czy wartości logiczne. Możemy jednak definiować także własne obiekty, a robimy to za pomocą klas.
Definiowanie klas jest bardzo ważne, bo pozwala nam myśleć o pewnych abstrakcjach. W sklepie internetowym mówimy o użytkowniku, sprzedawcach, produktach itp. W grze mówimy o graczu, przeciwnikach, przedmiotach. W aplikacji medycznej mówimy o pacjentach, lekarzach, receptach, skierowaniach. To dzięki klasom możemy wyrazić takie pojęcia i operować na nich. To one określają także, jak te obiekty będą wyglądały i się zachowywały.
Klasa to taki szablon do tworzenia obiektów. Trochę jakby klasa była przepisem, a obiekt gotowym daniem. Klasa określa, co obiekt powinien zawierać i jak powinien się zachowywać.
Zacznijmy od najprostszej opcji, czyli pustej klasy. Taka klasa nic nie będzie zawierać, niemniej będzie miała swoją nazwę i będziemy mogli przy jej pomocy stworzyć obiekt. Klasę tworzymy przy użyciu słówka class
, po czym określamy nazwę klasy, stawiamy dwukropek i określamy ciało tej klasy. Na ten moment określamy pustą klasę, więc w jej ciele umieścimy tylko pass
.
Aby przy użyciu klasy stworzyć obiekt, używamy nazwy tej klasy oraz nawiasu. Przypomina to więc wywołanie funkcji, która zwraca pojedynczy obiekt. W poniższym przykładzie będzie na niego wskazywała zmienna cookie
.
class Cookie:
pass
cookie = Cookie()
Na ten moment nasza klasa jest pusta, a przez to i nasz obiekt nie jest zbyt ciekawy. Nie musi tak być. Obiekty mogą mieć przypisane do siebie zmienne oraz funkcje. Są one nazywane atrybutami, z czego funkcje w klasie określa się jako metody. Pomówimy o nich niedługo, zacznijmy jednak od nazywania klas.
Nazywanie klas
Przy nazywaniu klas możemy używać tych samych znaków co w przypadku zmiennych i funkcji: małych i dużych liter oraz znaku podkreślenia _
. Konwencja nazewnicza jest jednak inna. Dla funkcji i zmiennych używaliśmy snake_case. W przypadku klas używamy PascalCase (lub UpperCamelCase), czyli każde słowo zaczynamy wielką literą, nie używamy spacji ani znaków podkreślenia.
Oto kilka przykładów dobrze nazwanych klas:
User
Invoice
OrderReceipt
NeuralNetwork
Ćwiczenia: Nazywanie klas
Czy poniższe klasy są dobrze nazwane?
Personal_Invoice
UserAddress
Carengine
doctor
Dog
Odpowiedzi na końcu książki.
Zmienne obiektu
Do obiektu możemy przypisać zmienną z określoną wartością. Taka wartość dotyczyć będzie wyłącznie tego jednego obiektu. Aby odnieść się do zmiennej w obiekcie, musimy wskazać zarówno obiekt, jak i zmienną, a oddzielamy ich nazwy kropką. Dla przykładu, aby odnieść się do zmiennej type
w obiekcie cookie1
, użyjemy cookie1.type
. Zarówno do przypisania wartości, jak i do jej pobrania.
class Cookie:
pass
cookie1 = Cookie()
cookie2 = Cookie()
cookie1.type = "Dog"
cookie1.breed = "Border Collie"
cookie2.type = "Food"
print(cookie1.type) # Dog
print(cookie1.breed) # Border Collie
print(cookie2.type) # Food
Nieczęsto tworzy się zmienne obiektu tak jak w powyższym przykładzie: poza klasą. Często jest to wręcz uznawane za złą praktykę. Częściej tworzy się je w obrębie metod, a zwłaszcza szczególnej metody zwanej inicjalizatorem. Do tego dojdziemy jednak krok po kroku.
Ćwiczenie: Klasy i zmienne obiektu
Zdefiniuj klasę Player
reprezentującą gracza w grze. Nadaj mu zmienną points
z wartością 0
. Wyświetl liczbę punktów (powinna wynosić 0
), a następnie dodaj jeden punkt do atrybutu points
i wyświetl ją ponownie (powinna wynosić 1
).
Odpowiedzi na końcu książki.
Metody
Wewnątrz klas możemy definiować funkcje. Takie funkcje nazywane są metodami. Definiujemy je w ciele klasy, a ich pierwszym parametrem jest odniesienie do instancji obiektu, na którym tę metodę wywołamy. Parametr ten powinno nazywać się self
. Gdy wywołujemy metodę, zaczynamy od obiektu, następnie stawiamy kropkę, nazwę metody i nawias z argumentami.
class User:
def cheer(self):
print(f"Cześć, jestem {self.name}")
def say_hello(self, other):
print(f"Cześć {other}, jestem {self.name}")
user = User()
user.name = "Maciek"
user.cheer() # Cześć, jestem Maciek
user.say_hello("Marta") # Cześć Marta, jestem Maciek
Obiektu self
możemy użyć także, by zmodyfikować atrybuty danego obiektu.
class Position:
def step_right(self):
self.x += 1.0
def move_up(self, value):
self.y += value
pos = Position()
pos.x = 0.0
pos.y = 0.0
pos.step_right()
print(pos.x) # 1.0
pos.move_up(6.0)
print(pos.y) # 6.0
pos.move_up(3.0)
print(pos.y) # 9.0
W powyższych przykładach określaliśmy atrybuty obiektu zaraz po jego utworzeniu. Takie podejście jest bardzo niebezpieczne. Co, jeśli użytkownik zapomniałby zdefiniować jeden z koniecznych atrybutów? Zamiast tego znacznie lepiej będzie, gdy ustawimy wartości tych atrybutów przy użyciu konstruktora.
Konstruktor i inicjalizator
Gdy tworzymy nowy obiekt, stawiamy nawias za nazwą klasy. Ten nawias to wywołanie funkcji tworzącej obiekt, zwanej konstruktorem. Funkcja ta przechodzi przez szereg kroków, niezbędnych do utworzenia obiektu, w tym między innymi woła specjalną metodę o nazwie __init__
1 z naszej klasy. Ta metoda zwana jest inicjalizatorem. W jej ciele określamy, co powinno się dziać w czasie tworzenia obiektu. Najczęściej definiujemy w niej atrybuty obiektu.
class Game:
def __init__(self):
print("Starting...")
self.started = True
game = Game() # Starting...
print(game.started) # True
Liczba parametrów funkcji __init__
określa, ile argumentów powinno się znaleźć w wywołaniu konstruktora (czyli nawiasie, który stawiamy za nazwą klasy, gdy tworzymy obiekt). Jeśli więc w funkcji __init__
dodamy parametr name
, to przy tworzeniu obiektu nie możemy już zostawić pustego nawiasu. Powinniśmy podać tam argument, który posłuży jako imię. Typowym dla funkcji __init__
jest, że spodziewa się określonych parametrów, po czym przypisuje je do obiektu jako atrybuty o takiej samej nazwie.
class User:
def __init__(self, name):
self.name = name
user1 = User("Maciek")
user2 = User("Marta")
print(user1.name) # Maciek
print(user2.name) # Marta
Inicjalizator może zawierać wiele parametrów. Możemy je w dowolny sposób wykorzystać do określenia atrybutów, choć najczęściej wartości parametrów są bezpośrednio ustawiane do atrybutów o takiej samej nazwie, tak jak w przypadku name
i surname
w poniższym przykładzie. Wartość atrybutu full_name
obliczana jest na podstawie name
i surname
. Wartość points
jest określana jako 0
.
class Player:
def __init__(self, name, surname):
self.name = name
self.surname = surname
self.full_name = f"{name} {surname}"
self.points = 0
player = Player("Michał", "Mazur")
print(player.name) # Michał
print(player.surname) # Mazur
print(player.full_name) # Michał Mazur
print(player.points) # 0
Ćwiczenie: Konto bankowe
Utwórz klasę reprezentującą konto bankowe. Jaka byłaby dobra nazwa dla takiej klasy? Powinna mieć ona atrybut balance
, reprezentujący ilość środków na tym koncie. Powinna także posiadać metody:
deposit
, dodającą podaną kwotę pieniężną dobalance
,withdraw
, która dla wystarczającej liczby środków odejmuje je odbalance
i zwracaTrue
, w przeciwnym wypadku zwracaFalse
.
account = BankAccount()
print(account.balance) # 0
account.deposit(1000)
print(account.balance) # 1000
account.deposit(2000)
print(account.balance) # 3000
res = account.withdraw(1500)
print(res) # True
print(account.balance) # 1500
res = account.withdraw(2000)
print(res) # False
print(account.balance) # 1500
Dodatkowo utwórz dwa obiekty reprezentujące konta bankowe i zobacz czy, jeśli dodasz lub wypłacisz pieniądze z jednego z nich, wpłynie to na ten drugi.
Odpowiedzi na końcu książki.
Obiekty i zmienne
Przy tej okazji chciałbym podkreślić, że każdy obiekt jest osobnym bytem. To, że wyglądają podobnie, nie znaczy, że mają na siebie wpływ. Dlatego też w poniższym przykładzie zmiana name
w obiekcie user1
nie będzie miała żadnego wpływu na user2
.
class User:
def __init__(self, name):
self.name = name
user1 = User("Rafał")
user2 = User("Rafał")
print(user1.name) # Rafał
print(user2.name) # Rafał
user1.name = "Bartek"
print(user1.name) # Bartek
print(user2.name) # Rafał
Z drugiej strony, jeśli mamy dwie zmienne wskazujące na jeden obiekt, to możemy go zmienić przy użyciu dowolnej z nich. Po takim zabiegu, wartości dla obu zmiennych ulegną zmianie, bo przecież przekształcone zostało coś, na co obydwie wskazują.
user1 = User("Rafał")
user2 = user1
print(user1.name)
# Rafał
print(user2.name)
# Rafał
user1.name = "Bartek"
print(user1.name)
# Bartek
print(user2.name)
# Bartek
Warto porównać to z przykładem, gdy dwa obiekty pokazywały na tą samą wartość, a potem zmieniło się to, na co jedna z tych zmiennych wskazuje. Wynik będzie inny.
user1 = User("Rafał")
user2 = user1
print(user1.name)
# Rafał
print(user2.name)
# Rafał
user1 = User("Bartek")
print(user1.name)
# Bartek
print(user2.name)
# Rafał
user1
zaczyna wskazywać na inny.Elementy prywatne
W Pythonie przyjęła się konwencja, że atrybuty i metody, wobec których wolelibyśmy, by nie były używane poza tą klasą, zaczynają się od znaku podkreślenia _
. Takie atrybuty i metody nazywamy prywatnymi i z zasady należy ich używać wyłącznie w innych metodach tej samej klasy.
class BankAccount:
# ...
def _validate_user(self, token):
# ...
pass
def make_transaction(self, token, transaction):
self._validate_user(token)
# ...
Z technicznego punktu widzenia takie elementy wciąż są dostępne, ale należy unikać używania ich poza klasą. Jest wiele powodów, by uczynić pewne atrybuty prywatnymi. Dla przykładu, w klasie BankAccount
z poprzedniego ćwiczenia moglibyśmy chcieć pilnować, by stan konta nigdy nie spadł poniżej zera. Moglibyśmy to zrobić poprzez uczynienie balance
prywatnym, oraz zwracanie jego wartości metodą get_balance
, zaś w metodzie withdraw
przez pilnowanie stanu konta.
Atrybuty klasy
Czasem chcemy stworzyć zmienną lub funkcję, która nie będzie dotyczyła obiektu, a klasy. Innymi słowy, będzie ona współdzielona przez wszystkie obiekty tej klasy.
Aby utworzyć zmienną klasy, wystarczy ją zdefiniować w ciele tej klasy. By odnieść się do niej, używamy najpierw nazwy klasy, następnie kropki i wreszcie nazwy tej zmiennej. Nie ma więc potrzeby tworzenia żadnych obiektów.
class Score:
points = 0
print(Score.points) # 0
Score.points = 1
print(Score.points) # 1
Tutaj czeka nas jednak pewna niespodzianka. O taką zmienną możemy też zapytać obiekt i on również zwróci jej wartość. Pod warunkiem jednak, że nie ma zmiennej obiektu o takiej samej nazwie. To oznacza, że gdy w poniższym przykładzie pytamy po raz pierwszy o score1.points
, to otrzymujemy wartość zmiennej klasy, a za drugim już zmienną obiektu.
class Score:
points = 0
score1 = Score()
print(score1.points) # 0
score1.points = 10
print(score1.points) # 10
W tym miejscu jest jednak pewien haczyk. Gdy ktoś zmieni wartość zmiennej klasy, inną wartość będą zwracały wszystkie obiekty, które nie mają zmiennej obiektu o takiej samej nazwie.
score1 = Score()
score1.points = 10
score2 = Score()
score3 = Score()
print(score1.points) # 10
print(score2.points) # 0
print(score3.points) # 0
Score.points = 2
print(score1.points) # 10
print(score2.points) # 2
print(score3.points) # 2
Innymi słowy, gdy mamy zdefiniowane atrybuty o tej samej nazwie w klasie i obiekcie, to w pierwszej kolejności odczytywane będą te z obiektu, a gdy ich nie ma to z klasy. Gdy po obiekcie określamy nazwę zmiennej i używamy znaku przypisania, to ustawiamy wartość zmiennej obiektu, a nie klasy.
Zmienne klasy często są wykorzystywane do przechowywania wartości, które nie powinny ulegać zmianom. Dla przykładu klasa obliczająca wartość podatku mogłaby definiować jako stałe wartości stawek podatkowych.
class TaxCalculator:
VAT = 0.23
# ...
Zauważ, że użyłem samych wielkich liter. W przypadku wartości stałych, czyli takich, które ustawia programista i nie ulegają zmianom, używa się wielkich liter, czyli notacji znanej jako SCREAMING_SNAKE_CASE.
Czasem tworzy się klasy wyłącznie do przechowywania stałych wartości. Dla przykładu w części trzeciej, poświęconej napisaniu gry w węża, potrzebujemy w jakiś sposób określić kierunek poruszania się. Do tego użyjemy klasy Direction
ze zmiennymi odpowiadającymi za kolejne kierunki. Zmienne te mogłyby być zdefiniowane inaczej, ale taki sposób daje nam czytelne użycie.
class Direction:
UP = 1
DOWN = 2
LEFT = 3
RIGHT = 4
# Przykładowe użycie
direction = Direction.UP
if direction == Direction.UP:
direction = Direction.DOWN
Funkcje przypisane do klasy, a nie obiektu, nazywane są statycznymi. Są one definiowane analogicznie jak zwyczajne metody, ale nie mają parametru self
. Powinniśmy także postawić przed nimi @staticmethod
. Takie metody możemy wywoływać na klasie i nie potrzebują obiektu.
class Counter:
num = 0
def __init__(self):
print("Tworzę")
Counter.num += 1
@staticmethod
def print_counter():
print(f"Stworzono {Counter.num}")
c1 = Counter() # Tworzę
c2 = Counter() # Tworzę
c3 = Counter() # Tworzę
Counter.print_counter() # Stworzono 3
Sprawdzanie klasy obiektu
Aby sprawdzić, przy użyciu jakiej klasy powstał obiekt, możemy użyć funkcji type
, która zwraca obiekt zwany typem. Typy poznaliśmy już w rozdziale Podstawowe wartości, gdzie poznaliśmy str
, int
, float
i bool
. Typami są również nazwy klas, a więc możemy sprawdzić, czy obiekt powstał przy użyciu klasy Cookie
poprzez przyrównanie jego typu do Cookie
. Typy porównujemy przy użyciu słówka is
, które poznaliśmy już przy okazji wartości None
w rozdziale Zmienne. Jeśli chcemy sprawdzić, czy typy są różne, używamy is not
.
class Cookie:
pass
c = Cookie()
print(type(c)) # <class '__main__.Cookie'>
print(type(c) is Cookie) # True
print(type(c) is not Cookie) # False
print(type(c) is int) # False
print(type(c).__name__) # Cookie
Klasa str
W rozdziale Podstawowe wartości poznaliśmy stringi, wartości liczbowe oraz logiczne. One również są obiektami, a więc mają swoją klasę, konstruktor i metody. Skupmy się na klasie str
, przy użyciu której tworzone są stringi. Nazwa str
nie jest zbyt typowa dla klas, ale przecież string jest wartością specjalną. Jej konstruktor pozwala na zamianę obiektów innego typu właśnie na obiekt klasy str
3.
str1 = "AAA"
print(type(str1)) # <class 'str'>
i = 10
print(type(i)) # <class 'int'>
str2 = str(i)
print(type(str2)) # <class 'str'>
b = True
print(type(b)) # <class 'bool'>
str3 = str(b)
print(type(str3)) # <class 'str'>
Gdy przekazujemy do funkcji print
obiekt, który nie jest typu str
, zostanie on zamieniony właśnie przy użyciu konstruktora. Jest on również używany przez f-stringi, na przykład w niedawno użytym f"Stworzono {Counter.num}"
.
Klasa str
definiuje także pewne metody, które możemy wywołać na dowolnym obiekcie typu str
. Oto najistotniejsze z nich:
upper
zwraca tekst, w którym wszystkie małe litery zostały zamienione na wielkie.lower
zwraca tekst, w którym wszystkie wielkie litery zostały zamienione na małe.capitalize
zwraca tekst, w którym pierwsza litera zostaje zamieniona na wielką.title
zwraca tekst, w którym pierwsze litery każdego słowa zostają zamienione na wielkie.replace
zwraca tekst, w którym wszystkie wystąpienia jednego słowa zostają zastąpione innym.
name = "dOmInIkA sito"
print(name.upper()) # DOMINIKA SITO
print(name.lower()) # dominika sito
print(name.capitalize()) # Dominika sito
print(name.title()) # Dominika Sito
text = "Cześć {name}, piszę do ciebie"
new_text = text.replace("{name}", "Michał")
print(new_text) # Cześć Michał, piszę do ciebie
new_text = new_text.replace("ciebie", "Ciebie")
print(new_text) # Cześć Michał, piszę do Ciebie
Zakończenie
W tym rozdziale poznaliśmy klasy. Są one bardzo ważną częścią programowania w języku Python i będą nam przydatne w dalszych częściach tej książki. Teraz jednak przejdźmy do bardzo specjalnej i bardzo przydatnej klasy, jaką jest lista.
"init" to skrót od "initialization", czyli "inicjalizator". Na początku i na końcu tej nazwy są po dwa znaki podkreślenia.
Element to pojęcie obejmujące klasy, zmienne i funkcje.
Jak przekonamy się w rozdziale Operatory, dokonuje tego przy użyciu metody specjalnej __str__
.