OOP: klassid ja objektid

Kuigi Python toetab mitut programmeerimise paradigmat, on neist kõige enam rõhutatud just objekt-orienteeritud programmeerimise (OOP) paradigmat. Võib öelda, et OOP-i kasutamine tähendab programmikoodi teatud (kindlal) viisil organiseerimist. Selle lähenemise juures leiavad kasutamist mõisted nagu klass ja objekt, teemasse enam süvenedes ka kapseldamine, polümorfism, pärimine, modulaarsus, abstraktsioon. Viimaseid nimetatakse OOP tehnikateks.

OOP-i juures on tavaline rääkida klasside kirjeldamisest ja nende kirjelduste alusel objektide loomisest ning manipuleerimisest. Objektidega manipuleerimine leiab aset klassi kirjelduses antud meetodite (põhimõtteliselt funktsioonide) alusel. Klassi kirjelduse alusel luuakse objekte, millistel on ühesugused omadused ja tegevused (meetodid).

Järgneva näite pideva täiendamisega püüame samm-sammult antud temaatika olulised ideed edasi anda.

Oletame, et tahame koostada programmi, mis simuleerib tavalise koera käitumist. Mida oskab tavaline koer? Eeldame, et iga tavaline koer oskab haukuda, veereda ja tervitada oma omanikku. Samal ajal, tavaline koer ei oska rääkida (üsna loogiline, eks). Selleks, et koostada sellist programmi, defineerime klassi Dog (mille alusel saab hiljem programmis just samade, klassis kirjeldatud, oskustega koeri luua):

# Defining the Dog class
class Dog:
    # Code

On teada, et iga tavaline koer oskab haukuda, veereda ja tervitada oma omanikku:

class Dog:

    def bark(self):
        print("Bark!")

    def roll(self):
        print("*rolling*")

    def greet(self):
        print("Hey, master")

    def speak(self):
        print("I cannot!")

Klassi Dog kirjelduses on neli funktsiooni, mida OOP lähenemise puhul nimetatakse klassi meetoditeks. Pöörame tähelepanu, et iga meetod kasutab parameetrit nimega self, millest räägime natuke hiljem. Meie klass kirjeldab tavalise koera käitumist - on meetodid, mis paiknevad klassi sees ning kirjeldavad iga tavalise koera oskusi. See ongi abstraktsioon. S.t. klass (meil Dog) on kui piparkoogivorm ning objektid kui antud vormiga tehtud piparkoogid. Nüüd, kui meil on klassis tavalise koera käitumine kirjeldatud, saab luua objekti - klassi isendi (instance). Loome kaks koera: Clyde ja Jenkins.

class Dog:

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

    def roll(self):
        print("*rolling*")

    def greet(self):
        print("Greetings, master")

    def speak(self):
        print("I cannot!")

# Creating the Dog class instance and saving it to the variable <clyde>
clyde = Dog()
clyde.bark()   # --> Woof!
clyde.roll()   # --> *rolling*
clyde.greet()  # --> Greetings, master
clyde.speak()  # --> I cannot!

# Creating another Dog instance
jenkins = Dog()
jenkins.bark()  # --> Woof!
jenkins.roll()  # --> *rolling*
# .. And other methods
# .. Infinite objects can be created this way, all implementing the same methods defined in our class

Tuleb välja, et vaatamata sellele, et clyde ja jenkins on erinevad koerad, oskavad mõlemad teha asju, mida meie definitsiooni järgi oskab iga tavaline koer. Nii, nagu reaalses elus umbes ongi - näiteks iga koer oskab haukuda sõltumata tema liigist.

Klassil võib olla ka konstruktor, mis kujutab endast erilist, fikseeritud nimega, meetodit klassi sees. See fikseeritud nimi on järgmine: __init__().

class Dog:
    # Class constructor
    def __init__(self):
        # Code here

Konstruktor ja selle sees olev kood käivitatakse vaid isendi (objekti) loomisel. Näiteks tahame, et klassi Dog isendi loomisel oleks igale klassi objektile antud oma nimi. Teeme nii, et meie konstruktor võtaks sisse teise argumendi, name, ja salvestaks selle uue muutuja self.name sisse.

class Dog:

    # Class constructor
    def __init__(self, name):
        self.name = name

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

    def roll(self):
        print("*rolling*")

    def greet(self):
        print("Greetings, master")

    def speak(self):
        print("I cannot!")

    # Returns the current dog's name
    def get_name(self):
        return self.name

    # Returns the reference to the current dog instance
    def get_this_dog(self):
        return self

# Creating the Dog class instance and giving it name
dog1 = Dog("Clyde")
dog1.bark()  # --> Woof!
dog1.roll()  # --> *rolling*
dog1.greet() # --> Greetings, master
dog1.speak() # --> I cannot!
print(dog1.get_name())  # --> Clyde
print(dog1.get_this_dog())  # --> (Example) <__main__.Dog object at 0x0119D710>
print(dog1)  # --> (Example) <__main__.Dog object at 0x0119D710>

# Creating some more Dog instances
dog2 = Dog("Jenkins")
dog2.bark() # --> Woof!
dog2.roll()  # --> *rolling*
dog2.greet() # --> Greetings, master
dog2.speak() # --> I cannot!
print(dog2.get_name())  # --> Jenkins
print(dog2.get_this_dog())  # --> (Example) <__main__.Dog object at 0x0143D710>
print(dog2)  # --> (Example) <__main__.Dog object at 0x0143D710>

dog3 = Dog("Robbie")
print(dog3.get_name())  # --> Robbie
print(dog3.get_this_dog())  # --> (Example) <__main__.Dog object at 0x01CED710>

Seega, vaatamata sellele, et iga koer oskab haukuda, veereda ja tervitada, on kõik koerad erinevad objektid, nagu reaalelus ongi. Muutuja self iseloomustab antud juhul just seda konkreetset koera, kelle objekt on loodud, self on viide sellele objektile. Seda on näha, kui printida välja meie Dog klassil olev meetod get_this_dog või kui lihtsalt üritada välja printida objekti ennast, nagu on üleval tehtud. Nagu näha, on erinevatel objektidel erinev viide, mis tähendabki, et näiteks Jenkins ja Clyde ei ole üks ja sama koer, vaid kaks erinevat. Kui meie loome muutuja, pannes self juurde prefiksina, nagu tegime self.name korral, siis saame nn isendimuutuja, mis on nähtav igale klassi meetodile:

class Dog:

    # Class constructor
    def __init__(self, name):
        # Instance variable
        self.name = name

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

    def roll(self):
        print("*rolling*")

    def greet(self):
        print(self.name + " greets you!")

    def speak(self):
        print("I cannot")

    # Returns the current dog's name
    def get_name(self):
        return self.name

    # Returns the current dog instance
    def get_this_dog(self):
        return self

dog1 = Dog("Clyde")
dog1.bark()  # --> Clyde says: Woof!
dog1.greet()  # --> Clyde greets you!
print(dog1.get_name())  # --> Clyde
print(dog1)  # --> Reference

dog2 = Dog("Jenkins")
dog2.bark() # --> Jenkins says: Woof!
dog2.greet() # --> Jenkins greets you!
print(dog2.get_name())  # --> Jenkins

Klassil võib olla ka klassimuutuja, mis on samal moel nähtav igale klassi meetodile:

class Dog:

    # Class variable
    name = "Some name"

    # Class constructor
    def __init__(self, new_name):
        # Assign class variable to new value
        Dog.name = new_name

    def bark(self):
        print(Dog.name + " says: Woof!")

    def roll(self):
        print("*rolling*")

    def greet(self):
        print(Dog.name + " greets you!")

    def speak(self):
        print("I cannot")

    # Returns the current dog's name
    def get_name(self):
        return Dog.name

# We do not need to create an object to access the class variable
print(Dog.name)  # --> Some name

dog1 = Dog("Clyde")
dog1.bark()  # --> Clyde says: Woof!
dog1.greet()  # --> Clyde greets you!
print(dog1.get_name())  # --> Clyde
print(Dog.name)  # --> Clyde

dog2 = Dog("Jenkins")
dog2.bark() # --> Jenkins says: Woof!
dog2.greet() # --> Jenkins greets you!
print(dog2.get_name())  # --> Jenkins
print(Dog.name)  # --> Jenkins

# Assign new value to the variable name
Dog.name = "Some random dog name"
print(Dog.name)  # --> Some random dog name

Isendimuutuja ja klassimuutuja erinevad teineteisest selle poolest, et klassimuutuja eksisteerib, on kättesaadav ja muudetav ilma objekti loomiseta, kuid samal ajal isendimuutuja eksisteerib ainult koos oma objektiga.

Veel mõni lihtne näide. Oletame, et me tahame koostada klassi, mis kirjeldaks tavalist tudengit. Igal tudengil on olemas nimi ja kool, kus ta õpib. Samas on suurem osa tudengeid laisad, aga mitte kõik. Ehk igal tudengil on kolm parameetrit: nimi, kool ja laiskus, mis on vaikimisi tõene.

class Student:
    """Student class."""

    def __init__(self, name, college, is_lazy=True):
        """
        Class constructor.

        :param name: student name
        :param college: student college
        :param is_lazy: is student lazy? (by default yes)
        """
        self.name = name
        self.college = college
        self.is_lazy = is_lazy

Tudengitel on alati mingi kodutöö. Eeldame, et tudeng saab teha homset kodutööd ainult siis, kui ta ei ole laisk.

def is_homework_done(self):
    """
    Did the student do the homework for tomorrow?

    :return: string
    """
    if self.is_lazy:
        return "Homework? Pff, I have TV shows to watch."

    return "Homework is done!"

Lisaks sellele eeldame, et iga tudeng saab oma kooli vahetada.

def change_college(self, college):
    """
    Change the college student attends.

    :param college:
    :return none
    """
    print("Student {} leaves the {} and starts studying in {}".format(self.name, self.college, college))
    self.college = college

Ja samal moel saab tudeng mõistuse pähe võtta ja enam mitte laisk olla (või vastupidi):

def change_laziness(self):
    """
    Is the student still lazy/not lazy?

    :return: none
    """
    self.is_lazy = not self.is_lazy

Siis, pannes kõik kokku, saame:

class Student:
    """Student class."""

    def __init__(self, name, college, is_lazy=True):
        """
        Class constructor.

        :param name: student name
        :param college: student college
        :param is_lazy: is student lazy? (by default - yes)
        """
        self.name = name
        self.college = college
        self.is_lazy = is_lazy

    def is_homework_done(self):
        """
        Did the student do the homework for tomorrow?

        :return: string
        """
        if self.is_lazy:
            return "Homework? Pff, I have TV shows to watch."

        return "Homework is done!"

    def get_college(self):
        """
        Get the college student attends.

        :return: college
        """
        return "{} studies in {}".format(self.name, self.college)

    def change_college(self, college):
        """
        Change the college students attends.

        :param college:
        :return none
        """
        print("Student {} leaves the {} and starts studying in {}".format(self.name, self.college, college))
        self.college = college

    def get_name(self):
        """
        Get the student name.

        :return: name
        """
        return self.name

    def get_laziness(self):
        """
        Is the student lazy?

        :return: true/false
        """
        return self.is_lazy

    def change_laziness(self):
        """
        Is the student still lazy/not lazy?

        :return: none
        """
        self.is_lazy = not self.is_lazy

# Some examples

st1 = Student("Alice", "Tallinn University of Technology")
print(st1)  # --> (Example) <__main__.Student object at 0x01E3DB30>
print(st1.get_name())  # --> Alice
print(st1.get_college())  # --> Tallinn University of Technology
print(st1.get_laziness())  # --> True
print(st1.is_homework_done())  # --> Homework? Pff, I have TV shows to watch.
print()

# Alice decides she is more into art than computers, so she leaves TUT and starts studying in Estonian Academy of Arts.
# Output: Student Alice leaves the Tallinn University of Technology and starts studying in Estonian Academy of Arts
st1.change_college("Estonian Academy of Arts")
print(st1.get_college())  # --> Estonian Academy of Arts
# She is still lazy though
print(st1.get_laziness())  # --> True
print()

st2 = Student("Mary", "Massachusetts Institute of Technology", False)
print(st2.get_name())  # --> Mary
print(st2.get_college())  # --> Massachusetts Institute of Technology
print(st2.get_laziness())  # --> False
print(st2.is_homework_done())  # --> Homework is done!
# It is weekend now, so Mary can be a bit lazier than usual
st2.change_laziness()
print(st2.is_homework_done())  # --> Homework? Pff, I have TV shows to watch.

Vaatleme lisaks üht sisseehitatud meetodit, mis teeb meie elu veidi lihtsamaks. Meetodi nimi on __str__. Meetod käivitub siis, kui me proovime objekti välja printida ja tagastab meie poolt määratud stringi tavalise viite asemel. Vaatame, kuidas see meetod töötab Student klassi näitel.

class Student:
    """Student class."""

    def __init__(self, name, college, is_lazy=True):
        """
        Class constructor.

        :param name: student name
        :param college: student college
        :param is_lazy: is student lazy? (by default - yes)
        """
        self.name = name
        self.college = college
        self.is_lazy = is_lazy

    def __str__(self):
        """
        Returns name of the student when invoked.

        :return: student name
        """
        return self.name

    # <...Other methods skipped...>


# Some examples

st1 = Student("Alice", "Tallinn University of Technology")
print(st1)  # --> Alice
print()

st2 = Student("Mary", "Massachusetts Institute of Technology", False)
print(st2)  # --> Mary

Ehk nüüd ei ole meil vaja luua eraldi meetodit get_name tudengi nime tagastamiseks, nime on võimalik saada lihtsalt objekti printimisel. __str__ ei pea tagastama ainult nime, antud juhul näiteks võiks ta tagastada ka kooli, kus tudeng õpib.