Objektno orjentisano programiranje

Python je objektno orjentisan

Python je objektno orjentisan jezik, i zapravo smo već koristili puno objektno orjentisanih koncepata. Ključni pojam je objekat (object). Objekat se sastoji od dve stvari: podataka i funkcija (koje ze zovu metodama), koje rade sa podacima. Na primer stringovi u Python su objekti. Podaci u string objektu su slova od kojih je string sačinjen. Metode su funkcije kao što su lower, replace, i split. U Python-u, sve je zapravo objekat. To uključuje ne samo stringove, već i cele brojeve, decimalne brojeve, pa čak i same funkcije.

Kreiranje klase

Klasa (class) je na neki način templejt (mustra, šablon) za objekte. Klasa sadrži kod za sve metode objekata iz te klase.

Jedan jednostavan primer

Ovde je prikazan jedan jednostavan primer kojim se demonstrira kako izgleda jedna klasa ( mustra, šablon za objekte). Ova klasa ne radi ništa posebno interesantno.

class Primer:
    def __init__(self, a, b):
        self.a = a
        self.b = b

    def add(self):
        return self.a + self.b

e = Primer(8, 6)
print(e.add())

Za kreiranje nove klase koristimo naredbu class. Naziv klase obično počinjemo velikim slovom.

Većina klasa ima metod pod nazivom __init__. Donje crte pokazuju da je to specijalna vrsta metode. Ova metoda se zove konstruktor , i automatski se poziva kada neko kreira novi objekat iz vaše klase. Konstruktor se obično koristi da se zadaju početne vrednosti varijablama iz klase. U gornjem programu konstruktor prihvata dve vrednosti, a i b, i dodeljuje ih varijablama klase self.a i self.b.

Prvi argument u svakom metodu vaše klase je specijalna varijabla pod nazivom self. Svaki put kada se pozovete na neku od varijabli ili metoda iz klase morate reč self stavite ispred naziva varijable ili metode. Svrha reči self je da napravi razliku između varijabli i metoda iz klase, sa ostalim varijablana i funkcijama koje se mogu pojaviti u programu.

Za kreiranje novog objekta iz klase, prosto pozivate naziv klase zajedno sa vrednostima koje želite da pošaljete konstruktoru. Obično ćete objekat pridružiti nekoj varijabli. To je urađeno u liniji e=Primer(8, 6) gde je varijabli e pridružen novokreirani objekat. Za upotrebu objekta koristi se dot (.) operator kao u e.add().

Malo praktičniji primer

Ovde posmatramo klasu pod nazivom Analizator koja vrši jednostavnu analizu stringa. Klasa ima metode koje vraćaju koliko reči ima u stringu, koliko je reči zadate dužine i koliko njih počinje sa zadatim stringom.

from string import punctuation

class Analizator:
    def __init__(self, s):
        for c in punctuation:
            s = s.replace(c,'';)
        s = s.lower()
        self.words = s.split()

    def number_of_words(self):
        return len(self.words)

    def starts_with(self, s):
        return len([w for w in self.words if w[:len(s)]==s])

    def number_with_length(self, n):
        return len([w for w in self.words if len(w)==n])


s = 'This is a test of the class.'
analizator = Analizator(s)
print(analizator.words)
print('Number of words:', analizator.number_of_words())
print('Number of words starting with "t":', analizator.starts_with('t'))
print('Number of 2-letter words:', analizator.number_with_length(2))

Rezultat:

['this', 'is', 'a', 'test', 'of', 'the', 'class']
Number of words: 7
Number of words starting with "t": 3
Number of 2-letter words: 2

Nekoliko napomena o ovom programu:

Jedan od razloga što smo „uveli“ ovaj kod u klasu je što ga tako „uvijenog“ možemo koristiti u mnogim drugim programima. Takođe je dobro i zbog bolje organizacije programa. Ako je jedini posao našeg programa da analizira neke stringove, onda nema neke posebne prednosti pisanja klase, ali ako je ovo deo nekog velikog programa, tada pisanje klase omogućava lep način razdvajanja koda za Analizator od ostatka koda. To takođe znači da ako treba da menjamo nešto u samom Analizatoru, to neće uticati na ostatak koda. Analizator može biti importovan u druge programe. U sledećoj naredni pristupamo jednoj varijabli klase:

print(analizator.words)

Vi možete da menjate vrednost varijable iz klase. To nije uvek pametno činiti. U nekim slučajevima to može biti pogodno, ali morate bit veoma pažljivi. Neselektivna upotreba varijabli iz klase suprotna je ideji „učaurivanja“ (encapsulation) i može da dovede do programskih grešaka koje je teško otkloniti. Neki drugi programski jezici uvode pojmove public i private varijable klase, gde u public varijable takve da im svako može pristupati, a private varijablama mogu pristupati samo metode iz iste klase. U Python-u sve su varijable public, pa je na programerima odgovornost za njihovo korišćenje. Postoji konvencija da se private varijablama daju imena koja počinju sa donjom crtom, na primer _var1. To služi da se drugima stavi do znanja da je ova varijabla interna i da je ne treba dirati.

Nasleđivanje

U objektno orjentisanom programiranju postoji koncept koji se naziva nasleđivanje (inheritance) gde vi kreirate klasu na bazi već postojeće klase. Kada to radite, nova klasa dobija sve verijable i metode klase koju nasleđuje (koja se naziva baznom klasom). U izvedenoj klasi možete da dodajete nove varijable i metode koje nisu bile prisutne u baznoj klasi, a takođe možete i da redefinišete (override) neke metode iz bazne klase. Evo jednostavnog primera:

class Parent:
    def __init__(self, a):
        self.a = a
    def method1(self):
        print(self.a*2)
    def method2(self):
        print(self.a+'!!!')

class Child(Parent):
    def __init__(self, a, b):
        self.a = a
        self.b = b
    def method1(self):
        print(self.a*7)
    def method3(self):
        print(self.a + self.b)

p = Parent('hi')
c = Child('hi', 'bye')

print('Parent method 1: ', p.method1())
print('Parent method 2: ', p.method2())
print()
print('Child method 1: ', c.method1())
print('Child method 2: ', c.method2())
print('Child method 3: ', c.method3())

Rezultat:

Parent method 1: hihi
Parent method 2: hi!!!

Child method 1: hihihihihihihi
Child method 2: hi!!!
Child method 3: hibye

U ovom primeru vidimo da je Child redefinisao method1 iz Parent klase, što rezultira da se string ponavlja sedam puta ( a ne 2 puta kao pre redefinisanja). Klasa Child je nasledila method2 od klase Parent, pa ne moramo da je ponovo definišemo. Klase Child takođe dodaje još neke osobine na klasu Parent, to jest dodaje novu varijablu b i novi metod method3.

Napomena u vezi sa sintaksom: kada nasleđujemo klasu, bazna klase se stavlja unutar zagrada u class naredbi.

Ako izvedena klasa dodaje nove verijable, ona može da pozove konstruktor bazne klase, kako je prikazano dole. Slično je kada metoda u izvedenoj klasi želi da napravi dodatak na metodu iz bazne klase. U donjem primeru izvedena print_var metoda poziva baznu print_var i na nju dodaje još jednu naredbu.

class Parent:
    def __init__(self, a):
        self.a = a

    def print_var(self):
        print("The value of this class's variables are:")
        print(self.a)

class Child(Parent):
    def __init__(self, a, b):
        Parent.__init__(self, a)
        self.b = b

    def print_var(self):
        Parent.print_var(self)
        print(self.b)

Napomena

Nasleđivati se mogu i Python built-in tipovi (klase), kao na primer stringovi (str) i liste (list), kao i bilo koja druga klasa u bilo kom od modula koji dođu sa Python-om.

Može se nasleđivati i iz više klasa istovremeno, ali to može biti malo zapetljano.

Program za igranje karata

Sada ćemo pokazati kako se pišu programi sa klasama. Napravićemo jednu jednostavnu igru sa kartama (Manja-Veća) u kojoj se korisniku podeli jedna karta a na njemu je da pogodi da li je sledeća karta manja ili veća. Ova igra može biti lako napravljena i bez upotrebe klase, ali mi ćemo kreirati dve klase, klasu karta i klasu špil, pa ćemo ove klase koristiti u programu za igu.

Počinjemo sa klasom koja reprezentuje pojedinačnu kartu. Podaci klase su vrednost karte (od 2 do 14) i boja karte. Klasa Card prikazana dole, pored konstruktora, ima samo još jednu metodu, __str__. Ovo je specijalna metoda koja pored ostalog pokazuje print funkciji kako da štampa Card objekat.

class Card:
    def __init__(self, value, suit):
        self.value = value
        self.suit = suit

    def __str__(self):
        names = ['Jack', 'Queen', 'King', 'Ace']
        if self.value <= 10:
            return '{} of {}'.format(self.value, self.suit)
        else:
            return '{} of {}'.format(names[self.value-11], self.suit)

Zatim imamo klasu koja reprezentuje grupu karata. Njeni podaci se sastoje od liste objekata iz klase Card . Ova klasa ima više metoda: nextCard koja uzima i vraća prvu kartu iz liste; hasCard koja vraća True ili False zavisno od toga da li u listi ima još karata; size, koja vraća broj karata u listi i metodu shuffle, koja promeša listu karata.

import random

class Card_group:
    def __init__(self, cards=[]):
        self.cards = cards

    def nextCard(self):
        return self.cards.pop(0)

    def hasCard(self):
        return len(self.cards)>0

    def size(self):
        return len(self.cards)

    def shuffle(self):
        random.shuffle(self.cards)

Imamo još jednu klasu Standard_deck, koja nasleđuje klasu Card_group. Ideja je da Card_group predtsvlja bilo kakvu grupu karata a Standard_deck predstvalja specifičnu grupu karata, odnosno standardni špil od 52 karte koji se načešćekoristi u igrama sa kartama.

class Standard_deck(Card_group):
    def __init__(self):
        self.cards = []
        for s in ['Hearts', 'Diamonds', 'Clubs', 'Spades']:
            for v in range(2,15):
                self.cards.append(Card(v, s))

Pretpostavimo da smo upravo kreirali standardni špil zajedno sa uobičajenom operacijom mešanja karata. Ako bi želeli da kreiramo novu klasu za igrenje recimo Preferansa ili neke druge igre koja se igra sa drugačijim špilom karata, morali bi da kopiramo i modifikujemo standardni špil. Ali pošto smo uveli klasu Card_group ne moramo ništa da kopiramo. Dovoljno je da novu klasu za Preferas napravimo nasleđivanjem klase Card_group, kao što je prikazano:

class Preferans_deck(Card_group):
    def __init__(self):
        self.cards = []
        for s in ['Hearts', 'Diamonds', 'Clubs', 'Spades']:
            for v in range(7,15):
                self.cards.append(Card(v, s))

Špil za preferans ima samo karte od sedmice do asa.

A sada evo i programa za igru Manja-Veća koji koristi gornje klase koje smo napravili. Jedan način na koji možemo da posmatramo napravljeni kod, jeste da razmišljamo o njemu kao o minijaturnom programskom jeziku za igranje karata, gde sada možemo da razmišljamo kako se igra sa kartma odvija, a ne moramo da brinemo kako se karte dele ili mešaju pošto smo te operacije „uvili“ u kreirane klase. Za igru Veća-Manja, uzimamo šipil karata, promešamo ga, a zatim delimo karte iz špila jednu po jednu. Kada podelimo sve karte uzmemo novi špil i promešamo ga. Jedna lepa osobima ove igre je što se karte dele iz špila od 52 karte tako da igrač može da koristi svoju memoriju da mu pomogne u igri.

deck = Standard_deck()
deck.shuffle()

new_card = deck.nextCard()
print('\n', new_card)
choice = input("Higher (h) or lower (l): ")
streak = 0

while (choice=='h' or choice=='l'):
    if not deck.hasCard():
        deck = Standard_deck()
        deck.shuffle()

    old_card = new_card
    new_card = deck.nextCard()

    if (choice.lower()=='h' and new_card.value>old_card.value or\
        choice.lower()=='l' and new_card.value<old_card.value):
        streak = streak + 1
        print("Right!  That's", streak, "in a row!")

    elif (choice.lower()=='h' and new_card.value<old_card.value or\
          choice.lower()=='l' and new_card.value>old_card.value):
        streak = 0
        print('Wrong.')
    else:
        print('Push.')

    print('\n', new_card)
    choice = input("Higher (h) or lower (l): ")

Rezultat:

King of Clubs
Higher (h) or lower (l): l
Right!  That's 1 in a row!

2 of Spades
Higher (h) or lower (l): h
Right!  That's 2 in a row!

Tik-tak-tu primer

A sada ćemo kreirati objektno orjentisanu verziju igre Tik-tak-tu. Koristićemo klasu da u nju „uvijemo“ logiku ove igre. Klasa sadrži dve varijable, jednu celobrojnu koja predstavlja redni broj igrača na potezu, i drugu koja je 3 x 3 reprezentacija tabele za igru. Tabela za igru se sastoji od nila, jedinica i dvojaka. Nule predtavljaju prazna mesta, dok jedinice i dvojke predstavljaju mesta označena od strane igrača, tako da su jedinice mesta koja je označio igrač 1, a dvojke mesta koja je označio igrač 2. Klasa ima i četiri metode:

get_open_spots — vraća listu mesta na tabeli koja još nisu označena od strane igrača is_valid_move — prihvata red i kolonu tabele za igru i vraća True ako je mesto slobodno, u suprotnom vraća False make_move — prihvata red i kolonu, poziva is_valid_move metodu da proveri da li je potez moguć, i u skladu sa tim označava to mesto u tabeli i menja varijablu koja pokazuje koji je igrač na potezu. check_for_winner — skenira tabelu za igru i vraća 1 ako je igrač 1 pobedio, 2 ako je igrač dva pobedio, 0 ako nema više mogućih poteza a niko nije pobedio, i vraća -1 ako igra treba da se nastavi

Evo kako izgleda ova klasa:

class tic_tac_toe:
    def __init__(self):
        self.B = [[0,0,0],
                  [0,0,0],
                  [0,0,0]]
        self.player = 1

    def get_open_spots(self):
        return [[r,c] for r in range(3) for c in range(3)
                if self.B[r][c]==0]

    def is_valid_move(self,r,c):
        if 0<=r<=2 and 0<=c<=2 and self.board[r][c]==0:
            return True
        return False

    def make_move(self,r,c):
        if self.is_valid_move(r,c):
            self.B[r][c] = self.player
            self.player = (self.player+2)%2 + 1

    def check_for_winner(self):
        for c in range(3):
            if self.B[0][c]==self.B[1][c]==self.B[2][c]!=0:
                return self.B[0][c]
        for r in range(3):
            if self.B[r][0]==self.B[r][1]==self.B[r][2]!=0:
                return self.B[r][0]
        if self.B[0][0]==self.B[1][1]==self.B[2][2]!=0:
            return self.B[0][0]
        if self.B[2][0]==self.B[1][1]==self.B[0][2]!=0:
            return self.B[2][0]
        if self.get_open_spots()==[]:
            return 0
        return -1

Ova klasa sadrži logiku igre. U klasi nema ničega što je vezano za korisnički interfejs. Dole imamo prikazan korisnički interfejs koji koristi input i print funkcije za unos i štampanje teksta. Ako u nekom trenutku odlučimo da koristimo grafički interfejs, i tada možemo koristiti bez ikakvih izmena klasu Tic_tac_toe . Uočite da se metoda get_open_spots ne koristi u donjem programu. Ona je međutim korisna ako budemo želeli da implementiramo igru u kojoj će jedan (ili oba) igrača biti sam kompjuter. Jedan prost način kako kompjuter može da preuzme ulogu jednog od igrača je da slučajno bira (random.choice funkcijom)svoj potez iz liste pogućih slododnih mesta.

def print_board():
    chars = ['-', 'X', 'O']
    for r in range(3):
        for c in range(3):
            print(chars[game.B[r][c]], end=' ')
        print()

game = tic_tac_toe()
while game.check_for_winner()==-1:
    print_board()
    r,c = eval(input('Enter spot, player ' + str(game.player) + ': '))
    game.make_move(r,c)

print_B()
x = game.check_for_winner()
if x==0:
    print("It's a draw.")
else:
    print('Player', x, 'wins!')

Evo kako bi mogli da izgledaju prvih nekolko poteza:

- - -
- - -
- - -
Enter spot, player 1: 1,1
- - -
- X -
- - -
Enter spot, player 2: 0,2
- - O
- X -
- - -
Enter spot, player 1:

Dodatne mogučnosti

Specijalne metode

Već smo videli dve specijalne metode, konstruktor metodu __init__ i __str__ koja određuje na koji će način objekat biti štampan . Postoji još mnogo drugin specijalnih metoda. Na primer, postoji metoda __add__ koja omogućava da vaši objekti koriste operator +. Zapravo postoje specijalne metode za sve Python operatore. Takođe postoji i metoda __len__ koja omogućava da vaši objekti koriste „built in“ funkciju len. Postoji čak i metoda __getitem__ omogućava vašem programu da radi sa listama i zagradama; [].

Kopiranje objekata

Ako želite da napravite kopiju nekog objekta x, nije dovoljno da napišete:

x_copy = x

Umesto toga treba uraditi ovako:

from copy import copy
x_copy = copy(x)

Čuvanje programskog koda u više fajlova

Ako neku klasu želite da koristite u više različitih programa, nije potrebno da primenjujete copy/paste klase za svaki od tih programa. Umesto toga, možete da sačuvate klasu u poseban fajl i da koristite import naredbu u programima gde se ta klasa koristi. Takav fajl mora biti smešten u nekom direktorijumu koji vaš program može da pronađe, ili u istom direktorijumu sa programom koji ga importuje.

from analizator import Analizator

Vežbe

14.1 Napišite klasu Investicija sa varijablama glavnica i kamata. Konstruktor klase treba da postavi vrednosti ovih varijabli. Klasa treba da ima metodu pod nazivom vrednost_posle koja vraća vrednost investicije posle n godina. Formula za to je g*(1+k)**n, gde je g glavnica, a k kamata. Takođe treba da postoji i specijalna metoda __str__ takva da štampanje objekta rezultira sledećim izgledom:

Glavnica - 1000.00, Kamatna stopa - 5.12%

14.2 Napišite klasu Proizvod. Klasa treba da ima varijable naziv,kolicina i cena koje sadže naziv proizvoda, broj proizvoda na skladištu i normalnu cenu proizvoda. Treba da postoji metoda daj_cenu koja prihvata broj proizvoda koji se kupuju i vraća cenu koju treba platiti za taj broj proizvoda, kada se normalna cena primenjuje za broj manji od 10, a popust od 10% za količinu između 10 i 99, a popust 20% za količinu veću od 100. Takođe treba da postoji i metoda napravi_prodaju koja prihvata broj proizvoda koji se kupuju i za taj broj umanjuje vrednost varijable kolicina.

14.3 Napisati klasu Password_manager. Klasa treba da sadrži listu old_passwords u kojoj se nalaze korisnikovi stari pasvordi. Poslednji pasvord u toj listi je korisnikov tekući pasvord. Treba da postoji metoda get_password koja vraća tekući pasvord i metoda set_password koja postavlja novi pasvord. Metoda set_password treba da menja tekući pasvord samo ako je novi pasvord različit od svih prethodnih pasvorda koje je korisnik do tada korsitio. Na kraju kreirajte i metodu is_correct koja prihvata string i vraća True ili False u zavisnosti od toga da li je string jednak tekućem pasvordu ili ne.

14.4 Napišite klasu Time čija jedina varijabla je vreme u sekundama. Klasa treba da sadrži metodu convert_to_minutes koja vraća string minuta i sekundi formatiziran kao u sledećem primeru: ako je broj sekundi 230, metoda treba da vrati ‘3:50’. Takođe treba da postoji metoda convert_to_hours koja vraća string sati, minuta i sekundi slično kao prethodna metoda.

14.5 Napišite klasu Wordplay. Klasa treba da sdrži listu reči. Korisnik ove klase treba da klasi prosledi listu reči koju će koristiti. Klasa treba da ima sledeće metode:

  • words_with_length(length) — vraća listu svih reči dužine length
  • starts_with(s) — vraća listu reči koje počinju stringom s
  • ends_with(s) — vraća listu reči koje se završavaju stringom s
  • palindromes() — vraća listu svih palindromskih reči iz liste reči
  • only(L) — vraća listu reči koje sadrže samo slova koja su u L
  • avoids(L) — vraća listu reči koje ne sadrže slova iz L

14.6 Napišite klasu Converter. Korisnik će proslediti dužinu i jedincu mere kada budeo kreirao objekat iz ove klase – na primer c = Converter(9, ‘inč’). Moguće jedinice su inč, fit, jard,milja,kilometar,metar,centimetar i milimetar. Za svaku od ovih jedinica treba da postoji metod koji vraća dužinu konverovanu u tu jedinicu. Na primer, korišćenjem Converter objekta kreiranog gore, korisnik može pozvati c.feet() i dobiti 0.75 kao rezultat.

14.7 Iskoristite klasu Standard_deck koja je definisana u ovom poglavlju, za kreiranje jedne uproščene verzije igre sa kartama koja se zove Rat. U ovoj igri postoje dva igrača. Svaki od njih počinje sa jednom polovinom špila. Svaki od igrača okreće po jednu kartu sa vrha špila. Jača karta dobija protivničku kartu i obe idu na dno pobednika. Ako je nerešeno obe karte se izbacuju iz igre. Igra se završava kada jedan od igrača ostane bez ijedne karte.

14.8 Napišite klasu koja nasleđuje klasu Card_group iz ovog poglavlja. Izvedena klasa treba da napravi špli karata koji sadrži herčeve (hearts) i pikove (spades), samo sa vrednostima karata od 2 do 10 u svakoj od ove dve boje. Dodajte i metod next2 koji vraća dve karte sa vrha špila.

14.9 Napišite klasu Rock_paper_scissors koja implementira logiku igre Rock-paper-scissors. U ovoj igri igrač igra protiv kompjutera određeni broj rundi. Klasa treba da ima varijablu koja sadrži broj rundi, tekuću rundu, kao i broj pobeda za svakog igrača. Treba da postoje metode za dobijanje kompjuterskog izbora poteza, odrđivanje pobednika runde, i provere da li je neko ukupni pobednik cele igre. Možete dodati i druge metode koje smatrate pogodnim za ovu igru.

14.10 Napišite klasu Poker_hand koja sadrži listu Card objekata. Klasa treba da sadrži sledeće metode:

  • has_royal_flush, has_straight_flush, has_four_of_a_kind,
  • has_full_house, has_flush, has_straight,
  • has_three_of_a_kind, has_two_pair, has_pair
  • Takođe treba da postoji metoda best koja vraća string koji pokazuje koja je najjača „ruka“ koja se može napraviti od raspoloživih karata.