Arcade-pelien ohjelmointi Pythonilla ja Pygamella

Arcade-pelien ohjelmointi
Pythonilla ja Pygamella

Chapter 12: Luokkien käyttö ohjelmoinnissa

Luokat ja objektit ovat erittäin ilmaisuvoimaisia ohjelmoinnissa. Niiden avulla ohjelmoinnista tulee paljon helpompaa. Itse asiassa oletkin jo tutustunut luokkiin ja objekteihin. Luokka (class) on tavallaan luokittelu (“classification”) objekteista. Esimerkikiksi “person” tai “image.” Objekti on luokasta ilmentymä eli instanssi, joka on luokan 'tyyppiä'. Esim. “Mary” voisi olla luokan “Person.” instanssi (eli objekti).

Objekteilla on attribuutteja; kuten henkilön nimi, paino ja ikä. Objekteilla voi olla metodeja. Methodit määräävät mitä objekti voi tehdä; kuten juosta, hypätä, istua jne.

12.1 Miksi opettelemme luokkien ohjelmointia?

Video: Why Classes?

Kaikki pelin hahmot tarvitsevat dataa: nimen, sijainnin, voiman, missä asennossa esim. kädet ovat, mihin suuntaan hahmo osoittaa jne. Ja lisäksi hahmo tekee joitain asioita. Ne hyppävät, juoksevat, lyövät, puhuvat jne.

Ilman luokkien käyttöä ohjelmamme Python-koodi voisi näyttää tältä:

name = "Link"
sex = "Male"
max_hit_points = 50
current_hit_points = 50

Seuraavaksi mitä tahansa hahmolle tapahtuukin, pitää kaikki tapahtumadata siirtää funktioiden käsiteltäväksi:

def display_character(name, sex, max_hit_points, current_hit_points):
    print(name, sex, max_hit_points, current_hit_points)

Kuvittelepa seuraavaksi ohjelma, jossa jokaisella pelissä olevalla objektilla olisi tuollainen muuttujajoukko. Ja jokainen muuttuja tarvitsee funktion, jossa se käsiteltäisiin. Tästä seuraisi lohduton räpiminen muuttujien suossa. Ei kuulosta enää viisaalta ohjelmoinnilta.

Ja aina pahemmaksi menee! Ajattele, että peliin lisättäisiin jokin uusi ominaisuus; esimerkiksi max_speed:

name = "Link"
sex = "Male"
max_hit_points = 50
current_hit_points = 50
max_speed = 10

def display_character(name, sex, max_hit_points, current_hit_points, max_speed):
    print(name, sex, max_hit_points, current_hit_points)

Yllä esimerkissä on vain yksi funktio, mutta hieman laajemmissa peleissä saattaa olla satoja funktioita käsittelemässä yhden objektin muutoksia. Tässä pitäisi siis käydä kaikki funktiot läpi ja lisätä jokaiseen tämä parametri. Siinä olisi siis paljon työtä. Tähän olisi siis paljon 'fiksumpi' ratkaisu. Jotenkin siis pakkaamme nuo pelissä käsiteltävät tietokentät, jotta niitä voidaan käsitellä tehokkaammin.

12.2 Yksinkertaisen luokan määrittäminen

Video: Defining Simple Classes

Moninaiset attribuuttijoukot saamme hallittua paremmin määrittämällä rakenteen, joka sisältää kaiken tiedon. Sitten annamme tälle “tietojoukolle” nimen, esimerkiksi Hahmo tai Osoite. Tämä voidaan tehdä helposti Pythonilla ja monilla muilla moderneilla ohjelmointikielillä käyttämällä luokkia.

Esimerkiksi, määritetään pelissä käytettävä Character (Hahmo) luokka:

class Character():
    """ Tämä luokka määrittelee pelissä olevan hahmon. """
    def __init__(self):
        """ Tämä metodi asettaa objektin muuttujien arvot. """
        self.name = "Link"
        self.sex = "Male"
        self.max_hit_points = 50
        self.current_hit_points = 50
        self.max_speed = 10
        self.armor_amount = 8

Toisessa esimerkissä määritetään luokka, jossa on kaikki osoitetiedon kentät:

class Address():
    """ Sisältää kaikki postiosoitetiedon kentät. """
    def __init__(self):
        """ Asettaa kenttien tiedot. """
        self.name = ""
        self.line1 = ""
        self.line2 = ""
        self.city = ""
        self.state = ""
        self.zip = ""

Koodissa on Address, joka on luokan nimi. Luokan muuttujia ovat esimerkiksi name ja city, ja niitä kutsutaan attribuuuteiksi tai kentiksi. (Huomaathan samankailtaisuudet ja eroavaisuudet luokan ja funktion määrittelyissä.)

Poiketen funktion ja muuttujan nimeämisestä, luokan nimen tulee alkaa ISOLLA alkukirjaimella. Vaikka luokan nimen voi kirjoittaa pienellä kirjaimella, niin sitä ei pidetä hyvänä ohjelmointitapana.

def __init__(self): on erikoisfunktio, constructor, joka kutsuu itseään automaattisesti, kun luokka luodaan. Puhutaan konstruktoreista hieman lisää.

Varattu sana self. on eräänlainen (pronomini) viittaus itseen. Kun luokassa Address puhutaan minun nimestä, minun kaupunkista (city), jne. Emme voi viitata varatulla sanalla self. luokan määrityksen ulkopuolella luokkaan Address, jos haluamme viitata luokan Address kenttään. Mutta miksi ei? No, siksi, että sana (pronomini) “minä,” tarkoittaa aina eri henkilöa, kun kyseessä on eri henkilö!

Luokan ja luokkasuhteiden visualisoimikseksi voidaan piirtää erilaisia diagrammeja. Luokkaa Address kuvaava diagrammi voisi olla oheisen kuvan kaltainen 12.1. Luokan nimi on ylimmäisenä ja sen alapuolella on listattuna kaikki atrribuutit. Attribuutin datatyyppi on mainittuna sen oikealla puolella, esim String, integer.

fig.address_example_3
Figure 12.1: Class Diagram

Luokan koodi määrittelee luokan, mutta se ei saa aikaiseksi yhtään luokan instanssia eli ilmentymää (objektia). Koodi kertoo sen, mitä kenttiä luokassa on ja mitkä ovat kenttien oletusarvot. Luokan määrittämisen jälkeen meillä ei ole yhtään osoitetta. Voimme määrittää luokan luomatta yhtään sen ilmentymää, aivan kuten voimme määrittää funktion kutsumatta sitä milloinkaan. Alla esimerkissä luodaan Address-luokan ilmentymä ja annetaan sen kentille alkuarvot:

# Luodaan address-luokan olio
home_address = Address()

# Asetetaan address-olion kentille arvot
home_address.name = "John Smith"
home_address.line1 = "701 N. C Street"
home_address.line2 = "Carver Science Building"
home_address.city = "Indianola"
home_address.state = "IA"
home_address.zip = "50125"

Rivillä 2 luodaan address luokan olio eli instanssi. Huomaa, kuinka luokan Address nimi on kirjoitettu päättyen kaarisulkeisiin. Muuttujan nimi voi olla mitä tahansa nimeämissäännön puitteissa.

Kenttien arvojen asettaminen tehdään pistenotaatiolla. Pistenotaatiossa piste kirjoitetaan luokan oliomuuttujan home_address ja kentän väliin. Katso riveiltä 5-10 kuinka pistenotaatiolla asetetaan kenttien arvot.

Luokkien käytössä tehdään yleisesti se osoitusvirhe, että toiminnot kohdistetaan luokkaan, ei olioon. Toiminnot tulee kuitenkin kohdistaa olioon. Jos luodaan vain yksi osoite, niin koneen voi olettaa käyttävän tätä osoitetta. Käytännössä tilanne ei kuitenkaan ole tällainen. Katso esimerkki alla:

class Address():
    def __init__(self):
        self.name = ""
        self.line1 = ""
        self.line2 = ""
        self.city = ""
        self.state = ""
        self.zip = ""

# Luodaan osoiteolio
my_address = Address()

# Huom! Tämä ei aseta nimikentän arvoa!
name = "Dr. Craven"

# Tämäkään ei aseta nimiolion kentän arvoa
Address.name = "Dr. Craven"

# Tämä toimii hienosti
my_address.name = "Dr. Craven"

Jos luokasta luodaan toinen olio, niin molempien address-olioiden kenttien arvoja voidaan asettaa ja käyttää. Katso ao. esimerkki:

class Address():
    def __init__(self):
        self.name = ""
        self.line1 = ""
        self.line2 = ""
        self.city = ""
        self.state = ""
        self.zip = ""

# Luodaan address-olio
home_address = Address()

# Asetetaan olion kenttien arvot
home_address.name = "John Smith"
home_address.line1 = "701 N. C Street"
home_address.line2 = "Carver Science Building"
home_address.city = "Indianola"
home_address.state = "IA"
home_address.zip = "50125"

# Luodaan toinen olio
vacation_home_address = Address()

# Asetetaan tämän toisen olion kenttien arvot
vacation_home_address.name = "John Smith"
vacation_home_address.line1 = "1122 Main Street"
vacation_home_address.line2 = ""
vacation_home_address.city = "Panama City Beach"
vacation_home_address.state = "FL"
vacation_home_address.zip = "32407"

print("The client's main home is in " + home_address.city)
print("His vacation home is in " + vacation_home_address.city)

Rivillä 11 luodaan ensimmäinen Addressolio; rivillä 22 luodaan toinen olio. Oliomuuttuja home_address osoittaa/viittaa ensimmäiseen olioon ja oliomuuttuja vacation_home_address viittaa toiseen olioon.

Riveillä 25-30 asetetaan toisen olion kenttien arvot. Rivillä 32 printataan kotiosoite (home address), koska home_address on pistenotaatiossa ensin mainittu oliomuuttujan nimi. Rivillä 33 printataan loma-asunnon osoite, kun vacation_home_address on oliomuuttujana pistenotaatiossa.

Esimerkissä Address ia kutsutaan luokaksi (class) koska se määrittelee uuden datatyypin. Muuttujia home_address ja vacation_home_address kutsutaan olioiksi (object) ja ne ovat luokan Address tyyppiä. Yksinkertaisesti sanottuna ne ovat luokan instansseja eli objekteja. Aivan kuten “Pekka” ja “Liisa” ovat luokan Human instansseja.

Käyttämällä www.pythontutor.com voidaan visualisoida koodin toimintaa (katso alla). Ohjelmassa on kolme muuttujaa. Yksi osoittaa luokan määritykseen Address. Kaksi muuta muuttujaa osoittavat luokasta luotuihin oliomuuttujiin ja niihin tallennettuihin kenttien tietoihin.

fig.two_addresses
Figure 12.2: Two Addresses

Laittamalla luokan kenttiin paljon tietoa, voidaan tietoja välittää funktioiden avulla. Alla olevassa koodiesimerkissä funktio saa osoitteen parametrina ja se tulostetaan näytölle. Kaikkia address-olion kenttiä ei tarvitse välittää parametreina.

# Tulosta osoite näytölle
def print_address(address):
    print(address.name)
    # Jos on saatavilla line1, tulosta se
    if len(address.line1) > 0:
        print(address.line1)
    # Jos löytyy line2, niin tulostetaan
    if len(address.line2) > 0:
        print( address.line2 )
    print(address.city + ", " + address.state + " " + address.zip)

print_address(home_address)
print()
print_address(vacation_home_address)

12.3 Lisätään luokkiin metodeja

Video: Methods

Attribuuttien lisäksi luokissa voi olla metodeja. Metodi on luokan sisällä oleva funktio.Laajennetaan aiempaa esimerkkiluokkaa Dog liisäämällä siihen metodi koiran haukunta bark.

class Dog():
    def __init__(self):
        self.age = 0
        self.name = ""
        self.weight = 0

    def bark(self):
        print("Woof")

MEtodin määrittelyrivit ovat koodissa riveillä 7-8. Metodin määrittely näyttää hyvin samanlaiselta kuin funktion määrittely. Suurimpana erona on parametri self rivillä 6. Ensimmäinen parametri tulee metodissa olla aina self. Tämä self parametri tulee olla vaikka metodia ei kutsuttaisikaan.

Alla on listattuna tärkeimmät seikat jotka pitää huomioida luotaessa metodeja luokkiin:

Metodeja voidaan kutsua pistenotaatiolla samalla tavalla kuin objektin attribuuttejakin. Katso alla olevaa esimerkkiä.

my_dog = Dog()

my_dog.name = "Spot"
my_dog.weight = 20
my_dog.age = 3

my_dog.bark()

Rivillä 1 luodaan my-dog objekti ja riveillä 3-5 asetetaan objektin atrribuuttien arvot. Rivillä 7 on bark funktion kutsu. Huomaa, että kutsussa ei ole välitettävää parametria vaikka itse bark funktiossa on yksi parametri, self. Tämä toimii, koska ensimmäisen parametrin oletetaan olevan viittaus dog objektiin itseensä. Pythonin voisi ajatella tekevän siis seuraavanlaisen kutsun:

# Esimerkki, joka ei ole suositeltava, eikä laillinen
Dog.bark(my_dog)

Jos bark funktion pitää viitata johonkin attribuuttiin, niin se onnistuu viittaamalla self muuttujaan. Esimerkiksi, voimme muuttaa Dog luokkaa siten, että kun koira haukkuu, niin samalla tulostetaan koiran nimi. Alla esimerkissä name attribuutti saadaan käyttöön pistenotaatiolla self viittauksella.

    def bark(self):
        print("Woof says", self.name)

Attribuutit ovat adjektiiveja ja metodit ovat verbejä. Luokan kaavion voi piirtää oheisen kuvan 12.3 mukaiseksi.

fig.dog_2_1
Figure 12.3: Dog Class

12.3.1 Vältä tämä virhe

Kirjoita kaikki metodin toiminnat yhteen määrittelyyn. älä siis tee määrittelyä kahdesti. Esimerkiksi:

# Väärin:
class Dog():
    def __init__(self):
        self.age = 0
        self.name = ""
        self.weight = 0

    def __init__(self):
        print("New dog!")

Tietokone ohittaa ensimmäisen __init__ määrittelyn ja aloittaa jälkimmäisestä. Tee siis näin:

# Oikein:
class Dog():
    def __init__(self):
        self.age = 0
        self.name = ""
        self.weight = 0
        print("New dog!")

12.3.2 Esimerkki: Pallo-luokka

Tätä esimerkkikoodia voi käyttää Python/Pygamessa pallojen piirtämiseen. Laittamalla kaikki parametrit luokan sisään voidaan tietojen hallintaa yksinkertaistaa. Kaaviokuva Ball luokasta on oheisessa kuvassa 12.4.

fig.ball_2_1
Figure 12.4: Ball Class
class Ball():
    def __init__(self):
        # --- Luokan attribuutit ---
        # Pallon sijainti
        self.x = 0
        self.y = 0

        # Pallon vektori
        self.change_x = 0
        self.change_y = 0

        # Pallon koko
        self.size = 10

        # Pallon väri
        self.color = [255,255,255]

	# --- Luokan metodit ----
    def move(self):
        self.x += self.change_x
        self.y += self.change_y

    def draw(self, screen):
        pygame.draw.circle(screen, self.color, [self.x, self.y], self.size )

Alla olevan koodin voi sijoittaa ohjelman pääsilmukkaan. Tällä luodaan Ball-olioita ja asetetaan sen attribuutit:

theBall = Ball()
theBall.x = 100
theBall.y = 100
theBall.change_x = 2
theBall.change_y = 1
theBall.color = [255,0,0]

Tämä koodi laitetaan pääsilmukkaan ja sillä saadaan siirrettyä ja piirrettyä pallo:

theBall.move()
theBall.draw(screen)

12.4 Viittaukset

Video: References

Tässä kohdassa erottelemme koodaajat ja aloittelijat. Luokan viittausten ymmärtäminen erottelee aloittelijat ja taitavammat koodaajat. Katso seuraavaa esimerkkiä:

class Person:
    def __init__(self):
        self.name = ""
        self.money = 0

bob = Person()
bob.name = "Bob"
bob.money = 100

nancy = Person()
nancy.name = "Nancy"

print(bob.name, "has", bob.money, "dollars.")
print(nancy.name, "has", nancy.money, "dollars.")

Koodi luo kaksi Person() luokan ilmentymää (objektia). Käyttämällä www.pythontutor.com we voimme visualisoida luokkaa kuvalla 12.5.

fig.two_persons
Figure 12.5: Two Persons

Koodissa ei ole mitään uutta. Mutta seuraavassa koodissa on:

class Person:
    def __init__(self):
        self.name = ""
        self.money = 0

bob = Person()
bob.name = "Bob"
bob.money = 100

nancy = bob
nancy.name = "Nancy"

print(bob.name, "has", bob.money, "dollars.")
print(nancy.name, "has", nancy.money, "dollars.")

Katso eroa rivillä 10, mitä huomaat?

Yleisesti tässä ymmärretään virheellisesti, että muuttuja bob on Person objekti. Näin ei siis ole. Muuttuja bob on viittaus Person objektiin. Tämä tarkoittaa, että objektin muistiosoite tallennetaan muuttujan nimeen, ei siis itse objektia. (Kääntäjän huom! tässä yhteydessä puhutaan viittaustyypin muuttujista).

Jos bob olisi itse objekti (viittauksen sijaan), luotaisiin rivillä 10 kopio objektista ja sen jälkeen olisi kaksi samaa objektia. Ohjelma tulostaisi, että molemmilla, sekä Bobilla että Nancylla, olisi 100 dollaria. Mutta, kun ohjelma ajetaan, tulostuukin:

Nancy has 100 dollars.
Nancy has 100 dollars.

Tässä bob on muuttujaan tallennettu viittaus objektiin. Rivillä 10 viittaus kopioidaan myös nancy muuttujaan. Viittausta kutsutaan myös nimillä osoite, osoitin, tai kahva. Viitaustyypin osoite (muistiosooite) on heksaluku, joka tulostettuna voisi näyttää esimerkiksi tällaiselta 0x1e504. Viittauksen kopioituminen on esitetty kuvassa 12.6.

fig.ref_example1
Figure 12.6: Class References

Tämä voidaan suorittaa myös www.pythontutor.com jolloin näemme, että molemmat muuttujat osoittavat samaan objektiin.

fig.one_person
Figure 12.7: One Person, Two Pointers

12.4.1 Funktiot ja viittaukset

Katso alla olevaa koodiesimerkkiä. Rivillä 1 määritetään funktio, jolla on yksi parametri. Muuttuja money on muuttuja, johon välitetään arvo funktion kutsusta. Vaikka muuttujaan lisätään 100, niin se ei muuta rivillä 10 olevaa olion muuttujaa bob.money. Joten, tulostus rivillä 13 tulostaa 100, ei 200.

def giveMoney1(money):
    money += 100

class Person:
    def __init__(self):
        self.name = ""
        self.money = 0

bob = Person()
bob.name = "Bob"
bob.money = 100

giveMoney1(bob.money)
print(bob.money)

Ajamalla PythonTutor huomaamme, että muuttujasta money luodaan kaksi ilmentymää. Toinen on paikallinen kopio, joka luodaan giveMoney1 funktiossa.

fig.function_references_1
Figure 12.8: Function References

Katsotaan lisäesimerkki tästä koodista. Tämä saa aikaan bob.money arvon muutoksen ja tulostaa arvon 200.

def giveMoney2(person):
    person.money += 100

giveMoney2(bob)
print(bob.money)

Miten tämä toimii? Parametri person sisältää kopion bob-olion muistipaikan osoitteesta, ei siis itse oliota. Funktiossa tilin saldon kasvatus kohdistuu oliomuuttujan kenttään money. Eli parametri person osoittaa samaan muistipaikkaan kuin bob olio. Tällä tavalla saadaan muutettua olion kentän arvoa.

fig.function_references_2
Figure 12.9: Function References

Taulukot ja listat toimivat samalla tavalla. Funktio, joka saa parametrinaan taulukon tai listan viittaa (muokkaa) samaa taulukko-oliota kuin kutsussa välitetty parametri. Tässä siis kopioituu taulukon osoite, ei kopiota taulukosta.

12.4.2 Kertauskysymyksiä

  1. Luo uusi luokka nimeltään Cat. Anna attribuuteiksi name, color, ja weight. Tee luokkaan metodi meow.
  2. Luo luokasta instanssi ja asteta attribuuteille arvot. Kutsu metodia meow.
  3. Luo luokka nimeltään Monster. Anna attribuutti name ja kokonaisluku attribuutti health. Luo metodi nimeltään decreaseHealth, jolla on parametrina amount ja joka kasvattaa terveyttä parametrin arvon verran. Kirjoita metodiin lause, joka tulostaa, että otus kuolee, jos terveys menee alle nollan.

12.5 Konstruktorit eli luontimetodit

Video: Constructors

Oheisessa koiran olioluokassa on ongelma. Kun Dog luokasta luodaan olio, niin oletuksena sillä ei ole nimeä. Koiralla pitäisi olla nimi! Alla oleva koodiesimerkki sisältää vielä tämän puutteen ja sallii koiraolion luonnin ilman nimeä.

class Dog()
    def __init__(self):
        self.name = ""

my_dog = Dog()

Pythonissa tällaista ei saisi tapahtua. Siksi Pythonissa on luokkia varten erikoisfunktio, jota kutsutaan aina kun uusi olio luodaan luokasta. Lisäämällä luokkaan konstruktorin, suoritetaan konstruktori aina, kun luodaan uusi olio. Konstruktorissa voidaan olion muuttujille antaa haluttaessa oletusarvot. Katso alla olevaa koodia:

class Dog():
    def __init__(self):
        self.name = ""

    # Konstruktori
    # Kutsutaan, kun luodaan tämän tyyppinen olio
    def __init__(self):
        print("A new dog is born!")

# Tämä luo koiraolion
my_dog = Dog()

Konstruktori alkaa riviltä 6. Se pitää nimetä __init__. Tässä on kaksi alaviivaa ennen init sanaa ja kaksi sen jälkeen. Yleinen virhe on kirjoittaa vain yksi alaviiva.

Konstruktorissa pitää olla ensimmäisenä parametrina self kuten myös muillakin luokan metodeilla. Kun ohjelma suoritetaan, se tulostaa:
A new dog is born!
Kun Dog objekti on luotu rivillä 10, __init__ funktiota kutsutaan automaattisesti ja teksti tulostetaan ruudulle.

Konstruktoria voidaan käyttää olion kenttien alustamiseen. Esimerkiksi Dog luokka yllä sallii name attribuutin tyhjäksi luotaessa oliota. Kuinka tässä siis meneteltäisiin? Monet objektit tarvitsevat oikeat alkuarvot luonnin yhteydessä. Tämä saadaan tehtyä konstruktorilla. Katso alla olevaa koodia:

class Dog():

    # Konstruktori
    # kun luodaan tämän tyypin objekti
    def __init__(self, new_name):
        self.name = new_name

# Tässä luodaan koiraolio
my_dog = Dog("Spot")

# Printtaa nimen, jotta nähdään, että se on asetettu
print(my_dog.name)

# Tältä riviltä generoituu virhe, koska 
# ei ole parametria välitettäväksi
herDog = Dog()

Rivillä 3 konstruktorissa on yksi lisäparametri new_name. Tätä paarametrin arvoa käytetään asettamaan koiraolion muuttujan name arvo Dog luokassa rivillä 7. Enää ei ole mahdollista luoda koiraa Dog luokasta siten, että sillä ei olisi nimeä. Koodissa rivillä 15 tätä yritetään. Tästä seuraa Pythonin generoima virhe ja ohjelmaa ei suoriteta. Toisinaan virheellisesti nimetään __init__ funktion parametri samalla tavalla kuin olion attribuutti ja kuvitellaan että arvot synkronoituvat automaattisesti. Tämä ei tietenkään ole totta.

12.5.1 Kertaavia kysymyksiä

  1. Pitääkö luokan nimi kirjoittaa isolla vai pienellä alkukirjaimella?
  2. Pitääkö metodien nimet kirjoittaa isolla vai pienellä alkukirjaimella?
  3. Pitääkö atrribuuttien nimet kirjoittaa isolla vai pienellä alkukirjaimella?
  4. Kummatko pitää olla luokassa ensin attribuutit vai metodit?
  5. Mitä muita nimityksiä viittauksesta käytetään?
  6. Mitä muita nimityksiä oliomuuttujasta käytetään?
  7. Mitä nimitystä luokan oliosta käytetään?
  8. Luo luokka nimeltään Star, joka tulostaa “A star is born!” joka kerta kun luokasta luodaan olio.
  9. Luo luokka nimeltään Monster jolla on attribuutteina health ja name. Lisää konstruktori, joka asettaa health ja name attribuuteille arvot. Arvot tulee välittää parametreina.

12.6 Perintä

Video: Inheritance

Toinen tehokas ominaisuus luokkien ja objektien käytössä on mahdollisuus käyttää olioiden perintää. On siis mahdollista luoda luokka, joka perii kaikki ominaisuudet, attribuutit ja metodit isäluokalta.

Esimerkiksi, jos luodaan luokka nimeltään Boat, jossa on kaikki tarvittavat ominaisuudet pelissä käytettävään veneeseen:

class Boat():
    def __init__(self):
        self.tonnage = 0
        self.name = ""
        self.isDocked = True

    def dock(self):
        if self.isDocked:
            print("You are already docked.")
        else:
            self.isDocked = True
            print("Docking")

    def undock(self):
        if not self.isDocked:
            print("You aren't docked.")
        else:
            self.isDocked = False
            print("Undocking")

Testataan koodia:

b = Boat()

b.dock()
b.undock()
b.undock()
b.dock()
b.dock()

Outputtina saadaan:

You are already docked.
Undocking
You aren't docked.
Docking
You are already docked.

(Jos katsot tämän kappaleen luokka käsittelevän videon, huomaat, että "Boat" luokka ei toiminut kunnolla. Yllä oleva koodi on toimiva, mutta videolle onm jäänyt virheellinen koodi. Kannattaa siis oppia tästä ja testaa koodin aina huolella ennen jakamista :) )

Ohjelmassa tarvitaan myös sukellusvene. Sukellusveneellä on kaikki samat ominaisuudet kuin veneelläkin. Lisäksi sukellusvene voi sukeltaa komennolla submerge. Ilman perintää meillä on kaksi mahdollisuutta.

Onneksi on parempikin tapa. Voimme luoda luokasta aliluokan, joka perii kaikki yliluokan attribuutit ja metodit. Aliluokkaan voidaan lisätä attribuutteja ja metodeja, jotka kuvaavat luokan olioiden ominaisuuksia paremmin. Esimerkiksi:

class Submarine(Boat):
    def submerge(self):
        print("Submerge!")

Rivi 1 on tärkeä. Laittamalla sulkeisiin Boat saamme automaattisesti poimittua käyttöömme kaikki attribuutit ja metodit, jotka ovat luokassa Boat valmiina. Jos luokkaa Boat päivitetään tai muutetaan, niin Submarine luokassa muutokset tulevat automaattisesti. Perintä on Pythonissa näin helppoa!

Seuraavaa koodia esittävä diagrammin on kuvassa 12.10.

fig.person1
Figure 12.10: Class Diagram
class Person():
    def __init__(self):
        self.name = ""

class Employee(Person):
    def __init__(self):
        # kutsutaan yliluokan (isäluokan) konstruktoria ensiksi
        super().__init__()

        # Nyt asetetaan muuttujien arvot
        self.job_title = ""

class Customer(Person):
    def __init__(self):
        super().__init__()
        self.email = ""

john_smith = Person()
john_smith.name = "John Smith"

jane_employee = Employee()
jane_employee.name = "Jane Employee"
jane_employee.job_title = "Web Developer"

bob_customer = Customer()
bob_customer.name = "Bob Customer"
bob_customer.email = "send_me@spam.com"

Asettamalla Person sulkeisiin riveillä 5 ja 13, ohjelmoija kertoo koneelle, että Person on yliluokka molemmille aliluokille Employee ja Customer. Tässä saadaan asetettua name attribuuutit riveillä 17 ja 21.

Methodit perityvät myös. Aliluokan olioilla on kaikki metodit, jotka ovat yliluokallakin. Mutta mitä, jos sama metodi on sekä yliluokassa että aliluokassa?

Meillä on kaksi mahdollisuutta. Voimme ajaa yliluokan metodin käyttämällä super() varattua sanaa. Kirjoittamalla super() sanan perään pisteen ja yliluokan metodin nimen saadaan kutsuttua yliluokkassa määritettyä metodia.

Yllä olevassa koodissa on esitettynä sana super jolla saadaan aliluokan konstruktorin lisäksi suoritettua myös yliluokan konstruktori.

Jos kirjoitata metodia aliluokkaan ja haluat kutsua yliluokan metodia, normaalisti se kirjoitetaan ensimmäiseksi lauseeksi aliluokassa. Huomaa tämä yllä olevassa koodiesimerkissä.

Kaikkien konstruktoreiden pitäisi kutsua yliluokan konstruktoria, koska muutoin aliluokan objekteilla ei ole yliluokan metodeissa esitettyjä ominaisuuksia. Joissakin ohjelmointikielissä tämä sääntö on pakollinen, mytta Pythonissa ei ole.

Toinen mahdollisuus? Metodit voidaan uudelleen määritellä eli ylikirjoittaa (override) aliluokassa ja antaa niille yliluokan metodeista poikkeavia ominaisuuksia. Alla oleva koodiesimerkki sisältää molemmat tapaukset. Employee.report määrittelee uudelleen Person.report metodin ja yliluokan metodia ei koskaan kutsuta. Customer report metodi kutsuu yliluokan report metodia ja Customer on lisätty ominaisuuden Person luokan metodin lisäksi.

class Person():
    def __init__(self):
        self.name = ""

    def report(self):
        # perusraportti
        print("Report for", self.name)

class Employee(Person):
    def __init__(self):
        # kutsuu yliluokan (parent/super class) konstruktoria ensin
        super().__init__()

        # asetetaan muuttuja
        self.job_title = ""

    def report(self):
        # tässä määritellään uudelleen report ja suoritetaan vain tämä:
        print("Employee report for", self.name)

class Customer(Person):
    def __init__(self):
        super().__init__()
        self.email = ""

    def report(self):
        # suoritetaan yliluokan report:
        super().report()
        # Lisätään vielä oma juttu loppuun ja suoritetaan molemmat
        print("Customer e-mail:", self.email)

john_smith = Person()
john_smith.name = "John Smith"

jane_employee = Employee()
jane_employee.name = "Jane Employee"
jane_employee.job_title = "Web Developer"

bob_customer = Customer()
bob_customer.name = "Bob Customer"
bob_customer.email = "send_me@spam.com"

john_smith.report()
jane_employee.report()
bob_customer.report()

12.6.1 Is-A ja Has-A suhteet perinnässä

Luokkien välillä on kahdenlaisia suhteita, “is a” ja “has a” suhteet.

Yliluokka on aina yleisemmin määritelty kuin aliluokka. Se on siis abstraktimpi kuin aliluokka. Tämän tyyppistä perinnän tapaa ja suhdetta kutsutaan is a suhteeksi. Esimerkiksi, yliluokka Animal voisi saada aliluokakseen luokan Dog. Dog luokalla voisi olla aliluokkana edelleen luokka Poodle. Toinen esimerkki, delfiini on (is a) nisäkäs ja on siis Nisäkäs-luokan aliluokka. Toisin päin tämä perintäsuhde ei toimi, sillä kaikki nisäkkäät eivät ole delfiinejä. Näin siis luokka Delfiini ei voi olla luokan Nisakas yliluokka. Samalla tapaa luokka Table ei voi olla luokan Chair yliluokka, koska tuoli ei ole koskaan pöytä.

Toinen peintähierarkiaan liittyvä käsite on has a perintäsuhde. Tässä perintä on implementoitu luokan koodiin attribuuttien avulla. Koiralla on nimi ja siten luokalla Dog on attribuuttina name. Samoin henkilöllä voi olla koira, ja näin voitaisiin implementoida Person luokkaan attribuutti Dog. Person -luokka ei kuitenkaan periydy luokasta Dog, olisihan se jotenkin omalaatuista.

Katsotaan vielöä edellä esitettyä koodia:

12.7 Staattiset luokkamuuttuja ja oliomuuttujat

Staattisten luokkamuuttujien ja oliomuuttujien välinen ero on hieman sekava. Onneksi näiden eroa ei tässä vaiheessa tarvitse täysin ymmärtääkään. Jos perehdyt ohjelmointiin syvällisemmin, tulee tämäkin asia selvittää. Esitetään siis tästä lyhyesti seuraavaa.

Pythonissa on muutamia kummallisuuksia, jotka voivat aiheuttaa sekaannuksia. Siksi materaiaalin alkuperäisen kirjoittajan tekemissä ensimmäisissä videoissa saattaa olla joitakin asioitaesitetty virheellisesti.

Instanssimuuttuja on luokan tyyppiä, kuten tähän asti olemme oppineet. Jokainen luokan instanssi saa oman arvon. Esimerkiksi huoneessa olvat ihmiset ovat jokainen eri-ikäisiä. Joillakin voi olla sama ikä, mutta jokaisella on oma yksilöllinen ikä.

Kun puhumme huoneessa olevien ihmisten iästä, meidän pitää tarkoin määrittää kenen iästä on kyse. Jos huoneessa ei ole ihmisiä laisinkaan, on myös heidän iästään puhuminen järjetöntä.

Staattisella muuttujalla saadaan kaikkien olioiden muuttujien arvo samaksi. Silloinkin, vaikka ei olisi luotu yhtään oliota, staattisella muuttujalla on arvo. Esimerkiksi, meillä voisi olla staattinen muuttuja count, jolla esitetään lukumäärää Human luokassa. Jos ihmisiä ei ole, niin arvo on nolla. Arvo siis on joka tapauksessa olemassa.

Alla olevassa esimerkissä, ClassA luodaan instanssimuuttuja. ClassB:ssä on sitä vastoin staattinen muuttuja.

# Esimerkki instanssimuuttujasta
class ClassA():
    def __init__(self):
        self.y = 3

# Esimerkki staattisesta muuttujasta
class ClassB():
    x = 7

# Luodaan luokan instanssit
a = ClassA()
b = ClassB()

# Kaksi tapaa tulostaa staattisen muuttujan arvo
# Jälkimmäinen on se oikea tapa. 
print(b.x)
print(ClassB.x)

# Yksi tapa tulostaa instanssimuuttujan arvo
# Toinen saa aikaan virheilmoituksen, koska ei ole tietoa, mihin olioon käsky kohdistuu
print(a.y)
print(ClassA.y)

Yllä olevan esimerkin rivit 16 ja 17 tulostavat staattisen muuttujan arvot. Rivillä 17 on se “oikea”tapa. Edellä olleesta poiketetn, staattisen muuttujaan voidaan viitata luokan nimellä. Oliomuuttujaan taasen tulee viitata olion nimellä, ei luokan nimellä. Näin siis voimme viittaustavasta rivillä 17 päätellä, että kyseessä on staattinen muuttuja. Rivillä 16 tätä ei voida päätellä. Kyseessä voi olla tai ei olla staattinen muuttuja. Siksi rivin 17 esitys on selkeämpi, kun käsiteltävänä on staattinen muuttuja.

Rivillä 22 tulostetaan instanssimuuttujan arvo. Rivillä 23 oleva koodi generoi virheen, koska jokaisen instanssi muuttujan y arvo on erilainen (sehän onkin instanssimuuttuja) ja koneelle ei ole missään kohtaa määritelty mistä luokan ClassA instanssista on kyse.

12.7.1 Instanssimuuttuja piilottaa staattisen muuttujan

Tämä ominaisuus on alkuperäioskirjoittajan mielestä yksi epämieluisa “ominaisuus” Pythonissa. Staattinen muuttuja ja instanssimuuttuja voidaan nimetä samalla tavalla, siis samannimisiksi. Katso alla olevaa koodiesimerkkiä:

# Luokka staattisellla muuttujalla
class ClassB():
    x = 7

# Luodaan olio
b = ClassB()

# Tämä tulostaa 7
print(b.x)

# Tämökin tulostaa 7
print(ClassB.x)

# asetetaan x:lle uusi arvo luokan nimiviittauksella
ClassB.x = 8

# Tämä tulostaa myös 8
print(b.x)

# Tämä tulostaa 8
print(ClassB.x)

# asetetaan x:lle uusi arvo ja käytetään instanssia
# Mitä! Itse aiassa, tässä ei aseteta staattisen muutujan x:lle arvoa.
# Tämä luo uuden muuttujan, x. Tämä x
# on instanssimuuttuja. Staattinen muuttuja on myös x 
# Mutta nämä x-muuttujat ovat kaksi eri muuttujaa 
# Tämä on todella sekavaa ja hankalaa opettelun kannalta.
# 
b.x = 9

# Tämä tulostaa 9
print(b.x)

# Tämä tulostaa 8, EI 9!!!
print(ClassB.x)

Koska instanssimuuttuja ja staattinen muuttuja voidaan nimetä samalla tavalla, voi tästä seurata sekaannuksia!

12.8 Review

12.8.1 Multiple Choice Quiz

Klikkaa tästä monivalintatehtävään.

12.8.2 Short Answer Worksheet

Klikkaa tästä kappaleen kertaustehtäviin.

12.8.3 Lab

Klikkaa tästä ohjelmointitehtäviin.


You are not logged in. Log in here and track your progress.