11. Turul pe scurt al bibliotecii standard --- Partea II
********************************************************

În partea a doua a turului ne vom referi la module avansate care
deservesc nevoile programatorului profesionist. Nu este de așteptat ca
astfel de module să apară prea des în scripturile de dimensiuni mici.


11.1. Formatarea ieșirii
========================

Modulul "reprlib" ne pune la dispoziție o versiune a funcției "repr()"
adaptată la afișările cu *abrevieri de conținut* atât pentru
containere masive cât și pentru containerele cu imbricări multiple
(stratificate):

   >>> import reprlib
   >>> reprlib.repr(set('pneumonoultramicroscopicsilicovolcaniconioza'))
   "{'a', 'c', 'e', 'i', 'l', 'm', ...}"

Modulul "pprint" oferă un control (încă și) mai sofisticat asupra
afișării atât a obiectelor predefinite cât și a celor definite de
utilizator, control care le face pe acestea lizibile pentru
interpretor. În caz că rezultatul va depăși lungimea unui rând,
"tipograful îngrijit" (sau *realizatorul de aranjări sugestive*; de la
englezescul pretty printer) îi va adăuga caractere sfârșit-de-rând și
indentări pentru a revela cu claritate structura datelor (ce trebuie
afișate):

   >>> import pprint
   >>> paletă = [[[['negru', 'turcoaz'], 'alb', ['verde',
   ...           'roșu']], [['fucsia', 'galben'], 'albastru']]]
   ...
   >>> pprint.pprint(paletă, width=30)
   [[[['negru', 'turcoaz'],
      'alb',
      ['verde', 'roșu']],
     [['fucsia', 'galben'],
      'albastru']]]

Modulul "textwrap" formatează paragrafele de text pentru ca acestea să
încapă pe un ecran (de terminal) cu lungimea dată:

   >>> import textwrap
   >>> documentul = """Metoda wrap() este la fel cu metoda fill() doar că returnează
   ... o listă de șiruri de caractere în locul unui șir de caractere foarte lung care
   ... conține caractere sfârșit-de-rând pentru a putea să delimiteze rândurile
   ... în care a fost împărțit."""
   ...
   >>> print(textwrap.fill(documentul, width=40))
   Metoda wrap() este la fel cu metoda
   fill() doar că returnează o listă de
   șiruri de caractere în locul unui șir de
   caractere foarte lung care conține
   caractere sfârșit-de-rând pentru a putea
   să delimiteze rândurile în care a fost
   împărțit.

Modulul "locale" accesează o bază de date (POSIX) cu formate de date
specifice diverselor *culturi*. Atributul de *grupare* al funcției de
format din (modulul) *locale* ne pune la îndemână o modalitate
imediată de formatare a numerelor cu ajutorul separatoarelor de grup:

   >>> import locale
   >>> locale.setlocale(locale.LC_ALL, 'English_United States.1252')
   'English_United States.1252'
   >>> # Obțineți o mapare a convențiilor (obiceiurile locale):
   >>> convenții = locale.localeconv()
   >>> numărul = 1234567.8
   >>> locale.format_string("%d", numărul, grouping=True)
   '1,234,567'
   >>> locale.format_string("%s%.*f", (convenții['currency_symbol'],
   ...                      convenții['frac_digits'], numărul),
   ...                      grouping=True)
   '$1,234,567.80'


11.2. Folosirea șabloanelor
===========================

Modulul "string" include o clasă "Template" versatilă a cărei sintaxă
simplificată o face ușor de întrebuințat de către utilizatori. Cu
ajutorul său, aceștia pot să-și personalizeze aplicațiile fără a fi
nevoiți să opereze modificări în codul de bază al respectivelor
aplicații.

Formatul (din șablon) utilizează *nume de înlocuire* construite din
identificatori valizi în Python (adică, alcătuiți doar din caractere
alfanumerice și din caractere bară jos; atenție, doar caractere ASCII)
pe care le prefațăm cu "$". Numele de înlocuire se încadrează cu
acolade, ceea ce le permite să fie urmate de (și mai multe) litere
alfanumerice, însă acestea *fără spații goale* printre ele. Pentru
escaparea unui caracter "$", el va fi inserat sub forma "$$":

   >>> from string import Template
   >>> șablonul = Template("Amice-din-orașul-${localitatea}City, "
   ...                     "dă și matale $$10 pe $visul_nostru.")
   >>> șablonul.substitute(localitatea='Medgidia',
   ...                     visul_nostru='apa gârlei')
   'Amice-din-orașul-MedgidiaCity, dă și matale $10 pe apa gârlei.'

Metoda "substitute()" ridică o excepție "KeyError" dacă vreun nume de
înlocuire nu îi va fi furnizat fie de către un argument de tip
dicționar de date fie de un argument cuvânt-cheie. În cazul unor
aplicații cu documente construite în stilul Mail Merge (îmbinare-de-
mesaje) survin situații când datele oferite de utilizator rămân
incomplete, din care motiv utilizarea metodei "safe_substitute()" ar
putea fi (mai) potrivită --- căci aceasta nu va modifica numele de
înlocuire dacă lipsesc datele corespunzătoare respectivelor nume.
Exemplul de mai jos face referire la episodul cu rândunica (Monty
Python):

   >>> șablonul = Template('De înapoiat $colet la $expeditor.')
   >>> dicționarul = dict(colet='rândunica fără nimica')
   >>> șablonul.substitute(dicționarul)
   Traceback (most recent call last):
     ...
   KeyError: 'expeditor'
   >>> șablonul.safe_substitute(dicționarul)
   'De înapoiat rândunica fără nimica la $expeditor.'

Urmașele clasei *Template* își pot personaliza *delimitatorul*. De
exemplu, un script utilitar de redenumire a unor loturi de fotografii,
atașat unui album fotografic, poate întrebuința semnul grafic
*procent* pe post de caracter de înlocuire pentru șiruri de caractere
precum data calendaristică, numărul (de ordine în lot al) fișierului
foto, respectiv extensia fișierului:

   >>> import string, time, os.path
   >>> fișiere_foto = ['img_1074.jpg', 'img_1076.jpg', 'img_1077.jpg']
   >>> class RedenumeșteLotul(string.Template):
   ...     delimiter = '%' # suprascriem atributul delimiter
   ...
   >>> formatul = input("Introduceți stilul redenumirii "
   ...                  "(%d-data %n-numărul %f-extensia_fișierului):  ")
   Introduceți stilul redenumirii (%d-data %n-numărul %f-extensia_fișierului):  Viorica_%n%f

   >>> șablonul = RedenumeșteLotul(formatul)
   >>> data = time.strftime('%d%b%y')
   >>> for i, nume_de_fișier in enumerate(fișiere_foto):
   ...     baza, extensia = os.path.splitext(nume_de_fișier)
   ...     noul_nume = șablonul.substitute(d=data, n=i, f=extensia)
   ...     print('{0} --> {1}'.format(nume_de_fișier, noul_nume))

   img_1074.jpg --> Viorica_0.jpg
   img_1076.jpg --> Viorica_1.jpg
   img_1077.jpg --> Viorica_2.jpg

Ceea ce face șabloanele atât de utile în practică este chiar
caracteristica esențială a acestora, și anume *separarea* logicii unui
program de afișarea rezultatelor execuției lui. Astfel, pot fi
construite șabloane personalizate pentru introducerea convenabilă a
acestor rezultate în fișiere XML, în rapoarte sub formă de text
simplu, ori în rapoarte HTML destinate Internetului.


11.3. Lucrul cu machete ale înregistrărilor de date binare
==========================================================

Modulul "struct" ne pune la dispoziție funcțiile "pack()" și
"unpack()" pentru a putea manipula formate de înregistrări binare de
lungimi diverse. Exemplul de mai jos ne arată cum putem parcurge
informația din antetul unui fișier ZIP fără a face uz de modulul
"zipfile". Codurile de arhivare (de la englezescul *pack*) ""H"" și
""I"" reprezintă numere întregi fără semn (cu lungime) de doi și
respectiv de patru octeți. Semnul ""<"" ne arată că datele au lungime
standard și sunt dispuse în formatul de stocare inversat (sau de
*memorare inversată*; de la englezescul, ca jargon informatic,
*little-endian*):

   >>> import struct
   >>> # Fișierul fișierul_meu.zip este arhiva ZIP a (minim) 3 fișiere text:
   >>> with open('fișierul_meu.zip', 'rb') as f:
   ...    datele = f.read()
   ...
   >>> început = 0
   >>> for i in range(3):                # afișează primele 3 antete
   ...                                   # de fișier
   ...    început += 14
   ...    câmpuri = struct.unpack('<IIIHH', datele[început:început+16])
   ...    crc32 \
   ...    ,mărime_comprimate \
   ...    ,mărime_necomprimate \
   ...    ,mărime_numedefișier \
   ...    ,mărime_suplimentar = câmpuri
   ...    început += 16
   ...    numedefișier = datele[început:început+mărime_numedefișier]
   ...    început += mărime_numedefișier
   ...    suplimentar = datele[început:început+mărime_suplimentar]
   ...    print(numedefișier \
   ...          ,hex(crc32) \
   ...          ,mărime_comprimate \
   ...          ,mărime_necomprimate)
   ...    început += mărime_suplimentar \
   ...             + mărime_necomprimate # trecem la antetul următor


11.4. Execuții multifilare
==========================

Execuția *pe mai multe fire* (sau *multifir* ori *multifilară*; de la
englezescul *threading*) a unui program este o tehnică de organizare a
execuției acestuia prin care se decuplează *sarcinile* (sau
*subunitățile*; de la englezescul *task*) programului care nu depind
(în mod direct) una de cealaltă. *Firele de execuție* (numite și
*procese de categorie ușoară*) pot fi întrebuințate pentru a
îmbunătăți *receptivitatea* unei aplicații la acțiunile utilizatorului
său în timp ce execuția sarcinilor de durată se desfășoară *în fundal*
(asincron). Un caz de utilizare relevant este cel al *rulării*
(îndeplinirii) unor sarcini I/E (mai lente, de obicei) *în paralel* cu
realizarea de sarcini de calcul, acestea din urmă fiind efectuate în
alt fir de execuție.

Fragmentul de cod care urmează ne arată cum poate modulul de nivel
înalt "threading" să execute sarcini în fundal pe când programul
principal rulează (în prim-plan):

   import threading, zipfile

   class ArhivareaZipAsincronă(threading.Thread):
       def __init__(self, fișier_de_intrare, fișier_de_ieșire):
           threading.Thread.__init__(self)
           self.fișier_de_intrare = fișier_de_intrare
           self.fișier_de_ieșire = fișier_de_ieșire

       def run(self):    # suprascriem metoda run() a clasei Thread
           f = zipfile.ZipFile(self.fișier_de_ieșire, 'w', \
                               zipfile.ZIP_DEFLATED)
           f.write(self.fișier_de_intrare)
           f.close()
           print('Terminat arhivarea zip, în fundal, a lui:', \
                 self.fișier_de_intrare)

   în_fundal = ArhivareaZipAsincronă('datele_mele.txt', 'arhiva_mea.zip')
   în_fundal.start()
   print('Programul principal rulează (în continuare) în prim-plan.')

   în_fundal.join()    # Așteptăm să se încheie sarcina de fundal
   print('Programul principal a așteptat încheierea sarcinii de fundal.')

Dificultatea principală a aplicațiilor *multifir* (adică, a
aplicațiilor cu execuție pe mai multe fire) este cea a *coordonării*
acestor fire de execuție vizavi de accesul la date ori la alte resurse
(memorie, șamd.). În acest scop, modulul *threading* ne furnizează un
număr de *primitive de sincronizare*, precum *blocări* (sau *zăvoare*;
de la englezescul *lock*), evenimente, variabile de control și
semafoare.

Chiar dacă aceste unelte sunt puternice, erorile de proiectare (fie
și) minore ne pot pune în fața unor complicații dificil de reprodus
(în mod sistematic). Din acest motiv, abordarea preferată în practică
în ceea ce privește coordonarea sarcinilor este să realizăm (toată)
accesarea resurselor într-un singur fir de execuție și să utilizăm
modulul "queue" pentru a *hrăni* acest fir cu *cereri* din partea
celorlalte fire de execuție. Aplicațiile care întrebuințează obiecte
"Queue" pentru *comunicarea între fire* (sau *comunicarea
interprocese*) și pentru coordonarea acestora sunt (mai) ușor de
proiectat, (mai) stabile și au codul-sursă (mai) facil de citit.


11.5. Jurnalizarea
==================

Modulul "logging" ne pune la dispoziție un sistem de jurnalizare
flexibil și complet accesorizat. Folosite în modul cel mai simplu cu
putință, mesajele de (introdus în) jurnal îi sunt transmise fie unui
fișier (prestabilit) fie (fluxului) "sys.stderr":

   import logging
   logging.debug('Informații de depanare')
   logging.info('Mesaj de informare')
   logging.warning('Avertisment: fișierul de configurare %s negăsit', 'serverul.conf')
   logging.error('Eroare survenită')
   logging.critical('Eroare critică -- opresc sistemul')

Iată ce se va afișa:

   WARNING:root:Avertisment: fișierul de configurare serverul.conf negăsit
   ERROR:root:Eroare survenită
   CRITICAL:root:Eroare critică -- opresc sistemul

În mod prestabilit, atât mesajele informative cât și cele de depanare
sunt suprimate (de la jurnalizare) în timp ce ieșirea (conținutul) lor
se trimite către fluxul standard de eroare. Alte opțiuni de ieșire
includ *distribuirea* (sau *rutarea*; de la englezescul, ca jargon
informatic, *routing*) mesajelor prin poșta electronică, datagrame,
socluri, ori către un server HTTP. Filtre suplimentare permit alegerea
unor distribuiri bazate pe *prioritatea mesajelor*: "DEBUG", "INFO",
"WARNING", "ERROR", precum și "CRITICAL".

Sistemul de jurnalizare se poate configura atât direct din Python cât
și indirect, prin încărcarea setărilor dintr-un fișier de configurare
care poate fi editat de către utilizator, permițându-se astfel
jurnalizări personalizate a căror introducere să nu necesite
modificarea codului-sursă de bază al aplicației.


11.6. Referințe slabe
=====================

Python-ul își administrează memoria în mod automat (realizând atât
*contorizarea referințelor* pentru majoritatea obiectelor cât și
*colectarea gunoiului* pentru eliminarea *circularităților*). Orice
zonă de memorie (rezervată) va fi eliberată la puțin timp după ce a
fost eliminată (și) ultima referință la ea.

O atare abordare funcționează mulțumitor pentru cele mai multe dintre
aplicații, însă câteodată este nevoie ca urmărirea anumitor obiecte să
fie realizată doar atâta vreme cât ceva anume (din sistem) le
întrebuințează. Din păcate, simpla urmărire a unui obiect creează o
referință la acesta, care referință îl va face permanent (nemuritor).
Modulul "weakref" conține unelte pentru urmărirea unor obiecte fără să
se creeze nicio referință la ele. Atunci când un obiect anume nu mai
este folosit, el va fi eliminat automat dintr-o tabelă de *referințe
slabe* (de la englezescul, ca jargon informatic, *weakref*) și o
*rutină de răspuns* (sau un *apel de răspuns*; de la englezescul, ca
jargon informatic, *callback*) va fi declanșată pentru respectivul
obiect *weakref*. Întrebuințările tipice ale referințelor slabe includ
*memorarea locală rapidă* (de la englezescul, ca jargon informatic,
*caching*) a obiectelor care sunt costisitor de creat:

   >>> import weakref, gc
   >>> class A:
   ...     def __init__(self, valoarea):
   ...         self.valoarea = valoarea
   ...     def __repr__(self):
   ...         return str(self.valoarea)
   ...
   >>> a = A(10)                            # creăm o referință (x)
   >>> dicționarul = weakref.WeakValueDictionary()
   >>> dicționarul['referință primară'] = a # nu creează nicio referință
   >>> dicționarul['referință primară']     # găsește-l, dacă e în viață
   10
   >>> del a                                # elimină referința (x)
   >>> gc.collect()                         # colectează gunoiul (chiar acum)
   0
   >>> dicționarul['referință primară']     # eliminat automat din tabelă
   Traceback (most recent call last):
     File "<stdin>", line 1, in <module>
       dicționarul['referință primară']     # eliminat automat din tabelă
     File "C:/python313/lib/weakref.py", line 46, in __getitem__
       o = self.data[key]()
   KeyError: 'referință primară'


11.7. Unelte pentru lucrul cu liste
===================================

Multe din cerințele unor structuri (abstracte) de date pot fi
deservite de tipul predefinit (de date) *listă*. Cu toate acestea, în
anumite situații este nevoie de implementări alternative ale unei
structuri de date, punându-se în balanță, la alegerea lor,
*performanțe* specifice.

Modulul "array" dispune de obiectul "array" (adică, tipul *tablou*,
*vector* ori *matrice*) care îi seamănă listei, doar că nu poate stoca
decât date de același tip (omogene) iar stocarea propriu-zisă se face
(mai) compact. Exemplul de mai jos prezintă un tablou de numere
întregi stocate sub formă de numere binare fără semn (lungi) de câte
doi octeți (și având *codul de tip* ""H""), aceasta spre deosebire de
cazul listelor Python uzuale formate din obiecte *int*, în care
fiecare item este stocat pe câte 16 octeți:

   >>> from array import array
   >>> tablou = array('H', [4000, 10, 700, 22222])
   >>> sum(tablou)
   26932
   >>> tablou[1:3]
   array('H', [10, 700])

Modulul "collections" ne furnizează obiectul "deque" (adică, tipul
*deque* ori *coadă cu două capete*) care îi seamănă listei, doar că
acceptă (de la englezescul *append*) și elimină (de la englezescul
*pop*) itemi la și de la capete mai repede decât o poate face o listă,
respectiv caută itemi (de la englezescul *lookup*) mai încet decât
poate o listă atunci când itemii respectivi se găsesc în interiorul
(mijlocul) său. Instanțele acestui tip sunt cum nu se poate mai
potrivite pentru implementarea de *cozi* (FIFO, LIFO), respectiv de
*căutări în lățime* (a unor itemi) în diverși *arbori* de date:

   >>> from collections import deque
   >>> coada_cu_două_capete = deque(["sarcina1", "sarcina2", \
   ...                               "sarcina3"])
   >>> coada_cu_două_capete.append("sarcina4")
   >>> print("Mă ocup de ", coada_cu_două_capete.popleft())
   Mă ocup de sarcina1

   unde_caut = deque([nodul_de_început])
   def căutare_în_lățime(unde_caut):     # căutare breadth-first
       nodul = unde_caut.popleft()
       for c in ce_mi_am_propus(nodul):
           if este_țelul_meu(c):
               return c
           unde_caut.append(c)

În plus față de implementările alternative ale *listei*, biblioteca ne
pune la dispoziție și altfel de unelte, precum cele din modulul
"bisect" care conține funcții pentru manipularea listelor (deja)
*sortate*:

   >>> import bisect
   >>> scoruri = [(100, 'Perl'), (200, 'Tcl'), (400, 'Lua'), (500, 'Python')]
   >>> bisect.insort(scoruri, (300, 'Ruby'))
   >>> scoruri
   [(100, 'Perl'), (200, 'Tcl'), (300, 'Ruby'), (400, 'Lua'), (500, 'Python')]

Modulul "heapq" deține funcții pentru implementarea structurilor
arborescente *heap* (adică, generic, *grămadă*; se utilizează de multe
ori, în mod superficial, și expresia *coadă cu priorități*) folosind
*lista* obișnuită. Itemul cu *cea mai mică valoare* este păstrat
întotdeauna în poziția *zero*. Această caracteristică a structurilor
*heap* le face să fie de folos pentru aplicațiile în care accesăm în
mod frecvent elementul cu cea mai mică valoare (dintr-o listă de
valori) dar nu dorim să realizăm o sortare completă a listei:

   >>> from heapq import heapify, heappop, heappush
   >>> datele = [1, 3, 5, 7, 9, 2, 4, 6, 8, 0]
   >>> # reorganizăm lista pentru a avea o ordonare de heap:
   >>> heapify(datele)
   >>> heappush(datele, -5)               # introducem un item
   >>> # extragem cele mai mici trei valori:
   >>> [heappop(datele) for i in range(3)]
   [-5, 0, 1]


11.8. Aritmetică în virgulă mobilă pentru numere zecimale
=========================================================

Modulul "decimal" ne oferă tipul de date "Decimal", dedicat
aritmeticii zecimale în virgulă mobilă. Prin comparație cu
implementarea predefinită "float" a numerelor binare în virgulă
mobilă, clasa de față este realmente utilă pentru

* aplicațiile financiare ori alte aplicații în care se cer
  *reprezentări exacte* în baza zece ale datelor numerice,

* situațiile în care *controlul* (numerelor) este mai important decât
  *precizia* (cifrelor din dreapta virgulei),

* cazurile când *rotunjirea* valorilor numerice trebuie să țină seama
  de cerințe legale sau de alte reglementări specifice,

* monitorizea cifrelor zecimale *semnificative* din dreapta punctului
  (virgulei), respectiv pentru

* aplicațiile la care utilizatorul se așteaptă ca rezultatele
  diverselor operații să se potrivească cu cele obținute chiar de el
  în urma calculelor *făcute cu creionul pe hârtie*.

Cu titlu de exemplu, calculul unei taxe de 5% la valoarea de 70 de
cenți a unei facturi de convorbiri telefonice produce un rezultat
diferit, atunci când este efectuat în virgulă mobilă *binară*, de
rezultatul obținut în virgulă mobilă *zecimală*. Iar diferența nu mai
poate fi trecută cu vederea dacă rotunjim rezultatele până la
obținerea *celui mai apropiat* cent:

   >>> from decimal import *
   >>> round(Decimal('0.70') * Decimal('1.05'), 2)
   Decimal('0.74')
   >>> round(.70 * 1.05, 2)
   0.73

Rezultatul "Decimal" păstrează zerouri pe post de sufix, presupunând
în mod automat că, în urma multiplicării unor operanzi cu câte două
cifre semnificative la dreapta punctului, el va avea patru cifre
semnificative după punct. Clasa *Decimal* reproduce matematica făcută
cu creionul pe hârtie și evită, astfel, complicații care pot surveni
atunci când anumite cantități zecimale nu sunt reprezentabile exact cu
valori binare în virgulă mobilă.

Întrebuințarea de reprezentări exacte îi permite clasei "Decimal" să
realizeze atât împărțiri cu rest (calcule cu operatorul *modulo*) cât
și testări de egalități care nu sunt realizabile cu numere binare în
virgulă mobilă:

   >>> Decimal('1.00') % Decimal('.10')
   Decimal('0.00')
   >>> 1.00 % 0.10
   0.09999999999999995

   >>> sum([Decimal('0.1')]*10) == Decimal('1.0')
   True
   >>> 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 == 1.0
   False

În modulul "decimal", aritmetica poate fi efectuată cu *precizia*
stabilită de către utilizator:

   >>> getcontext().prec = 36
   >>> Decimal(1) / Decimal(7)
   Decimal('0.142857142857142857142857142857142857')
