Python : comprendre l'Unicode grâce à Socrate

written by ccomb, on Apr 24, 2011 2:59:00 PM.

Socratis

Les nombreux grecs (dont Socrate ci-contre :) ) que vous avez fréquentés pendant vos études ont bourré, sans vous le demander, votre disque dur de milliers de fichiers MP3 et Vorbis, depuis les veilles musiques traditionnelles jusqu'aux derniers trucs pourris à la mode. Le premier problème est que vous n'avez aucun souvenir de la façon dont cela s'est passé. Ce phénomène doit pouvoir s'expliquer par un excès d'ούζο. Le deuxième problème est que certains de ces fichiers ont plus de dix ans, donc proviennent d'une époque où chaque langue avait son propre jeu de caractères. Tous ces fichiers ont donc un nom illisible. Non pas parce qu'ils sont écrits en grec, mais parce qu'ils sont encodés dans le jeu de caractère grec : iso8859-7. Pour résumer, vos fichiers s'appellent « ??????? » au lieu de s'appeler « συρτάκι ».

Qu'est-ce qu'un jeu de caractères ?

Un ordinateur ne stocke que des nombres. Pour qu'un symbole linguistique ou mathématique par exemple « Alpha » puisse avoir la moindre existence informatique, il faut le convertir en nombre. Ainsi, un jeu de caractère n'est qu'une table associative donnant la correspondance entre les symboles linguistiques et les nombres informatiques.

/static/alpha.png

Dans le jeu de caractères ASCII, l'un des plus anciens (1961), le nombre en question n'occupe que 7 bits, donc on ne peut représenter que 128 caractères différents, ceux de la langue anglaise ! Pour travailler avec d'autres langues il faut utiliser d'autres jeux de caractères, par exemple iso8859-1 pour les langues accentuées ouest-européennes ou iso8859-9 pour le turc, qui sont sur 8 bits et permettent de représenter deux fois plus de caractères que l'ASCII. L'inconvenient est que le même nombre, par exemple 233, peut correspondre à « θ » en grec ou « è » en français. Pas facile dans ces conditions d'échanger des données, ni de mélanger les langues... La solution est d'utiliser un jeu de caractère universel comme l'UCS qui fait partie du standard Unicode, et est capable de représenter de manière unique tous les symboles de toutes les langues et de toutes les époques. Ainsi, en Unicode, le caractère grec Alpha correspond au nombre 945, qu'on écrit de manière normalisée et en hexadécimal : U+03B1. Ce nombre peut ensuite être stocké dans un ordinateur de différentes manières : la plus courante est le codage UTF-8, qui utilise de 1 à 4 octets, ce qui lui permet d'être compatible avec l'ASCII. En Python 3, les chaînes de caractères (str) sont des objets Unicode par défaut. Leur représentation interne peut être sur 16 bits (UCS2) ou 32 bits (UCS4). Vous ne devez vous préoccuper de l'encodage que lorsque vous échangez des données avec le monde extérieur, par exemple le disque ou le réseau.

Et notre musique ??

Revenons à notre συρτάκι. Il y a toutes les chances pour que vos noms de fichiers grecs, qui ne sont que des chaînes d'octets écrits sur votre disque dur, soient encodés en iso8859-7. Si jamais votre système est configuré pour afficher ces noms de fichiers en utilisant autre chose que iso8859-7, vous ne verrez probablement que des symboles incohérents. Vous pouvez changer cette configuration, mais dans ce cas, ce sont vos noms de fichiers accentués en français qui deviendront illisibles.

La seule solution est de renommer tous vos fichiers en UTF-8 !

Comment ?

Plutôt que de deviner vous-même les lettres grecques et de renommer les fichiers à la main un par un, nous pouvons écrire un petit script Python qui fera le boulot à notre place. Attention ce programme n'est compatible qu'avec Python 3.2 !

#!/usr/bin/env python
# coding: utf-8

import os, sys, pdb
from argparse import ArgumentParser
from os.path import join, isdir, isfile, dirname, basename

# analyse des arguments avec argparse (Python2.7+ ou 3.2+)
parser = ArgumentParser(description='Renommage de fichiers vers utf-8')
parser.add_argument('--renomme', dest='renomme', action='store_true',
                    help='Renomme pour de vrai')
parser.add_argument('-r', dest='recursif', action='store_true',
                    help='Mode recursif')
parser.add_argument('-c', dest='encodage', action='store',
                    default='iso8859-1',
                    help="Encodage d'origine des fichiers")
parser.add_argument('fichiers', metavar='fichier', nargs='+',
                    help='Noms de fichiers à renommer')
args = parser.parse_args()

# liste des fichiers à renommer
fichiers = []
encodage_systeme = sys.getfilesystemencoding()
for arg in args.fichiers:
    fichiers.append(arg.encode(encodage_systeme, 'surrogateescape'))

def renomme(racine, nom_fichier):
    """fonction qui renomme
    """
    ancien_nom = join(racine, nom_fichier)
    nouveau_nom = ancien_nom.decode(args.encodage).encode(encodage_systeme)
    if ancien_nom != nouveau_nom:
        print("%s => %s" % (ancien_nom, nouveau_nom.decode(encodage_systeme)))
        if args.renomme:
            os.rename(ancien_nom, nouveau_nom)

# lancement du traitement
for fichier in fichiers:
    if isdir(fichier) and args.recursif:
        for racine, dossiers, fichiers in os.walk(fichier, topdown=False):
            for element in fichiers + dossiers:
                renomme(racine, element)
    elif isfile(fichier) or isdir(fichier) and not args.recursif:
        renomme(dirname(fichier), basename(fichier))

Dans ce programme on note l'utilisation du module argparse. Ce module a été introduit dans Python 2.7 et 3.2 et remplace optparse. Il permet de gérer et valider facilement des arguments passés sur la ligne de commande : on crée un parser (un analyseur), puis on lui indique les arguments souhaités. On retrouve ensuite ces arguments comme attributs de la variable args. Par exemple pour --renomme, l'association de dest='renomme' et action='store_true permet de stocker la valeur True dans args.renomme. De même, dest='encodage' et action='store' permet de stocker l'encodage fourni dans args.encodage.

Encodage ? Décodage ?

Puisque la suite du programme consiste à encoder ou décoder des caractères, rappelons en quoi consitent ces deux notions. Les jeux de caractères (ASCII, ISO8859, UTF-8) sont une façon d'encoder les symboles linguistiques sous forme de chaîne d'octets. « Encoder » signifie donc transformer un symbole unicode en chaîne d'octets, et « décoder » consiste à interpréter une chaîne d'octet pour la transformer en objet Unicode. L'objet Python Unicode sert à représenter le symbole linguistique alors que la chaîne d'octets contient sa représentation informatique. (En réalité, l'objet Python Unicode est à fortiori lui aussi représenté en interne sous forme d'octets, mais vous n'avez pas à vous en soucier puisque Python prend en charge nativement l'Unicode, contrairement à PHP.)

On peut résumer ceci dans le schéma suivant :

codec

Le reste des arguments de la ligne de commande se retrouve dans notre attribut args.fichiers. Une particularité de Python 3 est que ces arguments sont automatiquement décodés en Unicode en utilisant l'encodage du système (par ex utf-8). Dans notre cas le décodage est normalement impossible car cela revient à considérer de l'iso8859-7 comme de l'utf-8. Néanmoins pour éviter les erreurs à la lecture, Python applique une gestion automatique des erreurs de décodage appelée « surrogateescape » qui revient à transformer les caractères indécodables en caractères de substitution (des « surrogates »). Il faut donc d'abord réencoder ces arguments dans leur état d'origine, en utilisant la même option 'surrogateescape' dans l'autre sens, pour rétablir les octets initiaux depuis les caractères de substitution. C'est un cas un peu tordu et heureusement assez rare. Pour résumer, Python s'attend donc à ce que vos fichiers soient encodés correctement, mais ne fait pas d'erreur s'ils ne le sont pas. C'est à vous de traiter ce cas particulier. Vous trouverez plus d'explications sur les « surrogates » dans le PEP 383.

Nous avons ensuite une fonction de renommage, qui effectue le transcodage et le renommage, et qui est utilisé lors du parcours de l'arborescence. Ce parcours se fait grâce à la fonction os.walk qui évite d'avoir à écrire une fonction récursive. L'option « topdownFalse » inverse l'ordre de génération de la liste des répertoires pour commencer par les plus profonds, en remontant jusqu'aux dossiers proches de la racine. Sinon vous serez interrompus en plein milieu par une erreur Fichier non trouvé.

Conclusion

La gestion de l'Unicode dans Python 3 est plus cohérente et plus facile à comprendre que dans Python 2. Elle devrait éviter de nombreuses erreurs et incompréhensions. Par défaut une chaîne de caractères (str) est toujours en unicode. Et si vous ne travaillez pas avec des chaînes de caractère Unicode, vous pouvez travailler avec des chaînes d'octets (bytes). Il y a toutefois un changement de vocabulaire auquel il faudra faire attention :

Changement sur les types
Python 2   Python 3
unicode str
str bytes

Le doctest, une technique anti-chat

written by ccomb, on Mar 29, 2011 11:24:00 AM.

/static/python-250px.png

Un petit article pour comprendre les doctests en Python.

Pourquoi ?

Peu nombreux sont les langages de programmation qui proposent le principe du doctest. Un doctest est une documentation testable, ou un test documenté. Si vous faites du Python, il faut en profiter ! Un projet logiciel ne commence JAMAIS par du code. Il commence par une idée. Cette idée doit d'abord être jetée sur le papier en anglais, en islandais, en turc, en japonais ou ce que vous voulez. Ouvrez un éditeur de texte puis commencez par écrire ce qui vous passe par la tête à propos de votre projet.

Voici un exemple:

Le but de ce logiciel est de contrôler le niveau de croquettes dans
l'assiette de mon chat et de compter le nombre de fois par jour où il mange.
Le but final non avouable est de ne plus avoir à lui filer à manger
en concevant un système automatique à base d'Arduino. Je dois donc
avoir un capteur pour le poids de l'assiette et un détecteur de présence. Il y
aura donc deux composants : un pour chaque type de capteur.

Une fois que votre idée commence à se préciser, des bouts de code vont germer dans votre esprit. C'est le moment de les écrire ! À la suite de votre introduction, vous pouvez alterner des paragraphes en français et des paragraphes en Python, en reproduisant une session Python interactive :

Pour pouvoir accéder à chaque capteur, on peut donc imaginer avoir une classe
pour chaque type de capteur::

  >>> from marducha.capteur import Presence
  >>> capteur1 = Presence()
  >>> capteur1.get_presence()
  False

Et pour l'autre capteur :

  >>> from marducha.capteur import Poids
  >>> capteur2 = Poids()
  >>> capteur2.get_poids()
  0.35

Presence et Poids n'existent pas encore, mais vous êtes déjà en train de réfléchir à la façon de les utiliser. Le simple fait d'écrire des exemples d'utilisation de votre futur code vous aide à repérer prématurément les erreurs ou les difficultés de conception : vous avez déjà économisé un premier tour de refactoring ! Par exemple vous pouvez immédiatement réaliser que vous aurez besoin de passer des paramètres pour configurer l'accès au capteur physique ou de configurer une échelle d'unité pour le poids.

copyleft http://www.flickr.com/photos/felinest/

Et à l'instant même où vous comprenez que vous allez devoir aussi gérer une boucle d'événements pour réagir au capteur de présence, votre chat vous saute subitement sur les genoux et vous rappelle qu'il est prêt à vous ouvrir les yeux, au sens propre, si vous ne remplissez pas son assiette au plus vite. Après un aller-retour à la cuisine, vous avez tellement pesté contre votre animal bien-aimé, que vous avez complètement perdu le fil de votre pensée. Le doctest est une technique anti-chat : relisez-le rapidement depuis le début, vous allez revenir subitement dans le même état d'esprit qu'avant la perturbation. Vous allez retrouver vos idées et pouvoir reprendre votre réflexion sans perte de temps. Les niveaux d'abstraction nécessaires pour mener à bien une conception logicielle s'accommodent très mal des distractions et des changements de contexte fréquents. Utiliser le doctests dès la phase de conception vous offre déjà deux avantages non négligeables :

  1. détection précoce des erreurs de conception
  2. rapidité de reprise du travail

Comment ?

La prise en charge des doctests est offerte dans la bibliothèque standard. Enregistrez le texte que vous avez écrit dans un fichier README.txt, puis créez un fichier test.py avec le contenu suivant :

import doctest
doctest.testfile('README.txt')

Si vous l'exécutez, avec python test.py, vous constaterez que le doctest est exécuté et génère des erreurs. Pour corriger ces erreurs, il suffit d'écrire l'implémentation de votre module capteur ! Créez un package marducha contenant un module capteur.py avec les définitions suivantes (pour rappel, un package est un simple répertoire contenant un fichier vide __init__.py ; et un module est un simple fichier Python) :

"""
Module contenant les capteurs.
"""

class Presence():
    """Classe permettant d'interroger le capteur de presence

    >>> p = Presence()
    >>> p.get_presence()
    False
    """

    def get_presence(self):
        """le chat est-il present ?
        """
        return False


class Poids():
    """Classe pour mesurer le poids
    """
    def get_poids(self):
        """Combien pese l'assiette ?
        """
        return 0.35

Maintenant votre implémentation est correcte vis-à-vis du test : vérifiez-le en lançant python test.py. Notez qu'il est possible de mettre des doctests à l'intérieur du code python, dans les docstrings, c'est à dire les chaînes de documentation situées au début des modules, des classes ou des fonctions. Pour tester les docstrings d'un module, vous pouvez simplement ajouter dans votre fichier test.py :

from marducha import capteur
doctest.testmod(capteur)

Les fonctions testmod et testfile constituent l'API simplifiée du module doctest. Consultez la documentation pour connaître l'API avancée et l'API unittest du module doctest.

Conclusion

Au fil du temps, vous constaterez que les doctests vous offrent des avantages supplémentaires : vous pouvez plus facilement concevoir à plusieurs, échanger et fixer des idées. Vous réaliserez aussi que vous aurez écrit de la documentation presque sans vous en rendre compte, et que cette documentation est à jour. Il suffira ensuite de la publier grâce à un outil comme Sphinx.

Le doctest cumule ainsi les avantages du développement dirigé par les tests (TDD) et du développement dirigé par la documentation (DDD). Mais gardez à l'esprit qu'un doctest est avant tout une documentation testée, plus qu'un test documenté. Il doit se contenter de donner des exemples de code significatifs et d'assez haut niveau et doit aider le lecteur à comprendre l'architecture du projet. Il ne doit en aucun cas remplacer de vrais tests unitaires exhaustifs !


test