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 daMetody
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:
| Metoda | Opis | Przykład użycia |
|---|---|---|
__init__ | Konstruktor | obj = Klasa() |
__str__ | Reprezentacja tekstowa | print(obj), str(obj) |
__repr__ | Reprezentacja dla debugowania | repr(obj) |
__len__ | Długość obiektu | len(obj) |
__eq__ | Równość | obj1 == obj2 |
__lt__ | Mniejsze niż | obj1 < obj2 |
__gt__ | Większe niż | obj1 > obj2 |
__add__ | Dodawanie | obj1 + obj2 |
__sub__ | Odejmowanie | obj1 - obj2 |
__mul__ | Mnożenie | obj * 5 |
__getitem__ | Dostęp przez indeks | obj[key] |
__setitem__ | Ustawianie przez indeks | obj[key] = value |
__call__ | Wywołanie jak funkcja | obj() |
Odsyłam Was do fajnego artykułu Magiczne metody – czyli szczypta magii w Pythonie.
Modyfikatory dostępu
__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_budowyprint(f"Utworzono obiekt {self}.")def rok_budowy(self):return self._rok_budowyclass 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) = Falseisinstance(<__main__.Blok object at 0x0000026706EDAED0>, Blok) = Trueisinstance(<__main__.Blok object at 0x0000026706EDAED0>, Budynek) = True----------isinstance(<__main__.Blok object at 0x0000026706EDBD90>, Szpital) = Falseisinstance(<__main__.Blok object at 0x0000026706EDBD90>, Blok) = Trueisinstance(<__main__.Blok object at 0x0000026706EDBD90>, Budynek) = True----------isinstance(<__main__.Szpital object at 0x0000026706EEC190>, Szpital) = Trueisinstance(<__main__.Szpital object at 0x0000026706EEC190>, Blok) = Falseisinstance(<__main__.Szpital object at 0x0000026706EEC190>, Budynek) = True----------isinstance(<__main__.Szpital object at 0x0000026706EEC1D0>, Szpital) = Trueisinstance(<__main__.Szpital object at 0x0000026706EEC1D0>, Blok) = Falseisinstance(<__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 Blokrok_budowy = 2001-----Metoda w klasie BlokObiekt <__main__.Blok object at 0x0000026706EDAED0> - kondygnacje: Blok 5 kondygnacjiRok budowy = 2001-----------------------------To jest wywołanie metody z klasy nadrzędnej w klasie Blokrok_budowy = 2002-----Metoda w klasie BlokObiekt <__main__.Blok object at 0x0000026706EDBD90> - kondygnacje: Blok 5 kondygnacjiRok budowy = 2002-----------------------------To jest wywołanie metody z klasy nadrzędnej w klasie Szpitalrok_budowy = 2010-----Metoda w klasie SzpitalObiekt <__main__.Szpital object at 0x0000026706EEC190> - kondygnacje: Budynek szpitala 2 kondygnacyjnyRok budowy = 2010-----------------------------To jest wywołanie metody z klasy nadrzędnej w klasie Szpitalrok_budowy = 2012-----Metoda w klasie SzpitalObiekt <__main__.Szpital object at 0x0000026706EEC1D0> - kondygnacje: Budynek szpitala 2 kondygnacyjnyRok 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:
- Większa elastyczność – możesz łatwo zmieniać komponenty
- Łatwiejsze testowanie – każdy komponent można testować osobno
- Unikanie problemów z wielodziedziczeniem
- Luźniejsze powiązania – komponenty są niezależne
- Łatwiejsze ponowne użycie – komponenty działają w różnych kontekstach
Wady kompozycji:
- Więcej kodu do napisania (więcej klas)
- Może być mniej intuicyjna niż dziedziczenie dla prostych relacji
- 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
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
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
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
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)