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