Osa 9

Staattiset piirteet

Olio-ohjelmoinnissa puhutaan piirteistä. Näillä tarkoitetaan olion ominaisuuksia: luokan sisälle kirjoitettuja metodeja ja luokassa määriteltyjä muuttujia.

Tähän mennessä olemme käsitelleen olioiden piirteitä eli oliometodeita ja attribuutteja. Olio-ohjelmointiin kuuluvat kuitenkin myös luokan piirteet, joita kutsutaan usein myös staattisiksi piirteiksi. Myös käsitettä luokkamuuttuja käytetään.

Luokkamuuttujat

Kuten on aiemmin opittu, jokaisella oliolla on omat itsenäiset arvonsa attribuuteille. Attribuuttien lisäksi luokassa voidaan määritellä luokkamuuttujia eli staattisia muuttujia. Luokkamuuttujalla tarkoitetaan muuttujaa, jota käytetään luokan kautta eikä luokasta muodostettujen olioiden kautta. Luokkamuuttujalla on yksi yhteinen arvo riippumatta siitä, montako oliota luokasta muodostetaan.

Luokkamuuttujan määrittely eroaa attribuutista siinä, että se määritellään ilman self-aluketta. Jos luokkamuuttujaa halutaan käyttää koko luokassa ja mahdollisesti luokan ulkopuoleltakin, se tulee määritellä metodien ulkopuolella.

class Korkotili:
    yleiskorko = 0.03

    def __init__(self, tilinumero: str, saldo: float, korko: float):
        self.__tilinumero = tilinumero
        self.__saldo = saldo
        self.__korko = korko

    def lisaa_korko(self):
        # Korko on yleiskorko + tilin korko
        korko_yhteensa = Korkotili.yleiskorko + self.__korko
        self.__saldo += self.__saldo * korko_yhteensa

    @property
    def saldo(self):
        return self.__saldo

Koska yleiskorko on määritelty luokassa eikä metodin sisällä eikä sen alustuksessa ole käytetty self-aluketta, se on luokkamuuttuja.

Luokkamuuttujaan viitataan luokan nimen avulla, esimerkiksi näin:

# Yleiskorko on olioista riippumaton
print("Yleiskorko on", Korkotili.yleiskorko)

tili = Korkotili("12345", 1000, 0.05)
# Lisätään kokonaiskorko saldoon
tili.lisaa_korko()
print(tili.saldo)
Esimerkkitulostus

Yleiskorko on 0.03 1080.0

Luokkamuuttujiin viitataan siis luokan nimen avulla, esimerkiksi Korkotili.yleiskorko, ja oliomuuttujiin eli attribuutteihin olion nimen avulla tili.saldo. Oliomuuttujiin voi luonnollisesti viitata vasta, kun luokasta on muodostettu olio.

Luokkamuuttujaa on kätevä käyttää, kun halutaan tallentaa arvoja, jotka on jaettu kaikkien olioiden kesken. Edellisessä esimerkissä oletetaan, että kaikilla pankkitileillä on sama yleiskorkoprosentti, jonka lisäksi tilille voidaan erikseen määrittää oma korkoprosenttinsa. Yleiskorkokin voi muuttua, mutta muutos vaikuttaa kaikkiin luokasta muodostettuihin olioihin:

class Korkotili:
    yleiskorko = 0.03

    def __init__(self, tilinumero: str, saldo: float, korko: float):
        self.__tilinumero = tilinumero
        self.__saldo = saldo
        self.__korko = korko

    def lisaa_korko(self):
        # Korko on yleiskorko + tilin korko
        korko_yhteensa = Korkotili.yleiskorko + self.__korko
        self.__saldo += self.__saldo * korko_yhteensa

    @property
    def saldo(self):
        return self.__saldo

    @property
    def kokonaiskorko(self):
        return self.__korko + Korkotili.yleiskorko
tili1 = Korkotili("12345", 100, 0.03)
tili2 = Korkotili("54321", 200, 0.06)

print("Yleiskorko:", Korkotili.yleiskorko)
print(tili1.kokonaiskorko)
print(tili2.kokonaiskorko)

# Nostetaan yleiskorko 10 prosenttiin
Korkotili.yleiskorko = 0.10

print("Yleiskorko:", Korkotili.yleiskorko)
print(tili1.kokonaiskorko)
print(tili2.kokonaiskorko)
Esimerkkitulostus

Yleiskorko: 0.03 0.06 0.09 Yleiskorko: 0.1 0.13 0.16

Kun yleiskorko nousee, kaikkien luokasta määriteltyjen tilien kokonaiskorko nousee. Huomaa, että kokonaiskorko on määritelty havainnointimetodiksi, vaikkei vastaavaa attribuuttia olekaan suoraan määritelty. Metodi palauttaa tilin koron ja yleiskoron summan.

Tarkastellaan vielä toista esimerkkiä. Luokassa Puhelinnumero on maatunnukset tallennettuna sanakirjaan. Lista maatunnuksista on yhteinen kaikille luokasta luoduille puhelinnumero-olioille, koska maatunnus saman maan puhelinnumeroille on aina sama.

class Puhelinnumero:
    maatunnukset = {"Suomi": "+358", "Ruotsi": "+46", "Yhdysvallat": "+1"}

    def __init__(self, nimi: str, puhelinnumero: str, maa: str):
        self.__nimi = nimi
        self.__puhelinnumero = puhelinnumero
        self.__maa = maa

    @property
    def puhelinnumero(self):
        # Puhelinnumerosta jää etunolla pois, kun maatunnus lisätään alkuun
        return Puhelinnumero.maatunnukset[self.__maa] + " " + self.__puhelinnumero[1:]
paulan_nro = Puhelinnumero("Paula Pythonen", "050 1234 567", "Suomi")
print(paulan_nro.puhelinnumero)
Esimerkkitulostus

+358 50 1234 567

Kun puhelinnumero-olio luodaan, tallennetaan nimen ja numeron lisäksi maa. Kun numero haetaan havainnointimetodilla, haetaan numeron eteen maatunnus luokkamuuttujasta olion attribuuttiin tallennetun maatiedon avulla.

Esimerkkiluokka on toiminnallisuudeltaan melko vajavainen. Katsotaan vielä, miltä näyttäisi parempi toteutus, jossa on havainnointi- ja asetusmetodit eri attribuuteille:

class Puhelinnumero:
    maatunnukset = {"Suomi": "+358", "Ruotsi": "+46", "Yhdysvallat": "+1"}

    def __init__(self, nimi: str, puhelinnumero: str, maa: str):
        self.__nimi = nimi
        # Tämä kutsuu metodia puhelinnumero.setter
        self.puhelinnumero = puhelinnumero
        # Tämä kutsuu metodia maa.setter
        self.maa = maa

    # Havainnointimetodissa yhdistetään maatunnus ja puhelinnumero
    @property
    def puhelinnumero(self):
        # Puhelinnumerosta jää etunolla pois, kun maatunnus lisätään alkuun
        return Puhelinnumero.maatunnukset[self.__maa] + " " + self.__puhelinnumero[1:]

    @puhelinnumero.setter
    def puhelinnumero(self, numero):
        # Varmistetaan, että puhelinnumerossa on vain numeroita ja välilyöntejä
        for merkki in numero:
            if merkki not in "1234567890 ":
                raise ValueError("Puhelinnumero saa sisältää vain lukuja ja välilyöntejä")
        self.__puhelinnumero = numero

    # Pelkkä puhelinnumero ilman maatunnusta
    @property
    def paikallinen_numero(self):
        return self.__puhelinnumero

    @property
    def maa(self):
        return self.__maa

    @maa.setter
    def maa(self, maa):
        # Varmistetaan, että maa on maatunnusten listalla
        if maa not in Puhelinnumero.maatunnukset:
            raise ValueError("Annettua maata ei löydy listalta.")
        self.__maa = maa

    @property
    def nimi(self):
        return self.__nimi

    @nimi.setter
    def nimi(self, nimi):
        self.__nimi = nimi

    def __str__(self):
        return f"{self.puhelinnumero} ({self.__nimi})"
if __name__ == "__main__":
    pnro = Puhelinnumero("Pertti Python", "040 111 1111", "Ruotsi")
    print(pnro)
    print(pnro.puhelinnumero)
    print(pnro.paikallinen_numero)
Esimerkkitulostus

+46 40 111 1111 (Pertti Python) +46 40 111 1111 040 111 1111

Loading

Luokkametodit

Luokkametodi eli staattinen metodi on luokassa oleva metodi, jota ei ole sidottu mihinkään luokasta muodostettuun olioon. Niinpä luokkametodia voi kutsua ilman, että luokasta muodostetaan oliota.

Luokkametodit ovat yleensä työkalumetodeja, jotka liittyvät jotenkin luokkaan mutta joita on tarkoituksenmukaista kutsua ilman olion muodostamista. Luokkametodit ovat yleensä julkisia, jolloin niitä voidaan kutsua sekä luokan ulkopuolelta että luokan ja siitä muodostettujen olioiden sisältä.

Luokkametodi merkitään annotaatiolla @classmethod ja sen ensimmäinen parametri on aina cls. Tunnistetta cls käytetään samaan tapaan kuin tunnistetta self, mutta erotuksena on, että cls viittaa luokkaan ja self viittaa olioon. Kummallekaan parametrille ei anneta kutsuessa arvoa, vaan Python tekee sen automaattisesti.

Esimerkiksi luokassa Rekisteriote voisi olla staattinen metodi, jolla voidaan tarkistaa, onko annettu rekisteritunnus oikeamuotoinen. Metodi on staattinen, jotta tunnuksen voi tarkastaa myös ilman, että luodaan uutta oliota luokasta:

class Rekisteriote:
    def __init__(self, omistaja: str, merkki: str, vuosi: int, rekisteritunnus: str):
        self.__omistaja = omistaja
        self.__merkki = merkki
        self.__vuosi = vuosi

        # Kutsutaan metodia rekisteritunnus.setter
        self.rekisteritunnus = rekisteritunnus

    @property
    def rekisteritunnus(self):
        return self.__rekisteritunnus

    @rekisteritunnus.setter
    def rekisteritunnus(self, tunnus):
        if Rekisteriote.rekisteritunnus_kelpaa(tunnus):
            self.__rekisteritunnus = tunnus
        else:
            raise ValueError("Rekisteritunnus ei kelpaa")

    # Luokkametodi tunnuksen validoimiseksi
    @classmethod
    def rekisteritunnus_kelpaa(cls, tunnus: str):
        if len(tunnus) < 3 or "-" not in tunnus:
            return False

        # Tarkastellaan alku- ja loppuosaa erikseen
        alku, loppu = tunnus.split("-")

        # Alkuosassa saa olla vain kirjaimia
        for merkki in alku:
            if merkki.lower() not in "abcdefghijklmnopqrstuvwxyzåäö":
                return False

        # Loppuosassa saa olla vain numeroita
        for merkki in loppu:
            if merkki not in "1234567890":
                return False

        return True
ote = Rekisteriote("Arto Autoilija", "Volvo", "1992", "abc-123")

if Rekisteriote.rekisteritunnus_kelpaa("xyz-789"):
    print("Tämä on validi tunnus!")
Esimerkkitulostus

Tämä on validi tunnus!

Rekisteriotteen oikeellisuuden voi tarkistaa kutsumalla metodia (esimerkiksi Rekisteriote.rekisteritunnus_kelpaa("xyz-789"))) ilman, että muodostaa luokasta oliota. Samaa metodia kutsutaan myös uutta oliota muodostaessa luokan konstruktorista. Huomaa kuitenkin, että myös tässä kutsussa viitataan metodiin luokan nimen avulla eikä self-tunnisteella!

Loading