11. Brief Tour of the Standard Library — Part II¶
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("%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
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 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 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, sur des sockets 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:/python35/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
expose 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
>>> 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')