Skip to content

Elemente avansate ale limbajului Python

Autor: Rareș Stanciu, Nitro AI Workshops 2024

Rezolvă în Colab: Elemente avansate ale limbajului Python

Scopul acestui notebook este de a introduce conceptele mai avansate necesare pentru faza judeteana a Olimpiadei de Inteligenta Artificiala.

Sunt necesare cunostinte minime ale limbajului de programare Pyhton. Pentru cei ce isi doresc sa invete de la zero sau isi doresc o recapitulare, recomand acest videoclip publicat pe canalul Olimpiadei de IA: . Puteti lua acest acest notebook ca o parte secunda a lectiei Python.

Videoclipul suport pentru parcurgerea acestui notebook este, de asemenea, disponibil pe canalul de youtube al Olimpiadei de IA: .

1. Recapitulare

1.1 Tipuri de date:

Acesta este un comentariu, nu este bagat in seama de calculator, este doar pentru noi :)

a, b = 12, 5 # ATENTIE! Numarele nu sunt neaparat intregi
suma = a + b # 17
diferenta = a - b # 7
produs = a * b # 60
impartire_cu_virgula = a / b # 2.4
impartire_fara_virgula = a // b # 2

Functioneaza exact ca o lista, unde fruct[i] acceseaza a (i + 1)-a litera

fruct = "mar" # sir de caractere sau "string"
print(fruct[0], fruct[2])
m r

ATENTIE! In python listele nu trebuie sa contina acelasi timp de date

lista = [5, 4.7, "ce caut aici?", 12]
print(lista[1], lista[2])
4.7 ce caut aici?

Un dictionar, pentru care valorile din stanga functioneaza drept chei, iar cele din dreapta drept valori

medii = {
    'Rares': 9,
    'Mircea': 9.5,
    'Ratusca NLP': 10
}
print(medii['Rares'], medii['Ratusca NLP'])
9 10

1.2 Structuri decizionale si structuri repetitive

Ce credeti ca va afisa codul din secvențele de mai jos?

for i in range(10):
  if i % 2 == 0:
    print(f'{i} este par')
  else:
    print(f'{i} este impar')
for i in range(10, 0, -1):
  print(i)
n = 6
suma = 0
while (n > 0):
  suma += n
  if (suma % 3 == 0):
    print('Am gaist o suma M3!')

  n -= 1

Forul in python este detul de puternic, avand cateva functii interesante precum:

nume = ['Laura', 'Luca', 'Rares', 'Mircea', 'Victor', 'Alex', 'Nicolas', 'Mihai']

for prenume in nume: # in python putem itera direct si prin obiecte
  print(prenume)

varste = [17, 17, 18, 17, 18, 18, 19, 18]
for (prenume, varsta) in zip(nume, varste): # listele nume si varsta au aceeasi marime asa ca am putut sa le lipim prin zip
  print(f'{prenume} are {varsta} ani') # si sa iteram prin amandoua in acelasi timp

1.3 Functii

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

def afiseaza_divizori(a):
  for i in range(1, a + 1):
    if a % i == 0:
      print(i)

afiseaza_divizori(suma(suma(1, 6), 3))

1.4 Exercitii

  1. Completati functia pentru a returna o lista cu primii n multiplii nenuli ai lui k
def afla_multiplii(n, k):
  return
  1. Completati functia pentru a returna un dictionar unde cheile sunt de la 1-n, iar valorile sunt patratul lor
def patrate(n):
  return

2. List & Dictionary comprehension

Aceasta tehnica reprezinta construirea concisa a unui sir sau a unui dictionar, de obicei chiar fiind posibila intr-un singur rand. Cunostinta acesteia este esentiala pentru intelegerea oricarui cod in python, inscriindu-se paradigmei pitonice de a scrie cod, deosebindu-se de C++ prin stilul concis, dar mai greu de inteles initial.

A fost destul de multa munca sa faci functiile precedente, nu?

Asa credeam si eu, ei bine, exista un mod mai usor

n, k = 11, 3
multiplii = [i * k for i in range(1, n + 1)]
print(multiplii)
[3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33]

Sa incercam sa pastram doar multiplii pari din aceasta lista

multiplii_pari = [i * k for i in range(1, n + 1) if (i * k) % 2 == 0]
print(multiplii_pari)
[6, 12, 18, 24, 30]

Acum hai sa incercam sa generam o lista de lungime n, unde pe pozitie para se afla cuvantul 'DA', iar pe pozitii impare cuvantul 'NU'

da_nu = ['DA' if i % 2 == 0 else 'NU' for i in range(n)]
print(da_nu)
['DA', 'NU', 'DA', 'NU', 'DA', 'NU', 'DA', 'NU', 'DA', 'NU', 'DA']

in final, sa rezolvam si cu dictionarul

dict_patrate = {x: x**2 for x in range(1, n + 1)}
print(dict_patrate)
{1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36, 7: 49, 8: 64, 9: 81, 10: 100, 11: 121}

Si ceva putin mai complex, putem itera si prin obiecte direct

metrici = ['acuratete', 'precizie', 'recall', 'scor-f1']
dict_metrici = {metric: len(metric) for metric in metrici} # un dictionar ce contine drept chei metricile, iar drept valori lungimile lor
print(dict_metrici)
{'acuratete': 9, 'precizie': 8, 'recall': 6, 'scor-f1': 7}

2.1 Exercitii

  1. Completati codul ca fiind data o lista cu numere, sa se pastreze doar cele de pe pozitie para
numere = [1, 10, 3, 2, 7, 9]
lista = []
print(lista)
  1. Creati un dictionar pentru care cheile lui sa fie numerele de la 1-10, iar valorile patratul numarului, daca este par sau cubul numarului, daca este impar
patrat_sau_cub = {}
print(patrat_sau_cub)
  1. Dandu-se o lista cu numele unor elevi si o lista cu notele lor, completati codul pentru a forma un dictionar in care cheia sa fie numele, iar valoarea sa fie nota lor. Listele au aceeasi lungime, iar daca un copil se afla pe pozitia i, atunci nota sa se afla tot pe pozitia i
nume_copii = ['Teia', 'Victor', 'Andreas', 'Adriana', 'Tamara', 'Melvin']
note_copii = [10, 8.33, 8.5, 9.5, 9.5, 9.85]
  1. Pentru a antrena un model este nevoie de doua dictionare id2label si label2id. Creati aceste doua dictionare, stiind ca label2id are forma {label: pozitia in sir}, iar id2label are forma {pozitia in sir: label}
labels = ['cat', 'dog', 'car', 'duck', 'airplane', 'train', 'teddy bear']
label2id = {}
id2label = {}

3. Functii

3.1 Functii recursive

Functiile recursive sunt functii care se apeleaza singure in mod "recursiv" pentru a rezolva diverse probleme intr-un mod foarte elegant.

Evident, python sustine acest tip de functii si impreuna vom face cateva exemple, care va vor fi familiare.

Toti suntem familiari cu numarul lui fibonacci, care este definit dupa recurenta fib(n) = fib(n - 1) + fib(n - 2), iar fib(0) = 0, fib(1) = 1

Scopul este sa putem face o functie recursiva care sa calculeze al n-lea termen fibonacci

Varianta iterativa:

def fib_iterativ(n):
  if (n == 0): # caz de baza
    return 0

  if (n == 1): # caz de baza
    return 1

  fib_0, fib_1 = 0, 1
  for i in range(2, n + 1):
    fib_nou = fib_0 + fib_1

    fib_0 = fib_1
    fib_1 = fib_nou

  return fib_1

Varianta recursiva

def fib_recursiv(n): # OBSERVATIE! Important e faptul ca se pastreaza cazurile de baza
  if (n == 0):
    return 0

  if (n == 1):
    return 1

  return fib_recursiv(n - 1) + fib_recursiv(n - 2)

3.2 Functii lambda (functii anonime)

Functiile lambda sunt extrem de utile atunci cand functia reprezinta o expresie simpla. Acestea sunt foarte asemanatoare celor normale, insa lor le lipseste un nume, de aici li se mai spune si functii anonime.

Un exemplu simplu de transformat intr-o functie lambda este functia f(x) = 2 * x

def f(x):
  return 2 * x
print(f(3))
6

Cum am face asta cu lambda?

lambda x: 2 * x
<function __main__.<lambda>(x)>

Acum ca am creat functia, cum o apelam? Ei bine, trebuie sa folosim apelarea imediata

print((lambda x: 2 * x)(3))
6

SAU ii putem da un nume functiei lambda, moment in care devine echivalenta cu una obisnuita

dublu = lambda x: 2 * x
print(dublu(3))
6

Totodata, functiile lambda pot primi mai multe argumente, spre ex:

rezultat = (lambda x, y, z: x + y + z)(2, 3, 4)
print(rezultat)
9

Multi acum probabil va intrebati de ce trebuie sa stiti despre aceste functii ciudate, care au acelasi scop ca functiile obisnuite, numai ca arata mai urat. Ei bine, raspunsul este ca sunt foarte des intalnite la functiile filter si map, care se intampla sa fie si urmatorul subcapitol.

3.3 Filter & Map

Functiile de filter si map sunt esentiale pentru arsenalul fiecarui utilizator de python. Acestea permit modificarea sirurilor de date intr-un mod eficient si elegant.

Filter!

Functia de filter, dupa cum sugereaza si numele, filtreaza un set de date

Spre exemplu, sa zicem ca avem lista unor persoane care vor sa isi faca cont pe un site de dating, ce necesita varsta minima de 18 ani

Noi, respectand legea, vrem sa retinem doar persoanele majorate atunci. Filter este perfect pentru aceasta situatie!

varste = [12, 18, 14, 24, 70, 13]

Vom incepe cu solutia fara filter

def afla_majori_fara_filter(varste):
  majori = []
  for varsta in varste:
    if varsta >= 18:
      majori.append(varsta)
  return majori


majori = afla_majori_fara_filter(varste)
print(majori)
[18, 24, 70]

Pfft, cam mult cod. Se poate observa cum aceasta metoda poate duce la erori in cazul unor conditii mai complexe

Aceasta este problema pe care filter o rezolva

Solutia cu filter, dar fara lambda

def este_major(varsta):
  return varsta >= 18

majori = list(filter(este_major, varste))
print(majori)
[18, 24, 70]

Haideti sa disecam ce s-a intamplat aici,

Functia filter ia 2 parametrii, o functie booleana si un iterabil si returneaza un iterator

Astfel, filter itereaza prin fiecare obiect si il trece prin functia noastra.

Daca aceasta returneaza True, atunci trebuie pastrata, daca nu, trebuie filtrata

Separand functia de verificare de codul de iterare, ne permite sa scriem functii mai complexe si reduce sansa de a gresi

In final, a trebuit sa convertim iteratorul intr-o lista

Nu in ultimul rand, filter, dar cu lambda

majori = list(filter(lambda x: x >= 18, varste))
print(majori)
[18, 24, 70]

WOW! Cat de mult am putut reduce marimea codului...

Map!

Functia de map are rolul de a aplica o transformare pe fiecare element dintr-un set de date

Spre exemplu avem o lista de viteze a elevilor ce nu prea vin la ora de sport pentru a coda si vrem o lista noua in care fiecare viteza este dublata, pentru a le putea incheia media

Acum ca am discutat despre filter, map va parea mult mai usor

v = [6, 3, 4, 8, 12]

Solutia fara map(anti-pitonica)

def dubleaza_sirul(v):
  dublat = []
  for x in v:
    dublat.append(2 * x)
  return dublat

dublat = dubleaza_sirul(v)
print(dublat)

Din nou, cam mult cod si e destul de usor sa gresim

Solutia cu map, dar fara lambda

def dubleaza(x):
  return 2 * x

dublat = list(map(dubleaza, v))
print(dublat)
[12, 6, 8, 16, 24]

Solutia cu map si cu lambda

dublat = list(map(lambda x: 2 * x, v))
print(dublat)
[12, 6, 8, 16, 24]

ATENTIE! Map poate fi folosita pe mai mult de un sir in acelasi timp, spre exemplu:

Sa zicem ca inregistram timpul pentru care fiecare copil alearga si vrem sa aflam distanta pe care acesta a alergat

t = [3, 5, 2, 2, 1]
distanta = list(map(lambda x, y: x * y, v, t))

3.4 Exercitii

  1. faceti o functie recursiva 'tribonacci', care calculeaza al n-lea termen 'tribonacci', unde trib(n) = trib(n - 1) + trib(n - 2) + trib(n -3 ) si trib(0) = 0, trib(1) = 1, trib(2) = 1
def trib(n):
  return
  1. Creati o functie lambda care calculeaza cubul unui numar si numiti-o cub

  1. Creati o functie lambda care calculeaza restul unui numar la 5 si numiti-o mod_5

4a. Dandu-se un sir de numere, v, folositi functiile de la 2 si 3 pentru a creea alte 2 siruri noi a si b cu ajutorul functiei map, unde a = cub(v) si b = mod_5(v).

4b. Sa se calculeze sirul c care este diferenta dintre a si b, folosindu-se map

v = [27, 19, 20, 24, 11]
  1. Dandu-se un sir de numere naturale, v, folositi filter pentru a creea un sir nou care sa contina numerele care sunt multiplii de 5 sau de 3, dar nu de 15
v = [9, 8, 12, 15, 20, 25, 30, 14]

4. Clase

Clasele reprezinta cel mai utilizat mod de a structura atat date cat si functii intr-o singura structura reutilizabila. In acest fel codul devine organizat si modular.

4.1 Initializarea unei clase

Sa zicem ca vrem sa tinem cont de mai multe masini care sunt caracterizate de: brand, greutate, HP

Fara sa stim clase, cea mai buna optiune a noastra ar fi sa ne construim 3 vectori separati si sa pastram fiecare indice pentru cate o masina(adica masina_i sa corespuna cu brand_i, greutate_i si HP_i)

Spre ex

branduri = ['Mercedes', 'Hyundai', 'Dacia']
greutati = [1200, 1050, 1125]
HPs = [300, 150, 200]

Pentru a itera prin masini ar trebui sa folosim un for alaturi de functia zip:

for (brand, greutate, HP) in zip(branduri, greutati, HPs):
  print(f'Caracterizarea masinii: {brand}, {greutate}, {HP}')
Caracterizarea masinii: Mercedes, 1200, 300
Caracterizarea masinii: Hyundai, 1050, 150
Caracterizarea masinii: Dacia, 1125, 200

Nu pare asa de rau... hai totusi sa adaug inca doua masini noi

branduri.append('Audi')
greutati.append(1300)
HPs.append(250)

branduri.append('Volkswagen')
HPs.append(175)

Pfiu, a fost destul de mult cod, hai sa verificam daca totul este in regula

for (brand, greutate, HP) in zip(branduri, greutati, HPs):
  print(f'Caracterizarea masinii: {brand}, {greutate}, {HP}')
Caracterizarea masinii: Mercedes, 1200, 300
Caracterizarea masinii: Hyundai, 1050, 150
Caracterizarea masinii: Dacia, 1125, 200
Caracterizarea masinii: Audi, 1300, 250

Hmmm, masina Volkswageb nu apare..

Oh nu! Se pare ca am uitat sa adaug greutatea ultimei masini

Ei bine, aceasta este una dintre problemele pe care clasele le rezolva

Haideti sa ne cream prima clasa

class Masina():
  def __init__(self, brand, greutate, HP):
    self.brand = brand
    self.greutate = greutate
    self.HP = HP

  def descriere(self):
    print(f'Caracterizarea masinii: {self.brand}, {self.greutate}, {self.HP}')

Functia 'init' actioneaza drept constructor si este necesara pentru orice clasa intre paranteze se afla parametrii pe care vrem sa ii primeasca clasa la intializare

Cuvantul cheie 'self' descrie ceva ce apartine clasei

Inauntrul fiecarei clase, va exista aceasta functie pe care o putem apela

Acum sa incercam sa adaugam aceleasi masini ca inainte

De data aceasta putem tine un vector pentru ele

masini = []
masini.append(Masina(brand='Mercedes', greutate=1200, HP=300))
masini.append(Masina('Hyundai', 1050, 150))
masini.append(Masina('Dacia', 1125, 200))

for masina in masini:
  masina.descriere()
Caracterizarea masinii: Mercedes, 1200, 300
Caracterizarea masinii: Hyundai, 1050, 150
Caracterizarea masinii: Dacia, 1125, 200

Dintr-odata codul a devenit mult mai lizbil. Putem, de asemenea, sa punem parametrii in ordine fara sa le mentionam numele. Putem, de asemenea, sa punem parametrii in ordine fara sa le mentionam numele

Sa incercam sa gresim intetinoat sa vedem daca ne lasa, voi omite adaugarea greutatii

masini.append(Masina('Audi', 200))
---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

/tmp/ipython-input-2628771624.py in <cell line: 0>()
----> 1 masini.append(Masina('Audi', 200))


TypeError: Masina.__init__() missing 1 required positional argument: 'HP'

De data aceasta am fost anuntati din timp, astfel devine mult mai putin probabil sa gresim!

4.2 Ereditatea claselor

Adesea in OOP(object oriented programming), vom crea o clasa generala, care va functiona drept o schela, pentru clasele specializate in viitor.

Avand clasa 'Masina' implementata, sa zicem ca am vrea sa facem o clasa Camion, care are aceleasi proprietati ca o masina(brand, greutate, HP) si una noua, anume: capacitate. Are trei functii in plus: 1. incarca_marfa 2. descarca_marfa 3.afiseaza_marfa

Mai jos, va voi arata cum sa faceti asta, folosind ereditatea claselor. Aceasta sectiune va fi scruta, intrucat scopul ei este de a construi, mai degraba, o intuitie pentru ce este ereditatea si o idee generala pentru cum functioneaza. Astfel, nu va mai parea infricosatoare cand o veti intalni pe viitor.

OBSERVATIE! Intre paranteze se afla clasa 'parinte', din care se trage

class Camion(Masina):
  def __init__(self, brand, greutate, HP, capacitate):
    super().__init__(brand, greutate, HP) # Linia asta apeleaza init-ul parintelui pentru a asigura intializarea corecta
    self.capacitate=capacitate
    self.marfuri = []

  def incarca_marfa(self, marfa_noua):
    self.marfuri.append(marfa_noua)

  def descarca_marfa(self):
    self.marfuri.pop()

  def afiseaza_marfa(self):
    print(f'Marfuri: {self.marfuri}')

  def descriere(self):
    print(f'Caracterizarea masinii: {self.brand}, {self.greutate}, {self.HP} si marfurile: {self.marfuri} cu capacitatea: {self.capacitate}')

camion = Camion('Mercedes', 10, 200, 10)
camion.incarca_marfa('peste')
camion.incarca_marfa('pui')
camion.descarca_marfa()
camion.afiseaza_marfa()
camion.descriere()
Marfuri: ['peste']
Caracterizarea masinii: Mercedes, 10, 200 si marfurile: ['peste'] cu capacitatea: 10

OBSERVATIE! Am preluat functia prin ereditate, insa am editat-o pentru a descrie camionul

4.3 Exercitii

  1. Creati o clasa numita Carte, care sa descrie o carte prin urmatoarele:

    a. titlu: string

    b. autor: string

    c. pagini: numar

si o functie descriere pentru a descrie cartea in urmatorul format:

Cartea 'titlu' de 'autor' are 'pagini' pagini.


  1. Folosindu-va de clasa Carte, creati o subclasa Ebook care sa preia din Carte trebuie adaugat un atribut nou: dimensiune si sa fie editata functia de descriere pentru a afisa si dimensiunea

5. Citirea si scrierea in fisiere text

5.1 Citirea din fisiere

Citirea din fisiere in python este foarte simpla

Asa putem citi intregul continut al unui fisier:

with open("exemplu.txt", "r") as file:
  continut = file.read()
print(continut)

Asa putem itera linie cu linie prin continutul unui fisier:

with open("exemplu.txt", "r") as file:
  for line in file:
    print(line.strip())

.strip() scapa de spatiile albe de la inceputul si finalul liniei

5.2 Scrierea intr-un fisier

Scrierea este la fel de usoara

with open("exemplu.txt", "w") as file:
  file.write('Ne apropiem de finalul tutorialului!')

5.3 Exercitii

  1. Citind din acelasi fisier, calculati media notelor elevilor

  1. Adaugati un nou elev Fred, cu scorul 88

Sfarsit!

Sper ca acest notebook a fost educativ! Daca aveti nevoie de clarificari sau de solutiile problemelor, va puteti uita la videoclipul postat pe canalul de youtube al Olimpiadei de IA.