Sam & Max » encoding http://sametmax.com Du code, du cul Sat, 07 Nov 2015 10:56:13 +0000 en-US hourly 1 http://wordpress.org/?v=4.1 Un header d’encoding plus simple pour Python 16 http://sametmax.com/un-header-dencoding-plus-simple-pour-python/ http://sametmax.com/un-header-dencoding-plus-simple-pour-python/#comments Tue, 14 Apr 2015 12:15:40 +0000 http://sametmax.com/?p=16074 # -*- coding: utf-8 -*- Je le copie/colle à chaque fois, et sur sublime j'ai snippet pour le tapper.]]> Je sais pas pour vous, mais moi je me souviens jamais de :

# -*- coding: utf-8 -*-

Je le copie/colle à chaque fois, et sur sublime j’ai un snippet pour le taper.

Je viens d’apprendre, après 10 putain d’années à coder en python, que le header suivant était parfaitement valide :

# coding: utf-8

Bordel de merde, pourquoi c’est pas écrit dans tous les tutos ? Pourquoi on se tape encore l’ancien ?

Évidement avec Python 3, y a souvent plus besoin d’en-tête du tout, mais shit, ce genre détail c’est super con. Vous imaginez pas le nombre d’élèves à qui j’ai fait apprendre la première version toute pourrie alors que la seconde est si simple.

Grr, dirais-je.

]]>
http://sametmax.com/un-header-dencoding-plus-simple-pour-python/feed/ 16
assert “а” == “a” # lol 19 http://sametmax.com/assert-%d0%b0-a-lol/ http://sametmax.com/assert-%d0%b0-a-lol/#comments Mon, 09 Jun 2014 08:45:51 +0000 http://sametmax.com/?p=10433 Python 3, le bonheur d’avoir UTF8 comme encoding par défaut !

En plus, ça ajoute un petit potentiel de lulz.

Par exemple, ceci marche très bien :

def test():
    print('Youpi')
    print('Youpi')

Et ceci…

def test():
    print('Arg')
    print('Arg')

… provoque une syntaxe error !

  File "<stdin>", line 2
       print('Arg')
    ^
SyntaxError: invalid character in identifier

La raison est que la première ligne print(‘Arg’) contient le caractère unicode U+0020, qui est un espace inimprimable, mais pas le même que l’ascii :)

Bon, vous allez-me dire, on pouvait déjà mélanger les tabs et les espaces, s’amuser avec les espaces insécables, ou simplement déclarer manuellement l’encoding et faire pareil…

Allons plus loin. Saviez-vous qu’on pouvait utiliser des caractères non-ASCII dans les identifiants en Python 3 ?

Ceci est donc parfaitement valide :

éôà = 1

Ce qui invite bien entendu a des choses tout à fait amusantes comme :

def аttention():
    print('!')
 
>>> аttention()
!
>>> attention()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'attention' is not defined

En effet, j’ai utilisé le caractère “а” cyrilique comme première lettre, et non le “a” ASCII. Ils n’ont pas le même code :

>>> ord("а"), ord("a")
(1072, 97)
>>> "а" == "a"
False

Quand j’étais au lycée, une bonne blague qu’on faisait aux profs était de faire une capture d’écran du bureau de leur ordi, la mettre en fond d’écran, virer les icônes et régler la barrer des tâches pour se cacher automatiquement. Ils pensaient que leur machine était freezée, et un reboot ne changeait rien. Des heures à s’arracher les cheveux.

Avec les identifiants unicodes je pense qu’on peut retrouver cette merveilleuse créativité avec ses collègues.

]]>
http://sametmax.com/assert-%d0%b0-a-lol/feed/ 19
En Python 3, le type bytes est un array d’entiers 16 http://sametmax.com/en-python-3-le-type-bytes-est-un-array-dentiers/ http://sametmax.com/en-python-3-le-type-bytes-est-un-array-dentiers/#comments Thu, 05 Dec 2013 16:00:32 +0000 http://sametmax.com/?p=8160 Le plus gros changement quand on passe de Python 2 à Python 3, c’est la gestion des chaînes de caractères.

Pour rappel :

  • En 2.7, les chaînes sont par défaut des arrays d’octets, et il faut les décoder pour obtenir de l’unicode.
  • En 3, les chaînes sont par défaut de type ‘unicode’, et il faut les encoder pour obtenir de un array d’octets.

Si vous avez besoin d’une mise à jour sur l’encoding en Python, on a un article pour ça.

Comme toute entrée ou sortie est forcément un flux d’octets, mais pas forcément dans le même encodage, Python 2.7 pouvait poser problème pour le débutant qui essayait de comprendre pourquoi son programme plantait, bordel de merde.

La version 3 prend plusieurs mesures pour éviter les bugs vicieux liés à l’encodage de caractères:

  • L’encodage par défaut du code est UTF8.
  • L’encodage par défaut de lecture et d’écriture est UTF8.
  • On ne peut plus mélanger ‘bytes’ et ‘unicode’.
  • Les messages d’erreur expliquent clairement et tôt tout problème.

La plupart du temps, quand on va manipuler du texte, on va donc toujours manipuler de l’unicode, en Python 3. Ce dernier va nous forcer à faire le décodage / encodage au bon moment.

Mais il restera quelques fois le besoin de manipuler du bytes, et ce type a subi un lifting…

La base

Créer un array d’octets (le type bytes‘, en Python 3) demande de préfixer une chaîne avec ‘b’ :

>>> s = b'I am evil, stop laughing!'
>>> type(s)
<class 'bytes'>
>>> print(s)
b'I am evil, stop laughing!'

Première remarque, on ne peut plus utiliser ce type pour afficher quoi que ce soit, puisque l’affichage est une représentation du type (appel à __repr__), et pas du texte mis en forme. Déjà Python vous indique la couleur : si vous voulez manipulez du texte, n’utilisez pas ce type.

Comparez avec le type unicode :

>>> u = s.decode('utf8')
>>> type(u)
<class 'str'>
>>> print(u)
I am evil, stop laughing!

L’affichage marche comme on s’y attend. Bref, vous êtes forcé de toujours rester sur de l’unicode (le type str en Python 3, ce qui porte à confusion) si vous manipulez du texte. Heureusement, c’est quasiment toujours ce que vous aurez.

Par exemple, si vous ouvrez un fichier en Python 3 :

>>> content = open('/etc/fstab').read()
>>> type(content)
<class 'str'>

C’est du texte. A moins de demander qu’il soit ouvert en mode binaire :

>>>> content = open('/etc/fstab', 'rb').read()
>>> type(content)
<class 'bytes'>

Une autre différence MAJEURE, c’est que, si dans Python 2.7, les arrays d’octets pouvaient être manipulés comme un array de lettres :

>>> s = 'I put the goal in golem...' 
>>> s[0] # en Python 2.7
>>> 'I'

En Python 3, les array d’octets sont au mieux manipulables comme un array d’entiers :

>>> s = b'I put the goal in golem...'
>>> s[0] # en Python 3
73

La représentation sous forme de lettre est gardée pour l’initialisation pour des raisons pratiques, mais sous le capot, il se passe ça:

>>> bytes([73, 32, 112, 117, 116, 32, 116, 104, 101, 32, 103, 111, 97, 108, 32, 105, 110, 32, 103, 111, 108, 101, 109, 46, 46, 46])
b'I put the goal in golem...'

D’ailleurs, on ne peut même plus faire d’opérations de formatage avec des octets comme en Python 2.7 :

>>> b"Welcome to the league of %s" % input('')
Draven
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for %: 'bytes' and 'str'

format() ne marche pas non plus. On est assez proche du tableau d’octets en C, sauf qu’en plus, on ne peut pas le modifier :

>>> s = b"My right arm is a lot stronger than my left arm."
>>> s[0] = 1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'bytes' object does not support item assignment

Les arrays d’octets sont donc maintenant essentiellement des outils de communication avec le monde extérieur.

Bytearray

Il existe encore des raisons de manipuler des arrays d’octets : les applications scientifiques. Typiquement, les algos de crypto opérent sur des arrays d’octets.

Pour cette raison, Python 3 vient également avec un nouveau type de base : bytearray, un array d’octets modifiable.

>>> s = bytearray(b"this tasted purple !")
>>> s[2:4] = b'at'
>>> print(s)
bytearray(b'that tasted purple !')

Et on a toutes les opérations de liste dessus, comme append, pop(), etc :

>>> for x in b' ,puY':
...     s.insert(0, x)
... 
>>> print(s)
bytearray(b'Yup, that tasted purple !')

Attention par contre, ces opérations attendent un entier en paramètres et NON un array d’octets.

Et un dernier détail :

>>> isinstance(bytes, bytearray)
False
>>> isinstance(bytearray, bytes)
False

Différence entre string et array d’octets

Il est facile de confondre tout ce merdier.

En Python 2.7, le type str était un array d’octets, et on le manipulait comme une chaîne, d’où la difficulté de transition.

En Python 3, bien qu’on puisse créer un array d’octets avec une syntaxe utilisant des lettres, ils ne sont plus du tout utilisés pour la manipulation de texte. Si vous voulez manipuler du texte qui vient de l’extérieur de votre programme, il faudra toujours le décoder pour obtenir un type str (qui est l’ancien type unicode de Python 2.7).

Le décodage sera fait automatiquement dans la plupart des cas, et plantera si on tombe sur un cas où vous devez le faire à la main et que vous ne le faites pas. Du coup, plus de difficulté à trouver d’où vient ce bug d’encoding, car on a toujours l’erreur à la source.

En ce sens, Python 3 est beaucoup plus clair : les octets d’un côté, le texte de l’autre. Bon, tout ça c’est de la surcouche, au final, tout est octet. Mais on a rarement envie de manipuler un octet directement, sinon on coderait encore en assembleur.

Avec ce système, Python 3 est le langage le plus sain que j’ai pu rencontrer dans sa gestion de l’encodage : il ne cache rien, oblige l’utilisateur à coder avec de bonnes habitudes, facilite le débugage et met sur le devant de la scène la problématique de l’encoding, qui est le plus souvent cachée vite fait sous le tapis.

L’alternative intelligente la plus proche étant celle de node.js, qui interdit tout simplement la plupart des encodings dans son API.

La bonne nouvelle ? 99% du temps, vous n’aurez même pas à vous en soucier, car ASCII est inclus dans UTF8, et ce sont les encodings les plus utilisés. Avec Python 3 forçant UTF8 par défaut partout et des chaînes en unicode dès le départ, il n’y a presque rien à faire. Je doute que la plupart des gens aient même à manipuler le type bytes.

]]>
http://sametmax.com/en-python-3-le-type-bytes-est-un-array-dentiers/feed/ 16
L’encoding en Python, une bonne fois pour toute 89 http://sametmax.com/lencoding-en-python-une-bonne-fois-pour-toute/ http://sametmax.com/lencoding-en-python-une-bonne-fois-pour-toute/#comments Sun, 21 Apr 2013 07:02:43 +0000 http://sametmax.com/?p=5824 J’avais oublié la zik, je rajoute:

Vous avez tous un jour eu l’erreur suivante :

UnicodeDecodeError: 'machine' codec can't decode character 'trucmuche' in position x: ordinal not in range(z)

Et là, pour vous en sortir, vous en avez chié des ronds de pâté.

Le problème vient du fait que la plupart du temps, ignorer l’encoding marche : nous travaillons dans des environnements homogènes et toujours avec des données dans le même format, ou un format plus ou moins compatible.

Mais le texte, c’est compliqué, terriblement compliqué, et le jour où ça se gâte, si vous ne savez pas ce que vous faites, vous ne vous en sortirez pas.

C’est d’autant plus vrai en Python car :

  • Par défaut, Python plante sur les erreurs d’encoding là où d’autres langages (comme le PHP) se débrouillent pour vous sortir un truc (qui ne veut rien dire, qui peut corrompre toute votre base de données, mais qui ne plante pas).
  • Python est utilisé dans des environnements hétérogènes. Quand vous codez en JS sur le navigateur, vous n’avez presque jamais à vous soucier de l’encoding : le browser gère quasiment tout pour vous. En Python dès que vous allez lire un fichier et l’afficher dans un terminal, cela fait potentiellement 3 encoding différents.
  • Python 2.7 a des réglages par défaut très stricts, et pas forcément adaptés à notre informatique moderne (fichier de code en ASCII par exemple).

A la fin de cet article, vous saurez vous sortir de toutes les situations merdiques liées aux encodages.

Règle numéro 1 : Le texte brut n’existe pas.

Quand vous avez du texte quelque part (un terminal, un fichier, une base de données…), il est forcément représenté sous forme de 0 et de 1.

La corrélation entre cette suite de 0 et de 1 et la lettre est faite dans un énorme tableau qui contient toutes les lettres d’un côté, et toutes les combinaisons de 0 et de 1 de l’autre. Il n’y a pas de magie. C’est un énorme tableau stocké quelque part dans votre ordinateur. Si vous n’avez pas ce tableau, vous ne pouvez pas lire du texte. Même le texte le plus simple.

Malheureusement, au début de l’informatique, presque chaque pays a créé son propre tableau, et ces tableaux sont incompatibles entre eux : pour la même combinaison de 0 et de 1, ils donnent un caractère différent voire rien du tout.

La mauvaise nouvelle, c’est qu’ils sont encore utilisés aujourd’hui.

Ces tableaux, c’est ce qu’on appelle les encodings, et il y en a beaucoup. Voici la liste de ceux que Python gère :

>>> import encodings
>>> print ''.join('- ' + e + '\n' for e in sorted(set(encodings.aliases.aliases.values())))
- ascii
- base64_codec
- big5
- big5hkscs
- bz2_codec
- cp037
- cp1026
- cp1140
- cp1250
- cp1251
- cp1252
- cp1253
- cp1254
- cp1255
- cp1256
- cp1257
- cp1258
- cp424
- cp437
- cp500
- cp775
- cp850
- cp852
- cp855
- cp857
- cp858
- cp860
- cp861
- cp862
- cp863
- cp864
- cp865
- cp866
- cp869
- cp932
- cp949
- cp950
- euc_jis_2004
- euc_jisx0213
- euc_jp
- euc_kr
- gb18030
- gb2312
- gbk
- hex_codec
- hp_roman8
- hz
- iso2022_jp
- iso2022_jp_1
- iso2022_jp_2
- iso2022_jp_2004
- iso2022_jp_3
- iso2022_jp_ext
- iso2022_kr
- iso8859_10
- iso8859_11
- iso8859_13
- iso8859_14
- iso8859_15
- iso8859_16
- iso8859_2
- iso8859_3
- iso8859_4
- iso8859_5
- iso8859_6
- iso8859_7
- iso8859_8
- iso8859_9
- johab
- koi8_r
- latin_1
- mac_cyrillic
- mac_greek
- mac_iceland
- mac_latin2
- mac_roman
- mac_turkish
- mbcs
- ptcp154
- quopri_codec
- rot_13
- shift_jis
- shift_jis_2004
- shift_jisx0213
- tactis
- tis_620
- utf_16
- utf_16_be
- utf_16_le
- utf_32
- utf_32_be
- utf_32_le
- utf_7
- utf_8
- uu_codec
- zlib_codec

Et certains ont plusieurs noms (des alias), donc on pourrait en compter plus:

>>> len(encodings.aliases.aliases.keys())
307

Quand vous affichez du texte sur un terminal avec un simple print, votre ordinateur va implicitement chercher le tableau qu’il pense être le plus adapté, et fait la traduction. Même pour le texte le plus simple. Même pour un espace tout seul.

Mais surtout, ça veut dire que votre propre code EST dans un encoding. Et vous DEVEZ savoir lequel.

Règle numéro 2 : utf8 est le langage universel, utilisez-le

Il existe un encoding qui essaye des regrouper toutes les langues du monde, et il s’appelle unicode. Unicode est un tableau gigantesque qui contient des combinaisons de 1 et de 0 d’un côté, et les caractères de toutes la langues possibles de l’autre : chinois, arabe, français, espagnol, russe…

Bon, il ne contient pas encore absolument tout, mais il couvre suffisamment de terrain pour éliminer 99.999999999% des problèmes de communications de texte entre machines dans le monde.

Le défaut d’Unicode est qu’il est plus lent et prend plus de place que d’autres représentations du même texte. Aujourd’hui le téléphone le plus pourri a 10 fois la puissance nécessaire, et ce n’est plus un souci : il peut être utilisé presque partout (sauf peut-être dans l’embarqué drastique) sans même réfléchir à la question. Tous les langages les plus importants, tous les services les plus importants, tous les logiciels les plus importants gèrent unicode.

Il y a plusieurs implémentations concrètes d’unicode, la plus célèbre est “UTF 8″.

Moralité, par défaut, utilisez utf-8.

Une fois, à l’entretien d’embauche, un mec m’avait reproché d’utiliser UTF8 parce que “ça posait des problèmes d’encoding”. Comprenez bien qu’utf-8 ne pose aucun problème d’encoding. Ce sont tous les autres codecs du monde qui posent des problèmes d’encoding. UTF-8 est certainement le seul à justement, ne poser aucun problème.

UTF 8 est le seul encoding vers lequel, aujourd’hui, on puisse convertir vers et depuis (pratiquement) n’importe quel autre codec du monde. C’est un espéranto. C’est une pierre de rosette. C’est au texte ce que l’or est à l’économie.

Si UTF8 vous pose “un problème d’encoding”, c’est que vous ne savez pas dans quel encoding votre texte est actuellement ou comment le convertir. C’est tout.

Il n’y a presque aucune raison de ne pas utiliser UTF8 aujourd’hui (à part sur des vieux systèmes ou des systèmes où les ressources sont tellement limitées que vous n’utiliseriez pas Python de toute façon).

Utilisez utf8. Partout. Tout le temps.

Si vous communiquez avec un système qui ne comprend pas UTF8, convertissez.

Mais gardez votre partie en UTF8.

Règle numéro 3 : il faut maîtriser l’encoding de son code

Le fichier dans lequel vous écrivez votre code est dans un encoding et ce n’est pas lié à votre OS. C’est votre éditeur qui s’en occupe. Apprenez à régler votre éditeur pour qu’il utilise l’encoding que vous voulez.

Et l’encoding que vous voulez est UTF8.

Si vous ne savez pas dans quel encoding est votre code, vous ne pouvez pas manipuler du texte et garantir l’absence de bug.

Vous ne POUVEZ PAS.

Donc réflexe : vous configurez votre éditeur de texte pour sauvegarder tous vos nouveaux fichiers par défaut en UTF8. Maintenant. Tout de suite.

Regardez dans la doc de l’éditeur, dans l’aide ou tapez sur Google, mais faites le.

Puis il faut déclarer cet encoding à la première ligne de chaque fichier de code avec l’expression suivante :

# coding: encoding

Par exemple :

# coding: utf8

C’est une spécificité de Python : si l’encoding du fichier est différent de l’encoding par défaut du langage, il faut le déclarer sinon le programme plantera à la première conversion. En Python 2.7, l’encoding par défaut est ASCII, donc il faut presque toujours le déclarer. En Python 3, l’encoding par défaut est UTF8 et on peut donc l’omettre si on l’utilise. Ce que vous allez faire après la lecture de cet article.

Ensuite, il existe deux types de chaînes de caractères en Python :

  • La chaîne de caractères encodée: type ‘str’ en Python 2.7, ‘byte’ en Python 3.
  • La chaîne de caractères décodée: type ‘unicode’ en Python 2.7, et ‘str’ en python 3 (sic).

Illustration :

$ python2.7
Python 2.7.3 (default, Aug  1 2012, 05:14:39) 
[GCC 4.6.3] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> type('chaine') # bits => encodée
<type 'str'>
>>> type(u'chaine') # unicode => décodée
<type 'unicode'>
$ python3
Python 3.2.3 (default, Oct 19 2012, 20:10:41) 
[GCC 4.6.3] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> type("chaine") # unicode => decodée
<class 'str'>
>>> type(b"chaine") # bits => encodée
 
<class 'bytes'>

Votre but, c’est de n’avoir dans votre code que des chaînes de type ‘unicode’.

En Python 3, c’est automatique. Toutes les chaînes sont de type ‘unicode’ (appelé ‘str’ dans cette version, je sais, je sais, c’est confusionant à mort) par défaut.

En Python 2.7 en revanche, il faut préfixer la chaîne par un u.

Donc, dans votre code, TOUTES vos chaînes doivent être déclarées ainsi :

u"votre chaîne"

Oui, c’est chiant. Mais c’est indispensable. Encore une fois, il n’y a pas d’alternative (dites le avec la voix de Thatcher si ça vous excite).

Si vous voulez, vous pouvez activer le comportement de Python 3 dans Python 2.7 en mettant ceci au début de CHACUN de vos modules :

from __future__ import unicode_literals

Ceci n’affecte que le fichier en cours, jamais les autres modules.

On peut le mettre au démarrage d’iPython également.

Je résume :

  • Réglez votre éditeur sur UTF8.
  • Mettez # coding: utf8 au début de vos modules.
  • Préfixez toutes vos chaînes de u ou faites from __future__ import unicode_literals en début de chaque module.

Si vous ne faites pas cela, votre code marchera. La plupart de temps. Et un jour, dans une situation particulière, il ne marchera plus. Plus du tout.

Oh, et ce n’est pas grave si vous avez d’anciens modules dans d’autres encodings. Tant que vous utilisez des objets ‘unicode’ partout, ils marcheront sans problème ensemble.

Règle numéro 4 : décodez toutes les entrées de votre programme

La partie difficile de ce conseil, c’est de savoir ce qu’est une entrée.

Je vais vous donner une définition simple : tout ce qui ne fait pas partie du code de votre programme et qui est traité dans votre programme est une entrée.

Le texte des fichiers, le nom de ces fichiers, le retour des appels système, le retour d’une ligne de commande parsée, la saisie utilisateur sur un terminal, le retour d’une requête SQL, le téléchargement d’une donnée sur le Web, etc.

Ce sont toutes des entrées.

Comme tous les textes du monde, les entrées sont dans un encoding. Et vous DEVEZ savoir lequel.

Comprenez bien, si vous ne connaissez pas l’encoding de vos entrées, ça marchera la plupart du temps, et un jour, ça va planter.

Il n’y a pas d’alternative (bis).

Or, il n’y a pas de moyen de détecter un encoding de façon fiable.

Donc, soit le fournisseur de la donnée vous donne cette information (settings dans la base de données, doc de votre logiciel, configuration de votre OS, spec du client, coup de fils au fournisseur…), soit vous êtes baisés.

On ne peut pas lire un simple fichier si on ne connait pas son encoding. Point.

Si cela a marché jusqu’ici pour vous, c’est que vous avez eu de la chance : la plupart de vos fichiers étaient dans l’encoding de votre éditeur et de votre système. Tant qu’on travaille sur sa machine, tout va bien.

Si vous lisez une page HTML, l’encoding est souvent déclaré dans la balise META ou dans un header.

Si vous écrivez dans un terminal, l’encoding du terminal est accessible avec sys.(stdin|stdout).encoding.

Si vous manipulez des noms de fichier, on peut récupérer l’encoding du file system en cours avec sys.getfilesystemencoding().

Mais parfois il n’y a pas d’autres moyens d’obtenir cette information que de demander à la personne qui a produit la donnée. Parfois même, l’encoding déclaré est faux.

Dans tous les cas, vous avez besoin de cette information.

Et une fois que vous l’avez, il faut décoder le texte reçu.

La manière la plus simple de faire cela est :

votre_chaine = votre_chaine.decode('nom_du_codec')

Le texte sera de type ‘str’, et decode() retourne (si vous lui fournissez le bon codec ;-)), une version ‘unicode’.

Exemple, obtenir une chaîne ‘unicode’ depuis une chaîne ‘str’ encodée en utf8 :

>>> une_chaine = 'Chaîne' # mon fichier est encodé en UTF8, donc la chaine est en UTF8
>>> type(une_chaine)
<type 'str'>
>>> une_chaine = une_chaine.decode('utf8')
>>> type(une_chaine)
<type 'unicode'>

Donc dès que vous lisez un fichier, récupérez une réponse d’une base de données ou parsez des arguments d’un terminal, appelez decode() sur la chaîne reçue.

Règle numéro 5 : encodez toutes les sorties de votre programme

La partie difficile de ce conseil, c’est de savoir ce qu’est une sortie.

Encore une fois, une définition simple : toute donnée que vous traitez et qui va être lue par autre chose que votre code est une sortie.

Un print dans un terminal est une sortie, un write() dans un fichier est une sortie, un UPDATE en SQL est une sortie, un envoi dans une socket est une sortie, etc.

Le reste du monde ne peut pas lire les objets ‘unicode’ de Python. Si vous écrivez ces objets dans un fichier, un terminal ou dans une base de données, Python va les convertir automatiquement en objet ‘str’, et l’encoding utilisé dépendra du contexte.

Malheureusement, il y a une limite à la capacité de Python à décider du bon encoding.

Donc, tout comme il vous faut connaitre l’encoding d’un texte en entrée, il vous faut connaitre l’encoding attendu par le système avec lequel vous communiquez en sortie : sachez quel est l’encoding du terminal, de votre base de données ou système de fichiers sur lequel vous écrivez.

Si vous ne pouvez pas savoir (page Web, API, etc), utilisez UTF8.

Pour ce faire, il suffit d’appelez encode() sur tout objet de type ‘unicode’ :

une_chaine = une_chaine.encode('nom_du_codec')

Par exemple, pour convertir un objet ‘unicode’ en ‘str’ utf8:

>>> une_chaine = u'Chaîne'
>>> type(une_chaine)
<type 'unicode'>
>>> une_chaine = une_chaine.encode('utf8')
>>> type(une_chaine)
<type 'str'>

Résumé des règles

  1. Le texte brut n’existe pas.
  2. Utilisez UTF8. Maintenant. Partout.
  3. Dans votre code, spécifiez l’encoding du fichier et déclarez vos chaînes comme ‘unicode’.
  4. À l’entrée, connaissez l’encoding de vos données, et décodez avec decode().
  5. A la sortie, encodez dans l’encoding attendu par le système qui va recevoir la données, ou si vous ne pouvez pas savoir, en UTF8, avec encode().

Je sais que ça vous démange de voir un cas concret, alors voici un pseudo programme (téléchargeable ici) :

# coding: utf-8 
 
 
# toutes les chaines sont en unicode (même les docstrings)
from __future__ import unicode_literals
 
"""
    Un script tout pourri qui télécharge plein de page et les sauvegarde
    dans une base de données sqlites.
 
    On écrit dans un fichier de log les opérations effectuées.
"""
 
import re
import urllib2
import sqlite3
 
pages = (
    ('Snippets de Sebsauvage', 'http://www.sebsauvage.net/python/snyppets/'),
    ('Top 50 de bashfr', 'http://danstonchat.com/top50.html'),
)
 
# création de la base de données
conn = sqlite3.connect(r"backup.db")
c = conn.cursor()
 
try:
    c.execute('''
        CREATE TABLE pages (
            id INTEGER PRIMARY KEY,
            nom TEXT,
            html TEXT
        )'''
    )
except sqlite3.OperationalError:
    pass
 
log = open('backup.log', 'wa')
 
for nom, page in pages:
 
    # ceci est une manière très fragile de télécharger et
    # parser du HTML. Utilisez plutôt scrapy et beautifulsoup
    # si vous faites un vrai crawler
    response = urllib2.urlopen(page)
    html = response.read(100000)
 
    # je récupère l'encoding à l'arrache
    encoding = re.findall(r'<meta.*?charset=["\']*(.+?)["\'>]', html, flags=re.I)[0]
 
    # html devient de l'unicode
    html = html.decode(encoding)
 
    # ici je peux faire des traitements divers et varié avec ma chaîne
    # et en fin de programme...
 
    # la lib sqlite convertie par défaut tout objet unicode en UTF8
    # car c'est l'encoding de sqlite par défaut donc passer des chaînes
    # unicode marche, et toutes les chaînes de mon programme sont en unicode
    # grace à mon premier import
    c.execute("""INSERT INTO pages (nom, html) VALUES (?, ?)""", (nom, html))
 
    # j'écris dans mon fichier en UTF8 car c'est ce que je veux pouvoir lire
    # plus tard
    msg = "Page '{}' sauvée\n".format(nom)
    log.write(msg.encode('utf8'))
 
    # notez que si je ne fais pas encode(), soit:
    # - j'ai un objet 'unicode' et ça plante
    # - j'ai un objet 'str' et ça va marcher mais mon fichier contiendra
    #   l'encoding de la chaîne initiale (qui ici serait aussi UTF8, mais
    #   ce n'est pas toujours le cas)
 
conn.commit()
c.close()
 
log.close()

Quelques astuces

Certaines bibliothèques acceptent indifféremment des objets ‘unicode’ et ‘str’ :

>>> from logging import basicConfig, getLogger
>>> basicConfig()
>>> log = getLogger()
>>> log.warn("Détécé")
WARNING:root:Détécé
>>> log.warn(u"Détécé")
WARNING:root:Détécé

Et ce n’est pas forcément une bonne chose car si il y a derrière écriture dans un fichier de log, cela peut poser problème.

D’autres ont besoin qu’on leur précise:

>>> import re
>>> import re
>>> re.search('é', 'télé')
<_sre.SRE_Match object at 0x7fa4d3f77238>
>>> re.search(u'é', u'télé', re.UNICODE)
<_sre.SRE_Match object at 0x7fa4d3f772a0>

Le module re par exemple aura des résultats biaisés sur une chaîne ‘unicode’ si on ne précise pas le flag re.UNICODE.

D’autres n’acceptent pas d’objet ‘str':

>>> import io
>>> >>> io.StringIO(u'é')
<_io.StringIO object at 0x14a96d0>
>>> io.StringIO(u'é'.encode('utf8'))
Traceback (most recent call last):
  File "<ipython-input-5-16988a0d4ac4>", line 1, in <module>
    io.StringIO('é'.encode('utf8'))
TypeError: initial_value must be unicode or None, not str

D’autres encore n’acceptent pas d’objet ‘unicode':

>>> import base64
>>> base64.encodestring('é'.encode('utf8'))
'w6k=\n'
>>> base64.encodestring(u'é')
Traceback (most recent call last):
  File "<ipython-input-3-1714982ca68e>", line 1, in <module>
    base64.encodestring('é')
  File "/usr/lib/python2.7/base64.py", line 315, in encodestring
    pieces.append(binascii.b2a_base64(chunk))
UnicodeEncodeError: 'ascii' codec can't encode character u'\xe9' in position 0: ordinal not in range(128)

Cela peut être pour des raison de performances (certaines opérations sont plus rapides sur un objet ‘str’), ou pour des raisons historiques, d’ignorance ou de paresse.

Vous ne pouvez pas le deviner à l’avance. Souvent c’est marqué dans la doc, sinon il faut tester dans le shell.

Une bibliothèque bien faite demandera de l’unicode et vous retournera de l’unicode, vous libérant l’esprit. Par exemple, requests et l’ORM Django le font, et communiquent avec le reste du monde (en l’occurence le Web et les bases de données) dans le meilleur encoding possible automatiquement de manière transparente. Quand c’est possible bien entendu, parfois il faudra forcer l’encoding car le fournisseur de votre donnée déclare le mauvais. Vous n’y pouvez rien, c’est pareil pour tous les langages du monde.

Enfin il existe des raccourcis pour certaines opérations, utilisez-les autant que possible. Par exemple, pour lire un fichier, au lieu de faire un simple open(), vous pouvez faire :

from codecs import open
 
# open() de codec à exactement la même API, y compris avec "with"
f = open('fichier', encoding='encoding')

Les chaînes récupérées seront automatiquement sous forme d’objet ‘unicode’ au lieu d’objet ‘str’ qu’il vous aurait fallu convertir à la main.

Les outils de la dernière chance

Je vous ai menti, si vous ne connaissez pas l’encoding de vos entrées ou de vos sorties, il vous reste encore quelques options.

Sachez cependant que ces options sont des hacks, des trucs à tenter quand tout ce qui a été décrit plus haut a foiré.

Si vous faites bien votre boulot, ça ne doit pas arriver souvent. Une à deux fois max dans votre année, sauf environnement de travail très très merdique.

D’abord, parlons de l’entrée.

Si vous recevez un objet et qu’il vous est impossible de trouver l’encoding, vous pouvez forcer un décodage imparfait avec decode() en spécifiant le paramètre error.

Il peut prendre les valeurs suivantes :

  • 'strict' : lever une exception en cas d’erreur. C’est le comportement par défaut.
  • 'ignore' : tout caractère qui provoque une erreur est ignoré.
  • 'replace' : tout caractère qui provoque une erreur est remplacé par un point d’interrogation.
>>> print 'Père Noël'.decode('ascii')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'ascii' codec can't decode byte 0xc3 in position 1: ordinal not in range(128)
>>> print 're Noël'.decode('ascii', errors='ignore')
Pre Nol
>>> print 're Noël'.decode('ascii', errors='replace')
P��re No��l

Mozilla vient également à la rescousse avec sa lib chardet qu’il faut donc installer :

pip install chardet

Et qui TENTE (du verbe ‘tenter’, “qui essaye”, et qui donc peut échouer et se tromper) de détecter l’encoding utilisé.

>>> chardet.detect(u'Le Père Noël est une ordure'.encode('utf8'))
{'confidence': 0.8063275188616134, 'encoding': 'ISO-8859-2'}
>>> chardet.detect(u"Le Père Noël est une ordure j'ai dis enculé".encode('utf8'))
{'confidence': 0.87625, 'encoding': 'utf-8'}

Cela marche pas trop mal, mais n’attendez pas de miracles. Plus il y a de texte, plus c’est précis, et plus le paramètre confidence est proche de 1.

Parlons maintenant de la sortie, c’est à dire le cas où le système qui va recevoir vos données est une grosse quiche qui plante dès qu’on lui donne autre chose que de l’ASCII.

Je ne veux balancer personne, mais mon regard se tourne vers l’administration américaine. Subtilement. De manière insistante.

D’abord, encode() accepte les mêmes valeurs pour errors que decode(). Mais en prime, il accepte 'xmlcharrefreplace', très pratique pour les fichiers XML :

>>> u"Et là-bas, tu vois, c'est la coulée du grand bronze".encode('ascii', errors='xmlcharrefreplace')
"Et l&#224;-bas, tu vois, c'est la coul&#233;e du grand bronze"

Enfin, on peut essayer d’obtenir un texte potable en remplaçant les caractères spéciaux par leur équivalent ASCII le plus proche.

Avec l’alphabet latin, c’est très facile :

>>> unicodedata.normalize('NFKD', u"éçûö").encode('ascii', 'ignore')
'ecuo'

Pour des trucs plus avancés comme le cyrilique ou le mandarin, il faut installer unidecode :

pip install unidecode
>>> from unidecode import unidecode
>>> print unidecode(u"En russe, Moscou s'écrit Москва")
En russe, Moscou s'ecrit Moskva
]]>
http://sametmax.com/lencoding-en-python-une-bonne-fois-pour-toute/feed/ 89
s&m.5.lezy…out@spamgourmet.net => erreur 18 http://sametmax.com/sm-5-lezy-outspamgourmet-net-erreur/ http://sametmax.com/sm-5-lezy-outspamgourmet-net-erreur/#comments Thu, 14 Mar 2013 15:40:05 +0000 http://sametmax.com/?p=5419 Bilou, je veux bien te répondre, mais ton adresse me donne une “Delivery Status Notification”

Salut les gars,

J’ai cru comprendre que vous avez des sites de vidéos, vous vous
démerdez comment pour envoyer la vidéo en streaming tous en l’encodant.
Par exemple j’ai des mkv qui doivent être passer en mk4.
Une petite idée ?

Merci d’avance,

Yop,

On encode pas à la volée. On a des workers (powered by kombu) avec des files d’attente qui encodent les videos avec ffmpeg, et qui déclarent quand elles sont prêtes. Derrière on streamp tout ça avec nginx.  Ca demande beaucoup de tests (il n’y a pas de commande magique car ça dépend de ta cible client), et surtout, des disques durs énormes pour stocker tout ça.

Pour ton cas particulier, n’oublie pas qu’il y a 3 choses à prendre en compte:

  • le codec video (h264, mpeg-2, etc)
  • le codec audio (mp3, aac, etc)
  • le conteneur (mkv, avi, mov, etc)

Donc il ne s’agit pas juste de streamer (c’est le plus facile, il suffit de compiler nginx avec l’extension qui va bien et payer un bon serveur), mais bien de trouver la combinaison des 3 qui corresponde à sa cible. Tester si on vise : le HTML5 ? Un lecteur flash ? Un truc lisible sur téléphone mobile (et quels modèles… ?). Quelle définition ? Quelle bande passante ? Quel traffic ?

]]>
http://sametmax.com/sm-5-lezy-outspamgourmet-net-erreur/feed/ 18
Transformer des caractères spéciaux en ASCII 5 http://sametmax.com/transformer-des-caracteres-speciaux-en-ascii/ http://sametmax.com/transformer-des-caracteres-speciaux-en-ascii/#comments Sat, 01 Dec 2012 14:23:37 +0000 http://sametmax.com/?p=3419 Dans beaucoup de cas, plutôt que de se taper la gestion de l’encodage, on préfère tout ramener au plus petit dénominateur commun: l’ASCII. Pas d’accent, pas de problème, comme disait mon grand-père juif. Ça devait être un autre contexte. Mais quand même.

Remplacer les accents par leurs équivalents ASCII les plus proches

Une astuce que j’ai lue pour la toute première fois dans les excellents snippets Python de Sauvage (encore et toujours…).

>>> import unicodedata
>>> chaine_unicode = u"Vous êtes le Père Noël ? s'étonna le petit garçon."
>>> unicodedata.normalize('NFKD', chaine_unicode).encode('ascii', 'ignore')
"Vous etes le Pere Noel ? s'etonna le petit garcon."

Si un caractère n’est pas vraiment remplaçable, on peut choisir plusieurs stratégies. Ignorer le caractère:

>>> unicodedata.normalize('NFKD', u"Bei Jing en chinois s'écrit 北亰").encode('ascii', 'ignore')
"Bei Jing en chinois s'ecrit "

Remplacer le caractère par un point d’interrogation:

>>> unicodedata.normalize('NFKD', u"Bei Jing en chinois s'écrit 北亰").encode('ascii', 'replace')
"Bei Jing en chinois s'e?crit ??"

Notez l’effet sur le caractère accentué…

Ou se vautrer comme une grosse loutre bourrée à la bière:

>>> unicodedata.normalize('NFKD', u"Bei Jing en chinois s'écrit 北亰").encode('ascii')
Traceback (most recent call last):
  File "<ipython-input-21-24181c80fc7f>", line 1, in <module>
    unicodedata.normalize('NFKD', u"Bei Jing en chinois s'écrit 北亰").encode('ascii')
UnicodeEncodeError: 'ascii' codec can't encode character u'\u0301' in position 23: ordinal not in range(128)

Ce qui pour nous a un intérêt limité.

Certains alphabets, comme le hongrois, ont des caractères qui pourraient être extrapolés vers l’ASCII, mais la normalisation NFKD ne le permet pas. On peut alors faire appel à une bibliothèque externe.

Unidecode, la cafetière de l’encoding

Unidecode est une lib pip installable qui permet la translittération de caractères. Ce n’est pas toujours très rigoureux, mais c’est toujours très pratique.

Par exemple, vous voulez créer un site Web de mode à destination du marché Hongrois. Si, si. C’est un marché en pleine expansion, j’vous dis.

Et bien entendu vous avez une rubrique manteau, qui se dit en hongrois, comme nous le savons tous: “kožušček”.

Bon, si vous essayé de mettre ça en slug dans vos URL, ça va être un peu chiant à taper, et malheureusement l’astuce précédente ne va pas vous aider:

In [5]: unicodedata.normalize('NFKD', u"kožušček").encode('ascii', 'replace')
Out[5]: 'koz?us?c?ek'

Partant de là, vous pouvez soit:

  • choisir d’inviter tous vos utilisateurs à lire l’éloge des femmes mûres dans la langue de l’auteur et arrêter d’acheter des manteaux de merde qui sont de toute façon faits en Chine.
  • soit utiliser unidecode.
In [7]: from unidecode import unidecode
In [8]: unidecode(u"kožušček")
Out[8]: 'kozuscek'

Encore plus fastoche, dans le XML et HTML

Dans ces formats à balise, on peut s’affranchir des problème d’encodage en transformant chaque caractère en une entité XML / HTML. On récupère alors du texte ASCII et le client se tapera le boulot d’afficher tous les caractères tordus auxquels l’humanité a pu penser:

In [11]: print u"Le Père Noël m'a offert un kožušček à 北亰".encode('ascii', 'xmlcharrefreplace')
Le P&#232;re No&#235;l m'a offert un ko&#382;u&#353;&#269;ek &#224; &#21271;&#20144;

Et dans tous les autres cas ?

Ben c’est comme d’hab: str.decode() pour tout ce qui rentre pour récupérer une chaîne unicode, et unicode.encode() pour tout ce qui sort pour rebalancer une chaîne de caractères au monde extérieur.

Ça mérite peut être un article d’ailleurs…

P.S: Lazarus vient de me sauver la mise en restaurant cet article suite à un clic malheureux. Vive lazarus.

]]>
http://sametmax.com/transformer-des-caracteres-speciaux-en-ascii/feed/ 5
Quelques erreurs tordues et leurs solutions en Python 16 http://sametmax.com/quelques-erreurs-tordues-et-leurs-solutions-en-python/ http://sametmax.com/quelques-erreurs-tordues-et-leurs-solutions-en-python/#comments Sun, 24 Jun 2012 02:29:56 +0000 http://sametmax.com/?p=995 Quand vous débuggez, rappelez-vous que pdb est votre ami, et qu’il est souvent bon de supprimer tous les fichiers .pyc pour éviter la confusion. Mais parfois l’erreur semble n’avoir aucun sens. Bien que Python soit un langage dont l’une des grandes qualités soit la cohérence, voici une liste d’erreurs et leurs solutions qui ont tendance à énerver (les erreurs hein, pas les solutions).

NameError: name 'x' is not defined

Python plante en annonçant que la variable n’est pas définie. Vous allez à la ligne donnée, et elle est là. Vous vérifiez qu’il n’y a pas de faute de frappe (genre un zéro mélangé avec la lettre O), ni une majuscule ou une minuscule échangée quelque part (Python est sensible à la casse).

Et rien.

Tout est niquel.

Alors pourquoi ça plante bordel de merde ?

Et bien ce message qui n’aide absolument pas peut venir du fait que les closures sont en lecture seule en Python. En résumé, vous avez essayé de faire un truc comme ça:

chose = 'truc'
def fonction():
    chose = 'machin'
    # ou chose += machin ou une variante

La solution est simple: ne modifiez pas chose. Si vous avez besoin de modifier son contenu, utilisez une variable intermédiaire:

chose = 'truc'
def fonction():
    bidule = chose
    bidule += 'machin' # je sais c'est bidon, c'est pour l'exemple

En Python 3.0, vous pouvez utiliser le mot clé nonlocal pour y palier: vous modifierez alors la variable du scope du dessus.

chose = 'truc'
def fonction():
    nonlocal chose
    chose += 'machin' # je sais c'est bidon, c'est pour l'exemple

Évitez d’utiliser global, qui a un fort potentiel d’effet de bord.

ImportError: cannot import name bidule et ImportError: No module named truc

Une fois que vous avez vérifié qu’un module existe bien avec ce nom (regardez de près, parfois c’est subtile), voici 3 possibilités:

Pas de fichier __init__.py

Un dossier n’est pas un module importable si il ne contient pas de fichier __init__.py. Vérifiez qu’il y en a un, et dans le cas contraire, créez en un vide.

Erreur de Python Path

Quand vous faites import bidule, bidule ne peut être importé que si le dossier qui le contient est dans le Python Path. Le Python Path est une variable qui contient une liste de dossiers dans lesquels chercher les modules à importer.

Le dossier courrant, le dossier contenant la bibliothèque standard de Python et le dossier où sont installés les bibliotèques Python de votre système d’exploitation sont automatiquement présents dans le Python Path.

Première chose: assurez-vous d’être à la racine du projet que vous lancez (erreur typique quand on utilise la commande ./manage.py avec Django par exemple).

Deuxième chose: si vous utilisez une bibliothèque qui n’est pas dans le Python Path (ça arrive assez souvent avec les tests unitaires: on éxécute les tests depuis le dossier de test, et le projet est dans un dossier à côté, donc pas dans le Python Path), vous pouvez ajouter manuellement un chemin dans le Python Path.

Pour se faire, avant l’import qui va foirer:

import sys
sys.path.append('/chemin/vers/le/dossier/parent/du/module/a/importer')

On peut tout à fait spécifier un dossier relativement au dossier courant. Il n’est pas rare d’ajouter le dossier parent du dossier courrant au Python Path:

import sys
import os
 
DOSSIER_COURRANT = os.path.dirname(os.path.abspath(__file__))
DOSSIER_PARENT = os.path.dirname(DOSSIER_COURRANT)
sys.path.append(DOSSIER_PARENT)

Par exemple, souvent dans le dossier d’un projet Django je fais un sous-dossier ‘apps’, puis je rajoute ceci au fichier settings.py:

import sys
import os
 
PROJECT_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.append(os.path.join(PROJECT_DIR, 'apps'))

Il y a deux avantages à cela:

  • Mes applications sont regroupées dans un dossier et pas en vrac à la racine du projet, mais je peux quand même les importer en faisant import nom et pas import apps.nom.
  • J’ai maintenant une variable PROJECT_DIR que je peux utiliser partout, notamment pour définir où sont certains dossiers comme le dossiers des fichiers statiques:
STATIC = os.path.join(PROJECT_DIR, 'static')

Imports circulaires

Si vous importez poisson.rouge dans force.py, et force.bleu dans poisson.py, vous aurez aussi ce message d’erreur (qui n’aide pas beaucoup, on est d’accord).

Il n’y a pas vraiment de façon élégante de s’en sortir, c’est une des plus grosses couillasses en Python.

Solution 1: vous refactorez votre code pour avoir bleu et rouge dans un fichier couleur.py, lequel est importé dans poisson.py et force.py. C’est propre, mais parfois ça n’a aucun sens, et parfois ce n’est juste pas possible.
Solution 2: vous mettez l’import dans une fonctions ou une méthode dans un des deux modules (n’importe lequel):

def make_bouillabaisse():
    from poisson import rouge

C’est moche, mais c’est facile. Et je le répète, je n’ai jamais vu quelqu’un en 10 ans de Python proposer une solution élégante à ce problème. C’est un What The Fuck d’or.

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

Arf. L’erreur à la con. Parce que généralement elle vient du fait que l’on ne comprend pas vraiment ce qu’on fait. Or difficile de résoudre un problème quand on ne comprend pas de quoi il est question. Ne vous sentez pas mal, on s’est tous retrouvé comme un demeuré devant un problème d’encodage.

A noter que ce n’est pas une erreur spécifique à Python, mais si vous venez d’un langage comme PHP qui passe silencieusement ce genre d’erreur et affiche en prod des texts illisibles, voire une grosse erreur à l’écran peut surprendre.

Voici des causes très fréquentes:

Encodage du fichier.py

Comme il peut y avoir 1 million de possibilités, forcez vous à:

– TOUJOURS avoir votre éditeur de texte réglé pour utiliser UTF-8. Surtout sur Windows. Si votre chef vous l’interdit parce que “ça pose des problèmes d’encodage” (sic), quittez votre job (meilleur choix) ou faites vous former pour comprendre comment marchent les encodages et travailler dans cet environnement hostile.
– TOUJOURS avoir votre encodage (UTF-8 j’ai dis !) déclaré en haut du fichier.py: # -*- coding: utf-8 -*-

Vérifiez que les textes en entrée sont dans l’encodage prévu

Le contenu des bases de données ne sont parfois pas dans l’encodage déclaré de la table ou de la base. Le contenu d’une page HTML n’est parfois pas encodé dans l’encodage déclaré dans le HEAD. Le contenu d’un fichier n’est parfois pas encodé dans l’encodage par défaut de votre OS.

Il n’y a pas de secret. Pas de moyen infaillible de détection automatique. Il faut vérifier.

Vous confondez encodage et décodage (Python 2.7 et moins)

En Python, on DECODE pour passer d’un texte en encodé (UTF8, ISO-8859, CP1552, etc) et donc de type ‘str’ c’est à dire un flux de bits, à un texte unicode, une représentation interne, un objet non imprimable. Il est recommandé de décoder tout texte venant d’une source extérieur à votre programme, pour tout uniformiser.

A l’inverse, on ENCODE pour passer du type ‘unicode’ à un type ‘str’. Il obligatoire d’encoder un texte pour le communiquer au monde extérieur. Si vous ne le faites pas manuellement, Python le fera automatiquement, en essayant de deviner. Il n’est pas excellent à deviner.

En résumé:

In [7]: texte = open('/etc/fstab').read() # ou un téléchargement, ou une requete SQL...
In [8]: type(texte)
Out[8]: str
In [9]: texte = texte.decode('UTF8')
In [10]: type(texte)
Out[10]: unicode
In [11]: print texte # encode automatiquement le texte car votre terminal ne comprend qu'un text encodé
# /etc/fstab: static file system information.
#
[.............]
In [12]: type(texte.encode('UTF8')) # à faire avant de faire un write
Out[12]: str

Si ça continue de foirer, prenez tous les fichiers de votre application un par un: changez toutes les strings en unicode (les précéder d’un “u”), assurez vous que tout ce qui entre est converti en unicode (unicode() après urllib, open, etc) et tout ce qui sort est converti dans un encodage adapté (souvent UTF8) (encode(‘UTF-8′) avant send(), write() ou print).

Si ça ne marche toujours pas, embauchez un mec comme moi qui est payé cher pour se taper la tête contre les murs à la place des autres.

TypeError: ‘int’ object has no attribute ‘__getitem__’ et autres erreurs sur les tuples

Tuples d’un seul élément

CECI N’EST PAS UN TUPLE: (1)

Ceci est un tuple: (1,)

>>> type((1))
<type 'int'>
>>> type((1,))
<type 'tuple'>
>>> t = (1,)
>>> t[0]
1
>>> t = (1)
>>> t[0]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'int' object has no attribute '__getitem__'

Et il y a plus vicieux:

>>> a = ("12345")
>>> b = ("12345",)
>>> a[0]
'1'
>>> b[0]
'12345'

C’est très dur à débugguer car on dans les deux cas il n’y a pas d’erreur étant donné que c’est une opération tout à fait légitime.

Concaténation automatique

Python vient avec une fonctionnalité qui concatène automatiquement les descriptions littérales de chaînes de caractères:

>>> "Ceci est un"                                  " test"
'Ceci est un test'

C’est très pratique pour les chaînes longues:

>>> print ("Ceci est une chaîne longue "
... "et je peux la diviser sur plusieurs lignes"
... " sans me fouler")
'Ceci est une chaîne longue et je peux la diviser sur plusieurs lignes sans me fouler'

Mais si vous oubliez une virgule dans un tuple (par exemple dans INSTALLED_APPS dans le fichier de settings.py de Django):

>>> REGLES = (
...     "Ne jamais parler du fight club",
...     "Ne jamais croiser les effluves",
...     "Ne jamais appuyer sur le petit bouton rouge" # <===== virgule oubliée !
...     "Ne jamais goûter"
... )
>>> print REGLES[3]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: tuple index out of range
>>> print REGLES[-1]
Ne jamais appuyer sur le petit bouton rougeNe jamais goûter

Le fichier/la liste est vide

On ne peut lire qu’une seule fois les générateurs en Python.

Si vous faites:

toto = (blague.title() for blague in histoire)

ou

toto = open('histoire.txt')

Et ensuite:

for blague in toto:
    print toto
 
len(list(toto))

La dernière ligne ne marchera pas. Toto aura été vidé par la première boucle for. Si vous souhaitez utiliser plusieurs fois le résultat de votre générateur, il faut le transformer en liste:

toto = list(toto)
for blague in toto:
    print toto
 
len(list(toto))

Attention, car vous avez maintenant l’intégralité des données chargées en RAM.

TypeError: ma_function() takes exactly x argument (y given)

Cette erreur est très explicite, et la plupart du temps ne pose aucun problème: vérifiez que vous passez le bon nombre d’arguments à la fonction. Faites particulièrement attention si vous utilisez l’opérateur splat.

Il existe néanmoins un cas particulier un peu taquin:

>>> class Americaine(object):
...     def dernier_mot(mot):
...         print mot
... 
>>> homme_le_plus_classe_du_monde = Americaine()
>>> homme_le_plus_classe_du_monde.dernier_mot("Monde de merde !")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: dernier_mot() takes exactly 1 argument (2 given)

On définie une seul argument (mot) et on en passe un seul ("Monde de merdes !"), alors pourquoi Python n’est pas d’accord ?

C’est parce que l’on déclare une méthode sans self dans la signature. Or Python va passer automatiquement (et de manière invisible) la référence à l’objet courrant en premier argument, du coup la méthode reçoit deux arguments: la référence à homme_le_plus_classe_du_monde et "Monde de merde !". Ca ne marche pas puisque la méthode est déclarée pour n’accepter qu’un seul argument.

Il y a deux solutions. La plus simple, ajoutez self:

>>> class Americaine(object):
...     def dernier_mot(self, mot):
...         print mot
... 
>>> homme_le_plus_classe_du_monde = Americaine()
>>> homme_le_plus_classe_du_monde.dernier_mot("Monde de merde !")
Monde de merde !

Une seconde solution consiste à déclarer une méthode statique. Du coup on a plus besoin d’instance:

>>> class Americaine(object):
...     @staticmethod
...     def dernier_mot(mot):
...         print mot
... 
>>> Americaine.dernier_mot("Monde de merde !")
Monde de merde !

Ma structure de données par défaut n’est pas la bonne

Piège classique en Python, qu’il est important de répéter encore et encore tant il est la source de frustration chez les personnes qui ne le connaissent pas.

>>> from random import choice
>>> def bioman(forces=['rouge', 'bleu', 'vert', 'rose', 'jaune devant, marron derriere'], invite=None):
...     if invite is not None:
...         forces.append(invite)
...     return choice(forces)
... 
>>> bioman()
'rose'
>>> bioman()
'rouge'
>>> bioman(invite='magenta a pois gris')
'vert'
>>> bioman()
'jaune devant, marron derriere'
>>> bioman() # WTF ??????????
'magenta a pois gris'

Dans le dernier appel ‘magenta a pois gris’ est tiré au sort alors qu’on ne l’a pas passé en paramètre. Comment cela est-il possible ?

Cela vient du fait que les paramètres par défaut sont initialisés une seule fois pour tout le programme: dès que le module est chargé.

Si vous utilisez un objet mutable (liste, set, dico) et que vous le modifiez (ici avec append), le prochain appel de la fonction utilisera toujours la référence de cet objet, et donc de sa versio modifiée.

La solution est soit de ne pas utiliser d’objet mutable (tuple, strings, int, etc), soit de ne pas modifier l’objet:

>>> def bioman(forces=('rouge', 'bleu', 'vert', 'rose', 'jaune devant, marron derriere'), invite=None):
...     if invite is not None:
...         forces += (invite,) # ne modifie pas l'ancien objet
...     return choice(forces)

Ou alors (et ceci est souvent utilisé même si c’est moche):

>>> def bioman(forces=None, invite=None):
...     if forces is None:
...        forces = ['rouge', 'bleu', 'vert', 'rose', 'jaune devant, marron derriere']
...     if invite is not None:
...         forces.append(invite)
...     return choice(forces)

Toutes les parties qui sont éxécutées à l’inialisation du code (en opposition à celles qui le sont à l’appel du code) sont concernées par ce problème: les paramètres par défaut, les variables à la racine des modules, les attributs de classe déclarés en dehors d’une méthode, etc.

ItZ naute a beuhgue, Itse fitiure

Néanmoins cela a aussi son utilité. On peut en effet l’utiliser pour partager des états:

class Model(object):
    _pool = {
        'mysql': MySQL().connect('test'),
        'sqlite': Sqlite.open('test.db')
    }
    default_connection = 'mysql'
 
    def query(self, connection=default_connection, *params):
        connection.super_query(*params)

Et vous avez maintenant une classe de modèle qui gère plusieurs connections. Si vous l’étendez, les enfants de la classe et toutes les instances partageront le même objet connection, mais tout le reste sera unique à chacun d’eux. Cela évite un effet de bord du singleton qui oblige à partager un état et une identité. Ici on ne partage que la partie de l’état que l’on souhaite, et pas l’identité.

On gagne sur les deux tableaux: si on update la connection MySQL (par exemple parcequ’on a détecté qu’elle était stale), toutes les instances ont accès à l’objet modifé. Mais si on veut overrider la connection pour une seule classe, on peut le faire sans affecter les autres simplement en remplaçant l’objet à la déclaration de la classe.

On peut aussi utiliser cette fonctionnalité pour créer un cache. On appelle ça “mémoiser”:

def fonction_lente(param1, param2, _cache={}):
    # les tuples peuvent être des clés de dico \o/
    key = (param1, param2)
    if key not in _cache:
        _cache[key] = process_lent(param1, param2)
    return _cache[key]

Tous les résultats sont alors stockés en mémoire vive.

]]>
http://sametmax.com/quelques-erreurs-tordues-et-leurs-solutions-en-python/feed/ 16
Résoudre les problèmes d’encoding avec Python Mechanize 2 http://sametmax.com/resoudre-les-problemes-dencoding-avec-python-mechanize/ http://sametmax.com/resoudre-les-problemes-dencoding-avec-python-mechanize/#comments Wed, 28 Mar 2012 17:02:46 +0000 http://sametmax.com/?p=306 UnicodeDecodeError: 'utf8' codec can't decode byte machin in position truc vous fait faire des cauchemars ? Suivez le guide.]]> Un de ces jours, il faudra qu’on fasse un petit post sur la gestion de l’encoding en Python. En attendant, voici une astuce pour éviter une des erreurs les plus énervantes pour un débutant :

UnicodeDecodeError: 'utf8' codec can't decode byte machin in position truc

De quoi choper un SMIC d’amende pour infraction au code de moralité du langage.

Contrairement à BeautifulSoup, Mechanize n’essaie pas de détecter l’encoding de la page, et vous retourne directement la soupe de bytes qu’il reçoit en réponse. Ça marche, jusqu’à ce que vous tombiez sur un site avec un encodage différent de celui de votre système.

Heureusement, maintenant, la plupart des sites ont la politesse de déclarer leur encoding dans une balise meta, que l’on peut récupérer dans les headers. Du coup, on peut convertir facilement le HTML de la page en unicode :

import re
import mechanize
response = mechanize.Browser().open('http://google.com')
# on chope l'encoding dans les headers
# si ça marche pas on beugle une exception
content_type = response.info().getheader('content-type') 
try:
    encoding = re.search(r'charset=([^\s]+)', content_type).groups()[0]
except AttributeError, IndexError:
    raise ValueError('Unable to guess the page encoding')
# et on decode tout ça
return response.read().decode(encoding)

Par facilité, nous on utilise carrément une sous-classe, vu que 99% du temps on a besoin que du HTML, et qu’on se sert essentiellement de Mechanize comme un gros urllib2 avec plein de paramètres :

import re
import mechanize
class Browser(mechanize.Browser):
 
    def get_content_as_unicode(self, url):
        response = self.open(url)
        content_type = response.info().getheader('content-type')
        try:
            encoding = re.search(r'charset=([^\s]+)', content_type).groups()[0]
        except AttributeError, IndexError:
            raise ValueError('Unable to guess the page encoding')
        return response.read().decode(encoding)

Hop :

>>> type(Browser().get_content_as_unicode('http://google.com'))
<type 'unicode'>

Plus besoin de coquillages maintenant !

]]>
http://sametmax.com/resoudre-les-problemes-dencoding-avec-python-mechanize/feed/ 2