11. Survol de la Bibliothèque Standard – Partie II

Cette seconde visite vous fera découvrir des modules d’un usage plus professionnel. Ces modules sont rarement nécessaires dans de petits scripts.

11.1. Formatage

Le module reprlib est une variante de la fonction repr(), spécialisée 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 primitifs que définis par l’utilisateur, et souvent lisible part l’interpréteur. Lorsque le résultat fait plus d’une ligne, il est séparé sur plusieurs lignes et 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 offre une base de donnée de formats de donnée spécifique à chaque région. 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("%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)

Le module string contiens une classe fort polyvalente : Template permettant d’écrire des gabarits (dits « templates ») avec une syntaxe simple, si simple qu’elle est compréhensible par des non-développeurs. Cela permet donc à vous utilisateurs de personnaliser leur application sans la modifier.

Le format est constitué de 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 un espace. Écrire $$ créé 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 n’est connur que partiellement, la méthode safe_substitute() est plus appropriée car elle laissera tel quel 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, par exemple, 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

Un autre usage des templates est de séparer la logique métier dub côté et les détails spécifiques à chaque format de sortie. Il est possible de générer de cette manière des fichiers XML, texte, HTML, …

11.3. Travailler avec 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 une 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 dans le style 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. Threads

Des tâches indépendantes peuvent être exécutées simultanément (on parle de concourance), en utilisant des threads.Les threads peuvent améliorer la réactivité d’une application qui accepterait d’interagir avec l’utilisateur pendant que d’autres traitements sont exécutés en arrière plan. Un autre usage typique est de séparer sur deux threads distincts les I/O (entrées / sorties) et le calcul.

Le code suivant donne un exemple d’usage 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 challenge principal des applications multi tâches est la coordination entre les threads 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), événement (events), variables conditionnelles (condition variables), et les sémaphores.

Bien que ces outils soient puissants, des erreurs de conceptions peuvent engendrer des problèmes difficiles à reproduire. Donc, l’approche favorite pour coordonner des tâches est de restreindre l’accès d’une ressource à un seul thread, et d’utiliser le module queue pour alimenter ce thread de requêtes venant d’autres threads. Les applications utilisant des Queue pour leurs communication et coordination entre threads sont plus simples à concevoir, plus lisible, 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')

Produisant 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 écrites sur la sortie standard. Il est aussi possible d’envoyer les messages par email, datagrammes, sur des sockets, ou postés sur un serveur HTTP. Les nouveaux filtres permettent d’utiliser des sorties différentes en fonction de la priorité du message : DEBUG, INFO, WARNING, ERROR, et CRITICAL.

depuis un fichier de configuration, permettant de personnaliser le log sans modifier l’application.

11.6. Références faibles

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

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

11.7. Outils pour travailler avec des listes

Beaucoup de structures de données peuvent être représentés avec des listes natives. Cependant, parfois, d’autres besoins émergent, pour des structures ayant des caractéristiques différentes, typiquement en terme 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’aurais 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(), qui ressemble à une liste, mais plus rapide à insérer ou sortir des éléments par la gauche, et plus lente pour accéder aux éléments du milieu. C’est objets sont particulièrement adaptés pour construire des queues ou des algorithme de parcours d’arbre en largeur :

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

Au delà 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, permettant d’implémenter des tas (heaps) à partir de simple listes. La valeur la plus faible est toujours à la première position (indice 0). C’est utile dans les cas où l’application souvent besoin de trouver l’élément le plus petit sans trier 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 expose la classe Decimal, 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 application traitant de finance est autres usages nécessitant une représentation décimale exacte,
  • contrôle sur la précision,
  • contrôle sur les arrondis pour correspondre aux obligations légales ou du régulateur,
  • suivre les décimales significatives, ou
  • les applications avec 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 a virgule flottantes binaires et décimales. 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éro 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 tels qu’elles pourraient être effectuées à la main, évitant les problèmes typique 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 du modulo ou des tests d’égalité qui ne seraient pas possibles avec des virgules flottantes binaires :

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

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