11. Giro Breve della Libreria Standard — Parte II

Questo secondo giro copre moduli più avanzati che supportano esigenze di programmazione professionali. Questi moduli raramente si trovano in piccoli script.

11.1. Formattazione dell’Output

Il modulo reprlib fornisce una versione di repr() personalizzata per visualizzazioni abbreviate di contenitori grandi o profondamente annidati:

>>> import reprlib
>>> reprlib.repr(set('supercalifragilisticexpialidocious'))
"{'a', 'c', 'd', 'e', 'f', 'g', ...}"

Il modulo pprint offre un controllo più sofisticato sulla stampa di oggetti sia integrati che definiti dall’utente in modo leggibile dall’interprete. Quando il risultato è più lungo di una riga, il «pretty printer» aggiunge interruzioni di riga e indentazione per rivelare più chiaramente la struttura dei dati:

>>> import pprint
>>> t = [[[['black', 'cyan'], 'white', ['green', 'red']], [['magenta',
...     'yellow'], 'blue']]]
...
>>> pprint.pprint(t, width=30)
[[[['black', 'cyan'],
   'white',
   ['green', 'red']],
  [['magenta', 'yellow'],
   'blue']]]

Il modulo textwrap formatta paragrafi di testo per adattarsi a una larghezza di schermo specificata:

>>> 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.

Il modulo locale accede a un database di formati di dati specifici della cultura. L’attributo di raggruppamento della funzione di formattazione del locale fornisce un modo diretto di formattare i numeri con separatori di gruppo:

>>> 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_string("%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. Templating

Il modulo string include una versatile classe Template con una sintassi semplificata adatta per l’editing da parte degli utenti finali. Questo permette agli utenti di personalizzare le loro applicazioni senza dover alterare l’applicazione.

Il formato utilizza nomi di segnaposto formati da $ con identificatori Python validi (caratteri alfanumerici e trattini bassi). Circondando il segnaposto con parentesi graffe, è possibile farlo seguire da altre lettere alfanumeriche senza spazi intermedi. Scrivere $$ crea un singolo $ escapato:

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

Il metodo substitute() solleva un’eccezione KeyError quando un segnaposto non è fornito in un dizionario o come parametro keyword. Per applicazioni stile mail-merge, i dati forniti dall’utente possono essere incompleti e il metodo safe_substitute() potrebbe essere più appropriato — lascerà i segnaposto invariati se i dati sono mancanti:

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

Le sottoclassi di Template possono specificare un delimitatore personalizzato. Ad esempio, un’utilità per il rinominamento dei batch di un browser di foto può scegliere di usare segni percentuali per i segnaposto come la data corrente, il numero di sequenza delle immagini o il formato del file:

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

Un’altra applicazione per il templating è separare la logica del programma dai dettagli di più formati di output. Questo rende possibile sostituire template personalizzati per file XML, rapporti in testo semplice e rapporti HTML web.

11.3. Lavorare con Layout Binari dei Dati

Il modulo struct fornisce le funzioni pack() e unpack() per lavorare con formati di record binari a lunghezza variabile. Il seguente esempio mostra come scorrere le informazioni dell’intestazione in un file ZIP senza usare il modulo zipfile. I codici pack "H" e "I" rappresentano numeri senza segno a due e quattro byte rispettivamente. Il "<" indica che hanno dimensioni standard e sono in byte order 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. Multi-threading

Il threading è una tecnica per disaccoppiare i compiti che non sono sequenzialmente dipendenti. I thread possono essere utilizzati per migliorare la reattività delle applicazioni che accettano l’input dell’utente mentre altri compiti vengono eseguiti in background. Un caso d’uso correlato è eseguire I/O in parallelo con calcoli in un altro thread.

Il seguente codice mostra come il modulo ad alto livello threading può eseguire compiti in background mentre il programma principale continua a funzionare:

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

La principale sfida delle applicazioni multi-threaded è coordinare i thread che condividono dati o altre risorse. A tal fine, il modulo threading fornisce numerosi primitivi di sincronizzazione tra cui lock, eventi, variabili di condizione e semafori.

Sebbene questi strumenti siano potenti, piccoli errori di progettazione possono provocare problemi difficili da riprodurre. Quindi, l’approccio preferito per il coordinamento dei compiti è concentrare tutto l’accesso a una risorsa in un singolo thread e poi usare il modulo queue per alimentare quel thread con richieste provenienti da altri thread. Le applicazioni che usano oggetti Queue per la comunicazione e il coordinamento tra thread sono più facili da progettare, più leggibili e più affidabili.

11.5. Logging

Il modulo logging offre un sistema di logging completo e flessibile. Nel suo uso più semplice, i messaggi di log vengono inviati a un file o a 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')

Questo produce il seguente output:

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

Per impostazione predefinita, i messaggi informativi e di debug sono soppressi e l’output è inviato all’errore standard. Altre opzioni di output includono l’instradamento dei messaggi tramite email, datagrammi, socket o a un server HTTP. Filtri nuovi possono selezionare l’instradamento diverso basato sulla priorità del messaggio: DEBUG, INFO, WARNING, ERROR, e CRITICAL.

Il sistema di logging può essere configurato direttamente da Python o può essere caricato da un file di configurazione modificabile dall’utente per un logging personalizzato senza alterare l’applicazione.

11.6. Riferimenti Deboli

Python gestisce automaticamente la memoria (conteggio dei riferimenti per la maggior parte degli oggetti e garbage collection per eliminare i cicli). La memoria viene liberata poco dopo che l’ultimo riferimento ad essa è stato eliminato.

Questo approccio funziona bene per la maggior parte delle applicazioni ma occasionalmente è necessario tracciare gli oggetti solo finché vengono utilizzati da qualcos’altro. Sfortunatamente, solo il tracciamento crea un riferimento che li rende permanenti. Il modulo weakref fornisce strumenti per tracciare gli oggetti senza creare un riferimento. Quando l’oggetto non è più necessario, viene automaticamente rimosso da una tabella weakref e viene attivato un callback per gli oggetti weakref. Applicazioni tipiche includono il caching di oggetti costosi da creare:

>>> 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:/python313/lib/weakref.py", line 46, in __getitem__
    o = self.data[key]()
KeyError: 'primary'

11.7. Strumenti per Lavorare con Liste

Molte necessità di strutture dati possono essere soddisfatte con il tipo di lista integrato. Tuttavia, a volte c’è bisogno di implementazioni alternative con diversi compromessi di prestazioni.

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)

Oltre alle implementazioni alternative di liste, la libreria offre anche altri strumenti come il modulo bisect con funzioni per manipolare liste ordinate:

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

Il modulo heapq fornisce funzioni per implementare heap basati su liste regolari. L’ingresso a valore più basso viene sempre mantenuto in posizione zero. Questo è utile per applicazioni che accedono ripetutamente all’elemento più piccolo ma non vogliono eseguire un ordinamento completo della lista:

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

  • applicazioni finanziarie e altri usi che richiedono una rappresentazione decimale esatta,

  • controllo sulla precisione,

  • controllo sull’arrotondamento per soddisfare requisiti legali o normativi,

  • tracciamento delle cifre decimali significative, oppure

  • applicazioni in cui l’utente si aspetta che i risultati corrispondano ai calcoli fatti a mano.

Ad esempio, calcolare una tassa del 5% su una tariffa telefonica di 70 centesimi dà risultati diversi in decimale a virgola mobile e binario a virgola mobile. La differenza diventa significativa se i risultati vengono arrotondati al centesimo più vicino:

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

Il risultato Decimal mantiene uno zero finale, inferendo automaticamente una significatività a quattro posti dai fattori moltiplicativi con significatività a due posti. Decimale riproduce la matematica fatta a mano ed evita problemi che possono sorgere quando la virgola mobile binaria non può rappresentare esattamente quantità decimali.

La rappresentazione esatta permette alla classe Decimal di eseguire calcoli modulo e test di uguaglianza che sono inadatti per la virgola mobile binaria:

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

Il modulo decimal fornisce aritmetica con precisione quanto richiesta:

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