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