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. Antud 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 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 omaniku:

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 - ehk meetodid, mis paiknevad klassi sees, kirjeldavad iga tavalist koera oskused. 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, meetodi 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 igale klassi objektile oleks antud oma nimi. Teeme nii, et meie konstruktor võtaks sisse teise argumenti, name, ja salvestaks seda 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 antud juhul iseloomustab just selle konkreetse koera, kelle objekt on loodud, self on viide sellele objektile. Seda on näha printides välja meie Dog klassil oleva meetodi get_this_dog või kui lihtsalt üritada välja printida objekti ise, 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 ka olla klassimuutuja, mis on samal moel nähtav igale klassimeetodile:

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. Olgu näiteks meie tahame koostada klassi, mis kirjeldaks tavalist tudengit. Igal tudengil on olemas nimi ja kool, kus ta õpib. Samal ajal suurem osa tudengeid on laisad, aga mitte kõik. Ehk igal tudengil on kolm parameetrit - nimi, kool ja laiskus, mis on nö true by default.

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 tudeng saab võtta mõistuse pähe ja mitte rohkem 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.

Edasi tuleb juttu pärilikkusest ja polümorfismist, mis ei ole meie kursuse raames kohustuslik materjal, aga on väga huvitav!

Pärilikkus ja Polümorfism (Inheritance & Polymorphism)

Pärilikkus

Klassist on võimalik mõelda kui selliste objektide hulga kirjeldusest, millistel on ühesugused omadused (nimi, pikkus, ...) ja tegevused/meetodid (haugu, räägi, ...). Klassi Dog korral on hulgaks ühesuguste omadustega (mitte segamini ajada omaduste väärtustega, mis võivad olla iga objekti puhul erinevad) ja tegevustega koerad, klassi Student korral tudengid.

Pärilikkus esineb siis, kui meie hulk on teise hulga alamhulk. Näiteks hulk koerad on loomade hulga alamhulk. Klasside puhul nimetatakse alamhulka iseloomustavat klassi alamklassiks. Klassi mida alamklass pärib nimetatakse ülemklassiks ehk superklassiks. Dog on klassi Animal alamklass ja klass Animal on klassi Dog superklass.

Praktikas tähendab pärilikkus, et kui meil on olemas super- ja alamklass ja superklassi sees on defineeritud mingisugused funktsioonid/muutujad, siis need on kättesaadavad ka alamklassile. Alamklassis saab lisaks defineerida ka uusi funktsione.

Näide pärilikkusest

Olgu meil kaks klassi - Student ja ITStudent, viimane on klassi Student alamklass

# Base or superclass
class Student:

    def say_hello(self):
        print("Hello!")


# Extending the base class
class ITStudent(Student):

    def assist_friend(self):
        print("Have you tried turning it off and on again?")


alice = Student()
mary = ITStudent()

alice.say_hello()  # --> Hello!
mary.say_hello()  # --> Hello!
alice.assist_friend()  # --> AttributeError: 'Student' object has no attribute 'assist_friend'
mary.assist_friend()  # --> Have you tried turning it off and on again?

Klass ITStudent pärib Student klassi funktsiooni say_hello, ning defineerib lisaks assist_friend funktsiooni. ITStudent isendile on mõlemad meetodid kättesaadavad, kuid Student isendile ei ole assist_friend meetod kättesaadav.

_images/student_graph2.png

Pärilikkus Pythoni lähtekoodis

Pärilikkust kasutatakse näiteks erinevate erindite defineerimiseks. Näiteks tekib TypeError sellise koodi peale:

>>> 1 + "str"
Traceback (most recent call last):
    File "<input>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'int' and 'str'

Vaadates pythoni lähtekoodi näeme, et TypeError on Exception klassi alamklass (Lähtekoodi saab vaadata kirjutades PyCharmi TypeError ja vajuta sinna peale hoides alla ctrl klahvi)

class TypeError(Exception):
    """ Inappropriate argument type. """
    def __init__(self, *args, **kwargs): # real signature unknown
        pass

    @staticmethod # known case of __new__
    def __new__(*args, **kwargs): # real signature unknown
        """ Create and return a new object.  See help(type) for accurate signature. """
        pass

Exception ise on aga BaseException klassi alamklass.

class Exception(BaseException):
    """ Common base class for all non-exit exceptions. """
    def __init__(self, *args, **kwargs): # real signature unknown
        pass

    @staticmethod # known case of __new__
    def __new__(*args, **kwargs): # real signature unknown
        """ Create and return a new object.  See help(type) for accurate signature. """
        pass

Erinditel on palju ühist koodi mida jagada. Näiteks on kõikidel erinditel sõnum ja erindi tagasijälitus (traceback). Kuna iga erindi defineerimisel oleks traceback funktsiooni defineerimine ajakulukas ja mõttetu kasutatakse erindite puhul pärilikkust.

Polümorfism

Polümorfism on olukord, mille korral sõltuvalt klassist käitub meetod erinevalt.

Polümorfism esineb siis, kui alamklass kasutab superklassil defineertud meetodi teisel moel. Teiste sõnadega, kui superklassil ja alamklassil on olemas sama nimega meetodid, millel on sama palju argumente ning mis teevad erinevat asja, siis tegemist on polümorfismiga.

Näide polümorfismist

# Base or superclass
class Student:

    def say_hello(self):
        print("Hello!")


# Extending the base class
class ITStudent(Student):

    # Overriding the method with the same name from the base class
    def say_hello(self):
        print("Hello! Does anybody need help with their computer?")

    def assist_friend(self):
        print("Have you tried turning it off and on again?")


alice = Student()
mary = ITStudent()

alice.say_hello()  # --> Hello!
mary.say_hello()  # --> Hello! Does anybody need help with their computer?
mary.assist_friend()  # --> Have you tried turning it off and on again?

Kuna klass ITStudent on Student klassi alamklass ja say_hello meetod on defineeritud erinevalt ülemklassist on tegu polümorfismiga. Sellist olukorda nimetatakse ülekirjutamiseks (override).

Polümorfism Pythoni lähtekoodis

Python kasutab polümorfismi näiteks operaatorite kasutamisel. Erinevate sisendite puhul käitub + operaator erinevalt.

  • Kahe arvu liitmisel liidetakse kaks arvu.
>>> 1 + 1
2
  • Kahe sõne liitmisel, aga ühentatakse kaks sõna omavahel.
>>> "Foo " + "Bar"
'Foo Bar'
  • Kahe järjendi puhul, ühendatakse samuti listid omavahel üheks järjendiks.
>>> [1, 2] + [3]
[1, 2, 3]

Samuti on ka meetodeid, mida saab välja kutsuda erinevat tüüpi andmete peal ja mille käitumine on vastavalt andmetele erinev.

>>> "Foo Bar".index("Bar")
4
>>> ["Foo", "Bar"].index("Bar")
1

Põhjalikum näide pärilikkusest ja polümorfismist

class Animal:
    """Animal class."""

    def __init__(self, name="None", is_pet=False):
        """
        Class constructor.

        :param name: animal name
        :param is_pet: pet or wild?
        """
        self.name = name
        self.is_pet = is_pet

    def speak(self):
        return "I cannot"

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

    def is_friendly(self):
        return self.is_pet

    def __str__(self):
        return self.name


class Dog(Animal):
    """Class Dog exends class Animal"""

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

        :param name: dog name
        :param is_pet: dog usually is a pet
        """
        # Overriding the base constructor, passing new values to self.is_pet and self.name
        super().__init__(name, is_pet)

    def roll(self):
        """
        Extra method, specific for this class.

        :return:
        """
        print("*rolling*")

    def speak(self):
        """
        Overriding the base method.
        By default it returns "I cannot", we change it to return "Bark!".

        :return: Bark!
        """
        return "Bark!"


class Dolphin(Animal):
    """Class Dolphin extends class Animal."""

    # Class constructor with two more arguments
    def __init__(self, name, playful, smart):
        """
        Class constructor with two extra parameters.

        :param name:
        :param playful
        :param smart
        """
        # Overriding the base constructor, passing new value to self.name
        super().__init__(name)

        self.playful = playful
        self.smart = smart

    def is_friendly(self):
        """
        Overriding the base method.
        By default if the animal is not a pet, it isn't friendly either. However in our case,
        if the dolphin is in playful mood or smart, it will be friendly to us.

        :return: boolean
        """

        return self.smart or self.playful

    def greet(self):
        """
        Overriding the base method.
        By default it returns "<name of the animal> greets you!". In our case it returns "*Water splash*".

        :return: *Water splash*
        """
        return "*Water splash*"

    def perform_jump(self):
        """
        Extra method, specific for this class.

        :return:
        """

        if self.playful:
            return "*Jumps*"

        elif self.smart:
            return "I will jump.. for food."

        return "No."


animal = Animal()
print(animal.is_pet)  # --> False
print(animal)  # --> None
print(animal.is_friendly())  # --> False
print(animal.greet())  # --> None greets you!
print(animal.speak())  # --> I cannot

print()

dog = Dog("Sparky")
print(dog.is_pet)  # --> True
print(dog)  # --> Sparky
print(dog.is_friendly())  # --> True
print(dog.greet())  # --> Sparky greets you!
print(dog.speak())  # --> Bark!
print(dog.roll())  # --> *rolling*

print()

dolphin = Dolphin("Steve", False, True)
print(dolphin.is_pet)  # --> False
print(dolphin)  # --> Steve
print(dolphin.is_friendly())  # --> True
print(dolphin.greet())  # --> *Water splash*
print(dolphin.speak())  # --> I cannot
print(dolphin.perform_jump())  # --> I will jump.. For food.