# 1. Suprascrierea metodelor (overriding methods)¶

Daca o metoda mostenita este redefinita in corpul subclasei, atunci se spune ca metoda mostenita este suprascrisa. In corpul subclasei, metoda mostenita este apelata pe baza referintei generate de functia `super()`. In exteriorul subclasei, oricare apel al metodei care poarta numele metodei suprascrise, daca utilizeaza o referinta la un obiect de tipul subclasei, apeleaza metoda definita in corpul subclasei.

    class Superclasa:
        def nume_metoda(...):
            # corp metoda

    class Subclasa(Superclasa):
        def nume_metoda(...):          # suprascrie metoda nume_metoda mostenita din Superclasa
            # corp metoda

    ob1 = Superclasa()
    ob2 = Subclasa()

    ob1.nume_metoda()                 # se apeleaza metoda nume_metoda din Superclasa
    ob2.nume_metoda()                 # se apeleaza metoda nume_metoda din Subclasa

In [1]:

    # Exemplul 1 - suprascrierea metodelor mostenite

    # implementare superclasa
    class GeometricForms:

        def info(self):
            print("I am a geometric form.")

    # implementare subclasa
    class Circle(GeometricForms):

        # suprascreire metoda info
        def info(self):
            print("I am a circle.")

    form = GeometricForms()
    circ = Circle()

    form.info()
    circ.info()

    I am a geometric form.
    I am a circle.

# 2. Polimorfism (Polymorphism)¶

### Implementarea polimorfismului prin intermediul claselor¶

O forma simpla de polimorfism poate fi implementata daca in corpul unor clase diferite se definesc metode cu acelasi nume. Prin aceasta solutie, care nu este bazata pe mostenire, fiecare clasa furnizeaza propria sa implementare a metodelor cu acelasi nume.

O metoda care are implementari diferite in clase diferite, poate fi apelata pentru obiecte de diferite tipuri (de diferite clase), in fiecare caz fiind rulat comportamentul metodei implementat in clasa care da tipul obiectului utilizat ca referinta.

    class A:
        def nume_metoda(...):   # implementarea metodei nume_metoda conform clasei A
            # corp metoda
    class B:
        def nume_metoda(...):    # implementarea metodei nume_metoda conform clasei B
            # corp metoda

    ob1 = A()
    ob2 = B()

    ob1.nume_metoda()            # metoda nume_metoda se comporta in conformitate
                                 # cu modul in care a fost implementata in clasa A

    ob2.nume_metoda()            # metoda nume_metoda se comporta in conformitate
                                 # cu modul in care a fost implementata in clasa B

In [2]:

    # Exemplul 2 - implementarea polimorfismului cu ajutorul claselor
    from math import pi

    class Circle:
        def __init__(self, a_radius = 1.0):
            self.__radius = a_radius
        @property
        def radius(self):
            return self.__radius
        @radius.setter
        def radius(self, new_radius):
            self.__radius = new_radius
        def get_area(self):
            return 0.5*pi*pow(self.__radius,2)

    class Square:
        def __init__(self, a_length = 1.0):
            self.__length = a_length
        @property
        def length(self):
            return self.__length
        @length.setter
        def radius(self, new_length):
            self.__length = new_length
        def get_area(self):
            return pow(self.__length,2)

    circ = Circle()
    sqar = Square()

    circ.get_area(), sqar.get_area()

Out[2]:

    (1.5707963267948966, 1.0)

### Implementarea polimorfismului cu ajutorul functiilor polimorfe¶

O varianta mai eficienta de implementare polimorfismului este bazata pe mostenire si suprascriere a metodelor supercalsei. Aceasta tehnica permite definirea unor functii polimorfe (polymorphic functions), care sunt caracterizate prin faptul ca isi pot adapta comportamentul la tipul argumentelor pe care le primesc la apelare.

Pentru implementarea functiilor polimorfe sunt necesare urmatoarele:

- definirea unei superclase: metodele sale vor fi astfel stabilite incat sa compuna o interfata pentru functiile polimorfe;
- definirea unui set de subclase pentru superclasa in care este definita interfata; metodele definite in fiecare subclasa vor suprascrie metodele din superclasa
- definirea functiei polimorfe: contine ca parametru/parametri obiecte de tipul superclasei, sau a subclaselor

    class A:
        def nume_metoda(...):    # metoda nume_metoda va fi implementata ca functie polimorfa
            pass

    class B(A):
        def nume_metoda(...):    # implementarea metodei nume_metoda conform clasei B
            # corp metoda

    class C(A):
        def nume_metoda(...):    # implementarea metodei nume_metoda conform clasei C
            # corp metoda

    def functie_polimorfa(parametru_polimorf):  # argumentul pasat functiei poate fi de tipul clasei A, sau B, sau C
        parametru_polimorf.nume_metoda          # comportamentul functiei se adapteaza in functie de tipul argumentului

In [3]:

    # Exemplul 3 - implementarea polimorfismului prin functii polimorfe

    from math import pi

    class GeometricForms:
        def get_area(self):
            pass

    class Circle(GeometricForms):
        def __init__(self, a_radius = 1.0):
            self.__radius = a_radius
        @property
        def radius(self):
            return self.__radius
        @radius.setter
        def radius(self, new_radius):
            self.__radius = new_radius

        def get_area(self):
            return 0.5*pi*pow(self.__radius,2)

    class Square(GeometricForms):
        def __init__(self, a_length = 1.0):
            self.__length = a_length
        @property
        def length(self):
            return self.__length
        @length.setter
        def radius(self, new_length):
            self.__length = new_length

        def get_area(self):
            return pow(self.__length,2)

    def print_area(geometric_form):
        if isinstance(geometric_form, GeometricForms):
            area = geometric_form.get_area()
            print(f'Area of {geometric_form} is {area}')
        else:
            print('I|am not a GeometricForm')

    circ = Circle()
    sqar = Square()

    print_area(circ)
    print_area(sqar)
    print_area(0.0)

    Area of <__main__.Circle object at 0x000001F4C0BA8B88> is 1.5707963267948966
    Area of <__main__.Square object at 0x000001F4C0BA8C88> is 1.0
    I|am not a GeometricForm

# 3. Mostenirea claselor pe mai multe niveluri (multilevel inheritance)¶

Mostenirea unei clase se poate intinde pe mai multe niveluri: o clasa C, poate mosteni o clasa B, care, la randul ei poate mosteni o clasa A, etc. Relatia dintre clasele inrudite prin mostenire multinivel (pe mai multe niveluri) creeaza un lant, ca in figura de mai jos,

    A
       |
       B
       |
       C

care se noteaza astfel,

    C -> B -> A

ceea ce inseamna clasa C, mosteneste clasa B, care mosteneste clasa A. In acest caz, clasa C mosteneste atat clasa B, cat si clasa A. Acest lucru inseamna clasa C mosteneste toti membrii clasei B si toti membrii clasei A. In limbajul Python, adancimea lantului de mostenire poate fi oricat.

In [4]:

    # Exemplul 4 - mostenire pe multe niveluri

    # implementare clasa A
    class A:
        def __init__(self, a_val = 'A'):
            print("Initialize A'attributes")
            self.a = a_val

    # implementare clasa B: subclasa pentru clasa A
    class B(A):
        def __init__(self, b_val = 'B'):
            super().__init__(b_val)
            print("Initialize B'attributes")
            self.b = b_val

    # implementare clasa C: subclasa pentru clasa B si indirect pentru clasa A
    class C(B):
        def __init__(self, c_val = 'C'):
            super().__init__(c_val)
            print("Initialize C'attributes")
            self.c = c_val

    # creare obiect clasa C
    ob_c = C()

    # accesare atribute:
    c = ob_c.c   # atribut propriu
    b = ob_c.b   # atribut mostenit de la clasa B
    a = ob_c.a   # iatribut mostenit indirect de la clasa A

    # printing results:
    print(f'ob_c has c={c}, b={b}, a={a}')

    Initialize A'attributes
    Initialize B'attributes
    Initialize C'attributes
    ob_c has c=C, b=C, a=C

La apelarea unui membru al unui obiect de tipul unei clase aflata intr-un lant de mostenire, membrul respectiv este cautat pe baza lantului de mostenire astfel:

- prima data, membrul este cautat in clasa curenta - clasa obiectului, clasa C in exemplu;
- in cazul in care acesta nu este gasit in clasa curenta (clasa C), procesul de cautare a membrului apelat urca pe nivelul imediat superior al lantului de mostenire si continua in supeclasa clasei curente, clasa B in exemplu;
- in cazul in care membrul apelat nu este gasit in superclasa clasei curente (clasa B), procesul de cautare a membrului apelat urca pe urmatorul nivel ierarhic al lantului de mostenire si continua in supeclasa superclasei clasei curente, clasa A in exemplu.
  Procedura de cautare a membrului apelat continua prin deplasarea operatiei de cautare spre nivelurile ierarhice superioare, fara a se cauta de 2 ori in aceeasi clasa, pana la gasirea membrului apelat, sau pana se ajunge cu cautarea in clasa `object`, care reprezinta clasa parinte o tuturor claselor definite in limbajul Python si este plasata pe nivelul final (top) al tuturor cautarilor.

#### Method Resolution Order - MRO¶

Setul de reguli care sunt utilizate in stabilirea ordinii in care un membru al unei clase este cautat in lantul de mostenire, la apelarea sa, se numeste Method Resolution Order (MRO).

Ordinea de cautare MRO a unui membru al unei clase poate fi obtiunuta prin citirea valorii atributului `__mro__` sau prin apelarea metodei `mro()`, care sunt continute in oricare clasa definita in Python. Atributul reprezinta un obiect de tip `tuple`, in timp ce metoda returneaza la apelarea sa un obiect de tip `list`, ambele obiecte returnate continand ordinea de cautare a membrilor apelati.

    NumeClasa.__mro__   # returneza ordinea de cautare a membrilor clasei NumeClasa,
                        # sub forma unui obiect tuple

    NumeClasa.mro()     # returneza ordinea de cautare a membrilor clasei NumeClasa,
                        # # sub forma unui obiect list

In [5]:

    # Exemplul 5 - ordinea de cautare a unui membru apelat in cazul mostenirii pe multe niveluri

    # implementare clasa A
    class A:
        def __init__(self, a_val = 'A'):
            print("Initialize A'attributes")
            self.a = a_val

    # implementare clasa B: subclasa pentru clasa A
    class B(A):
        def __init__(self, b_val = 'B'):
            super().__init__(b_val)
            print("Initialize B'attributes")
            self.b = b_val

    # implementare clasa C: subclasa pentru clasa B si indirect pentru clasa A
    class C(B):
        def __init__(self, c_val = 'C'):
            super().__init__(c_val)
            print("Initialize C'attributes")
            self.c = c_val

    # afisarea MRO sub forma unui obiect tuple:
    print(C.__mro__)

    # afisarea MRO sub forma unui obiect list:
    print(C.mro())

    (<class '__main__.C'>, <class '__main__.B'>, <class '__main__.A'>, <class 'object'>)
    [<class '__main__.C'>, <class '__main__.B'>, <class '__main__.A'>, <class 'object'>]

# 4. Mostenirea multipla (multiple inheritance)¶

Limbajul Python permite ca o clasa sa mosteneasca mai multe clase simultan, chiar daca acestea se afla in lanturi de mostenire diferite, asa cum este sugerat in figura de mai jos. In acest caz, mostenirea este multipla.

    A B
       \ /
        C

Pentru a defini o clasa care mosteneste superclase plasate in lanturi diferite de mostenire, sintaxa utilizata este urmatoarea:

    class NumeSubclasa(NumeSuperclasa_1, ..., NumeSuperclasa_n)

In cazul mostenirii multiple, ordinea superclaselor specificate intre parantezele rotunde ale definitiei sublcasei este foarte importanta, deoarece stabileste urmatoarele:

- care dintre constructorii superclaselor mostenite sunt apelati
- ordinea in care sunt cautati membrii apelati ai unui obiect

Ordinea mentionata mai sus poate fi obtiunuta prin citirea valorii atributului `__mro__` sau prin apelarea metodei `mro()`.

In [6]:

    # Exemplul 6 - mostenire multipla din arbori de mostenire diferiti

    # implementare clasa A:
    class A:
        pass

    # implementare clasa B:
    class B:
        pass

    # implementare clasa C: mosteneste clasele B si A
    class C(B,A):
        pass

    # afisarea MRO:
    C.__mro__

Out[6]:

    (__main__.C, __main__.B, __main__.A, object)

In cazul in care mai multi membri mosteniti au acelasi nume, ordinea superclaselor specificate intre parantezele rotunde ale definitiei sublcasei, indica de fapt, care membri mosteniti sunt utilizati. Daca de exemplu, 2 superclase diferite implementeaza aceeasi metoda, MRO stabileste ordinea in care aceste metode sunt cautate la crearea unui obiect de tipul subclasei. Imediat dupa ce metoda mostenita este gasita, aceasta nu mai este cautata.

In [7]:

    # Exemplul 7 - mostenire multipla - membri mosteniti, cu acelasi nume

    # implementare clasa A:
    class A:
        def printMessage(self):
            print('Hello, I\'m class A')

    # implementare clasa B:
    class B:
        def printMessage(self):
            print('Hello, I\'m class B')

    # implementare clasa C: mosteneste clasele B si A
    class C(B,A):   # C(A,B)
        pass

    # creare obiect de tip C: mosteneste metodele printMessage() definite in ambele superclase B si A
    ob_c = C()

    # afisare ordine cautare membri clasa C:
    C.__mro__

    # apel metoda printMessage - este apelata metoda care aparine
    # clasei situata cel mai apropate de clasa curenta a obiectului
    # in lantul de cautare
    ob_c.printMessage()

    Hello, I'm class B

### Initializarea unui obiect, in cazul mostenirii din lanturi de mostenire diferite¶

In cazul mostenirii multiple, daca superclasele mostenite apratin unor lanturi de mostenire diferite, daca nu sunt initializate toate atributele mostenite de la superclase, se creeaza un obiect incomplet. Pentru evitarea acestei situatii, este necesar ca toti constructorii superclaselor mostenite sa fie executati, la crearea unui obiect de tipul subclasei.

In cazul in care este necesara executarea tuturor constructorilor superclaselor mostenite, la crearea unui obiect de tipul subclasei, este necesar ca apelarea acestora sa fie specificata explicit, in corpul metodei de initializarea a subclasei, asa cum este precizat mai jos

    NumeSuperclasa.__init__(self)

In linia de cod indicata mai sus, parametrul `self` reprezinta referinta la obiectul nou creat si trebuie specificat intre parantezele metodei `__init()__` a subclasei. Ordinea in care sunt executati constructorii superclaselor mostenite depinde de ordinea in care acestia sunt apelati in corpul metodei `__init()__` a subclasei.

In cazul in care, in corpul metodei de initializarea a subclasei se scrie linia de cod utilizata in mod uzual la initializarea unui obiect dintr-o subclasa

    super().__init__()

atunci va fi executat numai constructorul superclasei plasate cel mai aproape in lantul de mostenire de subclasa care reprezinta obiectul creat. In acest caz, deoarece nu sunt executati constructorii tuturor superclaselor mostenite, se va creea un obiect incomplet.

In [8]:

    # Exemplul 8 - mostenire multipla - crearea unui obiect complet prin executia tuturor constructorilor superclaselor

    # implementare clasa A:
    class A:
        def __init__(self):
            print("Initialize A'attributes")

    # implementare clasa B:
    class B:
        def __init__(self):
            print("Initialize B'attributes")

    # implementare clasa C - mosteneste A si B
    class C(A,B):
        def __init__(self):
            A.__init__(self)   # metoda de initializare a clasei A
            B.__init__(self)   # metoda de initializare a clasei B
            print("Initialize C'attributes")

    # creare obiect de clasa C:
    ob_c = C()

    Initialize A'attributes
    Initialize B'attributes
    Initialize C'attributes

In [9]:

    # Exemplul 9 - mostenire multipla - crearea unui obiect incomplet - eroare de programare

    # implementare clasa A:
    class A:
        def __init__(self):
            print("Initialize A'attributes")

    # implementare clasa B:
    class B:
        def __init__(self):
            print("Initialize B'attributes")

    # implementare clasa C - mosteneste A si B
    class C(B,A):  # C(A,B)
        def __init__(self):
            super().__init__()   # metoda de initializare a clasei A
            print("Initialize C'attributes")

    # creare obiect de clasa C:
    ob_c = C()

    Initialize B'attributes
    Initialize C'attributes

# 5. Adaptarea arborelui de clase Animal la polimorfism¶

|  |  | Animal |  |  |
|  | / |  | \ |  |
| Pet |  |  |  | Wild |
/ |  | \ |  | / |  | \
Cat |  | Dog |  | Lion |  | Wolf

Animal
---------------------------------------------------------------------------------------------------------------
__name : object private attribute; str type; stores the name of the Animal obj
__health : object private attribute; int type [0,10]; stores the health level of the Animal obj
__coord : object private attribute; tuple of int type; stores the coordinate (x,y) = [0,100] x [0,100] of the Animal obj
---------------------------------------------------------------------------------------------------------------
talk() : prints out a message about the specific behaviour of the animal - plymorphic method
do_something() : prints out a message about the specific behaviour of the animal - plymorphic method
eat() : prints out a message about the behaviour of the animal
sleep() : prints out a message about the behaviour of the animal
run() : returns the new coordinates (x,y) of the animal depending on the direction
---------------------------------------------------------------------------------------------------------------

Pet
---------------------------------------------------------------------------------------------------------------
do_something() : prints out a message about the specific behaviour of the Pet object
implements the polymorphic method
---------------------------------------------------------------------------------------------------------------

Wild
---------------------------------------------------------------------------------------------------------------
do_something() : prints out a message about the behaviour of the Wild object
implements the polymorphic method
---------------------------------------------------------------------------------------------------------------

Cat
---------------------------------------------------------------------------------------------------------------
__no_of_cats : class private attribute; int type; stores the number of the Cat objects
__id : object private attribute; int type; stores the id of the Dog object
---------------------------------------------------------------------------------------------------------------
get_cats() : returns the number of Cat objects
talk() : prints out a message about the specific behaviour of the cat
implements the polymorphic method
---------------------------------------------------------------------------------------------------------------

Dog
---------------------------------------------------------------------------------------------------------------
__no_of_dogs : class private attribute; int type; stores the number of the Dog objects
__id : object private attribute; int type; stores the id of the Dog object
---------------------------------------------------------------------------------------------------------------
get_dogs() : returns the number of Dog objects
talk() : prints out a message about the specific behaviour of the dog
implements the polymorphic method
---------------------------------------------------------------------------------------------------------------

Lion
---------------------------------------------------------------------------------------------------------------
__no_of_lions : class private attribute; int type; stores the number of the Lion objects
__id : object private attribute; int type; stores the id of the Lion object
---------------------------------------------------------------------------------------------------------------
get_lions() : returns the number of Lion objects
talk() : prints out a message about the specific behaviour of the lion
implements the polymorphic method
---------------------------------------------------------------------------------------------------------------

Wolf
---------------------------------------------------------------------------------------------------------------
__no_of_wolfs : class private attribute; int type; stores the number of the Wolf objects
__id : object private attribute; int type; stores the id of the Wolf object
---------------------------------------------------------------------------------------------------------------
get_wolfs() : returns the number of Wolf objects
talk() : prints out a message about the specific behaviour of the wolf
implements the polymorphic method
---------------------------------------------------------------------------------------------------------------

In [10]:

    # Animal class implementation:

    class Animal:
        '''
        Animal class:
        -----------------------------------------------------------------------------------------------------------
        __name:               str type;   name of the animal;                                  private object attribute
        __health:             int type;   = [0,10]                                             private object attribute
                              health level of the animal;
        __coord:              tuple type; (x,y) coordinates of the animal;                     private object attribute
                              x = [0,100]
                              y = [0,100]
        -----------------------------------------------------------------------------------------------------------
        talk()                prints out a message about the specific behaviour of the animal  public regular method
        do_something()        prints out a message about the specific behaviour of the animal  public regular method

        eat():                prints out a message about the behaviour of the animal           public regular method
        sleep():              prints out a message about the behaviour of the animal           public regular method
        run()                 returns the new coordinates (x,y)                                public regular method
                              of the animal, depending on the direction and speed params
        -----------------------------------------------------------------------------------------------------------
        '''

    # initializer:

        def __init__(self, n = '*', h = 10, c = (50,50)):
            self.__name   = n
            self.__health = h
            self.__coord  = c

    # getters for private attributes:

        @property
        def name(self):
            return self.__name

        @property
        def health(self):
            return self.__health

        @property
        def coord(self):
            return self.__coord

    # setters for private attributes:

        @name.setter
        def name(self, new_name):
            self.__name = new_name

        @health.setter
        def health(self, new_health):
            self.__health = new_health

        @coord.setter
        def coord(self, new_coord):
            self.__coord = new_coord

    # regular methods:

        def eat(self):
            '''
            implements the behaviour of the Animal during running action.
            '''
            print(f"I'm {self.__name} and now I'm eating!")

        def sleep(self):
            '''
            implements the behaviour of the Animal during taking a nap action.
            '''
            print(f"I'm {self.__name} and now I'm sleeping!")

        def run(self, direction, speed):
            '''
            implements the behaviour of the Animal during running action.
            - direction:
               - 'f' = running forward
               - 'b' = running backward
               - 'r' = running to right
               - 'l' = running to left
            - speed: integer [0,5]
            '''

            # define a speed_control variable:
            speed_ctrl = speed if 0 < speed <= 5 else 0

            # define a direction control variable:
            dir_ctrl = 's'
            if {'f', 'b', 'r', 'l'}.intersection(set(direction.lower())) != set({}):
                dir_ctrl = direction.lower()

            # define local functions to control the moving actions
            def to_forward(c, i):
                '''
                local function: implements the forward moving action
                increments y coordinate with i step or limits to max. value
                '''
                x, y = c[0], c[1]
                y = y + i if 0 <= y < 100 else 100
                return (x,y)

            def to_backward(c, i):
                '''
                local function: implements the backward moving action
                decrements y coordinate with i step or limits to min. value
                '''
                x, y = c[0], c[1]
                y = y - i if 0 <= y < 100 else 0
                return (x,y)

            def to_right(c, i):
                '''
                local function: implements to right moving action
                increments x coordinate with i step or limits to max. value
                '''
                x, y = c[0], c[1]
                x = x + i if 0 <= x < 100 else 100
                return (x,y)

            def to_left(c, i):
                '''
                local function: implements to left moving action
                decrements y coordinate with i step or limits to min. value
                '''
                x, y = c[0], c[1]
                x = x - i if 0 <= x < 100 else 0
                return (x,y)

            def stop(c):
                '''
                local function: returns the current coordinates
                '''
                return c

            # define path actions as lists:
            forward_path   = ['running forward',   to_forward(self.__coord, speed_ctrl)]
            backward_path  = ['running backward',  to_backward(self.__coord, speed_ctrl)]
            right_path     = ['running to right',  to_right(self.__coord, speed_ctrl)]
            left_path      = ['running to left',   to_left(self.__coord, speed_ctrl)]
            stop_path      = ['not running',       stop(self.__coord)]

            # define a controller to set the moving actions as a dictionary
            controller = {'f': forward_path,
                          'b': backward_path,
                          'r': right_path,
                          'l': left_path,
                          's': stop_path}

            # implement the running behaviour:
            print(f"I'm {self.__name} and now I'm { controller[dir_ctrl][0] }!")
            self.__coord = controller[dir_ctrl][1]

    # overwritten methods:

        def talk(self):
            '''
            polymorphic function: to be implemented inside the subclasses
            prints out the specific language of the animal
            '''
            pass

        def do_something(self):
            '''
            polymorphic function: to be implemented inside the subclasses
            prints out the specific action taken by an animal
            '''
            pass

In [11]:

    # Pet class implementation:

    class Pet(Animal):
        '''
        Pet class:
        -----------------------------------------------------------------------------------------------------------
        -----------------------------------------------------------------------------------------------------------
        do_something():       prints out a message about the specific action taken by a pet animal - public regular method
        -----------------------------------------------------------------------------------------------------------
        '''

    # initializer:

        def __init__(self, n = '*', h = 10, c = (50,50)):
            super().__init__(n, h, c)

    # regular methods:

        def do_something(self):
            '''
            prints out the specific action taken by a pet
            '''
            pet_name = self.name
            print(f"I'm {pet_name} and now I'm playing!")

In [12]:

    # Wild class implementation:

    class Wild(Animal):
        '''
        Wild class:
        -----------------------------------------------------------------------------------------------------------
        -----------------------------------------------------------------------------------------------------------
        do_something():       prints out a message about the specific action taken by a wild animal - public regular method
        -----------------------------------------------------------------------------------------------------------
        '''

    # initializer:

        def __init__(self, n = '*', h = 10, c = (50,50)):
            super().__init__(n, h, c)

    # regular methods:

        def do_something(self):
            '''
            prints out the specific action taken by a wild animal
            '''
            wild_name = self.name
            print(f"I'm {wild_name} and now I'm hunting!")

In [13]:

    # Cat class implementation:

    class Cat(Pet):
        '''
        Cat class:
        -----------------------------------------------------------------------------------------------------------
        __no_of_cats:       int type;                                                        private class attribute
                            stores the current number of Cat objects
        __id:               int type;                                                        private object attribute
                            stores the id number of the Cat objects
        -----------------------------------------------------------------------------------------------------------
        get_cats():         returns the current number of Cat objects                        public class method

        talk():             prints out a message about the specific behaviour of the cat     public regular method
        -----------------------------------------------------------------------------------------------------------
        '''

    # publi class attributes:

        __no_of_cats = 0

    # initializer:

        def __init__(self, n = '*', h = 10, c = (50,50)):
            super().__init__(n, h, c)
            Cat.__no_of_cats += 1
            self.__id = Cat.__no_of_cats

    # classmethods:

        @classmethod
        def get_cats(cls):
            return cls.__no_of_cats

    # regular methods:

        def talk(self):
            '''
            prints out the specific behaviour of the cat
            '''
            cat_name = self.name
            print(f"I'm {cat_name} and now I'm meowing!")

In [14]:

    # Dog class implementation:

    class Dog(Pet):
        '''
        Dog class:
        -----------------------------------------------------------------------------------------------------------
        __no_of_dogs:       int type;                                                        private class attribute
                            stores the current number of Dog objects
        __id:               int type;                                                        private object attribute
                            stores the id number of the Dog objects
        -----------------------------------------------------------------------------------------------------------
        get_dogs():         returns the current number of Dog objects                        public class method

        talk():             prints out a message about the specific behaviour of the dog     public regular method
        -----------------------------------------------------------------------------------------------------------
        '''

    # public class attributes:

        __no_of_dogs = 0

    # initializer:

        def __init__(self, n = '*', h = 10, c = (50,50)):
            super().__init__(n, h, c)
            Dog.__no_of_dogs += 1
            self.__id = Dog.__no_of_dogs

    # classmethods:

        @classmethod
        def get_dogs(cls):
            return cls.__no_of_dogs

    # regular methods:

        def talk(self):
            '''
            prints out the specific behaviour of the dog
            '''
            dog_name = self.name
            print(f"I'm {dog_name} and now I'm barking!")

In [15]:

    # Lion class implementation:

    class Lion(Wild):
        '''
        Lion class:
        -----------------------------------------------------------------------------------------------------------
        __no_of_lions:      int type;                                                        private class attribute
                            stores the current number of Lion objects
        __id:               int type;                                                        private object attribute
                            stores the id number of the Lion objects
        -----------------------------------------------------------------------------------------------------------
        get_lions():        returns the current number of Lion objects                        public class method

        talk():             prints out a message about the specific behaviour of the lion     public regular method
        -----------------------------------------------------------------------------------------------------------
        '''

    # public class attributes:

        __no_of_lions = 0

    # initializer:

        def __init__(self, n = '*', h = 10, c = (50,50)):
            super().__init__(n, h, c)
            Lion.__no_of_lions += 1
            self.__id = Lion.__no_of_lions

    # classmethods:

        @classmethod
        def get_lions(cls):
            return cls.__no_of_lions

    # regular methods:

        def talk(self):
            '''
            prints out the specific behaviour of the lion
            '''
            lion_name = self.name
            print(f"I'm {lion_name} and now I'm roaring!")

In [16]:

    # Wolf class implementation:

    class Wolf(Wild):
        '''
        Wolf class:
        -----------------------------------------------------------------------------------------------------------
        __no_of_wolfs:      int type;                                                        private class attribute
                            stores the current number of Wolf objects
        __id:               int type;                                                        private object attribute
                            stores the id number of the Wolf objects
        -----------------------------------------------------------------------------------------------------------
        get_wolfs():        returns the current number of Wolfn objects                        public class method

        talk():             prints out a message about the specific behaviour of the wolf      public regular method
        -----------------------------------------------------------------------------------------------------------
        '''

    # public class attributes:

        __no_of_wolfs = 0

    # initializer:

        def __init__(self, n = '*', h = 10, c = (50,50)):
            super().__init__(n, h, c)
            Wolf.__no_of_wolfs += 1
            self.__id = Wolf.__no_of_wolfs

    # classmethods:

        @classmethod
        def get_wolfs(cls):
            return cls.__no_of_wolfs

    # regular methods:

        def talk(self):
            '''
            prints out the specific behaviour of the wolf
            '''
            wolf_name = self.name
            print(f"I'm {wolf_name} and now I'm howling!")

In [17]:

    # defining a polymorphic method

    def animal_behaviour(animal):
        '''
        polymorphic function: takes Animal objects as arguments
                              and prints out their specific behaviour.
        '''

        animals = {'c':'cat', 'd':'dog', 'l':'lion', 'w':'wolf', 'a': 'unknown_animal'}

        def get_class(animal):
            '''
            local function - returns a string that indicates the type of the animal
            '''
            if isinstance(animal, Cat):
                key = 'c'
            elif isinstance(animal, Dog):
                key = 'd'
            elif isinstance(animal, Lion):
                key = 'l'
            elif isinstance(animal, Wolf):
                key = 'w'
            else:
                key = 'a'
            return animals[key]

        if isinstance(animal, Animal):
            name    = animal.name
            print(f"{name}'s {get_class(animal)} behaviour:")
            animal.talk()
            animal.do_something()
            print()
        else:
            print('Error, the given argument is not an Animal object.')

In [18]:

    # testing the polymorphic function:

    # create different objects:

    a_cat  = Cat('Ali')
    a_dog  = Dog('Bili')
    a_lion = Lion('Cili')
    a_wolf = Wolf('Dili')

    # testing the polymorphic function with various arguments:

    animal_behaviour(a_cat)
    animal_behaviour(a_dog)
    animal_behaviour(a_lion)
    animal_behaviour(a_wolf)

    Ali's cat behaviour:
    I'm Ali and now I'm meowing!
    I'm Ali and now I'm playing!

    Bili's dog behaviour:
    I'm Bili and now I'm barking!
    I'm Bili and now I'm playing!

    Cili's lion behaviour:
    I'm Cili and now I'm roaring!
    I'm Cili and now I'm hunting!

    Dili's wolf behaviour:
    I'm Dili and now I'm howling!
    I'm Dili and now I'm hunting!