CURS ONLINE INTERACTIV

Python 3

PENTRU ÎNCEPĂTORI


"Primul volum Python în română care pur și simplu m-a uimit. Foarte bine explicat și ușor de înțeles." (Alexandru Cosmin)

"Cea mai bună carte de Python din RO." (Iulian Geană)

"Livrare promptă! M-am pus pe treabă!" (Emil Ilie)

TOATE RECENZIILE

Cum funcționează mediul de programare Python?

Scriem câteva linii, apăsăm un buton și pe ecran apare un rezultat. Dar cine execută programul? Unde putem încerca o comandă? Și cum aflăm ce nu merge atunci când rezultatul este greșit?

Vom lămuri aceste lucruri prin exemple scurte. Pentru început, este suficient să înțelegem rolul fiecărui instrument și să îl folosim. Explicațiile despre mecanismele interne sunt în secțiunile „Pentru cei curioși”, pe care le puteți deschide când doriți să aprofundați.

De la codul scris la rezultatul afișat

Editorul este locul în care scriem și modificăm codul. Interpretorul Python este programul care îl execută. Un mediu precum IDLE le aduce împreună: are un editor pentru fișiere și o consolă în care vedem rezultatele.

Putem compara editorul cu foaia pe care scriem o rețetă, iar interpretorul cu cel care urmează instrucțiunile. Faptul că am scris rețeta nu înseamnă că am și pregătit mâncarea; la fel, după redactarea codului, trebuie să îl rulăm.

În CPython, implementarea obișnuită instalată de pe python.org, codul este transformat într-o formă intermediară numită bytecode. Interpretorul execută apoi aceste instrucțiuni. Nu trebuie să pornim manual o compilare separată: mediul se ocupă de acest pas.

Componenta care execută bytecode-ul este numită adesea Python Virtual Machine (PVM). Denumirea poate părea complicată, dar nu este un program suplimentar pe care trebuie să îl instalăm.

Schemă simplificată a traseului codului Python prin compilare, bytecode și interpretor
Schema din material prezintă simplificat execuția. Procesorul rulează interpretorul și codul nativ al extensiilor; „programul tău” nu este o componentă hardware separată.
Pentru cei curioși: compilator, interpretor și portabilitate

Un compilator transformă codul sursă într-o altă reprezentare. Rezultatul poate fi cod nativ pentru procesor, ca în utilizarea obișnuită a C și C++, sau o formă intermediară, precum bytecode-ul. Așadar, compilarea nu înseamnă obligatoriu producerea unui executabil nativ.

În CPython, analiza și compilarea sursei produc bytecode, iar bucla de evaluare a interpretorului îl execută. CPython este implementat în principal în C. Interpretorul este deja un program compilat; nu recompilăm cod C pentru fiecare linie Python executată.

Codul sursă Python poate fi folosit pe Windows, Linux și macOS dacă există versiuni și biblioteci compatibile. Căile de fișiere și funcțiile specifice unui sistem pot necesita adaptări. Bytecode-ul este un detaliu al implementării și nu este garantat compatibil între versiuni Python sau între implementări.

Unde intră bibliotecile în această poveste?

Unele module sunt scrise în Python și sunt executate prin același mecanism. Altele conțin cod nativ, scris în C, C++ sau alte limbaje. De exemplu, modulul math din CPython și componente ale NumPy folosesc implementări native. Codul Python le apelează prin interfețele oferite, fără să trebuiască să gestionăm noi detaliile interne.

Pentru cei curioși: bytecode, variabile și memorie

La nivelul limbajului, variabilele sunt nume asociate unor obiecte. CPython gestionează obiectele într-o zonă de memorie numită heap; aici intră și numerele, nu doar listele sau dicționarele. În timpul evaluării, interpretorul folosește și o stivă cu referințe la obiecte pentru operațiile intermediare.

Modulul dis ne permite să inspectăm bytecode-ul. Următorul exemplu definește o funcție și afișează instrucțiunile ei interne; nu afișează valorile variabilelor:

import dis

def f():
    a, b = 3, 4

dis.dis(f)
Dezasamblarea funcției f în Python 3.10
Captură realizată în Python 3.10. În alte versiuni, instrucțiunile și pozițiile afișate pot diferi.

În captura de mai sus, LOAD_CONST încarcă referința la tuplul constant (3, 4), iar UNPACK_SEQUENCE îi despachetează elementele. Instrucțiunile STORE_FAST le asociază numelor locale a și b. Ultimele instrucțiuni încheie funcția, care returnează implicit None. În Python 3.12, finalul poate apărea ca RETURN_CONST.

Dacă reprezentăm vârful stivei în dreapta, imediat după despachetare ordinea referințelor este [4, 3]: prima extrasă este cea către 3, pentru a, apoi cea către 4, pentru b. Aceasta este o reprezentare didactică, nu o listă Python creată de program.

Un alt exemplu util este schimbarea valorilor a două variabile:

a, b = 3, 4
a, b = b, a
print(a, b)  # 4 3

Partea dreaptă este evaluată înainte de atribuiri, astfel încât valorile inițiale pot fi schimbate fără o variabilă auxiliară scrisă de noi. Nu trebuie să presupunem că interpretorul construiește obligatoriu un tuplu temporar: compilatorul poate optimiza această operație. Nici scrierea ei pe o singură linie nu reprezintă o garanție generală de atomicitate pentru programe cu execuție concurentă.

Consola interactivă: un loc pentru experimente

Consola interactivă ne permite să introducem o comandă și să observăm imediat efectul ei. Este potrivită pentru un calcul, verificarea unei funcții sau testarea unei idei, fără să salvăm mai întâi un program într-un fișier.

Acest mod de lucru se numește REPL, de la Read–Eval–Print Loop: citește, evaluează, afișează și așteaptă următoarea comandă. Promptul >>> arată unde putem scrie.

>>> 2 + 3
5
>>> x = 5
>>> x + 2
7

Observați că atribuirea x = 5 nu afișează nimic. Ea păstrează valoarea pentru utilizări ulterioare. Variabilele și funcțiile definite rămân disponibile în sesiunea curentă. Comenzile care ocupă mai multe linii, cum sunt definițiile de funcții, se introduc ca blocuri complete.

Cum deschidem consola pe Windows?

Apăsăm Win + R, scriem cmd și apăsăm Enter:

Deschiderea Command Prompt prin dialogul Run

În Command Prompt scriem python și apăsăm Enter. Dacă instalarea este accesibilă prin PATH, apare consola Python. Dacă avem instalat lansatorul Python, putem folosi și py. Comanda disponibilă depinde de instalare; pe alte sisteme se folosește frecvent python3.

Consola Python 3.10 cu expresia 2 + 3 și rezultatul 5
Capturile acestui articol folosesc în principal Python 3.10; principiile prezentate rămân utile și în versiunile mai noi.

Pentru a ieși, scriem exit() și apăsăm Enter. Putem face aceleași experimente și în fereastra Shell din IDLE, care oferă evidențierea sintaxei și alte facilități de editare.

În ce mediu scriem programele?

Nu trebuie să instalăm toate instrumentele de mai jos. Pentru exemplele noastre folosim IDLE; celelalte sunt alternative pe care le putem explora în funcție de experiență și de proiect.

IDLE

IDLE înseamnă Integrated Development and Learning Environment. Include un editor, o consolă interactivă și un debugger simplu. Este oferit împreună cu distribuțiile uzuale Python, deși disponibilitatea sa depinde de pachetul și opțiunile de instalare.

Aplicația IDLE pentru Python 3.10 în Windows

În Shell încercăm comenzi; în editor scriem programe pe care le salvăm și le rulăm. Completarea automată, culorile pentru cod și depanarea fac lucrul mai comod. Pentru pașii de instalare și primul program, puteți urma lecția despre instalarea Python și lucrul în IDLE.

Thonny

Thonny este un mediu conceput pentru începători. Oferă urmărirea execuției pas cu pas și afișarea variabilelor, fiind util atunci când vrem să vedem cum se schimbă valorile în program.

Interfața Thonny cu depanare pas cu pas și variabile vizibile

Visual Studio Code

Visual Studio Code este un editor extensibil. Cu extensiile potrivite oferă suport Python, depanare, terminal integrat și instrumente pentru proiecte. Este o opțiune flexibilă când dorim să lucrăm și cu alte limbaje.

PyCharm

PyCharm este un mediu de dezvoltare dedicat Python, cu facilități pentru completarea codului, refactorizare și depanare. Îl putem explora pe măsură ce proiectele cresc și avem nevoie de instrumente suplimentare.

Cum urmărim un program pas cu pas?

Un program poate rula fără să dea eroare și totuși să producă un rezultat greșit. Debugger-ul ne ajută să oprim execuția în puncte alese și să inspectăm valorile. Este ca și cum am pune pauză pentru a verifica un calcul înainte de a continua.

Luăm următoarea funcție:

def f(x):
    y = x + 2
    z = y * 3
    return z

f(5)

Pentru x = 5, obținem y = 7, apoi z = 21. Funcția returnează 21. Într-un fișier, apelul simplu f(5) nu afișează rezultatul; dacă dorim să îl vedem și în Shell după rulare, scriem print(f(5)).

Un punct de oprire în IDLE

Salvăm programul. În editor, facem clic dreapta pe linia z = y * 3 și alegem Set Breakpoint. Acesta este un punct în care dorim să suspendăm execuția:

Setarea unui breakpoint pe linia z = y * 3 în editorul IDLE
Linia cu breakpoint marcată în editor

În fereastra Shell, activăm Debug → Debugger. Revenim în editor și rulăm programul cu F5. În fereastra debugger-ului, folosim Go pentru a continua până la punctul de oprire.

Debugger-ul IDLE oprit înaintea calculului lui z, cu x egal cu 5 și y egal cu 7

Când execuția ajunge la linia marcată, aceasta urmează să fie executată. Putem verifica deja că x este 5, iar y este 7; valoarea lui z va fi calculată după executarea liniei.

  • Go continuă până la următorul punct de oprire sau până la final.
  • Step avansează pas cu pas și poate intra în funcțiile apelate.
  • Over execută linia curentă fără a urmări pas cu pas interiorul funcțiilor apelate.
  • Out continuă până la ieșirea din funcția curentă.

Debugger-ul ne arată variabilele și stiva apelurilor: funcțiile prin care a trecut execuția până în acel punct. Nu trebuie să cunoaștem organizarea internă a memoriei pentru a-l folosi.

Pentru cei curioși: depanarea cu modulul pdb

pdb este debugger-ul inclus în biblioteca standard Python. Îl controlăm prin comenzi text. Exemplul de mai jos folosește aceeași idee de calcul:

import pdb

def calc(x):
    y = x + 2
    z = y * 3
    return z

pdb.set_trace()
rezultat = calc(5)
print(rezultat)

Salvăm codul într-un fișier și îl rulăm din terminal, de exemplu cu python test.py, din dosarul respectiv. Capturile originale au fost realizate în Shell-ul IDLE, cu Python 3.10; comenzile de depanare sunt aceleași, dar aspectul afișării poate diferi.

pdb.set_trace() activează depanarea. În secvența ilustrată pentru Python 3.10, oprirea apare înaintea apelului rezultat = calc(5). Din Python 3.13, oprirea la set_trace() are loc imediat, deci prima poziție afișată poate fi diferită. Urmărim întotdeauna linia indicată de săgeată.

Promptul pdb înaintea apelului calc(5), în Python 3.10

Când linia indicată este apelul calc(5), comanda s intră în funcție:

Comanda s intră în funcția calc

Putem inspecta parametrul fără să avansăm execuția: scriem p x. Rezultatul este 5.

Comanda p x afișează valoarea 5

În captura următoare, comanda n aduce execuția la linia y = x + 2. Linia indicată nu a fost încă executată:

Debugger-ul indică linia y = x + 2 înainte de execuție

Încă o comandă n execută atribuirea. Acum p y afișează 7, iar următoarea linie este calculul lui z:

După atribuirea lui y, comanda p y afișează 7

Cu c continuăm execuția. Dacă nu mai există alte puncte de oprire, programul afișează rezultatul 21 și se încheie:

Secvența completă de depanare cu pdb și rezultatul final 21

Comenzile de bază sunt n pentru următoarea linie fără a intra în apeluri, s pentru avansare cu intrare în funcții, c pentru continuare și p expresie pentru afișarea valorii unei expresii. Mai multe exemple se găsesc în documentația pdb.

Cum aflăm unde se consumă timpul?

După ce programul produce rezultatele corecte, ne putem întreba dacă este suficient de rapid. Primul pas este să măsurăm, nu să schimbăm la întâmplare fragmente care ni se par lente.

cProfile înregistrează apelurile de funcții și timpul petrecut în ele. Dacă debugger-ul ne ajută să urmărim valorile, profiler-ul ne ajută să găsim funcțiile care merită analizate pentru performanță.

De exemplu, o funcție poate calcula o sumă printr-o buclă. Dacă în ea se consumă cea mai mare parte a timpului, analizăm algoritmul și operațiile folosite. Raportul pe funcții nu spune singur cât durează fiecare linie din buclă.

Pentru cei curioși: un raport cProfile explicat

Rulăm următorul exemplu:

import cProfile

def calc(n):
    s = 0
    for i in range(n):
        s += i
    return s

cProfile.run("calc(1000000)")

Funcția adună numerele de la 0 la 999999 și returnează 499999500000. În acest exemplu, pe ecran apare raportul profiler-ului, nu suma, deoarece nu am cerut afișarea rezultatului funcției.

Raport cProfile cu patru apeluri și aproximativ 0,027 secunde în captura originală
Valorile de timp sunt ilustrative și depind de calculator, versiune și condițiile rulării. Captura este ordonată după numele standard, nu după timp.
  • ncalls: numărul apelurilor.
  • tottime: timpul în funcție, fără timpul din funcțiile apelate de aceasta.
  • cumtime: timpul în funcție, inclusiv apelurile efectuate din ea.
  • percall: media pe apel pentru coloana de timp corespunzătoare.

În captura originală apar patru apeluri și aproximativ 0,027 secunde, aproape tot timpul fiind asociat funcției calc. Numărul intrărilor poate varia cu modul de lansare. Pentru ordonare după timpul cumulativ, înlocuim ultima linie cu:

cProfile.run("calc(1000000)", sort="cumulative")

Profiler-ul adaugă un cost de măsurare. Raportul este util pentru identificarea zonelor costisitoare, dar nu trebuie tratat ca o măsurătoare perfectă a timpului unei rulări fără instrumentare. Detalii: documentația profiler-elor Python.

Ce putem îmbunătăți în practică?

Începem cu algoritmul și structurile de date. O abordare care face mai puține operații poate conta mai mult decât mici modificări de sintaxă. Verificăm apoi dacă limbajul oferă deja o funcție potrivită, precum sum, len sau sorted, fără să presupunem că orice înlocuire este automat mai rapidă.

Păstrăm codul lizibil și verificăm rezultatele după modificări. Pentru exercițiile de început, obiectivul este să scriem corect, să putem explica pașii și să învățăm să măsurăm atunci când există o problemă de performanță.

Ce merită să reținem?

Editorul ne ajută să scriem codul, interpretorul îl execută, iar consola ne permite să experimentăm. Debugger-ul ne arată ce valori avem într-un anumit moment, iar profiler-ul ne ajută să aflăm în ce funcții se consumă timpul.

Nu trebuie să stăpânim toate instrumentele deodată. Putem începe cu câteva comenzi în Shell, apoi cu un program salvat și, când vrem să înțelegem mai bine un calcul, îl urmărim pas cu pas în debugger.

 arrow_back   home   perm_identity   list   arrow_upward