O functie reprezinta o colectie de instructiuni grupate impreuna, in scopul executiei unei operatii. Functiile sunt utilizate pentru optimizarea codului, prin divizarea acestuia in segmente de dimensiuni mici, usor de controlat. Totodata, functiile permit evitarea copierii aceluiasi grup de instructiuni, oridecateori este necesar a se executa aceeasi operatie, furnizand solutii pentru reutilizarea aceluiasi cod, in continutul unui program.

Inainte de a fi utilizata intr-un program, o functie trebuie definita.

Dupa ce este definita, o functie poate fi utilizata intr-un cod/program prin apelarea sa. Codul care apeleaza functia se numeste cod apelant.

# 1. Definitia functiei¶

In limbajul de programare Python, o functie se defineste conform sintaxei:

Sintaxa definitie functie:

    def nume_functie(lista_parametri):      # header-ul, (sau semnatura) functiei
        corp_functie                        # corpul functiei = segment de cod

O functie contine un header si un corp (body).

Header-ul functiei

- header-ul incepe cu cuvantul cheie `def`, urmat de catre numele functiei nume_functie si de catre lista de parametri ai functiei, specificati intre paraneteze rotunde.
- header-ul se termina cu caracterul 2 puncte `:`

**Parametrii functiei vs. argumentele functiei**

- variabilele specificate intre paranteze, in header-ul functiei, se numesc parametri;
- parametrii functiei sunt utilizati in corpul acesteia ca niste containere (placeholders), care preiau valorile cu care este apelata functia: valorile cu care este apelata functia se numesc argumente.
- in definitia unei functii, parametrii sunt optionali: pot lipsi in definitia functiei.

**Corpul functiei**

- contine un set de instructiuni, grupate intr-un segment de cod, in scopul executarii operatiei implementate de catre functie
- in interiorul functiei, este necesar ca instructiunile din corpul scesteia sa fie identate
- se recomanda ca prima linie de cod din corpul functiei sa contina documentatia functiei, care este reprezentata prin intermediul unui obiect de tip string, denumit docstring care descrie rolul functiei, al parametrilor, valoarea returnata, daca este cazul, etc.
- daca in corpul functiei este definit un docstring, la apelarea functiei `help()` cu argumentul specificat de denumirea functiei, atunci se va afisa continutul textului inclus in docstring.
- unele functii returneaza la executie o valoare (sau mai multe valori), denumita valoarea (valori) returnata;
  - functiile care returneaza valori implementeaza in general operatii care prelucreaza informatia
  - returnarea unei valori din corpul unei functii se realizeaza cu comanda `return`;
  - functiile care returneaza valori isi termina executia in momentul in care returneaza valoarea.
- unele functii nu returneaza nicio valoare (in acest caz, este returnata valoarea `None`);
  - functiile care nu returneaza valori executa actiuni utile codului apelant
  - functiile care nu returneaza valori isi termina executia in momentul in care este executata ultima linie de cod din corpul functiei.

Observatie importanta:
Se recomanda ca functiile sa fie astfel proiectate incat sa execute doar operatii elementare. In acest mod, codul este mai usor de controlat.

### 1.1. Definitia unei functii fara parametri¶

Sintaxa:

    def nume_functie():                     # header-ul functiei
        corp_functie

In [1]:

    # Exemplul 1a: definirea unei functii fara parametri - desenarea unei linii de lungime fixa

    def drawing_line():                               # header

    # body:   docstring = descrie functia
        '''
        deseneaza o linie
        '''
    # body:
        symbol = '-'           # declaratie variabila locala functiei
        length = 50            # declaratie variabila locala functiei

        print(symbol*length)   # actiune executata de functie

In [2]:

    # Exemplul 1b: afisarea informatiilor despre o functie
    help(drawing_line)

    Help on function drawing_line in module __main__:

    drawing_line()
        deseneaza o linie

### 1.2. Definitia unei functii cu parametri¶

Sintaxa:

    def nume_functie(param_1, ..., param_n):     # header-ul functiei
        corp_functie

In [3]:

    # Exemplul 2: definirea unei fctii cu parametri - desenarea unui dreptunghi parametrizabil
    def drawing_rectangle(symbol, height, width):
        '''
        deseneaza un dreptunghi
        symbol: caracterul utilizat la umplerea dreptunghiului
        height: inaltimea dreptunghiului
        width:  latimea dreptunghiului
        '''
        for r in range(height):
            for c in range(width):
                if c == width-1:
                    print(symbol)
                else:
                    print(symbol, end = '')

### 1.3 Definirea unei functii care returneaza o valoare¶

Sintaxa:

    def nume_functie(param_1, ..., param_n):     # header-ul functiei
        segment_cod                              # corp functie
        return expresie                          # corp functie; la executia acestei linii, functia returneaza
                                                 # valoarea expresiei, apoi isi termina executia

Observatii:

- linia de cod care contine comanda `return` se numeste punct de returnare a valorii
- o functie poate avea puncte multiple de returnare a valorii
- o functie poate returna simultan mai multe valori:
  - prin intermediul unui container
  - separate de caracterul virgula `,`

In [4]:

    # Exemplul 3: definirea unei functii care returneaza o valoare - aria unui dreptunghi
    def area_rectangle(height, width):
        '''
        returneaza aria unui dreptunghi
        height: inaltimea dreptulnghiului
        width:  latimea dreptunghiului
        '''
        area = height * width
        return area

In [5]:

    # Exemplul 4: definirea unei functii care are puncte multiple de returnare a valorii
    # - clasificare luminozitate pixel
    def pixel_classification():
        '''
        returneaza clasa unui pixel
        '''
        BLACK = 0
        GRAY  = 1
        WHITE = 2

        pixel_intensity = int(input('Enter the pixel intensity [0;255]:  '))
        if pixel_intensity <= 50:
            return BLACK
        elif 50 < pixel_intensity < 200:
            return GRAY
        else:
            return WHITE

In [6]:

    # Exemplul 5: definirea unei functii care returneaza valori multiple - calcul max, min
    def compute_distances(value, neighbours):
        '''
        calculeaza valoarea maxima, respectiv minima a distantei
        dintre o valoare si un grup de valori
        value:      valoarea in raport cu care sunt determinate distantele
        neighbours: grupul de valori invecinate
        max_distance: valoarea maxima a distantei
        min_distance: valoarea minima a distantei

        '''
        # abs() = Python built-in function;
        #   returns the absolute value of the argument
        # min() = Python built-in function;
        #   returns the smallest item of an iterable
        #   or the smallest argument
        # max() = Python built-in function;
        #   returns the biggest item of an iterable
        #   or the biggest argument

        distances = [abs(value - neighbour) for neighbour in neighbours]  # list comprehension
        max_distance  = max(distances)
        min_distance  = min(distances)

        return max_distance, min_distance

## 2. Apelarea unei functii¶

Cand un program apeleaza o functie - call, sau invoca o functie - invoke, controlul programului este transferat functiei apelate.
In codul apelant (cel care apeleaza functia), in punctul de apel (locul in care este apelata functia) se vor executa instructiunile incluse in corpul functiei apelate; instructiunile executate vor utiliza valorile argumentelor cu care functia este apelata.
La iesirea din functia apelata (cand aceasta returneaza o valoare, sau isi executa instructiunea de pe ultima linie de cod a corpului), controlul programului este transferat inapoi codului apelant (programului).
In general, functiile care returneaza valori, sunt apelate in cadrul unor expresii, sau ca argumente pentru alte functii. In cazul in care aceste functii sunt apelate in linii de cod in care valorile returnate nu sunt utilizate, atunci aceste valori sunt ignorate in programul apelant.

#### Stiva de functii apelate¶

De fiecare data cand o functie este apelata, sistemul care gestioneaza executia codului software salveaza argumentele si variabilele utilizate in corpul functiei (numite variabile locale functiei), intr-un bloc de memorie, numita stiva (stack, sau execution stack, runtime stack, machine stack).
In cazul in care functia apelata, la randul ei apeleaza o alta functie, argumentele si variabilele locale functiei nou apelate sunt stocate in aceeasi stiva, pe un strat superior.
La apelul succesiv a mai multor functii, argumentele si variabilele locale acestora se pastreaza in stiva, pe straturi succesive, intr-o maniera last-in, first-out.
La terminarea executiei functiilor apelate, argumentele si variabilele locale acestora sunt automat sterse din stiva (sunt distruse), in ordinea inversa in care au fost stocate: mai intai sunt distruse argumentele si variabilele locale ultimei functii apelate, apoi sunt distruse argumentele si variabilele locale penultimei functii apelate, etc. pana se elibereaza intreaga stiva.

Sintaxa utilizata la apelarea unei functii:

    nume_functie(lista_argumente)

In [7]:

    # Exemplul 6: apelarea unei functii fara parametri

    drawing_line()

    --------------------------------------------------

In [10]:

    # Exemplul 7: apelarea unei functii cu parametri

    symbol = 'o'
    height = 20
    width  = 50

    drawing_rectangle(symbol, height, width)

    oooooooooooooooooooooooooooooooooooooooooooooooooo
    oooooooooooooooooooooooooooooooooooooooooooooooooo
    oooooooooooooooooooooooooooooooooooooooooooooooooo
    oooooooooooooooooooooooooooooooooooooooooooooooooo
    oooooooooooooooooooooooooooooooooooooooooooooooooo
    oooooooooooooooooooooooooooooooooooooooooooooooooo
    oooooooooooooooooooooooooooooooooooooooooooooooooo
    oooooooooooooooooooooooooooooooooooooooooooooooooo
    oooooooooooooooooooooooooooooooooooooooooooooooooo
    oooooooooooooooooooooooooooooooooooooooooooooooooo
    oooooooooooooooooooooooooooooooooooooooooooooooooo
    oooooooooooooooooooooooooooooooooooooooooooooooooo
    oooooooooooooooooooooooooooooooooooooooooooooooooo
    oooooooooooooooooooooooooooooooooooooooooooooooooo
    oooooooooooooooooooooooooooooooooooooooooooooooooo
    oooooooooooooooooooooooooooooooooooooooooooooooooo
    oooooooooooooooooooooooooooooooooooooooooooooooooo
    oooooooooooooooooooooooooooooooooooooooooooooooooo
    oooooooooooooooooooooooooooooooooooooooooooooooooo
    oooooooooooooooooooooooooooooooooooooooooooooooooo

In [11]:

    # Exemplul 8: apelarea unei functii care returneaza o valoare

    area = area_rectangle(2, 5)

    print(f'area: {area}')

    area: 10

In [12]:

    # Exemplul 9: apelarea unei functii ca argument pentru o alta functie

    print(f'area: {area_rectangle(2, 5)}')

    area: 10

In cazul in care o functie returneaza valori multiple, preluarea acestora se realizeaza in cadrul unei instructiuni de atribuire multipla.

In [13]:

    # Exemplul 10: apelarea unei functii care returneaza valori multiple

    max, min = compute_distances(10, [1, 0, 11, -22, 2, 33, -6])

    print(f'max: {max} / min: {min}')

    max: 32 / min: 1

### Apelare pozitionala (implicita) vs. apelare nominala (positional arguments vs. keyword arguments)¶

La apelarea unei functii, argumentele pot fi furnizate paramerilor functiei in doua moduri:

- pozitional (positional arguments): in acest caz, argumentele sunt furnizate in aceeasi ordine in care sunt definiti parametrii, in header-ul functiei; in cazul in care ordinea in care sunt furnizate argumentele nu respecta ordinea in care sunt definiti parametrii, rezultatul executiei functiei apelate este incorect;
- nominal (keyword arguments): in acest caz, argumentele sunt furnizate parametrilor in forma parametru = argument; acests varianta este mai sigura.
- este posibil ca la apelul unei functii cele 2 moduri utilizate in furnizarea argumentelor sa fie utilizate simultan, dar cu conditia ca argumentele furnizate pozitional sa fie plasate inaintea argumentelor furnizate nominal.

In [14]:

    # Exemplul 11: apelare pozitionala

    drawing_rectangle('x', 20, 50)

    xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

In [15]:

    # Exemplul 12: apelare nominala

    drawing_rectangle(height = 25, width = 40, symbol ='v' )

    vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
    vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
    vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
    vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
    vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
    vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
    vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
    vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
    vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
    vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
    vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
    vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
    vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
    vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
    vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
    vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
    vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
    vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
    vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
    vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
    vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
    vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
    vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
    vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
    vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv

In [18]:

    # Exemplul 13: apelare mixta

    drawing_rectangle('Y', width = 80, height = 20)

    YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY
    YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY
    YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY
    YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY
    YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY
    YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY
    YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY
    YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY
    YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY
    YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY
    YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY
    YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY
    YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY
    YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY
    YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY
    YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY
    YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY
    YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY
    YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY
    YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY

## 3. Functia `main()`¶

In implementarea programelor software, se recomanda ca principalele actiuni ale acestuia (implementate in functii) sa fie plasate intr-o functie principala, denumita `main()`.

Sintaxa:

    def main():
        corp_functie_main                       # creare variabile & apelare functii

In [19]:

    # Exemplul 14: - definitie functie main()
    def main():
        symbol = input('Enter a symbol:')
        height = int(input('Enter the height:'))
        width  = int(input('Enter the width:'))
        drawing_rectangle(symbol, height, width)

Pentru lansarea in executie a programului, functia `main()` a acestuia trebuie apelata.

### Apelarea functiei `main()`¶

In Pyhton, functiile care serversc un scop comun (de exemplu, sunt dezvoltate pentru implementarea unor operatii specifice unei categorii de aplicatii, sunt organizate in `module` Python. Un `modul` Python reprezinta un fisier Python care contine cod reutilizabil (cum sunt si functiile). Pentru utilizarea continutului unui `modul`, acesta trebuie importat cu comanda `import`.

In mod obisnuit, o aplicatie contine mai multe surse si teoretic, fiecare dintre acestea pot contine cate o functie `main()`. Dintre aceste surse, numai una reprezinta fisierul principal. Acesta contine functia `main()` care porneste aplicatia si importa modulele, definite in continutul celorlalte surse ale aplicatiei. Deoarece fiecare sursa a aplicatiei poate contine propria sa functie `main()` si numai cea definita in continutul fisierului principal trebuie executata, este util un mecanism care previne executia functiilor `main()` definite in modulele importate.

In limbajul Python este implementata o variabila interna, denumita `__name__`, care este setata automat, de catre sistem, la valoarea `__main__` doar in fisierul principal al aplicatiei. In modulele importate de catre fisierul principal, variabila `__name__` are denumirea modulului.

Mecanismul amintit mai sus se poate implementa daca in fiecare modul importat si optional si in fisierul principal, executia functiei `main()` este conditionata de catre variabila `__name__`, in cadrul unei instructiuni de decizie `if`. Acest lucru impune utilizarea in fiecare sursa a aplicatiei a codului:

    if __name__ == '__main__':    #__name__ = __main__ numai in fisierul principal
        main()

In fisierul principal, pentru apelarea functiei `main()` se poate utiliza si metoda simpla

    main()

Insa, deoarece fisierul principal poate fi, la randul lui importat in cadrul altei aplicatii, se recomanda ca si pentru fisierul principal, apelarea functiei `main()` sa respecte varianta care implica utilizarea instructiunii `if`.

In [20]:

    # Exemplul 15: apelarea functiei main

    if __name__ == '__main__':
        main()

    Enter a symbol:|
    Enter the height:25
    Enter the width:80
    ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
    ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
    ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
    ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
    ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
    ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
    ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
    ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
    ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
    ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
    ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
    ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
    ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
    ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
    ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
    ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
    ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
    ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
    ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
    ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
    ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
    ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
    ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
    ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
    ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||

## 4. Functii cu argumente implicite¶

Limbajul Python ofera posibilitatea definirii unei functii cu argumente implicite (default arguments). In cazul in care, la apelul functiei respective, acesteia nu i se furnizeaza argumente, in corpul functiei parametrii iau valorile argumentelor implicite.

Argumentele implicite sunt furnizate functiei, la definitia sa, in header.

Exista doua variante pentru acest tip de functii:

- functii cu argumente implicite pentru toti parametrii
- functii cu argumente implicite numai pentru o parte din parametri - varianta mixta

### 4.1. Functii cu argumente implicite pentru toti parametrii¶

Sintaxa:

    def nume_functie(parametru_1 = valoare_1, ..., parametru_n = valoare_n ):
        corp_functie

In [23]:

    # Exemplul 16: functie cu argumente implicite - desenarea unui dreptunghi cu valori initiale pentru toti param.
    def drawing_rectangle(symbol = '*', height = 20, width = 60):
        '''
        deseneaza un dreptunghi
        symbol: caracterul utilizat la umplerea dreptunghiului
        height: inaltimea dreptunghiului
        width:  latimea dreptunghiului
        '''
        for r in range(height):
            for c in range(width):
                if c == width-1:
                    print(symbol)
                else:
                    print(symbol, end = '')

Variante de apelare a functiilor cu argumente implicite:

In [24]:

    # Exemplul 17: apelare fara argumente

    drawing_rectangle()

    ************************************************************
    ************************************************************
    ************************************************************
    ************************************************************
    ************************************************************
    ************************************************************
    ************************************************************
    ************************************************************
    ************************************************************
    ************************************************************
    ************************************************************
    ************************************************************
    ************************************************************
    ************************************************************
    ************************************************************
    ************************************************************
    ************************************************************
    ************************************************************
    ************************************************************
    ************************************************************

In [25]:

    # Exemplul 18: apelare cu argumente

    drawing_rectangle(symbol = 'o', height = 15, width = 30)

    oooooooooooooooooooooooooooooo
    oooooooooooooooooooooooooooooo
    oooooooooooooooooooooooooooooo
    oooooooooooooooooooooooooooooo
    oooooooooooooooooooooooooooooo
    oooooooooooooooooooooooooooooo
    oooooooooooooooooooooooooooooo
    oooooooooooooooooooooooooooooo
    oooooooooooooooooooooooooooooo
    oooooooooooooooooooooooooooooo
    oooooooooooooooooooooooooooooo
    oooooooooooooooooooooooooooooo
    oooooooooooooooooooooooooooooo
    oooooooooooooooooooooooooooooo
    oooooooooooooooooooooooooooooo

In [26]:

    # Exemplul 19: apelare mixta

    drawing_rectangle(symbol = '=')

    ============================================================
    ============================================================
    ============================================================
    ============================================================
    ============================================================
    ============================================================
    ============================================================
    ============================================================
    ============================================================
    ============================================================
    ============================================================
    ============================================================
    ============================================================
    ============================================================
    ============================================================
    ============================================================
    ============================================================
    ============================================================
    ============================================================
    ============================================================

### 4.2. Functii cu argumente implicite numai pentru o parte din parametri - varianta mixta¶

- in varianta mixta ordinea parametrilor in definitia functiei conteaza: headerul functiei contine pe primele pozitii parametrii care nu primesc argumente implicite

Sintaxa:

    def nume_functie(parametru_1, parametru_2, parametru_3  = valoare_3, parametru_4  = valoare_4, ...):
        corp_functie

In definitia de mai sus, parametru_1 si parametru_2 nu primesc argumente implicite si din acest motiv sunt plasati pe primele pozitii ale definitiei header-ului.

In [27]:

    # Exemplul 20: functie cu argumente implicite - desenarea unui dreptunghi cu valori implicite pentru o parte din param.
    def drawing_rectangle(symbol, height = 20, width = 60):
        '''
        deseneaza un dreptunghi
        symbol: caracterul utilizat la umplerea dreptunghiului
        height: inaltimea dreptunghiului
        width:  latimea dreptunghiului
        '''
        for r in range(height):
            for c in range(width):
                if c == width-1:
                    print(symbol)
                else:
                    print(symbol, end = '')

    # apelare functie
    drawing_rectangle('-', 25)

    ------------------------------------------------------------
    ------------------------------------------------------------
    ------------------------------------------------------------
    ------------------------------------------------------------
    ------------------------------------------------------------
    ------------------------------------------------------------
    ------------------------------------------------------------
    ------------------------------------------------------------
    ------------------------------------------------------------
    ------------------------------------------------------------
    ------------------------------------------------------------
    ------------------------------------------------------------
    ------------------------------------------------------------
    ------------------------------------------------------------
    ------------------------------------------------------------
    ------------------------------------------------------------
    ------------------------------------------------------------
    ------------------------------------------------------------
    ------------------------------------------------------------
    ------------------------------------------------------------
    ------------------------------------------------------------
    ------------------------------------------------------------
    ------------------------------------------------------------
    ------------------------------------------------------------
    ------------------------------------------------------------

## 5. Functii cu un numar arbitrar de argumente¶

In limbajul Python se pot defini functii care, la apelare, primesc un numar arbitrar de argumente. In acest scop, limbajul Python furnizeaza 2 caractere speciale, care se plaseaza in fata numelui parametrului, in headerul care defineste functia:

- `*` => functia poate primi la apel un numar arbitrar de argumente pozitionale
- `**` => functia poate primi la apel un numar arbitrar de argumente nominale

### Functii cu un numar arbitrar de argumente pozitionale (positional arguments)¶

Sintaxa:

    def nume_functie(*args):
        corp_functie

- utilizarea `*args` in headerul definitiei functiei, determina functia sa accepte un numar arbitrar de argumente furnizate pozitional, la apelarea sa
- `*args` determina limbajul Python se creeze automat, la apelarea functiei, un container gol, de tip `tuple` in care se vor stoca toate argumentele furnizate functiei, la apelare
- in containerul `tuple` astfel creat, argumentele stocate sunt elemente ale acestuia
- in interiorul functiei, argumentele pot fi accesate conform solutiilor valabile pentru obiectele de tip `tuple`
- pentru denumirea parametrului in definitia functiei, se poate utiliza oricare nume valid pentru variabilele Python; utilizarea numelui `args` este optionala (se pot utiliza si alte nume de parametri),reprezentand doar o conventie a programatorilor Python

In [8]:

    # Exemplul 21 - functie care primeste un numar arbitrar de argumente pozitionale - functie aritmetica

    def adder (*args):              # in locul denumirii "args" se poate utiliza alt nume
        '''
        returneaza suma numerelor furnizate ca argumente la apelarea functiei
        '''
        sum = 0
        for number in args:         # args reprezinta un obiect de tip tuple;
            sum +=number            # fiecare element din args reprezinta un argument al functiei
        return sum

    # apelarea functiei - varianta 1
    result = adder(1,2,3)
    print(f'1 + 2 + 3 = {result}')

    # apelarea functiei - varianta 2
    result = adder(10,20,30,40,50)
    print(f'10 + 20 + 30  + 40 + 50 = {result}')

    1 + 2 + 3 = 6
    10 + 20 + 30  + 40 + 50 = 150

### Functii cu un numar arbitrar de argumente nominale (keyword arguments)¶

Sintaxa:

    def nume_functie(**kwargs):
        corp_functie

- utilizarea `**kwargs` in headerul functiei, determina functiasa accepte un numar arbitrar de argumente furnizate nominal, in forma parametru = argument, la apelarea sa
- `**kwargs` determina limbajul Python se creeze automat, la apelarea functiei, un container gol, de tip `dict` (dictionar) cu numele kwargs, in care se vor stoca toate perechile parametru:argument, furnizate la apelarea functiei
- in dictionarul astfel creat, argumentele stocate sunt elemente ale dictionarului, in care:
  - parametru reprezinta cheia elementului
  - argument reprezinta valoarea elementului
- in interiorul functiei, argumentele pot fi accesate conform solutiilor valabile pentru obiectele de tip `dict`
- pentru denumirea parametrului in definitia functiei, se poate utiliza oricare nume valid pentru variabilele Python; utilizarea numelui `kwargs` reprezinta doar o conventie a programatorilor Python

In [28]:

    # Exemplul 22 - functie care primeste un numar arbitrar de argumente nominale - informatii user

    def user_info (**kwargs):
        user = {}
        for k,v in kwargs.items():
            user[k] = v
        return user

    # apelare functie:

    user_1 = user_info(name = 'Anna', age = 22, status = 'student')
    user_2 = user_info(name = 'Bili', age = 23)

    # afisare rezultate:

    print(f'user_1: {user_1}', end = '\n'*2)
    print(f'user_2: {user_2}')

    user_1: {'name': 'Anna', 'age': 22, 'status': 'student'}

    user_2: {'name': 'Bili', 'age': 23}

### Ordinea parametrilor/argumentelor la apelarea functiilor cu tipuri mixte de parametri¶

In limbajul Python, se permite definirea unor functii cu tipuri mixte de parametri, care contin atat parametri standard, cat si parametri de tipul `*args` si/sau `**kwargs`. Aceste functii accepta la apelare atat argumente care sunt furnizate unor parametri standard, cat si un numar arbitrar de argumente pozitionale si/sau nominale. In cazul functiilor cu tipuri mixte de parametri, in header-ul definitiei functiei, ordinea in care sunt specificati parametrii este urmatoarea:

- parametrii standard preced parametrii `*args` si `**kwargs`
- parametrii `*args` preced parametrii `**kwargs`

Sintaxa:

    def nume_functie(nume_parametru_standard, *args):
        corp_functie

    def nume_functie(nume_parametru_standard, **kwargs):
        corp_functie

    def nume_functie(nume_parametru_standard, *args, **kwargs):
        segment_cod

Observatie:
La apelarea functiilor care contin tipuri mixte de parametri, ordinea in care se furnizeaza argumentele trebuie sa respecte regula enuntata mai sus.

In [29]:

    # Exemplul 23 - functie care contine parametri standard, args, kwargs
    def student_grade(course, *grades, **student_info):
        '''
        inregistreaza un student la un curs si-i calculeaza media notelor obtinute
        course: nunmele cursului
        grades: obiect tuple care contine notele obtinute
        student_infor: obiect dict care contine informatiile despre student
        returneaza informatiile despre student sub forma unui obiect dict
        '''
        sum = 0
        for grade in grades:
            sum += grade
        score = sum / len(grades)
        info = {}
        info['course'] = course
        for k,v in student_info.items():
            info[k] = v
        info['score'] = score
        return info

    # apelare functie:

    student_1 = student_grade('Python', 10, 7, name = 'Anna', student_class = '5505')
    student_2 = student_grade('Java',   10, 9, name = 'Bili', student_class = '5505', status = 'graduate')

    # afisare rezultate:

    print(f'student_1: {student_1}', end = '\n'*2)
    print(f'student_2: {student_2}')

    student_1: {'course': 'Python', 'name': 'Anna', 'student_class': '5505', 'score': 8.5}

    student_2: {'course': 'Java', 'name': 'Bili', 'student_class': '5505', 'status': 'graduate', 'score': 9.5}

## 6. Domeniul de valabilitate (scope) al unei variabile: variabile locale vs. globale¶

- parametrii functiei, precum si variabilele care sunt create (prin initializarea cu o valoare) in interiorul corpului functiei, sunt numite variabile locale exista in domeniul local de valabilitate al functiei (function’s local scope).
- variablele create in exteriorul tuturor functiilor intr-un cod sursa se numesc variabile globale si exista in domeniul global de valabilitate (global scope) al modulului Python (codului sursa).
- intr-un cod software, o variabila poate fi sau globala sau locala (nu ambele simultan)
- o variabila exista atat timp cat domeniul sau de valabilitate (domeniul in care a fost creata) exista; cand domeniul de valabilitate este distrus, variabila definita in domeniul respectiv este distrusa la randul sau;
- un domeniu global de valabilitate este unic si este creat atunci cand programul este lansat in executie; domeniul global de valabilitate este distrus atunci cand programul isi termina executia; in momentul in care domeniul de valabilitate este distrus, toate variabilele sunt distruse (globale si locale; uzual, variabilele locale sunt distruse anterior distrugerii domeniului golbal de valabilitate)
- domeniul global de valabilitate include oricare domeniu local de valabilitate; acest lucru inseamna ca o variabila creata in domeniul global de valabilitate - o variabila globala - exista si in oricare domeniu local de valabilitate
- un domeniu local de valabilitate este creat atunci cand este apelata o functie; domeniul astfel creat apartine doar functiei apelate; oricare variabila creata in corpul functiei apelate, precum si parametrii acesteia, exista doar in domeniul local al functiei apelate; domeniul local al functiei este distrus atunci cand aceasta isi termina executia; in momentul respectiv, sunt distruse toate variabilele create in corpul functiei, iar valorile parametrilor se sterg; la reapelarea aceleiasi functii, nu sunt pastrate vechile valori ale variabilelor locale si ale parametrilor functiei, avute la apelarea precedenta.
- o variabila care exista intr-un domeniu local de valabilitate, nu exista si in domeniul global de valabilitate
- intr-un cod pot exista mai multe domenii locale de valabilitate; o variabila care exista intr-un domeniu local de valabilitate, nu exista si in alt domeniu local de valabilitate

Exista un set de reguli pe care toate variabilele unui program le respecta, care sunt aplicate in functie de tipul variabilei, locala respectiv globala. Regulile respective sunt mentionate in continuare:

### Variabilele locale nu pot fi utilizate in domeniul global de valabilitate¶

O variabila locala este "vizibila" numai in corpul functiei in care a fost creata, deci nu poate fi utilizata in exteriorul functiei sale:

- => o variabila locala nu este vizibila in domeniul global de valabilitate,
- => o variabila locala nu este vizibila in alt domeniu local de valabilitate

Incercarea de a utiliza o variabila locala in exteriorul functiei in care a fost creata, lanseaza un mesaj de eroare

In [30]:

    # Exemplul 24 - utilizarea unei variabile locale in domeniul global de valabilitate
    def dummy_function(name):
        message = 'Hello!' + name  # message = variabila locala functiei dummy_function

    print(message)                 # utilizarea variabilei message in exteriorul domeniului
                                   # sau de valabilitate genereaza o eroare

    ---------------------------------------------------------------------------
    NameError                                 Traceback (most recent call last)
    <ipython-input-30-33a0a3c4ed50> in <module>
          4
          5
    ----> 6 print(message)                 # utilizarea variabilei message in exteriorul domeniului
          7                                # sau de valabilitate genereaza o eroare

    NameError: name 'message' is not defined

In [31]:

    # Exemplul 25 - utilizarea unei variabile locale in alt domeniu local de valabilitate
    def dummy_function(name):
        message = 'Hello!' + name  # message = variabila locala functiei dummy_function

    def print_message ():
        print(message)             # utilizarea variabilei message in exteriorul domeniului
                                   # sau de valabilitate genereaza o eroare

    print_message()                # apel functie printare

    ---------------------------------------------------------------------------
    NameError                                 Traceback (most recent call last)
    <ipython-input-31-37ec00c3198c> in <module>
          8                                # sau de valabilitate genereaza o eroare
          9
    ---> 10 print_message()                # apel functie printare

    <ipython-input-31-37ec00c3198c> in print_message()
          5
          6 def print_message ():
    ----> 7     print(message)             # utilizarea variabilei message in exteriorul domeniului
          8                                # sau de valabilitate genereaza o eroare
          9

    NameError: name 'message' is not defined

### Variabilele globale pot fi citite direct intr-un domeniu local de valabilitate¶

O variabila globala poate fi citita direct, in corpul oricarei functii. Operatia de citire reprezinta operatia prin care valoarea unei variabile este accesata, fara a fi modificata. Cu alte cuvinte, atunci cand o variabila este citita, aceasta nu poate fi utilizata decat intr-o expresie.

In [32]:

    # Exemplul 26 - citirea unei variabile globale in corpul unei functii

    message = 'Hello ' + input('Enter your name: ')  #  message este o variabila globala

    def print_message():
        print(message,'!')          # variabila message poate fi utilizata intr-un domeniu local

    print_message()                 # apel functie

    Enter your name: Bili
    Hello Bili !

### Variabile locale si globale cu nume identic¶

Daca o variabila locala este declarata cu acelasi nume ca cel al unei variabile globale, este creata o variabila locala, diferita fata de cea globala.

- => in interiorul domeniului local de valabilitate in care exista variabila locala, este utilizata doar variabila locala (cea globala este ignorata)

Pentru evitarea confuziei, se recomanda sa fie utilizate nume diferite pentru variabilele locale si globale.

In [33]:

    # Exemplul 27 - crearea unei variabile locale cu acelasi nume ca al unei variable globale

    message = 'Hello ' + input('Enter your name: ') + '!'  #  message este o variabila globala

    def print_message():
        message = ''                # variabila message locala functiei
        print(message,'!')          # este utilizata variabila message locala

    print_message()                 # apel functie

    Enter your name: Cili
     !

### Variabilele globale pot fi scrise intr-un domeniu local de valabilitate¶

Prin operatia de scriere se intelege o operatie prin care unei variabile i se atribuie o noua valoare. In limbajul Python, se permite scrierea intr-un domeniu local de valabilitate (cum ar fi corpul unei functii) a unei variabile globale numai daca aceasta este declarata explicit ca fiind de tip global.
O variabila se poate declara in interiorul unui domeniu local de valabilitate ca fiind de tip global, prin utilizarea cuvantului `global` inaintea numelui variabilei respective.

In [34]:

    # Exemplul 28 - scrierea unei variabile globale in corpul unei functii
    message = ''

    def write_message():
        global message              # variabila message locala functiei
        message = 'Hello ' + input('Enter your name: ') + '!'  #  message este o variabila globala

    write_message()

    print(message)                 # apel functie

    Enter your name: Dili
    Hello Dili!

## 7. Tehnici de apelare a functiilor in Python¶

In limbajul Python variabilele sunt obiecte. Atunci cand este apelata o functie si i se furnizeaza argumente, parametrul functiei preia referinta (adresa de memorie) catre obiectul reprezentat de catre argument. Aceasta tehnica de apelare a unei functii se numeste apelare prin obiect (call-by-object) si este specifica limbajului Python.
Tehnica de apelare prin obiect depinde de tipul obiectului reprezentat de catre argument, interpretorul Python adaptandu-se la acesta astfel:

- daca argumentul este un obiect mutabil (lista, dictionar, set, etc), interpretorul utilizeaza tehnica de apelare prin referinta (call-by-reference)
- daca argumentul este un obiect imutabil (un obiect de tip numeric, string sau tuple), interpretorul utilizeaza tehnica de apelare prin valoare (call-by-value)

### Apelare prin referinta (call by reference)¶

Acest mod de apelare este specific cazului in care argumentul este un obiect mutabil. In acest caz, functiei i se furnizeaza referinta spre obiectul reprezentat ca argument si astfel functia capata control deplin asupra valorii obiectului si ii poate modifica valoarea.

### Apelare prin valoare (call by value)¶

Acest mod de apelare este specific cazului in care argumentul este un obiect imutabil. Deoarece obiectele imutabile nu pot fi modificate (nu este permisa modificarea valorii/starii acestora), in cazul in care, in corpul functiei se incearca modificarea valorii argumentului imutabil, este generata o eroare. In acest caz, nu obiectul propriu-zis este furnizat functiei, ci numai valoarea sa.

In [35]:

    # Exemplul 30 - apelarea unei functii prin intermediul unui argument imutabil
    def replace_element(element, new_element, sequence):
        for i, item in enumerate(sequence):
            if item == element:
                sequence[i] = new_element
        return sequence

    message = list('Hello') # 'Hello'
    print(f'before function: {message}', end = '\n'*2)
    replace_element('l',' ', message)

    print(f'after function:  {message}')

    before function: ['H', 'e', 'l', 'l', 'o']

    after function:  ['H', 'e', ' ', ' ', 'o']