1/1
</> Programarea Calculatoarelor

Programarea Calculatoarelor

Lecția 1 ⏱ 90 min

Curs Complet de Programarea Calculatoarelor

De la fundamente la tehnici avansate — cu exemple în Python (și comparații C/C++/Java)


Cuprins

Partea I — Fundamente

  1. Introducere în programare și gândire algoritmică
  2. Variabile, tipuri de date și operatori
  3. Structuri de control — decizii și bucle
  4. Funcții și modularizare
  5. Șiruri de caractere (Strings)
  6. Structuri de date liniare — Liste și Tablouri
  7. Tupluri, Mulțimi și Dicționare
  8. Lucrul cu fișiere
  9. Tratarea excepțiilor și robustețea programelor

Partea II — Intermediar
10. Recursivitate
11. Algoritmi de sortare
12. Algoritmi de căutare
13. Complexitate computațională (notația Big-O)
14. Structuri de date fundamentale — Stive, Cozi, Liste înlănțuite
15. Arbori și grafuri — noțiuni de bază
16. Programare orientată pe obiecte — fundamente

Partea III — Avansat
17. OOP avansat — moștenire, polimorfism, compoziție
18. Programare funcțională în Python
19. Generatoare, iteratoare și comprehensions
20. Expresii regulate (Regex)
21. Module, pachete și ecosistemul Python
22. Introducere în testare și debugging
23. Bune practici și stil de cod


PARTEA I — FUNDAMENTE


1. Introducere în programare și gândire algoritmică

1.1 Ce este un program?

Un program este o secvență de instrucțiuni precise care descriu unui calculator cum să rezolve o problemă. Calculatorul execută instrucțiunile exact așa cum sunt scrise, fără interpretare, fără „bun simț” — ceea ce scriem este ceea ce obținem.

1.2 Ce este un algoritm?

Un algoritm este o descriere finită, clară și neambiguă a pașilor necesari pentru a rezolva o problemă. Algoritmul este independent de limbajul de programare — este ideea, metoda de rezolvare.

Proprietățile unui algoritm:

  • Finitudine — se termină după un număr finit de pași
  • Determinism — fiecare pas este precis definit
  • Intrare — primește zero sau mai multe date de intrare
  • Ieșire — produce cel puțin un rezultat
  • Efectivitate — fiecare pas poate fi executat în timp finit

1.3 Pseudocod

Pseudocodul descrie algoritmul într-un limbaj informal, structurat, independent de sintaxă:

ALGORITM: Verifică dacă un număr este prim

CITEȘTE n
DACĂ n < 2 ATUNCI
    SCRIE "Nu este prim"
    STOP

divisor ← 2
CÂT TIMP divisor * divisor <= n EXECUTĂ
    DACĂ n MOD divisor == 0 ATUNCI
        SCRIE "Nu este prim"
        STOP
    divisor ← divisor + 1

SCRIE "Este prim"

1.4 Primul program Python

# hello.py — primul program
print("Hello, World!")

# Programul Python se execută linie cu linie, de sus în jos.
# print() este o funcție care afișează text pe ecran.
# Textul între ghilimele este un string (șir de caractere).

Comparație — Hello World în alte limbaje:

// C
#include <stdio.h>
int main() {
    printf("Hello, World!\n");
    return 0;
}
// Java
public class Hello {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}

Observație: Python elimină ceremonia (include-uri, declarație de clasă, funcție main, punct și virgulă, acolade). Aceasta îl face ideal pentru învățarea conceptelor de programare fără a fi distras de sintaxă.

1.5 Cum funcționează Python

Codul sursă (.py) → Interpretor Python → Bytecode (.pyc) → Python VM → Execuție

Python este:
  - Interpretat (nu compilat direct în cod mașină)
  - Dinamic tipizat (tipul variabilei se determină la runtime)
  - Puternic tipizat (nu face conversii implicite periculoase)
  - Indentat semnificativ (indentarea definește blocurile de cod)

2. Variabile, tipuri de date și operatori

2.1 Variabile

O variabilă este un nume asociat cu o valoare stocată în memorie. În Python, nu declarăm tipul — acesta se deduce automat.

# Atribuire — se creează variabila și i se asociază o valoare
varsta = 21
nume = "Ana"
pi = 3.14159
este_student = True

# Python este dinamic tipizat:
x = 10        # x este int
x = "text"    # acum x este str (permis în Python, INTERZIS în C/Java)

# Reguli denumire:
#   - Poate conține litere, cifre, underscore
#   - NU poate începe cu cifră
#   - Case-sensitive (Nume ≠ nume)
#   - Convenție: snake_case (nu camelCase)
#   - Evită cuvintele rezervate (if, for, class, return, etc.)

numar_studenti = 30          # ✅ corect (snake_case)
_variabila_privata = 42      # ✅ corect (convenie: pseudo-privat)
2x = 10                      # ❌ SyntaxError (începe cu cifră)

Comparație C/Java — tipare statice:

int varsta = 21;         // C: tipul TREBUIE declarat, nu se schimbă
String nume = "Ana";     // Java: idem
varsta = "text";         // EROARE de compilare în C/Java

2.2 Tipuri de date fundamentale

# === Numerice ===
a = 42                  # int (întreg, precizie arbitrară în Python!)
b = 3.14                # float (virgulă mobilă, ~15 cifre semnificative)
c = 2 + 3j              # complex (parte reală + parte imaginară)

# Precizia arbitrară a int-urilor Python (vs. overflow în C):
big = 2 ** 1000          # Funcționează perfect! (în C, ar fi overflow)
print(len(str(big)))     # 302 cifre

# === Boolean ===
d = True                # bool (True sau False)
e = False
# Valori "falsy" în Python: False, 0, 0.0, "", [], {}, (), None, set()
# Totul altceva este "truthy"

# === String ===
f = "Hello"             # str (secvență de caractere, IMUTABIL)
g = 'World'             # ghilimele simple sau duble — echivalent
h = """Text
pe mai multe
linii"""                # triple quotes

# === NoneType ===
i = None                # Absența unei valori (echivalent null în Java, NULL în C)

# Verificare tip:
print(type(a))          # <class 'int'>
print(type(f))          # <class 'str'>
print(isinstance(a, int))  # True

2.3 Conversii de tip (casting)

# Conversii explicite:
x = int("42")           # str → int: 42
y = float("3.14")       # str → float: 3.14
z = str(42)             # int → str: "42"
w = int(3.99)           # float → int: 3 (TRUNCHIERE, nu rotunjire!)
r = round(3.99)         # Rotunjire: 4
b = bool(0)             # int → bool: False
b2 = bool("text")       # str → bool: True (string nevid = truthy)

# Erori de conversie:
# int("abc")            # ValueError: invalid literal
# int("")               # ValueError

2.4 Operatori

# === Aritmetici ===
print(7 + 3)      # 10  (adunare)
print(7 - 3)      # 4   (scădere)
print(7 * 3)      # 21  (înmulțire)
print(7 / 3)      # 2.3333...  (împărțire REALĂ — mereu returnează float!)
print(7 // 3)     # 2   (împărțire ÎNTREAGĂ — câtul)
print(7 % 3)      # 1   (modulo — restul)
print(7 ** 3)     # 343 (ridicare la putere)

# Atenție: / în Python 3 returnează MEREU float
# În C/Java: 7 / 3 == 2 (împărțire întreagă dacă ambii sunt int!)

# === Comparație ===
print(5 == 5)      # True  (egal)
print(5 != 3)      # True  (diferit)
print(5 > 3)       # True
print(5 < 3)       # False
print(5 >= 5)      # True
print(5 <= 3)      # False

# Comparații înlănțuite (unic Python!):
x = 5
print(1 < x < 10)   # True (echivalent cu: 1 < x and x < 10)
print(1 < x < 3)    # False

# === Logici ===
print(True and False)   # False
print(True or False)    # True
print(not True)         # False

# Short-circuit evaluation:
# 'and' returnează prima valoare falsy, sau ultima dacă toate sunt truthy
# 'or' returnează prima valoare truthy, sau ultima dacă toate sunt falsy
print(0 and 5)       # 0 (0 este falsy → se oprește)
print(3 and 5)       # 5 (3 este truthy → continuă, returnează ultimul)
print(0 or 5)        # 5 (0 falsy → continuă, 5 truthy → returnează)
print("" or "default")  # "default" (pattern util!)

# === Pe biți (Bitwise) ===
print(5 & 3)    # 1   (AND pe biți: 101 & 011 = 001)
print(5 | 3)    # 7   (OR pe biți:  101 | 011 = 111)
print(5 ^ 3)    # 6   (XOR pe biți: 101 ^ 011 = 110)
print(~5)       # -6  (NOT pe biți: complement față de 2)
print(5 << 1)   # 10  (shift stânga: 101 → 1010)
print(5 >> 1)   # 2   (shift dreapta: 101 → 10)

# === Atribuire compusă ===
x = 10
x += 5     # x = x + 5 → 15
x -= 3     # x = x - 3 → 12
x *= 2     # x = x * 2 → 24
x //= 5    # x = x // 5 → 4
x **= 3    # x = x ** 3 → 64
# Nota: Python NU are ++ sau -- (x++ nu există, folosim x += 1)

# === Identitate și apartenență ===
a = [1, 2, 3]
b = a
c = [1, 2, 3]
print(a is b)      # True  (ACELAȘI obiect în memorie)
print(a is c)      # False (obiecte diferite, chiar dacă valorile sunt egale)
print(a == c)      # True  (valori egale)

print(2 in a)      # True  (elementul 2 se află în lista a)
print(5 not in a)  # True

2.5 Citirea datelor de la utilizator

# input() citește MEREU un string
nume = input("Cum te cheamă? ")
varsta_str = input("Câți ani ai? ")
varsta = int(varsta_str)                  # Conversie explicită
# Sau pe scurt:
varsta = int(input("Câți ani ai? "))

# Citirea mai multor valori pe aceeași linie:
a, b = input("Introduceți a și b: ").split()   # split() împarte după spații
a, b = int(a), int(b)
# Sau cu map:
a, b = map(int, input("a b: ").split())

3. Structuri de control — decizii și bucle

3.1 Instrucțiunea if / elif / else

nota = int(input("Nota: "))

if nota >= 9:
    calificativ = "Excelent"
elif nota >= 7:
    calificativ = "Bine"
elif nota >= 5:
    calificativ = "Suficient"
else:
    calificativ = "Insuficient"

print(f"Calificativ: {calificativ}")

# Python folosește INDENTAREA (4 spații) pentru a delimita blocurile.
# Nu are acolade {} ca C/Java — indentarea este OBLIGATORIE și semnificativă.

Comparație C:

if (nota >= 9) {
    printf("Excelent");
} else if (nota >= 7) {
    printf("Bine");
} // ...acoladele sunt obligatorii pentru blocuri de mai multe linii
# Operatorul ternar (conditional expression):
status = "major" if varsta >= 18 else "minor"
# Echivalent C/Java: status = (varsta >= 18) ? "major" : "minor";

# Match/case (Python 3.10+) — echivalentul switch:
command = input("Comandă: ")
match command:
    case "start":
        print("Pornire...")
    case "stop":
        print("Oprire...")
    case "status":
        print("Stare: activ")
    case _:
        print("Comandă necunoscută")

3.2 Bucla while

# while — repetă CÂT TIMP condiția este adevărată
count = 5
while count > 0:
    print(f"Numărătoare inversă: {count}")
    count -= 1
print("Lansare!")

# Citire repetată până la input valid:
while True:
    try:
        n = int(input("Introduceți un număr pozitiv: "))
        if n > 0:
            break           # Ieșire din buclă
    except ValueError:
        pass                # Ignoră eroarea, reia bucla
print(f"Ați introdus: {n}")

# while cu else (specific Python — se execută dacă bucla NU a fost întreruptă cu break):
n = int(input("n: "))
d = 2
while d * d <= n:
    if n % d == 0:
        print(f"{n} nu este prim (divide cu {d})")
        break
    d += 1
else:
    # Se execută DOAR dacă while s-a terminat normal (fără break)
    print(f"{n} este prim")

3.3 Bucla for

# for în Python iterează peste o secvență (nu este for clasic cu contor!)

# Iterare peste un range:
for i in range(5):         # 0, 1, 2, 3, 4
    print(i)

for i in range(2, 10):     # 2, 3, 4, ..., 9
    print(i)

for i in range(0, 20, 3):  # 0, 3, 6, 9, 12, 15, 18 (pas 3)
    print(i)

for i in range(10, 0, -1): # 10, 9, 8, ..., 1 (numărătoare inversă)
    print(i)

# Iterare peste o colecție:
fructe = ["măr", "pară", "cireașă"]
for fruct in fructe:
    print(fruct)

# Cu index (enumerate):
for index, fruct in enumerate(fructe):
    print(f"{index}: {fruct}")

# Iterare peste caractere:
for char in "Python":
    print(char, end=" ")   # P y t h o n

# for cu else:
for x in range(2, 20):
    if x % 7 == 0:
        print(f"Primul multiplu de 7: {x}")
        break
else:
    print("Nu s-a găsit multiplu de 7 în interval")

Comparație — for clasic C/Java:

// C/Java: for cu contor explicit
for (int i = 0; i < 5; i++) {
    printf("%d\n", i);
}
// Python: range(5) generează aceeași secvență, dar sintaxa e diferită

3.4 break, continue, pass

# break — ieșire imediată din bucla cea mai interioară
for i in range(100):
    if i == 5:
        break               # Oprește la i=5
    print(i)                 # Afișează: 0 1 2 3 4

# continue — sare la următoarea iterație
for i in range(10):
    if i % 2 == 0:
        continue             # Sare peste numerele pare
    print(i)                 # Afișează: 1 3 5 7 9

# pass — instrucțiune goală (placeholder)
for i in range(10):
    pass                     # Nu face nimic (util pentru structuri incomplete)

def functie_neimplementata():
    pass                     # Placeholder — fără pass, ar fi SyntaxError

3.5 Bucle imbricate

# Tabel de înmulțire:
for i in range(1, 11):
    for j in range(1, 11):
        print(f"{i*j:4d}", end="")
    print()   # Linie nouă după fiecare rând

# Triunghi de caractere:
n = 5
for i in range(1, n + 1):
    print("*" * i)
# *
# **
# ***
# ****
# *****

# Matrice — parcurgere:
matrice = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
]
for rand in matrice:
    for element in rand:
        print(f"{element:3d}", end="")
    print()

4. Funcții și modularizare

4.1 Definirea și apelarea funcțiilor

# Funcția = bloc de cod reutilizabil, cu un nume, care realizează o sarcină specifică

def salut(nume):
    """Afișează un mesaj de salut personalizat."""    # Docstring
    print(f"Salut, {nume}!")

salut("Ana")        # Apel: Salut, Ana!
salut("Mihai")      # Apel: Salut, Mihai!

# Funcție cu valoare returnată:
def aduna(a, b):
    """Returnează suma a două numere."""
    return a + b

rezultat = aduna(3, 5)      # rezultat = 8
print(aduna(10, 20))        # 30

# Funcție fără return explicit returnează None:
def afiseaza(mesaj):
    print(mesaj)

x = afiseaza("Test")
print(x)                     # None

4.2 Parametri și argumente

# Parametri cu valoare implicită:
def salut(nume, salutare="Bună"):
    print(f"{salutare}, {nume}!")

salut("Ana")                      # Bună, Ana!
salut("Ana", "Salutare")         # Salutare, Ana!

# Argumente poziționale și keyword:
def profil(nume, varsta, oras):
    print(f"{nume}, {varsta} ani, din {oras}")

profil("Ana", 21, "Cluj")                  # Pozițional
profil(oras="Cluj", nume="Ana", varsta=21) # Keyword (ordinea nu contează)
profil("Ana", oras="Cluj", varsta=21)      # Mix (poziționale PRIMUL)

# *args — număr variabil de argumente poziționale (tuplu):
def suma(*numere):
    total = 0
    for n in numere:
        total += n
    return total

print(suma(1, 2, 3))       # 6
print(suma(10, 20, 30, 40)) # 100

# **kwargs — număr variabil de argumente keyword (dicționar):
def config(**optiuni):
    for cheie, valoare in optiuni.items():
        print(f"{cheie} = {valoare}")

config(host="localhost", port=8080, debug=True)

# Combinare completă (ordinea contează!):
def f(a, b, *args, cheie="default", **kwargs):
    print(f"a={a}, b={b}, args={args}, cheie={cheie}, kwargs={kwargs}")

f(1, 2, 3, 4, cheie="x", extra=True)
# a=1, b=2, args=(3, 4), cheie=x, kwargs={'extra': True}

4.3 Scope (domeniul de vizibilitate)

# Python are regula LEGB: Local → Enclosing → Global → Built-in

x = "global"               # Variabilă globală

def outer():
    x = "enclosing"         # Variabilă în funcția exterioară

    def inner():
        x = "local"         # Variabilă locală
        print(x)            # "local"

    inner()
    print(x)                # "enclosing"

outer()
print(x)                    # "global"

# Modificarea variabilei globale din funcție necesită 'global':
counter = 0
def incrementeaza():
    global counter
    counter += 1

incrementeaza()
print(counter)              # 1

# Modificarea variabilei enclosing necesită 'nonlocal':
def make_counter():
    count = 0
    def increment():
        nonlocal count
        count += 1
        return count
    return increment

counter = make_counter()
print(counter())            # 1
print(counter())            # 2
print(counter())            # 3

4.4 Funcții ca obiecte de primă clasă

# Funcțiile pot fi atribuite variabilelor, pasate ca argumente, returnate din alte funcții

def aplica(functie, valoare):
    return functie(valoare)

def dublu(x):
    return x * 2

def patrat(x):
    return x ** 2

print(aplica(dublu, 5))     # 10
print(aplica(patrat, 5))    # 25

# Funcții lambda (anonime, pe o singură expresie):
triplu = lambda x: x * 3
print(triplu(5))             # 15

# Lambda-urile sunt utile ca argumente:
numere = [3, 1, 4, 1, 5, 9]
print(sorted(numere))                        # [1, 1, 3, 4, 5, 9]
print(sorted(numere, reverse=True))          # [9, 5, 4, 3, 1, 1]

studenti = [("Ana", 9.5), ("Ion", 8.0), ("Maria", 9.8)]
print(sorted(studenti, key=lambda s: s[1]))  # Sortare după notă
print(sorted(studenti, key=lambda s: s[1], reverse=True))  # Descrescător

5. Șiruri de caractere (Strings)

5.1 Operații fundamentale

s = "Hello, World!"

# Indexare (0-based):
print(s[0])         # 'H' (primul caracter)
print(s[-1])        # '!' (ultimul caracter)

# Slicing [start:stop:step]:
print(s[0:5])       # 'Hello'
print(s[7:])        # 'World!'
print(s[:5])        # 'Hello'
print(s[::2])       # 'Hlo ol!' (din 2 în 2)
print(s[::-1])      # '!dlroW ,olleH' (inversare)

# Lungime:
print(len(s))       # 13

# Concatenare și repetare:
print("Hello" + " " + "World")   # Hello World
print("Ha" * 3)                   # HaHaHa

# Stringurile sunt IMUTABILE:
# s[0] = 'h'  # TypeError: 'str' object does not support item assignment
s = 'h' + s[1:]  # Creăm un string NOU: 'hello, World!'

5.2 Metode importante

s = "  Hello, World!  "

# Transformări:
print(s.strip())        # 'Hello, World!' (elimină spații la capete)
print(s.lower())        # '  hello, world!  '
print(s.upper())        # '  HELLO, WORLD!  '
print(s.title())        # '  Hello, World!  '
print(s.replace("World", "Python"))  # '  Hello, Python!  '

# Căutare:
print(s.find("World"))    # 9 (indexul primei apariții, -1 dacă nu e găsit)
print(s.count("l"))       # 3
print("World" in s)       # True (verificare apartenență)
print(s.startswith("  H"))# True
print(s.endswith("!  "))  # True

# Splitare și join:
csv_line = "Ana,21,Cluj,Student"
campuri = csv_line.split(",")      # ['Ana', '21', 'Cluj', 'Student']
print(" | ".join(campuri))          # 'Ana | 21 | Cluj | Student'

text = "Linia 1\nLinia 2\nLinia 3"
linii = text.splitlines()           # ['Linia 1', 'Linia 2', 'Linia 3']

# Verificări:
print("123".isdigit())     # True
print("abc".isalpha())     # True
print("abc123".isalnum())  # True
print("   ".isspace())     # True

5.3 Formatare stringuri

nume = "Ana"
varsta = 21
medie = 9.567

# F-strings (Python 3.6+ — RECOMANDAT):
print(f"Numele: {nume}, Vârsta: {varsta}")
print(f"Media: {medie:.2f}")         # 9.57 (2 zecimale)
print(f"Număr: {42:08d}")            # 00000042 (padding cu zerouri)
print(f"{'stânga':<20}")             # Aliniere stânga
print(f"{'dreapta':>20}")            # Aliniere dreapta
print(f"{'centrat':^20}")            # Aliniere centru
print(f"{1000000:,}")                # 1,000,000 (separator mii)
print(f"Debug: {varsta = }")         # Debug: varsta = 21 (Python 3.8+)

# Metoda .format():
print("Numele: {}, Vârsta: {}".format(nume, varsta))
print("Media: {:.2f}".format(medie))

# Operator % (stil C, legacy):
print("Numele: %s, Vârsta: %d, Media: %.2f" % (nume, varsta, medie))

6. Structuri de date liniare — Liste și Tablouri

6.1 Lista — structura de date fundamentală

# Lista = secvență ordonată, mutabilă, eterogenă
numere = [10, 20, 30, 40, 50]
mixt = [1, "doi", 3.0, True, None, [5, 6]]    # Tipuri diferite
gol = []

# Acces:
print(numere[0])       # 10
print(numere[-1])      # 50
print(numere[1:4])     # [20, 30, 40]

# Modificare (listele SUNT mutabile):
numere[0] = 99
print(numere)          # [99, 20, 30, 40, 50]
numere[1:3] = [200, 300]  # Înlocuire slice

6.2 Operații pe liste

# Adăugare:
lista = [1, 2, 3]
lista.append(4)          # [1, 2, 3, 4] — adaugă la final
lista.insert(0, 0)       # [0, 1, 2, 3, 4] — inserează la index
lista.extend([5, 6])     # [0, 1, 2, 3, 4, 5, 6] — adaugă mai multe

# Ștergere:
lista.remove(3)          # Șterge PRIMA apariție a valorii 3
del lista[0]             # Șterge elementul de la indexul 0
ultim = lista.pop()      # Scoate și returnează ultimul element
al_doilea = lista.pop(1) # Scoate și returnează elementul de la index 1
lista.clear()            # Golește lista

# Căutare:
numere = [3, 1, 4, 1, 5, 9, 2, 6]
print(numere.index(4))     # 2 (indexul primei apariții)
print(numere.count(1))     # 2 (de câte ori apare)
print(4 in numere)         # True
print(7 not in numere)     # True

# Sortare:
numere.sort()              # Sortare in-place (modifică lista originală)
numere.sort(reverse=True)  # Sortare descrescătoare
sortata = sorted(numere)   # Returnează o COPIE sortată (originalul intact)

# Inversare:
numere.reverse()           # In-place
inversata = list(reversed(numere))  # Copie inversată

# Funcții utile:
print(len(numere))     # Lungimea
print(min(numere))     # Minimul
print(max(numere))     # Maximul
print(sum(numere))     # Suma

6.3 List comprehensions

# Comprehension = mod concis de a crea liste noi din secvențe existente

# Patratele numerelor de la 0 la 9:
patrate = [x**2 for x in range(10)]
# [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

# Cu filtrare (if):
pare = [x for x in range(20) if x % 2 == 0]
# [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

# Cu transformare condiționată (if-else):
etichete = ["par" if x % 2 == 0 else "impar" for x in range(5)]
# ['par', 'impar', 'par', 'impar', 'par']

# Comprehension imbricat (matrice → listă plată):
matrice = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flat = [elem for rand in matrice for elem in rand]
# [1, 2, 3, 4, 5, 6, 7, 8, 9]

# Creare matrice identitate 3×3:
identitate = [[1 if i == j else 0 for j in range(3)] for i in range(3)]
# [[1, 0, 0], [0, 1, 0], [0, 0, 1]]

6.4 Copiere liste — shallow vs. deep

import copy

original = [1, [2, 3], 4]

# Referință (NU copie — ambele variabile pointează la ACEEAȘI listă):
ref = original
ref[0] = 99
print(original)           # [99, [2, 3], 4] — originalul MODIFICAT!

# Shallow copy (copiază lista, dar NU sub-listele):
shallow = original.copy()     # sau: list(original) sau original[:]
shallow[0] = 1                # Nu afectează originalul
shallow[1][0] = 999           # AFECTEAZĂ originalul! (sub-lista e partajată)
print(original)               # [99, [999, 3], 4]

# Deep copy (copie completă, recursivă):
deep = copy.deepcopy(original)
deep[1][0] = 0                # Nu afectează originalul
print(original[1][0])         # 999 (neschimbat de deep copy)

7. Tupluri, Mulțimi și Dicționare

7.1 Tupluri (Tuples)

# Tuplu = secvență ordonată, IMUTABILĂ
coordonate = (3, 4)
culori = ("rosu", "verde", "albastru")
singleton = (42,)            # Virgula e necesară pentru tuplu cu 1 element!

# Acces (ca la liste):
print(culori[0])             # "rosu"
print(culori[-1])            # "albastru"

# Imutabilitate:
# culori[0] = "galben"       # TypeError!

# Unpacking (destructurare):
x, y = coordonate            # x=3, y=4
a, *rest, z = (1, 2, 3, 4, 5)  # a=1, rest=[2, 3, 4], z=5

# Swap elegant:
a, b = 10, 20
a, b = b, a                  # Acum a=20, b=10

# Folosire ca chei de dicționar (listele NU pot fi chei):
distante = {
    ("București", "Cluj"): 450,
    ("București", "Iași"): 400,
}

7.2 Mulțimi (Sets)

# Mulțime = colecție NEORDONATĂ de elemente UNICE
fructe = {"măr", "pară", "cireașă", "măr"}   # Duplicatele se elimină automat
print(fructe)              # {'măr', 'pară', 'cireașă'}

# Creare din alte structuri (eliminare duplicate):
numere = [1, 3, 2, 3, 1, 4, 2, 5]
unice = set(numere)        # {1, 2, 3, 4, 5}

# Operații mulțimi:
a = {1, 2, 3, 4}
b = {3, 4, 5, 6}
print(a | b)         # {1, 2, 3, 4, 5, 6}   Reuniune
print(a & b)         # {3, 4}                 Intersecție
print(a - b)         # {1, 2}                 Diferență
print(a ^ b)         # {1, 2, 5, 6}           Diferență simetrică

# Verificări:
print(3 in a)        # True (verificare O(1) — foarte rapid!)
print({1, 2} <= a)   # True (submulțime)

# frozenset = mulțime imutabilă (poate fi cheie de dict sau element de set):
fs = frozenset([1, 2, 3])

7.3 Dicționare (Dictionaries)

# Dicționar = colecție de perechi cheie-valoare (hashmap)
student = {
    "nume": "Ana",
    "varsta": 21,
    "note": [9, 10, 8],
    "activ": True
}

# Acces:
print(student["nume"])            # "Ana"
print(student.get("email", "N/A"))  # "N/A" (valoare implicită dacă cheia lipsește)

# Modificare / Adăugare:
student["email"] = "ana@example.com"     # Adaugă cheie nouă
student["varsta"] = 22                    # Modifică valoare existentă

# Ștergere:
del student["activ"]
email = student.pop("email", None)        # Scoate și returnează, None dacă lipsește

# Iterare:
for cheie in student:                     # Iterează peste chei
    print(cheie, student[cheie])

for cheie, valoare in student.items():    # Iterează peste perechi
    print(f"{cheie}: {valoare}")

for valoare in student.values():          # Doar valorile
    print(valoare)

# Dict comprehension:
patrate = {x: x**2 for x in range(10)}
# {0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36, 7: 49, 8: 64, 9: 81}

# Numărare frecvențe (pattern clasic):
text = "abracadabra"
frecvente = {}
for char in text:
    frecvente[char] = frecvente.get(char, 0) + 1
print(frecvente)   # {'a': 5, 'b': 2, 'r': 2, 'c': 1, 'd': 1}

# Sau cu Counter:
from collections import Counter
print(Counter(text))   # Counter({'a': 5, 'b': 2, 'r': 2, 'c': 1, 'd': 1})

8. Lucrul cu fișiere

8.1 Citire și scriere

# Scriere într-un fișier:
with open("output.txt", "w", encoding="utf-8") as f:
    f.write("Linia 1\n")
    f.write("Linia 2\n")
    f.write(f"Rezultat: {42}\n")

# 'with' asigură ÎNCHIDEREA automată a fișierului, chiar dacă apare o eroare.
# Fără 'with', ar trebui: f = open(...) ... f.close()

# Citire completă:
with open("output.txt", "r", encoding="utf-8") as f:
    continut = f.read()         # Tot fișierul ca un singur string
    print(continut)

# Citire linie cu linie:
with open("output.txt", "r") as f:
    for linie in f:              # Eficient (nu încarcă tot fișierul în memorie)
        print(linie.strip())     # strip() elimină '\n' de la final

# Citire toate liniile într-o listă:
with open("output.txt") as f:
    linii = f.readlines()        # ['Linia 1\n', 'Linia 2\n', ...]

# Append (adăugare la final fără a șterge conținutul existent):
with open("output.txt", "a") as f:
    f.write("Linia adăugată\n")

# Moduri de deschidere:
# "r"  — citire (implicit, eroare dacă fișierul nu există)
# "w"  — scriere (creează sau SUPRASCRIE)
# "a"  — append (adaugă la final)
# "x"  — creare exclusivă (eroare dacă fișierul există deja)
# "rb" — citire binară
# "wb" — scriere binară

8.2 Lucrul cu CSV

import csv

# Scriere CSV:
studenti = [
    {"nume": "Ana", "nota": 9.5},
    {"nume": "Ion", "nota": 8.0},
    {"nume": "Maria", "nota": 9.8},
]

with open("studenti.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.DictWriter(f, fieldnames=["nume", "nota"])
    writer.writeheader()
    writer.writerows(studenti)

# Citire CSV:
with open("studenti.csv", "r", encoding="utf-8") as f:
    reader = csv.DictReader(f)
    for row in reader:
        print(f"{row['nume']}: {row['nota']}")

# Lucrul cu JSON:
import json

data = {"nume": "Ana", "note": [9, 10, 8], "activ": True}

# Scriere JSON:
with open("data.json", "w") as f:
    json.dump(data, f, indent=2, ensure_ascii=False)

# Citire JSON:
with open("data.json", "r") as f:
    data = json.load(f)
print(data["nume"])    # "Ana"

9. Tratarea excepțiilor și robustețea programelor

# Excepțiile sunt erori care apar la RUNTIME

# try / except / else / finally:
try:
    numar = int(input("Număr: "))
    rezultat = 100 / numar
except ValueError:
    print("Input invalid — trebuie un număr întreg")
except ZeroDivisionError:
    print("Nu se poate împărți la zero")
except Exception as e:
    print(f"Eroare neașteptată: {e}")
else:
    # Se execută DOAR dacă NU a apărut nicio excepție
    print(f"Rezultat: {rezultat}")
finally:
    # Se execută MEREU (indiferent dacă a fost excepție sau nu)
    print("Operație terminată")

# Ridicarea excepțiilor:
def factorial(n):
    if not isinstance(n, int):
        raise TypeError(f"Expected int, got {type(n).__name__}")
    if n < 0:
        raise ValueError(f"Factorial nu e definit pentru numere negative: {n}")
    if n <= 1:
        return 1
    return n * factorial(n - 1)

# Excepții custom:
class NotaInvalidaError(Exception):
    def __init__(self, nota, message="Nota trebuie să fie între 1 și 10"):
        self.nota = nota
        self.message = message
        super().__init__(self.message)

def seteaza_nota(nota):
    if not 1 <= nota <= 10:
        raise NotaInvalidaError(nota)
    return nota

Ierarhia excepțiilor Python (parțial):

BaseException
├── SystemExit
├── KeyboardInterrupt
└── Exception
    ├── ValueError
    ├── TypeError
    ├── KeyError
    ├── IndexError
    ├── FileNotFoundError
    ├── ZeroDivisionError
    ├── AttributeError
    ├── ImportError
    ├── RuntimeError
    ├── StopIteration
    └── OSError
        ├── FileExistsError
        ├── PermissionError
        └── ConnectionError

PARTEA II — INTERMEDIAR


10. Recursivitate

10.1 Conceptul

O funcție recursivă se apelează pe sine însăși. Fiecare apel rezolvă o subproblemă mai mică, până la un caz de bază care se rezolvă direct.

def factorial(n):
    """n! = n × (n-1) × ... × 1, cu 0! = 1"""
    if n <= 1:           # Caz de bază
        return 1
    return n * factorial(n - 1)   # Pas recursiv

# Execuție:
# factorial(4) = 4 * factorial(3)
#              = 4 * 3 * factorial(2)
#              = 4 * 3 * 2 * factorial(1)
#              = 4 * 3 * 2 * 1
#              = 24

def fibonacci(n):
    """Al n-lea termen Fibonacci (0, 1, 1, 2, 3, 5, 8, ...)."""
    if n <= 0: return 0
    if n == 1: return 1
    return fibonacci(n - 1) + fibonacci(n - 2)

# ATENȚIE: fibonacci recursiv naiv are complexitate O(2^n)!
# Soluție: memoizare (cache):
from functools import lru_cache

@lru_cache(maxsize=None)
def fibonacci_fast(n):
    if n <= 0: return 0
    if n == 1: return 1
    return fibonacci_fast(n - 1) + fibonacci_fast(n - 2)

print(fibonacci_fast(100))  # Instant! (fără cache: imposibil de calculat)

10.2 Exemple clasice

# Turnurile Hanoi:
def hanoi(n, sursa, dest, aux):
    if n == 1:
        print(f"Mută disc 1 de pe {sursa} pe {dest}")
        return
    hanoi(n - 1, sursa, aux, dest)
    print(f"Mută disc {n} de pe {sursa} pe {dest}")
    hanoi(n - 1, aux, dest, sursa)

hanoi(3, "A", "C", "B")

# Căutare binară recursivă:
def cautare_binara(lista, target, low=0, high=None):
    if high is None:
        high = len(lista) - 1
    if low > high:
        return -1                 # Negăsit

    mid = (low + high) // 2
    if lista[mid] == target:
        return mid
    elif lista[mid] < target:
        return cautare_binara(lista, target, mid + 1, high)
    else:
        return cautare_binara(lista, target, low, mid - 1)

# Parcurgere directoare (recursiv):
import os
def lista_fisiere(cale, indent=0):
    for nume in sorted(os.listdir(cale)):
        cale_completa = os.path.join(cale, nume)
        print("  " * indent + nume)
        if os.path.isdir(cale_completa):
            lista_fisiere(cale_completa, indent + 1)

11. Algoritmi de sortare

# === Bubble Sort — O(n²) ===
def bubble_sort(lst):
    n = len(lst)
    for i in range(n - 1):
        swapped = False
        for j in range(n - 1 - i):
            if lst[j] > lst[j + 1]:
                lst[j], lst[j + 1] = lst[j + 1], lst[j]
                swapped = True
        if not swapped:
            break              # Optimizare: oprire dacă e deja sortat
    return lst

# === Selection Sort — O(n²) ===
def selection_sort(lst):
    n = len(lst)
    for i in range(n):
        min_idx = i
        for j in range(i + 1, n):
            if lst[j] < lst[min_idx]:
                min_idx = j
        lst[i], lst[min_idx] = lst[min_idx], lst[i]
    return lst

# === Insertion Sort — O(n²), bun pentru liste aproape sortate ===
def insertion_sort(lst):
    for i in range(1, len(lst)):
        key = lst[i]
        j = i - 1
        while j >= 0 and lst[j] > key:
            lst[j + 1] = lst[j]
            j -= 1
        lst[j + 1] = key
    return lst

# === Merge Sort — O(n log n), stabil ===
def merge_sort(lst):
    if len(lst) <= 1:
        return lst

    mid = len(lst) // 2
    left = merge_sort(lst[:mid])
    right = merge_sort(lst[mid:])

    return merge(left, right)

def merge(left, right):
    result = []
    i = j = 0
    while i < len(left) and j < len(right):
        if left[i] <= right[j]:
            result.append(left[i])
            i += 1
        else:
            result.append(right[j])
            j += 1
    result.extend(left[i:])
    result.extend(right[j:])
    return result

# === Quick Sort — O(n log n) mediu, O(n²) worst case ===
def quick_sort(lst):
    if len(lst) <= 1:
        return lst

    pivot = lst[len(lst) // 2]
    left = [x for x in lst if x < pivot]
    middle = [x for x in lst if x == pivot]
    right = [x for x in lst if x > pivot]

    return quick_sort(left) + middle + quick_sort(right)

12. Algoritmi de căutare

# === Căutare liniară — O(n) ===
def cautare_liniara(lista, target):
    for i, element in enumerate(lista):
        if element == target:
            return i
    return -1

# === Căutare binară — O(log n) — necesită listă SORTATĂ ===
def cautare_binara(lista, target):
    low, high = 0, len(lista) - 1
    while low <= high:
        mid = (low + high) // 2
        if lista[mid] == target:
            return mid
        elif lista[mid] < target:
            low = mid + 1
        else:
            high = mid - 1
    return -1

# Cu modulul bisect:
import bisect
lista = [10, 20, 30, 40, 50]
pos = bisect.bisect_left(lista, 30)    # 2 (indexul unde se află/s-ar insera)
bisect.insort(lista, 35)                # Inserează menținând sortarea

13. Complexitate computațională (notația Big-O)

13.1 Clasele de complexitate

Timp                    Exemplu                        Notație
─────────────────────────────────────────────────────────────────
Constant                Acces element din array          O(1)
Logaritmic              Căutare binară                   O(log n)
Liniar                  Căutare liniară, parcurgere      O(n)
Linearitmic             Merge sort, sorted() Python      O(n log n)
Pătratic                Bubble sort, selecție             O(n²)
Cubic                   Înmulțire matrice naivă          O(n³)
Exponențial             Fibonacci recursiv naiv           O(2ⁿ)
Factorial               Permutări, TSP brute-force       O(n!)

13.2 Complexitatea operațiilor Python

Structură       Operație           Complexitate
──────────────────────────────────────────────
list            index [i]          O(1)
list            append             O(1) amortizat
list            insert(0, x)      O(n) (shift-ează toate elementele)
list            x in list          O(n)
list            sort()             O(n log n) — Timsort
list            len()              O(1)

dict            d[key]             O(1) mediu
dict            d[key] = val       O(1) mediu
dict            key in d           O(1) mediu
dict            del d[key]         O(1) mediu

set             x in s             O(1) mediu
set             add                O(1) mediu
set             reuniune           O(len(s1) + len(s2))

str             s[i]               O(1)
str             s1 + s2            O(len(s1) + len(s2))
str             s1 in s2           O(len(s1) * len(s2))

14. Structuri de date fundamentale — Stive, Cozi, Liste înlănțuite

14.1 Stiva (Stack) — LIFO

# Stiva cu lista Python:
stack = []
stack.append(1)      # push
stack.append(2)
stack.append(3)
print(stack.pop())   # pop → 3 (ultimul intrat, primul ieșit)
print(stack[-1])     # peek (vârf fără a scoate)

# Implementare cu clasă:
class Stack:
    def __init__(self):
        self._items = []

    def push(self, item):
        self._items.append(item)

    def pop(self):
        if self.is_empty():
            raise IndexError("Pop from empty stack")
        return self._items.pop()

    def peek(self):
        if self.is_empty():
            raise IndexError("Peek at empty stack")
        return self._items[-1]

    def is_empty(self):
        return len(self._items) == 0

    def __len__(self):
        return len(self._items)

# Aplicație: verificare paranteze echilibrate
def paranteze_valide(s):
    stack = []
    perechi = {')': '(', ']': '[', '}': '{'}

    for char in s:
        if char in "([{":
            stack.append(char)
        elif char in ")]}":
            if not stack or stack[-1] != perechi[char]:
                return False
            stack.pop()

    return len(stack) == 0

print(paranteze_valide("({[]})"))    # True
print(paranteze_valide("([)]"))      # False

14.2 Coada (Queue) — FIFO

from collections import deque

# deque este optimizat pentru operații la ambele capete: O(1)
queue = deque()
queue.append("A")        # enqueue (la coadă)
queue.append("B")
queue.append("C")
print(queue.popleft())   # dequeue → "A" (primul intrat, primul ieșit)

# Implementare cu clasă:
class Queue:
    def __init__(self):
        self._items = deque()

    def enqueue(self, item):
        self._items.append(item)

    def dequeue(self):
        if self.is_empty():
            raise IndexError("Dequeue from empty queue")
        return self._items.popleft()

    def peek(self):
        return self._items[0]

    def is_empty(self):
        return len(self._items) == 0

    def __len__(self):
        return len(self._items)

14.3 Lista înlănțuită (Linked List)

class Node:
    def __init__(self, data):
        self.data = data
        self.next = None         # Pointer la următorul nod

class LinkedList:
    def __init__(self):
        self.head = None

    def append(self, data):
        new_node = Node(data)
        if not self.head:
            self.head = new_node
            return
        current = self.head
        while current.next:
            current = current.next
        current.next = new_node

    def prepend(self, data):
        new_node = Node(data)
        new_node.next = self.head
        self.head = new_node

    def delete(self, data):
        if not self.head:
            return
        if self.head.data == data:
            self.head = self.head.next
            return
        current = self.head
        while current.next and current.next.data != data:
            current = current.next
        if current.next:
            current.next = current.next.next

    def __iter__(self):
        current = self.head
        while current:
            yield current.data
            current = current.next

    def __repr__(self):
        return " → ".join(str(item) for item in self) + " → None"


ll = LinkedList()
ll.append(1)
ll.append(2)
ll.append(3)
ll.prepend(0)
print(ll)                # 0 → 1 → 2 → 3 → None
ll.delete(2)
print(ll)                # 0 → 1 → 3 → None

Comparație liste Python vs. linked list vs. array C:

Operație         | Python list (array dinamic) | Linked List    | Array C (fix)
─────────────────┼────────────────────────────┼────────────────┼──────────────
Acces [i]        | O(1)                       | O(n)           | O(1)
Append la final  | O(1) amortizat             | O(n) sau O(1)* | N/A (fix)
Insert la început| O(n)                       | O(1)           | O(n)
Delete din mijloc| O(n)                       | O(1)*          | O(n)
Căutare          | O(n)                       | O(n)           | O(n)
Memorie          | Contiguă + overhead        | Fragmentată    | Contiguă, fixă

* dacă avem pointer la nodul anterior

15. Arbori și grafuri — noțiuni de bază

15.1 Arbore binar

class TreeNode:
    def __init__(self, value):
        self.value = value
        self.left = None
        self.right = None

class BinarySearchTree:
    def __init__(self):
        self.root = None

    def insert(self, value):
        if not self.root:
            self.root = TreeNode(value)
        else:
            self._insert_rec(self.root, value)

    def _insert_rec(self, node, value):
        if value < node.value:
            if node.left is None:
                node.left = TreeNode(value)
            else:
                self._insert_rec(node.left, value)
        else:
            if node.right is None:
                node.right = TreeNode(value)
            else:
                self._insert_rec(node.right, value)

    def search(self, value):
        return self._search_rec(self.root, value)

    def _search_rec(self, node, value):
        if node is None:
            return False
        if value == node.value:
            return True
        elif value < node.value:
            return self._search_rec(node.left, value)
        else:
            return self._search_rec(node.right, value)

    def inorder(self):
        """Parcurgere in-order: stânga → rădăcină → dreapta (sortată!)"""
        result = []
        self._inorder_rec(self.root, result)
        return result

    def _inorder_rec(self, node, result):
        if node:
            self._inorder_rec(node.left, result)
            result.append(node.value)
            self._inorder_rec(node.right, result)


bst = BinarySearchTree()
for val in [5, 3, 7, 1, 4, 6, 8]:
    bst.insert(val)

print(bst.inorder())        # [1, 3, 4, 5, 6, 7, 8]
print(bst.search(4))        # True
print(bst.search(9))        # False

15.2 Graf — reprezentare și parcurgere

# Reprezentare: dicționar de adiacență
graf = {
    "A": ["B", "C"],
    "B": ["A", "D", "E"],
    "C": ["A", "F"],
    "D": ["B"],
    "E": ["B", "F"],
    "F": ["C", "E"]
}

# BFS (Breadth-First Search) — parcurgere pe lățime (coadă):
from collections import deque

def bfs(graf, start):
    visited = set()
    queue = deque([start])
    visited.add(start)
    order = []

    while queue:
        node = queue.popleft()
        order.append(node)
        for vecin in graf[node]:
            if vecin not in visited:
                visited.add(vecin)
                queue.append(vecin)

    return order

print(bfs(graf, "A"))  # ['A', 'B', 'C', 'D', 'E', 'F']

# DFS (Depth-First Search) — parcurgere în adâncime (stivă/recursie):
def dfs(graf, start, visited=None):
    if visited is None:
        visited = set()
    visited.add(start)
    result = [start]
    for vecin in graf[start]:
        if vecin not in visited:
            result.extend(dfs(graf, vecin, visited))
    return result

print(dfs(graf, "A"))  # ['A', 'B', 'D', 'E', 'F', 'C']

16. Programare orientată pe obiecte — fundamente

class Student:
    """Modelează un student universitar."""

    # Atribut de clasă (partajat de toate instanțele):
    universitate = "Universitatea Tehnică"

    def __init__(self, nume, varsta, note=None):
        """Constructor — inițializează o instanță nouă."""
        self.nume = nume              # Atribut de instanță
        self.varsta = varsta
        self.note = note if note else []
        self._nr_matricol = None      # Convenție: _ = "protejat"

    def adauga_nota(self, nota):
        if not 1 <= nota <= 10:
            raise ValueError(f"Nota invalidă: {nota}")
        self.note.append(nota)

    @property
    def media(self):
        """Proprietate calculată (se accesează ca un atribut)."""
        if not self.note:
            return 0.0
        return sum(self.note) / len(self.note)

    @property
    def nr_matricol(self):
        return self._nr_matricol

    @nr_matricol.setter
    def nr_matricol(self, value):
        if self._nr_matricol is not None:
            raise AttributeError("Numărul matricol nu poate fi schimbat")
        self._nr_matricol = value

    def __repr__(self):
        return f"Student({self.nume!r}, {self.varsta}, note={self.note})"

    def __str__(self):
        return f"{self.nume} (media {self.media:.2f})"

    def __eq__(self, other):
        if not isinstance(other, Student):
            return NotImplemented
        return self.nume == other.nume and self.varsta == other.varsta

    def __lt__(self, other):
        return self.media < other.media

    @classmethod
    def din_string(cls, text):
        """Constructor alternativ (factory method)."""
        parti = text.split(",")
        return cls(parti[0].strip(), int(parti[1].strip()))

    @staticmethod
    def este_nota_valida(nota):
        """Nu depinde de instanță sau clasă."""
        return isinstance(nota, (int, float)) and 1 <= nota <= 10


# Utilizare:
s1 = Student("Ana", 21)
s1.adauga_nota(9)
s1.adauga_nota(10)
print(s1)                    # Ana (media 9.50)
print(repr(s1))              # Student('Ana', 21, note=[9, 10])

s2 = Student.din_string("Ion, 22")  # Constructor alternativ
print(Student.este_nota_valida(11))  # False

# Sortare studenti după medie:
studenti = [Student("A", 20, [8]), Student("B", 21, [10]), Student("C", 22, [7])]
print(sorted(studenti, reverse=True))  # Sortare descrescătoare după medie

PARTEA III — AVANSAT


17. OOP avansat — moștenire, polimorfism, compoziție

from abc import ABC, abstractmethod

# Clasă abstractă:
class Forma(ABC):
    @abstractmethod
    def arie(self) -> float: ...

    @abstractmethod
    def perimetru(self) -> float: ...

    def __str__(self):
        return f"{self.__class__.__name__}(arie={self.arie():.2f})"

# Moștenire:
class Cerc(Forma):
    def __init__(self, raza):
        self.raza = raza

    def arie(self):
        import math
        return math.pi * self.raza ** 2

    def perimetru(self):
        import math
        return 2 * math.pi * self.raza

class Dreptunghi(Forma):
    def __init__(self, latime, inaltime):
        self.latime = latime
        self.inaltime = inaltime

    def arie(self):
        return self.latime * self.inaltime

    def perimetru(self):
        return 2 * (self.latime + self.inaltime)

class Patrat(Dreptunghi):
    def __init__(self, latura):
        super().__init__(latura, latura)   # Apelează constructorul părinte

# Polimorfism — aceeași interfață, comportament diferit:
forme = [Cerc(5), Dreptunghi(3, 4), Patrat(6)]
for f in forme:
    print(f"{f} — perimetru: {f.perimetru():.2f}")
# Cerc(arie=78.54) — perimetru: 31.42
# Dreptunghi(arie=12.00) — perimetru: 14.00
# Patrat(arie=36.00) — perimetru: 24.00

# Arie totală — funcționează cu ORICE Forma:
arie_totala = sum(f.arie() for f in forme)

18. Programare funcțională în Python

# map — aplică o funcție fiecărui element:
numere = [1, 2, 3, 4, 5]
patrate = list(map(lambda x: x**2, numere))    # [1, 4, 9, 16, 25]

# filter — selectează elementele care satisfac o condiție:
pare = list(filter(lambda x: x % 2 == 0, numere))  # [2, 4]

# reduce — reduce o secvență la o singură valoare:
from functools import reduce
suma = reduce(lambda acc, x: acc + x, numere, 0)    # 15
produs = reduce(lambda acc, x: acc * x, numere, 1)  # 120

# Compunere funcții:
def compose(*funcs):
    """Compune mai multe funcții: compose(f, g, h)(x) = f(g(h(x)))"""
    return reduce(lambda f, g: lambda x: f(g(x)), funcs)

double = lambda x: x * 2
add_one = lambda x: x + 1
square = lambda x: x ** 2

transform = compose(square, add_one, double)
print(transform(3))    # square(add_one(double(3))) = square(7) = 49

# zip — combină secvențe element cu element:
nume = ["Ana", "Ion", "Maria"]
note = [9.5, 8.0, 9.8]
for n, nota in zip(nume, note):
    print(f"{n}: {nota}")

# any / all:
print(any(x > 0 for x in [-1, -2, 3]))    # True (cel puțin unul > 0)
print(all(x > 0 for x in [1, 2, 3]))      # True (TOATE > 0)

19. Generatoare, iteratoare și comprehensions

# Generator = funcție care produce valori LAZY (la cerere, una câte una)
# Folosește 'yield' în loc de 'return'

def numara_pana_la(n):
    i = 0
    while i < n:
        yield i        # „Produce" o valoare, apoi se „suspendă"
        i += 1

for x in numara_pana_la(5):
    print(x)                 # 0, 1, 2, 3, 4

# Avantaj: nu stochează TOATĂ secvența în memorie
# range(1_000_000_000) funcționează, dar list(range(1_000_000_000)) → MemoryError

# Generator Fibonacci infinit:
def fibonacci():
    a, b = 0, 1
    while True:              # Infinit! Se oprește doar când consumatorul se oprește
        yield a
        a, b = b, a + b

# Primele 10 numere Fibonacci:
from itertools import islice
print(list(islice(fibonacci(), 10)))  # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

# Generator expression (ca list comprehension, dar lazy):
patrate_gen = (x**2 for x in range(1000000))  # Nu alocă memorie!
print(sum(patrate_gen))                         # Suma pătratelor — O(1) memorie

# Dict comprehension:
inverse = {v: k for k, v in {"a": 1, "b": 2}.items()}  # {1: 'a', 2: 'b'}

# Set comprehension:
vocale = {c for c in "hello world" if c in "aeiou"}     # {'e', 'o'}

20. Expresii regulate (Regex)

import re

text = "Email-urile sunt ana@example.com și ion.popescu@gmail.com, sună la 0712345678."

# Căutare:
match = re.search(r'\d+', text)          # Prima secvență de cifre
print(match.group())                       # '0712345678'... nu, găsește prima!

# Toate potrivirile:
emailuri = re.findall(r'[\w.]+@[\w.]+\.\w+', text)
print(emailuri)   # ['ana@example.com', 'ion.popescu@gmail.com']

telefoane = re.findall(r'\b07\d{8}\b', text)
print(telefoane)  # ['0712345678']

# Înlocuire:
cenzurat = re.sub(r'[\w.]+@[\w.]+\.\w+', '[EMAIL ASCUNS]', text)
print(cenzurat)

# Validare:
def email_valid(email):
    pattern = r'^[\w.+-]+@[\w-]+\.[\w.]+$'
    return re.match(pattern, email) is not None

print(email_valid("test@example.com"))     # True
print(email_valid("invalid@"))             # False

# Caractere speciale regex:
# .     Orice caracter (except newline)
# \d    Cifră [0-9]
# \w    Word character [a-zA-Z0-9_]
# \s    Whitespace [ \t\n\r]
# ^     Început de string
# $     Sfârșit de string
# *     0 sau mai multe repetiții
# +     1 sau mai multe repetiții
# ?     0 sau 1 repetiție (opțional)
# {n,m} Între n și m repetiții
# []    Set de caractere
# ()    Grup de captură
# |     Sau (alternativă)
# \b    Word boundary

21. Module, pachete și ecosistemul Python

# === Importuri ===
import math                          # Import complet
from math import sqrt, pi           # Import selectiv
from datetime import datetime as dt  # Alias
import os.path                       # Submodul

# === Creare modul propriu ===
# utils.py:
def celsius_to_fahrenheit(c):
    return c * 9/5 + 32

PI = 3.14159

# main.py:
from utils import celsius_to_fahrenheit

# === Structura unui pachet ===
# mypackage/
# ├── __init__.py          (marchează directorul ca pachet)
# ├── core.py
# ├── helpers.py
# └── models/
#     ├── __init__.py
#     ├── user.py
#     └── product.py

# from mypackage.models.user import User

Module standard utile

import os                   # Operații sistem de fișiere
import sys                  # Parametri sistem, argv
import math                 # Funcții matematice
import random               # Numere aleatoare
import datetime             # Date și ore
import json                 # Serializare JSON
import csv                  # Fișiere CSV
import re                   # Expresii regulate
import collections          # Counter, defaultdict, deque, namedtuple
import itertools            # Combinări, permutări, lanțuri
import functools            # reduce, lru_cache, wraps
import pathlib              # Căi de fișiere (modern)
import typing               # Type hints
import dataclasses          # Dataclass decorator
import unittest             # Framework de testare
import argparse             # Parsare argumente linie de comandă
import logging              # Logging structurat
import copy                 # Copiere deep/shallow

# Biblioteci externe populare (instalate cu pip):
# requests     — HTTP client simplu
# numpy        — Calcul numeric, matrice
# pandas       — Analiza datelor, DataFrames
# matplotlib   — Grafice
# flask/django — Web frameworks
# sqlalchemy   — ORM pentru baze de date
# pytest       — Framework de testare

22. Introducere în testare și debugging

22.1 Unit testing cu pytest

# test_calculator.py

def aduna(a, b):
    return a + b

def imparte(a, b):
    if b == 0:
        raise ValueError("Nu se poate împărți la zero")
    return a / b

# === Teste ===
def test_aduna_numere_pozitive():
    assert aduna(2, 3) == 5

def test_aduna_numere_negative():
    assert aduna(-1, -2) == -3

def test_aduna_zero():
    assert aduna(0, 5) == 5

def test_imparte_normal():
    assert imparte(10, 2) == 5.0

def test_imparte_la_zero():
    import pytest
    with pytest.raises(ValueError, match="zero"):
        imparte(10, 0)

# Parametrizare:
import pytest

@pytest.mark.parametrize("a, b, expected", [
    (1, 1, 2),
    (0, 0, 0),
    (-1, 1, 0),
    (100, 200, 300),
])
def test_aduna_parametrizat(a, b, expected):
    assert aduna(a, b) == expected

# Rulare: pytest test_calculator.py -v

22.2 Debugging

# 1. Print debugging (simplu dar eficient):
def functie_complexa(data):
    print(f"DEBUG: data = {data}")    # Temporar!
    result = process(data)
    print(f"DEBUG: result = {result}")
    return result

# 2. Breakpoint (debugger interactiv):
def functie_cu_bug(lista):
    for i, item in enumerate(lista):
        breakpoint()               # Oprește execuția aici → pdb prompt
        # Comenzi pdb: n(ext), s(tep), c(ontinue), p(rint) expr, l(ist)
        process(item)

# 3. Logging (pentru producție — NU print):
import logging

logging.basicConfig(level=logging.DEBUG,
                    format='%(asctime)s %(levelname)s: %(message)s')
logger = logging.getLogger(__name__)

logger.debug("Valoare intermediară: %s", value)  # Doar în mod debug
logger.info("Procesare completă")
logger.warning("Resurse aproape epuizate")
logger.error("Operație eșuată: %s", error_msg)

23. Bune practici și stil de cod

23.1 PEP 8 — ghidul de stil Python

# Denumire:
variabila_locala = 42         # snake_case
CONSTANTA_GLOBALA = 3.14      # SCREAMING_SNAKE_CASE
class NumeClasa:               # PascalCase
    def metoda_publica(self):  # snake_case
        self._atribut_intern = 1  # _ prefix = convenție privat

# Spațiere:
x = 1                         # Spațiu în jurul = la atribuire
y = x + 2                     # Spațiu în jurul operatorilor
f(arg1, arg2)                 # Fără spațiu înaintea parantezei funcției
lista[0]                      # Fără spațiu înaintea indexării

# Lungime linie: max 79 caractere (sau 120 în proiecte moderne)
# Linii lungi se sparg cu \ sau paranteze:
rezultat = (prima_valoare
            + a_doua_valoare
            - a_treia_valoare)

# Importuri — ordinea:
# 1. Biblioteci standard
# 2. Biblioteci terțe
# 3. Module locale
import os
import sys

import requests
import numpy as np

from mypackage import utils

23.2 Principii de clean code

# 1. Funcții mici, cu un singur scop
# ❌ O funcție face totul:
def proceseaza_si_salveaza_si_trimite(data): ...
# ✅ Funcții separate:
def valideaza(data): ...
def proceseaza(data): ...
def salveaza(data): ...

# 2. Denumiri descriptive
# ❌ x, d, temp, data2, flag
# ✅ temperatura_celsius, distanta_km, este_valid, studenti_activi

# 3. Evită numerele magice
# ❌ if varsta > 18:
# ✅ VARSTA_MAJORAT = 18
#    if varsta > VARSTA_MAJORAT:

# 4. DRY (Don't Repeat Yourself)
# Dacă copiezi cod, extrage-l într-o funcție

# 5. Early return (evită nesting adânc)
# ❌
def proceseaza(data):
    if data is not None:
        if len(data) > 0:
            if valideaza(data):
                return rezultat
    return None

# ✅
def proceseaza(data):
    if data is None:
        return None
    if len(data) == 0:
        return None
    if not valideaza(data):
        return None
    return rezultat

# 6. Type hints (documentare + verificare):
def calculeaza_media(note: list[float]) -> float:
    """Calculează media aritmetică a listei de note."""
    if not note:
        return 0.0
    return sum(note) / len(note)

Anexe

A. Comparație Python vs. C vs. Java — rezumat

Aspect Python C Java
Tipare Dynamic, strong Static, weak Static, strong
Compilare Interpretat Compilat (mașină) Compilat (bytecode JVM)
Memorie Garbage collector Manual (malloc/free) Garbage collector
Sintaxă blocuri Indentare Acolade {} Acolade {}
OOP Opțional, flexibil Nu (struct, nu clase) Obligatoriu (totul=clasă)
Pointeri Nu Da (explicit) Nu (referințe implicite)
Moștenire multiplă Da N/A Nu (interfețe da)
Generice Type hints (opțional) Macro-uri / void* Generice (erasure)
Performanță Lentă (interpretat) Foarte rapidă Rapidă (JIT)
Ecosistem pip, PyPI Manual / package mgr Maven, Gradle
Ideal pentru Scripting, ML, web, edu Sisteme, embedded, OS Enterprise, Android

B. Glosar de termeni

Termen Definiție
Variabilă Nume asociat cu o valoare stocată în memorie
Tip de date Categoria unei valori (int, str, list etc.)
Funcție Bloc de cod reutilizabil cu nume, parametri și valoare returnată
Parametru Variabilă în definiția funcției
Argument Valoarea concretă pasată la apelul funcției
Iterare Parcurgerea element cu element a unei secvențe
Recursivitate Funcție care se apelează pe ea însăși
Excepție Eroare detectată la runtime
Clasă Șablon (blueprint) pentru crearea obiectelor
Obiect Instanță a unei clase
Moștenire Mecanism prin care o clasă preia atribute/metode de la alta
Polimorfism Obiectele diferite răspund la aceeași interfață în mod propriu
Încapsulare Ascunderea detaliilor interne, expunere prin interfață
Complexitate Măsura resurselor (timp/spațiu) necesare unui algoritm
Mutabil Obiect a cărui valoare poate fi schimbată după creare
Imutabil Obiect a cărui valoare NU poate fi schimbată (str, tuple)
Generator Funcție care produce valori lazy (la cerere) cu yield
Decorator Funcție care modifică comportamentul altei funcții
Comprehension Sintaxă concisă pentru crearea colecțiilor din iterabile

Curs realizat ca material de referință pentru studenții anului I de informatică, automatică și calculatoare.

Pe această pagină