momente şi schiţe de informatică şi matematică
To attain knowledge, write. To attain wisdom, rewrite.

Experimente de modelare a datelor (Python, Django şi SQL)

Django | MySQL | Python | SQL
2013 jan

[1] Modele de date pentru reflectarea situaţiei şcolare, cu Python şi Django

Într-o aplicaţie de reflectare dinamică a situaţiei şcolare, partea esenţială este modelarea datelor specifice (notele şi mediile elevilor la obiectele de învăţământ). Am încercat în [1] o modelare "de tip CSV", în care pentru fiecare elev păstram într-un câmp de tip "text" toate informaţiile, concatenând liniile de forma nume_obiect: note_curente/medii, pentru fiecare obiect; avantajul era că fiecărui elev îi corespunde astfel o singură înregistrare pentru toate notele (la toate obiectele) şi deasemenea, o singură înregistrare pentru mediile semestriale şi anuale (la toate obiectele).

Dar astfel, toate calculele de medii au rămas de făcut în Python, construind şi manevrând liste şi dicţionare corespunzătoare liniilor din textul de obiecte şi note/medii (pentru fiecare elev, apoi pentru elevii clasei, pentru fiecare obiect, arie curriculară, etc.); până la urmă funcţiile necesare au devenit obositoare, dificil de pus la punct şi în fond, "discutabile" în privinţa eficienţei.

Ideea de a constitui un "class" Python de sine stătător cu metode de prelucrare corespunzătoare unui text de note/medii rămâne valabilă, dar cel mai firesc este ca genul acesta de calcule să fie interpretat ca "agregare de valori medii pentru grupuri de coloane din diverse tabele" şi să fie lăsat în seama serverului de date ("sistemul de gestiune a bazelor de date", sau "motorul" bazelor de date) - aşa că renunţat la modele "de tip CSV" şi am revenit la modelarea "clasică" a datelor.

Modele de date

Mai jos vom angaja următoarele modele de date (dintre cele ale aplicaţiei menţionate):

Arie(nume, acro)
caracterizează ariile curriculare, printr-un nume şi un acronim; de exemplu, nume="Limbă şi comunicare", acro="1[LC]"
Obiect(nume, arie)
fiecare obiect de învăţământ (vom zice obiect… ignorând riscul de a confunda cu "obiect Python") ţine de o anumită Arie; de exemplu, "Română" şi "Engleză" ţin de aria "1[LC]".
Clasa(cod, nume, obiects)
reflectă o clasă din şcoală (pentru anul şcolar curent); obiects reprezintă o relaţie "many-to-many" cu Obiect (o clasă are eventual mai multe obiecte, un acelaşi obiect ţine eventual de mai multe clase). Definiţia Python-Django constă în principal în:
class Clasa(models.Model):
    cod = models.CharField(max_length=2, unique=True) # 1A pt. IX-A, 2A pt. X-A, etc.
    nume = models.CharField(max_length=4) # exemplu: ixA, xA, xiA, xiiA
    obiects = models.ManyToManyField(Obiect, blank=True)
    
    def lista_obiecte(self):
        return [ob.id_arob() for ob in self.obiects.all()]
unde id_arob() este o metodă definită de Obiect pentru a returna un tuplu conţinând valoarea câmpului 'id' al obiectului respectiv şi un şir care concatenează acronimul de Arie (de care ţine obiectul) cu numele acelui obiect (de exemplu: (4, "1[LC] Română") pentru obiectul cu 'id'=4).
Clasa.objects.get(cod='2E').lista_obiecte() ne dă lista acestor tupluri (deci lista obiectelor) pentru clasa care are codul '2E'.
Elev(nume, pren, cnp, foto, clasa)
fiecare elev ţine de o anumită clasă (şi numai de una).
Medan(med1=0, med2=0, obiect, elev)
sintetizează (sau înscrie direct) media unui Elev la un Obiect, pe fiecare semestru şcolar.

În Django, fiecare model ("model de date") este un obiect Python care reprezintă (de regulă) o înregistrare dintr-un tabel al unei baze de date. Astfel, Medan.objects.create(med1=9, obiect_id=4, elev_id=1) creează un obiect Medan şi inserează în tabelul "stare_medan" o înregistrare ca (55, 9, 0, 4, 1) - unde valorile corespund respectiv câmpurilor "id" (întreţinut automat ca identificator unic al înregistrării, în cadrul tabelului), "med1", "med2" (=0, fiind eludat în .create()), "obiect_id" (care este "id"-ul unei anumite înregistrări din Obiect) şi "elev_id" (care identifică Elev-ul). Numele tabelului este constituit (implicit) din numele aplicaţiei Django ("stare") în cadrul căreia s-a definit modelul şi din numele modelului (rezultând "stare_medan").

Pentru operaţii la nivel de tabel (nu de înregistrare individuală), Django instituie un "manager de tabel"; acest obiect este accesibil prin intermediul clasei care defineşte modelul (şi nu direct din cadrul acestuia), folosind atributul .objects şi invocând apoi una dintre metodele definite de clasa Manager. Astfel, elevi = Elev.objects.all() obţine lista tuturor obiectelor Elev (corespunzătoare înregistrărilor existente în tabelul "stare_elev"); să observăm că aceasta nu este acelaşi lucru cu instrucţiunea SQL: SELECT * FROM stare_elev - aceasta obţine datele "brute" din tabelul respectiv, în timp ce în variabila elevi am obţinut direct "modelelele Python" ale acestor date "brute".

Ca să vedem ce putem face cu un obiect, sau ce putem cere unui obiect să facă - Python ne oferă funcţii de inspectare precum dir(). Presupunând că definiţia pentru Arie.objects este accesibilă din sesiunea de lucru curentă în care am lansat interpretorul Python, putem obţine lista atributelor şi metodelor disponibile prin .objects; iată câteva dintre aceste metode:

print dir(Arie.objects)
[ # ...
 'create',
 'all',
 'filter',
 'exclude',
 'get',
 'aggregate',
# ... 
]

Numele metodei sugerează deja ce anume face aceasta; astfel, Arie.objects.all() furnizează lista tuturor obiectelor Arie (corespunzător înregistrărilor existente în tabelul "stare_arie").

Un Manager pentru tabelul mediilor

Pentru a calcula media generală a elevului (angajând toate obiectele Medan asociate acelui Elev), media generală a clasei pe obiecte şi pe arii curriculare (ceea ce angajează toate obiectele Medan asociate elevilor acelei clase), etc. - definim metodele de calcul corespunzătoare în cadrul clasei Manager (pentru Medan):

from django.db.models import Avg
class MedanManager(models.Manager):
    def mg_arii(self, cl_cod='2E'):
        arii = Arie.objects.all()
        medans = self.filter(elev__clasa__cod=cl_cod)
        return [[ac.acro, 
                 medans.filter(obiect__arie=ac).aggregate(Avg('med1'), Avg('med2'))] 
                for ac in arii]
    
    def mg_obiecte(self, cl_cod='2E'):
        obiecte = Clasa.objects.get(cod=cl_cod).obiects.all()
        medans = self.filter(elev__clasa__cod=cl_cod)
        return [[ob.id_arob()[1], 
                 medans.filter(obiect=ob).aggregate(Avg('med1'), Avg('med2'))] 
                for ob in obiecte]

class Medan(models.Model):
    med1 = models.FloatField(default=0)
    med2 = models.FloatField(default=0)
    obiect = models.ForeignKey(Obiect)
    elev = models.ForeignKey(Elev)
    
    objects = models.Manager() # pentru a accesa Manager-ul de bază (instituit de Django)
    med_gen = MedanManager()   # Manager-ul propriu (accesibil prin .med_gen) 

Acum, Medan.med_gen.mg_arii('1A') ar trebui să ne dea lista mediilor generale pe ariile curriculare, pentru clasa '1A'.

Testarea directă, din linia de comandă

Pentru experimente şi testări "din linia de comandă", Django oferă utilitarul manage.py; cu opţiunea shell, acesta va deschide o sesiune de lucru cu IPython - "An enhanced Interactive Python". Fiindcă preferăm să folosim interpretorul Python obişnuit, creem un script în acelaşi director cu "manage.py" în care definim întâi "variabila de mediu" DJANGO_SETTINGS_MODULE astfel încât să putem importa din aplicaţia noastră ceea ce ne-ar trebui:

# helper.py (în acelaşi director cu manage.py)
import os
import sys
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "larcms.settings")

from stare.models import *
print Medan.med_gen.mg_arii('2E')
# print Medan.med_gen.mg_obiecte('2E')

larcms/settings.py defineşte între altele, DATABASES - dicţionar care precizează "driverul" de baze de date folosit, numele bazei de date, etc.; precizăm pentru ceea ce urmează, că folosim MySQL.

Obţinem lista mediilor pe arii curriculare (pentru clasa '2E'):

vb@vb:~/canrug$ python helper.py 
[['1[LC]', {'med1__avg': 7.505376344086022, 'med2__avg': 0.0}],
 ['2[MS]', {'med1__avg': 7.129032258064516, 'med2__avg': 0.0}],
 ['3[OS]', {'med1__avg': 8.241935483870968, 'med2__avg': 0.0}],
 ['4[TH]', {'med1__avg': 10.0, 'med2__avg': 0.0}],
 ['5[SP]', {'med1__avg': 10.0, 'med2__avg': 0.0}],
 ['6[AR]', {'med1__avg': None, 'med2__avg': None}],
 ['7[CO]', {'med1__avg': 9.96774193548387, 'med2__avg': 0.0}]]

Fiecare element al acestei liste este o listă formată din acronimul ariei şi un dicţionar de medii (în care cheile sunt îmbinări ale numelor câmpurilor din Medan cu sufixul "__avg" adăugat de metoda .aggregate(Avg(...))). Decomentând ultima linie din "helper.py" redat mai sus (şi relansând scriptul), obţinem şi lista mediilor generale pe obiecte, pentru clasa respectivă.

Eleganţă (sau fluenţă) versus eficienţă

Django promovează exprimarea fluentă (în fond, "orientată pe obiect") şi uşor de "prins" a unor lucruri care de fapt sunt complicate, implicând multe alte lucruri. De exemplu, pentru a obţine un dicţionar conţinând media generală pe aria curriculară '1[LC]' (pe toţi elevii, indiferent de clasă) - avem următoarea formulare, concisă şi elegantă: Medan.objects.filter(obiect__arie__acro='1[LC]').aggregate(Avg('med1')); termenii folosiţi exprimă exact operaţiile necesare: se filtrează tabelul pentru "înregistrările" care corespund ariei curriculare '1[LC]' şi acestea sunt agregate într-o valoare unică, reprezentând media - "the average", de unde şi termenul "Avg" - valorilor din câmpul 'med1'.

Formularea tocmai exemplificată s-ar "traduce" în SQL astfel:

SELECT AVG(stare_medan.med1) AS med1__avg FROM stare_medan 
    INNER JOIN stare_obiect ON (stare_medan.obiect_id = stare_obiect.id) 
    INNER JOIN stare_arie ON (stare_obiect.arie_id = stare_arie.id) 
    WHERE stare_arie.acro = '1[LC]'

şi desigur că preferăm formularea Django - concisă, expresivă şi valabilă pentru orice "motor" de baze de date (MySQL, SQLite, postgreSQL, etc.) pentru care Django a înregistrat un "driver".

Însă lucrurile se schimbă, dacă avem de obţinut mediile pe toate ariile curriculare (nu numai pentru '1[LC]', ca în formularea de mai sus). Putem vedea secvenţele SQL în care sunt convertite formulările Django din scriptul "helper.py" redat mai sus, consultând connection.queries - o listă de dicţionare {'sql': 'SELECT ...', 'time': '0.002'} în care sunt înregistrate toate interogările efectuate (în cursul cererii HTTP curente, sau în "sesiunea curentă" de lucru) asupra bazei de date:

# adăugare in "helper.py" (având DEBUG = True în larcms/settings.py) 
from django.db import connection
print connection.queries

.med_gen.mg_arii() (invocată în "helper.py") constituie lista de medii implicând câte o secvenţă SQL ca aceea redată mai sus, pentru fiecare arie curriculară; iar .med_gen.mg_obiecte() necesită pentru fiecare obiect câte două secvenţe SQL analoage celeia redate mai sus (două: pentru fiecare obiect s-a invocat şi metoda "id_arob()" a acestuia, necesitând o selectare prealabilă din 'stare_arie').

În loc de a "săpa" după o formulare Django care să reducă numărul aşa de mare de accesări ale bazei de date, constatat în cazul de faţă - este de preferat să înlocuim mg_arii() şi mg_obiecte() cu o formulă SQL care eventual, să obţină valorile respective "deodată"; desigur, SQL-ul respectiv ţine acum (spre deosebire de formulările Django) de "motorul" de baze de date folosit (trebuind eventual să fie adaptat, dacă la un moment dat am decide să folosim postgreSQL în loc de MySQL).

O formulă SQL pentru obţinerea mediilor pe arii şi pe obiecte

Folosind clauza GROUP BY cu modificatorul WITH ROLLUP (vezi manual MySQL) putem obţine "într-un singur pas" mediile generale pe fiecare obiect şi pe fiecare arie curriculară, pentru elevii unei clase specificate (în plus - şi media generală a clasei):

SELECT  a.acro AS Arie, o.nume AS Obiect, 
        COUNT(*) AS Elevi, AVG(m.med1) AS Medie 
    FROM stare_arie AS a, stare_obiect AS o, stare_medan AS m 
    WHERE m.elev_id IN (SELECT id FROM stare_elev 
                            WHERE stare_elev.clasa_id = 1)
          AND o.id = m.obiect_id 
          AND a.id = o.arie_id 
    GROUP BY a.id, m.obiect_id 
    WITH ROLLUP;

Înscriind această formulare într-un fişier "helper.sql" şi indicându-l interpretorului mysql (pentru baza de date "canrug"), obţinem (redând cu mici intervenţii de formatare/comentare):

vb@vb:~/canrug/DOC$ mysql canrug < helper.sql
Arie  Obiect      Elevi   Medie
1[LC] Română      31      7.32258064516129
1[LC] Engleză     31      6.967741935483871
1[LC] Franceză    24      8
1[LC] Italiană    7       9
1[LC] Italiană    93      7.505376344086022    # media ariei 1[LC]

2[MŞ] Matematică  31      5.580645161290323
2[MŞ] Fizică      31      7.032258064516129
2[MŞ] Chimie      31      8.580645161290322
2[MŞ] Biologie    31      7.32258064516129
2[MŞ] Biologie    124     7.129032258064516    # media ariei 2[MŞ]

3[OS] Istorie     31      7.516129032258065
3[OS] Geografie   31      7.967741935483871
3[OS] Psihologie  31      8
3[OS] Religie     31      9.483870967741936
3[OS] Religie     124     8.241935483870968    # media ariei 3[OS]

4[TH] Antreprenor 31      10
4[TH] TIC         31      10
4[TH] TIC         62      10    # media ariei 4[TH]

5[SP] Ed. fizică  30      10
5[SP] Ed. fizică  30      10    # media ariei 5[SP]

7[CO] Purtare     31      9.96774193548387
7[CO] Purtare     31      9.96774193548387    # media ariei 7[CO]

7[CO] Purtare     464     8.260775862068966    # media generală a clasei

Coloana "Elevi" nu reprezintă numărul de elevi ai clasei, ci numărul de obiecte Medan (asociate elevilor) care "intră" în calculul mediei respective; această informaţie suplimentară este utilă, pentru că la unele arii curriculare elevii pot opta pentru un obiect sau altul - lucru de care trebuie ţinut cont pentru a calcula corect media ariei respective.

De exemplu, la aria '1[LC]' media corectă nu este dată de suma mediilor pe 'română', 'engleză', 'franceză' şi 'italiană' (supra 4), pentru că media pe 'franceză' angajează numai 24 Elev (că numai atâţia elevi fac 'franceză'), iar media pe 'italiană' se face numai pe 7 elevi; media corectă a ariei '1[LC]' - şi aceasta a fost calculată de MySQL - este în cazul de faţă: (m_rom*31 + m_eng*31 + m_fra*24 + m_ita*7)/31, unde m_rom=7.32258064516129 (media pe 'română'), ş.a.m.d.

"ROLLUP" a adăugat câte o înregistrare pentru fiecare arie curriculară, după lista obiectelor acesteia - având însă aceleaşi valori în coloanele "Arie" şi "Obiect" ca şi ultima înregistrare din această listă; iar după ultima arie curriculară, a adăugat înregistrarea pentru media generală a clasei.

Media generală a clasei a fost calculată ca media tuturor celor 464 de valori "med1" din obiectele Medan asociate clasei; ea corespunde desigur cu media ponderată a mediilor pe ariile curriculare: (7.505376344086022*93 + 7.129032258064516*124 + 8.241935483870968*124 + 10*62 + 10*30 + 9.96774193548387*31)/464 = 8.260775862 - ceea ce nu este neapărat mai "uşor" de calculat, fiindcă implică şi înmulţiri (nu numai adunări, ca în primul caz).

Rescrierea Manager-ului pentru Medan

Putem folosi direct instrucţiunea SQL de mai sus, redefinind MedanManager() astfel:

class MedanManager(models.Manager):
    def medii_generale(self, cl_cod='2E'):
        from django.db import connection
        cursor = connection.cursor()
        cursor.execute("""
SELECT  a.acro, o.nume, COUNT(*), AVG(m.med1), AVG(m.med2) 
    FROM stare_clasa as k, stare_arie AS a, stare_obiect AS o, stare_medan AS m 
    WHERE k.cod = %s
          AND m.elev_id IN (SELECT id FROM stare_elev 
                                WHERE stare_elev.clasa_id = k.id)
          AND o.id = m.obiect_id 
          AND a.id = o.arie_id 
    GROUP BY a.id, m.obiect_id 
    WITH ROLLUP
""", [cl_cod])
        return cursor.fetchall()

Testând acum cu "helper.py" - în care am eliminat mg_arii() şi mg_obiecte(), înlocuind cu Medan.med_gen.medii_generale('2E') - obţinem un tuplu compus din câte un tuplu pentru fiecare obiect (plus tuplu-rile pentru mediile ariilor şi cel final pentru media clasei):

vb@vb:~/canrug$ python helper.py
((1[LC], Română, 31, 7.32258064516129, 0.0),
 (1[LC], Engleză, 31, 6.967741935483871, 0.0),
 (1[LC], Franceză, 24, 8.0, 0.0),
 (1[LC], Italiană, 7, 9.0, 0.0),
 (1[LC], Italiană, 93, 7.505376344086022, 0.0),
 (2[MŞ], Matematică, 31, 5.580645161290323, 0.0),
 # ş.a.m.d.
)

Tuplurile obţinute corespund rândurilor redate mai sus pentru "helper.sql" (înregistrând în plus - pe ultimul loc din tuplu - şi mediile pe câmpul "med2" din Medan).

Desigur că ne-ar putea interesa cel mult primele trei zecimale, din mediile redate; pentru a evita prelucrări ulterioare în cadrul view-urilor care ar angaja "managerul" de mai sus, trebuie să facem rotunjirile necesare chiar în MedanManager() (pentru a opera într-un singur loc); dar tuplu-rile sunt obiecte "imutabile" (nu pot fi modificate, în Python) - încât trebuie să constituim o listă (obiect "mutabil") în care să copiem şi să transformăm valorile respective.

Pentru aceasta, putem completa definiţia MedanManager() redată mai sus astfel:

        #return cursor.fetchall()
        tupluri = cursor.fetchall()
        result = [[tup[0], tup[1], int(tup[2]), round(tup[3], 3), round(tup[4], 3)] 
                  for tup in tupluri]
        return result

obţinând acum o listă de liste [['1[LC]', Română, 31, 7.323, 0.0], ..., ], care va putea fi redată ca atare (fără alte prelucrări).

Dar această listă ar trebui reorganizată sau reformulată - tot în cadrul ManagerMedan(), scutind view-urile - pentru a permite identificarea uşoară a acelor sub-liste care corespund mediilor pe ariile curriculare; fiecare dintre aceste sub-liste apare imediat după toate sublistele aferente obiectelor acelei arii (ceea ce am văzut mai sus). Pentru aceasta, adăugăm în MedanManager():

        #return result
        ar_ob_mg = {mg[0]: [] for mg in result}
        for acro in ar_ob_mg:
            ar_ob_mg[acro] = [[mg[1],mg[2],mg[3],mg[4]] 
                              for mg in result 
                              if mg[0]==acro]

        return ar_ob_mg        

Am introdus dicţionarul ar_ob_mg având drept chei acronimele arrilor curriculare (extrase de pe primul câmp al listelor conţinute de lista result obţinută anterior): ar_ob_mg = {'1[LC]': [], '2[MŞ]': [], etc.}. Apoi, pe cheile constituite astfel am înscris ca valori câte o listă constituită din acele sub-liste din result în care primul câmp are aceeaşi valoare ca şi cheia respectivă şi în final am returnat dicţionarul rezultat astfel:

vb@vb:~/canrug$ python helper.py 
{'1[LC]': [[Română', 31, 7.323, 0.0],
           [Engleză', 31, 6.968, 0.0],
           [Franceză', 24, 8.0, 0.0],
           [Italiană', 7, 9.0, 0.0],
           [Italiană', 93, 7.505, 0.0]],    # media ariei 1[LC]

 '2[MŞ]': [[Matematică', 31, 5.581, 0.0],
           [Fizică', 31, 7.032, 0.0],
           [Chimie', 31, 8.581, 0.0],
           [Biologie', 31, 7.323, 0.0],
           [Biologie', 124, 7.129, 0.0]],    # media ariei 2

 ### ş.a.m.d.

 '7[CO]': [[Purtare', 31, 9.968, 0.0],
           [Purtare', 31, 9.968, 0.0],    # media ariei 7
           [Purtare', 464, 8.261, 0.0]]}    # media clasei

Iată un exemplu de prelucrare a acestui dicţionar:

medii = Medan.med_gen.medii_generale('2E')
mg_clasa = medii['7[CO]'].pop() # ultima sub-listă (mediile generale ale clasei)
print "media generală a clasei: ", mg_clasa[2], mg_clasa[3]
for acro, m_list in medii.iteritems():
    arie = medii[acro][-1] # ultima sub-listă a ariei (mediile generale ale ariei curente)
    print acro + ': ', arie[2], arie[3] # 1[LC]: 7.505 0.0
    print m_list[:-1] # obiectele ariei curente şi mediile acestora

Adăugând secvenţa de mai sus în "helper.py", obţinem:

vb@vb:~/canrug$ python helper.py 
media generală a clasei:  8.261 0.0

3[OS]:  8.242 0.0   Mediile ariei OS
[['Istorie', 31, 7.516, 0.0], 
 ['Geografie', 31, 7.968, 0.0], 
 ['Psihologie', 31, 8.0, 0.0], 
 ['Religie', 31, 9.484, 0.0]]

1[LC]:  7.505 0.0   Mediile ariei LC
[['Română', 31, 7.323, 0.0], 
 ['Engleză', 31, 6.968, 0.0], 
 ['Franceză', 24, 8.0, 0.0],    24 elevi
 ['Italiană', 7, 9.0, 0.0]]     7 elevi
# ş.a.m.d.

Spre deosebire de liste (care păstrează implicit ordinea iniţială a obiectelor), dicţionarele (împerecheri neordonate de chei şi valori) nu păstrează ordinea cheilor (cum vedem şi pe listingul tocmai redat); dar ordonarea după chei poate fi lăsată pur şi simplu în seama şablonului HTML căruia i se va transmite dicţionarul (şi nu-i cazul de a o realiza în view), dat fiind că Django prevede modificatori "finali" corespunzători acestui scop.

Corelări şi factorizări

Modelele de date servesc celorlalte componente ale aplicaţiei; în principiu, cererile HTTP receptate de aplicaţie sunt pasate unor funcţii din views.py, iar acestea obţin datele solicitate accesând definiţiile modelelor de date din models.py şi le pasează (după o eventuală reorganizare) unui "şablon HTML" dintr-un director /templates, unde datele respective sunt încastrate în locurile prevăzute pentru ele - finalizându-se astfel pagina de răspuns la cererea primită.

Deci, discutând despre modele de date avem de vizat neapărat şi celelalte părţi ale aplicaţiei; mai mult - chiar conceperea definiţiilor din models.py trebuie să ţină seama de celelalte părţi.

Se cuvine astfel, să corijăm unele lucruri. De exemplu, ne-am cramponat mai sus de faptul că mediile ar trebui rotunjite chiar în MedanManager() şi în acest scop am implicat crearea unei noi liste, conţinând valorile rotunjite (acceptând tacit costurile acestei operaţii); dar aceasta este greşit: în principiu, rolul unei funcţii este acela de a furniza nişte rezultate şi este treaba celui care a apelat funcţia sau serviciul respectiv, să prelucreze mai departe sau să "afişeze" cum doreşte (în particular, rotunjind diverse valori), rezultatele respective.

Am formulat MedanManager() (pentru a obţine toate mediile dorite) implicând direct SQL - reducând practic la unul singur, numărul de accesări la baza de date; dar l-am tot "rescris", eliminând return cursor.fetchall(), apoi şi return result (unde 'result' era lista valorilor rotunjite, şi-atât) - ajungând în final la return ar_ob_mg (care constituie o organizare mai convenabilă a rezultatelor).

Aceste demersuri din aproape în aproape sunt oarecum fireşti, pe parcursul punerii la punct a lucrurilor. Dar iarăşi trebuie să vedem că în principiu, trebuia preferat return cursor.fetchall() - prelucrarea rezultatelor fiind sarcina celui care le-a cerut (şi care ştie ce are de făcut cu ele).

Desigur, sunt de luat în seamă diverse nuanţări faţă de "principiul" evocat. Astfel, în cazul aplicaţiei de faţă - este clar că mediile obţinute trebuie (în primul rând) să fie pur şi simplu "afişate", într-un format uşor de intuit; deci organizarea datelor în acest scop poate fi înglobată foarte bine şi în cadrul lui MedanManager() (ceea ce am şi făcut mai sus, scutind apelanţii posibili).

Dar mai este un aspect, pe care l-am neglijat: mediile obţinute mai sus ţin toate de clasa de elevi specificată - ori este necesar să prevedem cam acelaşi calcul şi pentru fiecare elev specificat: media generală a elevului, mediile generale pe arii curriculare ale sale, etc. Acest nou calcul, particular unui elev, poate fi "montat" în modelul Elev (dintr-o instanţă "elev" de Elev putem accesa setul de Medan asociate elevului respectiv, folosind elev.medan_set.all()).

Prin urmare, organizarea rezultatelor fiind cam aceeaşi - aceasta ar trebui concepută separat de "managerii" respectivi, într-o funcţie care să fie accesibilă ambelor situaţii. Altfel spus, ambii "manageri" trebuie să se încheie cu return _dict_medii(tupluri) - unde 'tupluri' este ca mai sus rezultatul returnat de SQL-ul "SELECT ... GROUP BY ... WITH ROLLUP", iar _dict_medii() este acea funcţie care organizează într-un dicţionar convenabil afişării, rezultatele respective:

# în "stare/models.py" (încât este accesibilă din oricare model)
def _dict_medii(rollup):
    arii = [mg[0] for mg in rollup] # ['1[LC]', ..., '7[CO]']
    arome = {} # organizare prealabilă
    for ar in arii:
        arome[ar] = [mg[1:] for mg in rollup if mg[0]==ar]
    dimed = {} # organizarea definitivă
    last_ar = arome[arii[-1]].pop() # ultima sub-listă din 'ultima' arie
    dimed['general'] = last_ar[-2:] # mediile generale clasă (sau elev)
    for ar, med in arome.iteritems():
        dimed[ar] = {}
        m_arie = med[-1] # ultima sub-listă din aria curentă
        dimed[ar]['general'] = m_arie[-2:] # medii generale Arie
        dimed[ar]['obiecte'] = med[:-1]   # liste cu Obiect, [Nr-elevi], medii
    return dimed

Într-un "exemplu de prelucrare" de mai sus foloseam medii['7[CO]'].pop() pentru a selecta ultima sub-listă (mediile generale ale clasei) din 'rollup'; dar nu neapărat '7[CO]' este ultima arie din listă (mediile trebuie furnizate în orice moment al anului şcolar, deci poate că mediile la "Purtare" încă nu există). De data aceasta am creat lista 'arii' conţinând acronimele ariilor conţinute de 'rollup'; arii[-1] va selecta ultima componentă (care o fi aceasta, nu neapărat '7[CO]').

Foloseam anterior [mg[1],mg[2],mg[3],mg[4]] pentru a constitui lista curentă [obiect, Nr. elevi, medie I, medie II]; dar în cazul unui elev (nu al clasei de elevi) lipseşte "Nr. elevi" (numărul elevilor care fac obiectul respectiv). Am folosit deci mg[1:] - ceea ce creează lista componentelor de la rangul 1 încolo (patru în cazul clasei de elevi, numai trei în cazul unui elev).

vb@vb:~/canrug$ python helper.py 
# mediile clasei de elevi
{
 'general': (8.260775862068966, 0.0), # medii generale clasă
 '1[LC]': {
           'general': (7.505376344086022, 0.0), # mediile ariei/clasa  
           'obiecte': [('Română', 31, 7.32258064516129, 0.0),
                       ('Engleză', 31, 6.967741935483871, 0.0),
                       ('Franceză', 24, 8.0, 0.0), # 24 elevi, medii obiect
                       ('Italiană', 7, 9.0, 0.0)] # 7 elevi, medii obiect
          },
 # etc. (celelalte arii)
}
# mediile unui elev
{
 'general': (7.666666666666667, 0.0), # medii generale elev
 '1[LC]': {
           'general': (7.0, 0.0), # mediile ariei/elev
           'obiecte': [('Română', 8.0, 0.0), 
                       ('Engleză', 6.0, 0.0), # medii obiect
                       ('Franceză', 7.0, 0.0)]
          },
 # etc. (celelalte arii)
}

Redat mai sus este rezultatul trunchiat al testării funcţiei create mai sus, pentru o clasă de elevi şi respectiv pentru un elev = Elev.objects.filter(clasa__cod='2E')[0]; print elev.medii() - unde medii() este funcţia din modelul Elev care furnizează 'rollup'-ul (folosind un SELECT ... GROUP BY ... WITH ROLLUP asemănător celui constituit în cazul clasei de elevi) pentru elevul respectiv.

vezi Cărţile mele (de programare)

docerpro | Prev | Next