Python - zajęcia 10 - programowanie obiektowe

Python - zajęcia 10 - programowanie obiektowe
Photo by David Clode / Unsplash

1) Tworzenie własnej funkcji

Nazwa funkcji to powinien być czasownik. Powinniśmy dać mu znać też co powinien robić. Funkcja pracuje na jakiś danych, które przekazujemy jako parametr. Zazwyczaj zwraca też użytkownikowi jakąś wartość, gdy użyjemy funkcji return.

def kwadrat(x):
    wynik = x * x
    return wynik

tu przekazujemy wartość x, tworzymy zmienną wynik , której wartość jest powiązana z x (np. tutaj to X kwadrat).

def kwadrat(x):
    wynik = x * x
    return wynik

Wynik:

Spowoduje wyświetlenie na ekranie 4 (2 podniesione do kwadratu). Niczym się to nie różni od wbudowanych funkcji, których używaliśmy do tej pory.

3) Dobre praktyki

Są 2 zasady, których warto przestrzegać podczas konstruowania funkcji:

1. Informujmy jakiego typu parametrów oczekujemy.

  1. Informujmy jaki typ danych funkcja zwróci.

(1) W wcześniejszym przykładzie (kwadrat) parametrem nie może być ciąg znaków (string), bo nie będzie można wykonać na nim mnożenia. Dlatego warto poinformować o tym użytkownika, że oczekujemy np. liczb całkowitych.

def kwadrat(x:int):
    wynik = x * x
    return wynik

(2) Potem informujmy jaki typ danych funkcja zwróci po strzałce :

def kwadrat(x:int) -> int:
    wynik = x * x
    return wynik

Zawsze na końcu linijki i tak dwukropek. Jeśli funkcja niż nie zwraca można tak to zapisać:

def nic() -> None:

4) Wracamy do książki kontaktowej

import os

kontaktowa = {}

if os.path.exists("ksiazka_tele.json"):
    with open("ksiazka_tele.json", "r", encoding="utf8") as plik:
        kontaktowa = json.load(plik)
        print(kontaktowa)
else:
    print("Nowa książka, wprowadź pierwszą wartość")

def czysc_ekran():
    if os.name == "nt":
        _ = os.system("cls")
    else:
        _ = os.system("clear")

def dopisz_kontakt(imie: str) -> None
  dane_kontaktowe = input("Waprowadz dane kontaktowe:")
  kontaktowa[imie]=dane_kontaktowe

def utworz_kontaktu() -> None:
    imie = input("Wprowadź imię: ")
    
    if imie in kontaktowa:
        print("""
          Wpis istnieje!
          1 - aktualizuj istniejący
          2 - wprowadź od nowa
          3 - przerwij
            """)
        wybor =  input("Twoj wybor:")
        if wybor == "1":
            dopisz_kontakt(imie)
        elif wybor == "2":
            utworz_kontakt()
        else:
            return
    else:
        dopisz_kontakt(imie)

5) Rekurencja i odnośnik do funkcji range

moje_liczby = []

# range start, koniec, krok
def counter(end: int, start: int = 0, step: int = 1):
    if start > end:
        return
    moje_liczby.append(start)
    counter(end, start + step, step)

counter(end=15, step=2)
print(moje_liczby)
def counter(end, start = 0, step = 1):
    if start > end:
        return
    yield start
    yield from counter(end, start + step, step)

print(list(counter(10)))

6) Klasy obiektów

Możemy tworzyć własne typy poprzez stworzenie jakiejś klasy obiektów.

Tworzymy jakiś ogólny opis cech i rzeczy, które dany obiekt może robić. To taki szablon obiektu.

Atrybuty Czasowniki
futro miauczenie
kolor futra zrzucanie rzeczy z parapetu
wąsy drapanie
łapy
ogon

Chodzi o to, żeby nie powtarzać tego samego kodu wiele razy. Ponadto wykorzystując własne klasy i funkcje jesteśmy w stanie stworzyć czytelniejszy i łatwiejszy w utrzymaniu program.

Odnosząc się do rzeczywistego języka - nie chcielibyście np. za każdym razem mówić: "Mam zwierzę, które ma 4 łapy, ogon, dobry węch i szczeka." Łatwiej jest powiedzieć: "Mam psa."

Unikamy też wielu błędów - wystarczy, że raz poprawnie stworzymy klasę. Użytkownicy klasy powinni dostać jasny komunikat co do jej celu.

Klasa nie może być mylona z obiektem!

7) Deklarowanie klas

Przebiega zgodnie ze schematem - na początek używamy class i podajemy nazwę klasy, a na końcu pusty nawias okrągły.

Nazwa klasy:

  • rzeczownik
  • zaczynamy od dużej litery (każdy wyraz np. NazwaKlasy)
  • na końcu pusty nawias okrągły
menu = {"sałatka" : 12.5, "sok" : 8.1, "burito" : 25.2,}

class Zamowienie():

Podczas tworzenia używamy specjalnych funkcji (magicznych). Zgodnie ze wzorem def _ _init_ _ (self, parametry)

Init to konstruktor, który służy przypisania domyślnych parametrów klasy (kot ma 4 nogi). Przed i po init jest PODWÓJNA podłoga. Następnie w kolejnych linijkach po wcięciu podajemy atrybuty klasy, które mamy zastosować - poprzedzając je self. Przypisujemy do nich wartości parametrów z metody init. KONSTRUKTOR NIE ZWRACA WARTOŚCI WIEC, ZWRACA ZAWSZE NONE I NIE MUSIMY TEGO PISAĆ.

Pokazuje to cechy indywidualne danego obiektu:

  def __init__(self, stolik: int, potrawy:list) - > None:
    self.nr_stolika = stolik
    self.potrawy = potrawy
    self.czy_oplacone = False

W tym przykładzie Zamówienie ma cechy takie jak: numer stolika, zamówione potrawy i czy zapłacono za jedzenie.

Jest też powiązane z innymi funkcjami, które odpowiedzą na dodatkowe pytania np. obliczaniem rachunku i przyjmowaniem zapłaty. Takie funkcje wbudowane w daną klasę to metody i powinny korzystać z indywidualnych cech obiektu (self).

  def oblicz_rachunek(self) -> float:
    rachunek = 0
    for potrawa in self.potrawy:
      rachunek += menu[potrawa]
    return rachunek
    
  def przyjmij_oplate(self):
    self.czy_oplacone = True

Tutaj nie daje po self nic, bo wcześniej już zdefiniowałem to self. Ale może być przypadek, że będę chciał coś dodatkowo tu zdefiniować np. rabat przy liczeniu rachunku).

Jeśli nie zastosujemy w metodzie self, sugeruje to, że daną metodę lepiej z klasy wyjąć. Możemy użyć funkcji i nie zaśmiecać czytelności klasy.

Teraz mogę wykonywać metodę stworzoną i przypisana do klasy:

zam1 = Zamowienie(1, ["sałatka", "burito"])
print(zam1.oblicz_rachunek())
print(zam1.potrawy)

Całość:

menu = {"sałatka": 12.5, "sok": 8.1, "burrito": 25.2}

class Zamowienie():
    def __init__(self, stolik: int, potrawy: list) -> None:
        self.nr_stolika = stolik
        self.potrawy = potrawy
        self.czy_oplacone = False

    def oblicz_rachunek(self) -> float:
        rachunek = 0
        for potrawa in self.potrawy:
            rachunek += menu[potrawa]
        return rachunek

    def przyjmij_oplate(self):
        self.czy_oplacone = True

zam1 = Zamowienie(1, ["sałatka", "burrito"])
print(zam1.oblicz_rachunek())

8) Ćwiczenie

Tworzenie postaci do gry RPG, gdzie klasą będą postacie, posiadające zestaw cech: imię, profesję, punkty życia, magii i siły.

class PostacGracza():
  def __init__(self, imie:str, profesja:str, sila:int, zycie: int = 100,
      magia:int = 100 , exp:int = 0,) -> None:
      
    self.imie = imie
    self.profesja = profesja
    self.sila = sila
    self.zycie = zycie     #punkty życia, ustawione na 100
    self.magia = magia     #punkty magii, ustawione na 100
    self.exp = exp         #punkty doświadczenia na początku 0

  def przywitaj (self) -> None:
      print(f"Witaj {self.imie}!")

gracz1 = PostacGracza("Fizban", "mag", 20, magia=200)  
gracz1.przywitaj()
print(f"magia: {gracz1.magia}, exp: {gracz1.exp})

Można wartości podawać też tutaj bez =, jeśli podajemy po kolei.

8) Dziedziczenie

Możemy stworzyć klasy, które dziedziczą atrybuty i metody klasy-rodzica, a potem dodają własne. Czyli rozszerzamy w ten sposób klase. Wszystko od rodzica dziedziczysz u klasy dziecka, ale jest możliwość wybrania tego też (hermetyzacja).

Dziedziczenie zapisujemy tak:

class Mag(PostacGracza):

Potem definiujemy konstruktor klasy dziecka używając def __ init __ żeby wskazać to co chcemy nowego wprowadzić do klasy. Ale ponieważ dziedziczymy też po rodzicu to kolejna linijka zawiera wywołanie konstruktora klasy rodzica (nadrzędnej klasy) po wcięciu za pomocą super(). __ init __

class Mag(PostacGracza):
    def __init__(self, imie: str, sila: int = 20, zycie: int = 50, magia: int = 200, exp: int = 0) -> None:
        super().__init__(imie, sila, zycie, magia, exp)

Ważne jest żeby do konstruktora klasy nadrzędnej przekazać wszystkie wymagane parametry.

Dla tej tej nowej klasy zdefiniowano też nową metodę - kula ognia. To robi się tak samo jak przy grupie rodzica (nie ma super etc.)

class Mag(PostacGracza):
    def __init__(self, imie: str, sila: int = 20, zycie: int = 50, magia: int = 200, exp: int = 0) -> None:
        super().__init__(imie, sila, zycie, magia, exp)

Na koniec tworzymy metodę - wyświetl kartę postaci w rodzicu, a dziecko ma dostęp do klasy rodzica i tym samym do tej metody.

Cały kod:

class PostacGracza:
    def __init__(self, imie: str, sila: int, zycie: int = 100, magia: int = 100, exp: int = 0) -> None:
        self.imie = imie
        self.sila = sila
        self.zycie = zycie
        self.magia = magia
        self.exp = exp

    def przywitaj(self) -> None:
        print(f"Witaj {self.imie}!")
    
    def wyswietl_karte_postaci(self):
        print(f"imie: {self.imie}\nsila: {self.sila}\nzycie: {self.zycie}\nmagia: {self.magia}\npkt. doswiadczenia: {self.exp}")
    
class Mag(PostacGracza):
    def __init__(self, imie: str, sila: int = 20, zycie: int = 50, magia: int = 200, exp: int = 0) -> None:
        super().__init__(imie, sila, zycie, magia, exp)
    
    def kula_ognia(self):
        print("Rzucono kulę ognia! Kaboooooom!")
        self.magia -= 10

gracz1 = Mag("Fizban")
gracz1.przywitaj()


gracz1.kula_ognia()

gracz1.wyswietl_karte_postaci()