Invatare Atomica

Subprograme in contextul OOP: modularizare, overloading, lambda, transmitere prin referinta

Progres lectie:
0%
🎯

Obiectivul lectiei

La clasa XII, subprogramele sunt fundament pentru OOP: metodele clasei sunt subprograme specializate. Vei consolida modularizarea, vei intelege supraincarcarea functiilor (C++), expresiile lambda si functiile de ordin superior, si vei analiza transmiterea prin valoare vs. referinta — baze pentru design-ul claselor OOP.

Dupa aceasta lectie vei putea:

  • Sa explici legatura directa intre subprogram si metoda de clasa OOP (self/this ca parametru implicit)
  • Sa implementezi supraincarcarea functiilor in C++ si sa explici rezolutia la compilare
  • Sa scrii expresii lambda si sa le folosesti cu map, filter, sort ca predicate
  • Sa aplici functii de ordin superior (map, filter, reduce) pentru procesarea colectiilor
  • Sa analizezi complexitatea temporala si spatiala a recursivitatii si sa recunosti tail recursion
  • Sa distingi transmiterea by value vs by reference in C++ si sa alegi varianta corecta in design

Incearca singur!

Provocare cls. XII:

In Python, sorted([3,1,2]) sorteaza crescator. Cum ai sorta lista [{"n":"Ana","v":9.5},{"n":"Ion","v":7.2},{"n":"Maria","v":8.8}] descrescator dupa valoarea cheii "v", folosind o expresie lambda pe o singura linie?

💡 Ai nevoie de un indiciu?

Lambda primeste un element din lista (un dict) si returneaza valoarea dupa care sortezi. key=lambda d: d["v"] sorteaza crescator; adauga reverse=True sau neaga: lambda d: -d["v"]. Aceasta tehnica se regaseste in metodele de sortare din clasele OOP pe care le vei construi in lectiile urmatoare.

1

1. Ce este un subprogram si de ce il folosim?

Un subprogram (functie sau procedura) este un bloc de cod cu un nume propriu, care efectueaza o sarcina bine definita si poate fi apelat (executat) ori de cate ori este nevoie, fara a rescrie codul.
De ce modularizam codul?
  • Reutilizare: scrii o data, apelezi de 1000 de ori
  • Claritate: programul principal devine mai scurt si mai usor de citit
  • Depanare: un bug se repara intr-un singur loc, nu in 50 de copii
  • Testare: poti testa fiecare subprogram independent
Analogie sigura:

Un subprogram este ca o tasta de scurtatura pe un sistem de taste programabile: apesi o singura tasta si ea executa automat o secventa predefinita. Definesti secventa o singura data, o folosesti la infinit.

# Fara subprograme - cod repetitiv (problematic)
print("--- Bun venit la magazin ---")
# ... calcule ...
print("--- Bun venit la magazin ---")

# Cu subprograme - cod curat si reutilizabil
def antet():
    print("--- Bun venit la magazin ---")

antet()   # apel 1
antet()   # apel 2 - acelasi cod, fara duplicare
Output real (rulat cu python):
--- Bun venit la magazin ---
--- Bun venit la magazin ---
2

2. Anatomia unui subprogram: antet, corp, return

Un subprogram are trei parti structurale:
Antet (capul) — Corp (blocul de instructiuni) — Return (rezultatul returnat apelantului)
#        ANTET: cuvant cheie + NUME + parametri formali
def suma(a, b):
    #    CORP: instructiunile care se executa
    rezultat = a + b
    #    RETURN: valoarea trimisa inapoi apelantului
    return rezultat

# APEL: folosim numele + argumente actuale
r = suma(7, 3)
print("7 + 3 =", r)
print("20 + 15 =", suma(20, 15))
Output real (rulat cu python):
7 + 3 = 10
20 + 15 = 35
Terminologie esentiala:
  • Parametri formali = variabilele din definitie (a, b in exemplu)
  • Argumente actuale = valorile transmise la apel (7, 3 sau 20, 15)
  • Definitia = scriem subprogramul o singura data (blocul def)
  • Apelul = folosim subprogramul apelandu-l dupa nume
3

3. Functii vs Proceduri (void)

Functie = subprogram care calculeaza si returneaza un rezultat (are return valoare).
Procedura = subprogram care efectueaza o actiune fara a returna un rezultat util (Python: implicit None; C++: tip void).
# PROCEDURA: efectueaza o actiune, nu returneaza nimic
def afiseaza_bun_venit(nume):
    print(f"Bun venit, {nume}!")

# FUNCTIE: calculeaza si returneaza
def calculeaza_aria(baza, inaltime):
    aria = (baza * inaltime) / 2
    return aria

afiseaza_bun_venit("Ana")
afiseaza_bun_venit("Ion")
a = calculeaza_aria(6, 4)
print(f"Aria triunghiului: {a}")
Output real (rulat cu python):
Bun venit, Ana!
Bun venit, Ion!
Aria triunghiului: 12.0
Regula practica:

Daca subprogramul calculeaza ceva ce ai nevoie mai tarziu → functie cu return.
Daca subprogramul face ceva vizibil (afiseaza, scrie in fisier) → procedura fara return.

⚡ Sectiune EXCLUSIV INTENSIV — C++ antet complet

In C++ tipul returnat este obligatoriu in antet. Procedura foloseste void. Tipul este necesar pentru fiecare parametru.

#include <iostream>
using namespace std;

// void = procedura: nu returneaza nimic
void afiseazaBunVenit(string nume) {
    cout << "Bun venit, " << nume << "!" << endl;
}

// double = functie: returneaza un numar real
double calculeazaAria(double baza, double inaltime) {
    return (baza * inaltime) / 2.0;
}

int main() {
    afiseazaBunVenit("Ana");
    afiseazaBunVenit("Ion");
    double a = calculeazaAria(6, 4);
    cout << "Aria triunghiului: " << a << endl;
    return 0;
}
Output real (compilat g++ -std=c++17, rulat):
Bun venit, Ana!
Bun venit, Ion!
Aria triunghiului: 12
4

4. Variabile locale, globale si domeniu de vizibilitate

Variabila locala = declarata IN subprogram; exista doar cat timp subprogramul ruleaza; nu e vizibila din afara.
Variabila globala = declarata IN AFARA oricarei functii; vizibila din tot programul.
total = 0  # variabila GLOBALA

def adauga(x):
    global total   # declaram ca vrem sa modificam globalul
    total += x

def demo_local():
    mesaj = "sunt local"  # variabila LOCALA
    print(mesaj)
    # mesaj dispare cand functia se termina

adauga(5)
adauga(3)
print("Total:", total)
demo_local()
# print(mesaj)  # NameError: mesaj nu exista in afara functiei!
Output real (rulat cu python):
Total: 8
sunt local
Regula de bun simt:

Evita variabilele globale modificabile — creeaza dependente ascunse. Prefera sa transmiti date prin parametri si sa primesti rezultate prin return.

⚡ Sectiune EXCLUSIV INTENSIV — C++ variabile locale si globale

In C++ variabilele globale sunt declarate in afara oricarei functii. Variabilele locale sunt in interiorul blocului { }.

#include <iostream>
using namespace std;

int total = 0;   // GLOBALA

void adauga(int x) {
    total += x;
}

void demoLocal() {
    int lungime = 10;   // LOCALA
    cout << "Lungime locala: " << lungime << endl;
}

int main() {
    adauga(5); adauga(3);
    cout << "Total: " << total << endl;
    demoLocal();
    return 0;
}
Output real (compilat g++ -std=c++17, rulat):
Total: 8
Lungime locala: 10
5

5. Parametri cu valori implicite (default) EXCLUSIV INTENSIV — C++

Parametrii pot avea valori implicite: daca apelantul nu transmite valoarea, se foloseste cea din definitie. Aceasta reduce numarul de variante de apel necesare.

# Python: parametru cu valoare implicita
def putere(baza, exponent=2):
    return baza ** exponent

print(putere(3))       # 3^2 = 9 (exponent implicit)
print(putere(2, 10))   # 2^10 = 1024 (exponent explicit)
Output real (rulat cu python):
9
1024
⚡ Sectiune EXCLUSIV INTENSIV — C++ parametri default

In C++ parametrii cu valoare implicita trebuie sa fie la sfarsitul listei. Tipul fiecarui parametru se declara explicit.

#include <iostream>
using namespace std;

int putere(int baza, int exp = 2) {
    int rez = 1;
    for (int i = 0; i < exp; i++) rez *= baza;
    return rez;
}

int main() {
    cout << putere(3) << endl;
    cout << putere(2, 10) << endl;
    return 0;
}
Output real (compilat g++ -std=c++17, rulat):
9
1024
Comparatie Python vs C++ — antet:
Python:  def suma(a, b):               # fara tipuri explicite
C++:     int suma(int a, int b) { }   # cu tipuri obligatorii
6

6. Recursia: functia care se apeleaza pe sine EXCLUSIV INTENSIV — C++

Recursia = o functie care se apeleaza pe ea insasi cu un subproblem mai mic, pana atinge un caz de baza (conditia de oprire).
Exemplu clasic: factorial (n!)

n! = n x (n-1)!   si   0! = 1! = 1 (cazul de baza)

# Python: factorial recursiv
def factorial(n):
    if n <= 1:                    # caz de baza: OPRIRE
        return 1
    return n * factorial(n - 1)   # apel recursiv

print("5! =", factorial(5))
print("10! =", factorial(10))
Output real (rulat cu python):
5! = 120
10! = 3628800
Urmarire pas cu pas: factorial(3)
factorial(3)
  = 3 * factorial(2)
        = 2 * factorial(1)
              = 1  (caz de baza, OPRIRE)
        = 2 * 1 = 2
  = 3 * 2 = 6
⚡ Sectiune EXCLUSIV INTENSIV — C++ recursie

In C++ recursia functioneaza identic. Folosim long long pentru factorial mare (evitam overflow la int).

#include <iostream>
using namespace std;

long long factorial(int n) {
    if (n <= 1) return 1;
    return n * factorial(n - 1);
}

int main() {
    cout << "5! = " << factorial(5) << endl;
    cout << "10! = " << factorial(10) << endl;
    return 0;
}
Output real (compilat g++ -std=c++17, rulat):
5! = 120
10! = 3628800
Complexitate:

Timp: O(n) — un apel per nivel.
Spatiu: O(n) — n cadre pe stiva de apel. La n mare: Stack Overflow.

Exercitii practice

Exercitiul 1 (Nivel minim) — Scrie o functie simpla

Scrie o functie Python numita perimetru_dreptunghi(lungime, latime) care returneaza perimetrul unui dreptunghi. Apeleaz-o cu valorile 5 si 3 si afiseaza rezultatul.

Asteptat: P = 2 * (5 + 3) = 16

Vezi rezolvarea

Definim functia care calculeaza perimetrul, dupa tiparul din lectie (antet, corp, return):

  1. Antet si corp: def perimetru_dreptunghi(lungime, latime): return 2 * (lungime + latime)
  2. Apel cu valorile cerute: rezultat = perimetru_dreptunghi(5, 3)
  3. Afisare: print("P =", rezultat)

Rezultat: P = 2 * (5 + 3) = 16. E o functie (nu procedura) pentru ca foloseste return — valoarea calculata e refolosita mai departe, in variabila rezultat.

Exercitiul 2 (Nivel standard) — Procedura si functie combinate

Scrie doua subprograme Python:

  • verifica_par(n) — functie care returneaza True daca n este par, False altfel
  • afiseaza_pari(lista) — procedura care afiseaza doar elementele pare dintr-o lista

Testeaza cu lista [1, 2, 3, 4, 5, 6, 7, 8]. Asteptat: 2 4 6 8

Vezi rezolvarea

Scriem cele doua subprograme separat, respectand diferenta functie/procedura din lectie:

  1. Functie (returneaza rezultat): def verifica_par(n): return n % 2 == 0
  2. Procedura (doar afiseaza, fara return util): def afiseaza_pari(lista): for x in lista: if verifica_par(x): print(x) — procedura foloseste functia de mai sus pentru fiecare element.
  3. Apel de test: afiseaza_pari([1, 2, 3, 4, 5, 6, 7, 8])

Rezultat afisat, cate un numar pe linie: 2, 4, 6, 8 — exact elementele pare din lista, in ordinea lor din lista originala.

Exercitiul 3 (Nivel performanta) — Recursie si C++ intensiv

Implementeaza suma cifrelor unui numar recursiv in Python si C++ (intensiv):

  • Caz de baza: n < 10 → returneaza n
  • Pas recursiv: returneaza (n % 10) + suma_cifre(n // 10)

Testeaza cu n=1234. Asteptat: 1+2+3+4 = 10. Analizeaza de ce complexitatea este O(log n in baza 10).

Vezi rezolvarea

Nivel performanta — schita, nu solutia gata scrisa. Urmeaza tiparul recursiv factorial din lectie (caz de baza + pas recursiv):

  1. Antet: def suma_cifre(n):
  2. Caz de baza (oprirea recursiei): daca n < 10, numarul are o singura cifra — returneaza direct n. Fara acest caz, recursia nu se opreste (vezi lectia: "caz de baza = conditia de oprire").
  3. Pas recursiv: return (n % 10) + suma_cifre(n // 10) — ultima cifra (n % 10) plus suma cifrelor din restul numarului (n // 10, care e tot mai mic).
  4. In C++ intensiv: aceeasi structura, cu int suma_cifre(int n) si acelasi if de oprire (compara cu atomul 6 — recursie C++).

Capcana tipica: n <= 10 in loc de n < 10 strica rezultatul pentru numere cu cifra 10 imposibila, dar mai grav — uitarea cazului de baza duce la recursie infinita. Verificare: suma_cifre(1234) trebuie sa dea 10. Complexitate O(log n): numarul de apeluri = numarul de cifre ale lui n, adica ~log10(n), nu n insusi.

Ce ai invatat astazi

  • Subprogramul = bloc de cod cu nume, reutilizabil; reduce duplicarea si creste claritatea
  • Anatomia: antet (def + nume + parametri formali) + corp + return
  • Functie = returneaza valoare; Procedura = efectueaza actiune (void in C++, None in Python)
  • Variabile locale (in functie, temporare) vs globale (in afara, vizibile oriunde)
  • Parametri impliciti (default) — valori folosite cand apelantul nu le transmite
  • Recursia = functie care se apeleaza pe sine; obligatoriu caz de baza + subproblem mai mic
  • C++ (intensiv): tipuri explicite la fiecare parametru si tip return; void pentru proceduri

Vrei mai mult?

Provocare: Modifica functia factorial din lectie ca sa numere de cate ori se apeleaza pe ea insasi (adauga un contor global incrementat la fiecare apel). Ruleaza factorial(15) si factorial(30) si compara numarul de apeluri cu valoarea lui n. Ce relatie observi intre n si numarul de apeluri?

De gandit: Cand apelezi factorial(2000) in Python, primesti eroare RecursionError: maximum recursion depth exceeded. De ce recursia consuma memorie proportional cu adancimea apelurilor (fiecare apel isi pastreaza propriul spatiu pe stiva), in timp ce o bucla for care calculeaza acelasi produs nu are aceasta limita?

Deschidere: Recursia sta la baza algoritmilor divide-et-impera (Quicksort, Mergesort) si a parcurgerii structurilor ierarhice: un folder de fisiere care contine subfoldere, sau un document JSON cu obiecte imbricate, se parcurg natural printr-o functie care se apeleaza pe subarborii proprii.

Urmatoarea lectie

Continua cu Transmiterea parametrilor: by value vs by reference, variabile locale/globale aprofundat.

Continua →