Scopul primelor trei laboratoare de Arhitectura Sistemelor de Calcul îl reprezintă familiarizarea cu probleme de multi-threading, concurență și sincronizare. Pentru a aplica mai ușor și mai rapid aceste noțiuni, vom folosi limbajul Python. În cadrul primului laborator vom prezenta principalele caracteristici ale limbajului Python, urmând ca în laboratoarele 2 și 3 și la tema 1 să lucrăm cu thread-uri pentru a implementa aplicații concurente.
Python este un limbaj de programare foarte popular ( statistici github, utilizare), oferind posibilitatea programării structurate dar și orientate pe obiect și incluzând și elemente din paradigma funcțională. Este un limbaj de scripting, ceea ce înseamnă că este interpretat și nu compilat, economisind mult timp în procesul de dezvoltare și debugging.
Limbajul oferă o multitudine de funcționalități într-o sintaxă ușor de învățat. Are module, clase, excepții, tipuri dinamice și garbage collection. API-ul Python oferă module pentru o gamă foarte mare de funcționalități, de la cele de bază pentru lucrul cu șiruri și fișiere, până la cele pentru lucrul cu procese, threaduri, sockets, serializare etc. Multe dintre aceste module oferă o interfață foarte similară programării la nivel de sistem din C (ceea ce se studiază la SO), astfel încât funcțiile Python au același nume si aproximativ aceiași parametrii ca cei pentru funcțiile din C pentru apeluri de sistem. Există o varietate de API-uri, cele mai populare la momentul actual fiind cele pentru aplicatii web, AI bots, data science si grafice, de exemplu: wit.ai, flask, NumPy, SciPy, pandas, plotly, Matplotlib, Python & RaspberryPi.
Implementarea principală a Python, CPython (în C) a fost concepută pentru a fi portabilă: funcționează pe Linux, Unix, Windows, Mac OS X.
De ce Python?
Python este un limbaj ce oferă foarte multe funcționalități și are o curbă de învățare rapidă atât pentru programatorii ce cunosc deja limbaje precum C și Java, cât și pentru începători. Deși este un limbaj interpretat, acest lucru nu a stat în calea popularității sale și folosirii în numeroase proiecte. Puteți utiliza Python atât pentru scripturi, dezvoltarea unor infrastructuri de testare, cât și pentru aplicații web și machine learning/data mining.
De ce Python la ASC?
În cadrul laboratoarelor dorim să ne axăm pe scrierea de programe multi-threaded corecte, nu pe particularitățile vreunui limbaj. Din acest punct de vedere Python este de preferat datorită interfeței simple pentru lucrul cu thread-urile și pentru modul de lucru mai rapid si numărul de linii de cod mult mai mic în comparație cu Java sau C. Avantajul este cel al simplității și asemănarea API-ului pentru thread-uri cu cel din C-pthreads, însă are și un dezavantaj oferit de un mecanism din interpretor, pe care îl vom discuta în laboratorul 2.
Python se poate descărca de pe site-ul oficial, secțiunea Download.
Pe un sistem Linux, este foarte probabil să fie deja instalat. Verificați acest lucru dintr-o consolă:
$ python Python 3.9.16 (default, Dec 7 2022, 10:16:11) [GCC 9.4.0] on linux Type "help", "copyright", "credits" or "license" for more information. >>>
Dacă nu îl aveți instalat, pasul următor ar fi să-l căutați în repository-urile distribuției cu care lucrați. Exemplele următoare prezintă instalarea pe o distribuție bazată pe Debian și pe una bazată pe Red Hat.
$ sudo apt-get install python
$ sudo yum install python
Pe un system Windows se poate descărca de aici. După instalare se poate adăuga calea către executabilul Python în variabila globală PATH, pentru a putea fi folosit din orice terminal. Acest lucru se poate face mai ușor selectând opțiunea Add Python to PATH la începutul instalării.
Dacă doriți să folosiți un IDE pentru Python vă recomandăm versiunea community a PyCharm.
Pentru a verifica respectarea code-style-ului recomandat de Python vă recomandăm PEP8 online. Pentru verificarea code-style-ului, detectarea erorilor și alte lucruri utile, puteți folosi Pylint. Aceste este instalat și pe calculatoarele din laborator, la fel ca și PyCharm.
În Windows, dacă extensia .py este deja înregistrată, un dublu-click pe numele scriptului este suficient.
Dacă folosiți Linux, există mai multe posibilități:
$ cat hello.py print('Hello World!') $ python hello.py Hello World!
$ cat hello.py #!/usr/bin/python print('Hello World!') $ chmod +x hello.py $ ./hello.py Hello World!
Pentru a testa anumite funcționalități sau a verifica o anumită sintaxă nici nu este nevoie să scrieți un script, puteți folosi direct consola Python. Din ea se iese scriind quit() sau apăsând Ctrl-D.
Din consolă puteți obține documentația despre anumite module, clase, metode folosind help(nume), inclusiv despre modulele și metodele scrise de voi dacă includeți în cod comentarii docstring (similare javadoc-ului din Java). Alternativ, dacă doriți să vedeți metodele disponibile pentru un anumit obiect puteți folosi funcția dir(obiect), ca în exemplele de aici.
>>> import io >>> help(open)
import sys if len(sys.argv) < 2: print('Usage:....') exit(0)
a = 1 # Afisam numerele de la 1 la 10 while a <= 10: print(a) a += 1 print("Am terminat")
[...] if b > 3: """ Silly comment """ a = b
$python comments.py File "comments.py", line 4 """ Silly comment """ ^ IndentationError: expected an indented block
Corect este:
if b > 3: """ Silly comment """ a = b
Python oferă tipuri de date numerice, booleene, șiruri (string, liste etc), dicționare, fișiere, clase, instanțe și excepții.
Python asociază numele unei variabile cu un obiect, care poate fi număr, șir de caractere sau ceva mai complex. Când este folosită o variabilă, tipul acesteia este tipul obiectului cu care este asociată. Este greșită folosirea într-o expresie a unei variabile care nu a fost asociată cu un obiect.
i = 5 # i va fi de tip Integer i = "Hello" # i va fi de tip String
Alocarea și dezalocarea de memorie se face automat de către Python, existând un mecanism de garbage collection.
În Python se poate face atribuirea de valori unor mai multe variabile simultan:
x, y = 2, 3
Datorită faptului că partea dreaptă a unei expresii este evaluată înainte de a se face atribuirea, valorile a două variabile pot fi foarte ușor interschimbate, fară a avea nevoie de o a treia variabilă:
x, y = y, x
Pentru lucrul cu șiruri de caractere, Python oferă tipul String și o serie de operații de bază pe acesta, descrise în documentație.
Stringurile sunt incluse între ghilimele ” sau '. Stringurile ce conțin mai multe linii sunt înconjurate de trei ghilimele sau apostrofuri succesive ”””.
s = "string pe o linie" linie = """string pe mai multe linii"""
Stringurile sunt tratate ca niște liste, cuvant[i] fiind caracterul din șir ce are indexul i, unde i ia valori în intervalul [-length,length). Folosirea unei valori în afara acestui interval va genera o eroare: IndexError: string index out of range. Python permite și folosirea unor indici negativi, care indexează elementele pornind de la sfârșitul șirului, precum în exemplul de mai jos
stringul: s t r i n g index: 0 1 2 3 4 5 index negativ: -6 -5 -4 -3 -2 -1
Nu există un tip de date special pentru caractere, acestea sunt văzute drept șiruri de lungime 1. Lungimea unui șir se poate afla cu ajutorul funcției: len(some_string).
Caracterul : specifică un subșir al unui şir folosind sintaxa: some_string[x:y]. Subșirul obținut conține toate caracterele din șirul inițial (some_string) între pozițiile x si y-1 (inclusiv). Dacă nu se specifică x sau y, acestea au implicit valorile 0, respectiv lungimea șirului.
>>> s="abcdef" >>> s[0]="g" Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'str' object does not support item assignment
s = "string" print(s[0:2]) # st print(s[:3]) # str print(s[3:]) # ing s2 = "one" print("Write " + 2*s2 + " " + s) # Write oneone string print("hello %s, lab %d !" % ("students",1)) # hello students, lab 1 ! import string print("hello {}, lab {}".format("world",1)) # hello world, lab 1 print("hello {0}, lab {1}".format("world",1)) # incepand cu python 2.6
În Python se poate lucra cu numere întregi și numere în virgulă mobilă. Numerele întregi dintr-o expresie se convertesc automat în numere în virgulă mobilă dacă este necesar. Cele patru tipuri numerice int, long, float și complex și operațiile acestora sunt descrise în API. În afară de operațiile aritmetice standard (+,-,*,/), există operatori pentru modulo % și pentru ridicarea la putere * *.
Deoarece Python este strongly typed, nu se face conversie automată între tipurile de date dintr-o expresie, cum ar fi operațiile între tipurile String și Integer. Pentru acest lucru se folosesc funcțiile int(some_string)
pentru transformarea din string în integer și respectiv: str(some_int)
sau repr(some_int)
sau `some_int` (` - backquote) pentru transformarea din integer în string. Atenție! În cazul transformării din string în integer, dacă stringul ce trebuie convertit conține și alte caractere decât cifre, la execuție va apărea o eroare: Value Error: invalid literal for int() with base 10.
print("Un string:", "4" + "2") # Un string: 42 print("Un numar:", 4 % 3 + int("41")) # Un numar: 42 print("Un string:", "4" + str(2)) # Un string: 42 print('persoana %s are %d ani' % ("X", 42)) # persoana X are 42 ani print((2.0 + 3.0j) * (2.1 -6.0j)) # (22.2-5.7j) print(2 ** 3 ** 2) # 512
Python pune la dispoziție două tipuri de structuri pentru a reprezenta liste de elemente: tupluri și liste. Diferența principală dintre cele două tipuri este că tuplul nu mai poate fi modificat după ce a fost declarat, fiind obiect immutable.
array
care se comportă ca o listă cu restricții asupra tipului elementelor din ea.lista = ["string", 10] tuplu = ("string", 10)
Accesarea elementelor unei liste sau unui tuplu se face la fel ca în cazul șirurilor de caractere, cu indecși pozitivi, negativi sau folosind operatorul :. Spre deosebire de stringuri și tupluri, elementele unei liste pot fi modificate cu ajutorul accesării prin indecși.
>>> print([ elem*2 for elem in [1,2,3,4] if elem!= 3]) [2, 4, 8] >>> print([(x, x**2) for x in [1,2,3,4]]) [(1, 1), (2, 4), (3, 9), (4, 16)]
>>> l = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] >>> list(map(lambda x: x + 1, l)) [2, 3, 4, 5, 6, 7, 8, 9, 10, 11] >>> list(filter(lambda x: x > 5, l)) [6, 7, 8, 9, 10] >>> from functools import reduce >>> reduce(lambda x, y: x + y, l, 0) 55
Pentru a folosi reduce, trebuie importat modulul functools
.
O structură dicționar este un set neordonat de chei și valori în care valoarea poate fi cautată folosindu-se cheia. Cheile sunt obiecte hashable, ceea ce presupune că au o funcție hash ce întoarce întotdeauna aceeași valoare pentru obiectul respectiv. Atenție! listele, dicționarele și alte tipuri de date mutable nu sunt hashable, deci folosirea lor drept chei nu este posibilă.
cheie:valoare
, despărțite de virgule.dict={} dict[0] = "primul" dict["unu"] = 2 print(dict) # {0: 'primul', 'unu': 2} dict2 = dict dict2[3] = "ceva" del dict2["unu"] print(dict) # {0: 'primul', 3: 'ceva'} print(len(dict)) # 2 print(5 in dict) # False if 0 in dict: dict["3"] = 2 print(dict) # {0: 'primul', '3': 2, 3: 'ceva'} print(list(dict.keys())) # [0, '3', 3]
Câteva operații utile (lista completă o găsiți în documentație):
len(some_dict)
- pentru a afla dimensiunea unui dicționardel some_dict[cheie]
- pentru a șterge o intrare din dicționarin
- pentru a afla dacă o cheie este în dicționar, înlocuitor pentru funcția has_key(cheie)
din Python2keys()
- întoarce o listă ce conține toate cheile din dicționarif conditie1: instructiuni elif conditie2: instructiuni else: instructiuni
elif
sau nici una, iar secțiunea else
apare o dată sau niciodată. None
sunt considerate False
dacă sunt folosite în evaluarea unei condiții.
?:
, ci x if cond else y
.
>>> a = 1 >>> b=0 if a>0 else 2 >>> b 0
Instrucțiunea for
iterează după elementele unei secvențe, fie ea String, list sau tuple.
for elem in lista: instructiuni
În partea de după in
din for-uri se folosesc și funcții care întorc liste, ca de exemplu range()
:
* range(stop)
sau range(start, stop [,step])
- formează o listă cu elemente mai mici ca stop
în progresie aritmetică, cu rația step
. Primul element al listei este start
. Implicit start
este 0 si rația este 1.
for i in range(len(s)): print(s[i])
Instrucțiunea while
continuă iterația cât timp condiția specificată este adevarată.
while conditie:
instructiuni
Instrucțiunea break
termină forțat orice buclă while
sau for
, iar instrucțiunea continue
sare la următoarea iterație.
Funcțiile sunt definite folosind cuvântul cheie def
urmat de o listă de argumente și apoi de ”:”.
return
sau se execută return
fară argumente, atunci funcția va întoarce None
.pass
>>> dir(hello_asc)
['__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__doc__', '__format__', '__get__', '__getattribute__', '__globals__', '__hash__', '__init__', '__module__', '__name__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'func_closure', 'func_code', 'func_defaults', 'func_dict', 'func_doc', 'func_globals', 'func_name']
>>> f = hello_asc
def fractie(x, y=1): # y are valoare implicita if (y==0): return # va întoarce None else: return float(x)/float( y) def fractie2(): #TODO pass print(fractie(6, 4)) print(fractie(6)) # y va fi 1
len
pentru lungimea unui obiect de tip colecție, id
care în implementarea CPython întoarce adresa de memorie a obiectului, funcții de conversie de tip cum ar fi str
, int
, float
, bool
, etc.
Pentru a accesa variabilele globale ale programului într-o funcție, trebuie să folosim cuvântul cheie global
cu sintaxa: global some_var
.
Nu este necesară folosirea acestui cuvânt cheie atunci când în funcție doar se citește variabila respectivă. Este obligatorie folosirea global
doar dacă în funcție se dorește modificarea valorii variabilei. Fără această declarație s-ar crea o nouă variabilă locală funcției, care nu ar afecta valoarea variabilei globale.
În Python se pot include funcții, variabile și clase definite în alte fișiere. Un astfel de fișier, ce poate fi inclus, poartă denumirea de modul. Exemplu:
import random
Atunci când includem modulul putem să îl folosim cu un alt nume, eventual, pentru a evita coliziuni de denumiri:
import random as rand
Instrucțiunea import
din exemplul de mai sus nu încarcă în tabela de simboluri numele funcțiilor definite în modulul random
, ci doar numele modulului. Folosind acest nume se pot accesa funcții definite în interiorul modulului folosindu-se sintaxa nume_modul.nume_functie(parametri)
.
random.random()
Un alt mod de a include este folosind from <nume_modul> import <lista_constructii>
, în felul acesta vom avea acces doar la clasele/metodele din modul precizate în <lista_constructii>. Această modalitate este indicată atunci când un modul are un număr foarte mare de clase. Un alt avantaj al acestei metode este că nu se mai folosește numele modulului atunci când se utilizează această construcție, ca în exemplul de mai jos:
from random import random # se include doar metoda random() random() # se apeleaza metoda random
Ca și Java și alte limbaje de nivel înalt, Python oferă suport pentru excepții. Pentru prinderea excepțiilor aruncate de către funcții se folosește mecanismul try-except
, asemănator celui try-catch
din Java.
try: x = int(buffer) except ValueError as e: print("Date de intrare invalide") finally: # clean-up
Mecanismul funcționează în felul următor:
try
except
, execuția sare la instrucțiunile din blocul respectiv. După ce excepția este tratată, execuția continuă cu prima instructiune de după blocul try
except
, aceasta continuă să fie propagată.finally
se execută întotdeauna, indiferent dacă a fost prinsă o excepție sau nu, iar excepțiile netratate se aruncă dupa ce se execută blocul finally
. Blocul acesta este opțional și este folosit pentru acțiuni de “clean-up”, de exemplu închiderea fișierelor.
O excepție poate fi aruncată folosind instrucțiunea raise
. Aceasta poate fi folosită și fară argumente în interiorul unui bloc except
pentru a re-arunca excepția prinsă de blocul respectiv.
if (j>100): raise ValueError(j)
O instrucțiune try
poate avea mai multe clauze except
. Ultima clauză except
poate să nu aibă specificată nicio excepție de tratat fiind astfel folosită pentru a trata toate exceptiile netratate de celelalte clauze.
while
și for
, și construcțiile try-except
pot avea opțional și o clauză else
. Instrucțiunile din blocul else
sunt executate atunci când blocul try
nu generează nicio excepție.
Python oferă un set de excepții predefinite (built-in), conținute în modulul exceptions
(lista lor este în documentație). În afară de acestea, pentru a vă defini propriile excepții este necesar să creați o subclasă a clasei Exception
.
Puteți folosi în loc de blocul try-except și keyword-ul with, ca în exemplul din secțiunea Operații cu fișiere. Această construcție apelează automat o metodă de clean-up a obiectului (ex: close
pentru fișiere) la apariția unei excepții. Acestă construcție o vom folosi în laboratoarele următoare și pentru obiectele de sincronizare.
Lucrul cu fișiere este, de asemenea, simplu, iar obiectele și metodele utilizate sunt oferite în modulul io.
Pentru a obține un obiect de tip fișier, se apelează funcția open
, de obicei cu doi parametri:
import io f = open('input.txt','w') f.write("hello") f.close()
Odată obținut obiectul fișier f
, se vor putea folosi funcțiile pentru lucrul cu fișiere: read
, readline
, readlines
, write
, seek
sau close
ca metode ale clasei file
(e.g. f.read()
).
Spre deosebire de Java, Python nu vă obligă să încadrați codul de lucru cu fișierele într-un bloc try/catch pentru a preveni eventualele excepții. Este recomandat însă să îl încadrați în try/except, sau să folosiți operatorul with
introdus în Python 2.5, ca în exemplul următor.
with open('in.txt', 'r') as f: for line in f: #citire linie cu linie din fisier print(line)
Pentru manipularea path-urilor, verificarea fișierelor și directoarelor și alte operații cu acestea puteți folosi funcțiile din modulele os și os.path. Exemplul următor folosește funcțiile de verificare a existenței și tipului unui path.
import os, os.path if not os.path.exists(output_folder): os.mkdir(output_folder) elif not os.path.isdir(output_folder): print("Given path is not a folder")
Construcția __main__ este opțională în scripturile Python, și rolul său este similar funcțiilor main din alte limbaje (C, Java etc).
Codul conținut într-un script Python este executat și fără prezența acestui mecanism, însa dezavantajul omiterii lui este că atunci când se include fișierul cu import
se va executa și codul din el, ca în exemplul următor:
def f1(): print("hello1") f1()
import utilities utilities.f1()
$ python mymodule.py hello1 hello1
Dacă în modulul utilities apelul funcției f1() se afla într-un __main__
, aceasta nu se apela, deoarece __name__
este numele modulului inclus și codul de după if __name__ == "__main__":
nu se va executa.
def f1(): print(__name__) print("hello1") if __name__ == "__main__": f1()
$ python mymodule.py utilities hello1 $ python utilities.py __main__ hello1
Folosind scheletul de cod implementați un CoffeeMaker. Acesta primește comenzi de la utilizator (e.g. să facă o cafea) și afișează rezultatele/mesajele. Comenzile sunt definite în modulul task0
. Puteți personaliza mesajele afișate, resursele, puteți alege ce structuri de date să folosiți etc.
Task 1 - Implementați comenzile din fisierul coffee_maker.py
Task 2 - citiți rețetele pentru cafea din fișierele oferite în directorul recipes. Implementați citirea în modulul load_recipes.py.
Disclaimer: Recomandăm scrierea în engleză a documentației, comentariilor și a numelor de variabile, funcții, clase, module atât la laborator cât și la teme. Încercăm să vă oferim schelete de cod care să respecte acest lucru.