# 1. Incapsularea clasei¶

Membrii unei clase pot fi:

- publici: sunt vizibili si din exteriorul clasei, ceea ce determina accesul direct la acestia
- privati: sunt vizibili numai in interiorul clasei, ceea ce impiedica accesul direct la acestia din exteeriorul clasei

Incapsularea unei clase (class encapsulation) reprezinta unul din cele mai importante concepte POO, care are drept efect abstactizarea clasei (class abstractization). Prin abstractizarea clasei se intelege decuplarea modului de implementare al clasei, de modul de utilizare al clasei, care este realizata prin ascunderea fata de utilizatorul clasei a detaliilor legate de modul in care aceasta este implementata. Prin aceasta tehnica, modul de implementare a clasei devine protejat fata de utilizatorul acesteia si astfel sunt prevenite eventuale modificari accidentale ale modului de implementare al clasei, care afecteaza toate obiectele create dupa tiparul acestora. Cu alte cuvinte, prin incapsulare, o clasa de comporta ca o "cutie neagra" (black-box) al carui continut nu poate fi accesat direct de catre utilizator, fiind astfel protejat la interventiile externe.

Incapsularea unei clase este realizata prin declararea membrilor sai ca privati.

In acest caz, atributele clasei devin atribute private, iar metodele clasei devin metode private. Se pot defini ca atribute private, atat atribute de obiect, cat si atribute de clasa.

Prin declarea ca privati, accesul direct a membrilor privati, prin intermediul operatorului punct `.` devine posibil numai in corpul clasei (restrictionat la interiorul clasei). Accesarea membrilor privati in corpul clasei se realizeaza prin aplicarea operatorului punct `.` referintei spre obiectul creat, `self`, sau spre clasa, `cls`, in functie de tipul membrului privat, de obiect, respectiv de clasa.

Prin incapsulare, accesarea directa, din exteriorul clasei, a membrilor privati nu mai este posibila, aplicarea operatorului punct `.` asupra unui membru privat lansand mesaje de eroare. Din acest motiv, la incapsularea unei clase, este necesar ca in corpul acesteia sa fie implementate 'cai de acces' catre membrii privati. In cazul atributelor, aceste cai de accces sunt implementate prin:

- metode publice, pentru accesarea atributelor private
- decoratori de acces, pentru accesarea atributelor private (o solutie alternativa)
- mostenirea clasei, pentru accesarea metodelor private

Aceste cai de acces controleaza modul in care sunt accesati membrii privati, putand restrictiona sau chiar impiedica accesul, daca anumite conditii nu sunt satisfacute (caile de acces filtreaza accesul la membrii privati).

In cazul atributelor private, caile de acces la acestea se numesc getter, respectiv setter:

- getterul returneaza valoarea atributului privat
- setterul permite setarea valorii atributului privat.

Ambele cai de acces pot fi astfel implementate incat sa restrictioneze accesul la atributele private; de exemplu, setterul poate fi astfel implementat incat sa permita setarea valorii atributului privat numai daca anumite conditii de compatibilitate (tip de date, domeniu de valori, etc) sunt satisfacute. Exista doua variante care pot fi utilizate pentru implementarea cailor de acces catre atributele private:

- implementare prin intermediul metodelor publice
- implementare prin intermediul decoratorilor de acces

# 2. Declararea membrilor privati¶

In limbajul Python, membrii privati sunt declarati prin prefixarea denumirii acestora cu 2 caractere undescore `__`. In cazul atributelor:

- atributele de obiect se declara in interiorul corpului metodei de intializare `__init()__`,
- atributele de clasa se declara in exteriorul metodei de initializare `__init()__`.

Sintaxa pentru declararea unui atribut de obiect privat:

    class NumeClasa:

       ....
       def __init__(...):
            self.__nume_atribut_obiect_privat  # atributul de obiect se declara
                                               # in interiorul metodei de initializare
       ...

Sintaxa pentru declararea unui atribut de clasa privat:

    class NumeClasa:
        __nume_atribut_clasa_privat     # atributul de clasa se declara in
                                        # exteriorul metodei de initializare
       ....
       def __init__(...):
            ....
       ...

Sintaxa pentru declararea unei metode private:

    class NumeClasa:

       ....
       def __nume_metoda(...):
            ....
       ...

# 3. Accesarea membrilor privati¶

Depinde de tipul membrului privat:

- atribut de obiect,
- atribut de clasa,
- metoda

si de tipul accesului:

- in corpul clasei: utilizeaza operatorul punct `.`
- din exteriorul clasei: utilizeaza metode publice de acces, sau decoratori de acces

| metoda | atribut obiect | atribut clasa
--- | --- | --- | ---
acces din ext. clasa | doar in corpul unei subclase | getter: metoda sau decorator | getter: metoda sau decorator
|  | setter: metoda sau decorator | setter: metoda sau decorator
acces in int. clasa | `self`.metoda_comuna | `self`.atribut | NumeClasa.atribut
| `cls`.metoda_clasa |  | `cls`.atribut
| `cls`.metoda_statica |  |

### Accesarea metodelor private in corpul clasei¶

In cazul in care se doreste accesarea metodelor private in corpul functiei se utilizeaza operatorul punct `.` astfel:

- pentru metode private comune, se utilizeaza referinta spre obiect `self`
- pentru metode private de clasa sau statice, se utilizeaza clasa `cls`

Accesarea in corpul clasei a unei metode comune:

    self.__metoda

Accesarea in corpul clasei a unei metode de clasa sau statice:

    cls.__metoda

### Accesarea atributelor private in corpul clasei¶

In cazul in care se doreste accesarea atributelor private in corpul functiei se utilizeaza operstorul punct `.` astfel:

- pentru atribute private de obiect se utilizeaza referinta spre obiect `self`
- pentru atribute private de clase se utilizeaza clasa `cls`

Accesarea in corpul clasei a unui atribut de obiect:

    self.__atribut

Accesarea in corpul clasei a unui atribut de clasa:

    cls.__atribut

### Accesarea metodelor private din exteriorul clasei¶

Este posibila numai in corpul unei subclase a clasei in care a fost definita, prin mostenire. Mostenirea claselor se va prezenta in urmatorul curs.

### Accesarea atributelor private din exteriorul clasei, prin intermediul metodelor publice¶

Se utilizeaza 2 tipuri de metode publice de acces:

- o metoda implementeaza getter-ul atributului, astfel incat sa returneze valoarea atributului privat (de obiect sa de clasa),
- o metoda implementeaza setter-ul atributului, astfel incat sa permita setarea valorii atributului privat (de obiect sa de clasa),

Cele 2 metode sunt definite dupa regulile specifice metodelor comune (sunt metode comune).

Prin conventie, pentru getter se utilizeaza numele get_atribut, iar pentru setter se utilizeaza numele set_atribut.

Exemplu de header pentru getter:

    def get_atribut(self):
        corp_metoda

Exemplu de header pentru setter:

    def set_atribut(self, valoare):
        corp_metoda

In cazul in care un atribut privat este reprezentat de catre o variabila booleeana, getterul acestuia este definit conform conventiei:

Exemplu de header pentru getter pentru atribut booleean:

    def is_atribut(self):
        corp_metoda

In [1]:

    # Exemplul 1 - incapsularea unei clase prin utilizarea metodelor publice

    class Human:

        __no_of_humans = 0                 # declarare atribut clasa privat

        def __init__(self, a_name = 'x'):
            if type(a_name) == str:
                self.__name = a_name        # declarare atribut obiect privat
            Human.__no_of_humans += 1       # acces atribut clasa privat
                                            # in interiorul clasei

        # getter pentru atribut de clasa, privat

        @classmethod
        def get_no_of_humans(cls):
            return cls.__no_of_humans

        # getter pentru atribut de obiect, privat

        def get_name(self):
            return self.__name

        # setter pentru atribut de obiect, privat

        def set_name(self, a_name):
            if type(a_name) == str:
                self.__name = a_name

        # metoda comuna privata

        def __learning(self, item = 'x'):
            print(f"{self.name} is learning {item}!")

In [2]:

    dir(Human)

Out[2]:

    ['_Human__learning',
     '_Human__no_of_humans',
     '__class__',
     '__delattr__',
     '__dict__',
     '__dir__',
     '__doc__',
     '__eq__',
     '__format__',
     '__ge__',
     '__getattribute__',
     '__gt__',
     '__hash__',
     '__init__',
     '__init_subclass__',
     '__le__',
     '__lt__',
     '__module__',
     '__ne__',
     '__new__',
     '__reduce__',
     '__reduce_ex__',
     '__repr__',
     '__setattr__',
     '__sizeof__',
     '__str__',
     '__subclasshook__',
     '__weakref__',
     'get_name',
     'get_no_of_humans',
     'set_name']

In [3]:

    # afisarea informatiilor despre clasa Human:

    help(Human)

    Help on class Human in module __main__:

    class Human(builtins.object)
     |  Human(a_name='x')
     |
     |  Methods defined here:
     |
     |  __init__(self, a_name='x')
     |      Initialize self.  See help(type(self)) for accurate signature.
     |
     |  get_name(self)
     |
     |  set_name(self, a_name)
     |
     |  ----------------------------------------------------------------------
     |  Class methods defined here:
     |
     |  get_no_of_humans() from builtins.type
     |
     |  ----------------------------------------------------------------------
     |  Data descriptors defined here:
     |
     |  __dict__
     |      dictionary for instance variables (if defined)
     |
     |  __weakref__
     |      list of weak references to the object (if defined)

In [4]:

    # testarea clasei Human:

    # creare obiect

    human = Human('Ali')

    # acces atribut privat clasa din exteriorul clasei

    humans = Human.get_no_of_humans()

    # acces atribut privat obiect din exteriorul clasei

    name = human.get_name()

    # afisare rezultat

    print(f'We have {humans} humans. His name is {name}.')

    # acces atribut privat obiect din exteriorul clasei

    human.set_name('Bili')
    name = human.get_name()

    # afisare rezultat

    print(f'We have {humans} humans. His name is {name}.')

    # acces metoda privata din exteriorul clasei

    try:
        human.__learning()  # acces nepermis
    except:
        print('Error" you cannot access a private method outside its class body, unless you use it inside a sublclass.')

    We have 1 humans. His name is Ali.
    We have 1 humans. His name is Bili.
    Error" you cannot access a private method outside its class body, unless you use it inside a sublclass.

### Accesarea atributelor private din exteriorul clasei, prin intermediul decoratorilor¶

Limbajul Python furnizeaza un tip special de metode, denumite decoratori de acces (property decorator), care permit o accesare facila a atributelor private, pe baza operatorului punct `.`, similar accesarii atributelor publice.
Pentru crearea unui decorator de acces se utilizeaza o caracteristica speciala a limbajului Python, denumita anotare (annotation), plasata cu o linie de cod inaintea metodelor publice care implementeaza caile de acces catre atributele private:

- pentru implementarea getterului atributului, se utilizeaza anotarea `@property`, plasata cu o linie de cod inaintea metodei publice care implementeaza getterul
- pentru implementarea setterului atributului, se utilizeaza anotarea `@nume_atribut.setter`, plasata cu o linie de cod inaintea metodei publice care implementeaza setterul

Exemplu de header pentru getter implementat ca decorator de acces:

    @property
    def nume_atribut(self):
       ...

Exemplu de header pentru setter implementat ca decorator de acces:

    @nume_atribut.setter
    def nume_atribut(self, valoare):
        ...

Prin implementarea getterului, respectiv a setterului atributelor private prin intermediul decoratorilor de acces, aceste atribute se pot accesa in exteriorul clasei prin intermediul operatorului punct `.`, la fel ca in cazul atributelor publice:

Accesarea atributelor private de obiect:

    referinta_obiect.nume_atribut

Accesarea atributelor private de clasa:

    NumeClasa.nume_atribut

In [5]:

    # Exemplul 2 - incapsularea unei clase prin utilizarea decoratorilor de acces

    class Human:

        __no_of_humans = 0                 # declarare atribut clasa privat

        def __init__(self, a_name = 'x'):
            if type(a_name) == str:
                self.__name = a_name        # declarare atribut obiect privat
            Human.__no_of_humans += 1       # acces atribut clasa privat
                                            # in interiorul clasei

        # getter atribut privat clasa

        @classmethod
        def get_no_of_humans(cls):
            return cls.__no_of_humans

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

        @name.setter
        def name(self, a_name):
            if type(a_name) == str:
                self.__name = a_name

In [6]:

    # afisarea informatiilor despre clasa Human

    help(Human)

    Help on class Human in module __main__:

    class Human(builtins.object)
     |  Human(a_name='x')
     |
     |  Methods defined here:
     |
     |  __init__(self, a_name='x')
     |      Initialize self.  See help(type(self)) for accurate signature.
     |
     |  ----------------------------------------------------------------------
     |  Class methods defined here:
     |
     |  get_no_of_humans() from builtins.type
     |
     |  ----------------------------------------------------------------------
     |  Data descriptors defined here:
     |
     |  __dict__
     |      dictionary for instance variables (if defined)
     |
     |  __weakref__
     |      list of weak references to the object (if defined)
     |
     |  name

In [7]:

    # test clasa Human:

    # creare obiect

    human = Human('Ali')

    # acces atribut privat clasa din exteriorul clasei

    humans = Human.get_no_of_humans()

    # acces atribut privat obiect din exteriorul clasei

    name = human.name

    # afisare rezultat

    print(f'We have {humans} humans. His name is {name}.')

    # acces atribut privat obiect din exteriorul clasei

    human.name = 'Bili'
    name = human.name

    # afisare rezultat

    print(f'We have {humans} humans. His name is {name}.')

    We have 1 humans. His name is Ali.
    We have 1 humans. His name is Bili.

# 4. Incapsularea clasei Employee¶

- Toate atributele listate mai jos sunt private; toate atributele private au definiti decoratori de acces.
- Toate metodele listate mai jos sunt publice.

__no_of_employee | atribut de clasa, tip intreg;
| stocheaza numarul de angajati ai institutiei
__id | atribut de obiect, tip int;
| stocheaza id-ul angajatului (numarul de ordine al angajatului);
| valoarea atributului id este setata automat, la crearea obiectului (la angajare)
__name | atribut de obiect, tip string;
| stocheaza numele angajatului
__address | atribut de obiect, tip string;
| stocheaza adresa angajatului
__salary | atribut de obiect, tip float;
| stocheaza valoarea salariului angajatului
__job | atribut de obiect,tip string;
| stocheaza pozitia angajatului
__rating | atribut de obiect,tip int;
| stocheaza ratingul angajatului = [0,10]
-------------------------- | ---------------------------------------------------------------------------------------
from_string | factory method
| returneaza un obiect de tip Employee, construit pe baza argumentelor furnizate
| utilizata cu rolul de alternativa la constructorul clasei
get_no_of_employees | metoda de clasa de tip getter
| returneaza numarul de angajati
employees_report | metoda de clasa
| prelucreaza informatiile furnizate de catre atributele de clasa
| returneaza un mesaj care contine numarul de angajati
reset_employees | metoda de clasa
| reseteaza atributul de clasa no_of_employees
is_working_day() | metoda statica
| verifica daca data frunzata ca argument reprezinta o zi de lucru
| returneaza True/False in functie de rezultatul testului
| utilizata ca metoda utilitara pentru clasa Employee
get_email() | metoda de obiect
| returneaza adresa de email a angajatului
| in forma "nume.prenume@catsBand.com"
get_recommendation() | metoda de obiect
| returneaza o recomandare pentru angajat, in functie de ratingul personal
| in functie de scorul furnizat ca argument
get_emp_info() | metoda de obiect
| returneaza si afiseaza toate informatiile despre un angajat
--------------------------- | ----------------------------------------------------------------------------------------------------------

In [8]:

    class Employee:
        '''
        Employee class:
        -----------------------------------------------------------------------------------------------------------
        __no_of_eployees:     int type;   current number of Employee objects;                 private class attribute

        __id:                 int type;   id number of the employee;                          private object attribute
        __name:               str type;   name of the employee;                               private object attribute
        __address:            str type;   address of the employee;                            private object attribute
        __salary:             float type; salary of the employee;                             private object attribute
        __job:                str type;   job of the employee;                                private object attribute
        __rating:             int type;   personal rating of the employee;                    private object attribute
        -----------------------------------------------------------------------------------------------------------
        from_string():        factory method: returns a new object based on a string;         public class method

        get_no_of_employees():gets the the current number of the employees;                   public class method
        employees_report():   prints out a report about the current number of the employees;  public class method
        reset_employees():    resets the current number of the employees;                     public class method

        is_working_day():     checks if a day is a working day;                               public static method

        get_email():          returns the email of the employee;                              public regular method
        get_recommandation(): returns a recommendation for the employee,                      public regular method
                              depending on its rating;
        get_emp_info():       returns and displays the information about the employee;        public regular method
        -----------------------------------------------------------------------------------------------------------
        '''

    # private class attribute

        __no_of_employees = 0

    # initializer: all object attributes are private

        def __init__(self, n = '*', a = '*', s = 0.0, j = 'unavailable', r = 1):
            Employee.__no_of_employees += 1
            self.__id   =  Employee.__no_of_employees   # id gets the current number of employees
            self.__name = n
            self.__address = a
            self.__salary = s
            self.__job = j
            self.__rating = r

    # factory method:

        @classmethod
        def from_string(cls, emp_info):
            '''
            takes a string provided in the format
            "firstName-lastName-address-salary-job-rating"
            and returns an Employee object
            '''
            first, last, address, salary, job, rating = emp_info.split('-')
            name = first + ' ' + last
            new_employee = cls(name, address, float(salary), job, int(rating))
            return new_employee

    # class methods:

        @classmethod
        def get_no_of_employees(cls):
            print("no_of_employees getter method called")
            return cls.__no_of_employees

        @classmethod
        def employees_report(cls):
            '''
            prints out a report concerning the current
            number of the employees
            '''
            print(f'The current number of the employees is {cls.__no_of_employees}')

        @classmethod
        def reset_employees(cls):
            '''
            if the current number of employees is greater than 0
            it resets to 0 the number of employees
            and returns a message to announce the action performed
            '''
            if (cls.__no_of_employees != 0):
                cls.__no_of_employees = 0
                print('The current number of employees was reseted.')

    # static methods:

        @staticmethod
        def is_working_day(day):
            '''
            checks if the day is a working day
            returns a boleean value, depending on the result of the test
            day has to be provided as "date" object from "datetime" module
            '''
            if isinstance(day, datetime.date):
                if day.weekday() == 5 or day.weekday() == 6:  # date.weekday returns the day of the week
                                                              # {0, 1, ..., 6}
                                                              # represented by the date
                    return False
                return True
            else:
                print('Error: the date format is wrong!')
                return None

    # getters as decorators:

        @property
        def id(self):
            print("id getter decorator called")
            return self.__id

        @property
        def name(self):
            print("name getter decorator called")
            return self.__name

        @property
        def address(self):
            print("address getter decorator called")
            return self._address

        @property
        def salary(self):
            print("salary getter decorator called")
            return self.__salary

        @property
        def job(self):
            print("job getter decorator called")
            return self.__job

        @property
        def rating(self):
            print("rating getter decorator called")
            return self.__rating

    # setters as decorators:

        @id.setter
        def id(self, id_val):
            print("id setter decorator called")
            self.__id = id_val if id_val > 0 else 0

        @name.setter
        def name(self, name_val):
            print("name setter decorator called")
            if type(name_val) == str:
                self.__name = name_val

        @address.setter
        def address (self, addr_val):
            print("address setter decorator called")
            if type(addr_val) == str:
                self.__address = addr_val

        @salary.setter
        def salary (self, new_salary):
            print("salary setter decorator called")
            if type(new_salary) == float:
                self.__salary = new_salary if new_salary > 0 else 0

        @job.setter
        def job (self, new_job):
            print("job setter decorator called")
            if type(new_job) == str:
                self.__job = new_job

        @rating.setter
        def rating (self, new_rating):
            print("rating setter decorator called")
            if type(new_rating) == int:
                self.__rating = new_rating if 0 <= new_rating <= 10 else 0

    # regular methods:

        def get_email(self):
            '''
            returns the email address of the employee in format:
            firstName.lastName@catsband.com
            '''
            first, last = self.__name.split(' ')
            return f'{first.lower()}.{last.lower()}@catsBand.com'

        def get_recommendation(self):
            '''
            returns a recommendeation for the employee
            depending on the personal rating
            '''
            dict = {'level_3': 'Perfect!. Move on to another challenge!',
                    'level_2': 'Good job! Keep going to reach the top!',
                    'level_1': 'Not so bad! You have to improve yourself!',
                    'level_0': 'You are running your time! Push harder!'}

            level = 'level_0'

            if 5 <= self.__rating <= 6:
                level = 'level_1'
            elif 7 <= self.__rating <= 9:
                level = 'level_2'
            elif self.__rating == 10:
                level = 'level_3'

            return dict[level]

        def get_emp_info(self):
            '''
            returns and displays all the information about an employee
            '''
            first, last = self.__name.split(' ')
            emp_dict = {
                'id':            self.__id,
                'first':         first,
                'last':          last.upper(),
                'email':         self.get_email(),
                'salary':        self.__salary,
                'job':           self.__job,
                'rating':        self.__rating,
                'recommendation':self.get_recommendation()
            }

            print('-'*16, ' '*2, 'Employee information', ' '*2, '-'*16)
            for k,v in emp_dict.items():
                print(f'{k:16}:   {v}')
            print('-'*16, '-'*2, '-'*len('Employee information'), '-'*2, '-'*16, end = '\n'*3)

In [9]:

    # displaying Employee class information:

    help(Employee)

    Help on class Employee in module __main__:

    class Employee(builtins.object)
     |  Employee(n='*', a='*', s=0.0, j='unavailable', r=1)
     |
     |  Employee class:
     |  -----------------------------------------------------------------------------------------------------------
     |  __no_of_eployees:     int type;   current number of Employee objects;                 private class attribute
     |
     |  __id:                 int type;   id number of the employee;                          private object attribute
     |  __name:               str type;   name of the employee;                               private object attribute
     |  __address:            str type;   address of the employee;                            private object attribute
     |  __salary:             float type; salary of the employee;                             private object attribute
     |  __job:                str type;   job of the employee;                                private object attribute
     |  __rating:             int type;   personal rating of the employee;                    private object attribute
     |  -----------------------------------------------------------------------------------------------------------
     |  from_string():        factory method: returns a new object based on a string;         public class method
     |
     |  get_no_of_employees():gets the the current number of the employees;                   public class method
     |  employees_report():   prints out a report about the current number of the employees;  public class method
     |  reset_employees():    resets the current number of the employees;                     public class method
     |
     |  is_working_day():     checks if a day is a working day;                               public static method
     |
     |  get_email():          returns the email of the employee;                              public regular method
     |  get_recommandation(): returns a recommendation for the employee,                      public regular method
     |                        depending on its rating;
     |  get_emp_info():       returns and displays the information about the employee;        public regular method
     |  -----------------------------------------------------------------------------------------------------------
     |
     |  Methods defined here:
     |
     |  __init__(self, n='*', a='*', s=0.0, j='unavailable', r=1)
     |      Initialize self.  See help(type(self)) for accurate signature.
     |
     |  get_email(self)
     |      returns the email address of the employee in format:
     |      firstName.lastName@catsband.com
     |
     |  get_emp_info(self)
     |      returns and displays all the information about an employee
     |
     |  get_recommendation(self)
     |      returns a recommendeation for the employee
     |      depending on the personal rating
     |
     |  ----------------------------------------------------------------------
     |  Class methods defined here:
     |
     |  employees_report() from builtins.type
     |      prints out a report concerning the current
     |      number of the employees
     |
     |  from_string(emp_info) from builtins.type
     |      takes a string provided in the format
     |      "firstName-lastName-address-salary-job-rating"
     |      and returns an Employee object
     |
     |  get_no_of_employees() from builtins.type
     |
     |  reset_employees() from builtins.type
     |      if the current number of employees is greater than 0
     |      it resets to 0 the number of employees
     |      and returns a message to announce the action performed
     |
     |  ----------------------------------------------------------------------
     |  Static methods defined here:
     |
     |  is_working_day(day)
     |      checks if the day is a working day
     |      returns a boleean value, depending on the result of the test
     |      day has to be provided as "date" object from "datetime" module
     |
     |  ----------------------------------------------------------------------
     |  Data descriptors defined here:
     |
     |  __dict__
     |      dictionary for instance variables (if defined)
     |
     |  __weakref__
     |      list of weak references to the object (if defined)
     |
     |  address
     |
     |  id
     |
     |  job
     |
     |  name
     |
     |  rating
     |
     |  salary

In [10]:

    # Employee class TEST:

    Employee.reset_employees()

    # creating 3 employees

    emp_1 = Employee('Moto TheAlphaCat',  'Aplha Avenue no.20',  7000.0, 'Team Leader', 10)
    emp_2 = Employee('Sunny TheWhiteCat', 'White Avenue no.21',  5000.0, 'Senior Programmer', 8)
    emp_3 = Employee('Gimi TheBlakeCat',  'Blake Avenue no.22',  2500.0, 'Junior Programmer', 1)

    # creating a list of employees:

    emps = [emp_1, emp_2, emp_3]

    # printing out the results:

    print(f'The current number of employees is {Employee.get_no_of_employees()}.', end = '\n'*3)

    for emp in emps:
        emp.get_emp_info()

    no_of_employees getter method called
    The current number of employees is 3.

    ----------------    Employee information    ----------------
    id              :   1
    first           :   Moto
    last            :   THEALPHACAT
    email           :   moto.thealphacat@catsBand.com
    salary          :   7000.0
    job             :   Team Leader
    rating          :   10
    recommendation  :   Perfect!. Move on to another challenge!
    ---------------- -- -------------------- -- ----------------

    ----------------    Employee information    ----------------
    id              :   2
    first           :   Sunny
    last            :   THEWHITECAT
    email           :   sunny.thewhitecat@catsBand.com
    salary          :   5000.0
    job             :   Senior Programmer
    rating          :   8
    recommendation  :   Good job! Keep going to reach the top!
    ---------------- -- -------------------- -- ----------------

    ----------------    Employee information    ----------------
    id              :   3
    first           :   Gimi
    last            :   THEBLAKECAT
    email           :   gimi.theblakecat@catsBand.com
    salary          :   2500.0
    job             :   Junior Programmer
    rating          :   1
    recommendation  :   You are running your time! Push harder!
    ---------------- -- -------------------- -- ----------------

In [11]:

    # changing the state of an object (testing setters)

    emp_3.salary = 3500.0
    emp_3.job = 'Programmer'
    emp_3.rating = 6
    print()

    # testing new state

    emp_3.get_emp_info()

    # reading the new state of an object (testing getters)

    if emp_3.rating >= 5:
        print('Now, you are our team member.')

    salary setter decorator called
    job setter decorator called
    rating setter decorator called

    ----------------    Employee information    ----------------
    id              :   3
    first           :   Gimi
    last            :   THEBLAKECAT
    email           :   gimi.theblakecat@catsBand.com
    salary          :   3500.0
    job             :   Programmer
    rating          :   6
    recommendation  :   Not so bad! You have to improve yourself!
    ---------------- -- -------------------- -- ----------------

    rating getter decorator called
    Now, you are our team member.