Log in or Sign Up

Posted by al on Nov. 18, 2012 python unicode

Erreurs unicode avec Python 2.x, des clés pour s'en sortir

Avertissement : ceci est un article pratique sur l'utilisation d'Unicode en Python. Les puristes y trouveront sûrement des imprécisions terminologiques. J'assimile ici volontairement Unicode à UTF-8, l'encodage du web.

Un monde de désolation

Parmi les problèmes les plus agaçant avec Python 2.x, on trouve les problèmes liés à Unicode. Quel développeur Python n'a jamais pesté devant une erreur ressemblant à ceci :

UnicodeDecodeError: 'ascii' codec can't decode byte 0xc3 in position 0: ordinal not in range(128)

Ou bien encore cela :

UnicodeEncodeError: 'ascii' codec can't encode character un position 0: ordinal not in range(128)'

S'en suit alors une session d'essais et erreurs à base d'appels unicode(), encode() ou decode() jusqu'à ce que l'on arrive à résoudre le problème, jusqu'au jour où il réapparaît dans une autre partie de code. Comme le résumait si bien un de mes estimés collègues : « le problème avec ce genre d'erreur, c'est que tu répares à un endroit, ça répète ailleurs ».

Puis la lumière fut

J'étais dans ce cas jusqu'au jour où j'ai découvert cette présentation en anglais :

Unicode In Python, Completely Demystified

Ce fut pour moi la révélation qui m'a permis de vraiment comprendre ces erreurs et de les corriger de façon rationnelle. Comme tout le monde n'est pas forcément à l'aise avec la langue de Beyoncé, je reprends ici les idée principales qui sont utiles pour se sortir des problèmes liés à unicode et je vous livre même un moyen mnémotechnique inédit pour se rappeler de l'usage des méthodes encode() et decode(), le tout dans la langue de Jean-Pierre Foucault.

Notions fondamentales

La première chose à comprendre c'est qu'en Python il y a deux types de chaînes, unicode et str, mais que ces objets peuvent contenir du texte dans n'importe quel encodage. Ainsi un objet de type str peut très bien contenir de l'utf-8 :

>>> s = "Éléphant"
>>> type(s)
<type 'str'>
>>> print s
Éléphant

Alors quel intérêt d'avoir un type unicode, si le type str peut effectivement contenir de l'Unicode ? La raison est que le type str n'est qu'une simple chaîne d'octets qui ne connaît pas Unicode. Les caractères non-ascii y apparaissent simplement comme deux octets distincts :

>>> s = "Éléphant"
>>> s[0]
'\xc3'
>>> s[0:2]
'\xc3\x89'
>>> print s[0]

>>> print s[0:2]
É
>>> len(s)
10

Alors que le même texte dans un objet unicode est beaucoup plus pratique à utiliser :

>>> u = u"Éléphant"
>>> u[0]
u'\xc9'
>>> print u[0]
É
>>> len(u)
8

Alors me direz vous, on peut toujours utiliser des objets unicode et ne pas se soucier des objets str ? Malheureusement non, car Python 2.x ne sait pas faire d'entrées/sorties sur des objets Unicode.

Permettez moi de répéter ce fait fondamental (et fondamentalement enquiquinant) :

Python 2.x ne sait pas faire d'entrée/sorties avec des objets unicode.

Démonstration :

>>> u = u"Éléphant"
>>> f = open('animaux.txt', 'w')
>>> f.write(u)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeEncodeError: 'ascii' codec can't encode character u'\xc9' in position 0: ordinal not in range(128)

Et réciproquement si on a un fichier unicode :

$ cat animaux.txt
Éléphant
$ file animaux.txt
animaux.txt: UTF-8 Unicode text

Et qu'on essaie de le lire en Python :

>>> s = open("animaux.txt").read()
>>> type(s)
<type 'str'>
>>> len(s)
11
>>> s
'\xc3\x89l\xc3\xa9phant\n'

Comme vous le voyez, il va valoir faire des conversion entre str et unicode, l'API pour faire ça n'est pas franchement intuitive.

Conversions entre unicode et str

Pour effectuer des conversion entre unicode et str, on a recours aux méthodes unicode.encode() et str.decode(). Personnellement j'utilise une astuce mnémotechnique pour me souvenir de comment utiliser ces méthodes. Dans le nom de chacune de ces méthode, je remplace mentalement code par «l'encodage par défaut de Python», qui se trouve être ascii, sauf si vous l'avez modifié. Selon ce modèle, unicode.encode() devient mentalement unicode.en_asciise_moi_ça() et str.decode() devient str.dé_asciise_moi_ça(). L'argument utf-8 a un sens différent et assez contre-intuitif pour les deux méthode ; pour encode il indique l'encodage de la chaîne originale, pour decode il indique l'encodage de la destination.

Exemple d'asciisation d'un objet unicode:

>>> u = u"Éléphant"
>>> s = u.encode("utf-8")
>>> type(s)
<type 'str'>
>>> f = open('animaux.txt', 'w')
>>> f.write(s)
>>> f.close()

Comme vous le voyez, cela nous permet d'écrire la chaîne dans un fichier. Pour lire des données, on a recours à str.decode() :

>>> s = open("animaux.txt").read()
>>> type(s)
<type 'str'>
>>> u = s.decode("utf-8")
>>> print u,
Éléphant
>>> len(u)
9

En fait, le module standard codecs fournit un moyen plus simple de faire ça :

>>> import codecs
>>> f = codecs.open("animaux.txt", encoding="utf-8")
>>> u = f.read()
>>> type(u)
<type 'unicode'>

Pour convertir un objet str en objet unicode, on peut aussi utiliser le constructeur unicode :

>>> unicode("Éléphant")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'ascii' codec can't decode byte 0xc3 in position 0: ordinal not in range(128)k

Il y a un problème car ce constructeur s'attend à recevoir du texte dans l'encodage par défaut de Python, qui se trouve être ascii. Il faut donc lui indiquer l'encodage de notre chaîne en argument :

>>> u = unicode("Éléphant", "utf-8") # Vachement clair comme code hein ;)
>>> type(u)
<type 'unicode'>

Conclusion

Voilà, même si Python 3 devient de plus en plus utilisable, il va y avoir du code Python 2.x à écrire et à maintenir pendant encore un bon bout de temps, alors autant avoir les idées claires au sujet de ces satanées UnicodeErrors.

N'oubliez pas de tester abondamment votre code avec des chaînes contenant des caractères non-ascii. En interne, utilisez uniquement de l'unicode et pour les conversion liées aux entrée/sorties, souvenez vous que dans encode et decode :

"code" = "encodage par défaut de Python" = ascii.

Comments on this post:

Please Login (or Sign Up) to leave a comment

View source in reStructuredText