13. Python – obiektowo


Programowanie obiektowe – OOP (object-oriented programming) – pozwala na przedstawienie rzeczywistości i relacji w niej zachodzących za pomocą obiektów. Dokumentacja.

Po co nam programowanie obiektowe?

Programowanie obiektowe (ang. Object-Oriented Programming, OOP) wprowadza strukturę i organizację do kodu, co ułatwia jego tworzenie, rozwijanie i utrzymanie. Poniżej znajdziesz wyjaśnienie, po co stosować programowanie obiektowe, z przykładami jego głównych zalet:


1. Organizacja i modularność

  • OOP pozwala dzielić kod na mniejsze, logiczne fragmenty (klasy), które odpowiadają rzeczywistym obiektom lub pojęciom.
  • Dzięki temu kod jest bardziej przejrzysty i łatwiejszy w zarządzaniu.

Przykład: W grze komputerowej można mieć klasy Gracz, Przeciwnik, Broń. Każda z nich zawiera tylko dane i funkcje związane z tym obiektem.


2. Reużywalność kodu

  • Możesz tworzyć klasy i wykorzystywać je wielokrotnie w różnych projektach lub częściach programu.
  • Dziedziczenie umożliwia tworzenie nowych klas na podstawie istniejących, dzięki czemu kod jest bardziej elastyczny.

Przykład: Klasa Pojazd może mieć podklasy Samochód, Motocykl, które dziedziczą podstawowe cechy pojazdu (np. prędkość, masa), ale dodają swoje specyficzne funkcje.


3. Łatwiejsze rozwijanie aplikacji

  • W OOP zmiany są łatwiejsze do wprowadzenia, ponieważ każda klasa działa jako oddzielny moduł.
  • Jeśli potrzebujesz zmienić zachowanie jednego elementu, modyfikujesz tylko odpowiednią klasę, bez wpływu na inne części programu.

Przykład: Dodanie nowej broni w grze wymaga jedynie stworzenia nowej klasy na podstawie istniejącego schematu, np. Broń.


4. Ukrywanie szczegółów (enkapsulacja)

  • OOP pozwala ukrywać szczegóły implementacji, dzięki czemu kod jest bardziej bezpieczny i trudniejszy do przypadkowego uszkodzenia.
  • Dane w klasach są chronione, a dostęp do nich odbywa się przez ściśle określone metody.

Przykład: W klasie KontoBankowe saldo konta jest ukryte, a użytkownik może je zmieniać tylko za pomocą metod wpłać() i wypłać().


5. Naturalne odwzorowanie rzeczywistości

  • OOP naśladuje sposób, w jaki myślimy o świecie, opierając się na obiektach, ich cechach i zachowaniach.
  • Dzięki temu kod staje się bardziej intuicyjny i łatwiejszy do zrozumienia.

Przykład: Klasa Kot ma cechy takie jak kolor futra (atrybuty) i umiejętności jak miauczenie (metody).


6. Łatwość debugowania i testowania

  • Dzięki temu, że klasy są niezależne, błędy w jednym obiekcie nie wpływają na inne.
  • Testowanie poszczególnych modułów (klas) jest prostsze niż testowanie całości kodu.

7. Polimorfizm – elastyczność w działaniu

  • Polimorfizm umożliwia różnym obiektom używanie tej samej metody, ale w różny sposób.
  • Pozwala to na pisanie bardziej uniwersalnego i elastycznego kodu.

Przykład: Metoda atakuj() może działać inaczej w klasie Rycerz i Mag, mimo że jest wywoływana w ten sam sposób.


8. Ułatwienie pracy zespołowej

  • W dużych projektach, podział kodu na klasy ułatwia współpracę, ponieważ każdy programista może pracować nad innymi obiektami, nie wchodząc sobie w drogę.

Podsumowanie

Programowanie obiektowe jest użyteczne, gdy:

  • Tworzysz duże, złożone systemy.
  • Chcesz pisać kod, który łatwo rozwijać, utrzymywać i testować.
  • Potrzebujesz odzwierciedlić rzeczywiste pojęcia lub obiekty w swoim kodzie.

Jeśli jednak tworzysz mały, prosty skrypt, nie zawsze konieczne jest stosowanie OOP – wtedy wystarczą inne podejścia, jak programowanie proceduralne.

Klasa i obiekt

Kiedy mówimy o klasie musimy wyobrazić ją sobie jak ogólny zarys/opis jakiegoś obiektu, zbiór cech wspólnych dla np. człowieka. Gdybyśmy mieli klasę człowiek moglibyśmy określić ogólne cechy jakie definiują ludzi, czyli np. to że śpi, że ma głos, oczy, włosy, ręce, nogi itd.

Obiekt jest instancją danej klasy, czyli konkretnym człowiekiem np. Janem Kowalskim, który ma brązowe włosy, jest typem „skowronka” i ma niebieskie oczy. Te cechy nie są bezpośrednio związane z klasą, a z obiektem (instancją/wystąpieniem klasy).

Można powiedzieć, że Jan Kowalski jest zmienną typu klasy, czyli obiektem typu człowiek.

Przykład utworzenia klasy:

#tworzymy klasę
class Test: #nazwa klasy wielką literą bez nawiasów
# własności, czyli zmienne
to_jest_wlasnosc = "jestem własnością klasy Test"
lista_test = ["kot", "pies", "królik"]
dict_test = {"k1": "v1", "k2": "v2"}
__prywatna = "nie ma do mnie dostępu z zewnątrz"

#tworzymy nowy obiekt na bazie klasy
nowy_obiekt = Test()

# wyświetlamy własności klasy, w Python nie ma modyfikatorów dostępu jeżeli chcemy zrobić wartość prywatną stawiamy 2 x podkreślnik np __nazwa

print(nowy_obiekt.to_jest_wlasnosc)
print(nowy_obiekt.lista_test)
print(nowy_obiekt.lista_test[2])
print(nowy_obiekt.dict_test)
print(nowy_obiekt.dict_test["k2"])
# print(nowy_obiekt._prywatna) #to se ne da
  

Metody

Klasa może definiować metody, czyli nic innego jak funkcje z podejścia proceduralnego. Kiedy tworzymy obiekt nadajemy mu dostęp do metod danej klasy.

Trochę praktyki:

#tworzymy klasę
class Test:
# własności, czyli zmienne
to_jest_wlasnosc = "jestem własnością klasy Test"
lista_test = ["kot", "pies", "królik"]
dict_test = {"k1": "v1", "k2": "v2"}
__prywatna = "nie ma do mnie dostępu z zewnątrz"

# metody, czyli funkcje   
def to_jest_metoda(self,):  # pierwszy parametr to zawsze self - to referencja do instancji obiektu
(tak jak $this-> w PHP) 
print(self.to_jest_wlasnosc)
 
def wyswietlam_private(self):       
print(self.__prywatna + ", ale ja działam w środku")   

# @property to dekorator, który mówi - nie traktuj mnie jak metodę, traktuj mnie jak własność. Dekorator @property pozwala traktować metodę jak atrybut, dzięki czemu możemy kontrolować dostęp do danych zachowując prostą składnię: obiekt.get_kot zamiast obiekt.get_kot()
@property
def get_kot(self):  # metoda zwróci nam własność       
return self.lista_test[0]   

# metoda z parametrami   
def add_test_to_list(self, name):       
self.lista_test.append(name)       
return self.lista_test   

def test(self):       
pass

#tworzymy nowy obiekt na bazie klasy
nowy_obiekt = Test()

# wyświetlamy własności klasy, w Python nie ma modyfikatorów dostępu jeżeli chcemy zrobić wartość prywatną stawiamy 2 x podłogę np __nazwa

print(nowy_obiekt.to_jest_wlasnosc)

print(nowy_obiekt.lista_test)
print(nowy_obiekt.lista_test[2])

print(nowy_obiekt.dict_test)
print(nowy_obiekt.dict_test["k2"])

# print(nowy_obiekt.__prywatna) #to spowoduje błąd AttributeError

nowy_obiekt.to_jest_metoda()
nowy_obiekt.wyswietlam_private()  # tak już możemy wyświetlić coś prywatnego
kot = nowy_obiekt.get_kot  # nie podaję() bo otrzymam wartość
print("Nie jestem właścicielką", kot)
nowy_obiekt.add_test_to_list("rybki")  #dodajemy zwierzę do listy
print(nowy_obiekt.lista_test)

Metody magiczne – czyli konstruktor, destruktor i reszta – dunder methods

Metody magiczne to nic innego jak funkcje, które należą do danej klasy. Charakteryzują się zapisem, który rozpoczyna się od dwóch znaków podkreślnika i kończy dwoma znakami podkreślnika np. __nazwa__

dunder = double underscores

 __new__(cls, ...)

metoda jest wywoływana podczas tworzenia nowych obiektów(tworzenia nowej instancji klasy) jako pierwsza, przed inną z magicznych metod czyli __init__ i jej zadaniem jest stworzenie i zwrócenie nowej instancji danej klasy, która to zaraz potem jest przekazywana do metody __init__ jako pierwszy argument (self).

__init__(self, ...)

najbardziej znana z magicznych metod, nazywana jest konstruktorem. Jej zadaniem jest inicjalizacja obiektu z pomocą argumentów przekazanych przy wywołaniu klasy. Uruchamiana jest automatycznie podczas tworzenia nowego obiektu.

__del__(self)

wywoływana jest, gdy obiekt kończy swój cykl życia i za zadanie ma zrobienie porządku po nim. Nazywana jest destruktorem.

__str__(self) i __repr__(self)

__str__ – zwraca „ładną”, czytelną dla użytkownika reprezentację obiektu
__repr__ – zwraca techniczną reprezentację obiektu, idealna do debugowania

class Ksiazka:
def __init__(self, tytul, autor, rok): self.tytul = tytul self.autor = autor self.rok = rok def __str__(self): # To zobaczy użytkownik return f'"{self.tytul}" - {self.autor} ({self.rok})' def __repr__(self): # To zobaczy programista podczas debugowania return f'Ksiazka(tytul="{self.tytul}", autor="{self.autor}", rok={self.rok})' ksiazka = Ksiazka("Wiedźmin", "Andrzej Sapkowski", 1990) print(str(ksiazka)) # "Wiedźmin" - Andrzej Sapkowski (1990) print(repr(ksiazka)) # Ksiazka(tytul="Wiedźmin", autor="Andrzej Sapkowski", rok=1990) print(ksiazka)

 __eq__ i inne operatory porównania

Pozwalają porównywać obiekty za pomocą operatorów ==, <, > itp.

class Osoba:
    def __init__(self, imie, wiek):
        self.imie = imie
        self.wiek = wiek
    
    def __eq__(self, other):
        # Definiuje zachowanie operatora ==
        if isinstance(other, Osoba):
            return self.wiek == other.wiek
        return False
    
    def __lt__(self, other):
        # Definiuje zachowanie operatora < (less than)
        if isinstance(other, Osoba):
            return self.wiek < other.wiek
        return NotImplemented
    
    def __gt__(self, other):
        # Definiuje zachowanie operatora > (greater than)
        if isinstance(other, Osoba):
            return self.wiek > other.wiek
        return NotImplemented
    
    def __str__(self):
        return f"{self.imie} ({self.wiek} lat)"


anna = Osoba("Anna", 25)
jan = Osoba("Jan", 30)
ola = Osoba("Ola", 25)

print(anna == ola)  # True (ten sam wiek)
print(anna == jan)  # False
print(anna < jan)   # True (Anna jest młodsza)
print(jan > anna)   # True (Jan jest starszy)

 __len__

Pozwala użyć funkcji len() na Twoim obiekcie.

class Playlista:
    def __init__(self, nazwa):
        self.nazwa = nazwa
        self.utwory = []
    
    def dodaj_utwor(self, utwor):
        self.utwory.append(utwor)
    
    def __len__(self):
        return len(self.utwory)
    
    def __str__(self):
        return f'Playlista "{self.nazwa}" ({len(self)} utworów)'


moja_playlista = Playlista("Ulubione")
moja_playlista.dodaj_utwor("Piosenka 1")
moja_playlista.dodaj_utwor("Piosenka 2")
moja_playlista.dodaj_utwor("Piosenka 3")

print(len(moja_playlista))  # 3
print(moja_playlista)       # Playlista "Ulubione" (3 utworów)

 __getitem__ i __setitem__

Pozwalają używać nawiasów kwadratowych [] do dostępu do elementów.

class Slownik:
    def __init__(self):
        self.dane = {}
    
    def __getitem__(self, klucz):
        # Pozwala na: obiekt[klucz]
        return self.dane.get(klucz, "Brak wartości")
    
    def __setitem__(self, klucz, wartosc):
        # Pozwala na: obiekt[klucz] = wartosc
        self.dane[klucz] = wartosc
    
    def __str__(self):
        return str(self.dane)


slownik = Slownik()
slownik["imie"] = "Anna"        # Wywołuje __setitem__
slownik["miasto"] = "Warszawa"

print(slownik["imie"])          # Wywołuje __getitem__ -> "Anna"
print(slownik["nieistniejacy"]) # "Brak wartości"
print(slownik)                  # {'imie': 'Anna', 'miasto': 'Warszawa'}

 __add__, __sub__, __mul__ – operatory matematyczne

class Wektor:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __add__(self, other):
        # Definiuje operator +
        if isinstance(other, Wektor):
            return Wektor(self.x + other.x, self.y + other.y)
        return NotImplemented
    
    def __mul__(self, skalar):
        # Definiuje operator *
        if isinstance(skalar, (int, float)):
            return Wektor(self.x * skalar, self.y * skalar)
        return NotImplemented
    
    def __str__(self):
        return f"Wektor({self.x}, {self.y})"


v1 = Wektor(1, 2)
v2 = Wektor(3, 4)

v3 = v1 + v2        # Wektor(4, 6)
v4 = v1 * 3         # Wektor(3, 6)

print(v3)
print(v4)

 __call__ – obiekt jako funkcja

class Powitanie:
    def __init__(self, jezyk="polski"):
        self.jezyk = jezyk
    
    def __call__(self, imie):
        # Pozwala wywoływać obiekt jak funkcję
        if self.jezyk == "polski":
            return f"Cześć, {imie}!"
        elif self.jezyk == "angielski":
            return f"Hello, {imie}!"
        else:
            return f"Witaj, {imie}!"


powitanie_pl = Powitanie("polski")
powitanie_en = Powitanie("angielski")

# Wywołujemy obiekty jak funkcje!
print(powitanie_pl("Anna"))  # Cześć, Anna!
print(powitanie_en("John"))  # Hello, John!

 Tabela najważniejszych metod specjalnych:

MetodaOpisPrzykład użycia
__init__Konstruktorobj = Klasa()
__str__Reprezentacja tekstowaprint(obj), str(obj)
__repr__Reprezentacja dla debugowaniarepr(obj)
__len__Długość obiektulen(obj)
__eq__Równośćobj1 == obj2
__lt__Mniejsze niżobj1 < obj2
__gt__Większe niżobj1 > obj2
__add__Dodawanieobj1 + obj2
__sub__Odejmowanieobj1 - obj2
__mul__Mnożenieobj * 5
__getitem__Dostęp przez indeksobj[key]
__setitem__Ustawianie przez indeksobj[key] = value
__call__Wywołanie jak funkcjaobj()

Odsyłam Was do fajnego artykułu Magiczne metody – czyli szczypta magii w Pythonie.

Modyfikatory dostępu

W Python nie ma modyfikatorów dostępu, możemy użyć „_” lub „__” – pojedynczy podkreślnik to uzgodnienia programistów żeby nie ingerować w taką własność lub metodę, użycie podwójnego podkreślnika (np. __moja_metoda) powoduje, że Python zmienia jej nazwę w taki sposób, że dodaje do niej nazwę klasy w postaci prefiksu. Na przykład, jeśli mamy klasę MojaKlasa i zdefiniujemy w niej metodę __moja_metoda, Python zmieni jej nazwę na _MojaKlasa__moja_metoda. Dzięki temu unika się konfliktów nazw w przypadku dziedziczenia.

Ale trochę praktyki:

class MojaKlasa:
def __init__(self):
self.publiczne_pole = 42
self.__ukryte_pole = 24

def publiczna_metoda(self):
print("To jest publiczna metoda.")

def __ukryta_metoda(self):
print("To jest ukryta metoda.")

obiekt = MojaKlasa()
print(obiekt.publiczne_pole) # Możemy odczytać publiczne pole
print(obiekt.__ukryte_pole) # To spowoduje błąd, ale można odczytać to pole inaczej
obiekt.publiczna_metoda() # Możemy wywołać publiczną metodę obiekt.__ukryta_metoda() # To spowoduje błąd, ale można wywołać tę metodę inaczej

Jest jednak sposób na dostęp do tych „ukrytych” pól i metod. Python faktycznie zmienia ich nazwy, dodając przed nimi nazwę klasy, w której się znajdują, co sprawia, że stają się trudniejsze do dostępu. Na przykład, pole __ukryte_pole zostanie zmienione na _MojaKlasa__ukryte_pole. Możesz uzyskać dostęp do tych elementów w ten sposób:

print(obiekt._MojaKlasa__ukryte_pole)
obiekt._MojaKlasa__ukryta_metoda();

Jednak w praktyce zaleca się unikania dostępu do ukrytych pól i metod, ponieważ łamie to konwencję i może prowadzić do problemów w przyszłości. Lepiej jest trzymać się konwencji i zakładać, że te elementy są przeznaczone do użytku wewnętrznego w ramach klasy lub modułu.

Dziedziczenie

Tak jak w życiu – jako potomkowie dziedziczymy po naszych przodkach pewne cechy (tak naprawdę to 50% od matki i 50% od ojca), tak samo klasy dziedziczą pola i metody, ale mogą zostać uzupełnione o inne elementy, których przodek nie posiada.

Aby klasa dziedziczyła po innej należy w nawiasie podać nazwę klasy przodka:

class Zwierze:
    rodzaj= "lądowe"
    def odglos(self):
        return "brak"

class Pies(Zwierze):
    def odglos(self):
        return "hał"

class Ryba(Zwierze):
  rodzaj= "wodne"
    def odglos(self):
        return "bulbul"

pies = Pies()

ryba = Ryba()

print(f"""Odgłos psa to {pies.odglos()}, należy do rodzaju: {pies.rodzaj}.
Odgłos ryby to {ryba.odglos()}, należy do rodzaju: {ryba.rodzaj}.""")

Python obsługuje również formę wielokrotnego dziedziczenia. Definicja klasy zawierająca wiele klas bazowych wygląda następująco:

class DerivedClassName(Base1, Base2, Base3):
pass

W większości przypadków, można myśleć o wyszukiwaniu atrybutów odziedziczonych z klasy nadrzędnej jako o wyszukiwaniu w głąb, od lewej do prawej, a nie o wyszukiwaniu dwukrotnie w tej samej klasie, gdzie hierarchia się nakłada. Zatem jeśli atrybut nie zostanie znaleziony w DerivedClassName, jest on wyszukiwany w Base1, następnie (rekurencyjnie) w bazowych klasach Base1, a jeśli tam go nie znaleziono, szukano go w Base2 i tak dalej.

Poniżej znajduje się przykład z wykorzystaniem dziedziczenia, a w tym:

  • konstruktora(__init__) – do którego przekazywane są argumenty
  • super() – który odwoła się do konstruktora klasy bazowej, a w ogólnym użyciu pozwala odwołać się do każdej metody przodka
  • isinstance – pozwala sprawdzić czy obiekt jest instancją danej klasy, zwraca True lub False
class Budynek:

    def __init__(self, rok_budowy):
        self._rok_budowy = rok_budowy
        print(f"Utworzono obiekt {self}.")

    def rok_budowy(self):
        return self._rok_budowy

class Szpital(Budynek):

    def __init__(self, rok_budowy):
        super().__init__(rok_budowy)
        self._kondygnacje = "Budynek szpitala 2 kondygnacyjny"

    def info(self):
        print(f"Metoda w klasie {__class__.__name__}")
        print(f"Obiekt {self} - kondygnacje: {self._kondygnacje}")
        print(f"Rok budowy = {super().rok_budowy()}")

    def test(self):
        print("To jest wywołanie metody z klasy nadrzędnej w klasie Szpital")
        print(f"rok_budowy = {super().rok_budowy()}")

class Blok(Budynek):

    def __init__(self, rok_budowy):
        super().__init__(rok_budowy)
        self._kondygnacje = "Blok 5 kondygnacji"

    def info(self):
        print(f"Metoda w klasie {__class__.__name__}")
        print(f"Obiekt {self} - kondygnacje: {self._kondygnacje}")
        print(f"Rok budowy = {super().rok_budowy()}")

    def test(self):
        print("To jest wywołanie metody z klasy nadrzędnej w klasie Blok")
        print(f"rok_budowy = {super().rok_budowy()}")

# definiujemy obiekty oraz tworzymy listę

budynek01 = Blok(2001)
budynek02 = Blok(2002)

budynek03 = Szpital(2010)
budynek04 = Szpital(2012)

"""
Utworzono obiekt <__main__.Blok object at 0x0000026706EDAED0>.
Utworzono obiekt <__main__.Blok object at 0x0000026706EDBD90>.
Utworzono obiekt <__main__.Szpital object at 0x0000026706EEC190>.
Utworzono obiekt <__main__.Szpital object at 0x0000026706EEC1D0>.
"""

spis_budynkow = [budynek01, budynek02, budynek03, budynek04,]

for budynek in spis_budynkow:
  print(f"isinstance({budynek}, Szpital) = {isinstance(budynek, Szpital)}")
  print(f"isinstance({budynek}, Blok) = {isinstance(budynek, Blok)}")
  print(f"isinstance({budynek}, Budynek) = {isinstance(budynek, Budynek)}")  
    print("----------")
"""
isinstance(<__main__.Blok object at 0x0000026706EDAED0>, Szpital) = False
isinstance(<__main__.Blok object at 0x0000026706EDAED0>, Blok) = True
isinstance(<__main__.Blok object at 0x0000026706EDAED0>, Budynek) = True
----------
isinstance(<__main__.Blok object at 0x0000026706EDBD90>, Szpital) = False
isinstance(<__main__.Blok object at 0x0000026706EDBD90>, Blok) = True
isinstance(<__main__.Blok object at 0x0000026706EDBD90>, Budynek) = True
----------
isinstance(<__main__.Szpital object at 0x0000026706EEC190>, Szpital) = True
isinstance(<__main__.Szpital object at 0x0000026706EEC190>, Blok) = False
isinstance(<__main__.Szpital object at 0x0000026706EEC190>, Budynek) = True
----------
isinstance(<__main__.Szpital object at 0x0000026706EEC1D0>, Szpital) = True
isinstance(<__main__.Szpital object at 0x0000026706EEC1D0>, Blok) = False
isinstance(<__main__.Szpital object at 0x0000026706EEC1D0>, Budynek) = True
"""

for budynek in spis_budynkow:
    print("-----------------------------")
    budynek.test()
    print("-----")
    budynek.info()

"""
-----------------------------
To jest wywołanie metody z klasy nadrzędnej w klasie Blok
rok_budowy = 2001
-----
Metoda w klasie Blok
Obiekt <__main__.Blok object at 0x0000026706EDAED0> - kondygnacje: Blok 5 kondygnacji
Rok budowy = 2001
-----------------------------
To jest wywołanie metody z klasy nadrzędnej w klasie Blok
rok_budowy = 2002
-----
Metoda w klasie Blok
Obiekt <__main__.Blok object at 0x0000026706EDBD90> - kondygnacje: Blok 5 kondygnacji
Rok budowy = 2002
-----------------------------
To jest wywołanie metody z klasy nadrzędnej w klasie Szpital
rok_budowy = 2010
-----
Metoda w klasie Szpital
Obiekt <__main__.Szpital object at 0x0000026706EEC190> - kondygnacje: Budynek szpitala 2 kondygnacyjny
Rok budowy = 2010
-----------------------------
To jest wywołanie metody z klasy nadrzędnej w klasie Szpital
rok_budowy = 2012
-----
Metoda w klasie Szpital
Obiekt <__main__.Szpital object at 0x0000026706EEC1D0> - kondygnacje: Budynek szpitala 2 kondygnacyjny
Rok budowy = 2012
"""

Kompozycja vs Dziedziczenie

Czym jest kompozycja?

Kompozycja to technika budowania złożonych obiektów z prostszych, poprzez zawieranie innych obiektów jako atrybutów. Zamiast mówić „klasa A jest klasą B” (dziedziczenie), mówimy „klasa A ma obiekt B” (kompozycja).

Kiedy używać kompozycji, a kiedy dziedziczenia?

Używaj dziedziczenia, gdy:

  • Istnieje relacja „jest” (is-a): Pies jest Zwierzęciem
  • Klasa potomna jest specjalizacją klasy bazowej
  • Chcesz współdzielić wspólny interfejs

Używaj kompozycji, gdy:

  • Istnieje relacja „ma” (has-a): Samochód ma Silnik
  • Potrzebujesz funkcjonalności z wielu źródeł
  • Chcesz większej elastyczności

Przykład 1: Problem z dziedziczeniem

# ZŁE PODEJŚCIE - nadmierne dziedziczenie
class Zwierze:
    def jedz(self):
        print("Jem")

class Ptak(Zwierze):
    def lataj(self):
        print("Lecę")

class Pingwin(Ptak):
    # Problem! Pingwin nie lata, ale dziedziczy metodę lataj()
    pass

# Pingwin dziedziczy lataj(), mimo że nie powinien latać

Przykład 2: Rozwiązanie z kompozycją

# DOBRE PODEJŚCIE - kompozycja

class Latanie:
    """Komponent odpowiedzialny za latanie"""
    def lataj(self):
        print("Lecę w powietrzu")

class Plywanie:
    """Komponent odpowiedzialny za pływanie"""
    def plywaj(self):
        print("Pływam w wodzie")

class Zwierze:
    def __init__(self, nazwa):
        self.nazwa = nazwa
    
    def jedz(self):
        print(f"{self.nazwa} je")

class Orzel(Zwierze):
    def __init__(self, nazwa):
        super().__init__(nazwa)
        # Orzeł MA zdolność latania (kompozycja)
        self.latanie = Latanie()
    
    def wykonaj_lot(self):
        self.latanie.lataj()

class Pingwin(Zwierze):
    def __init__(self, nazwa):
        super().__init__(nazwa)
        # Pingwin MA zdolność pływania (kompozycja)
        self.plywanie = Plywanie()
    
    def wykonaj_plywanie(self):
        self.plywanie.plywaj()

class Kaczka(Zwierze):
    def __init__(self, nazwa):
        super().__init__(nazwa)
        # Kaczka MA obie zdolności!
        self.latanie = Latanie()
        self.plywanie = Plywanie()
    
    def wykonaj_lot(self):
        self.latanie.lataj()
    
    def wykonaj_plywanie(self):
        self.plywanie.plywaj()


# Użycie:
orzel = Orzel("Bielik")
orzel.jedz()           # Bielik je
orzel.wykonaj_lot()    # Lecę w powietrzu

pingwin = Pingwin("Pongo")
pingwin.jedz()              # Pongo je
pingwin.wykonaj_plywanie()  # Pływam w wodzie

kaczka = Kaczka("Donald")
kaczka.jedz()              # Donald je
kaczka.wykonaj_lot()       # Lecę w powietrzu
kaczka.wykonaj_plywanie()  # Pływam w wodzie

Przykład 3: Samochód – praktyczny przykład kompozycji

class Silnik:
    def __init__(self, moc, pojemnosc):
        self.moc = moc
        self.pojemnosc = pojemnosc
        self.uruchomiony = False
    
    def uruchom(self):
        if not self.uruchomiony:
            self.uruchomiony = True
            print(f"Silnik {self.pojemnosc}L uruchomiony")
        else:
            print("Silnik już pracuje")
    
    def zatrzymaj(self):
        if self.uruchomiony:
            self.uruchomiony = False
            print("Silnik zatrzymany")

class Kola:
    def __init__(self, rozmiar):
        self.rozmiar = rozmiar
    
    def informacja(self):
        return f"Koła {self.rozmiar} cali"

class Nawigacja:
    def __init__(self):
        self.aktywna = False
    
    def wlacz(self):
        self.aktywna = True
        print("Nawigacja włączona")
    
    def wyznacz_trase(self, cel):
        if self.aktywna:
            print(f"Wyznaczam trasę do: {cel}")
        else:
            print("Najpierw włącz nawigację")

class Samochod:
    def __init__(self, marka, model):
        self.marka = marka
        self.model = model
        # Kompozycja - samochód SKŁADA SIĘ z innych obiektów
        self.silnik = Silnik(moc=150, pojemnosc=2.0)
        self.kola = Kola(rozmiar=17)
        self.nawigacja = Nawigacja()
    
    def uruchom_samochod(self):
        print(f"\n--- Uruchamianie {self.marka} {self.model} ---")
        self.silnik.uruchom()
        print(self.kola.informacja())
    
    def jedz_do(self, miejsce):
        if self.silnik.uruchomiony:
            self.nawigacja.wlacz()
            self.nawigacja.wyznacz_trase(miejsce)
            print(f"Jadę do: {miejsce}")
        else:
            print("Najpierw uruchom samochód")
    
    def zatrzymaj_samochod(self):
        print(f"\n--- Zatrzymywanie {self.marka} {self.model} ---")
        self.silnik.zatrzymaj()


# Użycie:
moj_samochod = Samochod("Toyota", "Corolla")
moj_samochod.uruchom_samochod()
# --- Uruchamianie Toyota Corolla ---
# Silnik 2.0L uruchomiony
# Koła 17 cali

moj_samochod.jedz_do("Warszawa")
# Nawigacja włączona
# Wyznaczam trasę do: Warszawa
# Jadę do: Warszawa

moj_samochod.zatrzymaj_samochod()
# --- Zatrzymywanie Toyota Corolla ---
# Silnik zatrzymany

Zalety kompozycji:

  1. Większa elastyczność – możesz łatwo zmieniać komponenty
  2. Łatwiejsze testowanie – każdy komponent można testować osobno
  3. Unikanie problemów z wielodziedziczeniem
  4. Luźniejsze powiązania – komponenty są niezależne
  5. Łatwiejsze ponowne użycie – komponenty działają w różnych kontekstach

Wady kompozycji:

  1. Więcej kodu do napisania (więcej klas)
  2. Może być mniej intuicyjna niż dziedziczenie dla prostych relacji
  3. Wymaga świadomego projektowania struktury

Złota zasada:

„Prefer composition over inheritance”
(Preferuj kompozycję nad dziedziczeniem)

Nie oznacza to, że dziedziczenie jest złe! Po prostu w wielu przypadkach kompozycja daje lepsze rezultaty. Używaj dziedziczenia, gdy relacja „jest” (is-a) ma sens, a kompozycji, gdy relacja „ma” (has-a) lepiej opisuje sytuację.


Praktyczne ćwiczenie

Spróbuj przeprojektować poniższy przykład używając kompozycji:

# Aktualny kod z dziedziczeniem:
class Pracownik:
    def pracuj(self):
        print("Pracuję")

class PracownikZSamochodem(Pracownik):
    def jedz_do_pracy(self):
        print("Jadę samochodem")

class PracownikZRowerem(Pracownik):
    def jedz_do_pracy(self):
        print("Jadę rowerem")

# Jak to przerobić na kompozycję?
# Zastanów się: czy pracownik JEST samochodem, czy MA samochód?

Rozwiązanie:

class Samochod: 
    def jedz(self): 
        print("Jadę samochodem") 

class Rower: 
    def jedz(self): 
        print("Jadę rowerem") 

class Pracownik: 
    def __init__(self, imie, srodek_transportu=None): 
        self.imie = imie 
        self.srodek_transportu = srodek_transportu 
        
        def pracuj(self): 
            print(f"{self.imie} pracuje") 
        
        def jedz_do_pracy(self): 
            if self.srodek_transportu: 
                self.srodek_transportu.jedz() 
            else: 
                print(f"{self.imie} idzie pieszo") 
    
# Użycie - teraz możemy łatwo zmieniać środek transportu! jan = Pracownik("Jan", Samochod()) anna = Pracownik("Anna", Rower()) piotr = Pracownik("Piotr") # Bez środka transportu jan.jedz_do_pracy() # Jadę samochodem anna.jedz_do_pracy() # Jadę rowerem piotr.jedz_do_pracy() # Piotr idzie pieszo

Klasy i metody abstrakcyjne

Klasa abstrakcyjna definiuje pewien model i zachowanie obiektu.

Taka klasa nie posiada definicji tych metod, jedynie ich deklaracje, że gdzieś dalej w kodzie muszą się pojawić. Czyli do czasu rozszerzenia ich o funkcjonalności w klasach dziedziczących, te metody nie istnieją (są abstrakcją). Klasa abstrakcyjna to taka klasa która służy do tego by z niej dziedziczyć i implementować te metody które oznaczone zostały jako abstrakcyjne.

Nie można utworzyć obiektu klasy abstrakcyjnej. Klasa ta jest jedynie „bazą” dla klas potomnych.

Wyobraźmy sobie, że mamy klasę Zwierze, na jej podstawi tworzymy konkretne gatunki. W tym kontekście klasa Zwierze jest abstrakcyjna, bo nie można utworzyć istoty, która jest ogólnie zwierzęciem. Zawsze będziemy mieli na myśli konkretny gatunek kot, pies itp., które będą posiadały ogólne cechy, ale również indywidualne dla każdego z nich, które zostaną zakodowane w klasach potomnych.

Mechanizm klas i metod abstrakcyjnych (klasa jest abstrakcyjna gdy ma co najmniej jedną metodę abstrakcyjną) w języku Python jest wprowadzony trochę sztucznie, bo klasa bazowa (abstrakcyjna) musi dziedziczyć po sztucznej klasie ABC, a metoda abstrakcyjna jest opatrzona dekoratorem @abstractmethod.

from abc import ABC, abstractmethod
class AbstractClass(ABC):
@abstractmethod
def method1(self):
pass

def method2(self):
return "Hello!"

def __str__(self):
        return f'Jestem {self.__class__.__name__}'

obj = AbstractClass() #TypeError: Can't instantiate abstract class AbstractObject with abstract method method1

class ConcreteClass(AbstractClass):
    def method1(self):
        print("Heeelo!")


print(ConcreteClass()) #to już się wykona

Interfejsy

W Pythonie nie ma dedykowanego słowa kluczowego „interface” jak w niektórych innych językach programowania (np. Java). W praktyce, interfejsy są często reprezentowane za pomocą klas abstrakcyjnych w Pythonie, a moduł abc (Abstract Base Classes) dostarcza mechanizmy do ich tworzenia.

Paradygmat

Z pojęciem programowania wiąże się paradygmat – jest to zbiór mechanizmów, używanych przez programistę podczas tworzenia kodu, a następnie wykonywany przez komputer. Paradygmat programowania obiektowego jest jak konstrukcja silnika z części składowych. Części to obiekty, które posiadają atrybuty i własności oraz współdziałają z innymi częściami.

Główne założenia programowania obiektowego to:

Abstrakcja

Pozwala programistom tworzyć obiekty, które ukrywają skomplikowane szczegóły i dostarczają prosty interfejs do korzystania z  tych obiektów. Dzięki temu programiści mogą skupić się na używaniu tych obiektów bez konieczności zrozumienia każdego detalu ich działania.

W praktyce używając smartfona, nie musisz znać dokładnych technicznych szczegółów dotyczących działania systemu operacyjnego czy aplikacji. Możesz korzystać z funkcji telefonu, przeglądać internet, rozmawiać z ludźmi i wiele innych rzeczy, nie zastanawiając się nad tym, jak to wszystko działa „pod maską”.

 

Dziedziczenie

Dziedziczenie to mechanizm w OOP, który umożliwia tworzenie nowych klas (klasy podrzędne lub potomne) na podstawie istniejących klas (klasy nadrzędne lub bazowe). Klasa potomna dziedziczy cechy (pola i metody) klasy nadrzędnej, co oznacza, że może korzystać z jej funkcjonalności. Dziedziczenie pozwala na tworzenie hierarchii klas, gdzie klasy potomne mogą rozszerzać lub modyfikować zachowanie klas nadrzędnych.

Przykład: Klasa „Samochód” może być klasą nadrzędną, a klasy „Sedan” i „SUV” mogą dziedziczyć od niej, dodając własne cechy specyficzne dla tych typów samochodów.

 

Enkapsulacja

Enkapsulacja to zasada OOP, która polega na ukrywaniu wewnętrznych danych obiektu oraz dostępu do nich i udostępnianiu tylko niezbędnych informacji. Klasa może ukrywać swoje pola (zmienne) przed innymi klasami i udostępniać dostęp do nich tylko za pomocą publicznych metod, co pozwala na kontrolowanie i zabezpieczanie danych. Enkapsulacja pomaga utrzymać spójność i integralność danych w programie.

Przykład: Możemy ukryć pole „saldo” w klasie „KontoBankowe” i dostarczyć publiczne metody do wpłacania i wypłacania pieniędzy.

 

Polimorfizm

Polimorfizm to zasada OOP, która pozwala na tworzenie wielu różnych klas, które posiadają wspólny interfejs, a jednocześnie mogą działać w różny sposób. Oznacza to, że różne obiekty mogą reagować na te same metody w zróżnicowany sposób, co jest użyteczne w dziedzinach, gdzie różne obiekty zachowują się inaczej. Istnieją dwa rodzaje polimorfizmu: polimorfizm klas (dziedziczenie i przesłanianie metod) oraz polimorfizm interfejsów (klasy implementujące wspólny interfejs).

Przykład: Mamy interfejs „Pojazd” i różne klasy, takie jak „Samochód” i „Rower”, które go implementują. Mogą być używane w ten sam sposób, choć działają inaczej.

Zadanie 1

Jesteś praktykantem w firmie zajmującej się tworzeniem witryn i aplikacji internetowych. Otrzymałeś zadanie polegające na stworzeniu skryptu w języku Python.

W skrypcie ma być utworzona klasa trójkąt, która zawiera dwa publiczne pola, takie jak: wysokość i podstawa, oraz konstruktor, który przypisze im losowo wygenerowane wartości.

Ponadto w klasie powinna być zadeklarowana metoda obliczająca pole trójkąta.
W aplikacji należy utworzyć dwa obiekty klasy trójkąt.

Wynikiem działania aplikacji ma być wyświetlona wartość wysokości, podstawy i pola powierzchni obu trójkątów oraz informacja, który z trójkątów ma większą powierzchnię.

print("=" * 60)
print("ZADANIE 1 - TRÓJKĄTY")
print("=" * 60)

import random

class Trojkat:
    def __init__(self):
        """Konstruktor generuje losowe wartości wysokości i podstawy"""
        self.wysokosc = random.randint(1, 20)
        self.podstawa = random.randint(1, 20)
    
    def oblicz_pole(self):
        """Metoda obliczająca pole trójkąta"""
        return (self.podstawa * self.wysokosc) / 2
    
    def __str__(self):
        """Reprezentacja tekstowa trójkąta"""
        return f"Trójkąt(podstawa={self.podstawa}, wysokość={self.wysokosc})"


# Tworzenie dwóch obiektów
trojkat1 = Trojkat()
trojkat2 = Trojkat()

# Obliczanie pól
pole1 = trojkat1.oblicz_pole()
pole2 = trojkat2.oblicz_pole()

# Wyświetlanie wyników
print(f"\n{trojkat1}")
print(f"Pole: {pole1} cm²")

print(f"\n{trojkat2}")
print(f"Pole: {pole2} cm²")

# Porównanie
if pole1 > pole2:
    print(f"\n✓ Trójkąt 1 ma większą powierzchnię ({pole1} cm² > {pole2} cm²)")
elif pole2 > pole1:
    print(f"\n✓ Trójkąt 2 ma większą powierzchnię ({pole2} cm² > {pole1} cm²)")
else:
    print(f"\n✓ Oba trójkąty mają taką samą powierzchnię ({pole1} cm²)")

Zadanie 2

Jesteś praktykantem w firmie zajmującej się tworzeniem witryn i aplikacji internetowych. Otrzymałeś zadanie polegające na stworzeniu skryptu w języku Python.

W skrypcie ma być utworzona klasa odcinek zawierająca cztery publiczne pola, określające współrzędne początku i końca odcinka we współrzędnych x, y. W klasie odcinek należy utworzyć konstruktor, który współrzędnym przypisze podane przez użytkownika (za pomocą formularza) wartości.

Ponadto w klasie powinna być zadeklarowana metoda obliczająca długość odcinka.

W aplikacji należy utworzyć dwa obiekty klasy odcinek.

Wynikiem działania aplikacji ma być wyświetlona wartość długości obu odcinków oraz informacja, który z nich jest dłuższy.

print("\n" + "=" * 60)
print("ZADANIE 2 - ODCINKI")
print("=" * 60)

import math

class Odcinek:
    def __init__(self, x1, y1, x2, y2):
        """
        Konstruktor przyjmuje współrzędne początku i końca odcinka
        x1, y1 - początek odcinka
        x2, y2 - koniec odcinka
        """
        self.x1 = x1
        self.y1 = y1
        self.x2 = x2
        self.y2 = y2
    
    def oblicz_dlugosc(self):
        """
        Metoda obliczająca długość odcinka ze wzoru:
        d = √[(x2-x1)² + (y2-y1)²]
        """
        return math.sqrt((self.x2 - self.x1)**2 + (self.y2 - self.y1)**2)
    
    def __str__(self):
        """Reprezentacja tekstowa odcinka"""
        return f"Odcinek[({self.x1}, {self.y1}) → ({self.x2}, {self.y2})]"


# Symulacja danych z formularza (w rzeczywistej aplikacji byłyby z input())
print("\nPodaj współrzędne pierwszego odcinka:")
odcinek1 = Odcinek(0, 0, 3, 4)
print(f"Utworzono: {odcinek1}")

print("\nPodaj współrzędne drugiego odcinka:")
odcinek2 = Odcinek(1, 1, 4, 5)
print(f"Utworzono: {odcinek2}")

# Obliczanie długości
dlugosc1 = odcinek1.oblicz_dlugosc()
dlugosc2 = odcinek2.oblicz_dlugosc()

# Wyświetlanie wyników
print(f"\nDługość odcinka 1: {dlugosc1:.2f} jednostek")
print(f"Długość odcinka 2: {dlugosc2:.2f} jednostek")

# Porównanie
if dlugosc1 > dlugosc2:
    print(f"\n✓ Odcinek 1 jest dłuższy ({dlugosc1:.2f} > {dlugosc2:.2f})")
elif dlugosc2 > dlugosc1:
    print(f"\n✓ Odcinek 2 jest dłuższy ({dlugosc2:.2f} > {dlugosc1:.2f})")
else:
    print(f"\n✓ Oba odcinki mają taką samą długość ({dlugosc1:.2f})")

Zadanie 3

Stwórz klasę Okrag, w trakcie tworzenia instancji tej klasy jako argument podawany jest promień. Klasa ma dodatkową metodę obwod, która zwraca obwód okręgu. Następnie stwórz klasę Kolo, która dziedziczy klasę Okrag. Poza metodą obwod, klasa ta posiada metodę pole zwracającą pole powierzchni danego koła. Wartość pi należy pobrać z modułu math.
Stwórz instancję klasy Kolo o promieniu 10 i wydrukuj poleceniem print() jego obwód i powierzchnię.
Opis
Wartości liczby π należy zaimportować z modułu math.
Klasa Okrag musi mieć dwie metody:

  • __init__(self, promien) – metoda wywoływana w momencie tworzenia instancji klasy, musi przyjmować jeden dodatkowy argument określający promień okręgu, ktorego wartość należy zapamiętać,
  • obwod(self) – metoda zwracająca obwód okręgu. Obwód jest liczony z wzoru 2πr, promień jest pobierany z atrybutu klasy, wartość π jest określona zmienną pi z modułu math.

Definiując klasę Kolo należy podać klasę Okrag w definicji, aby wykonać dziedziczenie.
Wszystkie atrybuty i metody zdefiniowane w klasie Okrag będą dostępne również w klasie Kolo. Dodatkowo należy dopisać metodę pole, w które z wzoru πr2 zostanie obliczone i zwrócone pole koła.
Po zdefiniowaniu obu klas należy utworzyć nową instancję klasy Kolo o promieniu 10 i wywołać obie metody: obwod i pole.

print("\n" + "=" * 60)
print("ZADANIE 3 - OKRĄG I KOŁO")
print("=" * 60)

from math import pi

class Okrag:
    def __init__(self, promien):
        """Konstruktor przyjmuje promień okręgu"""
        self.promien = promien
    
    def obwod(self):
        """Metoda zwracająca obwód okręgu: 2πr"""
        return 2 * pi * self.promien


class Kolo(Okrag):
    """Klasa Kolo dziedziczy po klasie Okrag"""
    
    def pole(self):
        """Metoda zwracająca pole koła: πr²"""
        return pi * self.promien ** 2


# Tworzenie instancji klasy Kolo o promieniu 10
kolo = Kolo(10)

# Wyświetlanie obwodu i powierzchni
print(f"\nKoło o promieniu {kolo.promien}:")
print(f"Obwód: {kolo.obwod():.2f} jednostek")
print(f"Pole: {kolo.pole():.2f} jednostek²")

Zadanie 4

Stwórz klasę Gracz dla gry RPG, która będzie miała:
– Atrybuty: nick, poziom, hp (punkty życia), exp (doświadczenie)
– Metody:
  * atak() – wyświetla „Gracz {nick} atakuje!”
  * otrzymaj_obrazenia(ilosc) – zmniejsza hp
  * zdobadz_exp(ilosc) – dodaje exp, jeśli exp >= 100, zwiększa poziom
  * __str__() – wyświetla statystyki gracza

Stwórz dwóch graczy i przeprowadź między nimi walkę.
print("\n\n" + "=" * 60)
print("ZADANIE 4 - GRACZ W GRZE RPG")
print("=" * 60)
print("""
Stwórz klasę Gracz dla gry RPG, która będzie miała:
- Atrybuty: nick, poziom, hp (punkty życia), exp (doświadczenie)
- Metody:
  * atak() - wyświetla "Gracz {nick} atakuje!"
  * otrzymaj_obrazenia(ilosc) - zmniejsza hp
  * zdobadz_exp(ilosc) - dodaje exp, jeśli exp >= 100, zwiększa poziom
  * __str__() - wyświetla statystyki gracza

Stwórz dwóch graczy i przeprowadź między nimi walkę.
""")

class Gracz:
    def __init__(self, nick):
        self.nick = nick
        self.poziom = 1
        self.hp = 100
        self.exp = 0
        self.max_hp = 100
    
    def atak(self, cel):
        """Metoda ataku na przeciwnika"""
        obrazenia = random.randint(10, 25) + (self.poziom * 5)
        print(f"{self.nick} atakuje {cel.nick} zadając {obrazenia} obrażeń!")
        cel.otrzymaj_obrazenia(obrazenia)
        self.zdobadz_exp(20)
        return obrazenia
    
    def otrzymaj_obrazenia(self, ilosc):
        """Otrzymywanie obrażeń"""
        self.hp -= ilosc
        if self.hp < 0:
            self.hp = 0
        print(f"{self.nick} ma teraz {self.hp}/{self.max_hp} HP")
    
    def zdobadz_exp(self, ilosc):
        """Zdobywanie doświadczenia i awans na poziom"""
        self.exp += ilosc
        if self.exp >= 100:
            self.poziom_w_gore()
    
    def poziom_w_gore(self):
        """Awans na wyższy poziom"""
        self.exp = 0
        self.poziom += 1
        self.max_hp += 20
        self.hp = self.max_hp
        print(f"{self.nick} awansował na poziom {self.poziom}!")
    
    def czy_zyje(self):
        """Sprawdzenie czy gracz żyje"""
        return self.hp > 0
    
    def __str__(self):
        return f"{self.nick} | Poziom: {self.poziom} | HP: {self.hp}/{self.max_hp} | EXP: {self.exp}/100"


# Rozwiązanie - walka graczy
print("\n--- POCZĄTEK WALKI ---\n")
gracz1 = Gracz("DragonSlayer")
gracz2 = Gracz("NinjaWarrior")

print(gracz1)
print(gracz2)
print()

runda = 1
while gracz1.czy_zyje() and gracz2.czy_zyje():
    print(f"\n--- RUNDA {runda} ---")
    gracz1.atak(gracz2)
    
    if gracz2.czy_zyje():
        gracz2.atak(gracz1)
    
    runda += 1
    
    if runda > 10:  # Zabezpieczenie przed nieskończoną pętlą
        print("\nWalka trwa zbyt długo - remis!")
        break

if not gracz1.czy_zyje():
    print(f"\n{gracz2.nick} WYGRYWA!")
elif not gracz2.czy_zyje():
    print(f"\n{gracz1.nick} WYGRYWA!")

print("\n--- KOŃCOWE STATYSTYKI ---")
print(gracz1)
print(gracz2)

Zadanie 5

Stwórz system zarządzania playlistami:
– Klasa Utwor: tytuł, wykonawca, czas_trwania (w sekundach)
– Klasa Playlista: nazwa, lista utworów
  * dodaj_utwor(utwor)
  * usun_utwor(tytul)
  * calkowity_czas() – suma czasu wszystkich utworów
  * najdluzszy_utwor() – zwraca najdłuższy utwór
  * __str__() – ładne wyświetlenie playlisty

Stwórz playlistę z kilkoma utworami i przetestuj wszystkie metody.
print("\n\n" + "=" * 60)
print("ZADANIE 5 - SPOTIFY - PLAYLISTA MUZYCZNA")
print("=" * 60)
print("""
Stwórz system zarządzania playlistami:
- Klasa Utwor: tytuł, wykonawca, czas_trwania (w sekundach)
- Klasa Playlista: nazwa, lista utworów
  * dodaj_utwor(utwor)
  * usun_utwor(tytul)
  * calkowity_czas() - suma czasu wszystkich utworów
  * najdluzszy_utwor() - zwraca najdłuższy utwór
  * __str__() - ładne wyświetlenie playlisty

Stwórz playlistę z kilkoma utworami i przetestuj wszystkie metody.
""")

class Utwor:
    def __init__(self, tytul, wykonawca, czas_trwania):
        self.tytul = tytul
        self.wykonawca = wykonawca
        self.czas_trwania = czas_trwania  # w sekundach
    
    def formatuj_czas(self):
        """Formatuje czas z sekund na MM:SS"""
        minuty = self.czas_trwania // 60
        sekundy = self.czas_trwania % 60
        return f"{minuty}:{sekundy:02d}"
    
    def __str__(self):
        return f"{self.tytul} - {self.wykonawca} [{self.formatuj_czas()}]"
    
    def __eq__(self, other):
        """Porównywanie utworów po tytule"""
        if isinstance(other, Utwor):
            return self.tytul == other.tytul
        return False


class Playlista:
    def __init__(self, nazwa):
        self.nazwa = nazwa
        self.utwory = []
    
    def dodaj_utwor(self, utwor):
        """Dodaje utwór do playlisty"""
        self.utwory.append(utwor)
        print(f"✓ Dodano: {utwor.tytul}")
    
    def usun_utwor(self, tytul):
        """Usuwa utwór o podanym tytule"""
        for utwor in self.utwory:
            if utwor.tytul == tytul:
                self.utwory.remove(utwor)
                print(f"✓ Usunięto: {tytul}")
                return True
        print(f"✗ Nie znaleziono utworu: {tytul}")
        return False
    
    def calkowity_czas(self):
        """Zwraca całkowity czas trwania playlisty"""
        return sum(utwor.czas_trwania for utwor in self.utwory)
    
    def formatuj_calkowity_czas(self):
        """Formatuje całkowity czas na HH:MM:SS"""
        total = self.calkowity_czas()
        godziny = total // 3600
        minuty = (total % 3600) // 60
        sekundy = total % 60
        return f"{godziny}:{minuty:02d}:{sekundy:02d}"
    
    def najdluzszy_utwor(self):
        """Zwraca najdłuższy utwór z playlisty"""
        if not self.utwory:
            return None
        return max(self.utwory, key=lambda u: u.czas_trwania)
    
    def __len__(self):
        """Zwraca liczbę utworów"""
        return len(self.utwory)
    
    def __str__(self):
        naglowek = f"\nPLAYLISTA: {self.nazwa}"
        naglowek += f"\n{'=' * 50}"
        naglowek += f"\nLiczba utworów: {len(self)}"
        naglowek += f"\nCałkowity czas: {self.formatuj_calkowity_czas()}"
        naglowek += f"\n{'-' * 50}"
        
        utwory_str = "\n".join(f"{i+1}. {utwor}" for i, utwor in enumerate(self.utwory))
        
        return f"{naglowek}\n{utwory_str if utwory_str else 'Brak utworów'}\n{'=' * 50}"


# Rozwiązanie
print("\n--- TWORZENIE PLAYLISTY ---\n")
moja_playlista = Playlista("Moje Ulubione 2024")

# Dodawanie utworów
utwor1 = Utwor("Bohemian Rhapsody", "Queen", 354)
utwor2 = Utwor("Stairway to Heaven", "Led Zeppelin", 482)
utwor3 = Utwor("Hotel California", "Eagles", 391)
utwor4 = Utwor("Imagine", "John Lennon", 183)
utwor5 = Utwor("Smells Like Teen Spirit", "Nirvana", 301)

moja_playlista.dodaj_utwor(utwor1)
moja_playlista.dodaj_utwor(utwor2)
moja_playlista.dodaj_utwor(utwor3)
moja_playlista.dodaj_utwor(utwor4)
moja_playlista.dodaj_utwor(utwor5)

# Wyświetlanie playlisty
print(moja_playlista)

# Najdłuższy utwór
print(f"\nNajdłuższy utwór: {moja_playlista.najdluzszy_utwor()}")

# Usuwanie utworu
print("\n--- USUWANIE UTWORU ---\n")
moja_playlista.usun_utwor("Imagine")
print(moja_playlista)

Zadanie 6

Stwórz system bankowy z klasami:
– Klasa KontoBankowe: właściciel, saldo, historia_transakcji
  * wplata(kwota)
  * wyplata(kwota) – sprawdza czy jest wystarczający balans
  * przelew(kwota, konto_docelowe)
  * pokaz_historie() – wyświetla ostatnie 5 transakcji
  * __str__() – informacje o koncie

Stwórz kilka kont i przeprowadź między nimi transakcje.
print("\n\n" + "=" * 60)
print("ZADANIE 6 - KONTO BANKOWE")
print("=" * 60)
print("""
Stwórz system bankowy z klasami:
- Klasa KontoBankowe: właściciel, saldo, historia_transakcji
  * wplata(kwota)
  * wyplata(kwota) - sprawdza czy jest wystarczający balans
  * przelew(kwota, konto_docelowe)
  * pokaz_historie() - wyświetla ostatnie 5 transakcji
  * __str__() - informacje o koncie

Stwórz kilka kont i przeprowadź między nimi transakcje.
""")

from datetime import datetime

class KontoBankowe:
    def __init__(self, wlasciciel, saldo_poczatkowe=0):
        self.wlasciciel = wlasciciel
        self.saldo = saldo_poczatkowe
        self.historia_transakcji = []
        self.numer_konta = f"{random.randint(1000, 9999)}-{random.randint(1000, 9999)}"
        
        if saldo_poczatkowe > 0:
            self._dodaj_do_historii("Wpłata początkowa", saldo_poczatkowe, "wpłata")
    
    def _dodaj_do_historii(self, opis, kwota, typ):
        """Prywatna metoda dodająca transakcję do historii"""
        transakcja = {
            "data": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
            "opis": opis,
            "kwota": kwota,
            "typ": typ,
            "saldo_po": self.saldo
        }
        self.historia_transakcji.append(transakcja)
    
    def wplata(self, kwota):
        """Wpłata środków na konto"""
        if kwota <= 0:
            print("Kwota wpłaty musi być dodatnia!")
            return False
        
        self.saldo += kwota
        self._dodaj_do_historii(f"Wpłata gotówki", kwota, "wpłata")
        print(f"✓ Wpłacono {kwota} zł. Nowe saldo: {self.saldo} zł")
        return True
    
    def wyplata(self, kwota):
        """Wypłata środków z konta"""
        if kwota <= 0:
            print("Kwota wypłaty musi być dodatnia!")
            return False
        
        if self.saldo < kwota:
            print(f"Brak wystarczających środków! (saldo: {self.saldo} zł)")
            return False
        
        self.saldo -= kwota
        self._dodaj_do_historii(f"Wypłata gotówki", -kwota, "wypłata")
        print(f"✓ Wypłacono {kwota} zł. Nowe saldo: {self.saldo} zł")
        return True
    
    def przelew(self, kwota, konto_docelowe):
        """Przelew środków na inne konto"""
        if kwota <= 0:
            print("Kwota przelewu musi być dodatnia!")
            return False
        
        if self.saldo < kwota:
            print(f"Brak wystarczających środków! (saldo: {self.saldo} zł)")
            return False
        
        self.saldo -= kwota
        konto_docelowe.saldo += kwota
        
        self._dodaj_do_historii(
            f"Przelew do {konto_docelowe.wlasciciel}", 
            -kwota, 
            "przelew wychodzący"
        )
        konto_docelowe._dodaj_do_historii(
            f"Przelew od {self.wlasciciel}", 
            kwota, 
            "przelew przychodzący"
        )
        
        print(f"✓ Przelano {kwota} zł do {konto_docelowe.wlasciciel}")
        print(f"  Twoje nowe saldo: {self.saldo} zł")
        return True
    
    def pokaz_historie(self, ostatnie=5):
        """Wyświetla historię transakcji"""
        print(f"\nHistoria transakcji - {self.wlasciciel}")
        print("=" * 70)
        
        if not self.historia_transakcji:
            print("Brak transakcji")
            return
        
        transakcje = self.historia_transakcji[-ostatnie:]
        
        for t in transakcje:
            symbol = "+" if t['kwota'] > 0 else "-"
            print(f"{t['data']} | {symbol} {abs(t['kwota']):>7.2f} zł | {t['opis']:<30} | Saldo: {t['saldo_po']:.2f} zł")
        
        print("=" * 70)
    
    def __str__(self):
        return f"Konto: {self.numer_konta} | Właściciel: {self.wlasciciel} | Saldo: {self.saldo:.2f} zł"


# Rozwiązanie
print("\n--- TWORZENIE KONT BANKOWYCH ---\n")
konto1 = KontoBankowe("Jan Kowalski", 1000)
konto2 = KontoBankowe("Anna Nowak", 500)
konto3 = KontoBankowe("Piotr Wiśniewski", 2000)

print(konto1)
print(konto2)
print(konto3)

print("\n--- OPERACJE BANKOWE ---\n")
konto1.wplata(500)
konto1.wyplata(200)
konto1.przelew(300, konto2)
konto2.wyplata(100)
konto3.przelew(500, konto1)

print("\n--- HISTORIA TRANSAKCJI ---")
konto1.pokaz_historie()
konto2.pokaz_historie()

Zadanie 7

Stwórz system biblioteki:
– Klasa Ksiazka: tytul, autor, rok_wydania, wypozyczona (bool)
– Klasa Biblioteka: nazwa, lista książek
  * dodaj_ksiazke(ksiazka)
  * wypozycz(tytul) – zmienia status książki
  * zwroc(tytul) – zmienia status z powrotem
  * dostepne_ksiazki() – zwraca listę dostępnych książek
  * wyszukaj_po_autorze(autor) – zwraca książki autora
  * __len__() – liczba książek w bibliotece

Stwórz bibliotekę, dodaj książki i przetestuj wypożyczenia.
print("\n\n" + "=" * 60)
print("ZADANIE 7 - BIBLIOTEKA KSIĄŻEK")
print("=" * 60)
print("""
Stwórz system biblioteki:
- Klasa Ksiazka: tytul, autor, rok_wydania, wypozyczona (bool)
- Klasa Biblioteka: nazwa, lista książek
  * dodaj_ksiazke(ksiazka)
  * wypozycz(tytul) - zmienia status książki
  * zwroc(tytul) - zmienia status z powrotem
  * dostepne_ksiazki() - zwraca listę dostępnych książek
  * wyszukaj_po_autorze(autor) - zwraca książki autora
  * __len__() - liczba książek w bibliotece

Stwórz bibliotekę, dodaj książki i przetestuj wypożyczenia.
""")

class Ksiazka:
    def __init__(self, tytul, autor, rok_wydania):
        self.tytul = tytul
        self.autor = autor
        self.rok_wydania = rok_wydania
        self.wypozyczona = False
    
    def wypozycz(self):
        """Wypożycza książkę"""
        if self.wypozyczona:
            return False
        self.wypozyczona = True
        return True
    
    def zwroc(self):
        """Zwraca książkę"""
        if not self.wypozyczona:
            return False
        self.wypozyczona = False
        return True
    
    def __str__(self):
        status = "WYPOŻYCZONA" if self.wypozyczona else "DOSTĘPNA"
        return f'"{self.tytul}" - {self.autor} ({self.rok_wydania}) [{status}]'
    
    def __eq__(self, other):
        if isinstance(other, Ksiazka):
            return self.tytul == other.tytul and self.autor == other.autor
        return False


class Biblioteka:
    def __init__(self, nazwa):
        self.nazwa = nazwa
        self.ksiazki = []
    
    def dodaj_ksiazke(self, ksiazka):
        """Dodaje książkę do biblioteki"""
        self.ksiazki.append(ksiazka)
        print(f"Dodano książkę: {ksiazka.tytul}")
    
    def wypozycz(self, tytul):
        """Wypożycza książkę o podanym tytule"""
        for ksiazka in self.ksiazki:
            if ksiazka.tytul.lower() == tytul.lower():
                if ksiazka.wypozycz():
                    print(f"Wypożyczono: {ksiazka.tytul}")
                    return True
                else:
                    print(f"Książka '{ksiazka.tytul}' jest już wypożyczona")
                    return False
        print(f"Nie znaleziono książki: {tytul}")
        return False
    
    def zwroc(self, tytul):
        """Zwraca książkę o podanym tytule"""
        for ksiazka in self.ksiazki:
            if ksiazka.tytul.lower() == tytul.lower():
                if ksiazka.zwroc():
                    print(f"Zwrócono: {ksiazka.tytul}")
                    return True
                else:
                    print(f"Książka '{ksiazka.tytul}' nie była wypożyczona")
                    return False
        print(f"Nie znaleziono książki: {tytul}")
        return False
    
    def dostepne_ksiazki(self):
        """Zwraca listę dostępnych książek"""
        return [k for k in self.ksiazki if not k.wypozyczona]
    
    def wypozyczone_ksiazki(self):
        """Zwraca listę wypożyczonych książek"""
        return [k for k in self.ksiazki if k.wypozyczona]
    
    def wyszukaj_po_autorze(self, autor):
        """Zwraca książki danego autora"""
        return [k for k in self.ksiazki if autor.lower() in k.autor.lower()]
    
    def __len__(self):
        """Zwraca liczbę książek w bibliotece"""
        return len(self.ksiazki)
    
    def __str__(self):
        naglowek = f"\nBIBLIOTEKA: {self.nazwa}"
        naglowek += f"\n{'=' * 70}"
        naglowek += f"\nLiczba książek: {len(self)}"
        naglowek += f"\nDostępne: {len(self.dostepne_ksiazki())} | Wypożyczone: {len(self.wypozyczone_ksiazki())}"
        naglowek += f"\n{'-' * 70}"
        
        ksiazki_str = "\n".join(str(k) for k in self.ksiazki)
        
        return f"{naglowek}\n{ksiazki_str}\n{'=' * 70}"


# Rozwiązanie
print("\n--- TWORZENIE BIBLIOTEKI ---\n")
biblioteka = Biblioteka("Miejska Biblioteka Publiczna")

# Dodawanie książek
ksiazka1 = Ksiazka("Wiedźmin", "Andrzej Sapkowski", 1990)
ksiazka2 = Ksiazka("Pan Tadeusz", "Adam Mickiewicz", 1834)
ksiazka3 = Ksiazka("Solaris", "Stanisław Lem", 1961)
ksiazka4 = Ksiazka("Lalka", "Bolesław Prus", 1890)
ksiazka5 = Ksiazka("Miecz przeznaczenia", "Andrzej Sapkowski", 1992)

biblioteka.dodaj_ksiazke(ksiazka1)
biblioteka.dodaj_ksiazke(ksiazka2)
biblioteka.dodaj_ksiazke(ksiazka3)
biblioteka.dodaj_ksiazke(ksiazka4)
biblioteka.dodaj_ksiazke(ksiazka5)

print(biblioteka)

print("\n--- WYPOŻYCZENIA ---\n")
biblioteka.wypozycz("Wiedźmin")
biblioteka.wypozycz("Solaris")
biblioteka.wypozycz("Solaris")  # Próba wypożyczenia ponownie

print(biblioteka)

print("\n--- WYSZUKIWANIE PO AUTORZE ---\n")
sapkowski_ksiazki = biblioteka.wyszukaj_po_autorze("Sapkowski")
print(f"Znaleziono {len(sapkowski_ksiazki)} książek Sapkowskiego:")
for ksiazka in sapkowski_ksiazki:
    print(f"  {ksiazka}")

print("\n--- ZWROT KSIĄŻEK ---\n")
biblioteka.zwroc("Wiedźmin")

print(biblioteka)

Zadanie 8

Stwórz prosty sklep internetowy:
– Klasa Produkt: nazwa, cena, ilosc_w_magazynie
– Klasa KoszykZakupowy:
  * dodaj_produkt(produkt, ilosc)
  * usun_produkt(nazwa_produktu)
  * oblicz_wartosc() – suma wartości wszystkich produktów
  * wyswietl_koszyk()
  * zrealizuj_zamowienie() – zmniejsza stan magazynowy

Stwórz kilka produktów, dodaj je do koszyka i zrealizuj zamówienie.