Arcade-pelien ohjelmointi
Pythonilla ja PygamellaChapter 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?
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
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.
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.
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
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:
- Luokan attribuutit listataan ensin ja sen jälkeen metodit.
- Ensimmäisen parametrin tulee olla self.
- Metodin määrittelylause sisennetään täsmälleen yhden tabulaattorin verran.
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.
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.
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
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.
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.
Tämä voidaan suorittaa myös www.pythontutor.com jolloin näemme, että molemmat muuttujat osoittavat samaan objektiin.
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.
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.
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ä
- Luo uusi luokka nimeltään Cat. Anna attribuuteiksi name, color, ja weight. Tee luokkaan metodi meow.
- Luo luokasta instanssi ja asteta attribuuteille arvot. Kutsu metodia meow.
- 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
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ä
- Pitääkö luokan nimi kirjoittaa isolla vai pienellä alkukirjaimella?
- Pitääkö metodien nimet kirjoittaa isolla vai pienellä alkukirjaimella?
- Pitääkö atrribuuttien nimet kirjoittaa isolla vai pienellä alkukirjaimella?
- Kummatko pitää olla luokassa ensin attribuutit vai metodit?
- Mitä muita nimityksiä viittauksesta käytetään?
- Mitä muita nimityksiä oliomuuttujasta käytetään?
- Mitä nimitystä luokan oliosta käytetään?
- Luo luokka nimeltään Star, joka tulostaa “A star is born!” joka kerta kun luokasta luodaan olio.
- 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ä
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.
- Ensinnäkin, lisätään submerge() komento veneeseemme. Tämä ei ole kovin hyvä idea, koska tavalliselle veneelle ei kuulu ominaisuutena sukeltaa.
- Toisekseen, voimme luoda kopion Boat luokasta ja kutsutaan sitä nimellä Submarine. Tähän luokkaan lisäämme submerge() komennon. Tämä vaikuttaa aluksi helpolta, mutta asia mutkistuu, kun jossain vaiheessa Boat luokkaa mahdollisesti muutetaan. Tällöin ei riitä, että vain luokkaa Boat muutetaan, vaan muutokset pitää tehdä myös Submarine luokalle. Tästä seuraa koddin ylläpitoon turhaa monimutkaisuutta ja virheiden todennäköisyys kasvaa.
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.
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:
- Employee on henkilö (person).
- Customer on henkilö (person).
- Henkilöllä on nimi (name).
- Työntekijällä (Employee) on tehtävä (job title).
- Asiakkaalla (Customer) on e-mail.
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.
English version by Paul Vincent Craven
Spanish version by Antonio Rodríguez Verdugo
Russian version by Vladimir Slav
Turkish version by Güray Yildirim
Portuguese version by Armando Marques Sobrinho and Tati Carvalho
Dutch version by Frank Waegeman
Hungarian version by Nagy Attila
Finnish version by Jouko Järvenpää
French version by Franco Rossi
Korean version by Kim Zeung-Il
Chinese version by Kai Lin