article banner (priority)

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()

Klasa przypomina przepis na potrawę, a obiekt jest konkretnym obiektem, powstałym na podstawie tego przepisu.

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

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

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 namesurname w poniższym przykładzie. Wartość atrybutu full_name obliczana jest na podstawie namesurname. 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ą do balance,
  • withdraw, która dla wystarczającej liczby środków odejmuje je od balance i zwraca True, w przeciwnym wypadku zwraca False.
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.

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

Dwie zmienne wskazują na ten sam obiekt i właściwość tego obiektu zmienia wartość.

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ł

Najpierw dwie zmienne wskazują na ten sam obiekt, następnie zmienna 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, floatbool. 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 str3.

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.

1:

"init" to skrót od "initialization", czyli "inicjalizator". Na początku i na końcu tej nazwy są po dwa znaki podkreślenia.

2:

Element to pojęcie obejmujące klasy, zmienne i funkcje.

3:

Jak przekonamy się w rozdziale Operatory, dokonuje tego przy użyciu metody specjalnej __str__.