# 1. Introducere¶

Programarea orientata pe obiecte (POO) furnizeaza o solutie pentru organizarea datelor si functiilor intr-un cod. In aceasta tehnologie, datele si functiile, care contribuie la modelarea un concept comun, sunt 'adunate' intr-un tip de data complex, denumit clasa. Datele proprii clasei se numesc atribute, iar functiile proprii clasei se numesc metode.
Intr-un cod software, clasa este utilizata pentru a declara un obiect. Intr-un cod, obiectul declarat este de tipul clasei respective si indeplineste rolul unei variabile de tipul clasei respective.
Se poate considera ca o clasa defineste un tipar, iar obiectul este creat conform tiparului definit de clasa.

### Terminologie specifica POO¶

- clasa: cod care defineste un tipar utilizat pentru crearea de date; acest tipar contine un set de atribute si descrie un comportament asociat tuturor datelor care pot fi create dupa tiparul stabilit de clasa
- obiect: reprezinta data creata dupa tiparul stabilit de clasa; este totodata numit si instanta a clasei; un obiect are o stare, definita de catre valorile tuturor atributelor clasei, particularizate pentru obiectul respectiv;
- atributul: reprezinta o informatie utilizata la stabilirea starii obiectului creat; atributele pot fi:
  - atribute de clasa, sau atribute globale: comune intregii clase
  - atribute de obiect: specifice doar obiectul creat
- comportamentul: stabileste actiunile care pot fi efectuate cu obiectele create dupa tiparul clasei; aceste actiuni sunt definite in cadrul functiilor clasei, care in terminologia POO se numesc metode; metodele definite in cadrul clasei alcatuiesc comportamentul unui obiect sau al clasei in care sunt definite; in functie de asocierea metoda-obiect, respectiv metoda-clasa, metodele se impart in mai multe categorii, detaliate in sectiunea 2 (Definitia clasei):
  - metode comune
  - metode de clasa
  - metode statice
- membri: reprezinta totalitatea de atribute si metode definite intr-o clasa; membrii unei clase se pot imparti in 2 mari categorii:
  - membri publici: pot fi accesati din exteriorul clasei sau a obiectului
  - membri privati: nu pot fi accesati direct din exteriorul clasei sau a obiectului decat prin mostenire

# 2. Definitia clasei¶

Sintaxa:

    class NumeClasa:        # header (signature)
        membri

- header-ul clasei: contine numele clasei;
  - prin conventie, numele clasei se declara cu litera mare;
  - prin conventie, daca numele clasei implica mai multe cuvinte, atunci acestea se concateneaza fara spatiu sau alte caractere intre ele, fiecare cuvant din componenta numelui clasei fiind specificat cu litera mare
- corpul clasei: contine membrii clasei;
  - este obligatoriu sa contina macar o linie de cod;
  - daca o clasa nu are membri, cazul clasei goale (empty class) clasa goala, atunci, in corpul clasei se utilizeaza cuvantul `pass`

### Membrii unei clase¶

- atribute de clasa (class attributes):
  - sunt atribute globale, care caracterizeaza intreaga clasa
  - sunt create in exteriorul metodei de initializare `__init__` a clasei

- atribute de obiect (object attributes sau instance attributes):
  - sunt atribute locale obiectului creat; valorile acestor atribute sunt particularizate obiectului creat
  - sunt create in interiorul metodei de initializare `__init__` a clasei

- metoda de initializare a obiectului (initializer)
  - este o metoda speciala a clasei, care este apelata automat de catre constructorul clasei, cand un obiect este creat
  - este o metoda "magica": este apelata automat, programatorul neavand acces la ea
  - este o metoda unica - **fiecare clasa are o singura metoda de initializare** (nu poate fi duplicata in corpul clasei)
  - cand este executata, initializeaza starea obiectului creat, altfel spus, initializeaza atributele obiectului
  - in limbajul Python, numele metodei de initializare este intotdeauna `__init__`
  - in header, include in mod obligatoriu, pe prima pozitie, un parametru denumit prin conventie `self`, care reprezinta o referinta la obiectul creat

- metode (methods):
  - sunt definite dupa criteriile utilizate pentru definirea functiilor
  - sunt mai multe tipuri de metode:
    - metode comune (regular methods): alcatuiesc comportamentul obiectelor
    - metode de clasa (class methods): utilizate pentru a defini metode alternative de initializare (alternative la metoda `__init__`) sau actiuni care se pot aplica asupra atributelor de clasa
    - metode statice (static methods): metode utilitare, care nu sunt legate de atributele clasei

In limbajul Pyhton, metoda de initializare si totodata, toate metodele comune, au in mod obligatoriu, ca prim parametru in header-ul definitiei lor, un parametru care, la crearea unui obiect, primeste automat (fara interventia programatorului) valoarea referintei catre obiectul creat. Prin conventie, acest parametru este denumit `self`.
Atunci cand un obiect este creat, parametrului `self` i se furnizeaza automat, fara interventia programatorului, o referinta catre obiectul creat. Din acest motiv, in codul in care se declara un obiect de tipul clasei, nu trebuie sa se furnizeze explicit un argument pentru parametrul `self`.
Referinta `self` la obiectul creat este vizibila numai in corpul metodelor comune. Din acest motiv, utilizarea referintei `self` la obiectul creat nu este posibila in exteriorul corpului metodelor. Referinta `self` la obiectul creat `self` furnizeaza in corpul metodelor comune o cale de acces la membrii obiectului respectiv.

### Vizibilitatea membrilor unei clase¶

Un membru al unei clase poate fi utilizat (accesat) direct numai in portiunile de cod in are este vizibil. D.p.d.v al vizibilitatii, membrii unei clase se impart in urmatoarele categorii:

- membri publici: sunt vizibili atat in interiorul cat si in exteriorul corpului clasei in care sunt definiti
- membri privati: sunt vizibili numai in interiorul corpului clasei in care sunt definiti si prin mostenire, in interiorul corpului subclaselor clasei in care sunt definiti; nu sunt vizibili in codul in care se creeaza un obiect de tipul clasei.

Accesarea directa a unui membru al unei clase utilizeaza operatorul punct `.`, aplicat unei referinte catre un obiect, sau aplicat direct unei clase:

    referinta_obiect.membru

    numeClasa.membru

Accesarea membrilor in portiunile de cod in care acestia nu sunt vizibili este gestionata prin intermediul unor metode, special dedicate acestui scop.

Observatie: in continutul acestui curs, in toate exemplele prezentate, se va considera numai cazul in care membrii sunt considerati membri publici. Cazul membrilor privati se va discuta in cursul urmator, dedicat metodei de incapsulare a claselor, deoarece aceasta tehnica implica ascunderea membrilor clasei (a atributelor in special), prin declararea acestora ca membri privati.

In [1]:

    # Exemplul 1 - definitia unei clase goale
    class Person:
        pass

In [2]:

    # afisarea informatiilor despre clasa:

    help(Person)

    Help on class Person in module __main__:

    class Person(builtins.object)
     |  Data descriptors defined here:
     |
     |  __dict__
     |      dictionary for instance variables (if defined)
     |
     |  __weakref__
     |      list of weak references to the object (if defined)

# 3. Atribute de clasa vs. atribute de obiect¶

Tipul unui atribut (de clasa sau de obiect) este determinat de locul in care acesta este declarat (creat):

- daca este creat in exteriorul metodei `__init__()` atunci este un atribut de clasa
- daca este creat in corpul metodei `__init__()` atunci este un atribut de obiect

Atributele de clasa caracterizeaza intreaga clasa, fiind atribute globale, comune tuturor obiectelor de tipul clasei respective. Aceste atribute nu descriu starea unui obiect, ci mai degraba caracteristici specifice clasei. Un exemplu de atribut de clasa este reprezentat de catre o variabila care contorizeaza numarul total de obiecte create.

Atributele de obiect sunt reprezentate de catre atributele care descriu starea obiectului creat. Atributele de obiect sunt create in interiorul metodei de initializare `__init__()`, iar valorile lor sunt particularizate obiectului creat. Aceste atribute nu sunt partajate de catre mai multe obiecte, indiferent daca obiectele respective sunt sau nu de acelasi tip (apartin aceleiasi clase sau nu). Un exemplu de atribut de obiect este reprezentat de catre o variabila care stocheaza un nume. La crearea unui obiect, atributul este initializat cu un nume propriu, care ii apartine numai obiectului creat.

La crearea unui atribut de obiect, este obligatoriu sa se specifice explicit obiectul caruia acesta apartine. Atfel spus, la creare unui atribut de obiect, este necesara specificarea legaturii "obiect - atribut". Legatura respectiva este definita prin aplicarea operatorului punct `.`, aplicat referintei `self` la obiectul creat:

    self.atribut_obiect

Aceeasi regula se mentine oridecateori se acceseaza continutul atributului respectiv. Deoarece referinta `self` exista numai in corpul metodelor comune, regula mentionata se aplica doar in corpul acestor metode.

Sintaxa creare atribute de clasa vs. atribute de obiect:

    class NumeClasa:
        atribut_clasa = valoare_initiala   # creare atribut de clasa (global)

        def __init__(self, ...):
            # corp metoda
            self.atribut_obiect = valoare_initializare  # creare atribut
                                                        # de obiect
                                                        # (local obiectului)
            # corp metoda

Pentru initializarea atributelor de obiect se pot utiliza 3 solutii:

- valorile de initializare sunt furnizate numai in corpul metodei de initializare: in acest caz, metoda de initializare are un singur parametru si anume referinta catre obiectul creat, `self`:

Sintaxa metoda initializare:

    def __init__(self):
        corp_metoda_initializare

- valorile de initializare sunt furnizate in codul in care este creat obiectul: in acest caz, metoda de initializare are parametrul referinta catre obiectul creat, `self` pe prima pozitie, urmat de o lista de parametri utilizati in corpul metodei `__init__()`, la initializarea atributelor de obiect:

Sintaxa metoda initializare:

    def __init__(self, lista_parametri):
        corp_metoda_initializare

- valorile de initializare sunt furnizate fie in codul in care este creat obiectul fie sunt furnizate ca valori implicite: in acest caz, metoda de initializare are parametrul referinta catre obiectul creat, `self`, pe prima pozitie, urmat de o lista de parametri utilizati la initializarea atributelor de obiect, care au atribuite valori implicite, in forma parametru = valoare implicita

Sintaxa metoda initializare:

    def __init__(self, lista_parametri, parametru = valoare_implicita):
        corp_metoda_initializare

In [3]:

    # Exemplul 2a - definitia unei clase cu atribut de clasa
    class Human:

        no_of_humans = 0    # atribut de clasa

In [4]:

    # Exemplul 2b - definitia unei clase
    # cu atribut de clasa si atribut de obiect
    # se prezinta cele 3 variante de initializare ale atributului de obiect
    # in acest exemplu, numai ultima varianta a metodei de intializare
    # ramane valabila (primele 2 sunt ignorate)

    class Human:

        no_of_humans = 0                   # atribut de clasa

        # varianta 1 metoda initializare:
        # valoarea atributului este initializata
        # automat, fara a se permite interventia programatorului
        def __init__(self):
            self.name = 'Bili'            # atribut de obiect

        # varianta 2 metoda initializare:
        # valoarea atributului este initializata de catre
        # programator, in codul in care obiectul este creat
        def __init__(self, a_name):
            self.name = a_name

        # varianta 3 metoda initializare:
        # valoarea atributului este initializata de catre
        # programator, in codul in care obiectul este creat
        # sau se considera valoarea implicita
        def __init__(self, a_name = 'x'):
            self.name = a_name

In [5]:

    # afisarea informatiilor despre clasa:

    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.
     |
     |  ----------------------------------------------------------------------
     |  Data descriptors defined here:
     |
     |  __dict__
     |      dictionary for instance variables (if defined)
     |
     |  __weakref__
     |      list of weak references to the object (if defined)
     |
     |  ----------------------------------------------------------------------
     |  Data and other attributes defined here:
     |
     |  no_of_humans = 0

# 4. Construirea obiectelor de tipul unei clase¶

Dupa ce o clasa este definita, se pot construi obiecte de tipul clasei respective, apeland la constructorul clasei.

Constructorul clasei realizeaza doua lucruri:

- creaza obiectul intr-o arie de memorie special dedicata stocarii obiectelor, denumita memorie heap.
- daca in corpul clasei exista definita o metoda de initializare, constructorul o apeleaza automat; acest apel determina pasarea automata, catre parametrul `self`, a referintei catre obiectul creat si initializarea starii obiectului creat (atribuirea unor valori de initializare atributelor obiectului).

Constructorul clasei este apelat cu sintaxa:

    NumeClasa(lista_de_argumente)

Atunci cand este apelat constructorul clasei, este creat un nou obiect, de tipul clasei respective. Daca se doreste ca obiectul respectiv sa fie utilizat in cod, atunci este necesara atribuirea referintei catre obiectul creat unei variabile a codulului. Atribuirea referintei catre obiectul creat unei variabile din cod se realizeaza pe baza operatorului de atribuire `=`, conform sintaxei:

    referinta_obiect = NumeClasa(lista_de_argumente)   # variabila
                                                       # referinta_obiect
                                                       # va contine
                                                       # referinta catre
                                                       # obiectul creat

In [6]:

    # Exemplul 3 - crearea unui obiect de tip Human

    commander = Human('Alpha')

### Listarea membrilor unei clase sau a unui obiect¶

Pentru listarea membrilor unei clase, sau ai unui obiect, limbajul Python furnizeaza functia `dir()`.

Sintaxa:

    dir(NumeClasa)  # returneaza membrii unei clase

    dir(objectName) # returneaza membrii unui obiect

In [7]:

    # Exemplul 4 - listarea membrilor unui obiect

    members = dir(commander)

    for m in members:
        print(m)

    __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__
    name
    no_of_humans

# 5. Accesarea membrilor de obiect / de clasa¶

Modul de accesare a membrilor de obiect, sau de clasa, depinde de urmatoarele informatii:

- tipul membrului: de obiect, respectiv de clasa
- vizibilitatea membrului: public, respectiv privat
- locatia accesului: in corpul clasei, respectiv in exteriorul clasei

Observatie: in continuare se prezinta in detaliu modul de accesare a membrilor publici. Este mentionat si modul de accesare a membrilor privati, dar detalii despre acest caz se prezinta in urmatorul curs, dedicat incapsularii claselor. Totodata, in cazurile prezentate in continuare, termenul "exteriorul clasei" se refera la codul in care se creeaza un obiect de tipul clasei.

### Accesarea membrilor de obiect in interiorul corpului clasei¶

Membrii de obiect sunt:

- metodele comune
- atributele de obiect.

Atunci cand o clasa este definita, in corpul acesteia se defineste modul de initializare/setare a starii, respectic se defineste comportamentul tuturor obiectelor care urmeaza a fi create pe tiparul clasei respective. In acest scop, este necesar ca membrii obiectelor care urmeaza a fi create sa fie accesati in corpul clasei.

Accesarea membrilor de obiect, publici sau privati, se realizeaza pe baza operatorului punct `.`, aplicat referintei `self` catre obiectul care urmeaza a fi creat. Pentru a putea accesa un astfel de atribut, este necesar ca mai intai sa existe referinta `self` catre obiectul care urmeaza a fi creat. Din acest motiv, accesarea membrilor unui obiect (publici si/sau privati), se poate realiza numai in interiorul corpului metodelor clasei, unde referinta `self` la obiectul care urmeaza a fi creat este disponibila. Referinta `self` la obiectul care urmeaza a fi creat nu exista in exteriorul metodelor clasei, deoarece `self` reprezinta un parametru local metodei.

Indiferent daca membrii de obiect sunt publici sau privati, sintaxa utilizata pentru accesarea acestora in interiorul corpului clasei este urmatoarea:

    self.membru_obiect          # atat pentru membri publici cat si privati

### Accesarea membrilor de obiect din exteriorul corpului clasei¶

In acest context, "exteriorul clasei" se refera la codul in care este creat un obiect de tipul clasei in discutie.

Dupa ce o clasa este definita, in oricare punct al unui cod, se poate creea un obiect de tipul clasei respective.

Membrii de obiect publici (metode comune publice si atribute de obiect publice) se pot accesa in codul in care a fost creat obiectul respectiv (adica in exteriorul clasei), pe baza operatorului punct `.`, aplicat referintei catre obiectul creat.

Sintaxa utilizata pentru accesarea membrilor de obiect publici, in codul in care obiectul respectiv este creat este urmatoarea:

    referinta_obiect.membru_obiect            # numai daca membrul de obiect este public

Membrii de obiect privati (metode comune private si atribute de obiect private) nu se pot accesa direct, prin utilizarea operatorului punct `.`, deoarece nu sunt vizibili in exteriorul clasei si implicit in codul respectiv.

Pentru accesarea atributelor de obiect private in codul in care un obiect este creat, se apeleaza metode publice speciale de acces, definite in corpul clasei, care au rolul de a gestiona accesul la atributele private. Aceste metode se numesc metode getter si setter.

Metodele comune private nu pot fi accesate in codul in care este creat obiectul. Acest tip de metode se pot accesa numai in corpul subclaselor clasei in care sunt definite, prin mostenire.

In [8]:

    # Exemplul 5 - accesarea membrilor de obiect din exteriorul corpului clasei

    # creare obiecte:

    human_1 = Human('Ali')
    human_2 = Human('Bili')

    # accesare atribute obiect din codul apelant (din exteriroul clasei):

    human_1_name = human_1.name
    human_2_name = human_2.name

    # afisare valori atribute de obiect:

    print(f" Human_1's name: {human_1_name} \n\n Human_2's name: {human_2_name}")

     Human_1's name: Ali

     Human_2's name: Bili

### Accesarea membrilor de clasa in interiorul corpului clasei¶

Membrii de clasa sunt:

- metodele de clasa,
- metodele statice,
- atributele de clasa.

Accesarea membrilor de clasa in interiorul corpului clasei poate fi realizata doar in corpul metodelor clasei. In corpul metodelor comune, accesarea unui membru de clasa este realizata aplicand operatorul punct `.`referintei la clasa, care este reprezentata de numele clasei respective. Aceasta varianta de accesare a membrilor de clasa, poate fi utilizata si in cazul metodelor de clasa, respectiv statice.

    NumeClasa.membru_clasa    # in corpul oricarei metode a clasei

In corpul metodelor de clasa, respectiv statice, referinta la clasa este reprezentata de un parametru notat prin conventie `cls`, care primeste automat ca valoare numele clasei. Sintaxa utilizata pentru accesarea membrilor de clasa este urmatoarea:

    cls.membru_clasa          # numai in corpul metodelor de clasa,
                              # respectiv in corpul metodelor statice

Observatie: daca accesarea unui membru se realizeaza in interiorul corpului unei metode comune, atunci, pentru accesarea membrului respectiv, se poate utiliza si referinta `self` la obiectul creat, deoarece aceasta este disponibila in corpul metodei (in exteriorul corpului metodei, referinta `self` nu exista, deci nu poate fi utilizata). Se recomanda insa, pentru evitarea confuziilor, utilizarea referintei catre clasa si in aceste cazuri.

### Accesarea membrilor de clasa din exteriorul corpului clasei¶

In acest context, "exteriorul clasei" se refera la codul in care este creat un obiect de tipul clasei in discutie.

Membrii de clasa publici (metode de clasa, sau statice publice si atribute de clasa publice) se pot accesa in exteriorul corpului clasei pe baza operatorului punct `.`, aplicat numelui clasei. Sintaxa utilizata pentru accesarea membrilor de clasa publici este urmatoarea:

    NumeClasa.membru_clasa       # numai daca membrul este public

Observatie: Daca se doreste accesarea unui membru de clasa public in exteriorul corpului unei clase, se poate utiliza si varianta in care operatorul punct `.` este aplicat unei referinte catre un obiect de tipul clasei respective, fara a fi generate erori. Se recomanda insa, pentru evitarea confuziilor, utilizarea referintei catre clasa si in aceste cazuri.

Membrii de clasa privati (metode de clasa, sau metode statice private si atribute de clasa private) nu se pot accesa direct, prin utilizarea operatorului punct `.`, deoarece nu sunt vizibili in exteriorul clasei si implicit in codul respectiv.

Pentru accesarea atributelor de clasa private in exteriorul corpului clasei, se apeleaza metode publice speciale de acces, definite in corpul clasei, care au rolul de a gestiona accesul la atributele private.

Metodele de clasa/statice private nu pot fi accesate in exteriorul clasei, in codul in care sunt create obiectele de tipul clasei. Aceste metode se pot accesa numai in corpul subclaselor clasei in care sunt definite, prin mostenire.

In [9]:

    # Exemplul 6 - accesarea atributelor de clasa

    class Human:

        no_of_humans = 0

        def __init__(self, a_name = ''):
            self.name = a_name
            Human.no_of_humans += 1   # accesarea unui atribut de clasa
                                      # in corpul unei metode comune

    # creare obiecte:

    human_1 = Human('Ali')
    human_2 = Human('Bili')
    human_3 = Human('Cili')
    human_4 = Human('Dili')

    humans = Human.no_of_humans       # accesarea unui atribut de clasa
                                      # in exteriorul corpului clasei

    # afisare rezultat:
    print(f"The total number of created humans is {humans}.")

    The total number of created humans is 4.

# 6. Metode comune (regular methods)¶

Metodele comune descriu comportamentul obiectelor. In definitia unei clase, toate metodele sunt definite pa baza regulilor valabile la definirea functiilor, cu exceptia faptului ca, in mod obligatoriu, primul parametru din header-ul metodei trebuie sa fie rezervat stocarii valorii referintei catre obiectul creat, parametru care prin conventie se noteaza `self`.

Parametrul `self` primeste automat, ca argument, valoarea referintei la obiectul creat. Prin aceasta, in corpul metodei, parametrul `self` devine referinta la obiectul creat si astfel conecteaza metoda la acesta. In corpul metodei, parametrul `self` este utilizat pentru accesarea membrilor de obiect ai obiectului creat. Parametrul `self` este vizibil doar in corpul metodei. Prin urmare, acesta este valabil atat timp cat metoda se executa. Utilizarea parametrului `self` in exteriorul metodei genereaza o eroare.

Pentru parametrul care primeste automat valoarea referintei obiectului creat se utilizeaza prin conventie denumirea `self`, dar se poate utiliza oricare alt nume de variabila, valid in limbajul Python. In header-ul metodei, parametrul `self` nu este vizibil programatorului, din acest motiv, la apelarea metodei, nu este necesar ca parametrului `self` sa i se furnizeze explicit un argument.

Sintaxa definitie metoda comuna:

    def nume_metoda(self, lista_de_parametri):  # parametrul "self" primeste automat
                                                # referinta catre obiectul creat
        corp_metoda

Sintaxa apelare metoda comuna:

    referinta_obiect.nume_metoda(...):        # la apelul unei metode comune
                                               # nu se furnizeaza un argument
                                               # pentru parametrul 'self'
        corp_metoda

In [10]:

    # Exemplul 6 - definirea metodelor comune

    class Human:

        no_of_humans = 0

        def __init__(self, a_name = ''):
            self.name = a_name
            Human.no_of_humans += 1

        # definitie metoda comuna
        def learning(self, item = 'Python'):
            print(f"{self.name} is learning {item}!")

    # creare obiect:

    human_1 = Human('Bili')
    human_2 = Human('Cili')

    # apel metoda comuna petru obiecte create:

    human_1.learning()
    human_2.learning('Java')

    Bili is learning Python!
    Cili is learning Java!

# 7. Metode de clasa (class methods)¶

Metodele de clasa (class methods) sunt metode partajate de catre toate obiectele create dupa tiparul unei anumite clase si lucreaza in mod uzual cu atributele de clasa.

Una din aplicatiile cele mai frecvente ale metodelor de clasa o constituie utlizarea acestora pentru implementarea unor constructori de clasa alternativi (o clasa permite un singur constructor de clasa, iar metoda de clasa furnizeaza o solutie suplimentarea, de creare a unui obiect, fata de cea furnizata de constructorul de clasa). In acest caz, metoda de clasa se mai numeste si factory method (denumirea sugereaza rolul sau - metoda utilizata la construirea obiectelor).

O metoda de clasa se defineste in corpul clasei prin intermediul decoratorului `@classmethod`. La definitia metodei de clasa, este obligatoriu ca primul parametru din header-ul metodei sa fie rezervat stocarii identificatorului clasei in care este definita. Prin conventie, acest parametru se noteaza `cls`. Decoratorul `@classmethod` se introduce deasupra liniei de cod in care metoda de clasa este definita.

Parametrul `cls` primeste automat ca argument clasa in care metoda de clasa este definita. Prin aceasta, in corpul metodei, parametrul `cls` reprezinta numele clasei si astfel conecteaza metoda la clasa in care este definita. In corpul metodei, parametrul `cls` este utilizat pentru accesarea membrilor de clasa. Parametrul `cls` este vizibil doar in corpul metodei. Prin urmare, acesta este valabil atat timp cat metoda se executa. Utilizarea parametrului `cls` in exteriorul metodei constituie o eroare.

Pentru acest parametru se utilizeaza prin conventie numele `cls`, dar se poate utiliza oricare alt nume de variabila, valid in limbajul Python. Parametrul `cls` nu este vizibil programatorului, din acest motiv, la apelarea metodei de clasa, nu este necesar ca parametrului `cls` sa i se furnizeze explicit un argument.

Sintaxa definitie metoda de clasa:

    @classmethod
    def nume_metoda_clasa(cls, lista_de_parametri):
        corp_metoda

O metoda de clasa este apelata prin intermediul operatorului punct `.`, plasat intre numele clasei si numele metodei apelate.

Sintaxa apelare metoda de clasa:

    NumeClasa.nume_metoda_clasa(...)  # la apelul metodei,
                                      # nu se furnizeaza un argument
                                      # pentru parametrul 'cls'

In [11]:

    # Exemplul 7 - definirea metodelor de clasa

    class Human:

        no_of_humans = 0

        def __init__(self, a_name = ''):
            self.name = a_name
            Human.no_of_humans += 1

    # definitie metoda comuna:

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

    # definitie classmethod - metoda de clasa

        @classmethod
        def get_no_humans(cls):
             return cls.no_of_humans

    # creare obiect

    human_1 = Human('Bili')
    human_2 = Human('Cili')

    # apel metoda de clasa

    humans = Human.get_no_humans()

    # afisare rezultat

    print(f'Number of humans is {humans}')

    Number of humans is 2

In [12]:

    # Exemplul 8 - definirea metodelor de clasa - constructorul alternativ = factory method

    class Human:

        no_of_humans = 0

        def __init__(self, a_name = ''):
            self.name = a_name
            Human.no_of_humans += 1

    # definitie metoda comuna

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

    # definitie classmethod - metoda de clasa

        @classmethod
        def get_no_humans(cls):
             return cls.no_of_humans

    # definitie constructor alternativ = factory method

        @classmethod
        def from_id_number(cls, id_number = 0, name_list = ['x']):
            if id_number in range(len(name_list)):
                a_name = name_list[id_number]
                return cls(a_name)                     # returneaza constructorul clasei
            else:
                print('error')

    # creare obiecte = varianta alternativa

    name_list = ['Ali', 'Bili', 'Cili']
    human_1 = Human.from_id_number(0,name_list)
    human_2 = Human.from_id_number(1,name_list)
    human_3 = Human.from_id_number(2,name_list)

    # apel metoda de clasa

    no_of_humans = Human.get_no_humans()

    humans    = [human_1, human_2, human_3]
    languages = ['Java', 'Python', 'Julia']

    for human_language in zip(humans, languages):
        human    = human_language[0]
        language = human_language[1]
        human.learning(language)            # apel metoda comuna

    print(no_of_humans)

    Ali is learning Java!
    Bili is learning Python!
    Cili is learning Julia!
    3

# 8. Metode statice (static methods)¶

Metodele statice sunt utilizate pentru a defini metode utilitare unei clase; aceste metode nu descriu comportamentul unui obiect. Din acest motiv, la apelarea metodelor statice, nu se utilizeaza o referinta la obiect, ci numele clasei.
O metoda statica se defineste in corpul clasei prin intermediul decoratorului `@staticmethod` si nu are ca prim parametru obligatoriu un parametru rezervat preluarii valorii referintei la obiect (`self`), sau denumirii clasei (`cls`).

Sintaxa definitie metoda statica:

    @staticmethod
    def nume_metoda_statica(lista_parametri):  # lista de parametri este optionala
        corp_metoda

Sintaxa apelare metoda statica:

    NumeClasa.nume_metoda_statica(...)

In [13]:

    # Exemplul 9 - clasa cu metoda statica

    class Human:

        no_of_humans = 0

        def __init__(self, a_name = ''):
            self.name = a_name
            Human.no_of_humans += 1

    # definitie metoda comuna

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

    # definitie classmethod - metoda de clasa

        @classmethod
        def get_no_humans(cls):
             return cls.no_of_humans

    # definitie constructor alternativ = factory method

        @classmethod
        def from_id_number(cls, id = 0):
            names = ('Ali', 'Bili', 'Cili')
            if id in [0,1,2]:
                name = names[id]
            else:
                name = ''
            return cls(name)              # returneaza constructorul clasei

    # definitie metoda statica

        @staticmethod
        def house_building(windows = 0, doors = 0, rooms = 0 ):
            if rooms == 0:
                return 'to build a house'
            else:
                return f"The human's house has {doors} doors, {windows} windows and {rooms} rooms."

    # apel metoda statica

    print(Human.house_building(2,8,4))

    human = Human('Ali')
    human.learning(Human.house_building())

    The human's house has 8 doors, 2 windows and 4 rooms.
    Ali is learning to build a house!

# 9. Proiectarea claselor¶

### 9.1 Clasa Employee¶

- defineste un tip de obiecte utilizat intr-o aplicatie pentru modelarea angajatilor (employees) intr-o institutie;
- se prezinta un exemplu de creare a unei clase in mai multe iteratii;
- la fiecare iteratie, definitia clasei este tot mai complexa;
- toti membrii clasei Employee se considera ca fiind publici

#### 1. Definirea atributelor de clasa si a metodei de initializare:¶

no_of_employee | atribut de clasa, public, tip intreg;
| stocheaza numarul de angajati ai institutiei
--------------------------- | ---------------------------------------------------------------------------------------
init() | metoda de initializare:
| incrementeaza numarul de obiecte de tip Employee, de fiecare data cand este creat un astfel de obiect
--------------------------- | --------------------------------------------------------------------------------------- |

In [14]:

    class Employee:
        '''
        Employee class:
        -----------------------------------------------------------------------------------------------------------
        no_of_eployees:     int type;   current number of Employee objects;                 public class attribute
        -----------------------------------------------------------------------------------------------------------
        '''

    # public class attribute

        no_of_employees = 0

    # initializer = called automatically when an Employee object is created
    # increments the number of Employee objects, every time a new Employee object is created

        def __init__(self):
            Employee.no_of_employees += 1

In [15]:

    # displaying Employee class information:

    help(Employee)

    Help on class Employee in module __main__:

    class Employee(builtins.object)
     |  Employee class:
     |  -----------------------------------------------------------------------------------------------------------
     |  no_of_eployees:     int type;   current number of Employee objects;                 public class attribute
     |  -----------------------------------------------------------------------------------------------------------
     |
     |  Methods defined here:
     |
     |  __init__(self)
     |      Initialize self.  See help(type(self)) for accurate signature.
     |
     |  ----------------------------------------------------------------------
     |  Data descriptors defined here:
     |
     |  __dict__
     |      dictionary for instance variables (if defined)
     |
     |  __weakref__
     |      list of weak references to the object (if defined)
     |
     |  ----------------------------------------------------------------------
     |  Data and other attributes defined here:
     |
     |  no_of_employees = 0

In [16]:

    # Employee class TEST:

    # Employee() constructor creates an object in the memory
    # the assignment statement assigns the reference (address) of the new created object to "emp" variable
    # "emp" refers to the new created Employee object

    emp_1 = Employee()
    emp_2 = Employee()

    # reading the current number of employees (reading a class attribute):

    emps = Employee.no_of_employees

    # printing out the current number of employees

    print(f'The current number of employees is {emps}.')

    # adding a new employee:

    emp_3 = Employee()

    # reading the current number of employees:

    emps = Employee.no_of_employees

    # printing out the current number of employees

    print(f'The new number of employees is {emps}.')

    The current number of employees is 2.
    The new number of employees is 3.

#### 2. Definirea atributelor de obiect: definirea elementelor care modeleaza STAREA obiectelor¶

Toate atributele listate mai jos sunt publice, de obiect:

id | tip int; stocheaza id-ul angajatului (numarul de ordine al angajatului);
| valoarea atributului id este setata automat, la crearea obiectului (la angajare)
name | tip string; stocheaza numele angajatului
address | tip string; stocheaza adresa amgajatului
salary | tip float; stocheaza valoarea salariului angajatului
job | tip string; stocheaza pozitia angajatului
rating | tip int; stocheaza ratingul angajatului = [0,10]
--------------------------- | ----------------------------------------------------------------------------------------------------------

Starea unui obiect de tip Employee este definita de valorile atributelor:

- id
- name
- address
- salary
- job
- rating

In [17]:

    class Employee:
        '''
        Employee class:
        -----------------------------------------------------------------------------------------------------------
        no_of_eployees:     int type;   current number of Employee objects;                 public class attribute

        id:                 int type;   id number of the employee;                          public object attribute
        name:               str type;   name of the employee;                               public object attribute
        address:            str type;   address of the employee;                            public object attribute
        salary:             float type; salary of the employee;                             public object attribute
        job:                str type;   job of the employee;                                public object attribute
        rating:             int type;   personal rating of the employee;                    public object attribute
        -----------------------------------------------------------------------------------------------------------
        '''

    # public class attribute

        no_of_employees = 0

    # initializer = called automatically when an Employee object is created
    # increments the number of Employee objects, every time a new Employee object is created
    # and initializes the object attributes to the argument values

        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

In [18]:

    # 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;                 public class attribute
     |
     |  id:                 int type;   id number of the employee;                          public object attribute
     |  name:               str type;   name of the employee;                               public object attribute
     |  address:            str type;   address of the employee;                            public object attribute
     |  salary:             float type; salary of the employee;                             public object attribute
     |  job:                str type;   job of the employee;                                public object attribute
     |  rating:             int type;   personal rating of the employee;                    public object attribute
     |  -----------------------------------------------------------------------------------------------------------
     |
     |  Methods defined here:
     |
     |  __init__(self, n='*', a='*', s=0.0, j='unavailable', r=1)
     |      Initialize self.  See help(type(self)) for accurate signature.
     |
     |  ----------------------------------------------------------------------
     |  Data descriptors defined here:
     |
     |  __dict__
     |      dictionary for instance variables (if defined)
     |
     |  __weakref__
     |      list of weak references to the object (if defined)
     |
     |  ----------------------------------------------------------------------
     |  Data and other attributes defined here:
     |
     |  no_of_employees = 0

In [19]:

    # Employee class TEST:

    # creating an anonymous employee:

    emp_1 = Employee()

    # creating a new employee:

    emp_2 = Employee('Sunny TheWhiteCat ','White Avenue No.11', 5000.0, 'Senior programmer', 8)

    # reading the status of the created objects:

    emp_1_id      = emp_1.id
    emp_1_name    = emp_1.name
    emp_1_address = emp_1.address
    emp_1_salary  = emp_1.salary
    emp_1_job     = emp_1.job
    emp_1_rating  = emp_1.rating

    emp_2_id      = emp_2.id
    emp_2_name    = emp_2.name
    emp_2_address = emp_2.address
    emp_2_salary  = emp_2.salary
    emp_2_job     = emp_2.job
    emp_2_rating  = emp_2.rating

    # printing out the infos:

    print(f'The current number of employees is {Employee.no_of_employees}.', end = '\n'*2)
    print('Employees: ', end = '\n'*2)

    print(f'\
     id = {emp_1_id:2}, name: {emp_1_name:18}, address: {emp_1_address:20}, salary: {emp_1_salary:6}, \
    job: {emp_1_job:17}, rating: {emp_1_rating:2}', end = '\n'*2)

    print(f'\
     id = {emp_2_id:2}, name: {emp_2_name:18}, address: {emp_2_address:20}, salary: {emp_2_salary:6}, \
    job: {emp_2_job:17}, rating: {emp_2_rating:2}', end = '\n'*2)

    The current number of employees is 2.

    Employees:

     id =  1, name: *                 , address: *                   , salary:    0.0, job: unavailable      , rating:  1

     id =  2, name: Sunny TheWhiteCat , address: White Avenue No.11  , salary: 5000.0, job: Senior programmer, rating:  8

In [20]:

    # updating the status of an object:

    emp_1.name     = 'Moto TheAlphaCat'
    emp_1.address  = 'Alpha Avenue no.10'
    emp_1.salary   = 7000.0
    emp_1.job      = 'Team Leader'
    emp_1.rating   =  10

    emp_1_id      = emp_1.id
    emp_1_name    = emp_1.name
    emp_1_address = emp_1.address
    emp_1_salary  = emp_1.salary
    emp_1_job     = emp_1.job
    emp_1_rating  = emp_1.rating

    # printing out the info for the updated object:

    print(f'\
     id = {emp_1_id:2}, name: {emp_1_name:18}, address: {emp_1_address:20}, salary: {emp_1_salary:6}, \
    job: {emp_1_job:17}, rating: {emp_1_rating:2}', end = '\n'*2)

     id =  1, name: Moto TheAlphaCat  , address: Alpha Avenue no.10  , salary: 7000.0, job: Team Leader      , rating: 10

In [21]:

    # creating a new employee:

    emp_3 = Employee('Gimi TheBlackCat ','Black Avenue No.12', 2500.0, 'Junior Programmer')

    # reading the status of the 3rd employee:

    emp_3_id      = emp_3.id
    emp_3_name    = emp_3.name
    emp_3_address = emp_3.address
    emp_3_salary  = emp_3.salary
    emp_3_job     = emp_3.job
    emp_3_rating  = emp_3.rating

    # printing out the info:

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

    print('Employees: ', end = '\n'*2)

    print(f'\
     id = {emp_1_id:2}, name: {emp_1_name:18}, address: {emp_1_address:20}, salary: {emp_1_salary:6}, \
    job: {emp_1_job:17}, rating: {emp_1_rating:2}', end = '\n'*2)

    print(f'\
     id = {emp_2_id:2}, name: {emp_2_name:18}, address: {emp_2_address:20}, salary: {emp_2_salary:6}, \
    job: {emp_2_job:17}, rating: {emp_2_rating:2}', end = '\n'*2)

    print(f'\
     id = {emp_3_id:2}, name: {emp_3_name:18}, address: {emp_3_address:20}, salary: {emp_3_salary:6}, \
    job: {emp_3_job:17}, rating: {emp_3_rating:2}', end = '\n'*2)

    The current number of employees is 3.

    Employees:

     id =  1, name: Moto TheAlphaCat  , address: Alpha Avenue no.10  , salary: 7000.0, job: Team Leader      , rating: 10

     id =  2, name: Sunny TheWhiteCat , address: White Avenue No.11  , salary: 5000.0, job: Senior programmer, rating:  8

     id =  3, name: Gimi TheBlackCat  , address: Black Avenue No.12  , salary: 2500.0, job: Junior Programmer, rating:  1

#### 3. Definirea metodelor de clasa¶

Toate metodele listate mai jos sunt publice:

from_string | factory method
| returneaza un obiect de tip Employee, construit pe baza argumentelor furnizate
| utilizata cu rolul de alternativa la constructorul clasei
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
--------------------------- | ----------------------------------------------------------------------------------------------------------

In [22]:

    class Employee:
        '''
        Employee class:
        -----------------------------------------------------------------------------------------------------------
        no_of_eployees:       int type;   current number of Employee objects;                 public class attribute

        id:                   int type;   id number of the employee;                          public object attribute
        name:                 str type;   name of the employee;                               public object attribute
        address:              str type;   address of the employee;                            public object attribute
        salary:               float type; salary of the employee;                             public object attribute
        job:                  str type;   job of the employee;                                public object attribute
        rating:               int type;   personal rating of the employee;                    public object attribute
        -----------------------------------------------------------------------------------------------------------
        from_string():        factory method: returns a new object based on a string;         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
        -----------------------------------------------------------------------------------------------------------
        '''

    # public class attribute

        no_of_employees = 0

    # initializer

        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 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.')

In [23]:

    # 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;                 public class attribute
     |
     |  id:                   int type;   id number of the employee;                          public object attribute
     |  name:                 str type;   name of the employee;                               public object attribute
     |  address:              str type;   address of the employee;                            public object attribute
     |  salary:               float type; salary of the employee;                             public object attribute
     |  job:                  str type;   job of the employee;                                public object attribute
     |  rating:               int type;   personal rating of the employee;                    public object attribute
     |  -----------------------------------------------------------------------------------------------------------
     |  from_string():        factory method: returns a new object based on a string;         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
     |  -----------------------------------------------------------------------------------------------------------
     |
     |  Methods defined here:
     |
     |  __init__(self, n='*', a='*', s=0.0, j='unavailable', r=1)
     |      Initialize self.  See help(type(self)) for accurate signature.
     |
     |  ----------------------------------------------------------------------
     |  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
     |
     |  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
     |
     |  ----------------------------------------------------------------------
     |  Data descriptors defined here:
     |
     |  __dict__
     |      dictionary for instance variables (if defined)
     |
     |  __weakref__
     |      list of weak references to the object (if defined)
     |
     |  ----------------------------------------------------------------------
     |  Data and other attributes defined here:
     |
     |  no_of_employees = 0

In [24]:

    # Employee class TEST:

    # testing "reset_employees" method:

    emp = Employee()   # creating an anonymous employee

    Employee.reset_employees()

    # printing out the current number of employees:

    print(f'The current number of employees is {Employee.no_of_employees}.')

    The current number of employees was reseted.
    The current number of employees is 0.

In [25]:

    # creating a list of 3 employees from strings:

    # defining the information about the employees: a list of 3 strings

    emps_info = ['Moto-ThAlphaCat-Aplha Avenue no. 20-7000.0-Team Leader-10',
                 'Sunny-ThWhiteCat-White Avenue no. 21-5000.0-Senior Progammer-8',
                 'Gimi-TheBaclkCat-Black Avenue no. 22-2500.0-Junior Progammer-1']

    # defining an empty list of employees:

    emps = []

    # reset the number of employees

    Employee.reset_employees()
    print()

    # creating 3 employees inside the emps list:

    for i in range(len(emps_info)):
        emps.append(Employee.from_string(emps_info[i]))

    # printing out the info:

    for i in range(len(emps)):
        print(f'employee: {emps[i].id:2}, {emps[i].name:18}, {emps[i].address:20}, {emps[i].salary:7}, {emps[i].job:17}, {emps[i].rating:2}', \
              end = '\n'*2)

    employee:  1, Moto ThAlphaCat   , Aplha Avenue no. 20 ,  7000.0, Team Leader      , 10

    employee:  2, Sunny ThWhiteCat  , White Avenue no. 21 ,  5000.0, Senior Progammer ,  8

    employee:  3, Gimi TheBaclkCat  , Black Avenue no. 22 ,  2500.0, Junior Progammer ,  1

#### 4. Definirea metodelor statice - metode utilitare clasei¶

Toate metodele listate mai jos sunt publice:

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
--------------------------- | ----------------------------------------------------------------------------------------------------------

In [26]:

    class Employee:
        '''
        Employee class:
        -----------------------------------------------------------------------------------------------------------
        no_of_eployees:       int type;   current number of Employee objects;                 public class attribute

        id:                   int type;   id number of the employee;                          public object attribute
        name:                 str type;   name of the employee;                               public object attribute
        address:              str type;   address of the employee;                            public object attribute
        salary:               float type; salary of the employee;                             public object attribute
        job:                  str type;   job of the employee;                                public object attribute
        rating:               int type;   personal rating of the employee;                    public object attribute
        -----------------------------------------------------------------------------------------------------------
        from_string():        factory method: returns a new object based on a string;         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
        -----------------------------------------------------------------------------------------------------------
        '''

    # public class attribute

        no_of_employees = 0

    # initializer

        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 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

In [27]:

    # 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;                 public class attribute
     |
     |  id:                   int type;   id number of the employee;                          public object attribute
     |  name:                 str type;   name of the employee;                               public object attribute
     |  address:              str type;   address of the employee;                            public object attribute
     |  salary:               float type; salary of the employee;                             public object attribute
     |  job:                  str type;   job of the employee;                                public object attribute
     |  rating:               int type;   personal rating of the employee;                    public object attribute
     |  -----------------------------------------------------------------------------------------------------------
     |  from_string():        factory method: returns a new object based on a string;         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
     |  -----------------------------------------------------------------------------------------------------------
     |
     |  Methods defined here:
     |
     |  __init__(self, n='*', a='*', s=0.0, j='unavailable', r=1)
     |      Initialize self.  See help(type(self)) for accurate signature.
     |
     |  ----------------------------------------------------------------------
     |  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
     |
     |  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)
     |
     |  ----------------------------------------------------------------------
     |  Data and other attributes defined here:
     |
     |  no_of_employees = 0

In [28]:

    # Employee class TEST:

    # testing "is_working_day" method:

    # import "datetime" module

    import datetime

    # create a data object: 20 feb.2020

    year = 2020
    month = 3
    day = 21
    a_date = datetime.date(year, month, day)

    # pass it as argument for "is_working_day" method:

    working_day = Employee.is_working_day(a_date)

    # prints out the result:

    if working_day != None:
        type_day = 'working day' if working_day else 'free day'
        print(f'{day}.{month}.{year} was a {type_day}.')

    21.3.2020 was a free day.

#### 5. Definirea metodelor de obiect (metodelor comune) - modeleaza COMPORTAMENTUL obiectului¶

Toate metodele listate mai jos sunt publice:

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 [29]:

    class Employee:
        '''
        Employee class:
        -----------------------------------------------------------------------------------------------------------
        no_of_eployees:       int type;   current number of Employee objects;                 public class attribute

        id:                   int type;   id number of the employee;                          public object attribute
        name:                 str type;   name of the employee;                               public object attribute
        address:              str type;   address of the employee;                            public object attribute
        salary:               float type; salary of the employee;                             public object attribute
        job:                  str type;   job of the employee;                                public object attribute
        rating:               int type;   personal rating of the employee;                    public object attribute
        -----------------------------------------------------------------------------------------------------------
        from_string():        factory method: returns a new object based on a string;         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
        -----------------------------------------------------------------------------------------------------------
        '''

    # public class attribute

        no_of_employees = 0

    # initializer

        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 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

    # 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 [30]:

    # 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;                 public class attribute
     |
     |  id:                   int type;   id number of the employee;                          public object attribute
     |  name:                 str type;   name of the employee;                               public object attribute
     |  address:              str type;   address of the employee;                            public object attribute
     |  salary:               float type; salary of the employee;                             public object attribute
     |  job:                  str type;   job of the employee;                                public object attribute
     |  rating:               int type;   personal rating of the employee;                    public object attribute
     |  -----------------------------------------------------------------------------------------------------------
     |  from_string():        factory method: returns a new object based on a string;         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
     |
     |  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)
     |
     |  ----------------------------------------------------------------------
     |  Data and other attributes defined here:
     |
     |  no_of_employees = 0

In [31]:

    # 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.no_of_employees}.', end = '\n'*3)

    for emp in emps:
        emp.get_emp_info()

    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!
    ---------------- -- -------------------- -- ----------------