str – Sam & Max http://sametmax.com Du code, du cul Wed, 30 Oct 2019 15:34:04 +0000 en-US hourly 1 https://wordpress.org/?v=4.9.7 32490438 Le type bytes n’est pas du texte http://sametmax.com/le-type-bytes-nest-pas-du-texte/ Fri, 11 Jan 2019 11:30:10 +0000 http://sametmax.com/?p=25125 je craque. Mais je me soigne, globalement j'ai récupéré plein de temps, et ça se voit sur mon quotidien. Et ce craquage, et bien il est cette fois dû à une totale mécompréhension des types de texte en Python 3.]]> J’ai beau essayer très fort de ne pas répondre en ligne, des fois je craque. Mais je me soigne, globalement j’ai récupéré plein de temps, et ça se voit sur mon quotidien.

Et ce craquage, et bien il est cette fois dû à une totale mécompréhension des types de texte en Python 3.

Mais c’est bien normal: Python 3 ne gère pas le texte de la même manière que la grande majorité des langages de programmation, justement à cause de la débâcle qu’on a eue en Python 2. Du coup, de nombreux programmeurs arrivent avec leur expérience d’ailleurs, et tentent de l’appliquer tel un utilisateur de SVN migrant sur git. En surface ça semble coller, malheuseuement à l’usage, ça fait faire des erreurs.

Donc un peu d’explications.

En informatique, tout est une histoire de convention. On dit que tel mot clé a tel effet. Que tel nom suppose telle chose. Que tel code de retour implique telle erreur. Que tel schéma XML représente tel type de document.

Essentiellement, tout cela est arbitraire: des gens ont décidé qu’il en serait ainsi. Impossible de deviner que ce que fait yield ou with si vous n’avez pas d’expérience similaire avant. Impossible de savoir que le code 0 en bash ou 200 en HTTP signifie tout va bien sans qu’on vous transmette l’information, ou faire de nombreux tests.

Quand je dis arbitrairement, évidemment je ne veux pas dire complètement sans raison. Il y a des raisons techniques, politiques, économiques, et parfois esthétiques à ces conventions. Cela n’en retire en rien l’aspect parfaitement artificiel de ces choix.

La convention la plus omniprésente, et pourtant aujourd’hui la plus masquée dans un monde où on utilise massivement des langages de haut niveau comme Javascript, Ruby, PHP et Python, est celle de l’organisation des octets.

Musique !

…je vois même plus le code : tout ce que je vois, c’est des blondes, des brunes, des rousses.

Tout ce qui passe par nos ordinateurs n’est qu’une suite de zéros et de uns, que nous avons groupés par paquets de 8:

Seulement la grande révélation, le “aaaaaaahhhhh okayyyyyyy” qui arrive un jour dans toute vie de dev, c’est que ces paquets de 8 ne veulent rien dire. Rien. C’est nous qui avons décidé, arbitrairement encore une fois, de leur signification.

Vous voyez ce moment dans les films et séries où un personnage arrive à “lire du binaire” ?

Evidement, "c'est une representation binaire ASCII de coordonnées WGS 84 Web Mercator" est plus dur à caser dans un dialogue

Evidement, “c’est une representation binaire ASCII de coordonnées WGS 84 Web Mercator” est plus dur à caser dans un dialogue

C’est de l’enculage de dauphin.

Le binaire n’est pas un langage, pas plus que les lettres “abcdefghijklmnopqrstuvwxyz”. Vous pouvez utiliser ces lettres pour représenter certains mots italiens, français, anglais, un nom propre (sans langue), le label d’un immeuble (sans langue encore) ou un chiffre latin.

Que veut dire “les gosses” ? Pour la même combinaisons de lettres, cela signifie “les enfants” avec la convention française européenne, et “les couilles” avec la convention québéquoise.

Pour le binaire c’est pareil, ce que veut dire un octet dépend de la convention que vous avez choisie.

Par exemple, que signifie cette suite d’octets ?

1100001 1100010 1100011 1100100

Bah rien. Mais on peut lui donner un sens en lui appliquant une convention.

Je peux lui appliquer la convention ASCII, et donc supposer que c’est un texte dans un certain format. Voici ce que ça donne en Python:

     
>>> data = bytearray([0b1100001, 0b1100010, 0b1100011, 0b1100100])     
>>> print(data.decode('ascii'))     
abcd 
Les processeurs modernes ne comprenent pas nativement l'american apparel

Les processeurs modernes ne comprenent pas nativement l’american apparel

Ou je peux lui appliquer une autre convention, et decider de lire ces octets comme si ils étaient le dump d’une structure C. Interprettons en Python ces octets comme un entier non signé en big-endian:

     
>>> data = bytearray([0b1100001, 0b1100010, 0b1100011, 0b1100100])     
>>> import struct     
>>> struct.unpack('>I', data)     
(1633837924,)

Même suite de bits, mais selon la convention choisie, elle veut dire les lettres “abcd” ou le nombre “1633837924”. Et oui, comme il n’y a pas une infinité de combinaisons de 0 et de 1 qui tiennent dans un espace mémoire limité, différentes conventions vont utiliser les mêmes octets mais décider que ça veut dire quelque chose de différent.

En fait, même des conventions pour le même type usage ne veulent pas forcément dire la même chose. Par exemple, prenez l’octet:

11101001

Un octet somme toute sympathique, de bonne famille. Il ne paie pas de mine, mais c’est un membre utile de la société.

Et maintenant, quelqu’un vous donne un indice, il vous dit que cet octet représente… du texte.

Super !

Oui, mais du texte avec quelle convention ? Car les pays du monde entier ont créé leur propre convention pour représenter du texte.

Avec la convention “latin-1”, utilisé par 0.7% de tous les sites Web du monde ?

 
>>> bytearray([0b11101001]).decode('latin-1') 
'é' 

Avec la convention “cp850”, utilisé par la console DOS ?

 
>>> bytearray([0b11101001]).decode('cp850')
'Ú'

Vous voulez rire ? Le premier à remplacé presque partout le second parce qu’ils contiennent les mêmes lettres. Elles ne sont juste pas représentées par la même combinaison d’octets.

Et cet octet, que veut-il dire avec la convention “utf8”, qui est aujourd’hui le standard international recommandé pour représenter du texte ?

 
>>> bytearray([0b11101001]).decode('utf8')
Traceback (most recent call last):
File "", line 1, in 
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe9 in position 0: unexpected end of data 

Il n’a pas de correspondance. Cet octet n’est pas de l’utf8 valide.

Si vous voulez représenter ces lettres en utf8, il faut utiliser une convention différente, en utilisant non pas un seul octet, mais une séquence d’octets:

 
>>> list(map(bin, 'é'.encode('utf8')))
['0b11000011', '0b10101001']
>>> list(map(bin, 'Ú'.encode('utf8')))
['0b11000011', '0b10011010']

Vous pourriez croire que puisque le texte est particulièrement compliqué, c’est normal d’avoir des conventions qui divergent. Mais non, c’est juste la nature des conventions. Puisqu’elles sont arbitraires, l’une n’est pas plus “la vérité” qu’une autre. On retrouve la même chose avec les nombres:

>>> struct.unpack("h", bytearray([0b11101001, 0b11101001]))
(-5655,)
>>> struct.unpack("H", bytearray([0b11101001, 0b11101001])) 
(59881,)

La même suite d’octets peut représenter deux nombres totalement différents, selon que je décide de les lire comme des “short”, ou des “unsigned short”.

Et l’inverse est aussi vrai.

Ben oui, si quelque chose peut être interprété de plusieurs façons, on a aussi le fait que deux représentations différentes peuvent être interprétées … pour aboutir au même résultat.

Par exemple, le nombre des doigts de ma main peut être représenté de plein de façons différentes:

  • décimal: 5
  • français écrit: cinq
  • chiffre latin: V
  • anglais écrit: five
  • espagnol écrit: cinco
  • base deux: 101
  • structure C d’un signed short en little-endian avec Python: bytearray([0b101, 0b0])

Que de manières différentes, pour le même concept ! En plus, il y a confusion possible: V est une lettre également. cinq, five et cinco utilisent le même alphabet, mais pas les mêmes symboles spécifiques, pour représenter la même chose. Et le plus confusionant, 101 est une représentation binaire, mais bytearray([0b101, 0b0]) aussi.

Bref, voilà toute la complexité de la différence entre la donnée, un concept abstrait qui n’existe pas, et sa représentation, une convention humaine concrète qui nous permet de communiquer entre nous.

Donc, pour lire “du binaire”, ou faire n’importe quoi en informatique, il faut connaitre la convention utilisée. Mais pas juste en informatique: pour lire le journal, il faut connaitre la convention des symboles imprimés sur les pages, pour conduire sans se faire tuer, il faut connaitre la convention des panneaux, et pour parler, il faut connaitre la convention de la compression des molécules d’air émise par l’appareil buccal et respiratoire d’un individu qui vient rencontrer votre système auditif.

Vous êtes un être très conventionnel au fond.

Évidemment on trouve la même chose en Python. Par exemple vous pouvez utiliser plusieurs conventions pour demander à Python de créer le même nombre en mémoire:

>>> 245 # base 10
245
>>> 0xF5 # hexadecimal
245
>>> 0b11110101 # binaire
245
>>> 245 == 0xF5 == 0b11110101
True     
>>> type(245)     
     
>>> type(0xF5)     
     
>>> type(0b11110101)     
 

Inversement, "1" et 1 paraissent similaire, mais ils ont différents buts. Le premier est un outil destiné à l’affichage, qui matérialise le caractère représentant le chiffre arabe après le zéro. Il est stocké en interne avec une séquence d’octets similaire à:

>>> bin(ord("1"))
'0b110001'

Tandis que que le second est un outil fait pour faire des calculs avec la plus petite valeur positive entière non nulle. Il est stocké en interne avec une séquence d’octets similaire à:

>>> list(map(bin, struct.pack('l', 1)))
['0b1', '0b0', '0b0', '0b0', '0b0', '0b0', '0b0', '0b0']

Je simplifie bien entendu, en vérité la representation interne des nombres et du texte en Python est plus complexe que cela, et dépend de l’implémentation choisie, du type de processeur, de la taille de la donnée et de votre configuration.

Retour sur le type bytes

J’ai soigneusement évité d’utiliser le type bytes durant cette démonstration, le remplaçant techniquement inutilement (mais pédagogiquement brillamment, car je suis génial) par bytearray.

En effet, toute cette leçon est là pour arriver à la conclusion que bytes ne représente pas du texte, mais si je vous avais montré tout ça avec lui, voilà qui vous aurait interloqué:

     
>>> bytes([0b1100001, 0b1100010, 0b1100011, 0b1100100])     
b'abcd' 

“Heu, mais c’est du texte !” me dirait alors un lecteur ayant diagonalisé l’article.

Mais bien entendu que non.

bytes ne présente pas du texte, c’est une structure de données dont le but est de permettre de manipuler une séquence d’octets ordonnée, et ce manuellement. N’importe laquelle.

Or, il se trouve que beaucoup de langages de programmation représentent le texte comme un array d’octets, et y attachent quelques opérations de manipulation. C’est le cas du C, ou de Python 2 par exemple. Les gens ayant eu cette expérience pensent donc que b'abcd' représente du texte, allant parfois jusqu’à aller lui donner l’appellation de “byte string”.

Il n’existe rien de tel en Python 3.

En Python 3, vous avez deux types pour manipuler des séquences d’octets: bytes et bytearray. Ils sont équivalents, à ceci près que bytes est non mutable (non modifiable) alors que bytearray est mutable (modifiable).

Ces types peuvent contenir n’importe quels octets, et nous avons vu ensemble qu’une même séquence d’octets pouvait être interprétée différemment selon la convention choisie pour la lire. Évidemment il est préférable de la lire avec la même convention qui a été utilisée pour la produire, sans quoi on ne comprendra pas ce que le producteur de la donnée à voulu dire.

Sauf que…

Beaucoup d’outils en informatique utilisent les conventions ASCII et hexadécimale pour symboliser les valeurs des octets. Si vous lancez Wireshark pour regarder les paquets d’un protocole réseau ou si vous ouvrez un PNG avec xxd, on va vous représenter le contenu avec un mélange de ces conventions.

Pour des raisons pratiques, Python fait donc la même chose, et permet ainsi de visualiser (ou produire) le type bytes à l’aide d’une notation ASCII:

    
>>> print(b'abcd'.decode('ascii'))     
abcd     
>>> struct.unpack('>I', b'abcd')     
(1633837924,)

Ou d’une notation héxa (ironiquement, l’héxa est representé par une combinaison de caractères ASCII \o/) si les valeurs ne tiennent pas dans la table ASCII:

     
>>> "é".encode('utf8')  # hexa C3 A9   
b'\xc3\xa9'     
>>> struct.unpack('h', b'\xc3\xa9')    
(-22077,)

Donc bytes, bien qu’il puisse contenir des octets interprétables comme du texte, n’est pas particulièrement fait pour manipuler du texte. Il peut contenir n’importe quoi. Mais pour des raisons pratiques, sa représentation dans le terminal est faite avec une convention familière. Après tout, il faut bien l’écrire en quelque chose pour l’affiquer à l’écran.

Si on veut manipuler du texte en Python 3, il faut utiliser le type str, qui est l’outil spécialisé dans la representation et la manipulation textuelle. Si vous savez qu’un type bytes contient des octets qui representent du texte, alors utilisez la méthode décode() avec la bonne convention (appelée “charset”), pour récupérer un str:

     
>>> print(b'P\xc3\xa8re No\xc3\xabl'.decode('utf8'))
Père Noël 

On a un très bon article sur l’encoding en Python sur le blog, d’ailleurs.

Toute cela n’était bien entendu pas vrai en Python 2. En Python 2, le type str était un array d’octets, rendant tout cela bien confus, et amenant à plein d’erreurs. L’introduction lors de la version 2.0 de l’objet unicode pour pallier le problème, bien que très utile, n’a fait que rajouter à l’incomprehension des nouveaux venus.

Or le monde extérieur, lui, n’a pas d’abstraction pour le texte. Faire des abstractions, c’est le rôle du langage de programmation. Si vous écrivez dans un terminal, ou lisez depuis un terminal, un nom de fichier, le contenu d’une base de données, une requête AJAX, etc., ce sont évidemment des octets qui sont échangés, et il vous faut la bonne convention pour faire partie de la discussion.

Le type bas niveau bytes est un outil qui sert donc à communiquer avec le monde extérieur, tandis que les types haut niveau (str, int, list, etc.) sont des outils qui font l’abstraction de ces conventions, pour vous permettre de manipuler confortablement un concept général (du texte, un nombre, une collection ordonnée) à l’interieur des murs de votre programme.

]]>
25125
En Python 3, le type bytes est un array d’entiers 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)

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

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

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

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

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 "", line 1, in 
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 "", line 1, in 
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/ 19 8160