Last modified: June 23, 2022
This article is written in: 🇵🇱
Programowanie obiektowe (ang. Object-Oriented Programming, OOP) to jeden z najpopularniejszych i najbardziej przemyślanych sposobów tworzenia oprogramowania. Polega na organizowaniu kodu w logiczne jednostki (obiekty), które łączą dane (atrybuty) i funkcje (metody) w jedną spójną całość. Dzięki temu kod staje się łatwiejszy w utrzymaniu, rozbudowie i ponownym wykorzystaniu. Klasy i obiekty są kluczowymi elementami tego paradygmatu: klasa to pewien „przepis” (lub „szablon”), a obiektem nazywamy konkretny egzemplarz stworzony na podstawie takiego przepisu.
Paradygmat obiektowy jest bardzo intuicyjny, ponieważ często odwzorowuje realne sytuacje w kodzie. Można to porównać do planów architektonicznych (klasy), które opisują, jak ma wyglądać i działać budynek, podczas gdy obiekty są już fizycznymi budowlami wzniesionymi według tego planu. Właśnie to rozróżnienie pomiędzy projektem (klasą) a konkretną realizacją (obiektem) stanowi serce OOP.
Klasy pozwalają definiować zarówno dane, które reprezentują stan obiektu (tzw. atrybuty), jak i metody, czyli czynności wykonywane na tych danych. Kiedy tworzony jest obiekt, staje się on instancją danej klasy i „dziedziczy” wszystkie właściwości oraz zachowania zdefiniowane w tej klasie.
Korzyści płynące z tego podejścia to:
Poniżej przedstawione zostały szczegółowe wyjaśnienia związane z klasami, obiektami oraz pojęciami z nimi powiązanymi. Przedstawione przykłady kodu w Pythonie pozwolą łatwiej zrozumieć, jak w praktyce wykorzystuje się programowanie obiektowe.
Kiedy tworzymy klasę w Pythonie, zazwyczaj definiujemy w niej:
__init__
) – specjalną metodę, która wywoływana jest automatycznie przy tworzeniu nowego obiektu. Służy do inicjowania (nadawania pierwszych wartości) atrybutów.Dzięki temu programista może zdefiniować, czym jest obiekt (jakie ma dane, co może robić), a także jakie operacje są na nim dozwolone czy typowe.
Poniżej znajduje się prosty przykład klasy Osoba
, która przechowuje imię i nazwisko oraz może się przedstawić:
class Osoba:
# Konstruktor klasy
def __init__(self, imie, nazwisko):
self.imie = imie
self.nazwisko = nazwisko
# Metoda klasy
def przedstaw_sie(self):
print(f"Cześć, jestem {self.imie} {self.nazwisko}")
# Tworzenie obiektów klasy Osoba
osoba1 = Osoba("Jan", "Kowalski")
osoba2 = Osoba("Adam", "Nowak")
# Wywołanie metody dla obiektów
osoba1.przedstaw_sie()
osoba2.przedstaw_sie()
W tym przykładzie:
Osoba
z dwoma atrybutami (imie
i nazwisko
) oraz jedną metodą (przedstaw_sie
).osoba1
i osoba2
. Każdy z nich ma swój własny stan (różne imię i nazwisko).przedstaw_sie
każdy obiekt może wyświetlić swoją unikalną charakterystykę.Atrybuty w obiektach odczytujemy i modyfikujemy za pomocą notacji kropkowej. Oznacza to, że do atrybutu docieramy przez nazwa_obiektu.atrybut
. Możemy je także zmieniać, przypisując do nich nową wartość:
osoba = Osoba("Jan", "Kowalski")
print(osoba.imie) # Wyświetli: Jan
osoba.imie = "Adam"
print(osoba.imie) # Wyświetli: Adam
W powyższym przykładzie tworzymy obiekt osoba
, a następnie uzyskujemy do niego dostęp przez pole imie
. Zmieniamy jego wartość na "Adam"
i ponownie wyświetlamy, co dowodzi, że atrybut został zaktualizowany. W praktyce jest to niezwykle wygodne, jednak nie zawsze chcemy, aby atrybuty były modyfikowane dowolnie z zewnątrz. Dlatego w Pythonie istnieją mechanizmy takie jak dekoratory @property
oraz @setter
, które umożliwiają kontrolę nad tym, w jaki sposób atrybut może być zmieniany (lub odczytywany).
W przypadku, gdy chcemy mieć większą kontrolę nad dostępem do atrybutów, możemy skorzystać z tzw. „właściwości” (properties). Dzięki dekoratorom @property
oraz @nazwa_atrybutu.setter
tworzymy specjalne metody wywoływane przy odczycie i zapisie atrybutu. Jest to elegancki i zalecany sposób, by uniknąć bezpośredniego modyfikowania atrybutów obiektu.
class Osoba:
def __init__(self, imie, nazwisko):
self._imie = imie # Konwencja z podkreślnikiem wskazuje na "chronione" atrybuty
self._nazwisko = nazwisko
@property
def imie(self):
print('Ktoś próbuje odczytać imię')
return self._imie
@imie.setter
def imie(self, nowa_wartosc):
print('Ktoś modyfikuje imię')
self._imie = nowa_wartosc
osoba = Osoba("Jan", "Kowalski")
print(osoba.imie) # Ktoś próbuje odczytać imię, wyświetli: Jan
osoba.imie = "Adam" # Ktoś modyfikuje imię
print(osoba.imie) # Ktoś próbuje odczytać imię, wyświetli: Adam
Dzięki temu mechanizmowi możemy również dodać logikę walidującą czy kontrolną w setterze (np. sprawdzanie, czy nowe imię jest ciągiem znaków i spełnia konkretne warunki). W praktyce pozwala to zabezpieczyć dane obiektu przed niepożądanymi wartościami.
Klasy i obiekty nie służą wyłącznie do porządkowania kodu. Zastosowanie programowania obiektowego niesie ze sobą szereg zalet w kontekście rozwijania większych projektów, pracy zespołowej czy ponownego wykorzystania już istniejących rozwiązań w przyszłości.
Klasy umożliwiają sensowne grupowanie atrybutów i metod w jednym miejscu, co sprzyja organizacji kodu i ułatwia odnalezienie potrzebnych fragmentów. Dzięki temu, gdy chcemy np. dodać nowe funkcjonalności lub naprawić błąd, szybko zlokalizujemy odpowiednią sekcję kodu.
Przykład: W aplikacji do zarządzania biblioteką możemy mieć klasę Ksiazka
, która łączy atrybuty (tytuł, autor, ISBN) z metodami (wypożycz, zwróć). Dzięki temu dokładnie wiadomo, gdzie szukać logiki związanej z obsługą książek.
class Ksiazka:
def __init__(self, tytul, autor, isbn):
self.tytul = tytul
self.autor = autor
self.isbn = isbn
def wypozycz(self):
print(f'Książka "{self.tytul}" została wypożyczona.')
def zwroc(self):
print(f'Książka "{self.tytul}" została zwrócona.')
Klasy można traktować jak uniwersalne „matryce”. Raz zdefiniowaną klasę da się wykorzystać wielokrotnie, tworząc dowolną liczbę obiektów o podobnym wzorcu, ale różniących się konkretnymi wartościami atrybutów. Minimalizuje to duplikację kodu i ułatwia jego konserwację.
ksiazka1 = Ksiazka("Pan Tadeusz", "Adam Mickiewicz", "1234567890")
ksiazka2 = Ksiazka("Lalka", "Bolesław Prus", "0987654321")
ksiazka1.wypozycz()
ksiazka2.zwroc()
Klasy dają możliwość ukrywania wewnętrznych szczegółów implementacji, tzn. to, jak dokładnie coś jest zrobione w środku, może być niedostępne lub niewidoczne dla zewnętrznego kodu. Dzięki temu możemy modyfikować wnętrze klasy, nie narażając istniejącego kodu na błędy z powodu zmian. Użytkownicy klasy nadal będą się nią posługiwać w taki sam sposób (ten sam interfejs).
class Ksiazka:
def __init__(self, tytul, autor, isbn):
self.tytul = tytul
self.autor = autor
self.isbn = isbn
self._wypozyczona = False
def wypozycz(self):
if not self._wypozyczona:
self._wypozyczona = True
print(f'Książka "{self.tytul}" została wypożyczona.')
else:
print(f'Książka "{self.tytul}" jest już wypożyczona.')
def zwroc(self):
if self._wypozyczona:
self._wypozyczona = False
print(f'Książka "{self.tytul}" została zwrócona.')
else:
print(f'Książka "{self.tytul}" nie była wypożyczona.')
W tym kodzie klasa sama decyduje, jaką logikę zastosować przy wypożyczeniu lub zwróceniu książki. Zewnętrzny kod jedynie wywołuje odpowiednie metody, nie martwiąc się o status wewnętrznych pól obiektu.
Dziedziczenie pozwala tworzyć nowe klasy na podstawie już istniejących. Nowa klasa (klasa pochodna) „dziedziczy” atrybuty i metody po klasie bazowej, co pozwala uniknąć dublowania kodu. Można wtedy dodawać nowe funkcjonalności lub nadpisywać już istniejące.
class Ebook(Ksiazka):
def __init__(self, tytul, autor, isbn, rozmiar_pliku):
super().__init__(tytul, autor, isbn)
self.rozmiar_pliku = rozmiar_pliku
def pobierz(self):
print(f'Pobieranie e-booka "{self.tytul}". Rozmiar pliku: {self.rozmiar_pliku}MB')
Dzięki temu klasa Ebook
przejmuje już istniejącą logikę z klasy Ksiazka
(jak np. metody wypozycz
czy zwroc
), a oprócz tego wprowadza nowe metody i atrybuty specyficzne dla e-booków (np. rozmiar pliku, metoda pobierania).
Polimorfizm oznacza, że różne obiekty mogą udostępniać wspólny interfejs, ale realizować go na swój własny sposób. Możemy wyobrazić sobie dwie klasy: Ksiazka
i Ebook
. Obie mogą mieć metodę wypozycz
, która zachowuje się podobnie z punktu widzenia wywołującego kod, ale w praktyce może wykonywać nieco inne operacje (np. weryfikować dostępność egzemplarza fizycznego lub cyfrowego pliku).
ksiazka = Ksiazka("Pan Tadeusz", "Adam Mickiewicz", "1234567890")
ebook = Ebook("Lalka", "Bolesław Prus", "0987654321", 5)
ksiazka.wypozycz()
ebook.wypozycz()
ebook.pobierz()
W przykładzie widać, że z punktu widzenia zewnętrznego kodu obiekty ksiazka
i ebook
zachowują się w podobny sposób przy wypożyczaniu, choć wewnętrznie mogą funkcjonować inaczej.
W programowaniu obiektowym, oprócz typowych metod instancyjnych, mamy też metody i pola statyczne oraz klasowe. Podstawowa różnica polega na tym, że:
W Pythonie metody statyczne oznaczamy dekoratorem @staticmethod
. Takie metody nie przyjmują jako pierwszego parametru self
ani cls
. Mogą być wywoływane zarówno poprzez nazwę klasy, jak i poprzez instancję:
class Czlowiek:
liczba_glow = 1
@staticmethod
def wyswietl_glowy():
print(f'Liczba głów: {Czlowiek.liczba_glow}')
Czlowiek.wyswietl_glowy() # Liczba głów: 1
przykladowy_czlowiek = Czlowiek()
przykladowy_czlowiek.wyswietl_glowy() # Liczba głów: 1
W tym przykładzie:
liczba_glow
jest polem klasowym (jest przypisane do klasy Czlowiek
, a nie do konkretnego obiektu).wyswietl_glowy
jest metodą statyczną, która bezpośrednio sięga do Czlowiek.liczba_glow
.Metody klasowe wykorzystują dekorator @classmethod
i otrzymują jako pierwszy argument cls
, czyli referencję do samej klasy (odpowiednik self
dla instancji). Metody klasowe mogą zatem modyfikować pola klasowe i wywoływać inne metody klasowe:
class Czlowiek:
liczba_glow = 1
@classmethod
def wyswietl_glowy(cls):
print(f'Liczba głów: {cls.liczba_glow}') # Używamy cls
, zamiast nazwy klasy
def zwykla_funkcja(self):
self.wyswietl_glowy()
Czlowiek.wyswietl_glowy() # Liczba głów: 1
przykladowy_czlowiek = Czlowiek()
przykladowy_czlowiek.wyswietl_glowy() # Liczba głów: 1
przykladowy_czlowiek.zwykla_funkcja() # Liczba głów: 1
Jak widać, metoda klasowa może być wywoływana zarówno poprzez klasę, jak i przez obiekt. W praktyce najczęściej używa się jej bezpośrednio z poziomu klasy.
self
.self.__class__
.Przykład:
class Samochod:
def __init__(self, marka, model):
self.marka = marka
self.model = model
def przedstaw_sie(self):
print(f"Samochód: {self.marka} {self.model}")
auto = Samochod("Toyota", "Corolla")
auto.przedstaw_sie() # Samochód: Toyota Corolla
cls
, będący referencją do klasy.Przykład:
class Samochod:
liczba_kol = 4
@classmethod
def wyswietl_liczbe_kol(cls):
print(f"Samochody mają {cls.liczba_kol} koła")
Samochod.wyswietl_liczbe_kol() # Samochody mają 4 koła
self
czy cls
.Przykład:
class Samochod:
@staticmethod
def informacje_o_samochodach():
print("Samochody to pojazdy mechaniczne służące do transportu")
Samochod.informacje_o_samochodach() # Samochody to pojazdy mechaniczne służące do transportu
Wykorzystanie pól i metod statycznych/klasowych jest szczególnie przydatne, gdy chcemy:
Poniższy przykład pokazuje, jak używając metody klasowej, można zliczać liczbę utworzonych instancji:
class Osoba:
liczba_instancji = 0
def __init__(self, imie):
self.imie = imie
Osoba.liczba_instancji += 1
@classmethod
def ile_instancji(cls):
return cls.liczba_instancji
osoba1 = Osoba("Jan")
osoba2 = Osoba("Anna")
print(Osoba.ile_instancji()) # 2
Tutaj każdorazowe stworzenie nowej osoby zwiększa licznik instancji. Metoda ile_instancji
jest metodą klasową i pozwala w prosty sposób odczytać, ile obiektów powstało.
Przykładem może być klasa zarządzająca dostępem do bazy danych. Zamiast za każdym razem tworzyć nowe połączenie, klasa może mieć jedną statyczną lub klasową zmienną, z której będą korzystać wszystkie instancje. Dzięki temu unika się wielokrotnych, kosztownych operacji na zasobach.
class BazaDanych:
polaczenie = None
@classmethod
def polacz(cls):
if cls.polaczenie is None:
cls.polaczenie = "Połączenie do bazy danych"
return cls.polaczenie
polaczenie1 = BazaDanych.polacz()
polaczenie2 = BazaDanych.polacz()
print(polaczenie1) # Połączenie do bazy danych
print(polaczenie1 is polaczenie2) # True
W tym kodzie metoda klasowa polacz
sprawdza, czy połączenie już istnieje. Jeśli nie, tworzy je. W przeciwnym wypadku zwraca istniejące połączenie. Dzięki temu zaoszczędzamy zasoby i zapobiegamy niekontrolowanemu namnażaniu połączeń.
Wzorce projektowe, takie jak Singleton, często korzystają z cech języka obiektowego (w tym pól lub metod statycznych i klasowych), by kontrolować powstawanie nowych instancji klas. Poniższy przykład pokazuje klasyczną implementację wzorca Singleton w Pythonie:
class Singleton:
_instancja = None
def __new__(cls, *args, **kwargs):
if not cls._instancja:
cls._instancja = super(Singleton, cls).__new__(cls, *args, **kwargs)
return cls._instancja
s1 = Singleton()
s2 = Singleton()
print(s1 is s2) # True
W momencie tworzenia nowego obiektu Singleton
sprawdza, czy _instancja
już istnieje. Jeśli nie, tworzy ją, a jeśli tak, zwraca referencję do istniejącego obiektu. W efekcie w całym programie istnieje tylko jeden obiekt tej klasy.