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 garbage collection 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')