Introducere în Python

Varianta rulabilă a fișierului curent poate fi accesată aici: Introducere în Python

Documentația completă Python poate fi accesată aici: Documentație Python

Informații generale

Toate variabilele din Python sunt considerate obiecte:

my_int = 1
my_float = 2.0
my_string = "my string"
my_bool = True

Putem afisa folosind functia print():

print(my_int)
print('nlp')
print(13)
print('----------------')

Output:

1
nlp
13
----------------

Fiecare obiect are un tip, chiar dacă nu trebuie declarat:

print(f"{my_int} - {type(my_int)}")
print(f"{my_float} - {type(my_float)}")
print(f"{my_string} - {type(my_string)}")
print(f"{my_bool} - {type(my_bool)}")

Output:

1 - <class 'int'>
2.0 - <class 'float'>
my string - <class 'str'>
True - <class 'bool'>

Litera 'f' inaintea unui string formateaza acel string si permite scrierea valorilor unor variabile.

De exemplu: a scrie f"{a} + {b} = {a + b}" este echivalent cu a scrie str(a) + " " + str(b) + " = " + str(a + b)

Unde str() converteste variabilele din tipul lor in string

Sintaxă

Operatorii sunt similari cu cei din majoritatea limbajelor de programare:

a = 5
b = 2

print(f"{a} + {b} = {a + b}")
print(f"{a} - {b} = {a - b}")
print(f"{a} * {b} = {a * b}")
print(f"{a} / {b} = {a / b}")
print(f"{a} % {b} = {a % b}")

Output:

5 + 2 = 7
5 - 2 = 3
5 * 2 = 10
5 / 2 = 2.5
5 % 2 = 1

Avem, în plus, ridicarea la putere și împărțirea întreagă:

print(f"{a} ** {b} = {a ** b}")
print(f"{a} // {b} = {a // b}")

Output:

5 ** 2 = 25
5 // 2 = 2

Indentarea este foarte importantă. În locul acoladelor din C++, aici avem sintagme de forma:

if a % 3 == 0:
  print("Restul 0")

Desigur, putem avea mai multe cazuri:

if a % 3 == 0:
  print("Restul 0")
elif a % 3 == 1:
  print("Restul 1")
else:
  print("Restul 2")

Output: Restul 2

Operații repetitive

Cu număr cunoscut de pași (for)

for i in range(4, 10):
  print(i)

Output:

4
5
6
7
8
9

Observați utilizarea funcției range pentru a stabili intervalul. Aceasta are ca parametri start, end și step (default 1) și iterează de la start până la end - 1.

Pentru a itera descrescător, putem porni de la un start mai mare decât end cu pas negativ (aici -1):

for i in range(10, 4, -1):
  print(i)

Output:

10
9
8
7
6
5

Cu număr necunoscut de pași (while)

i = 4
while i < 10:
  print(i)
  i += 1

Output:

4
5
6
7
8
9

Python nu are definită instrucțiunea do ... while, dar aceasta poate fi simulată:

i = 4
while True:
  print(i)
  i += 1

  if i >= 10:
    break

Output:

4
5
6
7
8
9

Operații cu șiruri de caractere

Șirurile de caractere se scriu între ghilimele sau apostrof (de exemplu "sir").

Putem scrie un șir pe mai multe rânduri daca folosim 3 apostrofuri sau 3 ghilimele la inceput și la finalul lui.

Atenție: dacă am deschis șirul cu apostrof sau ghlimele trebuie să îl închidem cu același tip de semn!

Putem accesa caracterele unui șir direct prin indici. Șirurile sunt immutable, așadar putem accesa un caracter prin indice, dar nu îl putem modifica. Prin urmare, metodele șirurilor nu modifică șirul pe care se aplică ci returnează un șir nou.

Dacă adunăm două șiruri obținem concatenarea lor:

"py" + "thon"

Output: 'python'

Dacă înmulțim un șir cu un număr n, obținem un șir nou în care se repetă șirul inițial de n ori:

"abc" * 5

Output: 'abcabcabcabcabc'

Putem concatena elementele unei liste de șiruri de caractere folosind funcția join:

"->".join(['a', 'b', 'c'])

Output: 'a->b->c'

Metode utile

Eliminarea spațiilor din capete:

sir = " abc "
sir.lstrip()

Output: 'abc '

sir.rstrip()

Output: ' abc'

sir.strip()

Output: 'abc'

Creează o listă cu subșirurile șirului inițial

"ab#cd#efgh#".split("#")

Output: ['ab', 'cd', 'efgh', '']

Operații cu liste

Listele sunt seturi ordonate de elemente și se enumeră între paranteze pătrate.

Dacă adunăm două liste obținem concatenarea lor:

[1,2] + [3,4]

Output: [1, 2, 3, 4]

Dacă înmulțim o listă cu un număr n, obținem o listă nouă în care se repetă șirul inițial de n ori:

[1,2] * 4

Output: [1, 2, 1, 2, 1, 2, 1, 2]

Crearea unei liste

Se enumeră elementele între paranteze drepte, sau se folosește constructorul list care primește un obiect prin care se poate itera (precum un sir de caractere)

l1 = [1,2,3]
l2 = list("abc")
print(l1)
print(l2)

Output:

[1, 2, 3]
['a', 'b', 'c']

Lungimea unei liste

Lungimea unei liste se calculează cu len(lista). De exemplu:

len([10, 5, 1])

Output: 3

Indicii unei liste

Indicii unei liste încep de la 0. Pentru lista l = [10,5,1], l[0] este 10, iar l[2] este 1.

Putem folosi și indici negativi. Indicii negativi îi putem considera pornind de la drepta spre stânga începând cu -1, și tot avansând cu 1 spre stânga.

De exemplu, pentru lista de mai sus, l[-1] este 1, l[-2] este 5 și l[-3] este 10.

Subliste

Pentru a obține o sublistă dintr-o listă putem folosi notația l[inidiceStart:indiceFinal] care va oferi sublista cu elementele din lista inițială cuprinse între pozițiile indiceStart inclusiv și indiceFinal exclusiv.

Dacă dorim un mod de parcurgere a listei diferit de cel implicit, pentru a crea sublista, putem adăuga și parametrul de iterare: l[inidiceStart:indiceFinal:iterator].

Astfel se va porni de la indicele de Start, punând elementul corespunzător în sublistă, si la indiceStart se va aduna apoi iteratorul, generând urmatorul element din sublistă. Procedeul se va repeta până se ajunge la un indice mai mare sau egal cu indicele final (pentru această ultimă valoare care depășește limita dată de indicele final nu se mai generează element în sublistă).

Oricare dintre cele trei argumente ale scrierilor l[inidiceStart:indiceFinal] și l[inidiceStart:indiceFinal:iterator] poate lipsi, caz în care se iau valorile implicite - indiceStart ia valoarea 0, indiceFinal ia lungimea listei și iteratorul devine 1.

Exemple de utilizări ale acestor scrieri:

l = list(range(10))
l

Output: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

l[-1]

Output: 9

l[2:8]

Output: [2, 3, 4, 5, 6, 7]

l[2:8:2]

Output: [2, 4, 6]

l[-1:-7]

Output: []

l[-1:-7:-1]

Output: [9, 8, 7, 6, 5, 4]

l[8:2:-1]

Output: [8, 7, 6, 5, 4, 3]

l[:5]

Output: [0, 1, 2, 3, 4]

l[4:]

Output: [4, 5, 6, 7, 8, 9]

l[:]

Output: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

l[::-1]

Output: [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

Observați că putem folosi și indici negativi, dar dacă vrem o parcurgere de la dreapta la stânga, trebuie să folosim un iterator negativ.

Iterarea printr-o listă

def proceseaza(x):
  return x + 1

Putem itera în mai multe moduri. Operatorul in permite iterarea prin fiecare element al listei fără a cunoaște indicele elementului:

for elem in l:
  proceseaza(elem)

Dacă avem nevoie și de indicele elementului putem parcurge lista folosindu-ne de accesul direct al elementului prin indice - insă nu e recomandată această parcurgere când modificăm lista în acest mod (adăugăm sau ștergem elemente), deoarece trebuie să fim atenți la actualizarea indicelui.

for i in range(len(l)):
  proceseaza(l[i])

Putem itera printr-o listă folosind enumerate(lista) care este un generator prin care obținem pe rând tupluri de forma (indice, lista[indice]) - indicele din listă și elementul corespunzător acestuia.

for i, elem in enumerate(l):
  proceseaza(elem)

Matrici

În Python nu avem în mod direct o structură pentru matrici (printre modulele implicite), în schimb putem simula o matrice printr-o listă de liste.

De exemplu:

l = [[1, 2, 3], [4, 5, 6]]

este o matrice de 2 linii și 3 coloane.

Adăugarea elementelor într-o listă

La finalul listei - metoda append(element):

l = [10, 7, 3, 5]
l.append(2)
print(l)

Output: [10, 7, 3, 5, 2]

La o poziție dată - metoda insert(indice, element):

l = [10, 7, 3, 5]
l.insert(1, 122)
print(l)

Output: [10, 122, 7, 3, 5]

Ștergerea elementelor dintr-o listă

Se folosește metoda pop(indice) care șterge din listă elementul de pe poziția dată. Utilizarea fără argumente este echivalentă apelării cu indicele -1, așadar va șterge ultimul element.

l = [10, 7, 3, 5]
l.pop(2)
print(l)

Output: [10, 7, 5]

Fără parametri:

l = [10, 7, 3, 5]
l.pop()
print(l)

Output: [10, 7, 3]

Putem șterge un anumit element cu metoda remove(element):

l = [10, 7, 3, 5]
l.remove(7)
print(l)

Output: [10, 3, 5]

Sortarea unei liste

Sortarea unei liste se poate face simplu prin metoda sort():

l = [2, 1, 10, 4, 100, 17, 23]
l.sort()
print(l)

Output: [1, 2, 4, 10, 17, 23, 100]

Sortarea implicită este cea crescătoare. Pentru a sorta descrescător, putem folosi parametrul reverse al funcției sort cu valoarea True:

l = [2, 1, 10, 4, 100, 17, 23]
l.sort(reverse = True)
print(l)

Output: [100, 23, 17, 10, 4, 2, 1]

Putem sorta oricum dorim folosind aceeași funcție. De exemplu, pentru sortarea după ultima cifră a numerelor vom folosi funcția lambda:

l = [2, 1, 10, 4, 100, 17, 23]
l.sort(key = lambda x: x % 10)
print(l)

Output: [10, 100, 1, 2, 23, 4, 17]

Modificarea listei în timpul parcurgerii

Dacă dorim să modificăm o listă (de exemplu să ștergem toate elementele pare) este comună următoarea greșeală:

l = [3, 2, 4, 5, 10, 12]
for i in range(len(l)):
  if l[i] % 2 == 0:
    l.pop(i)
    i -= 1

Output:

1 2
3 10



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

IndexError                                Traceback (most recent call last)

<ipython-input-4-2ff85a8985d8> in <module>
      1 l = [3, 2, 4, 5, 10, 12]
      2 for i in range(len(l)):
----> 3     if l[i] % 2 == 0:
      4         print (i, l[i])
      5         l.pop(i)


IndexError: list index out of range

Vom primi o eroare penru metoda pop() care va da un "IndexError: list index out of range" pentru că i-ul nu este decrementat la ștergerea elementului, ci continuă parcurgerea range-ului.

O altă greșeală:

l = [3, 2, 4, 5, 10, 12]
for elem in l:
    if elem % 2==0:
        l.remove(elem)
print(l)

Output: [3, 4, 5, 12]

Chiar dacă acum programul se oprește, nici acest caz nu este corect deoarece i-ul nu este actualizat și sare peste elemente.

Modul corect. Sunt mai multe moduri prin care putem realiza cerința de mai sus, având grijă să nu sărim elementele. De exemplu, putem folosi while - în felul acesta limita pentru i nu mai e rigidă, precalculată, cum era în cazul lui range:

l = [3, 2, 4, 5, 10, 12]
i = 0
while i < len(l):
    if l[i] % 2 == 0:
        l.pop(i)
        i -= 1
    i += 1
print(l)

Output: [3, 5]

Modul corect și elegant. Folosind comprehensions:

l = [3, 2, 4, 5, 10, 12]
rez = [x for x in l if x % 2 == 1]
print(rez)

Output: [3, 5]

Operații cu dicționare

Dicționarele reprezintă un set de perechi de chei și valori asociate. Cheile sunt unice în dicționar și trebuie să fie de tip immutable (pot fi stringuri, numere, tupluri etc., dar nu pot fi liste).

Pentru a crea un dicționar vid, folosim:

d = {}

Pentru a adăuga chei noi în dicționar, putem pur și simplu să le atribuinduim o valoare. Sintaxa este: dictionar[cheie] = valoare.

d["a"] = 100
d["b"] = 200
d["c"] = 300
print(d)

Output: {'a': 100, 'b': 200, 'c': 300}

Pentru a itera printr-un dicționar putem folosi operatorul in:

for k in d:
    print(k, d[k])

Output:

a 100
b 200
c 300

Pentru a verifica dacă o cheie se găsește într-un dicționar, putem folosi același operator:

print("a" in d)
print(100 in d)

Output:

True
False

sau să folosim metoda items() care returnează o listă cu tupluri de forma (cheie, valoare):

for k, v in d.items():
    print(k, v)

Output:

a 100
b 200
c 300

Pentru a obține lista de chei putem folosi metoda keys() iar pentru lista de valori metoda values().

Mulțimi

Mulțimile reprezintă seturi de elemente neordonate care nu acceptă duplicate.

Crearea unei mulțimi

multime_vida = set()
multime = {2, 3, 10, 8}
print(multime)

Output: {8, 3, 10, 2}

Observați că nu s-a păstrat ordinea din inițializarea mulțimii, fiindcă mulțimile sunt neordonate. Cu toate acestea, ordinea va fi actualizată la primul update sau querry pe mulțime:

multime.add(5)
multime

Output: {2, 3, 5, 8, 10}

Cardinalul unei mulțimi

Pentru a obține cardinalul unei mulțimi se folosește funcția len(). De exemplu:

len({2, 4, 10})

Output: 3

List comprehensions

Au sintaxa de forma:

[expresie for element in obiect_iterabil]

caz in care se va genera o listă cu același număr de elemente precum obiectul iterabil

sau

[expresie for element in obiect_iterabil if conditie]

caz în care va avea în listă doar elementele din obiectul iterabil care îndeplinesc condiția.

Exemple:

l = [2, 7, 5, 23, 10]

Lista cu dublul elementelor lui l:

l1 = [2*x for x in l]
l1

Output: [4, 14, 10, 46, 20]

Lista cu perechile de vecini din l:

l2 = [[l[i], l[i + 1]] for i in range(len(l) - 1)]
l2

Output: [[2, 7], [7, 5], [5, 23], [23, 10]]

Lista produsului cartezian:

l3 = [[x, y] for x in l for y in l]
l3

Output:

[[2, 2],
 [2, 7],
 [2, 5],
 [2, 23],
 [2, 10],
 [7, 2],
 [7, 7],
 [7, 5],
 [7, 23],
 [7, 10],
 [5, 2],
 [5, 7],
 [5, 5],
 [5, 23],
 [5, 10],
 [23, 2],
 [23, 7],
 [23, 5],
 [23, 23],
 [23, 10],
 [10, 2],
 [10, 7],
 [10, 5],
 [10, 23],
 [10, 10]]

Lista elementelor pare:

l4 = [x for x in l if x % 2 == 0]
l4

Output: [2, 10]

Crearea unei matrici folosind comprehensions

Putem folosi un comprehension cu for dublu.

De exemplu dacă dorim o matrice formată doar din 0-uri:

l = [[0] * 5 for _ in range(10)]
print(l)

Output: [[0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]]

Atenție, frecvent se face greșeala următoare:

l = [[0] * 5] * 10
print(l)
l[0][0] = 111
print(l)

Output:

[[0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]]
[[111, 0, 0, 0, 0], [111, 0, 0, 0, 0], [111, 0, 0, 0, 0], [111, 0, 0, 0, 0], [111, 0, 0, 0, 0], [111, 0, 0, 0, 0], [111, 0, 0, 0, 0], [111, 0, 0, 0, 0], [111, 0, 0, 0, 0], [111, 0, 0, 0, 0]]

Observați cum s-a schimbat primul element în fiecare listă?

Atunci cand apelăm lista * n, unde n e un număr natural nenul, se copiază elemente din listă de n ori. Problema apare când avem o listă de obiecte, deoarece se copiază referențele către acele obiecte. Practic am avut [ lista_de_0] * 10, care a dus la o listă cu 10 referințe către aceeași lista de 0-uri, deci când am schimbat primul element din prima listă am văzut modificarea în toate cele 10 liste fiindcă de fapt sunt toate același obiect.

import copy

a = copy.deepcopy(l)
a

Output: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Operații cu fișiere

Pentru a deschide un fișier folosim metoda open(cale_fisier, mod_deschidere).

De exemplu, pentru a citi fisierul input.txt, putem folosi:

f = open("input.txt", "r")
sir = f.read()

caz în care vom avea în șir tot conținutul fișierului.

O altă variantă este să folosim metoda readlines() care returnează o listă de stringuri cu liniile fișierului.

Pentru a scrie într-un fișier putem deschide fișierul cu "w" pentru a fi suprascris, sau cu "a" pentru a adăuga la final de fișier.

Pentru a scrie într-un fișier putem folosi metoda write().

Clase

Pentru a defini o clasă folosim cuvântul cheie class, urmat de numele clasei.

Clasele au o metodă specială prin care se construiesc instanțele clasei, numită __init__. În această funcție vom trimite argumentele necesare pentru a completa proprietățile noii instanțe a clasei.

Orice metodă proprie instanțelor are ca prim parametru chiar o referință către instanță respectivă (metoda __init__ nu face excepție). De obicei acest prim parametru este numit self, dar nu este obligatoriu. Parametrul self nu va avea un argument corespunzător în apelul metodei, practic metodele se apelează cu argumente corespunzătoare tuturor parametrilor (mai puțin self).

Proprietățile de instanță nu se definesc direct în clasă, ci sunt create în constructor atunci când le folosim numele prima oară. De exemplu, putem face o inițializare: self.proprietate = valoare.

class Cls:
  def __init__(self, aa, bb):
    self.a = aa
    self.b = bb
  def incrementeaza_a(self):
    self.a += 1


c1 = Cls(2,5)
c1.incrementeaza_a()
print(c1.a)

Output: 3

Observați cum în exemplul de mai jos, la crearea instanței c1, s-au dat valori doar pentru parametrii aa și bb din __init__, primul parametru fiind self, pentru care nu se oferă argument. Metoda incrementeaza_a() nu se apelează cu argumente deoarece are ca unic parametru self (adică instanța).

Pentru a asigura o afișare frumoasă a elementelor dintr-o clasă putem defini metodele __str__ și __repr__, ambele având ca rol returnarea unui string reprezentativ pentru instanța curentă.

Când apelăm print(obiect) se scrie ce returnează __str__. Când apelăm print(lista_de_obiecte) se afișează lista aplicând __repr__ pentru fiecare obiect.

Dacă apelăm str(obiect) obținem stringul returnat de __str__, iar cu repr(obiect) stringul returnat de __repr__.

class Cls:
  n = 100
  def __init__(self, aa, bb):
    self.a = aa
    self.b = bb

  def __str__(self):
    return "a = {} b = {}".format(self.a, self.b)
  def __repr__(self):
    return "({}, {})".format(self.a, self.b)
c1 = Cls(2, 5)
print(c1)

Output: a=2 b=5

print(str(c1))

Output: a=2 b=5

print(repr(c1))

Output: (2, 5)

c2 = Cls(3, 3)
c3 = Cls(4, 1)
print([c1, c2, c3])

Output: [(2, 5), (3, 3), (4, 1)]

Operatori

Se pot defini operatori pentru elementele unei clase. De exemplu, putem defini operatori de egalitate sau care să determine dacă un element se află într-o relație de ordine față de altul.

Pentru clasa de mai sus am putea considera că elementele se ordonează întâi după proprietatea a, apoi după b. Avem operatorii:

  • __eq__ operația de egalitate
  • __lt__ operatorul "<"
  • __le__ operatorul "<="
  • __gt__ operatorul ">"
  • __ge__ operatorul pentru ">="
class Cls:
  def __init__(self, aa, bb):
    self.a = aa
    self.b = bb
  def __eq__(self, elem):
    return (self.a, self.b) == (elem.a, elem.b)
  def __lt__(self, elem):
    return (self.a, self.b) < (elem.a, elem.b)
  def __le__(self, elem):
    return (self.a, self.b) <= (elem.a, elem.b)
  def __gt__(self, elem):
    return (self.a, self.b) > (elem.a, elem.b)
  def __ge__(self, elem):
    return (self.a, self.b) >= (elem.a, elem.b)

c1 = Cls(2,5)
c2 = Cls(2,5)
c3 = Cls(2,4)

Output:

print(c1 < c3)

Output: False

print(c1 >= c3)

Output: True

Proprietăți și metode de clasă

Proprietățile clasei se definesc direct în clasă, de obicei la început.

Funcțiile obișnuite se numesc metode de instanțiere.

Metodele de clasă vor fi precedate de decoratorul @classmethod.

Instanțele pot accesa proprietăți și metode de clasă.

În momentul în care o instanță încearcă să modifice o proprietate de clasă prin scrierea instanta.proprietate_clasa = valoare, nu se va modifica proprietatea clasei ci se va crea o proprietate a instanței cu acelasi nume. Din acel moment instanța nu mai poate accesa (în mod direct) decât propia proprietate cu acel nume:

class Cls:
  n = 100
  def __init__(self, aa, bb):
    self.a = aa
    self.b = bb

  @classmethod
  def incrementeaza_n(cls):
    cls.n += 1
print(Cls.n)

Output: 100

c1 = Cls(2, 5)
print(c1.n)

Output: 100

c1.n = 17
print(c1.n)
print(Cls.n)

Output:

17
100
c2 = Cls(2,5)
print(c2.n)
print(Cls.n)

Output:

100
100
Cls.incrementeaza_n()
print(c1.n)
print(Cls.n)

Output:

17
101
c1.incrementeaza_n()
print(c1.n)
print(c2.n)
print(Cls.n)

Output:

17
102
102

Modulul math

Modulul math este folosit pentru funcții matematice utilizate frecvent:

  • floor(numar) - returneaza partea întreagă inferioară
  • ceil(numar) - returnează partea întreagă superioară
  • sqrt(numar) - returnează rădăcina pătrată a numărului
  • funcții trigonometrice: sin(numar), cos(numar) etc.
import math

math.sqrt(8)

Output: 2.8284271247461903

Modulul time

Uneori avem nevoie să calculăm cât a durat o anumită zonă de cod. Pentru asta putem folosi funcția time() din modulul time. Exemplu:

import time

t1 = time.time()
# zona de cod pentru care dorim sa calculam timpul
t2 = time.time()
print(t2 - t1)  # in secunde

Output: 2.5987625122070312e-05