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('supercalifragilisticexpialidocious'))
   "{'a', 'c', 'd', 'e', 'f', 'g', ...}"

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
   >>> t = [[[['black', 'cyan'], 'white', ['green', 'red']], [['magenta',
   ...     'yellow'], 'blue']]]
   ...
   >>> pprint.pprint(t, width=30)
   [[[['black', 'cyan'],
      'white',
      ['green', 'red']],
     [['magenta', 'yellow'],
      'blue']]]

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

   >>> import textwrap
   >>> doc = """The wrap() method is just like fill() except that it returns
   ... a list of strings instead of one big string with newlines to separate
   ... the wrapped lines."""
   ...
   >>> print(textwrap.fill(doc, width=40))
   The wrap() method is just like fill()
   except that it returns a list of strings
   instead of one big string with newlines
   to separate the wrapped lines.

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'
   >>> conv = locale.localeconv()          # get a mapping of conventions
   >>> x = 1234567.8
   >>> locale.format("%d", x, grouping=True)
   '1,234,567'
   >>> locale.format_string("%s%.*f", (conv['currency_symbol'],
   ...                      conv['frac_digits'], x), 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
   >>> t = Template('${village}folk send $$10 to $cause.')
   >>> t.substitute(village='Nottingham', cause='the ditch fund')
   'Nottinghamfolk send $10 to the ditch fund.'

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):

   >>> t = Template('Return the $item to $owner.')
   >>> d = dict(item='unladen swallow')
   >>> t.substitute(d)
   Traceback (most recent call last):
     ...
   KeyError: 'owner'
   >>> t.safe_substitute(d)
   'Return the unladen swallow to $owner.'

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 time, os.path
   >>> photofiles = ['img_1074.jpg', 'img_1076.jpg', 'img_1077.jpg']
   >>> class BatchRename(Template):
   ...     delimiter = '%'
   ...
   >>> fmt = input('Enter rename style (%d-date %n-seqnum %f-format):  ')
   Enter rename style (%d-date %n-seqnum %f-format):  Ashley_%n%f

   >>> t = BatchRename(fmt)
   >>> date = time.strftime('%d%b%y')
   >>> for i, filename in enumerate(photofiles):
   ...     base, ext = os.path.splitext(filename)
   ...     newname = t.substitute(d=date, n=i, f=ext)
   ...     print('{0} --> {1}'.format(filename, newname))

   img_1074.jpg --> Ashley_0.jpg
   img_1076.jpg --> Ashley_1.jpg
   img_1077.jpg --> Ashley_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

   with open('myfile.zip', 'rb') as f:
       data = f.read()

   start = 0
   for i in range(3):                      # show the first 3 file headers
       start += 14
       fields = struct.unpack('<IIIHH', data[start:start+16])
       crc32, comp_size, uncomp_size, filenamesize, extra_size = fields

       start += 16
       filename = data[start:start+filenamesize]
       start += filenamesize
       extra = data[start:start+extra_size]
       print(filename, hex(crc32), comp_size, uncomp_size)

       start += extra_size + comp_size     # skip to the next header


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 AsyncZip(threading.Thread):
       def __init__(self, infile, outfile):
           threading.Thread.__init__(self)
           self.infile = infile
           self.outfile = outfile

       def run(self):
           f = zipfile.ZipFile(self.outfile, 'w', zipfile.ZIP_DEFLATED)
           f.write(self.infile)
           f.close()
           print('Finished background zip of:', self.infile)

   background = AsyncZip('mydata.txt', 'myarchive.zip')
   background.start()
   print('The main program continues to run in foreground.')

   background.join()    # Wait for the background task to finish
   print('Main program waited until background was done.')

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('Debugging information')
   logging.info('Informational message')
   logging.warning('Warning:config file %s not found', 'server.conf')
   logging.error('Error occurred')
   logging.critical('Critical error -- shutting down')

Iată ce se va afișa:

   WARNING:root:Warning:config file server.conf not found
   ERROR:root:Error occurred
   CRITICAL:root:Critical error -- shutting down

Î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, value):
   ...         self.value = value
   ...     def __repr__(self):
   ...         return str(self.value)
   ...
   >>> a = A(10)                   # create a reference
   >>> d = weakref.WeakValueDictionary()
   >>> d['primary'] = a            # does not create a reference
   >>> d['primary']                # fetch the object if it is still alive
   10
   >>> del a                       # remove the one reference
   >>> gc.collect()                # run garbage collection right away
   0
   >>> d['primary']                # entry was automatically removed
   Traceback (most recent call last):
     File "<stdin>", line 1, in <module>
       d['primary']                # entry was automatically removed
     File "C:/python311/lib/weakref.py", line 46, in __getitem__
       o = self.data[key]()
   KeyError: 'primary'


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.

The "array" module provides an "array()" object that is like a list
that stores only homogeneous data and stores it more compactly.  The
following example shows an array of numbers stored as two byte
unsigned binary numbers (typecode ""H"") rather than the usual 16
bytes per entry for regular lists of Python int objects:

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

The "collections" module provides a "deque()" object that is like a
list with faster appends and pops from the left side but slower
lookups in the middle. These objects are well suited for implementing
queues and breadth first tree searches:

   >>> from collections import deque
   >>> d = deque(["task1", "task2", "task3"])
   >>> d.append("task4")
   >>> print("Handling", d.popleft())
   Handling task1

   unsearched = deque([starting_node])
   def breadth_first_search(unsearched):
       node = unsearched.popleft()
       for m in gen_moves(node):
           if is_goal(m):
               return m
           unsearched.append(m)

Î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
   >>> scores = [(100, 'perl'), (200, 'tcl'), (400, 'lua'), (500, 'python')]
   >>> bisect.insort(scores, (300, 'ruby'))
   >>> scores
   [(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
   >>> data = [1, 3, 5, 7, 9, 2, 4, 6, 8, 0]
   >>> heapify(data)                      # rearrange the list into heap order
   >>> heappush(data, -5)                 # add a new entry
   >>> [heappop(data) for i in range(3)]  # fetch the three smallest entries
   [-5, 0, 1]


11.8. Decimal Floating Point Arithmetic
=======================================

The "decimal" module offers a "Decimal" datatype for decimal floating
point arithmetic.  Compared to the built-in "float" implementation of
binary floating point, the class is especially helpful for

* 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
   >>> sum([0.1]*10) == 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')
