# 11. Survol de la bibliothèque standard -- Deuxième partie

Cette deuxième partie aborde des modules plus à destination des programmeurs professionnels. Ces modules sont rarement nécessaires dans de petits scripts.

## 11.1. Formatage de l'affichage

Le module `reprlib` est une variante de la fonction `repr()`, spécialisé dans l'affichage concis de conteneurs volumineux ou fortement imbriqués :

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

Le module `pprint` propose un contrôle plus fin de l'affichage des objets, aussi bien natifs que ceux définis par l'utilisateur, de manière à être lisible par l'interpréteur. Lorsque le résultat fait plus d'une ligne, il est séparé sur plusieurs lignes et est indenté pour rendre la structure plus visible :

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

Le module `textwrap` formate des paragraphes de texte pour tenir sur un écran d'une largeur donnée :

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

Le module `locale` utilise une base de données des formats spécifiques à chaque région pour les dates, nombres, etc. L'attribut `grouping` de la fonction de formatage permet de formater directement des nombres avec un séparateur :

    >>> 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. Gabarits (templates en anglais)

Le module `string` contient une classe polyvalente : `Template`. Elle permet d'écrire des gabarits (templates en anglais) avec une syntaxe simple, dans le but d'être utilisable par des non-développeurs. Ainsi, vos utilisateurs peuvent personnaliser leur application sans la modifier.

Le format utilise des marqueurs formés d'un `$` suivi d'un identifiant Python valide (caractères alphanumériques et tirets-bas). Entourer le marqueur d'accolades permet de lui coller d'autres caractères alphanumériques sans intercaler une espace. Écrire `$$` produit un simple `$` :

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

La méthode `substitute()` lève une exception `KeyError` lorsqu'un marqueur n'a pas été fourni, ni dans un dictionnaire, ni sous forme d'un paramètre nommé. Dans certains cas, lorsque la donnée à appliquer peut n'être fournie que partiellement par l'utilisateur, la méthode `safe_substitute()` est plus appropriée car elle laisse tels quels les marqueurs manquants :

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

Les classes filles de `Template` peuvent définir leur propre délimiteur. Typiquement, un script de renommage de photos par lots peut choisir le symbole pourcent comme marqueur pour les champs tels que la date actuelle, le numéro de l'image ou son format :

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

Une autre utilisation des gabarits consiste à séparer la logique métier des détails spécifiques à chaque format de sortie. Il est ainsi possible de générer des gabarits spécifiques pour les fichiers XML, texte, HTML…

## 11.3. Traitement des données binaires

Le module `struct` expose les fonctions `pack()` et `unpack()` permettant de travailler avec des données binaires. L'exemple suivant montre comment parcourir un entête de fichier ZIP sans recourir au module `zipfile`. Les marqueurs `"H"` et `"I"` représentent des nombres entiers non signés, stockés respectivement sur deux et quatre octets. Le `"<"` indique qu'ils ont une taille standard et utilisent la convention petit-boutiste :

    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. Fils d'exécution

Des tâches indépendantes peuvent être exécutées de manière non séquentielle en utilisant des fils d'exécution (threading en anglais). Les fils d'exécution peuvent être utilisés pour améliorer la réactivité d'une application qui interagit avec l'utilisateur pendant que d'autres traitements sont exécutés en arrière-plan. Une autre utilisation typique est de séparer sur deux fils d'exécution distincts les entrées / sorties et le calcul.

Le code suivant donne un exemple d'utilisation du module `threading` exécutant des tâches en arrière-plan pendant que le programme principal continue de s'exécuter :

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

Le principal défi des applications avec plusieurs fils d'exécution consiste à coordonner ces fils qui partagent des données ou des ressources. Pour ce faire, le module `threading` expose quelques outils dédiés à la synchronisation comme les verrous (locks en anglais), les événements (events en anglais), les variables conditionnelles (condition variables en anglais) et les sémaphores (semaphore en anglais).

Bien que ces outils soient puissants, de petites erreurs de conception peuvent engendrer des problèmes difficiles à reproduire. Donc, l'approche classique pour coordonner des tâches est de restreindre l'accès d'une ressource à un seul fil d'exécution et d'utiliser le module `queue` pour alimenter ce fil d'exécution en requêtes venant d'autres fils d'exécution. Les applications utilisant des `Queue` pour leurs communication et coordination entre fils d'exécution sont plus simples à concevoir, plus lisibles et plus fiables.

## 11.5. Journalisation

Le module `logging` est un système de journalisation complet. Dans son utilisation la plus élémentaire, les messages sont simplement envoyés dans un fichier ou sur `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')

Cela produit l'affichage suivant :

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

Par défaut, les messages d'information et de débogage sont ignorés, les autres sont envoyés vers la sortie standard. Il est aussi possible d'envoyer les messages par courriel, datagrammes, en utilisant des connecteurs réseau ou vers un serveur HTTP. Des nouveaux filtres permettent d'utiliser des sorties différentes en fonction de la priorité du message : `DEBUG`, `INFO`, `WARNING`, `ERROR` et `CRITICAL`.

La configuration de la journalisation peut être effectuée directement dans le code Python ou peut être chargée depuis un fichier de configuration, permettant de personnaliser la journalisation sans modifier l'application.

## 11.6. Références faibles

Python gère lui-même la mémoire (par comptage des références pour la plupart des objets et en utilisant un ramasse-miettes (garbage collector en anglais) pour éliminer les cycles). La mémoire est libérée rapidement lorsque sa dernière référence est supprimée.

Cette approche fonctionne bien pour la majorité des applications mais, parfois, il est nécessaire de surveiller un objet seulement durant son utilisation par quelque chose d'autre. Malheureusement, le simple fait de le suivre crée une référence qui rend l'objet permanent. Le module `weakref` expose des outils pour suivre les objets sans pour autant créer une référence. Lorsqu'un objet n'est pas utilisé, il est automatiquement supprimé du tableau des références faibles et une fonction de rappel (callback en anglais) est appelée. Un exemple typique est le cache d'objets coûteux à créer :

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

## 11.7. Outils pour les listes

Beaucoup de structures de données peuvent être représentées avec les listes natives. Cependant, d'autres besoins peuvent émerger pour des structures ayant des caractéristiques différentes, typiquement en termes de performance.

Le module `array` fournit un objet `array()` ne permettant de stocker que des listes homogènes mais d'une manière plus compacte. L'exemple suivant montre une liste de nombres stockés chacun sur deux octets non signés (marqueur `"H"`) plutôt que d'utiliser 16 octets comme l'aurait fait une liste classique :

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

Le module `collections` fournit la classe `deque()`. Elle ressemble à une liste mais est plus rapide pour l'insertion ou l'extraction des éléments par la gauche et plus lente pour accéder aux éléments du milieu. Ces objets sont particulièrement adaptés pour construire des queues ou des algorithmes de parcours d'arbres en largeur (ou BFS, pour Breadth First Search en anglais) :

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

En plus de fournir des implémentations de listes alternatives, la bibliothèque fournit des outils tels que `bisect`, un module contenant des fonctions de manipulation de listes triées :

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

Le module `heapq` permet d'implémenter des tas (heap en anglais) à partir de simples listes. La valeur la plus faible est toujours à la première position (indice 0). C'est utile dans les cas où l'application accède souvent à l'élément le plus petit mais sans vouloir classer entièrement la liste :

    >>> 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. Arithmétique décimale à virgule flottante

Le module `decimal` exporte la classe `Decimal` : elle est spécialisée dans le calcul de nombres décimaux représentés en virgule flottante. Par rapport à la classe native `float`, elle est particulièrement utile pour :

- les applications traitant de finance et autres utilisations nécessitant une représentation décimale exacte,

- le contrôle de la précision,

- le contrôle sur les arrondis pour répondre à des obligations légales ou réglementaires,

- suivre les décimales significatives, ou

- les applications pour lesquelles l'utilisateur attend des résultats identiques aux calculs faits à la main.

Par exemple, calculer 5 % de taxe sur une facture de 70 centimes donne un résultat différent en nombre à virgule flottante binaire et décimale. La différence devient significative lorsqu'on arrondit le résultat au centime près :

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

Le résultat d'un calcul donné par `Decimal` conserve les zéros non-significatifs. La classe conserve automatiquement quatre décimales significatives pour des opérandes à deux décimales significatives. La classe `Decimal` imite les mathématiques telles qu'elles pourraient être effectuées à la main, évitant les problèmes typiques de l'arithmétique binaire à virgule flottante qui n'est pas capable de représenter exactement certaines quantités décimales.

La représentation exacte de la classe `Decimal` lui permet de faire des calculs de modulo ou des tests d'égalité qui ne seraient pas possibles avec une représentation à virgule flottante binaire :

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

Le module `decimal` permet de faire des calculs avec autant de précision que nécessaire :

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